2026-4-14-栈迁移

2026-4-14-栈迁移

栈迁移

栈迁移题目分析

第一步,为什么要栈迁移?

栈迁移因为可写缓冲区也就是buff 不足以支持完整的RPO链

我们只能将RPO链放到 .bss

  • .bss解释:
    • .bss是内存上一块内存区域,它的存在是编译器预留了存放未初始化的全局变量/静态变量的位置
    • 这也是我们平时写代码,为什么存在了未使用的变量会导致报错的原因。
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
; Attributes: bp-based frame

; __int64 __fastcall main(int, char **, char **)
main proc near

buf= byte ptr -80h

; __unwind {
push rbp
mov rbp, rsp
add rsp, 0FFFFFFFFFFFFFF80h
mov rax, cs:stdin
mov esi, 0 ; buf
mov rdi, rax ; stream
call _setbuf
mov rax, cs:stdout
mov esi, 0 ; buf
mov rdi, rax ; stream
call _setbuf
mov rax, cs:stderr
mov esi, 0 ; buf
mov rdi, rax ; stream
call _setbuf
lea rax, s ; "Are you the king of stack migrate?"
mov rdi, rax ; s
call _puts
lea rax, [rbp+buf]
mov edx, 90h ; nbytes
mov rsi, rax ; buf
mov edi, 0 ; fd
call _read
lea rax, aGoodLuck ; "Good luck."
mov rdi, rax ; s
call _puts
mov eax, 0
leave
retn
; } // starts at 40114B
main endp

_text ends

这段汇编代码中,你可能认为buf.bss地址,这是个常见的错误。

1
2
3
4
5
6
7
; __unwind {
push rbp
mov rbp, rsp
add rsp, 0FFFFFFFFFFFFFF80h
mov rax, cs:stdin
mov esi, 0 ; buf
mov rdi, rax ; stream

buf 没有被程序主动初始化,既没有清零,也没有赋值,但并不属于.bss,因为其在栈上。

pwntools工具可以直接寻找,不需要手动找

1
2
3
4
from pwn import *

elf = ELF('./pwn')
print(hex(elf.bss()))

output:

1
2
3
4
5
6
7
8
9
(base) ┌──(.venv)(cure㉿LAPTOP-CMAM5D0J)-[~/CTF/pwn/19]
└─$ /home/cure/miniconda3/bin/python /home/cure/CTF/pwn/19/exp2.py
[*] '/home/cure/CTF/pwn/19/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
0x404020

.bss=0x404020

然后我们即可开始第二步,伪造假栈,迁移原栈

首先我们必须知道 fake_rpb 也就是假栈的地址,是交给leave; ret 去执行的

我们需要让rsp精确落到伪造好的假栈开头

行为:

1
2
leave        ; mov rsp, rbp ; pop rbp
ret ; pop rip

例:

1
2
leave_ret = 0x4011c8 
fake_stack = 0x404800

假栈内容

1
2
3
4
fake_stack  = p64(0)              # 新 rbp
fake_stack += p64(0x401146) # pop rdi ; ret
fake_stack += p64(0x404900) # 参数
fake_stack += p64(0x401030) # puts@plt
1
fake_rbp = fake_stack_addr = 0x401146

ruler =

1
2
3
[fake_rbp + 0x00] = new_rbp
[fake_rbp + 0x08] = 第一个 gadget / 返回地址
[fake_rbp + 0x10] = 后续参数或 gadget

利用流程总结

本题的整体思路如下:

1. 确认栈空间不足

当前 buf 只有 0x80,不足以支撑完整 ROP 链。

2. 寻找可写区域

使用 .bss 作为假栈区域。

3. 找到 .bss 地址

通过 elf.bss() 获取 .bss 起始地址,再偏移一段距离作为真正使用的假栈区域。

4. 伪造假栈内容

.bss 中布置:

  • 新 rbp
  • 第一个 gadget
  • 后续参数
  • 后续 ROP 链

5. 控制 saved rbp

把它改成 fake_rbp

6. 控制返回地址

把返回地址改成 leave; ret

7. 执行栈迁移

leave; ret 执行后,rsp 就会落到 .bss 上,开始从假栈继续执行。


一句话总结

栈迁移的本质就是:

当前栈空间不够用,于是把 rsp 迁移到 .bss 这种更大、可写的区域中,再从那里继续执行 ROP 链。

fake_rbp 的本质,就是:

你希望下一次 leave 执行时,rsp 落到的那个地址。

最近要去准备开发类的比赛了暂放CTF,预留AI补充的思路过程暂时收尾此博客。