4.4 GCC 堆栈保护技术

    启用 CANARY 后,函数开始执行的时候会先往栈里插入 canary 信息,当函数返回时验证插入的 canary 是否被修改,如果是,则说明发生了栈溢出,程序停止运行。

    下面是一个例子:

    我们先开启 CANARY,来看看执行的结果:

    1. $ python -c 'print("A"*20)' | ./f.out
    2. *** stack smashing detected ***: ./f.out terminated
    3. Segmentation fault (core dumped)

    接下来关闭 CANARY:

    1. $ gcc -m32 -fno-stack-protector canary.c -o fno.out
    2. $ python -c 'print("A"*20)' | ./fno.out
    3. Segmentation fault (core dumped)

    可以看到当开启 CANARY 的时候,提示检测到栈溢出和段错误,而关闭的时候,只有提示段错误。

    下面对比一下反汇编代码上的差异:

    开启 CANARY 时:

    1. gdb-peda$ disassemble main
    2. Dump of assembler code for function main:
    3. 0x000005ad <+0>: lea ecx,[esp+0x4]
    4. 0x000005b1 <+4>: and esp,0xfffffff0
    5. 0x000005b4 <+7>: push DWORD PTR [ecx-0x4]
    6. 0x000005b7 <+10>: push ebp
    7. 0x000005b8 <+11>: mov ebp,esp
    8. 0x000005ba <+13>: push ebx
    9. 0x000005bb <+14>: push ecx
    10. 0x000005bc <+15>: sub esp,0x20
    11. 0x000005bf <+18>: call 0x611 <__x86.get_pc_thunk.ax>
    12. 0x000005c4 <+23>: add eax,0x1a3c
    13. 0x000005c9 <+28>: mov edx,ecx
    14. 0x000005cb <+30>: mov edx,DWORD PTR [edx+0x4]
    15. 0x000005ce <+33>: mov DWORD PTR [ebp-0x1c],edx
    16. 0x000005d1 <+36>: mov ecx,DWORD PTR gs:0x14 ; canary 值存入 ecx
    17. 0x000005d8 <+43>: mov DWORD PTR [ebp-0xc],ecx ; 在栈 ebp-0xc 处插入 canary
    18. 0x000005db <+46>: xor ecx,ecx
    19. 0x000005dd <+48>: sub esp,0x8
    20. 0x000005e0 <+51>: lea edx,[ebp-0x16]
    21. 0x000005e3 <+54>: push edx
    22. 0x000005e4 <+55>: lea edx,[eax-0x1940]
    23. 0x000005ea <+61>: push edx
    24. 0x000005eb <+62>: mov ebx,eax
    25. 0x000005ed <+64>: call 0x450 <__isoc99_scanf@plt>
    26. 0x000005f2 <+69>: add esp,0x10
    27. 0x000005f5 <+72>: nop
    28. 0x000005f6 <+73>: mov eax,DWORD PTR [ebp-0xc] ; 从栈中取出 canary
    29. 0x000005f9 <+76>: xor eax,DWORD PTR gs:0x14 ; 检测 canary
    30. 0x00000600 <+83>: je 0x607 <main+90>
    31. 0x00000602 <+85>: call 0x690 <__stack_chk_fail_local>
    32. 0x00000607 <+90>: lea esp,[ebp-0x8]
    33. 0x0000060a <+93>: pop ecx
    34. 0x0000060b <+94>: pop ebx
    35. 0x0000060c <+95>: pop ebp
    36. 0x0000060d <+96>: lea esp,[ecx-0x4]
    37. 0x00000610 <+99>: ret
    38. End of assembler dump.

    关闭 CANARY 时:

    1. gdb-peda$ disassemble main
    2. Dump of assembler code for function main:
    3. 0x0000055d <+0>: lea ecx,[esp+0x4]
    4. 0x00000561 <+4>: and esp,0xfffffff0
    5. 0x00000564 <+7>: push DWORD PTR [ecx-0x4]
    6. 0x00000567 <+10>: push ebp
    7. 0x0000056a <+13>: push ebx
    8. 0x0000056b <+14>: push ecx
    9. 0x0000056c <+15>: sub esp,0x10
    10. 0x0000056f <+18>: call 0x59c <__x86.get_pc_thunk.ax>
    11. 0x00000574 <+23>: add eax,0x1a8c
    12. 0x00000579 <+28>: sub esp,0x8
    13. 0x0000057c <+31>: lea edx,[ebp-0x12]
    14. 0x0000057f <+34>: push edx
    15. 0x00000580 <+35>: lea edx,[eax-0x19e0]
    16. 0x00000586 <+41>: push edx
    17. 0x00000587 <+42>: mov ebx,eax
    18. 0x00000589 <+44>: call 0x400 <__isoc99_scanf@plt>
    19. 0x0000058e <+49>: add esp,0x10
    20. 0x00000591 <+52>: nop
    21. 0x00000592 <+53>: lea esp,[ebp-0x8]
    22. 0x00000596 <+57>: pop ebx
    23. 0x00000597 <+58>: pop ebp
    24. 0x00000598 <+59>: lea esp,[ecx-0x4]
    25. 0x0000059b <+62>: ret
    26. End of assembler dump.

    FORTIFY

    FORTIFY 的选项 -D_FORTIFY_SOURCE 往往和优化 -O 选项一起使用,以检测缓冲区溢出的问题。

    下面是一个简单的例子:

    1. #include<string.h>
    2. void main() {
    3. char str[3];
    4. strcpy(str, "abcde");
    5. }

    开启优化 -O2 后,编译没有检测出任何问题,checksec 后 FORTIFY 为 No。当配合 -D_FORTIFY_SOURCE=2(也可以 =1)使用时,提示存在溢出问题,checksec 后 FORTIFY 为 Yes。

    在 Linux 中,当装载器将程序装载进内存空间后,将程序的 .text 段标记为可执行,而其余的数据段(.data、.bss 等)以及栈、堆均为不可执行。因此,传统利用方式中通过修改 GOT 来执行 shellcode 的方式不再可行。

    但这种保护并不能阻止攻击者通过代码重用来进行攻击(ret2libc)。

    PIE

    PIE(Position Independent Executable)需要配合 ASLR 来使用,以达到可执行文件的加载时地址随机化。简单来说,PIE 是编译时随机化,由编译器完成;ASLR 是加载时随机化,由操作系统完成。ASLR 将程序运行时的堆栈以及共享库的加载地址随机化,而 PIE 在编译时将程序编译为位置无关、即程序运行时各个段加载的虚拟地址在装载时确定。开启 PIE 时,编译生成的是动态库文件(Shared object)文件,而关闭 PIE 后生成可执行文件(Executable)。

    我们通过实际例子来探索一下 PIE 和 ASLR:

    1. #include<stdio.h>
    2. void main() {
    3. printf("%p\n", main);
    4. }
    1. $ gcc -m32 -pie random.c -o open-pie
    2. $ readelf -h open-pie
    3. ELF Header:
    4. Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
    5. Class: ELF32
    6. Data: 2's complement, little endian
    7. Version: 1 (current)
    8. OS/ABI: UNIX - System V
    9. ABI Version: 0
    10. Type: DYN (Shared object file)
    11. Machine: Intel 80386
    12. Version: 0x1
    13. Entry point address: 0x400
    14. Start of program headers: 52 (bytes into file)
    15. Start of section headers: 6132 (bytes into file)
    16. Flags: 0x0
    17. Size of this header: 52 (bytes)
    18. Size of program headers: 32 (bytes)
    19. Number of program headers: 9
    20. Size of section headers: 40 (bytes)
    21. Number of section headers: 30
    22. Section header string table index: 29
    23. $ gcc -m32 -no-pie random.c -o close-pie
    24. $ readelf -h close-pie
    25. ELF Header:
    26. Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
    27. Class: ELF32
    28. Data: 2's complement, little endian
    29. Version: 1 (current)
    30. OS/ABI: UNIX - System V
    31. ABI Version: 0
    32. Type: EXEC (Executable file)
    33. Machine: Intel 80386
    34. Version: 0x1
    35. Entry point address: 0x8048310
    36. Start of program headers: 52 (bytes into file)
    37. Flags: 0x0
    38. Size of this header: 52 (bytes)
    39. Size of program headers: 32 (bytes)
    40. Number of program headers: 9
    41. Size of section headers: 40 (bytes)
    42. Number of section headers: 30
    43. Section header string table index: 29

    可以看到两者的不同在 TypeEntry point address

    首先我们关闭 ASLR,使用 -pie 进行编译:

    1. # gcc -m32 -pie random.c -o a.out
    2. # checksec --file a.out
    3. RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
    4. Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH No 0 2 a.out
    5. # ./a.out
    6. 0x5655553d
    7. # ./a.out
    8. 0x5655553d

    我们虽然开启了 -pie,但是 ASLR 被关闭,入口地址不变。

    1. # ldd a.out
    2. linux-gate.so.1 (0xf7fd7000)
    3. libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
    4. /lib/ld-linux.so.2 (0xf7fd9000)
    5. # ldd a.out
    6. linux-gate.so.1 (0xf7fd7000)
    7. libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
    8. /lib/ld-linux.so.2 (0xf7fd9000)

    可以看出动态链接库地址也不变。然后我们开启 ASLR:

    1. # echo 2 > /proc/sys/kernel/randomize_va_space
    2. # ./a.out
    3. 0x5665353d
    4. # ./a.out
    5. 0x5659753d
    6. # ldd a.out
    7. linux-gate.so.1 (0xf7727000)
    8. libc.so.6 => /usr/lib32/libc.so.6 (0xf7529000)
    9. /lib/ld-linux.so.2 (0xf7729000)
    10. # ldd a.out
    11. linux-gate.so.1 (0xf77d6000)
    12. libc.so.6 => /usr/lib32/libc.so.6 (0xf75d8000)
    13. /lib/ld-linux.so.2 (0xf77d8000)

    入口地址和动态链接库地址都变得随机。

    接下来关闭 ASLR,并使用 -no-pie 进行编译:

    入口地址和动态库都是固定的。下面开启 ASLR:

    1. # echo 2 > /proc/sys/kernel/randomize_va_space
    2. # ./b.out
    3. 0x8048406
    4. # ./b.out
    5. 0x8048406
    6. # ldd b.out
    7. linux-gate.so.1 (0xf7797000)
    8. libc.so.6 => /usr/lib32/libc.so.6 (0xf7599000)
    9. /lib/ld-linux.so.2 (0xf7799000)
    10. # ldd b.out
    11. linux-gate.so.1 (0xf770a000)
    12. libc.so.6 => /usr/lib32/libc.so.6 (0xf750c000)
    13. /lib/ld-linux.so.2 (0xf770c000)

    所以在分析一个 PIE 开启的二进制文件时,只需要关闭 ASLR,即可使 PIE 和 ASLR 都失效。

    RELRO(ReLocation Read-Only)设置符号重定向表为只读或在程序启动时就解析并绑定所有动态符号,从而减少对 GOT(Global Offset Table)的攻击。

    RELOR 有两种形式:

    • Partial RELRO:一些段(包括 .dynamic)在初始化后将会被标记为只读。
    • Full RELRO:除了 Partial RELRO,延迟绑定将被禁止,所有的导入符号将在开始时被解析,.got.plt 段会被完全初始化为目标函数的最终地址,并被标记为只读。另外 link_map_dl_runtime_resolve 的地址也不会被装入。

    各种安全技术的编译参数如下:

    关闭所有保护:

    1. gcc hello.c -o hello -fno-stack-protector -z execstack -no-pie -z norelro

    开启所有保护:

    1. gcc hello.c -o hello -fstack-protector-all -z noexecstack -pie -z now
    • FORTIFY
      • -D_FORTIFY_SOURCE=1:仅在编译时检测溢出
      • -D_FORTIFY_SOURCE=2:在编译时和运行时检测溢出

    有许多工具可以检测二进制文件所使用的编译器安全技术。下面介绍常用的几种:

    checksec

    1. $ checksec --file /bin/ls
    2. RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
    3. Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 5 15 /bin/ls
    1. $ gdb /bin/ls
    2. gdb-peda$ checksec
    3. CANARY : ENABLED
    4. FORTIFY : ENABLED
    5. NX : ENABLED
    6. RELRO : Partial

    针对该保护机制的攻击,往往是通过信息泄漏来实现。由于同一模块中的所有代码和数据的相对偏移是固定的,攻击者只要泄漏出某个模块中的任一代码指针或数据指针,即可通过计算得到此模块中任意代码或数据的地址。