[SWPUCTF 2024 秋季新生赛]不可名状的东西
栈迁移 + ret2libc + ORW
注意 read 读取到缓冲区的地址 buf,在 rbp-0x80 处,记好,有大用处。
1 | -0000000000000080 // Use data definition commands to manipulate stack variables and arguments. |
同时我们注意到:栈溢出的空间太小,不够我们去获取 shell,我们考虑到可以使用栈迁移去扩展空间。
Payload 1:第一次栈迁移准备
1 | io.recvuntil(b"name!") |
- 劫持 RBP:b”\x00”*(0x80) 填满缓冲区后,紧接着的 p64(bss + 0x80) 覆盖了栈上的 RBP。这意味着当当前函数结束时,寄存器 rbp 会被修改为 bss + 0x80。
- 劫持 RIP:返回地址被修改为 read 的地址(0x4011ef)。程序不会崩溃,而是跳转到程序内部调用 read 的地方,再次等待你的输入。
- 由于此时 rbp 已经被改为了 bss + 0x80,如果这个 read 代码片段是通过 [rbp - 0x80] 来寻址写入缓冲区的,那么接下来的输入就会被直接写入到 bss 段
Payload 2:在 BSS 段伪造栈帧并泄露 Libc
1 | payload2 = p64(bss + 0x80 + 0x28) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(read) |
将数据直接打入bss 段
- 伪造下一层 RBP:p64(bss + 0x80 + 0x28) 是为了后续第三次 read 时精准控制数据落点。
- ****pop_rdi -> puts_got -> puts_plt,调用 puts(puts_got),泄露 puts 函数的真实地址
- ****泄露完成后,再次调用 read 函数接收 Payload 3。
- 执行栈迁移:payload2 填充到 0x80 字节后,覆盖当前处于 bss 上的 RBP 为 bss,并将 RIP 覆盖为 leave_ret (leave; ret)。leave 指令等价于 mov rsp, rbp; pop rbp,这会正式把栈指针 rsp 强行拉到 bss 段,完成真正的栈迁移。
接收泄露并计算 Libc 基址
1 | io.recvline(2) |
payload3:ORW
1 | # Part A: open("./flag", O_RDONLY) |
- b”./flag\x00\x00” 刚好落在 bss + 0x28。
- pop_rdi -> bss + 0x28:把 “./flag” 的地址传给 rdi(第一个参数)。
- pop_rsi -> 0:把 O_RDONLY (只读模式,值为 0) 传给 rsi(第二个参数)。
- 调用 open:打开 flag 文件。在 Linux 环境下,程序默认占用了 0, 1, 2 (标准输入/输出/错误) 三个文件描述符,所以新打开的文件描述符 (fd) 必然是 3。
设置的栈上读入到 rbp之间的空间有点小,用sendfile()来代替 read()+write(),以此来缩短ROP的长度。
1 | # Part B: sendfile(stdout, fd, offset, count) |
- rdi = 1:第一个参数,标准输出 (stdout)。
- rsi = 3:第二个参数,刚才 open 返回的文件描述符 (fd = 3)。
- rdx = 0:第三个参数,偏移量 (offset = 0)。pop_rdx_rbx 顺便把无关的 rbx 也清零了。
- rcx = 0x40:第四个参数,读取字节数 (大小为 64 字节)。注:Libc 封装的 sendfile 函数第四个参数走 rcx,如果是系统调用 syscall 则是 r10,这里调用的是 Libc 函数所以用 rcx 是完全正确的。
- 执行:调用 sendfile,直接把 fd=3 的内容发送到 stdout (你的屏幕上)就可以读取想要读取的文件了。
总 EXP:
1 | from pwn import* |
更新: 2026-04-05 14:21:45
原文: https://www.yuque.com/idcm/wnemg9/gquxf8mtudl472t0