栈迁移SROP
[SWPUCTF 2025 秋季新生赛]gift
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
这里原始栈空间不足以打 SROP,做一个栈迁移
EXP 思路:
- 环境准备:
1 | from pwn import * |
把 read 函数作为 start 地址,便于我们去循环读取 payload
- 栈迁移,将 RBP 迁移到 BSS 段
1 | payload1 = p64(pop_rbp_ret) |
- 程序原本的 read 结束后,触发 ret,跳去执行 pop rbp; ret。
- pop rbp 将栈上的下一个值(bss_addr)弹出到 RBP 寄存器。
- 随后的 ret 跳去执行 start_addr,重新开启一次 read。
- 当第二次进入 start_addr 并在最后执行 leave; ret 时(leave 等价于 mov rsp, rbp; pop rbp),RSP 会被强行赋值为现在的 RBP(即 bss_addr)。至此,栈指针正式转移到了 BSS 段。
- 构造伪造帧与内存布局
1 | frame = SigreturnFrame() |
- 构造 frame:利用 Pwntools 的 SigreturnFrame() 生成一个长达 248 字节的数据块。我们只需指定几个关键寄存器的值,目的是拼凑出一个调用 execve(“/bin/sh”, 0, 0) 的环境。
- 内存对齐魔法:注意看 payload2 的拼接。
- 作者把 b’/bin/sh\x00’ 放在了第四个 8 字节处。由于基址现在是 bss_addr,所以经过偏移计算,这个字符串在内存中的绝对地址就是 bss_addr + 0x20。
- 这完美对应了上面 frame.rdi = bss_addr + 0x20 的设定。
- *payload2 同样包含了 p64(start_addr),这意味着程序接收完这些数据后,会第三次*回到 read 等待输入。
- 控制 RAX 触发 SROP
1 | payload3 = p64(syscall_ret) + b'\x00' * 7 |
- 触发 rt_sigreturn 系统调用。该调用的系统调用号是 15。在 x86_64 架构中系统调用号由 RAX 寄存器决定
- 在 Linux 中,read() 函数执行完毕后,返回值(实际读取的字节数)会保存在 RAX 中。而 payload3 的长度:8 (syscall_ret) + 7 (\x00) = 15 字节。
- 当程序第三次调用 read 时,我们精准发送 15 个字节。read 结束后,RAX 会自然而然地变成 15。
- read 结束后触发 ret,因为栈顶现在是 payload3 写进去的 p64(syscall_ret),所以程序直接去执行 syscall。
- 此时 RAX = 15,内核判定这是一个 rt_sigreturn 请求。
- 内核开始将我们提前布局在 [bss+48] 的 frame 强行覆盖到 CPU 的各个物理寄存器中。
- 覆盖完成后,CPU 的 RIP 变成了 syscall_ret,RAX 变成了 59,RDI 指向了 /bin/sh。
- CPU 执行下一条指令(也就是刚刚恢复的 RIP 里的 syscall),顺利拿到 shell!
总 EXP:
1 | from pwn import * |
有个小小的思考:我把其中一段 exp 修改成这个样子:
1 |
|
当程序执行完第二次 read(也就是接收你的 payload2 时),它会到达函数的结尾并执行 ret
- 在原始代码中:ret 会把栈上的 start_addr 弹入 RIP(指令寄存器),程序乖乖地跑回去第三次开启 read,等待接收你的 payload3。
- 在这个错误的的代码中:栈上对应的位置变成了 b’/bin/sh\x00’ 或者 b’a’*8。ret 指令会把 /bin/sh 的 ASCII 码(0x0068732f6e69622f)弹入 RIP。CPU 会试图去内存地址 0x0068732f6e69622f 执行代码,这个地址根本没有代码,程序就会当场崩溃。
所以不可以这样子写哦 ^_^
更新: 2026-04-10 10:40:05
原文: https://www.yuque.com/idcm/wnemg9/dqrm2nnaovyfsow0