☰
ROP 进阶实战:SROP(Sigreturn-Oriented Programming)信号机制利用与原理
2026/10/13 5:00:44 网站建设 项目流程

在面向返回编程(ROP)的代码重用攻击体系中,传统的利用思路通常高度依赖于目标二进制代码段或libc动态库中存在足够丰富、且便于串联的通用指令碎片(如pop rdi; ret、pop rsi; ret、pop rdx; ret)。然而,在面对体积高度精简、静态编译且剥离了几乎所有外部依赖的边缘二进制(例如由 Go 语言静态打包或用汇编编写的微型守护服务)时,攻击者往往会陷入绝境:整个程序代码段可能只有寥寥几十条指令,连一个最基础的参数控制 Gadget 都无法凑齐。

在这种“巧妇难为无米之炊”的极端环境下,二进制攻防历史上诞生了一项极具系统底层美感的技术——SROP(Sigreturn-Oriented Programming,面向信号返回编程)。

SROP 的绝妙之处在于:它不再去苦苦搜寻并拼接零散的汇编 Gadget,而是直接利用 Linux 操作系统内核在处理软中断信号时原本就具备的“全寄存器现场恢复机制(Signal Frame Restoration)”,通过一次系统调用,实现对 CPU 物理全量寄存器(RAX、RDI、RSI、RDX、RSP、RIP 等)的绝对控制!

本文将从 Linux 内核信号分发机制的微观视角出发,深度剖析 Signal Frame 结构体在执行栈上的物理排布,并揭秘攻击者如何利用一条sigreturn指令完成惊艳的“乾坤大挪移”,最后给出系统内核层面的深度防御之道。


一、 Linux 信号机制的微观时序:内核是如何保护现场的?

在 Linux 操作系统中,当进程收到外部信号(如SIGALRM、SIGSEGV、SIGINT)时,内核需要暂时挂起用户态代码,转去执行预先注册的信号处理函数(Signal Handler)。

为了保证信号处理函数执行完毕后,主程序能够毫无察觉地继续在被打断的地方无缝运行,Linux 内核必须在挂起的一瞬间,将当前 CPU 的全套物理状态(所有通用寄存器、浮点寄存器、段寄存器和标志位)完完整整地保存在用户态的栈顶之上。

内核处理信号的完整生命周期:
用户态程序正在正常执行 (User Mode) | v [外部硬件中断或内核信号到达] ======================= 陷入 Linux 内核空间 (Kernel Mode) ======================= | | 1. 内核挂起当前进程 | 2. 内核在用户态的执行栈上开辟空间,推入一个巨大的结构体: | 【Signal Frame (包含 ucontext_t 与 siginfo_t)】 | 其中完整克隆了 RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, RIP, RFLAGS! | 3. 内核将返回地址伪造为一条系统调用: sigreturn v ======================= 返回用户态空间 (User Mode) ======================= | | 4. 跳转执行用户自定义的信号处理函数 (Signal Handler) | 5. 信号处理函数执行结束,调用 sigreturn 系统调用 (Syscall 15) v ======================= 再次陷入 Linux 内核空间 (Kernel Mode) ======================= | | 6. 内核收到 sigreturn 系统调用 | 7. 内核从用户栈顶读取刚才保存的 Signal Frame 结构体, | 原封不动地将里面的数据还原写回 CPU 硬件物理寄存器! v ======================= 恢复用户态空间 (User Mode) ======================= | v 程序在最初被打断的精确点继续平稳运行

二、 SROP 的攻击本质:伪造内核的记忆

仔细审视上述时序中的第 6 步与第 7 步,系统底层隐藏着一个致命的信任假设:

“Linux 内核在执行sigreturn系统调用时,它无法区分当前用户栈顶上的 Signal Frame 结构体,到底是由内核自己在之前合法保存的,还是由攻击者利用栈溢出漏洞人工在内存里伪造的一段静态数据!”

这意味着:

  1. 攻击者只需要利用栈溢出,在栈上按照内核ucontext_t的标准结构体格式,手工填好每一个字段:
    • 将RDI填入"/bin/sh"字符串的地址;
    • 将RAX填入59(x86-64 下execve的系统调用号);
    • 将RSI填入0,RDX填入0;
    • 将RIP填入syscall指令的物理内存地址!
  2. 随后,攻击者只需要通过某种方式将RAX赋值为15(x86-64 下__NR_rt_sigreturn的系统调用号),并触发一次syscall;
  3. Linux 内核收到sigreturn系统调用后,会天真地将攻击者在栈上伪造的这组虚假数据,以绝对信任的态度一次性全部刷入物理 CPU 的所有物理寄存器;
  4. 当内核返回用户态的一瞬间,物理 CPU 的RAX变成了 59,RDI变成了参数指针,RIP变成了syscall指令——execve("/bin/sh", 0, 0)瞬间触发,直接一步登天拿到 Shell!

三、 x86-64 架构下 Signal Frame 内存对齐实战

在 64 位 Linux(x86-64)环境下,内核维护的sigcontext结构体排布具有严格的字节对齐格式。借助 Python 的pwntools库,我们可以清晰看到这一结构的装配过程:

from pwn import * # 配置目标架构 context.arch = 'amd64' # 假设通过信息泄露或静态反编译已知如下地址 syscall_ret_gadget = 0x401186 # syscall; ret 指令地址 bin_sh_addr = 0x404050 # 存放在 .bss 段的 "/bin/sh" 地址 # 1. 初始化伪造的 Signal Frame frame = SigreturnFrame() # 2. 随心所欲配置任何想要的目标寄存器状态! frame.rax = 59 # __NR_execve 系统调用号 frame.rdi = bin_sh_addr # 第一个参数: filename = "/bin/sh" frame.rsi = 0 # 第二个参数: argv = NULL frame.rdx = 0 # 第三个参数: envp = NULL frame.rip = syscall_ret_gadget # 还原后执行的第一条指令地址: 直接执行 syscall! # 3. 构造溢出 Payload # 假设栈溢出填充偏移为 40 字节 payload = b"A" * 40 # 触发 sigreturn: 在 x86-64 下需要让 RAX=15 并执行 syscall # 某些精简程序恰好有可控的 read() 返回值可以控制 RAX=15,或者有专门的 pop rax payload += p64(pop_rax_syscall_gadget) payload += p64(15) # RAX = 15 (__NR_rt_sigreturn) payload += p64(syscall_ret_gadget) # 紧跟伪造的完整 Signal Frame (长约 248 字节) payload += bytes(frame) print(f"[+] 构造完成的 SROP 攻击载荷长度: {len(payload)} 字节")

在执行完这段代码后,原本极其复杂的“多步 Gadget 寻找传参”被完全降维成了一次静态结构体的内存平铺,彻底突破了可用 Gadget 匮乏的极端限制。


四、 SROP 的衍生杀招与万能用途

SROP 不仅仅可以用来拉起execve,在复杂渗透场景中它还是最完美的**“执行环境任意重塑器”**:

  1. 突破 NX:动态修改内存页权限(mprotect)
    攻击者利用 SROP 伪造RAX=10(__NR_mprotect),RDI=目标内存页基址,RSI=0x1000,RDX=7(PROT_READ | PROT_WRITE | PROT_EXEC),执行完毕后直接将原本不可执行的栈或堆内存修改为可读写执行(RWE),随后平滑跳转到栈上执行 Shellcode!
  2. 重定向执行栈(Stack Pivoting)
    在伪造的 Frame 中,攻击者可以直接修改RSP指针,将其引向任意由攻击者控制的内存区域(如堆空间或全局数据区),从而彻底摆脱原始函数局部栈溢出空间狭小的限制。

五、 防御视角的深入思考:内核是如何扼杀 SROP 的?

SROP 的强大源于其利用了操作系统自身合法的系统调用机制,普通的静态签名防护对它完全无效。

现代操作系统与编译器通过如下协同机制实施免疫加固:

  1. VDSO 与 ASLR 随机化:
    在现代 Linux 发行版中,内核不再将sigreturn代码散落在固定的绝对内存地址,而是将其置于动态映射的虚拟系统调用页(VDSO)中,并随系统级 ASLR(randomize_va_space=2)进行高位熵随机化。攻击者在不知道基址的前提下,无法盲跳进入sigreturn;
  2. 引入 Signal Frame 校验 Cookie(内核安全加固):
    在最新的 Linux 内核安全补丁提案中,内核在压入 Signal Frame 时,会向结构体中插入一个由当前线程特有的随机密钥哈希生成的不可预测 Cookie。当sys_rt_sigreturn被调用时,内核会强制校验该 Cookie 的完整性,一旦发现与保存时不匹配,立即认定为伪造数据并强行杀死进程;
  3. 严格开启 CFI(控制流完整性)与 CET 影子栈:
    硬件影子栈(Shadow Stack)只记录正常的函数call/ret。当程序试图通过篡改sigreturn强行改写硬件RIP时,硬件层面的状态不一致会立刻拉响警报。

六、 结语

从基础 ROP 到进阶 SROP,二进制攻防的演进清晰地折射出系统底层安全博弈的核心逻辑——“攻击者永远在寻找系统设计中那些未经验证的信任假设”。SROP 将对应用层代码碎片的利用升维到了对操作系统内核信号机制的借力打力,其构思之精妙令人叹为观止。而深入理解这一底层机制,正是为了让防守方看清软硬件协同防御的深水区:唯有在操作系统内核与硬件微架构源头消除盲目信任,方能真正构筑起坚不可摧的现代内存安全底座。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询