Bomb Lab

这是汇编语言的第四个实验,没想到会用 CSAPP 上知名的 bomb lab,做起来也是十分带劲呢。

将炸弹变成“哑弹”

作为一名玻璃心的完美主义者,自然是不希望自己的爆炸记录被登记在册,因此要想办法能够在本地调试炸弹。好在两边都是 x86,至少二进制文件是能运行的。在本地 linux 环境下运行,报错:

Initialization error: Running on an illegal host [2]

经过观察反汇编,以及给的 c 文件中有这么一行:

/* Do all sorts of secret stuff that makes the bomb harder to defuse. */
initialize_bomb();

发现是这个函数在检查运行环境,看它的注释就不是好东西。用二进制编辑器把函数调用语句替换为空指令:

replace-init

之后就可以在本地运行了,但是炸弹爆炸的信息还是会被发出去(上课的时候就不小心炸了),继续 hack——

炸弹爆炸会调用 explode_bomb 函数,炸弹被拆除会调用 phase_defused 函数(编译的时候保留了符号表真的很良心了,大大降低了难度);而这两个函数都会调用一个叫做 send_msg 的函数来传递信息。因此只需要将 explode_bomb 与 phase_defused 中的函数调用语句替换为空指令即可(函数有返回值,但是并没有没用到过,因此不详细追究了):

replace-call-msg

replace-call-msg

大功告成,至此这个炸弹就变成了任人摆布的哑弹了。

工具简介

工欲善其事,必先利其器。本次实验中主要使用到了以下几个工具:

工具作用
gdb动态调试
IDA辅助阅读汇编代码,反编译
objdump反汇编
Hex Editor Neo编辑二进制文件

Phase 1

第一题很简单,题目实现了 strings_not_equal 函数,功能如其名,只需要知道传入参数在 rdi、rsi,返回值为 eax 即可。根据地址找到参与比较的另外一个字符串Verbosity leads to unclear, inarticulate things.

Phase 2

第二题,有一个函数 read_six_numbers,功能是从第一个参数指向的字符串中读出六个数字,保存在第二个 int 指针指向的数组中。注意函数调用 sscanf 时参数超过七个,使用堆栈传递了参数。这一点反编译工具 IDA 并没有意识到。

读完六个数之后,程序确认第一项是 0,第二项是 1,之后遵循斐波那契数的规律,每项是前面两项的和,因此答案为:

0 1 1 2 3 5

PS 这里展示一下 IDA 的界面,能够分块显示跳转指令分开的程序块,大大提高了代码可读性:

ida-tool

Phase 3

第三题,跳转表,相当于一个 switch 语句。输入字符串是"%d %c %d",以第一个数字作为索引,有 0 到 7 七种选择,每一种都有对应的字符与数字,这里为了方便直接取第一个数字为 0,对应 79H 的字符为 y,数字 210H 为 528。因此一个可能的答案:0 y 528

Phase 4

这一关的输入是两个数字 a、b,拆弹的条件为a == func4(7, b)2 <= b <= 4

通过研究 func4 的函数体得知这是一个递归函数,与其逻辑等价的 python 函数为:

def func4(x, y):
    if x <= 0:
        return 0
    if x == 1:
        return y
    return func4(x-1, y) + func4(x-2, y) + y

取 b = 3,可以算出 func4(7, 3) = 99,因此答案为99 3

Phase 5

本题要求读入一个字符串,长度为 6。假设其中一个字符为 c,后面的程序从 1 到 6 循环,将array_3154[c & 0xF]处的字符加到一个空字符串 s 上。那个数组的长度刚好为 16,查看内存得知其内容为maduiersnfotvbyl。最后用 s 与目标串"flyers"比较。因此,构建一个字符串使得每个字符后四位的值为 flyers 对应位置的索引即可。见下图:

ph5

一个可行的答案为:

yonuvw

Phase 6

第六题实在是太绕了。。。

首先读入六个数(还是 read_six_numbers 函数),之后二重循环判定六个数不相同。(六个数的全排列是 720,想办法暴力搜索也是可以的)

后面还有两个循环看得有点懵了,只能借助 gdb 动态调试。

node

上图是循环中频繁涉及到的一个数组,经过观察,程序是新开了一个数组,按照原有的 1 到 6 将之对应为 0x603490 到 0x6034e0。之后比较数组中如上图第一列所示的位置,必须是降序排列。因此将上图第一列所示数字排列,之后将每个数字对应的顺序(同上图第二列)作为答案即可。

2 1 6 4 3 5

Secret Phase

从各种地方都得知隐藏关的存在,比如 c 文件中的注释,以及已经破解了七关的排行榜。

直接查看函数列表便可发现有一个名叫 secret_phase 的函数。使用 gdb 在return 0;语句前加上断点,在前六关破解之后使用call secret_phase指令即可进入隐藏关。如果直接调用隐藏关的话排行榜会显示 phase 1 invalid。

最后一关反而比较简单,读入一个十进制数 y,我们希望 fun7 函数的返回值为 0。

fun7

虽然函数的定义比较奇怪,但可以看出,想要函数返回值为 0 的话,只需要让 y 等于第一个参数的指针指向的数字就可以了,通过查看内存得知这个数字为 36。