拆弹记录

by Gu Wei
2021年10月

0. 准备工作

0)如何读取文件。./bomb <file> 注意文件中的换行符必须为LF,不能是CRLF,否则会爆炸。不要问我怎么知道

1)先把可执行文件反编译成汇编文件

2)在bomb.c中找到这串代码

发现六个phase是解决问题的关键

进入gdb调试:

下面开始正式拆弹!


 

1. phase_1

1.0 研究read_line函数

找到main函数中phase_1上下文:

%rax作为默认的函数返回寄存器,再根据read_line的函数名字,猜测%rdi会存刚刚输入的字符串,不妨利用gdb进行测试:

验证了猜想。而如果去看read_line函数汇编代码,会劝退的。。。

1.1 进入phase_1

把0x402400赋到%esi中,再去调用strings_not_equal,再加上之前%rdi存了我们输入的字符串!容易联想到就是比较%esi和%rdi地址的字符串是否相等。而且%rdi默认是第一参数,%rsi是第二参数!于是再去瞄一眼strings_not_equal!

1.2 看看strings_not_equal

整个代码又臭又长,看不懂。。。但是显然就是在比较%rdi和%rsi了!而且可以看出当相同时返回0;不相同返回1(额,这可能要看一会儿)。

1.3 回到phase_1

test %eax,%eax是将%eax与%eax做与运算,不改变%eax但是设置条件寄存器。当且仅当%eax值为0时,ZF寄存器会是1。

je通过ZF寄存器是否为1进行条件跳转。如果%eax为0的话,就会跳转,跳过 <explode_bomb>函数,也就是要让strings_not_equal(%rdi,%rsi)两个字符串相等即可。

1.4 defuse phase_1

Border relations with Canada have never been better.即为第一题答案。


 

2. phase_2

2.1 猜测read_six_numbers函数

当去研究<read_six_numbers>的汇编的时候又开始看不懂了。。。不过可以猜测就是读进去了六个数字。内存分布如下:

这里是一个运行时栈存数组的简单应用。

2.4 再看read_six_numbers函数

做完phase_3,弄清楚sscanf函数后,再来看read_six_numbers函数,终于有所眉目。

这里就是一个利用运行时栈local variables & argument build area的应用。sscanf此时要有八个参数,类似sscanf(*str, "%d %d %d %d", &a, &b, &c, &d)。但是x86寄存器只有rdi、rsi、rdx、rcx、r8、r9可以做为函数参数,还有两个参数就需要利用栈来储存。可以看到:

将phase_2栈帧中的两个地址存到了read_six_numbers栈帧之中。而sscanf在子函数中0x8(%rsp)、(%rsp)这两个地址的写入,其实就是在父函数phase_2的0x14+%rsi与0x10+%rsi两个地址进行写入。这就是read_six_numbers栈帧中argument build area的应用。

至于local variables区域体现在phase_2栈帧存了数组这一点上。

总感觉local variables & argument build area两个东西区分不清,有点杂合。2.4就算姑且谈谈而已。

2.2 gdb验证猜想

印证了之前的猜想,注意到这里是小端表示法。这里的rsp存的地址也很有栈的味道(0x7ffffff开头)。

2.3 dufuse phase_2

通过阅读phase_2的汇编代码,容易看出答案是1 2 4 8 16 32(这应该不难,就是一个do while循环)。


 

3. phase_3

3.1 <__isoc99_sscanf@plt>

这里调用了sscanf函数,函数原型为int sscanf(const char *str, const char *format, ...),譬如sscanf("hello 1", "%s %d", str, a)返回2且str=“hello”,a=1。根据x86汇编关于寄存器的约定,这里是sccanf(%rdi, %rsi, %rdx, %rcx)。%rdi是main函数的read_line输入,%rsi是格式化参数,%rdx和%rcx分别是两个存放输入的局部变量地址。由lea 0xc(%rsp),%rcx以及lea 0x8(%rsp),%rdx知道局部变量的地址。利用gdb进行验证,有:

有意思的是sscanf把传进去的寄存器值修改过了。rcx不再是0xc(%rsp),rci也不再是0x4025cf

3.2 switch

显然后半部分代码是一个switch的实现,可以看出只要输入的a、b两个数满足——b是case a中的值,就可以成功拆弹。

jmpq *0x402470(,%rax,8)是一个跳转表的实现,*意味着是间接跳转到该地址,这里就是下一条指令跳转到0x402470+8*%rax这个内存地址上存的地址。再强调一下jmp指令。jmp有直接跳转和间接跳转、无条件跳转和条件跳转之说,其中条件跳转必为直接跳转。直接跳转如jmp .L2就是下一条指令从.L2:处开始;间接跳转,如jmp *%rax跳转到寄存器%rax所存的值(该值是一个地址),再如jmp *(%rax)跳转到内存地址(%rax)上所存的值(自然该值也是个地址)。

通过gdb查看0x402470附近的值

至此所有拆弹前期工作做完。

3.3 defuse phase_3

如果第一个数输入1,在跳转表中找到0x402478: 0xb9 0x0f 0x40也就是跳转到0x400fb9,也就是400fb9: b8 37 01 00 00 mov $0x137,%eax。得到一组答案:1 311。完成拆弹!

当然也可以第一个输入1后利用(gdb) ni单步调试,发现下一条指令跳转到0x400fb9,也可以完成拆弹。第一次我是这么做的,但是当时显然对间接跳转理解不深。


 

4. phase_4

话不多说,先上phase_4代码

4.1 func4前

这里的汇编代码已经很熟悉了。容易知道这里要输入两个整数,并且第一个整数要小于等于14。不多说了。

4.2 func4后

func4后有一个小点要搞清楚,到底返回什么eax值才不会跳到炸弹呢?jne是由~ZF条件寄存器判断跳转,这里要不跳转,所以~ZF为0,也就是ZF=1。当且仅当%eax等于零时,test %eax, %eax才设置ZF为1。所以这里要func4返回的eax值等于0!

此外,还可以分析出,之前输入的第二个整数一定为0。这里不做展开。

4.3 困难的func4

在进入func4之前,我们给%edi、%esi、%edx三个寄存器设置了初值,所以可以得到int func4(int a, int b, int c)这个函数原型。可能有疑问,之前的%rcx也有初值,为什么不作func4的参数。这是因为%rcx是sscanf的参数,在sscanf结束后,%rcx的值是没有意义的。(不过如果进入函数的时候pop进了栈,这样也可以保存寄存器欸)

func4启动!!!

func4困难就困难在于它是一个递归函数(自己调用自己),在分析的过程中很容易超过脑子内存。。。于是需要把它化成C程序来进行分析。但是化成c程序的过程也很有挑战性!这里先强调一点:cmp a,b jle .L1是b≤a才跳转到L1!!!

直译版

意译版

4.4 defuse phase_4

现在只要找到那些让func4(a, 0, 14)返回0的a值即可,且a小于等于14。直接打表:0: 0; 1: 0; 2: 4; 3: 0; 4: 2; 5: 2; 6: 6; 7: 0; 8: 1; 9: 1; 10: 5; 11: 1; 12: 3; 13: 3; 14: 7;发现0、1、3、7四个值符合规则。于是答案可以为0 0、1 0、3 0以及7 0。完成拆弹!

4.5 番外

本拆弹最初是和wnhheu在上院完成,在多处分析出错的情况下,居然一遍拆弹成功。原来是马原课,却一起找了一个空教室,用教室的投影仪和黑板拆弹。我后面感觉分析不清的时候就放弃了,但是wnhheu还是坚持了下来并报出了正确的答案。很有意思,特此记录。


5. phase_5

冷知识:phase_5是本人拆弹最快的一个

5.1 金丝雀值

金丝雀是一种防止内存越界引用和缓冲区溢出的手段。具体为:在该函数栈帧中的局部变量区与栈状态(返回地址和寄存器保存区)之间存入一个随机的金丝雀值(Canary),然后在恢复寄存器状态和返回返回地址之前,会检测该值是否发生变化,如果发生变化,则程序异常终止。 在这个程序中:movq %fs:28, %rax把canary值赋到%rax中;再把%rax赋到栈帧上;一些指令后,把栈帧该处的值赋回%rax;最后xor %fs:40, %rax可以检测canary值是否被修改。

5.2 非关键代码部分

都做到phase_5了,具体就不展开了。就是要输入六个字符,否则要爆炸。

5.3关键代码部分

显然这里可以是一个for循环,把输入的字符串中的每一个字符通过0x4024b0(%rdx),%edx这个奇怪的操作变成另一个字符!另一个字符利用gdb查看:

可见映射出来的字符串为"flyers"。

关键中的关键0x4024b0(%rdx),%edx这个到底是什么映射?

分析代码我们可以看出,寄存器%rdx只有最初字符的后四位,其余为都为零。那么这个数再加上0x4024b0,就可以得到映射到的地址了。也就是说地址映射是这样一个函数:f(x)=0x4024b0+x(0x0xF),其中x是字符的后四位。(ASCII码是用八位来存的)

查看0x4024b0到0x4024b0+0xf的内存有:

flyers的ASCII码分别是:0x66 0x6c 0x79 0x65 0x72 0x73。对应着<array.3449>的第9、f、e、5、6、7位置(0-base)。于是只要找到最后四位是它们的字符即可!

顺带一提这个数组存了还存了之前看到过的So you think you can stop the bomb with ctrl-c, do you?。摊手.jpg

5.4 defuse phase_5

也就是找最后四位分别为9、f、e、5、6、7的字符即可,查查ASCII表就行了。正常的可以是:ionefg 不正常的可以是:)?>%&'

答案不唯一。


 

6. phase_6

phase_6代码真是又臭又长

可以把phase_6的代码分成六个部分,我们逐一解读。其实phase_6也并不是很难,只不过需要十足的耐心。

6.1 part1-输入六个数

part1部分再在2.4已经详细讲过了,就是读入六个整数,并将其放在%rsp、%rsp+0x4、...、%rsp+0x20这六个位置。也就是在栈里面存了数组。不做展开。

6.2 part2-输入的要求

稍有点麻烦。这里就是个嵌套循环,具体解释可以看注释。翻译出来的C代码如下:

可见输入的六个整数首先要满足两两不同且都小于等于6。于是可以不触发炸弹,进入part3了。

6.3 part3-修改输入的数

part3不难,就是数组的六个数分别变成7减去它自己。也就是假设原来数是a,现在变成7-a。比较简单,不做展开。

6.4 part4-虚假的链表

part4算是phase_6的难点了。这里就先放结论了:假设现在的数组是[A, B, C, D, E, F](均为1~6整数),那么在栈帧的%rsp+0x20到%rsp+0x48地址分别存了<nodeA><nodeB>、...、<nodeF>的地址。利用gdb可以看到以下<node>数据:

下面做一定的解读:

这里是一个用数组存的链表,用C描述如下:

可见该链表满足<node1>地址加0x8的地址上存<node2>的地址,<node2>地址加0x8的地址上存<node3>的地址,以此类推。

part4也有嵌套循环,外层循环利用%esi遍历数组,在%ecx中存了数组里具体的数。利用%eax来遍历节点,在%rdx存了节点的地址。通过比较%ecx和%eax,利用mov 0x8(%rdx),%rdx ,实现链表中p=p->next的操作。最后将%rdx值(也就是第%ecx个节点的地址)存到栈帧的指定区域。这里遍历节点是1-base的,所以可以得到之前输入的六个数除了满足小于等于6的条件外,还要大于等于1。

6.5 part5-重排链表

这里还行。就是在<nodeA>地址加0x8的地址上存<nodeB>的地址,在<nodeB>地址加0x8的地址上存<nodeC>的地址,以此类推。而原来节点是<node1>地址加0x8的地址上存<node2>的地址,<node2>地址加0x8的地址上存<node3>的地址,以此类推。这样刚刚虚假的链表,变得高级真实起来了。比如输入4 3 2 1 6 5,7减去它们变成3 4 5 6 1 2。利用gdb查看更新后的链表有:

符合node3->node4->node5->node6->node1->node2->NULL

6.6 part6-defuse phase_6

这里也不难了,就是要求<nodeA>地址上的值大于等于<nodeB>的,<nodeB>的大于<nodeC>的。注意这里的值都是32位的int,所以只取低32位。按照从大到小排列,依次是0x39c、0x2b3、0x1dd、0x1bb、0x14c、0xa8。对应节点依次为3、4、5、6、1、2。由于最初用7减去输入的数,所以答案是4 3 2 1 6 5。本题答案只有一个。

至此,六个炸弹全部被拆除。


 

7. secret_phase

然而Bomb Lab还安排了一个secret_phase要拆。。。其实给足了暗示:

  1. bomb.c中有
  1. <phase_6>汇编后面还有<fun7><secret_phase>

既然有,那就拆!

7.1 进入secret_phase

在<phase_defused>找到<secret_phase>的调用:

1)<num_input_strings>

在第七行我们看到cmpl $0x6,0x202181(%rip) # 603760 <num_input_strings>,很nice的给了注释。在运行时查看0x603760有:

可见num_input_strings就是一个当前在第几个phase的整数。只有当phase_6时,才有可能进入secret_phase,否则就直接跳转到return了。

2)sscanf

在第十四行程序调用了sscanf,老样子查看一下格式化参数:

是两个整数,一个字符串。

但是这里不同于其他的phase,sscanf的第一个参数%rdi不再是由input = read_line();读入,而是由$0x603870,%edi直接传入。在运行时查看0x603870的内存有:

可见该地址存的字符串就是当初phase_4传入的。

3)进入secret_phase

那phase_4还应该输入什么字符串才能触发secret_phase呢?利用gdb有:

可见phase_4输入0 0 DrEvil便可以进入secret_phase。

7.2 secret_phase

这段汇编不难,主要有两点

7.3 fun7

我们刚刚做完phase_6,这里又出现了mov 0x8(%rdi),%rdimov 0x10(%rdi),%rdi的代码了,可以联想到指针,直接用gdb查看内存有:

这里是一棵二叉搜索树,利用word画出如下word真香

汇编翻译出来的c代码如下:

7.4 defuse secret_phase

现在要返回2,可以分析得到答案为0x14或者0x16,也就是20或者22。

最后我的input文件为:

我的输出为:

至此六个phase以及secret_phase都完成了!整个Bomb Lab完美结束!!!


 

8. 写在后面

本人于2021/10/16完成了Bomb Lab,从开始做正好一周左右。个人认为Bomb Lab是一个相当好的练习汇编阅读的实验。虽然并没有什么代码量,只是利用gdb来查看寄存器和内存罢了,但是Bomb Lab在趣味上和几个汇编的设计上都很花心思。从一开始简单的比较字符串到后面switch、链表以及二叉树的实现,循序渐进,相当不错!

个人主要强化了以下几点知识:

个人还有一些猜测:

预计接下来: