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 | |
这些分别代表什么?我们只需要关注两样东西
首先,char pretcode[8];,这个地址通常是glibc提供的trampoline,我们不妨叫它rt_sigreturn_trampoline,指令你应该很熟悉
1 | |
然后,ucontext见下
1 | |
看到sigcontext了吗,这便是SROP中利用的关键,保存着所有的寄存器
代码如下
1 | |
内核在用户栈伪造了这个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,返回到trampoline,trampoline 做syscall SYS_rt_sigreturn
6.内核进入sys_rt_sigreturn:copy_from_user把rt_sigframe中sigcontext保存的寄存器读回,写入 pt_regs,恢复信号
7.内核再次return-to-user(iretq),CPU将pt_regs中的所有寄存器恢复到硬件,程序继续被中断前的执行点
至此,终于理解了SROP技术的本质:在用户栈上伪造一次“合法的信号处理返回现场”,然后诱骗内核执行rt_sigreturn(即想办法设置rax为15并syscall),让内核按攻击者提供的上下文(sigcontext)恢复寄存器,彻底接管执行流!
感谢阅读!