# bomblab 输入命令 `objdump -s -S -d -M att bomb > bomb.s` 进行反汇编。 ## Phase 1 对 `phase_1` 进行逆向: ```c void phase_1(char str[]) { if (strings_not_equal(str, "Border relations with Canada have never been better.") != 0) { explode_bomb(); } } ``` `explode_bomb` 函数将炸弹引爆,我们不希望它被调用;而 `strings_not_equal` 接受两个字符串作为参数,在两者不相等时返回 `1`,否则返回 `0`。 显然,输入应为 `Border relations with Canada have never been better.`。 ## Phase 2 在这个 phase 中,我们看到编译器在处理循环时使用了奇怪的策略,这使得生成的汇编混乱并且难以理解。对此,我们选择出较为混乱的代码,调整指令的顺序并相应改变的跳转,使之符合原书中描述的几种形式。 在这里,我们将 `400f0a` 至 `400f3a` 部分整理如下: ```esm cmpl $0x1,(%rsp) je .L1 call explode_bomb .L1: lea 0x4(%rsp),%rbx lea 0x18(%rsp),%rbp .L2: mov -0x4(%rbx),%eax add %eax,%eax cmp %eax,(%rbx) je .L3 call explode_bomb .L3: add $0x4,%rbx cmp %rbp,%rbx jne .L2 ``` 基于此逆向得到: ```c void phase_2(char str[]) { int x[6]; read_six_numbers(x); if (x[0] != 1) { explode_bomb(); } else { for (int i = 1; i < 6; i++) { if (x[i] != x[i - 1] * 2) { explode_bomb(); } } } } ``` `read_six_numbers(int x[])` 读入 6 个整数,并依次存贮到 `x[0]` 至 `x[5]`。 由此可知,输入应为 `1 2 4 8 16 32`。 ## Phase 3 这个 phase 包含一个 switch 语句。 ```c void phase_3(char str[]) { int x, y; if (sscanf(str, "%d %d", &x, &y) <= 1) { explode_bomb(); } if (x > 7 || x < 0) { explode_bomb(); } int z; switch (x) { case 0: z = 0xcf; break; case 2: z = 0x2c3; break; case 3: z = 0x100; break; case 4: z = 0x185; break; case 5: z = 0xce; break; case 6: z = 0x2aa; break; case 7: z = 0x147; break; default: z = 0x137; } if (y != z) { explode_bomb(); } } ``` 由此可知,输入的第 1 个数必须在 0 到 7 之间,第二个数与之相应即可。 ## Phase 4 `400fd6` 至 `400fdd` 部分使用了原书 2.3.7 中提到的方法实现除以 2。 ```c int func4(int x, int y, int z) { int mid = y + (z - y) / 2; if (mid <= x) { if (mid >= x) { return 0; } else { return 2 * func4(x, mid + 1, z) + 1; } } else { return 2 * func4(x, y, mid - 1); } } void phase_4(char str[]) { int x, y; if (sscanf(str, "%d %d", &x, &y) != 2) { explode_bomb(); } if (x > 15 || x < 0) { explode_bomb(); } if (func4(x, 0, 15) != 0 || y != 0) { explode_bomb(); } } ``` 结合线段树知识,输入的第 1 个数可以是 `0`、`1`、`3` 和 `7`,第二个数是 `0`。 ## Phase 5 将 `40107f` 至 `4010c6` 部分整理如下: ```esm cmp $0x6,%eax je .L1 call explode_bomb .L1: mov $0x0,%eax .L2: movzbl (%rbx,%rax,1),%ecx mov %cl,(%rsp) mov (%rsp),%rdx and $0xf,%edx movzbl 0x4024b0(%rdx),%edx mov %dl,0x10(%rsp,%rax,1) add $0x1,%rax cmp $0x6,%rax jne .L2 movb $0x0,0x16(%rsp) mov $0x40245e,%esi lea 0x10(%rsp),%rdi call strings_not_equal test %eax,%eax je .L3 call explode_bomb .L3: ``` 基于此逆向得到: ```c void phase_5(char str[]) { if (string_length(str) != 6) { explode_bomb(); } char str2[7]; for (int i = 0; i < 6; i++) { str2[i] = "maduiersnfotvbyl"[str[i] & 0xF]; } str2[6] = 0; if (strings_not_equal(str2, "flyers") != 0) { explode_bomb(); } } ``` 一个可行的输入是 `9?>567`。 ## Phase 6 将 `401176` 至 `4011a9` 部分整理如下: ```esm mov $0x0,%esi .L1: mov $0x6032d0,%edx mov (%rsp,%rsi,1),%ecx cmp $0x1,%ecx jle .end mov $0x1,%eax .L2: mov 0x8(%rdx),%rdx add $0x1,%eax cmp %ecx,%eax jne .L2 .end: mov %rdx,0x20(%rsp,%rsi,2) add $0x4,%rsi cmp $0x18,%rsi jne .L1 ``` 为了理解代码对以 `0x6032d0` 开头的一段内存的读取与写入,我们还需研究其中数据的组织方式,例如: ```txt 6032d0 4c010000 01000000 e0326000 00000000 L........2`..... ``` 结合汇编代码,不难猜到这块区域依次存储了两个 `int` 和一个指针,组成一个结构。我们给出它的声明: ```c struct chain_node { int val; int id; struct chain_node *next; }; ``` 综合以上,逆向得到: ```c struct chain_node { int val; int id; struct chain_node *next; } c[6] = {{0x014c, 1, &c[1]}, {0x00a8, 2, &c[2]}, {0x039c, 3, &c[3]}, {0x02b3, 4, &c[4]}, {0x01dd, 5, &c[5]}, {0x01bb, 6}}; void phase_6(char str[]) { int x[6]; read_six_numbers(x); for (int i = 0; ; i++) { if (x[i] <= 0 || x[i] > 6) { explode_bomb(); } if (i == 5) { break; } for (int j = i + 1; j < 6; j++) { if (x[i] == x[j]) { explode_bomb(); } } } for (int i = 0; i < 6; i++) { x[i] = 7 - x[i]; } struct chain_node *y[6]; for (int i = 0; i < 6; i++) { chain_node *p = c[0]; for (int j = 1; j < x[i]; j++) { p = p->next; } y[i] = p; } for (int i = 1; i < 6; i++) { y[i - 1]->next = y[i]; } y[5]->next = NULL; chain_node *p = y[0]; for (int i = 5; i > 0; i--) { if (p->val < p->next->val) { explode_bomb(); } p = p->next; } } ``` 结合链表知识,输入应为 `4 3 2 1 6 5`。 ## 进入 Secret Phase 唯一对 `secret_phase` 的调用位于 `phase_defused` 中。观察 `phase_defused`,在 `num_input_strings`(每次调用 `read_line` 都会使它加 1)等于 6,即完成 Phase 6 后的调用时,该函数从以 `0x603870` 开头的字符串中先后提取了两个整数和一个字符串,并检查提取的字符串是否与 `DrEvil` 相等,若相等,则调用 `secret_phase`。 `0x603870` 这个地址并没有在其他任何地方出现过,但是在 `skip` 和`read_line` 中,出现了地址 `0x603780`。对这两个函数逆向,大致如下: ```c char input[MAXLEN]; // 0x603780 int num_input_strings = 0; FILE *infile; int skip() { fgets(input + 90 * num_input_strings, 90, infile); ... } char* read_line() { ... char *start = input + 90 * num_input_strings; ... num_input_strings++; ... return start; } ``` 由此可知,以 `0x603870` 开头的字符串就是 phase 4 时输入的字符串,而在 phase 4 中恰好要输入两个整数,在它们之后再输入 `MrEvil`,我们就能够进入 Secret Phase。 ## Secret Phase ```c struct tree_node { int val; struct tree_node *ls, *rs; long place_holder; } t[15] = {{0x024, &t[1], &t[2]}, {0x008, &t[5], &t[3]}, {0x032, &t[4], &t[6]}, {0x016, &t[12], &t[10]}, {0x02d, &t[7], &t[13]}, {0x006, &t[8], &t[11]}, {0x06b, &t[9], &t[14]}, {0x028, NULL, NULL}, {0x001, NULL, NULL}, {0x063, NULL, NULL}, {0x023, NULL, NULL}, {0x007, NULL, NULL}, {0x014, NULL, NULL}, {0x02f, NULL, NULL}, {0x3e9, NULL, NULL}}; int fun7(struct tree_node *x, int y) { if (x == NULL) { return -1; } else { if (x->val <= y) { if (x->val == y) { return 0; } else { return 2 * fun7(x->rs, y) + 1; } } else { return 2 * fun7(x->ls, y); } } } void secret_phase() { long x = strtol(read_line(), NULL, 10); if (x > 1001 || x < 1) { explode_bomb(); } if (fun7(t, x) != 2) { explode_bomb(); } ... } ``` 结合二叉搜索树知识,输入应为 `22`。