深入理解计算机系统-bomblab详解

下载实验用的文件们戳这里
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,%rsp
  400ee4:	be 00 24 40 00       	mov    $0x402400,%esi
  400ee9:	e8 4a 04 00 00       	callq  401338 <strings_not_equal>
  400eee:	85 c0                	test   %eax,%eax
  400ef0:	74 05                	je     400ef7 <phase_1+0x17>
  400ef2:	e8 43 05 00 00       	callq  40143a <explode_bomb>
  400ef7:	48 83 c4 08          	add    $0x8,%rsp
  400efb:	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   %rbp
  400efd:	53                   	push   %rbx
  400efe:	48 83 ec 28          	sub    $0x28,%rsp
  400f02:	48 89 e6             	mov    %rsp,%rsi
  400f05:	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,%rsp
  400f02:	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,%rsp
  401460:	48 89 f2             	mov    %rsi,%rdx
  401463:	48 8d 4e 04          	lea    0x4(%rsi),%rcx
  401467:	48 8d 46 14          	lea    0x14(%rsi),%rax
  40146b:	48 89 44 24 08       	mov    %rax,0x8(%rsp)
  401470:	48 8d 46 10          	lea    0x10(%rsi),%rax
  401474:	48 89 04 24          	mov    %rax,(%rsp)
  401478:	4c 8d 4e 0c          	lea    0xc(%rsi),%r9
  40147c:	4c 8d 46 08          	lea    0x8(%rsi),%r8
  401480:	be c3 25 40 00       	mov    $0x4025c3,%esi
  401485:	b8 00 00 00 00       	mov    $0x0,%eax
  40148a:	e8 61 f7 ff ff       	callq  400bf0 <__isoc99_sscanf@plt>
  40148f:	83 f8 05             	cmp    $0x5,%eax
  401492:	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,%rsp
  40149d:	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),%rbx
  400f35:	48 8d 6c 24 18       	lea    0x18(%rsp),%rbp
  400f3a:	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),%eax
  400f1a:	01 c0                	add    %eax,%eax
  400f1c:	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,%rbx
  400f29:	48 39 eb             	cmp    %rbp,%rbx
  400f2c:	75 e9                	jne    400f17 <phase_2+0x1b>
  ...                           ...

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

  400f17:	8b 43 fc             	mov    -0x4(%rbx),%eax
  400f1a:	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,%rbx
  400f29:	48 39 eb             	cmp    %rbp,%rbx
  400f2c:	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),%eax
  400f75:	ff 24 c5 70 24 40 00 	jmpq   *0x402470(,%rax,8)

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

  400fbe:	3b 44 24 0c          	cmp    0xc(%rsp),%eax
  400fc2:	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,%eax
  400f81:	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,%rsp
  400fd2:	89 d0                	mov    %edx,%eax
  400fd4:	29 f0                	sub    %esi,%eax
  400fd6:	89 c1                	mov    %eax,%ecx
  400fd8:	c1 e9 1f             	shr    $0x1f,%ecx
  400fdb:	01 c8                	add    %ecx,%eax
  400fdd:	d1 f8                	sar    %eax
  400fdf:	8d 0c 30             	lea    (%rax,%rsi,1),%ecx
  400fe2:	39 f9                	cmp    %edi,%ecx
  400fe4:	7e 0c                	jle    400ff2 <func4+0x24>
  400fe6:	8d 51 ff             	lea    -0x1(%rcx),%edx
  400fe9:	e8 e0 ff ff ff       	callq  400fce <func4>
  400fee:	01 c0                	add    %eax,%eax
  400ff0:	eb 15                	jmp    401007 <func4+0x39>
  400ff2:	b8 00 00 00 00       	mov    $0x0,%eax
  400ff7:	39 f9                	cmp    %edi,%ecx
  400ff9:	7d 0c                	jge    401007 <func4+0x39>
  400ffb:	8d 71 01             	lea    0x1(%rcx),%esi
  400ffe:	e8 cb ff ff ff       	callq  400fce <func4>
  401003:	8d 44 00 01          	lea    0x1(%rax,%rax,1),%eax
  401007:	48 83 c4 08          	add    $0x8,%rsp
  40100b:	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;
        }
        else
            return 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;
    else
        return 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),%ecx
  40108f:	88 0c 24             	mov    %cl,(%rsp)
  401092:	48 8b 14 24          	mov    (%rsp),%rdx
  401096:	83 e2 0f             	and    $0xf,%edx
  401099:	0f b6 92 b0 24 40 00 	movzbl 0x4024b0(%rdx),%edx
  4010a0:	88 54 04 10          	mov    %dl,0x10(%rsp,%rax,1)		
  4010a4:	48 83 c0 01          	add    $0x1,%rax
  4010a8:	48 83 f8 06          	cmp    $0x6,%rax
  4010ac:	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,%esi
  4010b8:	48 8d 7c 24 10       	lea    0x10(%rsp),%rdi
  4010bd:	e8 76 02 00 00       	callq  401338 <strings_not_equal>
  4010c2:	85 c0                	test   %eax,%eax
  4010c4:	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,%rbp
  401117:	41 8b 45 00          	mov    0x0(%r13),%eax
  40111b:	83 e8 01             	sub    $0x1,%eax
  40111e:	83 f8 05             	cmp    $0x5,%eax
  401121:	76 05                	jbe    401128 <phase_6+0x34>
  401123:	e8 12 03 00 00       	callq  40143a <explode_bomb>
  401128:	41 83 c4 01          	add    $0x1,%r12d
  40112c:	41 83 fc 06          	cmp    $0x6,%r12d
  401130:	74 21                	je     401153 <phase_6+0x5f>
  401132:	44 89 e3             	mov    %r12d,%ebx
 --------------------------------------------------------------
  401135:	48 63 c3             	movslq %ebx,%rax
  401138:	8b 04 84             	mov    (%rsp,%rax,4),%eax
  40113b:	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,%ebx
  401148:	83 fb 05             	cmp    $0x5,%ebx
  40114b:	7e e8                	jle    401135 <phase_6+0x41>
 --------------------------------------------------------------
  40114d:	49 83 c5 04          	add    $0x4,%r13
  401151:	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),%rsi
  401158:	4c 89 f0             	mov    %r14,%rax
  40115b:	b9 07 00 00 00       	mov    $0x7,%ecx
 -------------------------------------------------------------
  401160:	89 ca                	mov    %ecx,%edx
  401162:	2b 10                	sub    (%rax),%edx
  401164:	89 10                	mov    %edx,(%rax)			
  401166:	48 83 c0 04          	add    $0x4,%rax
  40116a:	48 39 f0             	cmp    %rsi,%rax
  40116d:	75 f1                	jne    401160 <phase_6+0x6c>	
 -------------------------------------------------------------	
  40116f:	be 00 00 00 00       	mov    $0x0,%esi
  401174:	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),%rdx
  40117a:	83 c0 01             	add    $0x1,%eax
  40117d:	39 c8                	cmp    %ecx,%eax
  40117f:	75 f5                	jne    401176 <phase_6+0x82>
  401181:	eb 05                	jmp    401188 <phase_6+0x94>
  401183:	ba d0 32 60 00       	mov    $0x6032d0,%edx
  401188:	48 89 54 74 20       	mov    %rdx,0x20(%rsp,%rsi,2)
  40118d:	48 83 c6 04          	add    $0x4,%rsi
  401191:	48 83 fe 18          	cmp    $0x18,%rsi
  401195:	74 14                	je     4011ab <phase_6+0xb7>
  401197:	8b 0c 34             	mov    (%rsp,%rsi,1),%ecx
  40119a:	83 f9 01             	cmp    $0x1,%ecx
  40119d:	7e e4                	jle    401183 <phase_6+0x8f>
  40119f:	b8 01 00 00 00       	mov    $0x1,%eax
  4011a4:	ba d0 32 60 00       	mov    $0x6032d0,%edx
  4011a9:	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),%rbx
  4011b0:	48 8d 44 24 28       	lea    0x28(%rsp),%rax
  4011b5:	48 8d 74 24 50       	lea    0x50(%rsp),%rsi
  4011ba:	48 89 d9             	mov    %rbx,%rcx

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

  4011bd:	48 8b 10             	mov    (%rax),%rdx
  4011c0:	48 89 51 08          	mov    %rdx,0x8(%rcx)
  4011c4:	48 83 c0 08          	add    $0x8,%rax 
  4011c8:	48 39 f0             	cmp    %rsi,%rax
  4011cb:	74 05                	je     4011d2 <phase_6+0xde>
  4011cd:	48 89 d1             	mov    %rdx,%rcx
  4011d0:	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,%ebp
  4011df:	48 8b 43 08          	mov    0x8(%rbx),%rax
  4011e3:	8b 00                	mov    (%rax),%eax
  4011e5:	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),%rbx
  4011f2:	83 ed 01             	sub    $0x1,%ebp
  4011f5:	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),%r8
  4015e6:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
  4015eb:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx
  4015f0:	be 19 26 40 00       	mov    $0x402619,%esi
  4015f5:	bf 70 38 60 00       	mov    $0x603870,%edi
  4015fa:	e8 f1 f5 ff ff       	callq  400bf0 <__isoc99_sscanf@plt>
  4015ff:	83 f8 03             	cmp    $0x3,%eax
  401602:	75 31                	jne    401635 <phase_defused+0x71>
  401604:	be 22 26 40 00       	mov    $0x402622,%esi
  401609:	48 8d 7c 24 10       	lea    0x10(%rsp),%rdi
  40160e:	e8 25 fd ff ff       	callq  401338 <strings_not_equal>
  401613:	85 c0                	test   %eax,%eax
  401615:	75 1e                	jne    401635 <phase_defused+0x71>
  401617:	bf f8 24 40 00       	mov    $0x4024f8,%edi
  40161c:	e8 ef f4 ff ff       	callq  400b10 <puts@plt>
  401621:	bf 20 25 40 00       	mov    $0x402520,%edi
  401626:	e8 e5 f4 ff ff       	callq  400b10 <puts@plt>
  40162b:	b8 00 00 00 00       	mov    $0x0,%eax
  401630:	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,%rsp
  401208:	48 85 ff             	test   %rdi,%rdi
  40120b:	74 2b                	je     401238 <fun7+0x34>
  40120d:	8b 17                	mov    (%rdi),%edx
  40120f:	39 f2                	cmp    %esi,%edx
  401211:	7e 0d                	jle    401220 <fun7+0x1c>
  401213:	48 8b 7f 08          	mov    0x8(%rdi),%rdi
  401217:	e8 e8 ff ff ff       	callq  401204 <fun7>
  40121c:	01 c0                	add    %eax,%eax
  40121e:	eb 1d                	jmp    40123d <fun7+0x39>
  401220:	b8 00 00 00 00       	mov    $0x0,%eax
  401225:	39 f2                	cmp    %esi,%edx
  401227:	74 14                	je     40123d <fun7+0x39>
  401229:	48 8b 7f 10          	mov    0x10(%rdi),%rdi
  40122d:	e8 d2 ff ff ff       	callq  401204 <fun7>
  401232:	8d 44 00 01          	lea    0x1(%rax,%rax,1),%eax
  401236:	eb 05                	jmp    40123d <fun7+0x39>
  401238:	b8 ff ff ff ff       	mov    $0xffffffff,%eax
  40123d:	48 83 c4 08          	add    $0x8,%rsp
  401241:	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;
    else
      return 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;
    else
      return 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加个壳的。[暗中观察][狗头][狗头]

  • 55
    点赞
  • 202
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值