16.3 防止缓冲区溢出的方法

下面一些方法防止缓冲区溢出。MSVC使用以下编译选项:

  1. /RTCs Stack Frame runtime checking
  2. /GZ Enable stack checks (/RTCs)

一种方法是在函数局部变量和序言之间写入随机值。在函数退出之前检查该值。如果该值不一致则挂起而不执行RET。进程将被挂起。 该随机值有时被称为“探测值”。 如果使用MSVC编译简单的例子(16.1),使用RTC1和RTCs选项,将能看到函数调用@_RTC_CheckStackVars@8函数来检测“探测值“。

我们来看GCC如何处理这些。我们使用alloca()(4.2.4)例子:

  1. #include <malloc.h>
  2. #include <stdio.h>
  3. void f()
  4. {
  5. char *buf=(char*)alloca (600);
  6. _snprintf (buf, 600, "hi! %d, %d, %d", 1, 2, 3);
  7. puts (buf);
  8. };

我们不使用任何附加编译选项,只使用默认选项,GCC 4.7.3将插入“探测“检测代码:

Listing 16.3: GCC 4.7.3

  1. .LC0:
  2. .string "hi! %d, %d, %d
  3. "
  4. f:
  5. push ebp
  6. mov ebp, esp
  7. push ebx
  8. sub esp, 676
  9. lea ebx, [esp+39]
  10. and ebx, -16
  11. mov DWORD PTR [esp+20], 3
  12. mov DWORD PTR [esp+16], 2
  13. mov DWORD PTR [esp+12], 1
  14. mov DWORD PTR [esp+8], OFFSET FLAT:.LC0 ; "hi! %d, %d, %d
  15. "
  16. mov DWORD PTR [esp+4], 600
  17. mov DWORD PTR [esp], ebx
  18. mov eax, DWORD PTR gs:20 ; canary
  19. mov DWORD PTR [ebp-12], eax
  20. xor eax, eax
  21. call _snprintf
  22. mov DWORD PTR [esp], ebx
  23. call puts
  24. mov eax, DWORD PTR [ebp-12]
  25. xor eax, DWORD PTR gs:20 ; canary
  26. jne .L5
  27. mov ebx, DWORD PTR [ebp-4]
  28. leave
  29. ret
  30. .L5:
  31. call __stack_chk_fail

随机值存在于gs:20。它被写入到堆栈,在函数的结尾与gs:20的探测值对比,如果不一致,__stack_chk_fail函数将被调用,控制台(Ubuntu 13.04 x86)将输出以下信息:

  1. *** buffer overflow detected ***: ./2_1 terminated
  2. ======= Backtrace: =========
  3. /lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x63)[0xb7699bc3]
  4. /lib/i386-linux-gnu/libc.so.6(+0x10593a)[0xb769893a]
  5. /lib/i386-linux-gnu/libc.so.6(+0x105008)[0xb7698008]
  6. /lib/i386-linux-gnu/libc.so.6(_IO_default_xsputn+0x8c)[0xb7606e5c]
  7. /lib/i386-linux-gnu/libc.so.6(_IO_vfprintf+0x165)[0xb75d7a45]
  8. /lib/i386-linux-gnu/libc.so.6(__vsprintf_chk+0xc9)[0xb76980d9]
  9. /lib/i386-linux-gnu/libc.so.6(__sprintf_chk+0x2f)[0xb7697fef]
  10. ./2_1[0x8048404]
  11. /lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf5)[0xb75ac935]
  12. ======= Memory map: ========
  13. 08048000-08049000 r-xp 00000000 08:01 2097586 /home/dennis/2_1
  14. 08049000-0804a000 r--p 00000000 08:01 2097586 /home/dennis/2_1
  15. 0804a000-0804b000 rw-p 00001000 08:01 2097586 /home/dennis/2_1
  16. 094d1000-094f2000 rw-p 00000000 00:00 0 [heap]
  17. b7560000-b757b000 r-xp 00000000 08:01 1048602 /lib/i386-linux-gnu/libgcc_s.so.1
  18. b757b000-b757c000 r--p 0001a000 08:01 1048602 /lib/i386-linux-gnu/libgcc_s.so.1
  19. b757c000-b757d000 rw-p 0001b000 08:01 1048602 /lib/i386-linux-gnu/libgcc_s.so.1
  20. b7592000-b7593000 rw-p 00000000 00:00 0
  21. b7593000-b7740000 r-xp 00000000 08:01 1050781 /lib/i386-linux-gnu/libc-2.17.so
  22. b7740000-b7742000 r--p 001ad000 08:01 1050781 /lib/i386-linux-gnu/libc-2.17.so
  23. b7742000-b7743000 rw-p 001af000 08:01 1050781 /lib/i386-linux-gnu/libc-2.17.so
  24. b7743000-b7746000 rw-p 00000000 00:00 0
  25. b775a000-b775d000 rw-p 00000000 00:00 0
  26. b775d000-b775e000 r-xp 00000000 00:00 0 [vdso]
  27. b775e000-b777e000 r-xp 00000000 08:01 1050794 /lib/i386-linux-gnu/ld-2.17.so
  28. b777e000-b777f000 r--p 0001f000 08:01 1050794 /lib/i386-linux-gnu/ld-2.17.so
  29. b777f000-b7780000 rw-p 00020000 08:01 1050794 /lib/i386-linux-gnu/ld-2.17.so
  30. bff35000-bff56000 rw-p 00000000 00:00 0 [stack]
  31. Aborted (core dumped)

gs被叫做段寄存器,这些寄存器被广泛用在MS-DOS和扩展DOS时代。现在的作用和以前不同。简要的说,gs寄存器在linux下一直指向TLS(48)–存储线程的各种信息(win32环境下,fs寄存器同样的作用,指向TIB8 9)。 更多信息请参考linux源码arch/x86/include/asm/stackprotector.h(至少3.11版本)。

16.3.1 Optimizing Xcode (LLVM) + thumb-2 mode

我们回头看简单的数组例子(16.1)。我们来看LLVM如何检查“探测值“。

  1. _main
  2. var_64 = -0x64
  3. var_60 = -0x60
  4. var_5C = -0x5C
  5. var_58 = -0x58
  6. var_54 = -0x54
  7. var_50 = -0x50
  8. var_4C = -0x4C
  9. var_48 = -0x48
  10. var_44 = -0x44
  11. var_40 = -0x40
  12. var_3C = -0x3C
  13. var_38 = -0x38
  14. var_34 = -0x34
  15. var_30 = -0x30
  16. var_2C = -0x2C
  17. var_28 = -0x28
  18. var_24 = -0x24
  19. var_20 = -0x20
  20. var_1C = -0x1C
  21. var_18 = -0x18
  22. canary = -0x14
  23. var_10 = -0x10
  24. PUSH {R4-R7,LR}
  25. ADD R7, SP, #0xC
  26. STR.W R8, [SP,#0xC+var_10]!
  27. SUB SP, SP, #0x54
  28. MOVW R0, #aObjc_methtype ; "objc_methtype"
  29. MOVS R2, #0
  30. MOVT.W R0, #0
  31. MOVS R5, #0
  32. ADD R0, PC
  33. LDR.W R8, [R0]
  34. LDR.W R0, [R8]
  35. STR R0, [SP,#0x64+canary]
  36. MOVS R0, #2
  37. STR R2, [SP,#0x64+var_64]
  38. STR R0, [SP,#0x64+var_60]
  39. MOVS R0, #4
  40. STR R0, [SP,#0x64+var_5C]
  41. MOVS R0, #6
  42. STR R0, [SP,#0x64+var_58]
  43. MOVS R0, #8
  44. STR R0, [SP,#0x64+var_54]
  45. MOVS R0, #0xA
  46. STR R0, [SP,#0x64+var_50]
  47. MOVS R0, #0xC
  48. STR R0, [SP,#0x64+var_4C]
  49. MOVS R0, #0xE
  50. STR R0, [SP,#0x64+var_48]
  51. MOVS R0, #0x10
  52. STR R0, [SP,#0x64+var_44]
  53. MOVS R0, #0x12
  54. STR R0, [SP,#0x64+var_40]
  55. MOVS R0, #0x14
  56. STR R0, [SP,#0x64+var_3C]
  57. MOVS R0, #0x16
  58. STR R0, [SP,#0x64+var_38]
  59. MOVS R0, #0x18
  60. STR R0, [SP,#0x64+var_34]
  61. MOVS R0, #0x1A
  62. STR R0, [SP,#0x64+var_30]
  63. MOVS R0, #0x1C
  64. STR R0, [SP,#0x64+var_2C]
  65. MOVS R0, #0x1E
  66. STR R0, [SP,#0x64+var_28]
  67. MOVS R0, #0x20
  68. STR R0, [SP,#0x64+var_24]
  69. MOVS R0, #0x22
  70. STR R0, [SP,#0x64+var_20]
  71. MOVS R0, #0x24
  72. STR R0, [SP,#0x64+var_1C]
  73. MOVS R0, #0x26
  74. STR R0, [SP,#0x64+var_18]
  75. MOV R4, 0xFDA ; "a[%d]=%d
  76. "
  77. MOV R0, SP
  78. ADDS R6, R0, #4
  79. ADD R4, PC
  80. B loc_2F1C
  81. ; second loop begin
  82. loc_2F14
  83. ADDS R0, R5, #1
  84. LDR.W R2, [R6,R5,LSL#2]
  85. MOV R5, R0
  86. loc_2F1C
  87. MOV R0, R4
  88. MOV R1, R5
  89. BLX _printf
  90. CMP R5, #0x13
  91. BNE loc_2F14
  92. LDR.W R0, [R8]
  93. LDR R1, [SP,#0x64+canary]
  94. CMP R0, R1
  95. ITTTT EQ ; canary still correct?
  96. MOVEQ R0, #0
  97. ADDEQ SP, SP, #0x54
  98. LDREQ.W R8, [SP+0x64+var_64],#4
  99. POPEQ {R4-R7,PC}
  100. BLX ___stack_chk_fail

首先可以看到,LLVM循环展开写入数组,LLVM认为先计算出数组元素的值速度更快。 在函数的结尾我们能看到“探测值“的检测—局部存储的值与R8指向的标准值对比。如果相等4指令块将通过”ITTTT EQ“触发,R0写入0,函数退出。如果不相等,指令块将不会被触发,跳向___stack_chk_fail函数,结束进程。

16.4 One more word about arrays

现在我们来理解下面的C/C++代码为什么不能正常使用10:

  1. void f(int size)
  2. {
  3. int a[size];
  4. ...
  5. };

这是因为在编译阶段编译器不知道数组的具体大小无论是在堆栈或者数据段,无法分配具体空间。 如果你需要任意大小的数组,应该通过malloc()分配空间,然后访问内存块来访问你需要的类型数组。或者使用C99标准[15,6.7.5/2],但它内部看起来更像alloca()(4.2.4)。