现在的位置: 首页 > 综合 > 正文

shellcode之一:栈溢出

2013年04月22日 ⁄ 综合 ⁄ 共 6147字 ⁄ 字号 评论关闭

转载:http://blog.csdn.net/azloong/article/details/6158401

前言:现在我是嵌入式软件开发者,大学本科读的是电子信息专业,正常的来说不会与入侵、漏洞利用什么的打交道。只是大一时心血来潮用工具进入了另外一台电脑。其实这些也无关重要,重要的是我从那台电脑上down了第一部A片。不出意外的话,那台电脑应该属于女生的,因为那个IP段是女生楼那边的。后来我就可以很笃定的跟别人说:女生也是要看A片的。后来又用工具入侵了更多的局域网电脑,甚至学校一些社团的服务器。再后来,渐感觉没什么意思,而迷上了动漫和War3,随后的几年大学生活都在动画和游戏中挣扎。毕业后的前一两年,工作还不算忙,失去了游戏的激情,老是想找点别的东西消遣时间。于是就写写与工作无关的程序,对shellcode的研究也在这里面。现在是不可能提得起心情去搞入侵了,顶多研究一下底层原理,满足自己的好奇心。这些文章是很早之前写的,觉得有点价值就把它贴上来了。



声明:主要内容来自《The Shellcoder's Handbook》,摘录重点作为笔记并加上个人的一些理解,如有错,请务必指出。
 
在C、C++语言中,没有考虑检查缓冲区的内在边界,所以使栈溢出成为可能。用户故意提交超出缓冲区范围的数据。这种情形可导致不同的后果,包括程序崩溃或强制令程序执行用户提交的指令。
 
ESP:栈顶寄存器。注意:POP只改变ESP的值,而不改写或删除栈上的数据,它只是把栈上的数据复制到操作对象里。
EBP:栈底寄存器。通常以它为基址来计算其他的地址。也称为“帧指针”。
EIP:扩展指令指针。EIP中保存着下一条即将执行的机器指令的地址。在控制程序的执行流程中,是否可以访问和改变保存在EIP中的地址将是整个问题的关键。


函数调用与栈

系统首先会执行main里的指令,碰到函数调用时:
1、把调用函数func的参数压入栈;
2、把函数的返回地址(即RET,RET里保存的是调用函数时的指令指针EIP的地址)压入栈;
3、调用函数。
 
而系统在执行调用函数func指令前首先执行proglog。proglog在栈中存储一些值,使得系统更好地执行函数:
1、为了使函数可以引用栈上的数据,必须改变EBP的值,把当前EBP的值压入栈;函数执行结束后,为了计算main里的地址,我们要用到原先的EBP的值,之前有提及通常以EBP为基址来计算其他的地址;
2、一旦EBP的值被压入栈,proglog就把当前栈指针ESP复制到EBP;
3、接着,proglog计算func的局部变量所需的地址空间和栈上的保留空间,然后从ESP减去变量的大小,为程序保留必要的空间;
4、最后proglog把func的局部变量压入栈。
关于“为了使函数可以引用栈上的数据,必须改变EBP的值”,是不是指以EBP为基址来计算栈上的局部变量所在的地址?当前EBP位于局部变量的地址空间之处。

以上所说如图:

[plain] view
plain
copy

  1. +---------------+ 低内存地址,栈顶  
  2. |               |  
  3. +---------------+  
  4. |  局部变量     |  
  5. +---------------+  
  6. |  EBP          |  
  7. +---------------+  
  8. |  RET          |  
  9. +---------------+  
  10. |  参数1        |  
  11. +---------------+  
  12. |  参数2        |  
  13. +---------------+  
  14. |               |   
  15. +---------------+ 高内存地址,栈底  

这里没有实例,不够直观,建议看原书P12反汇编后的代码。值得一提的是:函数调用时,call指令会把RET(EIP)压入栈,之后才把执行控制权交给func函数。

栈上的缓冲区溢出


一个例子程序:
  1. //cc -mpreferred-stack-boundary=2 -ggdb overflow.c -o overflow    
  2. #include <stdio.h>    
  3.   
  4. void func()    
  5. {    
  6.     char arr[30];    
  7.     gets(arr);    
  8.     printf("%S/n", arr);    
  9. }    
  10.   
  11. int main()    
  12. {    
  13.     func();    
  14.     return 0;    
  15. }    

数组arr的长度为30,当输入的字符串“AAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDDDDDDDDD”时(这里有30个'A'和10个'D'),可引发segment fault。下面显示了在arr被溢出后栈的情形:

[plain] view
plain
copy

  1. +---------------+ 低内存地址,栈顶  
  2. |               |  
  3. +---------------+  
  4. |  AAAAA...ADD  | 数组(30个字符+2个填充字符)  
  5. +---------------+  
  6. |  DDDD         | EBP  
  7. +---------------+  
  8. |  DDDD         | RET  
  9. +---------------+  
  10. |               |   
  11. +---------------+ 高内存地址,栈底  

用32B填充数组并继续执行代码。我们改写了保存EBP的地址,它现在是包括DDDD的十六进制形式的双字节。更重要的是,我们用另一组DDDD的双字节改写了RET(原本RET是保存的返回地址,这个地址指向func()之后的return 0;指令)。当func函数退出时,它将读出存储在RET里的值--现在是0x44444444(DDDD的十六进制形式),并试图跳到这个地址。但是这个地址不是一个有效的地址或者位于受保护的地址空间,故程序将由于段故障而终止。
这里简单描述了大体过程,建议按照书上步骤用GDB跟踪一下整个过程,特别留意func栈上数据的变化状况。

控制EIP

现在输入数据成功溢出了缓冲区,并改写了EBP和RET的内容,因此,溢出的数据被加载到EIP,当然在上面的情况下进程会崩溃。我们应控制程序的执行流程,或控制加载到EIP的数据。用精心选择的地址代替D。这些数据将写入缓冲区并改写保存的EBP和RET。当系统从栈上取出RET的值并放入EIP时,这个地址指向的指令将被执行。
这里简单描述一下概念,还得进行一些练习以有个直观印象。

在overflow.c的例子中,正常的流程是只打印一次输入的数据。如果我们要令它打印两次,即是说令程序调用两次func,我们首先用GDB反汇编main,找到call <func>这句指令的地址如0x080483a5,接着我们设法让func栈上保存RET的地方改写为0x080483a5,这样func返回后,EIP从func栈上取出RET的值(当然这个值已经被改写),这个地址指向的call <func>会再次被执行,将进行第二次的打印。
$printf "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDDD/xa5/x83/x04/x08" | ./overflow
注:这里我的命令和书上的描述不同,要根据自己的实际情况来修改。
[plain] view
plain
copy

  1. sep@sep:~/project/shellcode$     
  2. sep@sep:~/project/shellcode$ gdb ./overflow    
  3. GNU gdb 6.4.90-debian    
  4. Copyright (C) 2006 Free Software Foundation, Inc.    
  5. GDB is free software, covered by the GNU General Public License, and you are    
  6. welcome to change it and/or distribute copies of it under certain conditions.    
  7. Type "show copying" to see the conditions.    
  8. There is absolutely no warranty for GDB. Type "show warranty" for details.    
  9. This GDB was configured as "i486-linux-gnu"...Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".    
  10.   
  11. (gdb) disas main                           ;反汇编main    
  12. Dump of assembler code for function main:    
  13. 0x080483a2 <main+0>: push %ebp    
  14. 0x080483a3 <main+1>: mov %esp,%ebp    
  15. 0x080483a5 <main+3>: call 0x8048384 <func> ;找到call <func>的指令地址    
  16. 0x080483aa <main+8>: mov $0x0,%eax         ;记住这个地址,将保存到func栈上的RET    
  17. 0x080483af <main+13>: pop %ebp    
  18. 0x080483b0 <main+14>: ret     
  19. End of assembler dump.    
  20. (gdb) disas func                           ;反汇编func    
  21. Dump of assembler code for function func:    
  22. 0x08048384 <func+0>: push %ebp    
  23. 0x08048385 <func+1>: mov %esp,%ebp    
  24. 0x08048387 <func+3>: sub $0x24,%esp    
  25. 0x0804838a <func+6>: lea 0xffffffe2(%ebp),%eax    
  26. 0x0804838d <func+9>: mov %eax,(%esp)    
  27. 0x08048390 <func+12>: call 0x80482a0 <gets@plt> ;找到call <gets>指令地址    
  28. 0x08048395 <func+17>: lea 0xffffffe2(%ebp),%eax    
  29. 0x08048398 <func+20>: mov %eax,(%esp)    
  30. 0x0804839b <func+23>: call 0x80482b0 <puts@plt> ;找到call <printf>指令地址    
  31. 0x080483a0 <func+28>: leave     
  32. 0x080483a1 <func+29>: ret     
  33. End of assembler dump.    
  34. (gdb) break *0x08048390                            ;设置断点-gets    
  35. Breakpoint 1 at 0x8048390: file overflow.c, line 7.    
  36. (gdb) break *0x0804839b                            ;设置断点-printf    
  37. Breakpoint 2 at 0x804839b: file overflow.c, line 8.    
  38. (gdb) run    
  39. Starting program: /home/sep/project/shellcode/overflow     
  40. Failed to read a valid object file image from memory.    
  41.   
  42. Breakpoint 1, 0x08048390 in func () at overflow.c:7    
  43. 7 gets(arr);    
  44. (gdb) x/20x $esp  ;打印未输入字符串时的栈上的内容,此时留意0x080483aa为RET,即func返回后下一条执行指令的地址,我们要修改的就是这个东东。    
  45. 0xbfe7109c: 0xbfe710a2 0x00000001 0xbfe71144 0xbfe710c8    
  46. 0xbfe710ac: 0x08048429 0xb7e46c8c 0xb7f68ff4 0x00000000    
  47. 0xbfe710bc: 0xb7f68ff4 0xbfe710c8 0x080483aa 0xbfe71118    
  48. 0xbfe710cc: 0xb7e50ea8 0x00000001 0xbfe71144 0xbfe7114c    
  49. 0xbfe710dc: 0x00000000 0xb7f68ff4 0x00000000 0xb7f8ecc0    
  50. (gdb) c    
  51. Continuing.    
  52.   
  53. ;输入字符串。这里共30个字符,用意看栈上内容变化。    
  54. AAAABBBBCCCCDDDDEEEEFFFFGGGGHH     
  55.   
  56. Breakpoint 2, 0x0804839b in func () at overflow.c:8    
  57. 8 printf("%s/n", arr);    
  58. (gdb) x/20x $esp ;输入字符串后,观看栈上内容。    
  59. 0xbfe7109c: 0xbfe710a2 0x41410001 0x42424141 0x43434242    
  60. 0xbfe710ac: 0x44444343 0x45454444 0x46464545 0x47474646    
  61. 0xbfe710bc: 0x48484747 0xbfe71000 0x080483aa 0xbfe71118    
  62. 0xbfe710cc: 0xb7e50ea8 0x00000001 0xbfe71144 0xbfe7114c    
  63. 0xbfe710dc: 0x00000000 0xb7f68ff4 0x00000000 0xb7f8ecc0    
  64. (gdb) c    
  65. Continuing.    
  66. AAAABBBBCCCCDDDDEEEEFFFFGGGGHH    
  67.   
  68. Program exited normally.    
  69. (gdb) q    
  70.   
  71. ;好了,我们已经知道栈上数据的存放位置,如何改写栈上的RET令程序打印两次呢?    
  72.   

抱歉!评论已关闭.