buu-re-73

[安洵杯 2019]game

这题主要是数独结合了ollvm混淆,题目本身的算法并不难,但是加了ollvm混淆就让分析工作量增大了不少

关于ollvm

这个ollvm混淆主要就是将程序的基本快之间的控制关系打乱,而交由统一的分发器进行管理(这个待会结合题目会讲到),实现控制流平坦化。

解决控制流平坦化混淆

有两个开源脚本工具:deflat.py(这个好像是针对x86架构下的混淆)和HexRaysDeob

《0到1》——NU1L:“这些通用的反混淆工具能解决的只是一部分标准的控制流平坦化混淆,但原版的ollvm工具在2017就停止维护了,现有的修改版ollvm大多是由个人维护的,一般会增加一些新功能,或者新的实现方式,干扰脚本分析。”

所以,做这题这两个工具我都没用上(扛着混淆硬干的555)

由于上诉原因,一般实际逆向过程中,难以指望上这些反混淆工具。较好的方法是对一些关键数据(如输入的flag)内存读取/或写入设置断点,静态和动态结合起来,找到关键的逻辑,复现出这些基本块之间真正的逻辑。

PS:分析过程中关注那个主分发器,一般是个大量出现在判断语句的变量

本题解法

首先看看main函数(大致分析变化过后的)

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
int *v4; // rsi
unsigned int i; // [rsp+2Ch] [rbp-54h]
int v7; // [rsp+38h] [rbp-48h]
char input[56]; // [rsp+40h] [rbp-40h] BYREF
int v9; // [rsp+78h] [rbp-8h]
int v10; // [rsp+7Ch] [rbp-4h]

v9 = 0;
printf("input your flag:");
gets(input);
v10 = general_inspection((int (*)[9])sudoku);
for ( i = 0x9471480F; ; i = 0xEDE5424E ) //这里的这个i就是一个主分发器
{
while ( 1 )
{
while ( i == 0x848D30C0 )
{
v7 = blank_num((int (*)[9])sudoku);
v4 = (int *)mem_alloc(v7);
trace((__int64)sudoku, v4, v7);
check((int (*)[9])sudoku); //这里的sudoku就是一个9*9的数独矩阵
check1(input);
check3(input); //两个关键函数
v9 = 0;
i = 0xEDE5424E;
}
if ( i != 0x9471480F )
break;
v3 = 0x848D30C0;
if ( v10 )
v3 = 0x27966BFF;
i = v3;
}
if ( i == 0xEDE5424E )
break;
printf("error");
check((int (*)[9])sudoku);
v9 = 0;
}
return v9;
}

从后往前,先来看看check3

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
__int64 __fastcall check3(char *input)
{
__int64 result; // rax
int v2; // eax
int v3; // [rsp+28h] [rbp-18h]
int v4; // [rsp+3Ch] [rbp-4h]

v4 = check2(input); // check2 == 1 即flag正确
v3 = 0xF607AE;
while ( 1 )
{
while ( v3 == 0xF607AE ) //这里v3是一个主分发器
{
v2 = 0x5819697A;
if ( !v4 )
v2 = 0x4BF1B86E;
v3 = v2;
}
result = (unsigned int)(v3 - 0x31271051);
if ( v3 == 0x31271051 )
break;
if ( v3 == 0x4BF1B86E )
{
v3 = 0x31271051;
printf("error!\n");
}
else
{
v3 = 0x31271051;
printf("you get it!\n"); // v3!=0x31271051 && v3!=0x4bf1b86e
}
}
return result;
}

看看check2,太长了就不全部贴出来了。。看看这个流程图吧hh

1

大概的逻辑就是,D0g3也就是那个初始未解数独,将输入的flag(check1变化后)先-48再依次填入D0g3中的非0元

再贴一下check1的关键逻辑
2

input[119] 和 input[2039]交换

3

2个一组 互相交换

4

类似异或的操作(逆向的时候重复操作就行)

分析完,贴完整脚本(解数独用网站上的就行了)

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
text = [1,        #用idapython将数独表跑出来
0,
5,
3,
2,
7,
0,
0,
8,
8,
0,
9,
0,
5,
0,
0,
2,
0,
0,
7,
0,
0,
1,
0,
5,
0,
3,
4,
9,
0,
1,
0,
0,
3,
0,
0,
0,
1,
0,
0,
7,
0,
9,
0,
6,
7,
0,
3,
2,
9,
0,
4,
8,
0,
0,
6,
0,
5,
4,
0,
8,
0,
9,
0,
0,
4,
0,
0,
1,
0,
3,
0,
0,
2,
1,
0,
3,
0,
7,
0,
4]

znum=0

for i in range(9):
for j in range(9):
print(text[i*9+j],"",end="")
if text[i*9+j]==0:
znum+=1
print()
print(znum) # znum==40,0处要填充

answer="4693641762894685722843556137219876255986"
flag=[]
for i in range(40):
flag.append(int(answer[i])+48)

for i in range(40):
flag[i] +=20
flag[i] = (flag[i] & 0xf3 | ~flag[i] & 0xc)

for i in range(20):
tmp = flag[i*2]
flag[i*2]=flag[i*2+1]
flag[i*2+1]=tmp

for i in range(20):
tmp = flag[i]
flag[i] = flag[i+20]
flag[i+20] = tmp

for i in range(40):
print(chr(flag[i]),end="")

flag{KDEEIFGKIJ@AFGEJAEF@FDKADFGIJFA@FDE@JG@J}

Comments