2020 DEF CON quals 小记

比赛时间: 5月16日-5月18日

终于,又一次来到了defcon的预选赛.仍然记得去年自闭了两天只做出来一道misc(锤地).今年仍然是令人自闭的一年.经过两晚奋战到四点的努力,总算做出3道(其实是2道)题.自然收获满满,也学到了很多有意思的东西.

RE

fountain-ooo-reliving

We have found the fountain OOO RElive. By discovering its secrets, you will restart the game of life with a chance to do it all over again. This challenge is in memory of John Conway (26 December 1937 – 11 April 2020).

Files:

文件下载下来是个文本,几万行:

1
2
3
4
5
6
7
8
9
10
11
[M2]
# GOOOlly its the Fountain OOO REliving
$$$$$$....**$....*$
.****$*...*$....*$*..*$$$.*.....*$*.*...*$
.....*$......*$.......*$
*..*..*$*...*.*$.....*$
4 1 2 3 4
$$$$$$.....*$*...*.*$
$$$$$$...*$..*.*$
.*..*..*$..*.*$...*$
# ......

一开始我以为是misc,但这道题的分类是reverse

于是开始找资料吧.切入点应该在第一行的[M2]上,以及题目的描述里.根据描述,这道题跟生命游戏(game of life)有关.经过各种关键词的谷歌,找到了一篇文章: Golly help: File format,文中描述的Macrocell format和题目的文件一致.

康威生命游戏

康威生命游戏(元胞自动机)具体是什么呢?可以参考几个wiki:

https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life

https://www.conwaylife.com/wiki/Main_Page

总的来讲每个像素点根据周围格子的像素点的情况,会消亡/生存/繁殖,从而在时间维度上产生一系列变化.值得注意的是,元胞自动机是图灵完备的!

另外之前的De1CTF中,队友SJoshua出过一道生命游戏的题,所以还算有一点点了解.

一开始我还想手动画,后来impakho提醒Golly工具可以直接画出来并运行生命游戏(捂脸

Quest For Tetris Assembly

用Golly打开之后我惊呆了,这竟然是一颗CPU!

01

放大看看这些东西:

左边标注了ROM,每一个格子都存放了一个bit的数据.

02

这里有ALU,可以看到9种opcode进入ALU进行运算

03

右上方是RAM,每一个小格子里面是一个bit,一共是72*16,一个地址是16bit的宽度.

04

中间是总线,连接着RAM,ROM,和ALU.

05

如果继续放大,图里的每一个像素点并不是一个点,而是这样的结构,以开关ON这里为例:

06

继续放大:

07

每个单元中存放着大量的信息,有这样的最小单元携带.

我们让这个自动机动起来:

08

宏观上看,并且加快速度,8^5速度下,这些单元携带着数据动了起来!

09

先来到ROM取指令

10
11

仔细看的话根据ROM中的结构,走的“路径”是不一样的,3*4这种结构不会“右拐”,另外一种像横着的“中”字的结构会“右转”.

不过像这样的CPU,其实是能找到一样的资料的(感觉OOO也不会专门写这么一个东西过来,应该是有完整的实现再改改).impakho师傅最终找到这篇文章:Build a working game of Tetris in Conway's Game of Life,这里详细的介绍了这个的实现过程.

同时还找到了这种汇编语言的解释器:QFTASM Interpreter.根据解释器和上面的文章,再加自己的调试,大概搞清楚了它在做什么.

首先依次取出每一条指令,根据图中标记的结构解析出opcode, arg 1 val, arg 1 mode, arg 2 val, arg2 mode, arg3 val, arg 3 mode.取指令的时候,根据ROM中的结构区分0和1.以opcode这里为例:

12

这里就代表着0001,1号指令对应的是MLZ.在QFTASM Interpreter这篇文章里还介绍了整个架构.MLZ指令的意思是小于零则移动.

Documentation

QFTASM is a very simple version of Assembly developed for the Quest For Tetris project. To use this interpreter, put your code in the text box and click Set code to enable the flow control buttons. Run will execute the instructions as quickly as possible, Step will execute one instruction at a time, and Slow will execute instructions at a user-defined rate.

Each instruction should be formatted like this: [number]: [INST] [arg1] [arg2] [arg3]. Optional comments are also allowed, and are denoted by a semicolon. Here is a list of instructions along with their arguments:

  • MNZ [test] [value] [dest] – Move if not zero; sets [dest] to [value] if [test] is not zero.
  • MLZ [test] [value] [dest] – Move if less than zero; sets [dest] to [value] if [test] is less than zero.
  • ADD [val1] [val2] [dest] – Add; adds [val1] to [val2] and stores the result in [dest].
  • SUB [val1] [val2] [dest] – Subtract; subtracts [val2] from [val1] and stores the result in [dest].
  • AND [val1] [val2] [dest] – Bitwise AND; bitwise ANDs together [val1] and [val2] and stores the result in [dest].
  • OR [val1] [val2] [dest] – Bitwise OR; bitwise ORs together [val1] and [val2] and stores the result in [dest].
  • XOR [val1] [val2] [dest] – Bitwise XOR; bitwise XORs together [val1] and [val2] and stores the result in [dest].
  • ANT [val1] [val2] [dest] – Bitwise AND-NOT; bitwise ANDs together [val1] and (NOT [val2]) and stores the result in [dest].
  • SL [val1] [val2] [dest] – Shift left; shifts [val1] left by [val2] bits and stores the result in [dest].
  • SRL [val1] [val2] [dest] – Shift right (logical); shifts [val1] right by [val2] bits and stores the result in [dest]. Doesn't preserve sign.
  • SRA [val1] [val2] [dest] – Shift right (arithmetic); shifts [val1] right by [val2] bits and stores the result in [dest], while preserving sign.

There are two formats for arguments: literal integers and references. Integers may either positive or negative, with the negative ones converted to two's complement (for example, -37 becomes 65573), and they are limited to the range 0–65535 (16 bits). For references, the number of dereferences is notated by one of A, B, C. For instance, A37 will be replaced with the data in address 37 at execution time. B37 will be replaced with the data in the address given by the data in address 37, and likewise, with one more level, for C37.

The first table shows the instructions and their machine code equivalents. In the ASM column is the code exactly as it appears in the input, sans comment. In the Opcode column is the code for that command. Type refers to the reference level, so 37 would have a type of 00 and A37 would have a type of 01. Finally, Location is just the value or address. Also, the highlights show the next two instructions to be executed.

The second table shows the RAM bank, and I think the columns are pretty self-explanatory. More RAM slots are added as needed (when an instruction reads from or writes to an address outside of the current range).

The three text boxes directly below can be used to set breakpoints. Simply enter addresses separated by commas and/or spaces. Any of the following will work: '1,2,3', '1, 2, 3', '1 2 3'.

Regarding the direct write to RAM line, that address will be set to that value when that address is read. Once that address is set, then the value box is blanked. Further reads of the same address will not do anything if the value box is blank. The accompanying checkbox is self-explanatory, with the caveat that if a value is entered and then the user resumes the interpreter, that address will be set to that value the next time it is read.

For specifying which addresses to display, the format should be something like 0 2-4 6. Ranges of numbers are denoted by a-b (inclusive), with no spaces on either side of the dash. Separate numbers and ranges by commas and/or spaces.

为验证我们的猜想,不一会我们便能在ALU附近看到一颗元胞(我不知道该怎么称呼这样的单元,电子吗?)来到了MLZ处(gif比较快,注意观察MLZ左侧的几个像素):

13

根据文档,RAM的0地址是PC,执行一条指令后会加一,我们观察RAM的0地址,不一会便会发现写1的操作:

图中从左往右的是读,从下往上的是写.可以看到最上方的bit被写为了1.

14

第一条指令MLZ -1 44 43的意思是将立即数44写进43地址,我们观察43地址,确实存放0x2C:

15

当然,让GOLLY直接Generate的确能跑完整个程序,但是太慢了,而且查看内存也极其不方便.

观察一下ROM部分,由于0/1的差别比较大,可以截图下来对图像处理,写个脚本提取出ROM:

提取ROM并求解

下方密恐慎入.

我们对一些特定的像素点进行识别,提取出01数据.再按照文档解析成指令,同时可以写个简单的解释器解释执行:

rom

⚠️:此脚本只对本图有效

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
from PIL import Image, ImageDraw
from struct import pack, unpack
img = Image.open("./rom.png")
pixel = img.load()
height = 1270
width = 2554
inst = []
for x in range(2549, 0, -22):
instruction = ""
for y in range(1261, 0, -22):
pix = pixel[x, y]
if pix[0]<=128:
instruction+= "1"
else:
instruction+= "0"
instruction = instruction[::-1]
inst.append(instruction)
# print(instruction)

op = {0:"MNZ", 1:"MLZ", 2:"ADD", 3:"SUB", 4:"AND", 5:"OR", 6:"XOR", 7:"ANT", 8:"SL", 9:"SRL"}
mode = {0:"", 1:"A", 2:"B", 3:"C"}


for i in range(len(inst)):
assert len(inst[i]) == 58
arg3_mode = int(inst[i][0:2], 2)
arg3_val = int(inst[i][2:18], 2)
arg3_val = unpack("<h", pack("<H", arg3_val))[0]
arg2_mode = int(inst[i][18:20], 2)
arg2_val = int(inst[i][20:36], 2)
arg2_val = unpack("<h", pack("<H", arg2_val))[0]
arg1_mode = int(inst[i][36:38], 2)
arg1_val = int(inst[i][38:54], 2)
arg1_val = unpack("<h", pack("<H", arg1_val))[0]
opcode = int(inst[i][54:58], 2)
s = "%d. %s %s%d %s%d %s%d"%(i, op[opcode], mode[arg1_mode], arg1_val, mode[arg2_mode], arg2_val, mode[arg3_mode], arg3_val)
print(s)
exit()
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
0. MLZ -1 44 43
1. XOR 0 0 2
2. MLZ -1 25971 2
3. MLZ -1 14554 3
4. MLZ -1 22445 4
5. MLZ -1 25411 5
6. MLZ -1 3743 6
7. MLZ -1 13391 7
8. MLZ -1 12059 8
9. MLZ -1 2554 9
10. MLZ -1 15823 10
11. MLZ -1 5921 11
12. MLZ -1 18009 12
13. MLZ -1 14823 13
14. MLZ -1 4757 14
15. MLZ -1 7754 15
16. MLZ -1 22480 16
17. MLZ -1 8371 17
18. MLZ -1 12418 18
19. MLZ -1 22738 19
20. MLZ -1 16499 20
21. MLZ -1 7132 21
22. MLZ -1 22793 22
23. MLZ -1 22307 23
24. MLZ -1 12485 24
25. MLZ -1 7936 25
26. MLZ -1 26630 26
27. MLZ -1 15483 27
28. MLZ -1 6471 28
29. MLZ -1 1806 29
30. MLZ -1 22705 30
31. MLZ -1 25019 31
32. MLZ -1 16442 32
33. MLZ -1 5145 33
34. MLZ -1 15593 34
35. MLZ -1 23867 35
36. MLZ -1 23738 36
37. MLZ -1 14086 37
38. MLZ -1 23123 38
39. MLZ -1 0 39
40. XOR A1 -27179 39
41. SUB A39 A2 2
42. XOR A1 -14018 39
43. SUB A39 A3 3
44. XOR A1 -22549 39
45. SUB A39 A4 4
46. XOR A1 -27735 39
47. SUB A39 A5 5
48. XOR A1 -225 39
49. SUB A39 A6 6
50. XOR A1 -15190 39
51. SUB A39 A7 7
52. XOR A1 -8339 39
53. SUB A39 A8 8
54. XOR A1 -1415 39
55. SUB A39 A9 9
56. XOR A1 -12768 39
57. SUB A39 A10 10
58. XOR A1 -6243 39
59. SUB A39 A11 11
60. XOR A1 -18725 39
61. SUB A39 A12 12
62. XOR A1 -13743 39
63. SUB A39 A13 13
64. XOR A1 -7402 39
65. SUB A39 A14 14
66. XOR A1 -4444 39
67. SUB A39 A15 15
68. XOR A1 -22495 39
69. SUB A39 A16 16
70. XOR A1 -12017 39
71. SUB A39 A17 17
72. XOR A1 -16138 39
73. SUB A39 A18 18
74. XOR A1 -22234 39
75. SUB A39 A19 19
76. XOR A1 -20283 39
77. SUB A39 A20 20
78. XOR A1 -5054 39
79. SUB A39 A21 21
80. XOR A1 -22161 39
81. SUB A39 A22 22
82. XOR A1 -22641 39
83. SUB A39 A23 23
84. XOR A1 -16096 39
85. SUB A39 A24 24
86. XOR A1 -4238 39
87. SUB A39 A25 25
88. XOR A1 -26510 39
89. SUB A39 A26 26
90. XOR A1 -13059 39
91. SUB A39 A27 27
92. XOR A1 -5726 39
93. SUB A39 A28 28
94. XOR A1 -2182 39
95. SUB A39 A29 29
96. XOR A1 -22211 39
97. SUB A39 A30 30
98. XOR A1 -28099 39
99. SUB A39 A31 31
100. XOR A1 -20296 39
101. SUB A39 A32 32
102. XOR A1 -7012 39
103. SUB A39 A33 33
104. XOR A1 -12961 39
105. SUB A39 A34 34
106. XOR A1 -21059 39
107. SUB A39 A35 35
108. XOR A1 -21210 39
109. SUB A39 A36 36
110. XOR A1 -14493 39
111. SUB A39 A37 37
112. XOR A1 -21817 39
113. SUB A39 A38 38
114. MLZ -1 -2 0
115. MLZ 0 0 0

整个代码非常简单,只是在做一些异或和减法运算.

丢进在线的解释器可以调试,经过一通调试,发现RAM中的结果有些不同.

最后发现RAM1地址是有东西的,这里好坑.

最终得到的结果并不是flag.似乎整个程序也是在做固定的工作,每次运行的结果都一样,那么要怎么得到flag呢?

回过头来看一下题目描述:

By discovering its secrets, you will restart the game of life with a chance to do it all over again.

找到什么secrets,再运行一遍就得到flag了.结合RAM1地址是有东西的,如果我们对他进行一些修改,计算出的结果也是不一样的.是否这个数据就是所谓的secrets.在上面的脚本基础上修改一下,爆破1地址的内容:

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
from PIL import Image, ImageDraw
from struct import pack, unpack
img = Image.open("./rom.png")
pixel = img.load()
height = 1270
width = 2554
inst = []
for x in range(2549, 0, -22):
instruction = ""
for y in range(1261, 0, -22):
pix = pixel[x, y]
if pix[0]<=128:
instruction+= "1"
else:
instruction+= "0"
instruction = instruction[::-1]
inst.append(instruction)
# print(instruction)

op = {0:"MNZ", 1:"MLZ", 2:"ADD", 3:"SUB", 4:"AND", 5:"OR", 6:"XOR", 7:"ANT", 8:"SL", 9:"SRL"}
mode = {0:"", 1:"A", 2:"B", 3:"C"}

for k in range(60000,65535):
asm = []
addr = [0 for i in range(50)]
addr[1] = k
for i in range(len(inst)):
assert len(inst[i]) == 58
arg3_mode = int(inst[i][0:2], 2)
arg3_val = int(inst[i][2:18], 2)
arg3_val = unpack("<h", pack("<H", arg3_val))[0]
arg2_mode = int(inst[i][18:20], 2)
arg2_val = int(inst[i][20:36], 2)
arg2_val = unpack("<h", pack("<H", arg2_val))[0]
arg1_mode = int(inst[i][36:38], 2)
arg1_val = int(inst[i][38:54], 2)
arg1_val = unpack("<h", pack("<H", arg1_val))[0]
opcode = int(inst[i][54:58], 2)
s = "%d. %s %s%d %s%d %s%d"%(i, op[opcode], mode[arg1_mode], arg1_val, mode[arg2_mode], arg2_val, mode[arg3_mode], arg3_val)
# print(s)
if opcode == 1:
addr[arg3_val] = arg2_val
elif opcode == 6:
if arg1_mode == 0:
addr[arg3_val] = (arg1_val ^ arg2_val) & 0xffff
elif arg1_mode == 1:
addr[arg3_val] = (addr[arg1_val] ^ arg2_val) & 0xffff
elif opcode == 3:
if arg1_mode == 1:
addr[arg3_val] = (addr[arg1_val] - addr[arg2_val]) & 0xffff
p = True
for i in addr[2:39]:
if i<0x20 or i >= 0x7f7f:
p = False
if p == True:
print(k)
print(bytes(addr[2:39]))

最终在61463的地方发现了flag:

1
2
61463
b'OOO{in_this_life___youre_on_your_own}'

BabyMaze & MamaMaze

The flag is on the wings of the flying plane. It is possible to get the flag without instrumenting or modifying the binary. Tested on Ubuntu 16.04, Ubuntu 18.04, and Ubuntu 20.04. You should use a system able to run the game at approximately 60 fps. It requires: sudo apt-get install freeglut3

  • babymaze.challenges.ooo 7777

Files:

  • BabyMaze 4d7aabfb62d46ea98379254a62023f178c1922c0908f3ff19634ca8c22f97cef

MamaMaze和BabyMaze没太大区别,只是第二张地图改的更大了.我这里的做法都一样.

迷宫游戏

按照题目描述,这是个迷宫挑战,需要freeglut.看到这个我就回想起了上学期的计算机图形学以及OpenGL...

运行一下是个3D的迷宫游戏,应该是用OpenGL实现的:

16

运行的时候命令行参数要附带题目中的IP和PORT

摸索一下操作,WASD移动,R重置,Q退出,ZX左右旋转视角.鼠标也能切换事业,但是可能代码有问题,不是正常的第一人称视角那种切换,而是根据鼠标位置上下左右移动,操作极其反人类...

Connecting to babymaze.challenges.ooo 7777... Connected! Solving POW... done!

Hello! Welcome to BabyMaze Press Q to quit

First of all, you have to find the computer!

根据命令行的提示,先要找到一个computer

玩一下很快就能找到第一个目标(这条路我闭着眼睛都快能走了,谁能想象这两天我调试了多少次...

17

到达后:

Good job, you found the computer! Now select your next maze (use your keyboard)

键盘选择下一个地图,只有1和2可用.我测试一直用的1

1 Now find the yellow box, you have 8 seconds! Too Slow!

需要在8秒之内找到yellow box,这是这道题的核心,也是卡了我一天的地方...

摸清功能,接下来开始逆向

主程序分析

fork 主进程

fork之后,主进程校验环境变量,不能有LD_开头.

之后连接远程服务器,发送1,经过POW后,服务器会回一些数据和checksum.

接下来通过ptrace调试子进程,只接受int 3断点(wstatus的高8bit的值为5)

根据触发断点的次数,有不同的行为,一共捕获6次断点.

19

fork 子进程

fork之后,子进程读取文件中第二个ELF头,解密elf,新建memfd后拷贝过去,然后execv运行该elf,同时命令行参数为主进程的一个地址93824994345264(为了方便调试,我把ALSR关闭了,每次这个地址相同).这个地址初始存的是0.

18

运行后可以把/proc/pid/exe拷贝出来,获取子进程执行的elf.

游戏进程是实际进行迷宫游戏的部分,具体是用OpenGL实现的,如果写过OpenGL的话会对这里比较熟悉.

可以直接调试这个elf,注意要正确的传递参数.全局搜int 3可以找到断点在哪里.也可以调试起来触发trap信号.

整体的思路其实比较简单,找到8秒限制的位置,想办法破除.但实际上操作起来比较麻烦,需要找到合适方法.

先理一边游戏流程,找到合适突破口,我们可以调试主进程+调试游戏进程,来搞清楚整个程序在做什么.

breakpoint1

先调试主进程,在wait后下断,查看每一次断点的捕获:

第一次断下来后,使用PRACE_PEEKDATA,对0x401000-0x413000的地方做循环异或,校验此处的结果,存进check_sum1.check_sum1在后面用到.显然这里是阻止我们patch游戏进程.

然后从0x401000开始遍历,找到第一个全是nop的地方,我们通过调试可以拿到这个地址是405DEE,然后把这里开始的0xd0个数据patch成之前服务器发送的数据.

20

所以第一个断点作用是校验子进程401000-413000的完整性,并修改代码.

breakpoint2

继续调试,触发第二个breakpoint.

先从0x610000开始搜索0xFFEEDDCCBBAA0099,搜到的地址存下来做key2_addr.经过调试/静态搜索不难发现是613498.613490这个地址开始的16个字节的数据非常关键,是一个解密密钥.整个程序都在围绕这个解密密钥进行.初始化为11223344556677889900aabbccddeeff.

22

然后这里又一次对401000-413000的数据进行校验.注意此时一部分代码已被修改,校验的是修改完的代码.然后将key的后8字节修改成checksum1 ^ checksum2 ^ key2.

21

breakpoint3 && breakpoint4

触发第三个断点时,创建一个线程执行线程函数:

23

线程函数:

这里修改一个变量,然后sleep10秒,然后判定pid1 和 pid2是否相等(在检验完环境变量后会将这两个变量都赋值为子进程pid),相等会kill掉子进程

24

对pid2查找引用,在第四个断点用到:

第四次触发断点时,会将pid2改为-1.也就是说,我们必须要在10秒内触发第4个断点,不然直接kill.这里是一处时间校验.

25

breakpoint5

第五个断点,先对main开始的数据做校验.结束的条件是指针的内容和unk_555555758130一样.而这里开始存的内容是0x1111111122222222.代码段中并没有0x1111111122222222的数据,光看这里指针p指向的内容永远不会为0x1111111122222222,最终跑到当前Segment以外,然后触发异常gg.

当然程序能够正常运行,所以还有其他的操作,我们后面再看(如果对unk_555555758130查找引用,会发现子进程的命令行参数刚好是这个地址,所以可以猜想在子进程里对这个数据做了一些事情)

26

校验完主进程的数据后,又一次连接远程服务器,发送2,然后POW,然后peak子进程中的16字节的key,发送给服务器,服务器会16个字节,在poke道子进程上.(注意这里goto LABEL_37后还有ptrace,所以16个字节都写进去了)

27

breakpoint6

第6次断点,获取key的前8字节,乘以mul_value后写回去,然后后面又peek key中的16字节,只是peek并没有poke.

28

迷宫程序分析

dump下来exe后,可以分析迷宫程序了.

可以结合调试分析.注意breakpoint1中修改了游戏进程的代码,所以运行到第一个breakpoint时要手动修改(或者提前patch好)

如果熟悉OpenGL的话,再逆向可能会轻松一点,实际影响不太大.整个程序比较大,有些细节没有逆完,一边逆一边遇到问题一边总结.

整个都逆下来显然工作量太大了,我们只能调重点的地方逆

freeglut使用glutKeyboardFunc函数设置键盘回调函数,可以看到按下不同按键的操作,只是将按键暂存起来.同时发现还有几个按键,h, p, f, v.可以进入游戏看看这些的功能,f和v暂时用不到,h是记录历史,p是查看历史.

glutIdleFunc设置Idle函数.具体的细节忘了,大概是每绘制一帧的时候都会调用它.Idle函数中的

sub_406380函数是主体部分,内容非常多,包含了大部分游戏逻辑的内容.

这里判定坐标是否到了computer的位置,computer坐标为(-45, 85)

29

找到后,游戏提示选择地图.测试中只有1和2可以选.我一直选的是1.

选完1后,又fork了一个子进程,去attach主进程(pid在最开头通过getppid获得),peek的地址是命令行参数传过来的地址,把它修改为0x9090909090909090结合上文breakpoint5中的动作,也就解释了为什么之前在breakpoint5校验main时那个不会无限往后校验.这里将变量修改为9090909090909090后,主进程check main函数时遇到9090909090909090会停下来.在main下面看一看,果然找到了一大片nop.也就是说,这里实际校验的是这一部分的内容.

30

我们运行起来,走到computer后,ps查看一下进程也能发现它的子进程:

31

由于一个进程只能有一个Tracer,所以我们调试主进程的话,子进程就不能对这里修改了.所以我们在调试的时候要手动修改.

继续往下,到字符串Too slow附近可以看到另一处时间校验:

32

这里的time_start在按下数字1后记录.

校验迷宫的逻辑:

map位于bss段,在main开头被从rdata段拷贝过来.经过改过的sha256和aes后和签名校验.

33

观察迷宫数据,不难猜出1是墙,0是走道儿:

34

如果成功抵达yellow box,就可以飞了.使用fvzx更改视野,提示flag在飞机翅膀上.

从选择迷宫开始,到飞到天上一定高度会触发几次断点,最后一次触发断点后会用之前提到的key解密一段密文,如果开头四个字符为GOOD的话说明解密正确,剩下部分会是真正flag的图片,否则显示的是一个cheater!的图片.

35

对key查找引用我们会发现有一大堆位置都在修改key:

36

其中有一个地方是线程函数,里面还有对时间的校验

由于这里是用的是系统调用获取时间,所以反编译不太好,我们看汇编代码.

37

大致逻辑是第三步之前一直sleep.第三步时,先通过系统调用获取时间,再循环睡眠.一旦时间差高于9秒,会将DBED58开始的数据无限循环异或0xffeeddcc11223344.那么必然会触发异常然后gg,除非9秒内进入第四步.

38

解决方案

我一开始就做歪了,卡了非常久.我一开始想延长8秒的时间限制,进行了大量操作patch主程序和游戏程序,最终确实可以解除8秒限制,但算出来的key总是不对.程序中有大量对key的修改,环环相扣,一个地方错了最终的key也会错.尽管我以为所有的校验都是对的,还是有些地方没处理好,导致key计算错误.

由于没有成功我就不介绍这种方法的具体操作了.

直接修改坐标

这是我解题时的做法.一开始想改时间,后面尝试了hook time函数,确实使得一个time校验失效了.但是还是计算不出来.卡了很久后想能不能hook libc后在libc里面直接改个坐标,传送到终点.

首先经过调试与分析,知道DC0588的指针指向跟位置相关的结构体.我们在调试时查看这里的数据:

39

经过测试,四组坐标都要进行修改才能真正进行移动.

那我们的目标就就是想办法运行时修改坐标.

经过一通操作,我想到可以hook 按下Q后的exit函数.不过hook exit的问题是exit是不会返回的,所以要写段shellcode返回到正确的地址,调试时计算一下偏移即可.在末尾加个类似这样的shellcode:

1
asm("leave\npop %rax\nsub $620, %rax\npush %rax\nret");

接下来的问题是,由于程序会校验LD_PRELOAD,所以要想办法绕过这个限制.

我的解决方案是,hook开头的setvbuf函数,将代码中的校验修改掉,经过检验后,在在一个合适的地方hook,把代码修改回去(过check sum).这里利用的是puts函数,在输出done!Hello!的时候hook.

然后修改一下elf的代码段权限,改成RWX(不知道为啥用mprotect失败了).

那么能hook了其实可以干很多事.比如hook ptrace来监视对key的修改等等

具体的poc如下:

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
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
#include <time.h>
#include <unistd.h>

typedef int(*Setvbuf)(FILE* , char *, int, size_t);
typedef int(*Puts)(const char*);
typedef void(*Exit)(int);

int setvbuf(FILE *stream, char *buf, int mode, size_t size) {
static void *handle = NULL;
static Setvbuf org_setvbuf = NULL;
if (!handle) {
handle = dlopen("libc.so.6", RTLD_LAZY);
org_setvbuf = (Setvbuf)dlsym(handle, "setvbuf");
}
org_setvbuf(stream, buf, mode, size);
unsigned char *p = (unsigned char *)0x000055555555596a;
void *a = (void *)0x0000555555555960;
printf("%c", *p);
*p = 'F';
p = (unsigned char *)0x00005555555550E1;
printf("%c", *p);
*p = 'F';
}

int puts(const char *s) {
static void *handle = NULL;
static Puts org_puts = NULL;
if (!handle) {
handle = dlopen("libc.so.6", RTLD_LAZY);
org_puts = (Puts)dlsym(handle, "puts");
}
if (!strcmp(s, "done!") || !(strcmp(s, "Hello!")))
{
org_puts("hack puts");
unsigned char *p = (unsigned char *)0x000055555555596a;
void *a = (void *)0x0000555555555960;
*p = 'L';
p = (unsigned char *)0x00005555555550E1;
*p = 'L';
}
return org_puts(s);
}

void exit(int status) {
static void *handle = NULL;
static Exit org_exit = NULL;
if (!handle) {
handle = dlopen("libc.so.6", RTLD_LAZY);
org_exit = (Exit)dlsym(handle, "exit");
}
if (status == 0) {
int *p = (int*)0x00000000006134C0;
printf("decrypt: %x\n", *p);

}
static int cnt = 0;
void *location = (void *)0x000000000DC0588;
double *a = *(double **)location;
//printf()
if (cnt == 0) {
a[2] = -65.0;
a[22] = -65.0;
a[30] = -65.0;
a[50] = -65.0;
a[4] = 90.0;
a[24] = 90.0;
a[32] = 90.0;
a[52] = 90.0;

}
else if (cnt == 1) {
a[2] = 120.0;
a[22] = 120.0;
a[30] = 120.0;
a[50] = 120.0;
a[4] = 140.0;
a[24] = 140.0;
a[32] = 140.0;
a[52] = 140.0;
}
cnt += 1;
asm("leave\npop %rax\nsub $620, %rax\npush %rax\nret");

}
//gcc -fPIC preload.c -shared -o preload.so -ldl
//LD_PRELOAD=./preload.so ./BabyMaze_hook babymaze.challenges.ooo 7777

另外,运行时直接修改/proc/pid/mem应该也可以修改坐标,但我还没试过.这种方法就不用管LD_PRELOAD了.

修改地图

这种方法是赛后和pizza交流,pizza用的.

之前提到,第二部分的地图会被保存到bss上,选完地图后会进行校验.如果我们直接将地图的每一个格子都改成yello box,就可以在8秒内到达了.而且bss段在程序校验的范围之外

由于有签名校验,我们可以先调试,修改地图后从程序中拿修改过的地图的签名,然后在写/proc/pid/mem.