signal

此篇blog为SROP那一章的后续,主要补充一下linux中的信号处理机制

为什么写这篇呢,因为我发现我虽然能构造SROP利用,但是却并不明白为什么能这么做

最近在学习操作系统时,学习到了这么一个知识点,进程在进行上下文切换时,也会有保存寄存器,恢复寄存器这样的一个动作,但是保存的地方是内核栈,那我为什么在用户栈布置上下文(context)时能make sense呢?

如图所示

经过查阅资料发现,原来这两样东西完全是不同的机制。

第一种情况,在进程进行上下文切换时,例如从进程A切换至进程B,进程A的寄存器此时保存在内核栈,待进程A重新被调度时,才会从内核栈恢复寄存器,重新进入进程A,此时用户不可控。

第二种情况,也就是本文的主题,即linux系统的信号处理机制

信号在硬件层面发生异常/中断时,内核会向进程发送信号,通知用户进程发生了某种事情

常见如:

  • 除数为零(异常) -> SIGFPE

  • 非法内存访问(异常) -> SIGSEGV 也就是pwn手经常看到的segmentation fault

  • 当你按下Ctrl+C时(产生键盘中断) -> SIGINT

那么,内核如何发送这个信号到用户进程呢,本质是在用户态调用了一个signal handler函数,想象一下,用户进程正在愉快地执行,此时发生了某种事件,于是内核决定发送信号,CPU此时立即进入内核态(trap),此时内核要保存被打断的寄存器,这便是第一种情况受限直接执行的一部分,但是这些寄存器只是暂时保存在内核栈,不是最终保存位置,我们接着看。

接下来,内核判断是否要处理信号,内核在一系列判断后,如果确定要发送这个signal handler,于是进入signal delivery path,接下来便是关键步骤:内核伪造用户态调用栈

内核会在用户栈上分配一个sigframe,简化后大致如下所示

1
2
3
4
5
struct rt_sigframe {
char pretcode[8];
struct ucontext uc;
struct siginfo info;
};

这些分别代表什么?我们只需要关注两样东西

首先,char pretcode[8];,这个地址通常是glibc提供的trampoline,我们不妨叫它rt_sigreturn_trampoline,指令你应该很熟悉

1
2
mov rax, SYS_rt_sigreturn  //linux x86-64下即是15
syscall

然后,ucontext见下

1
2
3
4
5
6
7
struct ucontext {
unsigned long uc_flags;
struct ucontext *uc_link;
stack_t uc_stack;
sigset_t uc_sigmask;
struct sigcontext uc_mcontext; // 想我了吗?
};

看到sigcontext了吗,这便是SROP中利用的关键,保存着所有的寄存器

代码如下

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
struct sigcontext {
unsigned long r8;
unsigned long r9;
unsigned long r10;
unsigned long r11;
unsigned long r12;
unsigned long r13;
unsigned long r14;
unsigned long r15;
unsigned long rdi;
unsigned long rsi;
unsigned long rbp;
unsigned long rbx;
unsigned long rdx;
unsigned long rax;
unsigned long rcx;
unsigned long rsp;
unsigned long rip;
unsigned long eflags;
unsigned short cs;
unsigned short gs;
unsigned short fs;
unsigned short __pad0;
unsigned long err;
unsigned long trapno;
unsigned long oldmask;
unsigned long cr2;
struct _fpstate *fpstate;
unsigned long reserved1[8];
};

内核在用户栈伪造了这个sigframe后将rip设置为handler(即原执行流变为执行信号处理函数),将rsp设置为rt_sigreturn_trampoline,在执行完信号处理函数后,因为handler函数的最后一条指令通常是ret,因此,pop rip,执行rt_sigreturn_trampoline

系统再次进入内核态,无条件地信任它自己在用户栈设置的sigframe,从sigcontext中恢复所有保存的寄存器,程序恢复至被中断前的执行点

总结一下全过程:

1.用户代码正常运行,突然,发生程序异常/硬件中断等”事故”

2.CPU进入内核态并保存寄存器到pt_regs(内核栈中的一个结构体,保存关键寄存器)

3.内核决定要运行handler:在用户栈写rt_sigframe(把pt_regs的快照写到uc_mcontext),然后修改pt_regs使得rip=handler,rsp=rt_sigframe, 并设置handler参数寄存器

4.内核执行return-to-user(iretq),CPU恢复并开始执行handler(看起来像是normal call)

5.handler执行完,ret,返回到trampolinetrampolinesyscall SYS_rt_sigreturn

6.内核进入sys_rt_sigreturn:copy_from_userrt_sigframesigcontext保存的寄存器读回,写入 pt_regs,恢复信号

7.内核再次return-to-user(iretq),CPU将pt_regs中的所有寄存器恢复到硬件,程序继续被中断前的执行点

至此,终于理解了SROP技术的本质:在用户栈上伪造一次“合法的信号处理返回现场”,然后诱骗内核执行rt_sigreturn(即想办法设置rax为15并syscall),让内核按攻击者提供的上下文(sigcontext)恢复寄存器,彻底接管执行流!

感谢阅读!


signal
https://roxy5201314.github.io/2026/01/15/signal/
作者
roxy
发布于
2026年1月15日
更新于
2026年1月29日
许可协议