CASPP 的 LAB 放在 Lab Assignments 上,在 “Self-Study Handout” 出下载对应 LAB 的资源。
课程 Lab 说明:“二进制炸弹” 是一个提供给学生的可执行文件。运行时,它会提示用户输入 6 个不同的字符串。如果这些字符串中有任何一个不正确,炸弹就会 “爆炸” 并打印错误信息。学生必须通过反汇编和逆向自己的炸弹来确定这 6 个字符串应该是什么。这个实验让学生理解汇编语言,并强迫他们学习如何使用调试器(GDB)。这是一个你可以自己尝试的 Linux/x86-64 二进制炸弹。
我们选择的是 “自学” 模块,没有远程服务端检查和扣分机制,可以随意调试。但是,CMU 的同学就会有这些机制,输错了就扣分,所以需要小心调试。
解压 tar 包,得到三个文件 bomb
、bomb.c
和一个 README
,README
只说了这是一个 x86-64
程序。
bomb.c
解读
bomb.c
应该就是 bomb
的主代码了。
代码引入了两个自定义库 support.h
和 phases.h
,也就是说我们想要的字符串必须从程序中得到。
然后是命令行参数要求,总结如下:
bomb
:标准的按行输入
bomb <file>
:先从 <file>
中读取输入,结束后再转到标准输入
- 不可提供更多命令行参数,否则打印用法
然后就是打印提示信息并要求输入了。整个代码解读完毕,现在转向程序。
bomb
逆向 - IDA
理论上来讲,这个 Lab 是需要在 Linux 环境下用 GDB 调试的,不过我们现在还在 Windows 上,所以先用 IDA 分析一下。这里可以先用 bomb.c
修复一下函数参数表,有些参数是没有用的:
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
| int __fastcall main(int argc, const char **argv) { const char *line; const char *v3; const char *v4; const char *v5; const char *v6; const char *v7;
if ( argc == 1 ) { infile = (FILE *)stdin; } else { if ( argc != 2 ) { __printf_chk(1, "Usage: %s [<input_file>]\n", *argv); exit(8); } infile = fopen(argv[1], "r"); if ( !infile ) { __printf_chk(1, "%s: Error: Couldn't open %s\n", *argv, argv[1]); exit(8); } } initialize_bomb(); puts("Welcome to my fiendish little bomb. You have 6 phases with"); puts("which to blow yourself up. Have a nice day!"); line = read_line(); phase_1(line); phase_defused(); puts("Phase 1 defused. How about the next one?"); v3 = read_line(); phase_2(v3); phase_defused(); puts("That's number 2. Keep going!"); v4 = read_line(); phase_3(v4); phase_defused(); puts("Halfway there!"); v5 = read_line(); phase_4(v5); phase_defused(); puts("So you got that one. Try this one."); v6 = read_line(); phase_5((__int64)v6); phase_defused(); puts("Good work! On to the next..."); v7 = read_line(); phase_6(v7); phase_defused(); return 0; }
|
先来看 Phase1,点进 phase_1(line)
,即可得到内容:
1
| result = strings_not_equal(line, "Border relations with Canada have never been better.");
|
Phase1 就是 Border relations with Canada have never been better.
。
接下来是 Phase2。有了 Phase1 的经验,我们点开 phase_2(v5)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| __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; }
|
先看第一个读取函数 read_six_numbers(a1, &v3)
:
1 2 3 4 5 6 7 8 9
| __int64 __fastcall read_six_numbers(__int64 a1, __int64 a2) { __int64 n5;
n5 = __isoc99_sscanf(a1, "%d %d %d %d %d %d", a2, a2 + 4, a2 + 1, a2 + 12, a2 + 2, a2 + 20); if ( (int)n5 <= 5 ) explode_bomb(); return n5; }
|
核心就是 __isoc99_sscanf
。查询函数定义 可知,这里是从我们输入的 Phase2 中读取格式化的 “%d %d %d %d %d %d
” 并写入 v3[i]
。
我们可以在这里修复一下函数定义,修复后的 phase_2(v5)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| int __fastcall phase_2(const char *a1) { int result; int *v2; int v3[6]; int v4;
read_six_numbers(a1, v3); if ( v3[0] != 1 ) explode_bomb(); v2 = &v3[1]; do { result = 2 * *(v2 - 1); if ( *v2 != result ) explode_bomb(); ++v2; } while ( v2 != &v4 ); return result; }
|
可以知道,v3[0]
,即第一个输入值必然是 1
。v2
对应第二个输入值的地址。
然后进入一个 do-while
循环,循环条件是 v2
不等于 v4
的地址。
循环体的逻辑是计算 2 * v2[i-1]
的值,然后和 v2[i]
进行比较。如果不相等,就引爆炸弹。然后移动到下一个元素。
v4
和我们的输入无关且分配内存时接在 v3
之后,基于内存连续性,我们认为数组 v3
结束于地址 v4
。