mirror of
https://github.com/nganhkhoa/CTF-All-In-One.git
synced 2024-12-25 11:41:16 +07:00
296 lines
10 KiB
Markdown
296 lines
10 KiB
Markdown
# GCC 堆栈保护技术
|
||
|
||
- [技术简介](#技术简介)
|
||
- [编译参数](#编译参数)
|
||
- [保护机制检测](#保护机制检测)
|
||
|
||
|
||
## 技术简介
|
||
Linux 中有各种各样的安全防护,其中 ASLR 是由内核直接提供的,通过系统配置文件控制。NX,Canary,PIE,RELRO 等需要在编译时根据各项参数开启或关闭。未指定参数时,使用默认设置。
|
||
|
||
#### 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
|
||
PIE(Position Independent Executable)需要配合 ASLR 来使用,以达到可执行文件的加载时地址随机化。简单来说,PIE 是编译时随机化,由编译器完成;ASLR 是加载时随机化,由操作系统完成。
|
||
|
||
我们通过实际例子来探索一下 PIE 和 ASLR:
|
||
```c
|
||
#include<stdio.h>
|
||
void main() {
|
||
printf("%p\n", main);
|
||
}
|
||
```
|
||
首先我们关闭 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 都失效。
|
||
|
||
> ASLR(Address Space Layout Randomization)
|
||
>
|
||
>关闭:`# echo 0 > /proc/sys/kernel/randomize_va_space`
|
||
>
|
||
>部分开启(将 mmap 的基址,stack 和 vdso 页面随机化):`# echo 1 > /proc/sys/kernel/randomize_va_space`
|
||
>
|
||
>完全开启(在部分开启的基础上增加 heap的随机化:`# echo 2 > /proc/sys/kernel/randomize_va_space`
|
||
|
||
#### RELRO
|
||
RELRO(ReLocation Read-Only)设置符号重定向表为只读或在程序启动时就解析并绑定所有动态符号,从而减少对 GOT(Global 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-L -fstack-protector -z execstack -no-pie -z norelro
|
||
```
|
||
开启所有保护:
|
||
```text
|
||
gcc hello.c -o hello-S -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
|
||
```
|