16.2 缓冲区溢出

Array[index]中index指代数组索引,仔细观察下面的代码,你可能注意到代码没有index是否小于20。如果index大于20?这是C/C++经常被批评的特征。 以下代码可以成功编译可以工作:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int a[20];
  5. int i;
  6. for (i=0; i<20; i++)
  7. a[i]=i*2;
  8. printf ("a[100]=%d", a[100]);
  9. return 0;
  10. };

编译后 (MSVC 2010):

  1. _TEXT SEGMENT
  2. _i$ = -84 ; size = 4
  3. _a$ = -80 ; size = 80
  4. _main PROC
  5. push ebp
  6. mov ebp, esp
  7. sub esp, 84 ; 00000054H
  8. mov DWORD PTR _i$[ebp], 0
  9. jmp SHORT $LN3@main
  10. $LN2@main:
  11. mov eax, DWORD PTR _i$[ebp]
  12. add eax, 1
  13. mov DWORD PTR _i$[ebp], eax
  14. $LN3@main:
  15. cmp DWORD PTR _i$[ebp], 20 ; 00000014H
  16. jge SHORT $LN1@main
  17. mov ecx, DWORD PTR _i$[ebp]
  18. shl ecx, 1
  19. mov edx, DWORD PTR _i$[ebp]
  20. mov DWORD PTR _a$[ebp+edx*4], ecx
  21. jmp SHORT $LN2@main
  22. $LN1@main:
  23. mov eax, DWORD PTR _a$[ebp+400]
  24. push eax
  25. push OFFSET $SG2460
  26. call _printf
  27. add esp, 8
  28. xor eax, eax
  29. mov esp, ebp
  30. pop ebp
  31. ret 0
  32. _main ENDP

运行,我们得到: a[100]=760826203

打印的数字仅仅是距离数组第一个元素400个字节处的堆栈上的数值。 编译器可能会自动添加一些判断数组边界的检测代码(更高级语言3),但是这可能影响运行速度。 我们可以从栈上非法读取数值,是否可以写入数值呢? 下面我们将写入数值:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int a[20];
  5. int i;
  6. for (i=0; i<30; i++)
  7. a[i]=i;
  8. return 0;
  9. };

我们得到:

  1. _TEXT SEGMENT
  2. _i$ = -84 ; size = 4
  3. _a$ = -80 ; size = 80
  4. _main PROC
  5. push ebp
  6. mov ebp, esp
  7. sub esp, 84 ; 00000054H
  8. mov DWORD PTR _i$[ebp], 0
  9. jmp SHORT $LN3@main
  10. $LN2@main:
  11. mov eax, DWORD PTR _i$[ebp]
  12. add eax, 1
  13. mov DWORD PTR _i$[ebp], eax
  14. $LN3@main:
  15. cmp DWORD PTR _i$[ebp], 30 ; 0000001eH
  16. jge SHORT $LN1@main
  17. mov ecx, DWORD PTR _i$[ebp]
  18. mov edx, DWORD PTR _i$[ebp] ; that instruction is obviously redundant
  19. mov DWORD PTR _a$[ebp+ecx*4], edx ; ECX could be used as second operand here instead
  20. jmp SHORT $LN2@main
  21. $LN1@main:
  22. xor eax, eax
  23. mov esp, ebp
  24. pop ebp
  25. ret 0
  26. _main ENDP

编译后运行,程序崩溃。我们找出导致崩溃的地方。 没有使用调试器,而是使用我自己写的小工具tracer足以完成任务。 我们用它看被调试进程崩溃的地方:

  1. generic tracer 0.4 (WIN32), http://conus.info/gt
  2. New process: C:PRJ...1.exe, PID=7988
  3. EXCEPTION_ACCESS_VIOLATION: 0x15 (<symbol (0x15) is in unknown module>), ExceptionInformation
  4. [0]=8
  5. EAX=0x00000000 EBX=0x7EFDE000 ECX=0x0000001D EDX=0x0000001D
  6. ESI=0x00000000 EDI=0x00000000 EBP=0x00000014 ESP=0x0018FF48
  7. EIP=0x00000015
  8. FLAGS=PF ZF IF RF
  9. PID=7988|Process exit, return code -1073740791

我们来看各个寄存器的状态,异常发生在地址0x15。这是个非法地址—至少对win32代码来说是!这种情况并不是我们期望的,我们还可以看到EBP值为0x14,ECX和EDX都为0x1D。 让我们来研究堆栈布局。 代码进入main()后,EBP寄存器的值被保存在栈上。为数组和变量i一共分配84字节的栈空间,即(20+1)*sizeof(int)。此时ESP指向_i变量,之后执行push something,something将紧挨着_i。 此时main()函数内栈布局为:

  1. ESP
  2. ESP+4
  3. ESP+84
  4. ESP+88
  5. 4 bytes for i
  6. 80 bytes for a[20] array
  7. saved EBP value
  8. returning address

指令a[19]=something写入最后的int到数组边界(这里是数组边界!)。 指令a[20]=something,something将覆盖栈上保存的EBP值。 请注意崩溃时寄存器的状态。在此例中,数字20被写入第20个元素,即原来存放EBP值得地方被写入了20(20的16进制表示是0x14)。然后RET指令被执行,相当于执行POP EIP指令。 RET指令从堆栈中取出返回地址(该地址为CRT内部调用main()的地址),返回地址处被存储了21(0x15)。CPU执行地址0x15的代码,异常被抛出。 Welcome!这被称为缓冲区溢出4。 使用字符数组代替int数组,创建一个较长的字符串,把字符串传递给程序,函数没有检测字符串长度,把字符复制到较短的缓冲区,你能够找到找到程序必须跳转的地址。事实上,找出它们并不是很简单。 我们来看GCC 4.4.1编译后的同类代码:

  1. public main
  2. main proc near
  3. a = dword ptr -54h
  4. i = dword ptr -4
  5. push ebp
  6. mov ebp, esp
  7. sub esp, 60h
  8. mov [ebp+i], 0
  9. jmp short loc_80483D1
  10. loc_80483C3:
  11. mov eax, [ebp+i]
  12. mov edx, [ebp+i]
  13. mov [ebp+eax*4+a], edx
  14. add [ebp+i], 1
  15. loc_80483D1:
  16. cmp [ebp+i], 1Dh
  17. jle short loc_80483C3
  18. mov eax, 0
  19. leave
  20. retn
  21. main endp

在linux下运行将产生:段错误。使用GDB调试:

  1. (gdb) r
  2. Starting program: /home/dennis/RE/1
  3. Program received signal SIGSEGV, Segmentation fault.
  4. 0x00000016 in ?? ()
  5. (gdb) info registers
  6. eax 0x0 0
  7. ecx 0xd2f96388 -755407992
  8. edx 0x1d 29
  9. ebx 0x26eff4 2551796
  10. esp 0xbffff4b0 0xbffff4b0
  11. ebp 0x15 0x15
  12. esi 0x0 0
  13. edi 0x0 0
  14. eip 0x16 0x16
  15. eflags 0x10202 [ IF RF ]
  16. cs 0x73 115
  17. ss 0x7b 123
  18. ds 0x7b 123
  19. es 0x7b 123
  20. fs 0x0 0
  21. gs 0x33 51
  22. (gdb)

寄存器的值与win32例子略微不同,因为堆栈布局也不太一样。