上周末因为各种原因没怎么打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; 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下来了。
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; 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