CTF PWN 常见二进制保护机制详解
在 CTF PWN 题中,拿到题目的第一步通常是使用 checksec 命令查看程序的保护开启情况。以下是五大核心保护机制的原理、作用及绕过思路。
1. Canary (栈溢出保护)
全称:Stack Canary / Stack Smashing Protector (SSP)
原理:
类似于煤矿中的金丝雀(Canary),系统会在栈底(EBP)之上、局部变量之下插入一个随机生成的数值(通常是 4 字节或 8 字节)。
函数在返回(ret)之前,会检查这个值是否被修改。如果发生了栈溢出,这个值通常会被覆盖,程序就会检测到并抛出 stack smashing detected 错误,从而终止运行。
内存布局:
[ buffer (局部变量) ]
[ Canary (随机值) ] <--- 溢出必须经过这里
[ Old EBP ]
[ Return Address ]
绕过思路:
- 泄露 (Leak):利用格式化字符串漏洞或数组越界读取,先读出 Canary 的值,在构造 Payload 时填回去。
- 爆破 (Brute Force):仅限于
fork出的子进程(如网络服务),因为子进程 Canary 不变,可以逐字节爆破。 - 劫持
__stack_chk_fail:如果能改写 GOT 表,劫持报错函数的地址,使其跳转到后门。
2. NX (不可执行内存)
全称:No-Execute (Linux) / DEP (Windows)
原理:
将数据所在的内存页(如 Stack 和 Heap)标识为不可执行(Not Executable)。只有代码段(Code Segment)拥有执行权限。
这意味着即使你把 Shellcode 写入了栈中,CPU 也不会执行它,而是直接报错。
绕过思路:
- ROP (Return Oriented Programming):既然不能执行栈上的代码,就利用程序现有的代码片段(Gadgets)。通过构造栈帧,让程序在现有的
ret指令之间跳跃,拼凑出攻击逻辑(如执行system("/bin/sh"))。 - ret2libc:跳转到 libc 库中的函数去执行。
3. PIE (代码段地址随机化)
全称:Position Independent Executable
原理:
开启 PIE 后,程序每次加载到内存时,其代码段(Code Segment)的基地址是随机的。
这意味着你在 IDA 里看到的固定地址(如 0x400520)在实际运行时会变动,你无法直接硬编码函数的地址。
区别:
- No PIE:
main函数地址通常固定(如0x400xxx)。 - PIE Enabled:
main函数地址随机,但偏移量(Offset)固定。
绕过思路:
- 泄露基址:利用漏洞泄露栈上保存的某个代码段地址(如返回地址),减去其在 IDA 中的偏移,算出程序的 Base Address。
- 公式:
真实地址 = Base Address + 偏移量
- 公式:
- Partial Overwrite:利用低 12 位(页内偏移)通常不变的特性,仅覆盖返回地址的低几位(有一定的爆破概率)。
4. ASLR (系统级地址随机化)
全称:Address Space Layout Randomization
原理:
这是操作系统的功能(Linux内核配置),不是编译选项。它负责随机化**堆(Heap)、栈(Stack)和共享库(libc)**的加载位置。
查看方式:
cat /proc/sys/kernel/randomize_va_space
分为三个等级:
0: 关闭 (Disabled)
- 特征:没有任何随机化。
- 表现:栈 (Stack)、堆 (Heap)、动态库 (Libc) 的加载基址全部是固定的。
- 利用:攻击者可以在本地调试好地址,直接硬编码攻击远程(前提是远程也是0)。
1: 保守随机化 (Conservative Randomization)
- 特征:“栈乱,堆不乱”。
- 随机区域:栈 (Stack)、动态库 (Libc/mmap)、vDSO。(注意:Libc 在这里通常是随机的)
- 固定区域:堆 (Heap)。此时堆的基地址紧跟在程序数据段(Data Segment)之后,如果程序本身没有开启 PIE,那么堆的地址就是固定的。
- 利用:常用于攻击堆溢出相关的题目,因为堆块位置可预测。
2: 完全随机化 (Full Randomization) —— [标准环境]
- 特征:“全乱”。
- 表现:在等级 1 的基础上,堆 (Heap) 的基地址也是随机的。
- 现状:这是现代 Linux 系统(以及大多数 CTF 远程服务器)的默认配置。
PIE vs ASLR:
- PIE 决定了程序本身(Main Binary) 是否随机。
- ASLR 决定了依赖库(libc)、栈和堆 是否随机。
- 注意:CTF 题目远程服务器通常默认开启 ASLR=2。
绕过思路:
- Leak Libc:利用
puts或write等输出函数,打印 GOT 表中已解析的函数地址,算出 libc 的基址(Base),再计算system和/bin/sh的真实地址。
5. RELRO (重定位只读)
全称:Relocation Read-Only
原理:
用于保护 GOT 表(Global Offset Table)不被恶意修改。
分类:
- No RELRO:GOT 表可读可写,非常危险。
- Partial RELRO (GCC 默认):
- 非 PLT 部分只读。
- GOT 表仍然可写。
- 意味着我们可以修改 GOT 表中的函数地址(如把
printf改成system)。
- Full RELRO:
- 程序加载时一次性解析所有函数。
- 整个 GOT 表只读。
- 攻击者无法修改 GOT 表,必须通过其他方式(如 Hook 劫持、写 Stack)攻击。
总结:Checksec 对照表
当你输入 checksec ./pwn 时:
| 保护项 | 显示结果 | 含义 | 攻击影响 |
|---|---|---|---|
| Arch | amd64-64-little |
64位程序 | 寄存器传参,地址 8 字节 |
| RELRO | Partial RELRO |
GOT 表可写 | 可以使用 GOT Overwrite 攻击 |
| Stack | Canary found |
开启 Canary | 需泄露 Canary 才能溢出 |
| NX | NX enabled |
栈不可执行 | 必须使用 ROP,不能用 Shellcode |
| PIE | PIE enabled |
开启 PIE | 需泄露程序基址才能用 Gadget |