版权声明: 知识共享-版权归属-相同方式共享 3.0 授权协议 |
CC BY-SA 3.0 CN Errors happen.
Writeup for Bomb Lab, CSAPP Overview Above all, I need to declare that I used the newest version of Bomb Lab , which means it differs in many ways from the 200X version of this lab. The most notable difference is that is uses x64 architecture.
1 2 curl http://csapp.cs.cmu.edu/3e/bomb.tar --output bomb.tar tar xvf bomb.tar
All files needed are stored in my github repo .
Initialize FileStream 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if ( argc == 1 ){ infile = (FILE *)stdin ; } else { v3 = argv; if ( argc != 2 ) { __printf_chk(1LL , "Usage: %s [<input_file>]\n" , *argv); exit (8 ); } *(_QWORD *)&argc = argv[1 ]; argv = (const char **)"r" ; infile = fopen(*(const char **)&argc, "r" ); if ( !infile ) { __printf_chk(1LL , "%s: Error: Couldn't open %s\n" , *v3, v3[1 ]); exit (8 ); } }
如果运行程序的时候有参数,那么将参数视为文件地址,然后把输入流重定向至文件读入流。
这意味着我们可以使用./bomb payload
来检验我们的payload了。(虽然原来也可以./bomb < payload
就是了)
bind initialize_bomb
函数把SIGINT
信号(一般来自于Ctrl+C
)绑定到了signal_handler
函数上面。[1]
Phase1 1 2 3 4 5 6 7 8 9 __int64 __fastcall phase_1 (__int64 a1) { __int64 result; result = strings_not_equal(a1, "Border relations with Canada have never been better." ); if ( (_DWORD)result ) explode_bomb(); return result; }
因为源文件并没有去除符号表,因此我们可以猜测 string_not_equal
函数在 arg1
和 arg2
相等时返回1。
所以Phase1的Payload就是 Border relations with Canada have never been better.
。
Phase2 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 __int64 __fastcall read_six_numbers (__int64 a1, __int64 a2) { __int64 result; result = __isoc99_sscanf(a1, &unk_4025C3, a2, a2 + 4 , a2 + 8 , a2 + 12 , a2 + 16 , a2 + 20 ); if ( (int )result <= 5 ) explode_bomb(); return result; } __int64 __fastcall phase_2 (__int64 a1) { __int64 result; char *v2; int v3; char v4; char v5; read_six_numbers(a1, &v3); if ( v3 != 1 ) explode_bomb(); v2 = &v4; do { result = (unsigned int )(2 * *((_DWORD *)v2 - 1 )); if ( *(_DWORD *)v2 != (_DWORD)result ) explode_bomb(); v2 += 4 ; } while ( v2 != &v5 ); return result; }
观察后发现:
unk_4025C3
处内存布局为 25 64 20 25 64 20 25 64 20 25 64 20 25 64 20 25 64 00
。考虑到字符串的 \x00
截断,unk_4025C3
为 %d %d %d %d %d %d
。翻阅cppreference 可以发现 sscanf
会将给定的第一个参数视为缓冲区,从此处读取数据。 只有把 a2
视为一个长度为6的int数组才能解释这两段代码。 这是修改后的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 __int64 __fastcall phase_2 (__int64 a1) { __int64 result; int *v2; int v3[6 ]; char v4; read_six_numbers(a1, v3); if ( v3[0 ] != 1 ) explode_bomb(); v2 = &v3[1 ]; do { result = (unsigned int )(2 * *(v2 - 1 )); if ( *v2 != (_DWORD)result ) explode_bomb(); ++v2; } while ( v2 != (int *)&v4 ); return result; }
1 DWORD = 4 BYTE[2]
解释一句,DWORD基本上可以看作unsigned int。
条件就是必须满足 。
显然,在v2指针遍历完v3后就会移动至v4,循环结束。
所以Phase2的Payload就是 1 2 4 8 16 32
。
Phase3 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 __int64 __fastcall phase_3 (__int64 a1) { __int64 result; int v2; int v3; if ( (int )__isoc99_sscanf(a1, "%d %d" , &v2, &v3) <= 1 ) explode_bomb(); switch ( v2 ) { case 0 : result = 207LL ; break ; case 1 : result = 311LL ; break ; case 2 : result = 707LL ; break ; case 3 : result = 256LL ; break ; case 4 : result = 389LL ; break ; case 5 : result = 206LL ; break ; case 6 : result = 682LL ; break ; case 7 : result = 327LL ; break ; default : explode_bomb(); return result; } if ( (_DWORD)result != v3 ) explode_bomb(); return result; }
这个Phase的本意是让我们逆向Assembly。而在汇编代码中 switch ... case
语句由跳转表实现,需要一定基础才能逆向出来。
Payload任选一个:0 207
。
Phase4 1 2 3 4 5 6 7 8 9 10 11 12 13 __int64 __fastcall phase_4 (__int64 a1) { __int64 result; unsigned int v2; int v3; if ( (unsigned int )__isoc99_sscanf(a1, "%d %d" , &v2, &v3) != 2 || v2 > 14 ) explode_bomb(); result = func4(v2, 0LL , 14LL ); if ( (_DWORD)result || v3 ) explode_bomb(); return result; }
显然我们要满足:
&接下来让我们分析 func4
。
1 2 3 4 5 6 7 8 9 10 11 12 13 __int64 __fastcall func4 (int a1, int a2, int a3) { int v3; __int64 result; v3 = (a3 - a2) / 2 + a2; if ( v3 > a1 ) return 2 * (unsigned int )func4(a1, a2, v3 - 1 ); result = 0LL ; if ( v3 < a1 ) result = 2 * (unsigned int )func4(a1, v3 + 1 , a3) + 1 ; return result; }
显然在 的情况下函数 func4
可以直接返回0。
此时 。
因此这个Phase的Payload就是 7 0
另外一种解法是,考虑到v2可能的取值空间很小,我们可以用爆破的方法解出这道题。
Phase5 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 unsigned __int64 __fastcall phase_5 (__int64 a1) { __int64 i; char v3[8 ]; unsigned __int64 v4; v4 = __readfsqword(0x28 u); if ( (unsigned int )string_length((_BYTE *)a1) != 6 ) explode_bomb(); for ( i = 0LL ; i != 6 ; ++i ) v3[i] = array_3449[*(_BYTE *)(a1 + i) & 0xF ]; v3[6 ] = 0 ; if ( (unsigned int )strings_not_equal(v3, "flyers" ) ) explode_bomb(); return __readfsqword(0x28 u) ^ v4; }
我们能够分析出两点:
a1的长度应该是6 对于每个a1中的字符,取其ASCII码的最低四位作为索引,取 array_3449
数组的对应位数组成新的字符串,该字符串必须为 flyers
。 导出 array_3449
数组的值后写出exp。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from string import ascii_letters, digitsarray_3449 = [ 0x6D , 0x61 , 0x64 , 0x75 , 0x69 , 0x65 , 0x72 , 0x73 , 0x6E , 0x66 , 0x6F , 0x74 , 0x76 , 0x62 , 0x79 , 0x6C ] target_str = "flyers" for chrs in target_str: for num in array_3449: if ord (chrs) != num: continue ; for i in range (10 ): if chr (array_3449.index(num) + i * (0xf + 1 )) in ascii_letters + digits: print (chr (array_3449.index(num) + i * (0xf + 1 )), end="" ) break
Phase5的payload为 9ON567
。
Phase6 我们可以将函数 phase_6
分为三个sections。
Section1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 v1 = v15; read_six_numbers(a1, v15); v2 = 0 ; while ( 1 ){ if ( (unsigned int )(*v1 - 1 ) > 5 ) explode_bomb(); if ( ++v2 == 6 ) break ; v3 = v2; do { if ( *v1 == v15[v3] ) explode_bomb(); ++v3; } while ( v3 <= 5 ); ++v1; }
分析出以下点:
输入为6个数组,存储在 v15
数组中。 v2
为循环的标定参数,在达到6时结束循环v15
数组中每个数字不能大于6,也就是必须小于等于6对于 v15
数组中每个数,不能与后面的数相等 Section2 1 2 3 4 5 6 7 v4 = (char *)v15; do { *(_DWORD *)v4 = 7 - *(_DWORD *)v4; v4 += 4 ; } while ( v4 != &v16 );
将 v15
数组中每个数变成7减去它自身。
Section3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 for ( i = 0LL ; i != 24 ; i += 4LL ){ v8 = v15[i / 4 ]; if ( v8 <= 1 ) { v6 = &node1; } else { v7 = 1 ; v6 = &node1; do { v6 = (_QWORD *)v6[1 ]; ++v7; } while ( v7 != v8 ); } *(__int64 *)((char *)&v17 + 2 * i) = (__int64)v6; }
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 .data:00000000006032 D0 node1 dd 14 Ch ; DATA XREF: phase_6:loc_401183↑o .data:00000000006032 D0 ; phase_6+B0↑o .data:00000000006032 D4 dd 1 .data:00000000006032 D8 dd 6032E0 h .data:00000000006032 DC dd 0 .data:00000000006032E0 public node2 .data:00000000006032E0 node2 dd 0 A8h .data:00000000006032E4 dd 2 .data:00000000006032E8 dd 6032F 0h .data:00000000006032 EC dd 0 .data:00000000006032F 0 public node3 .data:00000000006032F 0 node3 dd 39 Ch .data:00000000006032F 4 dd 3 .data:00000000006032F 8 dd 603300 h .data:00000000006032F C dd 0 .data:0000000000603300 public node4 .data:0000000000603300 node4 dd 2B 3h .data:0000000000603304 dd 4 .data:0000000000603308 dd 603310 h .data:000000000060330 C dd 0 .data:0000000000603310 public node5 .data:0000000000603310 node5 dd 1 DDh .data:0000000000603314 dd 5 .data:0000000000603318 dd 603320 h .data:000000000060331 C dd 0 .data:0000000000603320 public node6 .data:0000000000603320 node6 dd 1B Bh .data:0000000000603324 dd 6 .data:0000000000603328 dd 0 .data:000000000060332 C dd 0
我们可以看到,node的结构很像一个结构体,遵循着下面的结构:
data + id + next_node + 0
接下来把 node[v8[i]]
的地址保存在 v17
中。
Section4 1 2 3 4 5 6 7 8 9 for ( j = v17; ; j = v12 ){ v12 = *(_QWORD *)v10; *(_QWORD *)(j + 8 ) = *(_QWORD *)v10; v10 += 8 ; if ( v10 == &v19 ) break ; } *(_QWORD *)(v12 + 8 ) = 0LL ;
将后一个 node
的指针指向前一个 node
,也就是将 node
的顺序倒过来了。
1 2 3 4 5 6 7 8 9 10 v13 = 5 ; do { result = **(unsigned int **)(v9 + 8 ); if ( *(_DWORD *)v9 < (int )result ) explode_bomb(); v9 = *(_QWORD *)(v9 + 8 ); --v13; } while ( v13 );
如果(按照 node
的顺序),前者比后者的 data
小,那么炸弹爆炸。
我们将 node
按照 data
做一个排序:
0x0A8 < 0x14C < 0x1BB < 0x1DD < 0x2B3 < 0x39C
2 < 1 < 6 < 5 < 4 < 3
这是正确的顺序;逆转之后变成了
3 4 5 6 1 2
与7减去自身后变成了:
4 3 2 1 5 6
这就是Phase6的Payload。
Summary All-In-One:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 root@workshop:~/repos/SCUCCS/C-Programming/Security Labs/lab3/bomb Border relations with Canada have never been better. 1 2 4 8 16 32 0 207 7 0 9ON567 4 3 2 1 6 5 root@workshop:~/repos/SCUCCS/C-Programming/Security Labs/lab3/bomb Welcome to my fiendish little bomb. You have 6 phases with which to blow yourself up. Have a nice day!Phase 1 defused. How about the next one? That's number 2. Keep going! Halfway there! So you got that one. Try this one. Good work! On to the next... Congratulations! You' ve defused the bomb!
首先,这道题最有利于锻炼自己逆向能力的解题方法是对着汇编代码硬磕。比如Phase3的switch跳转表:CSAPP中专门花了~1页来讲跳转表的机制以及为什么可以起到优化的效果。而IDA Pro[3] 无疑让这个过程缺少了不少乐趣。
其次,逆向的过程中也用到了很多tricks————比如指针乱跳、内存操作等。CSAPP Labs作为CSAPP稳坐CS神课第一把交椅的重要原因,其中的这个Bomb Lab更是让人印象深刻。可惜作为self-learning使用的bomb版本没有autoGrader,没有爆炸一次就会减二分之一分数的惩罚,让我们在拆弹的过程中少了很多惊险 & 刺激。
References