上周末因为各种原因没怎么打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四个字符中的任何一个。
| 12
 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;
 int v4;
 int v5;
 
 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下来了。
| 12
 3
 4
 5
 6
 7
 8
 
 | start = 0x401220end = 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掉。

然后把原来的伪代码窗口关掉重新分析一下(顺便重命名),注释里面写了一些各部分的意思
| 12
 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;
 int sp1_x;
 int sp2_x;
 int check_x;
 int sp1_y;
 int sp2_y;
 int check_y;
 int i;
 int j;
 int last_x;
 int last_y;
 int k;
 int l;
 unsigned int v13;
 unsigned int v14;
 unsigned int v15;
 unsigned int v16;
 unsigned int v17;
 unsigned int v18;
 int sp1[4];
 int sp2[10];
 __int64 v21;
 __int64 v22;
 __int64 v23;
 __int64 v24;
 __int64 v25;
 __int64 v26;
 _DWORD *map;
 
 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 )
 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 )
 return 1i64;
 if ( !j && map[6 * i] % 8u / 4 )
 return 1i64;
 if ( j == 5 && map[6 * i + 5] % 2u )
 return 1i64;
 if ( !i && map[j] % 0x10u / 8 )
 return 1i64;
 if ( i == 5 && map[j + 30] % 4u / 2 )
 return 1i64;
 }
 }
 
 
 for ( k = 0; (unsigned __int64)k < 3; ++k )
 {
 sp1_x = sp1[k] / 10;
 sp1_y = sp1[k] % 10;
 if ( map[6 * sp1_x + sp1_y] % 0x10u / 8 && map[6 * sp1_x + sp1_y] % 4u / 2 )
 return 1i64;
 if ( map[6 * sp1_x + sp1_y] % 8u / 4 && map[6 * sp1_x + sp1_y] % 2u )
 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 )
 return 1i64;
 if ( map[6 * sp1_x + sp1_y] % 0x10u / 8 )
 {
 if ( !(map[6 * sp1_x - 6 + sp1_y] % 0x10u / 8) )
 return 1i64;
 }
 else if ( map[6 * sp1_x + sp1_y] % 4u / 2 )
 {
 if ( !(map[6 * sp1_x + 6 + sp1_y] % 4u / 2) )
 return 1i64;
 }
 else if ( map[6 * sp1_x + sp1_y] % 8u / 4 )
 {
 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) )
 {
 return 1i64;
 }
 }
 
 
 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)) )
 {
 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) )
 {
 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) )
 {
 return 1i64;
 }
 }
 
 
 
 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
