:降维打击——SROP(Sigreturn Oriented Programming)原理与实战)
写在前面在上一篇文章中我们通过栈迁移Stack Pivot解决了“舞台”不够的问题。但有时候即使舞台够了我们也可能找不到足够的“演员”Gadget来控制所有需要的寄存器。今天介绍的SROPSigreturn Oriented Programming技术堪称ROP家族中的“降维打击”——它只需要一个关键的Gadget就能一次性控制所有寄存器包括之前难以掌控的rdx。它的核心是利用Linux内核信号处理机制中的一个设计缺陷。 目录核心原理信号机制与 Sigreturn 的漏洞攻击条件SROP 需要哪些“积木”实战推演伪造 Signal Frame 调用execve高阶应用SROP 链与绕过沙箱总结与避坑指南1. 核心原理信号机制与 Sigreturn 的漏洞要理解SROP必须先了解Linux的信号机制。当进程收到信号如SIGINT时内核会暂停该进程并为其保存完整的上下文所有寄存器的值到用户态栈上形成一个称为Signal Frame的结构。然后内核会跳转到用户注册的信号处理函数去执行csdn.net1。信号处理函数执行完毕后会通过一个特殊的系统调用——sigreturn——来恢复进程上下文。sigreturn会从栈上读取之前保存的Signal Frame并将其中的值全部恢复到对应的寄存器中然后进程继续执行ba1100n.tech1。漏洞的关键点在于内核在执行sigreturn恢复上下文时并不会检查栈上的Signal Frame是否被篡改它完全信任用户态栈上的数据backblazeb2.com1。因此攻击者只要能通过栈溢出控制返回地址跳转到触发sigreturn的Gadget。在栈上精心伪造一个Signal Frame设置所有寄存器为我们想要的值例如rax0x3bfor execve,rdi/bin/sh,ripsyscall。当sigreturn执行时内核会忠实地将我们伪造的值恢复到寄存器中从而实现任意系统调用。下图清晰地展示了这一攻击流程攻击者控制栈伪造 Signal Frame触发 sigreturn 调用内核从栈上恢复所有寄存器执行伪造的 rip如 syscall完成任意系统调用如 execve与普通ROP对比普通ROP需要逐个寻找pop rdi; ret,pop rsi; ret,pop rdx; ret等Gadget来控制每个寄存器而SROP只需一个触发sigreturn的Gadget通常是syscall; ret就能控制全部寄存器csdn.net1。2. 攻击条件SROP 需要哪些“积木”启动一次SROP攻击通常需要满足以下条件csdn.net1栈溢出漏洞能控制返回地址。触发sigreturn的Gadget通常是syscall; ret64位或int 0x80; ret32位。有时还需要能将rax设置为0xf64位sigreturn系统调用号的Gadget如mov rax, 0xf; syscall; retcsdn.net1。可写的栈空间用于布置伪造的Signal Frame通常需要0x100字节左右。/bin/sh字符串地址如果要调用execve需要知道或能写入该字符串的地址。小贴士sigreturn的系统调用号在64位Linux下是150xf在32位下是1190x77github.io。3. 实战推演伪造 Signal Frame 调用execve假设我们已通过栈迁移将控制权转移到一个可控的栈区域如BSS段且已知syscall; ret的地址为0x401033。我们的目标是调用execve(/bin/sh, 0, 0)。3.1 构造 Signal Framepwntools提供了SigreturnFrame类大大简化了Frame的构造csdn.net2。from pwn import * context.arch amd64 # 假设 syscall_ret 0x401033, bin_sh_addr 0x404060 frame SigreturnFrame() frame.rax constants.SYS_execve # 0x3b frame.rdi bin_sh_addr # 指向 /bin/sh frame.rsi 0 # argv NULL frame.rdx 0 # envp NULL frame.rip syscall_ret # 恢复后执行 syscall frame.rsp bin_sh_addr # 随意设置一个可写地址通常无关紧要 payload bytes(frame)3.2 触发 sigreturn我们需要让rax寄存器等于0xf然后执行syscall。如果程序中恰好有mov rax, 0xf; syscall; ret这样的Gadget那最理想。如果没有可以利用read等系统调用的返回值来设置rax。例如程序中有以下Gadget0x401000: xor rax, rax; syscall; ret # sys_read(rdi, rsi, rdx)我们可以构造一个ROP链先调用read(0, frame_addr, len(payload))将伪造的Frame读入栈上此时rax的值就是实际读取的字节数。如果我们让读取的字节数恰好为0xf那么read返回后rax就是0xf紧接着再执行一个syscall就会触发sigreturncsdn.net1。完整Payload结构推演# 1. 调用 read(0, frame_addr, 0xf) 将伪造帧读入并使 rax 0xf rop ROP(./vuln) rop.read(0, frame_addr, 0xf) # 2. 触发 syscall (此时 rax0xf, 栈上是伪造的Frame) rop.call(syscall_ret) # 3. 伪造的 Signal Frame (放在 payload 末尾) payload rop.chain() bytes(frame)3.3 模拟执行当程序执行到我们构造的ROP链时read读取0xf个字节到frame_addrrax变为0xf。接下来的syscall指令执行sigreturn。内核从frame_addr处读取我们伪造的Signal Frame将所有寄存器恢复为设定值rax0x3b(execve)rdibin_sh_addr(“/bin/sh”)rsi0rdx0ripsyscall_ret进程恢复执行立即触发execve(/bin/sh, 0, 0)成功Getshell模拟终端输出[*] Switching to interactive mode $ id uid1000(user) gid1000(user) groups1000(user)4. 高阶应用SROP 链与绕过沙箱SROP的强大之处在于其可链式调用。通过在伪造的Frame中设置rsp指向下一个Frame并设置rip指向sigreturn的Gadget可以实现连续的多次SROP攻击csdn.net1。例如一个SROP链可以第一个Frame调用read将/bin/sh字符串写入BSS段。第二个Frame调用execve执行该字符串。这在程序没有/bin/sh字符串时非常有用。SROP绕过Seccomp沙箱当沙箱禁用execve但允许read和write时SROP可以伪造一个调用write(1, flag_addr, flag_len)的Frame来直接读取并输出flag文件完全绕过execve的限制csdn.net。5. 总结与避坑指南优点缺点/限制✅单Gadget控制所有寄存器无需寻找大量popGadget❌需要较大的栈空间约0x100字节来存放Frame✅可移植性强不同Unix系统信号机制类似❌依赖特定Gadgetsyscall; ret或能设置rax0xf的Gadget✅可链式调用实现复杂逻辑❌可能被防御机制检测如检查sigreturn的来源✅强大到可绕过沙箱调用任意系统调用❌在32位上系统调用号不同119需注意避坑指南Frame对齐在64位系统上Signal Frame必须16字节对齐否则可能在恢复上下文时崩溃。rsp的值在伪造Frame时rsp通常会被设置为某个值但在sigreturn恢复后程序会跳转到rip执行rsp的值一般不再重要除非你后续还需要使用栈。可将其设为一个可写地址。rax的设置这是最关键的一步。如果找不到现成的mov rax, 0xf; syscall; ret巧妙利用read的返回值是CTF中最常见的技巧csdn.net1。防御机制一些现代内核或加固程序可能会检测sigreturn调用的合法性但CTF题目中通常不会启用。6. 结语SROP是ROP技术的巅峰之作它将“代码重用”推向了极致——我们甚至不再重用任何代码片段而是直接“伪造”了进程的完整状态。理解SROP意味着你对Linux进程和信号机制有了更深层次的认识。下一篇我们将从“伪造状态”转向“伪造解析”学习如何通过手动解析PLT表项和ret2dl_resolve技术在程序自身没有所需函数Gadget时动态地调用任意库函数。如果本文对你有帮助请点赞收藏支持