【wp】2022D^3CTF

上周末因为各种原因没怎么打hhh,唯一在有空嗑的题都是赛后一个小时内解出来的🤣(没看wp,瞄了一眼解题人数好像是简单题xd。

薛定谔的音游题复现,咕——

Reverse

d3w0w

这是一个32位切64位运行的混淆,毕竟64位操作系统兼容32位,但是IDA(包括大部分反汇编器)只能运行单一模式,32或者64(

主函数看到逻辑很简单(原题目无符号,重命名了一下),主要分析check1和check2两个函数,要让两个函数都返回0才会输出win。

check1逻辑很明显啦~ 改了一下a2的数据类型让它更好看一点,光标点到数字上按r可以让数字转成字符串(比如1952658276 -> 'tc3d')。

v5是x,v4是y,可以看到这是个用a1(即flag)初始化a2(即map)的过程,开头检查了flag的开头和结尾分别是d3ctf{2},中间的数据只能是1234四个字符中的任何一个。

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
int __cdecl check1(int a1, _DWORD *a2)
{
int v3; // [esp+0h] [ebp-10h]
int v4; // [esp+8h] [ebp-8h]
int v5; // [esp+Ch] [ebp-4h]

v5 = 0;
v4 = 0;
v3 = 6;
if ( *(_DWORD *)a1 != 'tc3d' )
return 1;
if ( *(_WORD *)(a1 + 4) != '{f' )
return 1;
if ( *(_BYTE *)(a1 + 6) != '2' )
return 1;
while ( *(_BYTE *)(v3 + a1) != '}' )
{
switch ( *(_BYTE *)(v3 + a1) )
{
case '1':
a2[6 * v5 + v4] |= 8u;
a2[6 * --v5 + v4] |= 2u;
goto LABEL_14;
case '2':
a2[6 * v5 + v4] |= 2u;
a2[6 * ++v5 + v4] |= 8u;
goto LABEL_14;
case '3':
a2[6 * v5 + v4] |= 4u;
a2[6 * v5 - 1 + v4--] |= 1u;
goto LABEL_14;
case '4':
a2[6 * v5 + v4] |= 1u;
a2[6 * v5 + 1 + v4++] |= 4u;
LABEL_14:
if ( v5 < 0 || v4 < 0 || v5 > 5 || v4 > 5 )
return 1;
++v3;
break;
default:
return 1;
}
}
return 0;
}

switch这里是从开头的2开始对map按照一定的规则进行初始化,并对x和y进行加减移动。

简单地画了一下这里的规则,更直观一点(1/2/4/8分别表示二进制的四个位,所以每个格子的数字用二进制来表示,x表任意值,即x表示0或1):

check2看起来正常实际上并不太正常,看到一堆寄存器赋值和arpl

还有最开始那个跳转到CS是0x33的retf

刚开始还以为call $+5是花指令,结果想了半天没搞明白为啥是retf到0x33(

原谅一下没接触过这种混淆的菜鸡xd

arpl为关键字之一,摸到了binary analysis - What is “overlapping instructions” obfuscation? - Reverse Engineering Stack Exchange

才知道这是一种典型的混淆,64位模式的CS正是0x33

使用32位64位交叉编码混淆来打败静态和动态分析工具这里,更清楚地说明了这种混淆技术。

知道这是混淆以后,开始摸索这种混淆的破解方式,可以把标志位改了的这种瞎操作然后直接用ida64分析,效果还不错。

也可以把其中的64位的函数dump下来直接用IDA64打开用64-mode分析。

(因为这个函数的所有操作都没有调用其他函数,而唯二调用的两个函数是用来切换32/64模式的,所以完全可以这么干2333 不然就得把其他函数也dump下来了。

1
2
3
4
5
6
7
8
start = 0x401220
end = 0x40203E
with open('./64bit.dmp', 'wb') as f:
memory = []
while start <= end:
memory.append(get_wide_byte(start))
start += 1
f.write(bytes(memory))

做出来以后无意中看到了一篇对口指南:CTF中32位程序调用64位代码的逆向方法 - 安全客,安全资讯平台

这里用的是第二种方法,在最开头按一下p生成函数。

但是反编译会看到一些内存错误(毕竟没dump那两个切换函数),需要稍微修一下

看汇编可以看到这三句是没用的,第一句和第三句是用来保存栈原来的值的,中间那句call是调用切换函数,所以完全可以全部nop掉。

另外一个调用看流程图可以知道最后全部跳到了同一个块里,这句调用切换函数的call也是没用的,nop掉。

然后把原来的伪代码窗口关掉重新分析一下(顺便重命名),注释里面写了一些各部分的意思

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
__int64 sub_0()
{
__int64 result; // rax
int sp1_x; // [rsp+0h] [rbp-A8h]
int sp2_x; // [rsp+0h] [rbp-A8h]
int check_x; // [rsp+0h] [rbp-A8h]
int sp1_y; // [rsp+4h] [rbp-A4h]
int sp2_y; // [rsp+4h] [rbp-A4h]
int check_y; // [rsp+4h] [rbp-A4h]
int i; // [rsp+8h] [rbp-A0h]
int j; // [rsp+Ch] [rbp-9Ch]
int last_x; // [rsp+10h] [rbp-98h]
int last_y; // [rsp+14h] [rbp-94h]
int k; // [rsp+18h] [rbp-90h]
int l; // [rsp+1Ch] [rbp-8Ch]
unsigned int v13; // [rsp+20h] [rbp-88h]
unsigned int v14; // [rsp+24h] [rbp-84h]
unsigned int v15; // [rsp+28h] [rbp-80h]
unsigned int v16; // [rsp+2Ch] [rbp-7Ch]
unsigned int v17; // [rsp+30h] [rbp-78h]
unsigned int v18; // [rsp+34h] [rbp-74h]
int sp1[4]; // [rsp+38h] [rbp-70h]
int sp2[10]; // [rsp+48h] [rbp-60h]
__int64 v21; // [rsp+70h] [rbp-38h]
__int64 v22; // [rsp+78h] [rbp-30h]
__int64 v23; // [rsp+80h] [rbp-28h]
__int64 v24; // [rsp+88h] [rbp-20h]
__int64 v25; // [rsp+90h] [rbp-18h]
__int64 v26; // [rsp+98h] [rbp-10h]
_DWORD *map; // [rsp+B0h] [rbp+8h]

sp1[0] = 0;
sp1[1] = 14;
sp1[2] = 20;
sp2[0] = 4;
sp2[1] = 13;
sp2[2] = 15;
sp2[3] = 21;
sp2[4] = 24;
sp2[5] = 31;
sp2[6] = 32;
sp2[7] = 41;
sp2[8] = 45;
sp2[9] = 53;

//第一个循环:检查所有格子
for ( i = 0; i < 6; ++i )
{
for ( j = 0; j < 6; ++j )
{
if ( map[6 * i + j] > 0xFu ) //每一个格子的数最大为15
return 1i64;
v13 = map[6 * i + j] % 0x10u / 8;
v21 = j;
v14 = map[6 * i + j] % 8u / 4 + v13;
v22 = j;
v15 = map[6 * i + j] % 4u / 2 + v14;
v23 = j;
if ( map[6 * i + j] % 2u + v15 > 2 ) //格子数的二进制最多只有2个’1’
return 1i64;
if ( !j && map[6 * i] % 8u / 4 ) //j==0: x0xx
return 1i64;
if ( j == 5 && map[6 * i + 5] % 2u ) //j==5: xxx0
return 1i64;
if ( !i && map[j] % 0x10u / 8 ) //i==0: 0xxx
return 1i64;
if ( i == 5 && map[j + 30] % 4u / 2 ) //i==5: xx0x
return 1i64;
}
}

//第二个循环:检查sp1特定格子
for ( k = 0; (unsigned __int64)k < 3; ++k )
{
sp1_x = sp1[k] / 10; //x是当前数的十位
sp1_y = sp1[k] % 10; //y是当前数的个位
if ( map[6 * sp1_x + sp1_y] % 0x10u / 8 && map[6 * sp1_x + sp1_y] % 4u / 2 ) //不得是1x1x
return 1i64;
if ( map[6 * sp1_x + sp1_y] % 8u / 4 && map[6 * sp1_x + sp1_y] % 2u ) //不得是x1x1
return 1i64;
v16 = map[6 * sp1_x + sp1_y] % 0x10u / 8;
v24 = sp1_y;
v17 = map[6 * sp1_x + sp1_y] % 4u / 2 + v16;
v25 = sp1_y;
v18 = map[6 * sp1_x + sp1_y] % 2u + v17;
v26 = sp1_y;
if ( map[6 * sp1_x + sp1_y] % 8u / 4 + v18 != 2 ) //格子数的二进制必有2个’1’
return 1i64;
if ( map[6 * sp1_x + sp1_y] % 0x10u / 8 ) //如果当前格子为1xxx,那么上方格子与此相同
{
if ( !(map[6 * sp1_x - 6 + sp1_y] % 0x10u / 8) )
return 1i64;
}
else if ( map[6 * sp1_x + sp1_y] % 4u / 2 ) //如果当前格子为xx1x,那么下方格子与此相同
{
if ( !(map[6 * sp1_x + 6 + sp1_y] % 4u / 2) )
return 1i64;
}
else if ( map[6 * sp1_x + sp1_y] % 8u / 4 ) //如果当前格子为x1xx,那么左方格子与此相同
{
if ( !(map[6 * sp1_x - 1 + sp1_y] % 8u / 4) )
return 1i64;
}
else if ( map[6 * sp1_x + sp1_y] % 2u && !(map[6 * sp1_x + 1 + sp1_y] % 2u) ) //如果当前格子为xxx1,那么右方格子与此相同
{
return 1i64;
}
}

//第三个循环:检查sp2特定格子
for ( l = 0; (unsigned __int64)l < 0xA; ++l )
{
sp2_x = sp2[l] / 10;
sp2_y = sp2[l] % 10;
if ( (!(map[6 * sp2_x + sp2_y] % 0x10u / 8) || !(map[6 * sp2_x + sp2_y] % 4u / 2))
&& (!(map[6 * sp2_x + sp2_y] % 8u / 4) || !(map[6 * sp2_x + sp2_y] % 2u)) )
{ //只能是1x1x或者x1x1
return 1i64;
}
if ( map[6 * sp2_x + sp2_y] % 0x10u / 8
&& map[6 * sp2_x + sp2_y] % 4u / 2
&& !(map[6 * sp2_x - 6 + sp2_y] % 8u / 4)
&& !(map[6 * sp2_x - 6 + sp2_y] % 2u)
&& !(map[6 * sp2_x + 6 + sp2_y] % 8u / 4)
&& !(map[6 * sp2_x + 6 + sp2_y] % 2u) )
{ //不得出现:当前格子为1x1x、上方格子为x0x0、下方格子为x0x0
return 1i64;
}
if ( map[6 * sp2_x + sp2_y] % 8u / 4
&& map[6 * sp2_x + sp2_y] % 2u
&& !(map[6 * sp2_x + 1 + sp2_y] % 0x10u / 8)
&& !(map[6 * sp2_x + 1 + sp2_y] % 4u / 2)
&& !(map[6 * sp2_x - 1 + sp2_y] % 0x10u / 8)
&& !(map[6 * sp2_x - 1 + sp2_y] % 4u / 2) )
{ //不得出现:当前格子为x1x1、右方格子为0x0x、左方格子为0x0x
return 1i64;
}
}

//最后一轮check,要求按特定规则进行移动(当前位置map[check_x][check_y]),且不能走回头路(即将要移动的坐标!=map[last_x][last_y]),最后需要走回map[0][0]才能返回0
//四个if是通过map[0][0]的值决定check_x的初值,其实do-while循环可以抽象成LABEL_79,然后四个if的意思就是init check_x + 走LABEL_79
last_x = 0;
last_y = 0;
check_x = 0;
check_y = 0;
if ( *map % 0x10u / 8 )
{
check_x = -1;
do
{
LABEL_79:
if ( !(map[6 * check_x + check_y] % 0x10u / 8) || check_x - 1 == last_x && check_y == last_y )
{
if ( !(map[6 * check_x + check_y] % 4u / 2) || check_x + 1 == last_x && check_y == last_y )
{
if ( !(map[6 * check_x + check_y] % 8u / 4) || check_x == last_x && check_y - 1 == last_y )
{
if ( !(map[6 * check_x + check_y] % 2u) || check_x == last_x && check_y + 1 == last_y )
return 1i64;
last_x = check_x;
last_y = check_y++;
}
else
{
last_x = check_x;
last_y = check_y--;
}
}
else
{
last_x = check_x;
last_y = check_y;
++check_x;
}
}
else
{
last_x = check_x;
last_y = check_y;
--check_x;
}
}
while ( check_x || check_y );
result = 0i64;
}
else
{
if ( *map % 4u / 2 )
{
check_x = 1;
goto LABEL_79;
}
if ( *map % 8u / 4 )
{
check_y = -1;
goto LABEL_79;
}
if ( *map % 2u )
{
check_y = 1;
goto LABEL_79;
}
result = 1i64;
}
return result;
}

从最后的check可以看出需要走一条从map[0][0]开始的回路,并且走的方向刚好跟开头用1234初始化map的方向对应了(并且保证了不会往回走),说明每个格子里只能有两个’1’或者没有’1’(不然就交叉路了)。

这样的话sp2检查要求里的“只能是1x1x或者x1x1”其实就等同于“只能是1010或者0101”了(相当于直走),sp1检查要求里的“不得是1x1x“、”不得x1x1”等同于“不得是1010“、”不得0101”了(相当于需要拐弯)。

推的时候还有一个需要注意的地方是赋值的两个对应位是连锁的,比如上下两个格子,无论走的方向怎么样(向上or向下),上面的格子必为xx1x,下面的格子必为1xxx;而如果上面的格子是xx0x,那么下面的格子也必为0xxx。

map[0][0]由第一个循环可以推出为00xx,由sp1的要求(必有两个’1’)推出为0011,四个if里只会走check_x=1和check_y=1的if,而如果在走check_x=1的if就达成回路的话就不必走后面的。

而最开始限定了flag的开始是dectf{2,也就是从2向下走开始,也就走check_x=1。

最后就手推地图,其中紫色的数字是正推确定的,草绿色的箭头是正走推;深绿色的数字和箭头是倒推确定的。用蓝色标出了sp1的格子,用橙色标出了sp2的格子。

拿到flag:d3ctf{22441442223133324424441111133333}

(有个sp2上下格子的条件没用过就离谱x

本文作者: c10udlnk
本文链接: https://c10udlnk.top/p/wpFor-2022D3CTF/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 c10udlnk' Blog (https://c10udlnk.top)!