CTF-All-In-One/doc/4.3_gcc.md
2017-08-23 20:36:52 +08:00

344 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# GCC 堆栈保护技术
- [技术简介](#技术简介)
- [编译参数](#编译参数)
- [保护机制检测](#保护机制检测)
## 技术简介
Linux 中有各种各样的安全防护,其中 ASLR 是由内核直接提供的通过系统配置文件控制。NXCanaryPIERELRO 等需要在编译时根据各项参数开启或关闭。未指定参数时,使用默认设置。
#### CANARY
启用 CANARY 后,函数开始执行的时候会先往栈里插入 canary 信息,当函数返回时验证插入的 canary 是否被修改,如果是,就停止运行。
下面是一个例子:
```c
#include <stdio.h>
void main(int argc, char **argv) {
char buf[10];
scanf("%s", buf);
}
```
我们先开启 CANARY来看看执行的结果
```text
$ gcc -m32 -fstack-protector canary.c -o f.out
$ python -c 'print("A"*20)' | ./f.out
*** stack smashing detected ***: ./f.out terminated
Segmentation fault (core dumped)
```
接下来关闭 CANARY
```text
$ gcc -m32 -fno-stack-protector canary.c -o fno.out
$ python -c 'print("A"*20)' | ./fno.out
Segmentation fault (core dumped)
```
可以看到当开启 CANARY 的时候,提示检测到栈溢出和段错误,而关闭的时候,只有提示段错误。
下面对比一下反汇编代码上的差异:
开启 CANARY 时:
```text
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x000005ad <+0>: lea ecx,[esp+0x4]
0x000005b1 <+4>: and esp,0xfffffff0
0x000005b4 <+7>: push DWORD PTR [ecx-0x4]
0x000005b7 <+10>: push ebp
0x000005b8 <+11>: mov ebp,esp
0x000005ba <+13>: push ebx
0x000005bb <+14>: push ecx
0x000005bc <+15>: sub esp,0x20
0x000005bf <+18>: call 0x611 <__x86.get_pc_thunk.ax>
0x000005c4 <+23>: add eax,0x1a3c
0x000005c9 <+28>: mov edx,ecx
0x000005cb <+30>: mov edx,DWORD PTR [edx+0x4]
0x000005ce <+33>: mov DWORD PTR [ebp-0x1c],edx
0x000005d1 <+36>: mov ecx,DWORD PTR gs:0x14 ; 将 canary 值存入 ecx
0x000005d8 <+43>: mov DWORD PTR [ebp-0xc],ecx ; 在栈 ebp-0xc 处插入 canary
0x000005db <+46>: xor ecx,ecx
0x000005dd <+48>: sub esp,0x8
0x000005e0 <+51>: lea edx,[ebp-0x16]
0x000005e3 <+54>: push edx
0x000005e4 <+55>: lea edx,[eax-0x1940]
0x000005ea <+61>: push edx
0x000005eb <+62>: mov ebx,eax
0x000005ed <+64>: call 0x450 <__isoc99_scanf@plt>
0x000005f2 <+69>: add esp,0x10
0x000005f5 <+72>: nop
0x000005f6 <+73>: mov eax,DWORD PTR [ebp-0xc] ; 从栈中取出 canary
0x000005f9 <+76>: xor eax,DWORD PTR gs:0x14 ; 检测 canary 值
0x00000600 <+83>: je 0x607 <main+90>
0x00000602 <+85>: call 0x690 <__stack_chk_fail_local>
0x00000607 <+90>: lea esp,[ebp-0x8]
0x0000060a <+93>: pop ecx
0x0000060b <+94>: pop ebx
0x0000060c <+95>: pop ebp
0x0000060d <+96>: lea esp,[ecx-0x4]
0x00000610 <+99>: ret
End of assembler dump.
```
关闭 CANARY 时:
```text
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x0000055d <+0>: lea ecx,[esp+0x4]
0x00000561 <+4>: and esp,0xfffffff0
0x00000564 <+7>: push DWORD PTR [ecx-0x4]
0x00000567 <+10>: push ebp
0x00000568 <+11>: mov ebp,esp
0x0000056a <+13>: push ebx
0x0000056b <+14>: push ecx
0x0000056c <+15>: sub esp,0x10
0x0000056f <+18>: call 0x59c <__x86.get_pc_thunk.ax>
0x00000574 <+23>: add eax,0x1a8c
0x00000579 <+28>: sub esp,0x8
0x0000057c <+31>: lea edx,[ebp-0x12]
0x0000057f <+34>: push edx
0x00000580 <+35>: lea edx,[eax-0x19e0]
0x00000586 <+41>: push edx
0x00000587 <+42>: mov ebx,eax
0x00000589 <+44>: call 0x400 <__isoc99_scanf@plt>
0x0000058e <+49>: add esp,0x10
0x00000591 <+52>: nop
0x00000592 <+53>: lea esp,[ebp-0x8]
0x00000595 <+56>: pop ecx
0x00000596 <+57>: pop ebx
0x00000597 <+58>: pop ebp
0x00000598 <+59>: lea esp,[ecx-0x4]
0x0000059b <+62>: ret
End of assembler dump.
```
#### FORTIFY
FORTIFY 的选项 `-D_FORTIFY_SOURCE` 往往和优化 `-O` 选项一起使用,以检测缓冲区溢出的问题。
下面是一个简单的例子:
```c
#include<string.h>
void main() {
char str[3];
strcpy(str, "abcde");
}
```
```text
$ gcc -O2 fortify.c
$ checksec --file a.out
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH No 0 0a.out
$ gcc -O2 -D_FORTIFY_SOURCE=2 fortify.c
In file included from /usr/include/string.h:639:0,
from fortify.c:1:
In function strcpy,
inlined from main at fortify.c:4:2:
/usr/include/bits/string3.h:109:10: warning: __builtin___memcpy_chk writing 6 bytes into a region of size 3 overflows the destination [-Wstringop-overflow=]
return __builtin___strcpy_chk (__dest, __src, __bos (__dest));
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ checksec --file a.out
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH Yes 2 2a.out
```
开启优化 `-O2`编译没有检测出任何问题checksec 后 FORTIFY 为 No。当配合 `-D_FORTIFY_SOURCE=2`(也可以 `=1`使用时提示存在溢出问题checksec 后 FORTIFY 为 Yes。
#### NX
No-eXecute表示不可执行其原理是将数据所在的内存页标识为不可执行如果程序产生溢出转入执行 shellcode 时CPU 会抛出异常。其绕过方法是 ret2libc。
#### PIE
PIEPosition Independent Executable需要配合 ASLR 来使用以达到可执行文件的加载时地址随机化。简单来说PIE 是编译时随机化由编译器完成ASLR 是加载时随机化,由操作系统完成。开启 PIE 时编译生成的是动态库文件Shared object文件而关闭 PIE 后生成可执行文件Executable
我们通过实际例子来探索一下 PIE 和 ASLR
```c
#include<stdio.h>
void main() {
printf("%p\n", main);
}
```
```text
$ gcc -m32 -pie random.c -o open-pie
$ readelf -h open-pie
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x400
Start of program headers: 52 (bytes into file)
Start of section headers: 6132 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 9
Size of section headers: 40 (bytes)
Number of section headers: 30
Section header string table index: 29
$ gcc -m32 -no-pie random.c -o close-pie
$ readelf -h close-pie
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048310
Start of program headers: 52 (bytes into file)
Start of section headers: 5964 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 9
Size of section headers: 40 (bytes)
Number of section headers: 30
Section header string table index: 29
```
可以看到两者的不同在 `Type``Entry point address`
首先我们关闭 ASLR使用 `-pie` 进行编译:
```text
# echo 0 > /proc/sys/kernel/randomize_va_space
# gcc -m32 -pie random.c -o a.out
# checksec --file a.out
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH No 0 2 a.out
# ./a.out
0x5655553d
# ./a.out
0x5655553d
```
我们虽然开启了 `-pie`,但是 ASLR 被关闭,入口地址不变。
```text
# ldd a.out
linux-gate.so.1 (0xf7fd7000)
libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
/lib/ld-linux.so.2 (0xf7fd9000)
# ldd a.out
linux-gate.so.1 (0xf7fd7000)
libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
/lib/ld-linux.so.2 (0xf7fd9000)
```
可以看出动态链接库地址也不变。然后我们开启 ASLR
```text
# echo 2 > /proc/sys/kernel/randomize_va_space
# ./a.out
0x5665353d
# ./a.out
0x5659753d
# ldd a.out
linux-gate.so.1 (0xf7727000)
libc.so.6 => /usr/lib32/libc.so.6 (0xf7529000)
/lib/ld-linux.so.2 (0xf7729000)
# ldd a.out
linux-gate.so.1 (0xf77d6000)
libc.so.6 => /usr/lib32/libc.so.6 (0xf75d8000)
/lib/ld-linux.so.2 (0xf77d8000)
```
入口地址和动态链接库地址都变得随机。
接下来关闭 ASLR并使用 `-no-pie` 进行编译:
```text
# echo 0 > /proc/sys/kernel/randomize_va_space
# gcc -m32 -no-pie random.c -o b.out
# checksec --file b.out
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 0 2 b.out
# ./b.out
0x8048406
# ./b.out
0x8048406
# ldd b.out
linux-gate.so.1 (0xf7fd7000)
libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
/lib/ld-linux.so.2 (0xf7fd9000)
# ldd b.out
linux-gate.so.1 (0xf7fd7000)
libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
/lib/ld-linux.so.2 (0xf7fd9000)
```
入口地址和动态库都是固定的。下面开启 ASLR
```text
# echo 2 > /proc/sys/kernel/randomize_va_space
# ./b.out
0x8048406
# ./b.out
0x8048406
# ldd b.out
linux-gate.so.1 (0xf7797000)
libc.so.6 => /usr/lib32/libc.so.6 (0xf7599000)
/lib/ld-linux.so.2 (0xf7799000)
# ldd b.out
linux-gate.so.1 (0xf770a000)
libc.so.6 => /usr/lib32/libc.so.6 (0xf750c000)
/lib/ld-linux.so.2 (0xf770c000)
```
入口地址依然固定,但是动态库变为随机。
所以在分析一个 PIE 开启的二进制文件时,只需要关闭 ASLR即可使 PIE 和 ASLR 都失效。
> ASLRAddress Space Layout Randomization
>
>关闭:`# echo 0 > /proc/sys/kernel/randomize_va_space`
>
>部分开启(将 mmap 的基址stack 和 vdso 页面随机化):`# echo 1 > /proc/sys/kernel/randomize_va_space`
>
>完全开启(在部分开启的基础上增加 heap的随机化`# echo > /proc/sys/kernel/randomize_va_space`
#### RELRO
RELROReLocation Read-Only设置符号重定向表为只读或在程序启动时就解析并绑定所有动态符号从而减少对 GOTGlobal Offset Table的攻击。
## 编译参数
各种安全技术的编译参数如下:
安全技术 | 完全开启 | 部分开启 | 关闭
--- | --- | --- | ---
Canary | -fstack-protector-all | -fstack-protector | -fno-stack-protector
NX | -z noexecstack | | -z execstack
PIE | -pie | | -no-pie
RELRO | -z now | -z lazy | -z norelro
关闭所有保护:
```text
gcc hello.c -o hello -fno-stack-protector -z execstack -no-pie -z norelro
```
开启所有保护:
```text
gcc hello.c -o hello -fstack-protector-all -z noexecstack -pie -z now
```
- FORTIFY
- `-D_FORTIFY_SOURCE=1`:仅在编译时检测溢出
- `-D_FORTIFY_SOURCE=2`:在编译时和运行时检测溢出
## 保护机制检测
有许多工具可以检测二进制文件所使用的编译器安全技术。下面介绍常用的几种:
#### checksec
```text
$ checksec --file /bin/ls
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 5 15 /bin/ls
```
#### peda 自带的 checksec
```text
$ gdb /bin/ls
gdb-peda$ checksec
CANARY : ENABLED
FORTIFY : ENABLED
NX : ENABLED
PIE : disabled
RELRO : Partial
```