-
-
[原创]pediy&jd ctf_2018 之 密室逃脱 分析记录
-
发表于: 2018-11-7 16:47 4617
-
环境配置
系统 : Windows 7
程序 : Escape.rar
要求 : 输入口令
使用工具 :OD / IDA / c32asm
可在看雪论坛中查找关于此程序的讨论:传送门
开始分析
我们用ida载入cm,直接查看main函数:
int __cdecl main(int argc, const char **argv, const char **envp) { printf("Only 'a'-'z','A'-'Z','0'-'9' accepted.\n"); printf("Only 16 chars accepted.\n"); printf("Serial:"); scanf_s("%s", my_table_450B80, 17); sub_401050(); system("PAUSE"); return 0; }
进入中间那个很可疑的子函数:
int sub_401050() { // ...省略变量定义代码 do { for ( index_of_char_table = 0; ; ++index_of_char_table ) { if ( index_of_char_table >= 0x40 ) // my_char is not in char_table { printf("input error\n"); system("PAUSE"); exit(-1); } if ( char_table_40DA68[index_of_char_table] == (unsigned __int8)my_table_450B80[index] ) break; } char_index[index++] = index_of_char_table; } while ( index < 16 ); printf("\n"); index_K = 16; tmp = 16; kind_of_index = 2 - (_DWORD)&v21; do { v4 = &char_index[index_K]; res_pointer = &char_index[index_K]; index_j = 0; do { v6 = (char *)&v16; v7 = (signed int)&v4[kind_of_index]; index_i = 4; do { v8 = (v7 - 2) % 16; if ( *((_BYTE *)&v24 + index_j + v8) ) *v6++ = *((_BYTE *)&index_i + v8 + tmp); v9 = (v7 - 1) % 16; if ( *((_BYTE *)&v24 + index_j + v9) ) *v6++ = *((_BYTE *)&index_i + v9 + tmp); if ( *((_BYTE *)&v24 + index_j + v7 % 16) ) *v6++ = *((_BYTE *)&index_i + v7 % 16 + tmp); v10 = (v7 + 1) % 16; if ( *((_BYTE *)&v24 + index_j + v10) ) *v6++ = *((_BYTE *)&index_i + v10 + tmp); v7 += 4; --index_i; } while ( index_i ); *res_pointer = *(&byte_40FEF0[64 * (v17 + (v16 << 6))] + v18);// insert res from char_table_2 index_j += 16; v4 = res_pointer++ + 1; } while ( index_j < 0x100 ); kind_of_index -= 16; index_K = tmp + 16; tmp = index_K; } while ( index_K < 0x140 ); v11 = 0; do // error { if ( (unsigned __int8)res_buf[v11] != byte_40FEE0[v11] ) return printf("serial error\n"); ++v11; } while ( v11 < 16 ); do printf("%c", (unsigned __int8)char_table_40DA68[(unsigned int)(unsigned __int8)v22[v0++] >> 1]);// out put from your serial string while ( v0 < 16 ); return printf("\n"); }
经过动态调试验证,程序将我们输入的数据与程序表1对比,如果输入不在表内则退出,这是一种控制输入的手段。接着,程序将数据在程序表1中的索引插入结果表的第一行(层?)。
三重循环
接下来,程序进入三重循环,结合od分析,有一行比较关键:*res_pointer = *(&byte_40FEF0[64 * (v17 + (v16 << 6))] + v18);// insert res from char_table_2
这就是从固定表2中不断取数据插入结果表。
观察数据
以“0123456789abcdef”作为输入数据,当三重循环结束时,我们观察内存:
0012FD18 36 37 38 39 3A 3B 3C 3D 3E 3F 00 01 02 03 04 05 6789:;<=>?. 0012FD28 08 26 1E 32 2A 30 32 3D 08 29 1B 1F 15 1D 0D 3D &2*02=).= 0012FD38 0B 23 15 26 25 2F 36 2F 24 3F 3A 2C 1B 10 34 22 #&%/6/$?:,4" 0012FD48 12 03 0F 0C 21 2A 38 28 1F 21 21 08 39 2A 0E 15 .!*8(!!9* 0012FD58 11 02 17 1A 2F 27 24 34 23 05 2C 24 27 2A 0D 32 /'$4#,$'*.2 0012FD68 2D 2B 2F 3E 06 2D 1A 01 1C 0D 1A 33 2D 1E 2A 2A -+/>-.3-** 0012FD78 36 1A 1B 08 28 0C 1F 36 33 34 0B 35 03 08 13 3D 6(.6345= 0012FD88 08 2D 34 2B 24 23 1E 3C 07 1F 02 2D 04 17 24 13 -4+$#<-$ 0012FD98 08 0C 3E 1F 2C 3D 2C 2A 35 0F 36 33 08 1D 09 03 .>,=,*563. 0012FDA8 39 30 3A 24 27 2D 2E 0C 19 31 1A 1E 09 29 2B 08 90:$'-..1.)+ 0012FDB8 38 09 03 22 32 30 15 35 12 1C 37 33 27 2E 0C 1D 8."20573'.. 0012FDC8 15 2B 14 2B 11 06 2C 03 04 21 0D 28 1E 23 36 08 ++,!.(#6 0012FDD8 05 38 20 21 3B 04 05 26 0F 1E 11 2C 10 2B 05 0C 8 !;&,+. 0012FDE8 26 3C 0B 25 25 23 06 31 18 29 3C 3A 00 20 03 09 &<%%#1)<:. . 0012FDF8 0A 0D 10 25 08 2D 04 19 24 3B 2E 07 24 07 34 0F ..%-$;.$4 0012FE08 1A 24 18 3E 0E 26 08 1B 32 3F 09 2D 05 11 17 07 $>&2?.- 0012FE18 3D 19 13 10 21 03 2E 23 3C 3F 30 02 0D 1E 28 1A =!.#<?0.( 0012FE28 08 13 09 16 2F 26 12 38 05 01 01 06 27 34 1A 1F ./&8'4 0012FE38 11 30 07 3E 13 01 28 22 3F 17 20 12 21 18 23 2E 0>("? !#. 0012FE48 03 1B 29 14 20 1D 0C 3B 18 39 10 09 1F 28 02 06 ) .;9.(
总计0x140字节大小的数据,每行0x10个字节,总计0x14(20)行。
也就是说,这就是附带的解释文件里说的20层楼房了:
密室逃脱
这是一个数字迷宫 分为20层楼
16个人各选择一把初始钥匙 开始游戏
16个人进入第一层 按照一定的规则 每个人都会在另外2个人的帮助下开启一个房间 拿到1把第二层楼的钥匙
当16个人都拿到了各自的二层楼钥匙后 一起上楼
第二层重复相同的操作,16个人再一起上到第三层
...
直至16个人拿到了的20层天台的钥匙
天台上有一架直升机,需要16把正确的钥匙才能开走
试试看 你能否找出16把正确的初始钥匙
有人可能会想 只要砸碎直升机的门 不就可以把直升机开走了吗?
(那不算胜利哦)
在你开着直升机远去的时候,迷宫大楼的中间 那些曾经被你点亮的房间 会组成一个横幅
只有在你拿到了正确钥匙的情况下 才会出现成功逃脱的横幅(那才算胜利哟)
程序运行起来后,请输入16个初始钥匙
如果验证正确,最后会显示出正确的横幅
否则,算作挑战失败
我们定位到最后的判断代码处:if ( (unsigned __int8)res_buf[v11] != byte_40FEE0[v11] )
return printf("serial error\n");
这里,将最后一层的数据和固定表3对比,如果不相同则失败。
推理
从宏观的角度看,程序使用了 固定值y=F(序列号)的方式。我们想要夺旗,可分为以下三步:
- 确认三重循环的每一个步骤,接着写出其的逆算法。
- 以固定值byte_40FEE0的内容作为参数,算出前十九层的钥匙。
- 用第十九层的钥匙,作为索引,找出20层天台所对应最终密码。
最后,程序会取某一层的数据进行运算,并输出成功逃脱的横幅。
观点验证
我们结合动静态分析,开始逆向三重循环。
小技巧
调试中,可以使用一个小技巧,在od定位到迭代中经常访问的内存块,然后对于内部的小循环结束的地方下断点。设置好了之后,按下Ctrl + F8/F7,让其自动步进,而我们就可以慢慢观察内存的变化。这个小技巧对于调试加密函数很有用。
被隐藏的第五张表
我们在ida发现的四个if语句中:if ( *((_BYTE *)&v24 + index_j + _ECX) )
这样的操作全在引用一个表的内容,但作者没有将表地址直接硬编码在代码中,而是采用了数据相加再间接寻址的方式,所以这张表不是很容易找到。
内存分布
name | address | size | data |
---|---|---|---|
res_pointer | 12FD04 | 4h | ↑ |
index_i | 12FD08 | 4h | 4 -> 0 |
kind_of_index | 12FD0C | 4h | 0xFFED02DA |
index_of_table2 | 12FD10 | 4h | random value [0]->[1]->[2]->[0].. |
index_of_res | 12FD14 | 4h | 10h->20h->30h.. |
res_buf | 12FD18 | 140h | ↑ |
hidden_table | 12FE58 | 100h | / |
Char_table2 | 40FEF0 | 40000h | / |
table3 | 40FEE0 | 10h | / |
编码
我们根据分析出的线索,用c32asm在pe文件中找出Char_table2的40000h个数据。再用c++编码还原其算法:
do { size_t index_j = 0; size_t ESI = 0; do { size_t index_i = 4; do { ecx_index = ESI % 0x10; if ( hidden_table[index_j+(ecx_index%0x10)] ) rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]); ecx_index++; if ( hidden_table[index_j+(ecx_index%0x10)] ) rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]); ecx_index++; if ( hidden_table[index_j+(ecx_index%0x10)] ) rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]); ecx_index++; if ( hidden_table[index_j+(ecx_index%0x10)] ) rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]); ecx_index++; ESI += 4; --index_i; } while ( index_i ); ESI++; size_t random_num = rv.get_random_value(); *res_pointer++ = table2[random_num]; index_j += 0x10; } while ( index_j < 0x100 ); ESI = 0; index_K = tmp + 0x10; tmp = index_K; } while ( index_K < 0x140);
由上述代码可知,数据直接由random_num索引出;而同样的一个数据在256kb的数据中可能有N个索引,所以,无法由给定的表数据确认唯一的索引,这样就无法反推出逆算法。
灵感
我们无法写出逆算法,就无法得到原始数据。这个看似无解的逻辑令我困扰了一阵。
这时,我突然想到,我无法写,也不用去写逆算法,因为这个加密的逆算法可能就是其本身!再来看看被隐藏的第五张表的数据:
0012FE58 01 01 01 00 00 00 00 00 00 00 00 00 00 00 00 00 ............. 0012FE68 01 00 00 01 01 00 00 00 00 00 00 00 00 00 00 00 ............. 0012FE78 01 00 00 00 00 01 01 00 00 00 00 00 00 00 00 00 ............. 0012FE88 00 01 00 00 00 01 00 01 00 00 00 00 00 00 00 00 ............. 0012FE98 00 01 00 00 01 00 01 00 00 00 00 00 00 00 00 00 ............. 0012FEA8 00 00 01 01 00 00 00 00 01 00 00 00 00 00 00 00 ............. 0012FEB8 00 00 01 00 01 00 00 00 00 01 00 00 00 00 00 00 ............. 0012FEC8 00 00 00 01 00 00 00 01 00 00 01 00 00 00 00 00 ............. 0012FED8 00 00 00 00 00 01 00 00 01 00 00 00 01 00 00 00 ............. 0012FEE8 00 00 00 00 00 00 01 00 00 00 00 01 00 01 00 00 ............. 0012FEF8 00 00 00 00 00 00 00 01 00 00 00 00 01 01 00 00 ............. 0012FF08 00 00 00 00 00 00 00 00 00 01 00 01 00 00 01 00 ............. 0012FF18 00 00 00 00 00 00 00 00 01 00 01 00 00 00 01 00 ............. 0012FF28 00 00 00 00 00 00 00 00 00 01 01 00 00 00 00 01 ............. 0012FF38 00 00 00 00 00 00 00 00 00 00 00 01 01 00 00 01 ............. 0012FF48 00 00 00 00 00 00 00 00 00 00 00 00 00 01 01 01 .............
这似乎暗示了一个循环。。。
尝试
我们加大数组的长度,加大循环的周期,看看能不能等到原始数据的出现:
# include <iostream> # include <fstream> # include <sstream> using namespace std; void get_table2_data(unsigned char* table2) { ifstream fin; fin.open("c:\\char_table_2.data", ios::in | ios::binary); fin.read((char*)table2,0x40000); fin.close(); return ; } class random_value{ public: random_value() : mark(0) {} inline void set_value(unsigned char value) { values[mark] = value; if ( mark == 2 ) mark = 0; else mark += 1; } inline size_t get_random_value() { size_t index = 0; unsigned char A = values[1]; unsigned char B = values[2]; unsigned char C = values[0]; __asm{ push eax push ecx push edx movzx eax,A movzx ecx,B movzx edx,C shl edx,0x6 add edx,eax shl edx,0x6 lea eax,ds:[edx+ecx] mov index,eax pop edx pop ecx pop eax } return index; } private: unsigned char values[3]; int mark; }; int main(void) { unsigned char floor_table[0x14000] = { 0x14, 0x22, 0x1E, 0x10, 0x38, 0x30, 0x18, 0x10, 0x04, 0x1A, 0x24, 0x08, 0x02, 0x26, 0x38, 0x2A }; // origin unsigned char char_index[0x140] = { 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }; unsigned char hidden_table[0x100] = { 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01 }; unsigned char table2[0x40000]; get_table2_data(table2); size_t index_K = 0x10; size_t tmp = 0x10; size_t ecx_index = 0; random_value rv; unsigned char *res_pointer = floor_table+0x10; do { size_t index_j = 0; size_t ESI = 0; do { size_t index_i = 4; do { ecx_index = ESI % 0x10; if ( hidden_table[index_j+(ecx_index%0x10)] ) rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]); ecx_index++; if ( hidden_table[index_j+(ecx_index%0x10)] ) rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]); ecx_index++; if ( hidden_table[index_j+(ecx_index%0x10)] ) rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]); ecx_index++; if ( hidden_table[index_j+(ecx_index%0x10)] ) rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]); ecx_index++; ESI += 4; --index_i; } while ( index_i ); ESI++; size_t random_num = rv.get_random_value(); if ( index_j == 0 && ( *(unsigned int*)(res_pointer-0x10) == 0x101E2214 ) ){ cout << hex << res_pointer-0x10-floor_table << endl; } *res_pointer++ = table2[random_num]; index_j += 0x10; } while ( index_j < 0x100 ); ESI = 0; index_K = tmp + 0x10; tmp = index_K; } while ( index_K < 0x14000); cout << "Hello" << endl; return 0; }
在发现数据的if语句内下断点:cout << hex << res_pointer-0x10-floor_table << endl;
开始调试,果真断下了,此时res_pointer-0x140内存布局如下所示:
0011DA50 11 2B 21 05 3F 01 25 0B 39 13 33 09 3F 39 35 25 .+!.?.%.9.3 ?95% 0011DA60 20 30 32 16 32 12 06 32 3C 1A 16 18 30 22 24 1E 02.2..2<...0"$. 0011DA70 21 1F 17 03 27 2B 29 01 15 1F 1B 05 25 39 35 23 !...'+).....%95# 0011DA80 2E 22 24 0E 2A 36 00 0E 34 2A 32 00 14 20 14 08 ."$.*6..4*2.. .. 0011DA90 13 01 27 39 2F 25 1B 09 2D 13 11 1D 31 0B 37 17 ..'9/%. -...1.7. 0011DAA0 3E 0A 2A 02 2A 08 24 1E 1E 3A 38 1E 2C 2A 1C 3C >.*.*.$..:8.,*.< 0011DAB0 35 2D 31 17 3B 1D 29 03 21 0B 0B 3F 05 31 11 1D 5-1.;.).!..?.1.. 0011DAC0 16 2E 36 10 08 28 02 20 06 00 28 0E 14 0A 04 22 ..6..(. ..(...." 0011DAD0 25 05 1F 2D 27 33 19 0B 01 0D 21 31 15 31 3B 17 %..-'3....!1.1;. 0011DAE0 30 1C 28 22 24 08 22 10 00 16 10 24 0C 1C 1C 06 0.("$."....$.... 0011DAF0 27 3D 25 3F 2D 29 19 21 27 39 33 05 17 13 0D 15 '=%?-).!'93..... 0011DB00 38 1E 38 2A 0E 16 1E 2A 16 2C 1A 1E 3E 10 08 28 8.8*...*.,..>..( 0011DB10 19 2F 03 29 15 03 11 29 15 3F 33 09 1F 1D 23 33 ./.)...).?3 ..#3 0011DB20 3A 12 3C 0E 2C 3C 2E 08 1E 22 3C 12 26 18 0E 0E :.<.,<..."<.&... 0011DB30 0F 1F 33 0D 2B 2B 19 19 3F 33 35 2D 27 1B 11 07 ..3.++..?35-'... 0011DB40 0C 36 16 06 06 26 04 2E 0C 0C 3A 10 10 16 36 22 .6...&....:...6" 0011DB50 0F 07 0D 15 23 3F 3F 19 11 0D 03 25 37 3B 19 31 ....#??....%7;.1 0011DB60 3A 26 00 3A 34 06 2C 3C 0E 1E 0A 26 24 18 3C 18 :&.:4.,<...&$.<. 0011DB70 0F 0B 07 0B 35 03 19 1F 0B 31 3B 33 03 07 15 37 ....5....1;3...7 0011DB80 14 22 1E 10 38 30 18 10 04 1A 24 08 02 26 38 2A ."..80....$..&8*
将楼顶的数据与表1对比:
11 2B 21 05 3F 01 25 0B 39 13 33 09 3F 39 35 25 r P F f 9 b J l 3 t X j 9 3 Z J
逃出生天
Only 'a'-'z','A'-'Z','0'-'9' accepted. Only 16 chars accepted. Serial:rPFf9bJl3tXj93ZJ yourserialisgood 请按任意键继续. . .
技术总结
这个cm花了我两天的时间,不过终于是成功逃脱了密室。总的来说,程序没有摆出繁杂的逻辑结构和反调试机制,但其的三重循环非常考验破解者的耐心和经验。这的确是一个以智慧取胜,非常有趣的谜题。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
- [原创][安全运维向]模拟搭建小型企业内网 14350
- 攻防世界-PWN-高手进阶区-难度3到4-全部题解 18875
- [原创]攻击格式化字符串在.bss段的程序(bugku-pwn6) 15278
- [原创]XCTF攻防世界-pwn新手练习区全部十题解析 14409
- [原创]KCTF2021 第二题 write up 5550