前置条件:阅读完 3.10.3 前的内容
实验准备
在官网下载 handout
文件,解压到本地,阅读 Writeup
文档,有详细解释实验的做法(一开始完全没注意到还要看这个东西,导致解压完不知道要做啥)
解压后,运行命令:
反汇编出 bomb.s
。
建议在 VS Code
中阅读代码,可以安装插件 GNU Assembler Language Support
(牛头人插件)让汇编高亮,方便阅读。
查找地址的时候还可以用 ctrl + f
来进行快速查找。
实验过程
实验分为六个阶段和一个彩蛋阶段(需要自己先把汇编简单浏览一遍才能发现)。
Phase_0
阅读 bomb.c
文件,可以发现每个 phase
都是一个模板:
也就是需要输入一行字符串(之类的),然后系统会判断输入的字符串是否合法,若合法则炸弹被排除,否则爆炸。
(如果是 CMU 学生的话,每次炸弹爆炸都会把信息反馈到服务器…)
大概知道是怎么回事了就可以开始进行真正的实验了,如果还不知道,建议仔细阅读 Writeup
文档(虽然是全英的)
运行反汇编命令之后可以直接找到 main
函数,这与 bomb.c
中的结构是一样的,我们可以参照源代码来看这部分的汇编。
前面带有 plt
部分的汇编都代表了系统函数(库函数),功能和作用都可以在网上查到。
Phase_1
很轻易就能找到 phase_1
部分的汇编。
可以发现在 400ee9
处调用了 strings_not_equal
这个函数,并在下一行中查看 %rax
(返回值)是否为 0,是则结束阶段,否则引爆炸弹。
于是我们跳转到 401338
处查看 strings_not_equal
的代码。
在 40133f
处可以发现这个函数接受两个参数,分别存在 %rdi
与 %rsi
中,通过函数名也能猜出来,这个函数应该是判断两个字符串是否相等的。
于是根据我们一惯的判断方法:先看长度是否相等,再看每个字符是否相同。
事实上,我们只需要知道相等的返回值是多少即可(也可以直接猜出来)。
显然,长度不相等,从 401352
+ 40139b
可以看出来,应当返回 1 ,那么由 401381
/401388
即可看出,若相等应该返回 0。
但存储在 %rdi
与 %rsi
中的字符串到底是什么?
下面通过 gdb
去查看。
输入命令
随后随便输入一个字符串(假设输入了 11111
),程序便会在 strings_not_equal
处停下。
可以通过命令 stepi 1
来逐步运行语句,通过x/ls 0xffffffff
来查看在地址 0xffffffff
处的字符串,info r
查看寄存器信息(具体gdb
的命令可以上网查)
那么,我们就需要去检查 %rdi
与 %rsi
中储存的字符串到底是什么,如下图所示:
于是,我们就能看出来,我们需要输入的字符串就是在 %rsi
中的字符串(应该是起始地址存在 %rsi
中)。
于是输入 Border relations with Canada have never been better.
,阶段一就结束了。
Phase_2
先贴代码:
400f05
显然是最显眼的,函数名也很暴力,直接告诉我们需要输入六个数字,并且在上一行中,还将 %rsp
的值存到了 %rsi
(第二个参数)中,也就是说这六个数都存到了栈之中。
阅读 read_six_numbers
的汇编:
发现调用了一个陌生的函数 sscanf
,查阅文档后可知,其功能为将输入的字符串通过格式流进行转换,也就是可以将 1 1 1 1 1
转化为五个数字 1
,返回值为转化的字符的个数,如这里就是 5。
仔细阅读后,逆向工程即为:
可以看出,这里的数组 a
,就是在 400f02
中的 %rsp
中储存的地址,那么,读出来的数便存储在 %rsp, %rsp + 4, %rsp + 8, %rsp + 12, %rsp + 16, %rsp + 20
。
接下来,回到 phase_2
,发现 400f0a
与下一行共同要求了,输入的第一个数 (%rsp)
一定为 1 ,否则引爆炸弹。
若相等,代码令 %rbx = &(%rsp + 4)
,也就是令 %rbx = &a[i + 1]
,若 %rsp = &a[i]
;令 %rbp = &(%rsp + 24) = &a[6]
,比最后一个数多 4 字节。然后跳转到 400f17
,开始循环:
在前三行已经描述了输入的序列应满足的要求:a[i+1]=2a[i]。
由于第一个数一定为 1 ,于是这个序列为:12481632。
阶段二结束。
Phase_3
在汇编代码中给出注释。
要注意的是,我们可以从sscanf
的格式流知道应该输入多少个数字,这个格式流在 0x4025cf
中有,可以使用 x/ls 0x4025cf
来查看
在最后一行 400f75
注意到这个 jmpq
的结构,对比书中的 switch
跳转表结构,可以发现是一致的,只不过这里没用标签。
于是很显然,这里需要输入的两个数就是 switch
中的索引与索引对应的值。
那么,检查下面的代码:
并结合前面,输入的第一个数小于等于7,我们可以确定其索引为 0→7,通过上述的语句,便可以确定每个索引所对应的值(都储存在了 %rax
中)。
于是我们可以得到答案为(这里没有去算索引为 0 时对应的值):
1 311
,2 707
, 3 256
, 4 389
, 5 206
, 6 682
, 7 327
阶段三结束。
Phase_4
或许是最简单的一个阶段?
显然事情都在 fun4
中做了,phase_4
只是调用了一下函数,检查一下返回值而已。
转到fun4
去:
于是,直接做逆向工程:
反正 0≤a≤14,直接枚举就可以,很简单。
答案应该不止一个,我第一次试 7 0
直接就对了,所以没试其他的。
阶段四结束。
Phase_5
这里只提一下为什么会提到 maduiersnfotvbyl
这个字符串。
运行到 401089
为止,都只是在判断输入的字符串是否合法,并没有真正进入到这个阶段的关键部分,到这一步后,我们会跳转到 40108b
,随后会运行到 401099
,我们会注意到这里有一个奇怪的地址 0x4024b0
,这条语句将 %edx
设置为 (%rdx + 0x4024b0)
,而在前面我们已经知道 %rdx
是输入字符的 ASCII
码的低四位,可以理解为一个偏移量,于是我们去检查 0x4024b0
到底有什么:
于是,我们就可以找到这个神奇的字符串了。
剩下的我们只需要通过输入的字符串每个字符的低四位作为偏移量,在这个字符串中选择出与 4010bd
中函数所需的另一个字符串相等的字符串即可。
于是,可以检查 %rsi
或者前面4010b3
中提到的0x40245e
中存储的字符串:
那么,我们只需要从maduiersnfotvbyl
选择出 flyers
即可。
在 ASCII
表中寻找末尾为 0x9
0xf
0xe
0x5
0x6
0x7
即可,可以选择ionefg
。
阶段五结束。
Phase_6
最后一个阶段(是也不是),还是有点难度的,主要是汇编太长了,让人完全不想看。
简单来说,你只需要输入 1→6 这六个数,假设存储在 a[i] 中,进行一个逆转 a[i]=7−a[i] 后,构建一个链表,这个链表满足的是递减顺序,我们需要使新的链表中前一个节点存放的数据值的低 4 字节都大于后一个节点, 我们可以直接去检查这个 0x6032d0
(使用 x/12xg 0x6032d0
,需要运行到4011a9
才能进行检查),或者自己排序(在之前的步骤中,我们已经将索引与数值绑定了,第一个数的索引即为 1),于是,排序为:
3 4 5 6 1 2
然而这是被逆转后的,我们逆转回去,即可得到正确的输入 4 3 2 1 6 5
阶段六结束。
Secret_Phase
这个彩蛋,实际上我们可以在 bomb.c
中找到蛛丝马迹,在解决掉 phase_6
后,会一段注释
当然如果有开始做之前就乱翻代码的“良好习惯”,会很轻易发现还有个 fun7
和 secret_phase
。
但是我们会发现,我们无法直接进入这个彩蛋阶段。因为每个阶段的进入样式都一个板子,但是或许…还有一个 phase_defused
可以看看?
跳转到 phase_defused
函数后,直接看 callq
这种语句有没有提到 secret_phase
。(其实有一个更老赖的方法就是,直接ctrl + f
搜索 401242
,也就是这个函数的地址,一下就找到了)
那么接下来就是看如何从 phase_defused
进入 secret_phase
可以看到 4015d8
将函数num_input_strings
的返回值与 6 进行比较,如果不等于 6 则的直接跳过中间代码到达最后的结束部分,很显然这个函数应该是判断输入字符串的数量的,读取了 6 个字符串就不会跳过中间代码了。(其实就是看看是不是到阶段六才发现这个东西,完成阶段六之后就没有发现彩蛋的资格了)
分析中间代码,可以发现又有熟悉的 sscanf
函数,我们可以使用 gdb
来查看其格式流。这已经在 Phase_3
中提过,不再赘述。
发现是需要读取两个数字与一个字符串,但在下一行也为 %rdi
指定了地址,检查此地址:
由于我输入了7 0 0 0 0 0 0
,可以发现这个地址存储的就是输入的字符串。并且我们可以从+240 看出,这应该是第四行输入的字符串,也就是 Phase_4
输入的字符串。(因为第五阶段是+320,第三阶段可以自己看看…)
往下看可以发现,先判断了字符串是否相等,那么我们只需要去找0x402622
处的字符串即可。
(好呢,人如其名)
那么我们只需要在 Phase_4
的时候输入 7 0 DrEvil
即可进入 secret_phase
。
还是一样的操作,读一行字符串后赋值给 %rdi
,随后调用 strtol
函数,查阅文档知,此函数可以将一个字符串转换成对应的长整型数值。
在后面, %rax
赋值给 %rbx
后,将其减 1 ,并要求大于 0x3e8
。
后续,将输入的值置为fun7
的第二个参数,并将 0x6030f0
置为第一个参数,调用 fun7
。
调用后,将返回值与 2 比较,若相等则排除成功。
于是 fun7
应该返回 2。
阅读 fun7
的汇编:
测试输入的地址值是否为 0,如果是则返回 −1。刚进入这个函数的时候,我们可以检查0x6030f0
的值,可以发现为0x24
,显然不为 0。
往后看可以发现,函数会将 %rdi
的值与我们输入的值比较,若这个数小于等于我们输入的数就跳至401220
,将%eax
置 0,再进行一次相同的比较,如果相等则直接返回。
在这之中,还存在着递归,并且这还是存在分支条件的递归,显然可以想到树的左右子树。
我们可以检查0x6030f0
开始存储的结构:
可以看出来这是个二叉树,如图:
当 %edx
大于输入的数时,令%rdi
移到它的左子树的位置,接下来调用fun7
在返回后令%eax = 2 * %eax
。
当 %edx
小于输入的数,令%rdi
移到它的右子树的位置,接着调用fun7
,在返回后令%eax = 2 * %rax + 1
。
也就是说:%edi
为树上的一个结点,令%edi
节点的值与输入的值进行比较。
- 如果两者相等:返回 0
- 如果前者大于后者:
%rdi
移至左子树,返回2 * %rax
- 如果后者大于前者:
%rdi
移至右子树,返回2 * %rax + 1
因此,若想返回值为 2,按照返回顺序,可以返回 0, 2 * %rax + 1, 2 * %rax
,于是遍历顺序为,左-右-中。
答案为 0x16
,即 22
。
彩蛋结束。
总结
没有总结(