目录

  • Phase 1
  • Phase 2
  • Phase 3
  • Phase 4
  • Phase 5
  • Phase 6
  • Secret Phase
  • 几条野路子

下载实验用的文件们戳这里
bomblab的背景很有趣。Dr. Evil把“二进制炸弹”装在了教室的机子里。想要拆掉炸弹,你必须反编译“炸弹”,通过其中的汇编指令推测出可以拆掉炸弹的phrase。
好啦,我们看一看下载的文件夹里都有什么。bomb就是要我们去拆的“炸弹”;bomb.c是炸弹的源代码,但是最为关键的部分被删掉了,只保留了骨架。gdbnotes-x86-64是个重要的文件,里面有这个实验里用到的各种工具的使用方法。剩下的就是实验的各种说明了~

Phase 1

我们来看一看phase_1的反汇编是什么样子。一种方法是直接在终端里输入"objdump -d bomb > bomb.txt", 直接把objdump的输出重定向到bomb.txt里,之后就可以直接在记事本里面看反汇编了;或者,先在终端里输入"gdb bomb", 进到gdb里面;再输入"disas phase_1", 就能看到phase_1的反汇编了。我们这里用第一种方法,最终在记事本里看到的结果是下面这个样子:

0000000000400ee0 <phase_1>:400ee0: 48 83 ec 08             sub    $0x8,%rsp400ee4: be 00 24 40 00          mov    $0x402400,%esi400ee9:    e8 4a 04 00 00          callq  401338 <strings_not_equal>400eee:  85 c0                   test   %eax,%eax400ef0: 74 05                   je     400ef7 <phase_1+0x17>400ef2:  e8 43 05 00 00          callq  40143a <explode_bomb>400ef7:   48 83 c4 08             add    $0x8,%rsp400efb: c3                      retq

那我们就先来捋顺这段汇编的逻辑吧。这段代码先把一个值,更准确地说,是字符串的地址(0x402400)放到%esi里,之后调用一个叫"strings_not_equal"的函数;最后判断这个函数的返回值:等于零,通过;不等于零,调用"explode_bomb"把炸弹炸掉(或许你会问%edi在哪里?其实%edi就是我们输入的字符串的地址)。显然,这短短的一段汇编里,最重要的就是对strings_not_equal函数的调用。至于这个函数是干什么的,猜也能猜得出来:判断两个字符串是不是相等:相等,返回零;否则返回非零(仔细看strings_not_equal的实现,实际上不相等时返回1)(当然你也可以自己翻到0x401338,看一看这个推测是否正确。)。
现在让我们考虑一下strings_not_equal这个函数的两个参数。%rdi中的值,就是我们输入的字符串的地址;%rsi中的值是后面传进去的0x402400.那么0x402400指向的是什么字符串呢?我们打开gdb看一看。在终端中输入:

gdb bomb
x /s 0x402400

输出是什么呢?

0x402400:    "Border relations with Canada have never been better."

只要我们的输入和上面这个字符串相同就行了。打开终端试一试~
(P.S. 这个字符串是2016年初更新的。它的出处是,2001年时任美国总统的乔治·布什,为欢迎加拿大总理访美所作的讲话。原句为"Border relations between Canada and Mexico have never been better. " 5年后,布什签署法案,授权在美墨边境修建隔离墙。2015年9月,特朗普宣布竞选美国总统,而其竞选承诺中有一条就是“在美墨边境修墙”。从这个细节,我们似乎可以一瞥作者对美墨边境相关政策的态度。)

Phase 2

前面那个Phase就当热身啦,接下来的几个Phase才是重头戏。还是刚才的办法,我们把phase_2的汇编也拿出来:

0000000000400efc <phase_2>:400efc: 55                      push   %rbp400efd:  53                      push   %rbx400efe:  48 83 ec 28             sub    $0x28,%rsp400f02:    48 89 e6                mov    %rsp,%rsi400f05: e8 52 05 00 00          callq  40145c <read_six_numbers>...                           ...

这段汇编先“读入“六个数(0x400f05:read_six_numbers)。从哪里“读入”呢?当然不是stdin。我们注意到,和上面那个Phase一样,%rdi并没有在调用这个函数之前出现。也就是说,phase_2函数的第一个参数,被原封不动地传到了read_six_numbers的第一个参数。那么%rsi,也就是第二个参数,代表的是什么呢?注意看这两行汇编:

  400efe:    48 83 ec 28             sub    $0x28,%rsp400f02:    48 89 e6                mov    %rsp,%rsi

知道了吗?%rsi里放的是地址!结合read_six_numbers第一个参数的含义(恰好是我们输入的字符串)大胆猜想,我们可以知道—— 1. read_six_numbers从我们自己输进去的字符串里"read"; 2. %rsi在源代码里应该是个指针,这个指针指向一个数组的开头,而这个数组就是放read_six_numbers从字符串里抽出的六个数用的。不过我们还是来看一看read_six_numbers具体是怎么实现的:

000000000040145c <read_six_numbers>:40145c:    48 83 ec 18             sub    $0x18,%rsp401460:    48 89 f2                mov    %rsi,%rdx401463: 48 8d 4e 04             lea    0x4(%rsi),%rcx401467:    48 8d 46 14             lea    0x14(%rsi),%rax40146b:   48 89 44 24 08          mov    %rax,0x8(%rsp)401470:    48 8d 46 10             lea    0x10(%rsi),%rax401474:   48 89 04 24             mov    %rax,(%rsp)401478:   4c 8d 4e 0c             lea    0xc(%rsi),%r940147c: 4c 8d 46 08             lea    0x8(%rsi),%r8401480: be c3 25 40 00          mov    $0x4025c3,%esi401485:    b8 00 00 00 00          mov    $0x0,%eax40148a: e8 61 f7 ff ff          callq  400bf0 <__isoc99_sscanf@plt>40148f:   83 f8 05                cmp    $0x5,%eax401492: 7f 05                   jg     401499 <read_six_numbers+0x3d>401494: e8 a1 ff ff ff          callq  40143a <explode_bomb>401499:   48 83 c4 18             add    $0x18,%rsp40149d:    c3                      retq

%rsi是什么?我们猜是数组首个元素的地址。0x4(%rsi)是什么?我们猜是数组第二个元素的地址。那么0x14(%rsi)、0x10(%rsi)等等呢?同理。以上这些都被read_six_numbers做成了sscanf的末几个参数(不是"scanf"!多了一个"s"!!!)(注意到了吗?有几个地址,因为寄存器放不下,被送到栈里面了)。sscanf的第一个参数,从汇编来看,是read_six_numbers的第一个参数,也就是我们输入的字符串;而第二个参数是存储在0x4025c3的字符串。和上面一样,我们来看一看0x4025c3里有什么。

0x4025c3:    "%d %d %d %d %d %d"

觉得熟悉吗?事实上,sscanf的作用与scanf类似,只不过scanf从sdtin中读取数据,sscanf从它的第一个参数中读取数据。read_six_numbers就是借助sscanf,把我们输入的字符串中的数抽出来,放到一个数组里。我们的猜测是对的。
现在,我们知道了phase_2的要求是输入6个特定的数,数与数之间用空格隔开。那么这六个数又是什么呢?我们接着往下看。

  ...                                ...400f0a:  83 3c 24 01             cmpl   $0x1,(%rsp)400f0e:   74 20                   je     400f30 <phase_2+0x34>400f10:  e8 25 05 00 00          callq  40143a <explode_bomb>400f15:   eb 19                   jmp    400f30 <phase_2+0x34>...                                  ...

我们已经知道了,%rsp里放的就是数组第一个元素的地址。所以说,很显然,这段汇编在判断我们输入的六个数,第一个数是不是等于一。判断了之后就跳到0x400f30:

  ...                            ...400f30:  48 8d 5c 24 04          lea    0x4(%rsp),%rbx400f35:    48 8d 6c 24 18          lea    0x18(%rsp),%rbp400f3a:   eb db                   jmp    400f17 <phase_2+0x1b>...                               ...

嗯,这三行汇编把第二个数(int是四个字节)的地址放进%rbx里,把最后一个数下一个字节((18)16=(24)10(18)_{16} = (24)_{10}(18)16​=(24)10​)的地址放进%rbp里(是判断循环终止条件用的),之后跳到0x400f17.

  ...                           ...400f17:   8b 43 fc                mov    -0x4(%rbx),%eax400f1a:   01 c0                   add    %eax,%eax400f1c: 39 03                   cmp    %eax,(%rbx)400f1e:   74 05                   je     400f25 <phase_2+0x29>400f20:  e8 15 05 00 00          callq  40143a <explode_bomb>400f25:   48 83 c3 04             add    $0x4,%rbx400f29: 48 39 eb                cmp    %rbp,%rbx400f2c: 75 e9                   jne    400f17 <phase_2+0x1b>...                           ...

从0x400f17这里,程序就开始循环处理我们输进去的六个数了。首先,程序把%rbx指向的数的前一个放到%eax里,之后让它翻倍:

  400f17:    8b 43 fc                mov    -0x4(%rbx),%eax400f1a:   01 c0                   add    %eax,%eax

翻倍之后再检查%eax里的值是不是和%rbx当前指向的数相同:

  400f1c:    39 03                   cmp    %eax,(%rbx)400f1e:   74 05                   je     400f25 <phase_2+0x29>400f20:  e8 15 05 00 00          callq  40143a <explode_bomb>

如果相同呢,就跳到0x400f25;如果不相同,就调用explode_bomb引爆炸弹。看来,这好像是个等比数列!1为首项,2为公比!
我们最后再来看一看0x400f25那里的汇编是什么。

  400f25:    48 83 c3 04             add    $0x4,%rbx400f29: 48 39 eb                cmp    %rbp,%rbx400f2c: 75 e9                   jne    400f17 <phase_2+0x1b>

和我们想得一样,这里程序把%rbx里的指针后移四字节,再判断指针是否到达了数组末尾。
所以,要过这个phase,只要输入以一为首项,二为公比的等比数列前六项就行了。

Phase 3

这个phase考的是switch语句的汇编表示。
好啦,先看题吧。phase_3开头和上面那个read_six_numbers很像,也调用了sscanf,从我们输入的字符串里提取数字。只不过这次只有两个数,第一个放在0x8(%rsp)那里,第二个放在0xc(%rsp)那里。
仔细看汇编。我们发现,0x8(%rsp)和0xc(%rsp)这两个值,只在两个地方出现过。对于0x8(%rsp), 这个地方是:

  400f71:    8b 44 24 08             mov    0x8(%rsp),%eax400f75:    ff 24 c5 70 24 40 00    jmpq   *0x402470(,%rax,8)

而对于0xc(%rsp), 这个地方是:

  400fbe:    3b 44 24 0c             cmp    0xc(%rsp),%eax400fc2:    74 05                   je     400fc9 <phase_3+0x86>400fc4:  e8 71 04 00 00          callq  40143a <explode_bomb>

在0x400f75这一行,代码究竟要跳转到哪里呢?应该是0x402470这个地址中储存的值(星号解引用,和指针一样),和8乘%rax里的值加在一起组成的地址。%rax里的值我们知道,就是我们输入的第一个数;那么0x402470这个地址中的值又是什么呢?还是像前面那样,我们在gdb中输入x /wx 0x402470,看一看输出:

(gdb) x /wx 0x402470
0x402470:   0x00400f7c

0x400f7c显然是个地址。翻回到bomb的汇编,我们发现这个地址就是0x400f75的下一行;而这行恰巧对应switch的第一个case。其实,从0x402470这个地址开始,储存着7个指向不同case的地址。而从汇编来看,这7个case处理输入输出的逻辑是一致的,比如第一个case,对应"case 0:",也就是我们输入的首个值为零的情况:

  400f7c:    b8 cf 00 00 00          mov    $0xcf,%eax400f81:    eb 3b                   jmp    400fbe <phase_3+0x7b>

这两行汇编的逻辑,想必不难理解。先把一个数放到%eax里,之后跳到0x400fbe。当然0x400fbe那里有什么,上面已经提到过了——程序在那里处理我们输入的第二个数!
这样看来,程序的逻辑就清楚了。我们需要先输入两个数,第一个指示应该用哪个case;第二个用来和这个case放到%eax里的数比较。如果相等,这个phase就过掉了;不相等就引爆炸弹。
所以,这个phase的答案也不是唯一的啦。每一个case对应不同的值,只要我们输入的两个值像汇编里的值那样对应好就行。比如我们输入的第一个数是0(小于七的非负数就好),查过第一个case之后(0x400f7c)我们就知道第二个数应该是0xcf,也就是十进制的207.之后把"0 207"输进去就行了~

Phase 4

加油加油!phase已经做掉一半啦~
嗯,关于这个phase,我只能说我先看到了bomb.c里的一段注释:

    /* Oh yeah?  Well, how good is your math?  Try on this saucy problem! */

我的数学水平……嗯,一言难尽。还是来看汇编吧。phase_4和上面phase_3的开头是类似的,也是让我们输入两个数,并且要求第一个数不大于14,第二个数等于零(作者大概是想重用上面的字符串?还是另有原因呢……)。之后程序调用func4。func4第一个参数就是我们输入的第一个数,第二个、第三个参数都是常数,是0和14(%edx里的那个)。func4返回之后,程序判断func4的返回值,等于零就通过,否则引爆炸弹。
好了,我们现在来看一看func4里到底发生了什么。

0000000000400fce <func4>:400fce:   48 83 ec 08             sub    $0x8,%rsp400fd2: 89 d0                   mov    %edx,%eax400fd4: 29 f0                   sub    %esi,%eax400fd6: 89 c1                   mov    %eax,%ecx400fd8: c1 e9 1f                shr    $0x1f,%ecx400fdb:    01 c8                   add    %ecx,%eax400fdd: d1 f8                   sar    %eax400fdf:  8d 0c 30                lea    (%rax,%rsi,1),%ecx400fe2:    39 f9                   cmp    %edi,%ecx400fe4: 7e 0c                   jle    400ff2 <func4+0x24>400fe6:    8d 51 ff                lea    -0x1(%rcx),%edx400fe9:   e8 e0 ff ff ff          callq  400fce <func4>400fee:  01 c0                   add    %eax,%eax400ff0: eb 15                   jmp    401007 <func4+0x39>400ff2:    b8 00 00 00 00          mov    $0x0,%eax400ff7: 39 f9                   cmp    %edi,%ecx400ff9: 7d 0c                   jge    401007 <func4+0x39>400ffb:    8d 71 01                lea    0x1(%rcx),%esi400ffe:    e8 cb ff ff ff          callq  400fce <func4>401003:  8d 44 00 01             lea    0x1(%rax,%rax,1),%eax401007: 48 83 c4 08             add    $0x8,%rsp40100b: c3                      retq

当然这段汇编很乱……我最后把它翻译成了C语言代码:

int func4(int edi, int esi, int edx)
{int eax = edx;eax -= esi;int ecx = eax;ecx >>= 31;eax += ecx;eax >>= 1;ecx = eax + esi;if (ecx > edi){edx = ecx - 1;eax = func4(edi, esi, edx);eax *= 2;return eax;}else{eax = 0;if(ecx < edi){esi = ecx + 1;eax = func4(edi, esi, edx);eax = eax * 2 + 1;return eax;}elsereturn eax;}
}

其实改一改就更好理解了:

int func4(int edi, int esi, int edx)
{int eax = edx - esi;if (eax < 0)eax -= 1;eax /= 2;int ecx = eax + esi;if (ecx > edi)return func4(edi, esi, ecx - 1) * 2;else if (ecx < edi)return func4(edi, ecx + 1, edx) * 2 + 1;elsereturn 0;
}

或许这个函数有它的现实意义(我很想知道!)。不过现在我们只要让它返回零……这个简单。既然edx是14,esi是0,那么开头三行代码执行完之后ecx就是7. 什么情况下func4会返回零呢?当然是ecx等于edi的情况。这样我们就可以确定,edi是7!
(实际上满足题意的数不仅仅是7. 写循环把0到14的值都试一遍就知道了。不过我觉得作者的本意可能是让我们先推出func4的数学表达式……)

Phase 5

phase 5呢,让我们先输入一个长为六的字符串,之后程序就进到循环里操作这些字符串了:

  40108b:    0f b6 0c 03             movzbl (%rbx,%rax,1),%ecx40108f:    88 0c 24                mov    %cl,(%rsp)401092:    48 8b 14 24             mov    (%rsp),%rdx401096:   83 e2 0f                and    $0xf,%edx401099: 0f b6 92 b0 24 40 00    movzbl 0x4024b0(%rdx),%edx4010a0:   88 54 04 10             mov    %dl,0x10(%rsp,%rax,1)        4010a4: 48 83 c0 01             add    $0x1,%rax4010a8: 48 83 f8 06             cmp    $0x6,%rax4010ac: 75 dd                   jne    40108b <phase_5+0x29>

%rbx的地址就是我们输入的字符串的地址,%rax里是字符的索引。每轮循环,程序先把字符串的一个字符放到%edx里((%rsp)其实就是个中介),之后用0xf按位与。之后再把按位与的结果当成索引,从0x4024b0那里的字符串拿出在对应位置的字节,放到一个数组里(就是0x10(%rsp))。当然这个数组也相当于一个字符串。我们猜测,循环结束之后,程序很有可能会把这个这个字符串和某个特定的字符串比较。不过我们看一看0x4024b0那里的字符串是什么。

(gdb) x /s 0x4024b0
0x4024b0 <array.3449>:    "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"

嗯,有点乱,对不对?现在我们再来看循环结束之后程序又做了什么。

  4010ae:    c6 44 24 16 00          movb   $0x0,0x16(%rsp)4010b3:   be 5e 24 40 00          mov    $0x40245e,%esi4010b8:    48 8d 7c 24 10          lea    0x10(%rsp),%rdi4010bd:   e8 76 02 00 00          callq  401338 <strings_not_equal>4010c2:  85 c0                   test   %eax,%eax4010c4: 74 13                   je     4010d9 <phase_5+0x77>4010c6:  e8 6f 03 00 00          callq  40143a <explode_bomb>

看到了没有?在这里,程序调用了strings_not_equal,而它的第一个参数是前面新生成的字符串,第二个参数是0x40245e那里的一个字符串。现在我们来看一看0x40245e那里是什么:

(gdb) x /s 0x40245e
0x40245e:   "flyers"

再翻回到前面那个有点乱的的字符串,这里的六个字母前面都有对不对?所以,我们首先要逐一找到flyers里的六个字母在前面那个字符串里对应的索引;之后从可打印的字符中找到六个,让它们和0xf按位与的结果,恰好等于那些索引就行了。
好啦,我们很容易找到这六个字母对应的索引是“9 15 14 5 6 7”,而它们对应的二进制值是“1001 1111 1110 101 110 111“。0xf对应的二进制值是“1111”。什么样的字符是满足要求的呢?我们知道,可打印字符ascii码的范围是33-126,也就是说,只要我们在这个范围内,找到二进制表示下末尾几位和上面的值相同的数就可以了(所以说,这个phase答案也是不唯一的)。其实也不用太费心地去找,只要在上面那些二进制索引之前,都添上两个一就行(三位的先在前面添上0补成四位)。所以我们最后得到的字符串是“9/>567“。试试吧~

Phase 6

phase_6好长好长,而且循环好多好多!不过没关系,我们分块来看这些不知所云的汇编。
phase_6也是要求我们输入6个数,而且调用了前面的read_six_numbers. 我们知道,read_six_numbers这个函数会把读入的六个数放到第二个参数指向的一块内存里。在这里,第二个参数是%rsp的值;而在调用read_six_numbers之前,%rsp的值已经被同时拷到%r13里了。所以,%r13实际上包含了指向我们输入的六个数的地址。这个下面会用到。
在非常耐心地盯了很长时间汇编之后,我们发现第一个循环从0x401114开始,到0x401151结束。仔细捋里面的代码,我们发现其实从0x401135到0x40114b又是一个循环。那么这两个循环是干什么用的呢?我们先把这段汇编贴出来:

  401114:    4c 89 ed                mov    %r13,%rbp401117: 41 8b 45 00             mov    0x0(%r13),%eax40111b:    83 e8 01                sub    $0x1,%eax40111e: 83 f8 05                cmp    $0x5,%eax401121: 76 05                   jbe    401128 <phase_6+0x34>401123:  e8 12 03 00 00          callq  40143a <explode_bomb>401128:   41 83 c4 01             add    $0x1,%r12d40112c:    41 83 fc 06             cmp    $0x6,%r12d401130:    74 21                   je     401153 <phase_6+0x5f>401132:  44 89 e3                mov    %r12d,%ebx--------------------------------------------------------------401135:  48 63 c3                movslq %ebx,%rax401138: 8b 04 84                mov    (%rsp,%rax,4),%eax40113b:    39 45 00                cmp    %eax,0x0(%rbp)40113e:    75 05                   jne    401145 <phase_6+0x51>401140:  e8 f5 02 00 00          callq  40143a <explode_bomb>401145:   83 c3 01                add    $0x1,%ebx401148: 83 fb 05                cmp    $0x5,%ebx40114b: 7e e8                   jle    401135 <phase_6+0x41>--------------------------------------------------------------40114d:    49 83 c5 04             add    $0x4,%r13401151: eb c1                   jmp    401114 <phase_6+0x20>

我们已经知道,%r13里是我们输入的第一个数的地址(放在循环里来说,就是本轮迭代中检查到的那个数);所以前几行汇编的意思不难理解,就是检查一下我们输入的数是不是比六大。检查了之后呢,就把当前数的索引值加一,再放到%ebx里。重头戏在两条虚线之间的嵌套循环那里:先把%ebx里的索引放到%rax里;之后算出上面查过的数的地址,通过地址找到这个数,放到%eax里;之后把%eax和0x0(%rbp)中的中进行比较(当然你很可能会觉得%rbp出现得非常突兀——其实0x0(%rbp)指向的也是我们在上面检查过的那个数)。如果相等,就引爆炸弹。我们再来观察后面控制循环的指令。直到%ebx等于五,也就是查到了我们输入的最后一个数,循环才终止。之后程序把%r13加上4(移到下一个数),开始新一轮循环。
这样描述是不是有些抽象?没关系,我们把这段汇编翻译成C语言:

int six_numbers[6] = { /* 我们输入的六个数 */ };
for (int i = 0; i < 6; ++i)
{if (six_numbers[i] > 6)explode_bomb();for (int j = i + 1; j < 6; ++j)if (six_numbers[i] == six_numbers[j])explode_bomb();
}

所以说,这段汇编的实际作用是确定我们输入的六个数是不是全部小于等于六,并且是不是互不相等。这样的要求和数组索引的要求好像!那么这组数究竟是不是真正的索引呢?我们接着往下看。

  401153:    48 8d 74 24 18          lea    0x18(%rsp),%rsi401158:   4c 89 f0                mov    %r14,%rax40115b: b9 07 00 00 00          mov    $0x7,%ecx-------------------------------------------------------------401160:    89 ca                   mov    %ecx,%edx401162: 2b 10                   sub    (%rax),%edx401164:   89 10                   mov    %edx,(%rax)          401166: 48 83 c0 04             add    $0x4,%rax40116a: 48 39 f0                cmp    %rsi,%rax40116d: 75 f1                   jne    401160 <phase_6+0x6c> -------------------------------------------------------------   40116f: be 00 00 00 00          mov    $0x0,%esi401174: eb 21                   jmp    401197 <phase_6+0xa3>

这段汇编里也有一段循环。循环初始,%ecx的值是7,%rax指向我们输入的六个数;之后的操作就是用7减去每一个我们输入的数……(401162,401164;循环的控制语句是401166到40116d)如果这六个数真是索引的话,Dr. Evil您可真会玩儿……
紧挨着这段汇编的又是一段循环;注意这段循环是从中间的401197开始执行的:

  401176:    48 8b 52 08             mov    0x8(%rdx),%rdx40117a:    83 c0 01                add    $0x1,%eax40117d: 39 c8                   cmp    %ecx,%eax40117f: 75 f5                   jne    401176 <phase_6+0x82>401181:  eb 05                   jmp    401188 <phase_6+0x94>401183:  ba d0 32 60 00          mov    $0x6032d0,%edx401188:    48 89 54 74 20          mov    %rdx,0x20(%rsp,%rsi,2)40118d:    48 83 c6 04             add    $0x4,%rsi401191: 48 83 fe 18             cmp    $0x18,%rsi401195:    74 14                   je     4011ab <phase_6+0xb7>401197:  8b 0c 34                mov    (%rsp,%rsi,1),%ecx40119a:    83 f9 01                cmp    $0x1,%ecx40119d: 7e e4                   jle    401183 <phase_6+0x8f>40119f:  b8 01 00 00 00          mov    $0x1,%eax4011a4: ba d0 32 60 00          mov    $0x6032d0,%edx4011a9:    eb cb                   jmp    401176 <phase_6+0x82>

401197把刚才洗过的数放到%ecx里,之后把它和一比较。如果这个数小于等于一,就跳到401183. 否则,就把两个数分别放到两个寄存器里,之后跳到401176. 顺着这两个分支分别看过去,一个神奇的数引起了我们的注意:0x6032d0. 这种无厘头的魔数,出现在这里,超级像地址(“你确信吗?”),但是我们现在还没有证据。不过我们接着往下看大于一程序会往哪里走(等于一太特殊了,看大于一的一般情况就够了)。40119f那里,程序把两个数分别放到两个寄存器里,就朝着401176去了;401176的指令,证明了我们的猜想:确实,0x6032d0是个地址。但是为什么是 “mov 0x8(%rdx),%rdx” 呢?为什么偏移量偏偏就是8呢?我们接着往下看。40117a,程序把%eax加上了一,之后,40117d判断%ecx是否和%eax相等。不相等,就回到401176?看来,0x8(%rdx)可能也是个地址呢……因为之前放到%rdx里的值又被当成地址引用了一次……那么,如果两个值相等呢?程序会跳到401188,把%rdx里的地址放到内存里的某个位置,看起来像个数组。到底是什么样的地址值得如此大存特存呢?我们还是打开gdb查一查:

(gdb) x /wx 0x6032d0
0x6032d0 <node1>: 0x0000014c

嗯,gdb提示0x6032d0属于一个叫node1的变量。这下清楚了……敢情phase 6这儿有个链表……
我们继续看其他的地址:

(gdb) x /24wx 0x6032d0
0x6032d0 <node1>: 0x0000014c | 0x00000001 |   0x006032e0  0x00000000
0x6032e0 <node2>: 0x000000a8 | 0x00000002 |   0x006032f0  0x00000000
0x6032f0 <node3>: 0x0000039c | 0x00000003 |   0x00603300  0x00000000
0x603300 <node4>: 0x000002b3 | 0x00000004 | 0x00603310    0x00000000
0x603310 <node5>: 0x000001dd | 0x00000005 |   0x00603320  0x00000000
0x603320 <node6>: 0x000001bb | 0x00000006 |   0x00000000  0x00000000

这架势……单向链表实锤……
看起来nodeX开头四字节是节点存储的值,最后八字节是下个节点的地址,中间四字节相当于索引值。
看来node结构在源文件中应该是这样定义的:

typedef struct node
{int key;int index;struct node* next;
} node;

之后弄了不少全局变量:

node node6 = { 6, 0x1bb, NULL   };
node node5 = { 5, 0x1dd, &node6 };
node node4 = { 4, 0x2b3, &node5 };
node node3 = { 3, 0x39c, &node4 };
node node2 = { 2, 0xa8,  &node3 };
node node1 = { 1, 0x14c, &node2 };

这样,你应该就清楚了前面那个嵌套循环的具体作用——根据我们输入的索引,找到对应node变量的地址,并且把这个地址放到一个数组里。如果写成C语言就是这样的:

node* addresses[6] = { 0 }; //存node变量地址的数组
for (int i = 0; i < 6; ++i)
{int index = six_numbers[i];addresses[i] = &node1;while (addresses[i]->index != index)addresses[i] = addresses[i]->next;
}

没办法,C语言里可可爱爱的几行,放在汇编里就是不知所云的一大片。
现在我们可以说是搞定了整个phase 6最难的一部分!胜利在望!
接下来的四行,是折腾地址用的。0x20(%rsp)里放的相当于是addresses[0]的地址;0x28(%rsp)是addresses[1]的地址;这样一来,%rbx里放的就是addresses[0];%rax里放的就是(addresses + 1)。

  4011ab:    48 8b 5c 24 20          mov    0x20(%rsp),%rbx4011b0:   48 8d 44 24 28          lea    0x28(%rsp),%rax4011b5:   48 8d 74 24 50          lea    0x50(%rsp),%rsi4011ba:   48 89 d9                mov    %rbx,%rcx

下面又是一个循环。不过比起之前我们分析的那些,下面这几个就是小巫见大巫了。

  4011bd:    48 8b 10                mov    (%rax),%rdx4011c0:   48 89 51 08             mov    %rdx,0x8(%rcx)4011c4:    48 83 c0 08             add    $0x8,%rax 4011c8:    48 39 f0                cmp    %rsi,%rax4011cb: 74 05                   je     4011d2 <phase_6+0xde>4011cd:  48 89 d1                mov    %rdx,%rcx4011d0: eb eb                   jmp    4011bd <phase_6+0xc9>4011d2:  48 c7 42 08 00 00 00    movq   $0x0,0x8(%rdx)

这边循环折腾寄存器确实很乱……先从%rax指向的位置拿出某个node变量的地址,取道%rdx放到0x8(%rcx)里。那么这个0x8(%rcx)相当于什么呢?我们可以看出,%rcx里放的总是addresses数组里%rax指向元素的上一个,是某个node变量的地址;那么0x8(%rcx)就是nodeX.next. 原本nodeX.next里放的应该是node(X + 1)的地址对不对?假设我们输入的索引是" X Y Z…", 那么一轮循环执行完之后,nodeX.next里放的就是nodeY的地址了。这个循环是重排节点用的。
我们终于来到了最后一个循环(眼睛觉得不行的话,就休息一下吧……马上就结束了)。这个循环是最后判断我们输入的索引顺序对不对的。

  4011da:    bd 05 00 00 00          mov    $0x5,%ebp4011df: 48 8b 43 08             mov    0x8(%rbx),%rax4011e3:    8b 00                   mov    (%rax),%eax4011e5:   39 03                   cmp    %eax,(%rbx)4011e7:   7d 05                   jge    4011ee <phase_6+0xfa>4011e9:  e8 4c 02 00 00          callq  40143a <explode_bomb>4011ee:   48 8b 5b 08             mov    0x8(%rbx),%rbx4011f2:    83 ed 01                sub    $0x1,%ebp4011f5: 75 e8                   jne    4011df <phase_6+0xeb>

%rbx指向我们输入的第一个索引对应的节点。4011df把nodeX.next放到%rax里,再通过这个地址拿出%rax指向的节点的key值,把它和%rbx指向节点的key比较,如果前者大与等于后者就通过。
所以这个循环要求我们降序排列节点中的key值。翻回到前面看一看各个索引对应的key值,排好序就行了。别忘了输入之前每个值都要用7减哦。(所以最后结果是"4 3 2 1 6 5")

Secret Phase

Dr. Evil提示我们,可能有什么东西被忽略了:

    /* Wow, they got it!  But isn't something... missing?  Perhaps* something they overlooked?  Mua ha ha ha ha! */

翻到汇编一看,还真是。phase_6之后还有一个secret_phase. 我们先来搜一下这个函数是在哪里调用的:
嗯,是在phase_defused里面。现在让我们来研究一下什么情况下会触发secret_phase:

  4015e1:    4c 8d 44 24 10          lea    0x10(%rsp),%r84015e6:    48 8d 4c 24 0c          lea    0xc(%rsp),%rcx4015eb:    48 8d 54 24 08          lea    0x8(%rsp),%rdx4015f0:    be 19 26 40 00          mov    $0x402619,%esi4015f5:    bf 70 38 60 00          mov    $0x603870,%edi4015fa:    e8 f1 f5 ff ff          callq  400bf0 <__isoc99_sscanf@plt>4015ff:   83 f8 03                cmp    $0x3,%eax401602: 75 31                   jne    401635 <phase_defused+0x71>401604:    be 22 26 40 00          mov    $0x402622,%esi401609:    48 8d 7c 24 10          lea    0x10(%rsp),%rdi40160e:   e8 25 fd ff ff          callq  401338 <strings_not_equal>401613:  85 c0                   test   %eax,%eax401615: 75 1e                   jne    401635 <phase_defused+0x71>401617:    bf f8 24 40 00          mov    $0x4024f8,%edi40161c:    e8 ef f4 ff ff          callq  400b10 <puts@plt>401621:  bf 20 25 40 00          mov    $0x402520,%edi401626:    e8 e5 f4 ff ff          callq  400b10 <puts@plt>40162b:  b8 00 00 00 00          mov    $0x0,%eax401630: e8 0d fc ff ff          callq  401242 <secret_phase>

phase_defused调用sscanf处理在0x603870那里的一个字符串。之后把sscanf的第五个参数(输出参数)和0x402622那里的一个字符串比较,相等就可以触发phase_defused。那么我们先来看一看0x603870那里有什么。


我们先在合适的位置设好断点,之后按顺序输入之前的字符串。之后程序会在调用sscanf之前停下。输出0x603870那里的字符串,我们发现它和我们在phase_4中输入的内容是一致的。再看一看sscanf的格式说明符,我们发现只要我们再在phase_4的两个数后面加上一个合适的字符串,就能触发secret_phase了。那么这个字符串是什么呢?自己看一看0x402622那里的字符串~ 于是我们顺利地来到了secret_phase~
secret_phase调用了strtol,把我们输入的字符串转换成数(具体用法自行百度)。里面还有一行是检查这个数的大小的,不能大于1001(好有童话色彩的魔数!)。查完就调用fun7(没少敲"c"!!!真的是fun!),fun7返回之后检查返回值,等于二就通过。
那我们就看一看fun7吧。fun7的第一个参数是0x6030f0(又是魔数!),第二个参数是strtol的返回值。和func4类似,fun7有令人头疼的递归结构:

  401204:    48 83 ec 08             sub    $0x8,%rsp401208: 48 85 ff                test   %rdi,%rdi40120b: 74 2b                   je     401238 <fun7+0x34>40120d: 8b 17                   mov    (%rdi),%edx40120f:   39 f2                   cmp    %esi,%edx401211: 7e 0d                   jle    401220 <fun7+0x1c>401213: 48 8b 7f 08             mov    0x8(%rdi),%rdi401217:    e8 e8 ff ff ff          callq  401204 <fun7>40121c:   01 c0                   add    %eax,%eax40121e: eb 1d                   jmp    40123d <fun7+0x39>401220: b8 00 00 00 00          mov    $0x0,%eax401225: 39 f2                   cmp    %esi,%edx401227: 74 14                   je     40123d <fun7+0x39>401229: 48 8b 7f 10             mov    0x10(%rdi),%rdi40122d:   e8 d2 ff ff ff          callq  401204 <fun7>401232:   8d 44 00 01             lea    0x1(%rax,%rax,1),%eax401236: eb 05                   jmp    40123d <fun7+0x39>401238: b8 ff ff ff ff          mov    $0xffffffff,%eax40123d:  48 83 c4 08             add    $0x8,%rsp401241: c3                      retq

根据之前的经验,我们猜测%rdi里放着的应该也是个指向结构的指针,因为%rdi指向的值拿出来还是可以当成地址引用。所以经过大胆的联想与想象,我们猜测fun7原本应该是这样写的:

int fun7(node* rdi, int rsi)
{if (rdi == NULL)return -1;int key = rdi->key;if (rsi < key){rdi = /* 不知道怎么写…… */;return 2 * fun7(rdi, rsi) + 1;}else if (rsi == key)return 0;elsereturn 2 * fun7(rdi->next /* 是吗…… */ , rsi);
}

确实,基本结构是这样。不过如果你假设这里的数据结构还是上面的单向链表的话,你会发现"mov 0x10(%rdi),%rdi"这行汇编用C语言是描述不出来的……因为如果假设%rdi指向某个节点,那么0x10(%rdi)将指向下个节点的key值,这就相当于直接把一个int值当成了地址……
所以,我们去看一看那个魔数(0x6030f0)附近到底有什么……

(gdb) x /120wx 0x6030f0
0x6030f0 <n1>:        0x00000024  0x00000000  0x00603110  0x00000000
0x603100 <n1+16>:    0x00603130  0x00000000  0x00000000  0x00000000
0x603110 <n21>:       0x00000008  0x00000000  0x00603190  0x00000000
0x603120 <n21+16>:   0x00603150  0x00000000  0x00000000  0x00000000
0x603130 <n22>:       0x00000032  0x00000000  0x00603170  0x00000000
0x603140 <n22+16>:   0x006031b0  0x00000000  0x00000000  0x00000000
0x603150 <n32>:       0x00000016  0x00000000  0x00603270  0x00000000
0x603160 <n32+16>:   0x00603230  0x00000000  0x00000000  0x00000000
0x603170 <n33>:       0x0000002d  0x00000000  0x006031d0  0x00000000
0x603180 <n33+16>:   0x00603290  0x00000000  0x00000000  0x00000000
0x603190 <n31>:       0x00000006  0x00000000  0x006031f0  0x00000000
0x6031a0 <n31+16>:   0x00603250  0x00000000  0x00000000  0x00000000
0x6031b0 <n34>:       0x0000006b  0x00000000  0x00603210  0x00000000
0x6031c0 <n34+16>:   0x006032b0  0x00000000  0x00000000  0x00000000
0x6031d0 <n45>:       0x00000028  0x00000000  0x00000000  0x00000000
0x6031e0 <n45+16>:   0x00000000  0x00000000  0x00000000  0x00000000
0x6031f0 <n41>:       0x00000001  0x00000000  0x00000000  0x00000000
0x603200 <n41+16>:   0x00000000  0x00000000  0x00000000  0x00000000
0x603210 <n47>:       0x00000063  0x00000000  0x00000000  0x00000000
0x603220 <n47+16>:   0x00000000  0x00000000  0x00000000  0x00000000
0x603230 <n44>:       0x00000023  0x00000000  0x00000000  0x00000000
0x603240 <n44+16>:   0x00000000  0x00000000  0x00000000  0x00000000
0x603250 <n42>:       0x00000007  0x00000000  0x00000000  0x00000000
0x603260 <n42+16>:   0x00000000  0x00000000  0x00000000  0x00000000
0x603270 <n43>:       0x00000014  0x00000000  0x00000000  0x00000000
0x603280 <n43+16>:   0x00000000  0x00000000  0x00000000  0x00000000
0x603290 <n46>:       0x0000002f  0x00000000  0x00000000  0x00000000
0x6032a0 <n46+16>:   0x00000000  0x00000000  0x00000000  0x00000000
0x6032b0 <n48>:       0x000003e9  0x00000000  0x00000000  0x00000000
0x6032c0 <n48+16>:   0x00000000  0x00000000  0x00000000  0x00000000

好吧。这是棵二叉搜索树(相信你一眼就看出来了~)。每个变量前四字节(前八字节也说不定)是节点的key,从第九字节到第十六字节是指向左子结点的指针,从第十七字节开始的八字节是指向右子结点的指针,最后八字节用途不明(可能是对齐用的?)。画出来长这样:

所以fun7应该是这样写的:

int fun7(node* current, int input)
{if (current == NULL)return -1;int key = current->key;if (input > key)return 2 * fun7(current->right, input) + 1;else if (input == key)return 0;elsereturn 2 * fun7(current->left, input);
}

要让fun7返回2,我们应该怎么设计input的值呢?看fun7的C代码我们可以发现,我们的输入只能是树中的某个节点值,否则返回值肯定是负的。另外,我们也可以发现,最后一行,返回(2 * fun7()), 是虚的,只能把原来的值翻倍,而不能从0造出新的值。所以要想凑出2,只能从前面的(2 * fun7() + 1)入手。
我们从树顶的0x24开始,自顶向下地搭一棵递归树。最初调用的fun7()要返回2对不对。此时它面临向左向右两种选择。如果向右呢,就要求之后调用的fun7()要返回0.5——当然这是不可能的,所以只好向左走啦——要求之后调用的fun7()返回1.

好了,现在我们来到了递归树的第二层。这一层,%eax里的值应该是1才行。向左还是向右呢?如果向左走的话,那么要凑出返回值1,那么之后调用的fun7()又要返回0.5了对不对。所以这里要向右走,这时之后调用的fun7()只要返回0就可以了。

什么样的情况下fun7()会返回0呢?很显然,input等于key的时候。所以,8的右子结点值0x16就是答案。
不过,这个答案是唯一的吗?
事实上,从0x16那个节点,还可以再向左走一步到0x14,返回值也满足要求。

所以,secret_phase的答案有两个,20和22.

几条野路子

如果你时间紧任务重,只想快速刷掉这些phase,可以试试下面的方法。

  1. 我们知道,gdb可以查看寄存器或者某个地址的值。既然查看可以,那么修改也应该可以……
    其实gdb里的set命令就是干这个用的。语法是"set [something] = [value]",something那里填的东西和汇编里访问寄存器、内存的语法类似,不过要注意所有百分号都要换成’$’,而立即数之前的’$'去掉.
    我们用phase 1演示一下。先在合适的位置打上断点:

    (gdb) b *0x400eee
    Breakpoint 1 at 0x400eee
    

    没错,0x400eee就是strings_not_equal执行完之后的下一行。之后随便输些什么东西:

    (gdb) r
    Starting program: /media/snowingfield/Data2/csapp-lab/bomb/bomb2
    Welcome to my fiendish little bomb. You have 6 phases with which to blow yourself up. Have a nice day!
    北京欢迎你,为你开天辟地
    

    输完之后敲回车。因为我们设了断点,所以程序会在判断%eax的值之前停下来。当然我们知道现在%eax里的值肯定不是0:

    Breakpoint 1, 0x0000000000400eee in phase_1 ()
    (gdb) p $eax
    $1 = 1
    

    之后就可以用set命令了:

    (gdb) set $eax = 0
    (gdb) p $eax
    $2 = 0
    (gdb) c
    Continuing.
    Phase 1 defused. How about the next one?
    

    成功地骗过了bomb呢。

  2. 当然我们也可以在explode_bomb上做些手脚。我们先看一看explode_bomb的汇编:

    000000000040143a <explode_bomb>:
    40143a: 48 83 ec 08             sub    $0x8,%rsp
    40143e: bf a3 25 40 00          mov    $0x4025a3,%edi
    401443: e8 c8 f6 ff ff          callq  400b10 <puts@plt>
    401448: bf ac 25 40 00          mov    $0x4025ac,%edi
    40144d: e8 be f6 ff ff          callq  400b10 <puts@plt>
    401452: bf 08 00 00 00          mov    $0x8,%edi
    401457: e8 c4 f7 ff ff          callq  400c20 <exit@plt>
    

    explode_bomb是不会返回的,因为它最后调用了exit。如果我们把explode_bomb里的内容全部抹掉,并且直接修改字节码让它正常返回,炸弹不就不会爆炸了吗?所以我们用十六进制文本编辑器打开bomb文件,找到explode_bomb的位置(就是在编辑器里偏移量为143a的位置),并且把里面的内容全部用"90"(nop指令)替代,再把最后一字节改成"C3"(ret指令):

    之后我们只要随便输进一些东西就能过掉phase了。

    直接把火药去掉了呢。

  3. secret_phase不知道怎么进?没关系,我们通过改字节码的方式直接在main调用它。
    观察发现,main后面足足有9字节的nop指令!简直就是为了方便我们设计的好吗!观察一下其他位置的call指令,我们发现call指令占五字节,第一个字节都是"e8",后面跟上四字节的偏移量。不过在开始添指令之前,为了保证main正常返回,我们先把main的最后三个指令向后挪5字节:


    之后算call指令的偏移量 = 0x401242(secret_phase的地址)- 0x400ed1(e8后面那个字节的地址) - 4(e8后跟偏移量的长度,单位是字节) = 0x36d.(第七章有这个公式) 之后我们把"e8 6d 03 00 00"(偏移量按小尾数摆)填到那个5字节的空位里:

保存运行就行了。
突然觉得Dr. Evil应该给他的bomb加个壳的。[暗中观察][狗头][狗头]

深入理解计算机系统-bomblab详解相关推荐

  1. shell181网格划分_【2017年整理】ANSYS中SHELL181单元理解和参数详解.docx

    [2017年整理]ANSYS中SHELL181单元理解和参数详解 ANSYS中SHELL181单元参数详解 SHELL181单元说明: SHELL181单元适合对薄的到具有一定厚度的壳体结构进行分析. ...

  2. 深入理解SVM,详解SMO算法

    今天是机器学习专题第35篇文章,我们继续SVM模型的原理,今天我们来讲解的是SMO算法. 公式回顾 在之前的文章当中我们对硬间隔以及软间隔问题都进行了分析和公式推导,我们发现软间隔和硬间隔的形式非常接 ...

  3. 【NLP】完全解析!Bert Transformer 阅读理解源码详解

    接上一篇: 你所不知道的 Transformer! 超详细的 Bert 文本分类源码解读 | 附源码 中文情感分类单标签 参考论文: https://arxiv.org/abs/1706.03762 ...

  4. 看一遍就理解:动态规划详解

    前言 我们刷leetcode的时候,经常会遇到动态规划类型题目.动态规划问题非常非常经典,也很有技巧性,一般大厂都非常喜欢问.今天跟大家一起来学习动态规划的套路,文章如果有不正确的地方,欢迎大家指出哈 ...

  5. 还原计算机系统,图文详解怎么一键还原电脑系统

    怎么一键还原电脑系统呢?对于精通电脑的人来说就是轻而易举的事情,但对电脑一窍不通的人来说可就是大问题了.不过,不必担心有小编在呢?这就来教你们如何一键还原电脑系统. 很多时候,我们都会需要进行还原电脑 ...

  6. 【MySQL进阶-05】深入理解mvcc机制(详解)

    MySql系列整体栏目 内容 链接地址 [一]深入理解mysql索引本质 https://blog.csdn.net/zhenghuishengq/article/details/121027025 ...

  7. matlab pq变换,PQ变换与DQ变换的理解与推导详解.doc

    p-q变换与d-q变换的理解与推导 120变换和空间向量 120坐标系是一个静止的复数坐标系.120分量首先由莱昂(Lyon)提出,所以亦成为莱昂分量.下面以电流为例说明120变换...为三相电流瞬时 ...

  8. 完全解析!Bert Transformer 阅读理解源码详解

    接上一篇: 你所不知道的 Transformer! 超详细的 Bert 文本分类源码解读 | 附源码 中文情感分类单标签 参考论文: https://arxiv.org/abs/1706.03762 ...

  9. 深入理解计算机系统——bomblab

    https://blog.csdn.net/xbb224007/article/details/80155833 https://blog.csdn.net/weixin_41256413/artic ...

最新文章

  1. 用欧几里得算法求最大公约数_欧几里得算法:GCD(最大公约数),用C ++和Java示例解释...
  2. 51单片机串口通信(字符串接收和发送)
  3. apt命令与yum命令
  4. 【设置字符集】Win7 64位系统安装MySQL5.5.21图解教程
  5. C# 读写excel 用于导入数据库 批量导入导出excel
  6. 怎么创建计算机快捷方式到桌面两种方法,使用脚本主机创建Windows快捷方式 - Windows Client | Microsoft Docs...
  7. 简单高效有用的正则表达
  8. [转载] python numpy np.finfo()函数 eps
  9. oracle 删除xml记录,Oracle之xml的增删改查操作
  10. Windows小技巧 – Win+R提高Windows使用效率
  11. 如何查看Spark日志与排查报错问题
  12. 【双拼】双拼输入法入门指南
  13. peoplesoft笔记
  14. GitHub 中超过3.5万开源代码被投毒
  15. VBA取得EXCEL表格中的行数和列数
  16. 计算机怎样保存文档,【2人回答】怎么在电脑上写文档并保存?-3D溜溜网
  17. (附源码)小程序记账微信小程序 毕业设计180815
  18. zk4元年拆解_减配实锤!Kobe4 开箱+拆解:你凭什么叫Protro?
  19. hadoop学习步骤
  20. 解决word目录右侧页码大小不一致和不对齐的问题

热门文章

  1. 【软考系统架构设计师】计算机网络章节习题集
  2. 最全的TypeScript学习指南
  3. 响应时间过长问题分析
  4. SIMQKE-GR 生成人工波注意事项
  5. monkeyrunner 使用
  6. vue 组件名 下划线_团队Vue组件规范
  7. 魔兽会封python_Python爬取大量数据时,如何防止IP被封?
  8. 微信小程序H5预览页面框架
  9. 基于Nios-II的流水灯实验
  10. Java人力资源管理系统源码 HR源码