天天看点

《深入理解计算机系统》CSAPP_BombLab

操作系统:linux

调试工具:gdb

文章分为实验前准备、复习、拆炸弹三个模块。读者可以自取所需。

目录

Bomb Lab

实验前准备

复习

拆炸弹

phase_1

phase_2

phase_3

phase_4

phase_5

phase_6

secret_phase

结语

实验由<code>phase_1 ~ phase_6</code>组成,共6个阶段,

我们需要分别输入6个字符串密码,以便解除Dr. Evil的炸弹,

但只要有1个字符串输错,炸弹就会爆炸。

那么如何知道6个正确的字符串呢?

我们需要分别查看<code>phase_1 ~ phase_6</code>的汇编代码,

而炸弹就存放在其中,我们要根据汇编代码小心地避开炸弹,分析出正确的字符串即可。

而分析需要用到学习的汇编知识、常用的gdb指令以及linux终端指令。

需要用到的linux终端指令:

别忘了先进入文件地址中

<code>objdump -d bomb &gt; assembly.s</code>

反编译出<code>bomb.c</code>的汇编代码,并存放在<code>assembly.s</code>中(assembly.s是自己命名的)

<code>gdb 文件名</code>

用gdb调试工具打开某个文件

eg. <code>gdb bomb</code> 用gdb调试工具打开bomb.c文件

而我们就是要用gdb调试工具打开bomb.c文件后做实验

常用gdb指令:

gdb指令必须用gdb调试工具打开文件后才能调用

<code>run</code> (可简写为:<code>r</code>)

运行文件。

<code>quit</code> (可简写为:<code>q</code>)

停止运行,跳出gdb调试工具

<code>break 函数名</code> (可简写为:<code>b 函数名</code>)

在某个函数处打断点,以便之后用disas展示这个函数的汇编代码。

eg. <code>b phase_1</code> 在phase_1函数打断点

<code>disas 函数名</code>

展示这个函数的汇编代码(但必须先打断点)

eg. <code>disas phase_1</code> 展示phase_1的汇编代码

<code>x/NFU address or register</code>

用指定格式(FU)、指定数量(N)输出地址或者寄存器对应内容

eg1. <code>x/s 0x40139b</code> 输出0x40139b对应内容

eg2. <code>x/s $rsi</code> 输出%rsi对应内容

eg3.<code>x/4xw 0x6032d0</code> 按24个十六进制4字节格式输出0x6032d0对应内容

<code>p/F *address or *register</code>

用指定格式(F)打印地址或者寄存器对应内容(效果上,<code>x/F = p/F</code>)

eg1. <code>p/x *(0x6032f0)</code> 按十六进制打印0x6032f0对应内容

格式F、单位U都与c语言类似,参考链接gdb:Format、Variables and Memory部分

测试和比较指令

- <code>ZF、OF、SF、CF</code>(条件码)

zero flag 零标志位;overflow flag 溢出标志位;sign flag 符号标志位;carry flag 进位标志位。

- <code>test S1,S2</code>即 <code>S1 &amp; S2</code>

测试指令,进行位级与操作,若结果为0,返回<code>ZF = 1</code>;若结果非0,返回<code>ZF = 0</code>,但不会改变两个数的值。同时,需要特殊注意的是,可以对同一个寄存器使用test指令,例如<code>test %eax,%eax</code>。

- <code>cmp S1,S2</code>即<code>S2 - S1</code>

比较指令,进行减法操作,但不会改变两个数的值。

部分跳转指令

不区分unsiged(无符号)、siged(有符号)跳转

<code>jne</code>

根据ZF的值判断是否跳转,当<code>ZF = 0</code>才跳转。

<code>je</code>

根据ZF的值判断是否跳转,当<code>ZF = 1</code>才跳转。

siged jump 指令

<code>jle</code> (<code>jump&lt;=</code>)

j:jump;l:less(小于);e:equal

<code>jge</code> (<code>jump&lt;=</code>)

j:jump;g:great(大于);e:equal

<code>jg</code> (<code>jump&gt;</code>)

j:jump;g:great(大于)

unsiged jump 指令

<code>jb</code>(<code>jump&lt;</code>)

j:jump;b:below(小于)

<code>jbe</code> (<code>jump&lt;=</code>)

j:jump;b:below(小于);e:equal

<code>ja</code> (<code>jump&gt;</code>)

j:jump;a:above(大于)

<code>cmp</code>与signed jump、unsigned jump的联系(原理与条件码有关,在此不解释了)

当<code>cmp S1,S2</code>伴随signed jump指令使用时,S1置前,S2置后,例如:

当<code>cmp S1,S2</code>伴随unsigned jump指令使用时,S2置前,S1置后,例如:

部分二元操作

<code>lea S,D</code> (即<code>D &lt;-- S</code>,等价于赋值算式<code>D = S</code>)

不是读取数据,是加载有效地址,但和<code>mov</code>指令效果相同,常用于执行简洁的算术操作。

<code>sub S,D</code> (即<code>D &lt;-- D - S</code>,等价于赋值算式<code>D -= S</code>)

减法,D - S后赋值给D。

<code>sar S,D</code> (即<code>D &lt;-- D &gt;&gt; S</code>,等价于赋值算式<code>D &gt;&gt;= S</code>)

算术右移,补符号位(sign),D &gt;&gt; S后赋值给D。注意:若<code>sar D</code>,则默认是算术右移<code>1</code>位。

<code>shr S,D</code> (即<code>D &lt;-- D &gt;&gt; S</code>,等价于赋值算式<code>D &gt;&gt;= S</code>)

算术右移,补<code>0</code>,D &gt;&gt; S后赋值给D。

当寄存器为空时,其值为<code>0</code>

<code>%rdi</code>

系统往往要把一个值赋给%rdi后,才能当作入参(即输入的参数)

<code>register</code>

注意:汇编代码为了优化常常使用同一个系列的<code>register</code>表示同一个变量

例如:

register(64bits)

作用

%rax

存放返回值

%rbx

被调用者保存(存放被调用者的地址,可以是函数地址,也可以是其它地址,callee save)

%rcx

第4个参数(存储参数,可以是数字字符参数,也可以是地址参数)

%rdx

第3个参数

%rsi

第2个参数

%rdi

第1个参数(经常作入参使用)

%rbp

被调用者保存

%rsp

栈指针(用以开辟栈空间,注意入参的先进后出)

%r8

第5个参数

%r9

第6个参数

%10

调用者保存(存放调用者函数的地址,可以是函数地址,也可以是其它地址,caller save)

%r11

调用者保存

%r12

%r13

%r14

%r15

优化

有些看似毫无作用的汇编代码,其实是在做优化。

在正式拆炸弹之前需要注意到一点,我们输入的字符串都是放入到<code>%rdi</code>当中的。

(其实一般情况下,输入也是放到<code>%rdi</code>的)

也就是说,在每个<code>phase</code>调用时,默认<code>%rdi</code>这个寄存器存放着输入的字符串。

如何得到这点的呢?是通过分析<code>main</code>函数的汇编代码得出的。

分析:

我们可以看到,在每个<code>phase</code>调用前,都<code>callq 0x40149e &lt;read_line&gt;</code>,然后<code>mov %rax,%rdi</code>,这样子会将我们输入的字符串最终存到<code>%rdi</code>中。(我们是把字符串输入到<code>read_line</code>的,而<code>read_line</code>会返回到<code>%rax</code>中,<code>mov</code>操作将<code>%rax</code>存到<code>%rdi</code>。注意,<code>%rax</code>往往用来放返回值)

至于<code>read_line</code>的汇编代码我就不分析了,通过名字也可以看出:它是用来返回我们的输入值的

下面开始分析<code>phase_1 ~ phase_6</code>的汇编代码,

得出认为正确的字符串,并逐个解除炸弹

查看<code>phase_1</code>的汇编代码:

重点就是<code>&lt;+14&gt;</code>的test指令,可以看出当<code>%eax</code>为0时才不会爆炸。也就是说函数<code>strings_not_equal</code>的返回值必须为0才行,根据名字我们可以猜测,字符串不相等时返回1,字符串相等时返回0。下面查看<code>string_not_equal</code>的汇编代码验证我们的猜测。

通过查看<code>string_not_equal</code>函数的汇编代码,可以找到,<code>string_not_equal</code>函数先是比较了输入字符串和地址<code>0x402400</code>对应的字符长度,再逐个比较字符,最终,字符相同返回<code>0</code>,字符不同返回<code>1</code>。我们可以看到,汇编进行不少优化。例如,如果第一个字符已经是不同的,就不用循环进行更多的操作。

key:由此可以知道,只要我们输入的字符和地址<code>0x402400</code>对应的字符相同即可。那么,我们通过gdb指令<code>x/s 0x402400</code>即可访问<code>0x402400</code>对应的字符。代码如下:

那么,只要在运行界面输入<code>Border relations with Canada have never been better.</code>即可。(译为:与加拿大的边境关系从来没有这么好过。)

我们先查看<code>phase_2</code>的汇编代码:

<code>callq 0x40145c &lt;read_six_numbers&gt;</code>,我们查看这个函数的汇编代码:

(根据名字,我们猜测其作用是读取6个数字)

注意到<code>phase_2</code>中<code>&lt;+6&gt;</code>已经将%rsp赋给%rsi,那么,对%rsi进行操作也就是之后对<code>phase_2</code>中的%rsp操作。

以下表格是对%rsi操作的展示:

0x4(%rsi)

0x8(%rsi)

0xc(%rsi)

0x10(%rsi)

0x14(%rsi)

(%rsp)

0x8(%rsp)

注意,<code>0x10 = 16D</code>,<code>0x14 = 20D</code>。十进制后缀<code>D</code>,Decimal.

可以看到,这样操作传递了6个地址参数,其地址间隔为<code>4</code>,最后两个因为参数寄存器不够用了,只能用栈帧%rsp。(注意,此处的%rsp是<code>read_six_numbers</code>自己开辟的,原先<code>phase_2</code>的%rsp已经中断)

<code>&lt;+36&gt;</code>处,<code>mov $0x4025c3,%esi</code>,访问0x4025c3内容:

可以知道%eax中存放着格式字符串

<code>&lt;+46&gt;</code>处,<code>callq 0x400bf0 &lt;__isoc99_sscanf@plt&gt;</code>,函数<code>sscanf</code>是c语言自带的函数,同<code>scanf</code>类似,作用是读取输入的字符串,并根据指定格式存储到指定地址。这告诉了我们的输入的字符串,应该是6个整型数字,而且之间用空格隔开。同时,它们存放在上述的6个参数地址中,也就是之后<code>phase_2</code>的%rsp按<code>4</code>递增地址的栈帧中。符合我们的猜测。

我们回到<code>phase_2</code>继续分析: 可以知道第一个参数数值<code>(%rsp)</code>必须是<code>1</code>,之后会进入一个循环。我们把这个循环单独拿出来看:

我们已经知道<code>(%rsp) = 1</code>,那么第一次循环中,<code>&lt;+32&gt;</code>处的<code>%eax = 2</code>,故只有<code>(%rbx) = 0x4(%rsp) = 2</code>,才不爆炸;第二次循环中,<code>&lt;+32&gt;</code>处的<code>%eax = 4</code>,故<code>(%rbx) = 0x8(%rsp) = 4</code>;第三次循环中,<code>&lt;+32&gt;</code>处的<code>%eax = 8</code>,故<code>(%rbx) = 0xc(%rsp) = 8</code>,可以看出是以1为首项,后一项是前一项两倍的递增数列,共6项,以此类推,有下表。

0x4(%rsp)

0xc(%rsp)

0x10(%rsp)

0x14(%rsp)

1

2

4

8

16

32

key: 由此得知,phase_2该输入的字符串为<code>1 2 4 8 16 32</code>

<code>phase_3</code>的汇编代码如下:

访问<code>0x4025cf</code>,可知字符串格式为<code>"%d %d"</code>,从而得知输入字符串是两个整数。根据<code>&lt;+39&gt;: cmpl $0x7,0x8(%rsp)</code>和<code>&lt;+44&gt;: ja 0x400fad &lt;phase_3+106&gt;</code>可以知道<code>0x8(%rsp) &lt;= 7</code>。那么,第一个参数%rdx取值要小于等于7。

根据<code>&lt;+50&gt;: jmpq *0x402470(,%rax,8)</code>可以知道,跳转的地址是指针<code>*0x402470</code>的变换,增量为<code>8</code>,此时为<code>0x8(%rsp)</code>即为%eax。

下面我们用<code>x/s</code>指令逐个访问,指针地址是递增的,但对应存放的<code>phase_3</code>的地址并不是递增的。到这时,答案已经呼之欲出了。

(由此可以知道指针<code>*0x402470</code>指向一个存放<code>phase_3</code>部分地址的字符串。一开始我想的是对<code>phase_3</code>的部分地址直接进行增<code>8</code>操作,没有想到是对指针<code>*0x402470</code>进行增<code>8</code>操作,从而访问不同的<code>phase_3</code>中的地址,耽误了许久)

对<code>phase_3</code>继续分析:

根据<code>&lt;+123&gt;: cmp 0xc(%rsp),%eax</code>和<code>&lt;+127&gt;: je 0x400fc9 &lt;phase_3+134&gt;</code>知道,第二个参数<code>0xc(%rsp)</code>必须和%eax相同,此时的%eax根据<code>0x8(%rsp)</code>不同而不同。

key:

整理上述分析可以知道,共有<code>8</code>组答案如下:

3

x/s

*0x402470

*(~ +8)

*(~ +16)

*(~ +24)

&lt;phase_3+n&gt;

+57

+118

+64

+71

0xc(%rsp) H

0xcf

0x137

0x2c3

0x100

0xc(%rsp) D

207

311

707

256

5

6

7

*(~ +32)

*(~ +40)

*(~ +48)

*(~ +56)

+78

+85

+92

+99

0x185

0xce

0x2aa

0x147

389

206

682

327

(十六进制后缀,H,hexadecimal;十进制后缀,D,Decimal)

我们选择第8组答案<code>7 327</code>作为输入:

<code>phase_4</code>的汇编代码如下:

分析:仍旧是输入两个<code>int</code>参数,根据<code>&lt;+34&gt;</code>处可知,第一个参数<code>0x8(%rsp)</code>满足<code>0xe &gt; 0x8(%rsp)</code>,第二个参数<code>0xc(%rsp)</code>必须为0。但是第一个参数需要根据<code>&lt;func4&gt;</code>函数来确定。

<code>&lt;func4&gt;</code>的汇编代码如下:

第一次进入func4函数:

分析:其实这个时候答案已经出来了,只要第一个参数为<code>7</code>和<code>%ecx=7</code>相同即可。但显然<code>func</code>是一个递归函数,我们进入递归看看,还有什么答案。

第一次递归:(记为<code>R1</code>)

第一个参数为<code>3</code>和<code>%ecx=3</code>相同即可。

第二次递归:(记为<code>R2</code>)

第一个参数为<code>1</code>和<code>%ecx=1</code>相同即可。

第三次递归:(记为<code>R3</code>)

第一个参数为<code>0</code>和<code>%ecx=0</code>相同即可。

第四次递归:(记为<code>R4</code>)

第五次递归:(记为<code>R5</code>,此时<code>R5 = R3</code>)

由此可以知道,<code>R4</code>之后会一直在<code>R3、R4</code>之间无限循环,直到程序崩溃。那么,第一个参数的取值只能为:<code>0 1 3 7</code>

得出<code>4</code>组答案为:

0x8(%rsp) 第一个输入参数

0xc(%rsp) 第二个输入参数

我们取<code>0 0</code>这组输入。

<code>phase_5</code>的汇编代码如下:

很显然,输入的字符串长度得为<code>6</code>。利用x/s指令,在<code>&lt;+55&gt;</code>处我们查看<code>x/s 0x4024b0</code>,给出一句嘲讽我们的话,<code>"maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"</code>,很明显不是答案,也许是答案,也许不是答案,也许在暗示答案不能单单copy,我们无视他的嘲讽,对<code>&lt;+41&gt; ~ &lt;+74&gt;</code>处的循环进行专门分析。

第二次循环:

以此类推至第六次循环后,应当有<code>&lt;+41&gt;: %ecx = %rbx+5</code>和<code>&lt;+62&gt;: %rsp+0x10+5 = %dl</code>。此时,<code>%rbx</code>是输入,会有: <code>%rsp+0x10+5 = %dl = %edx = 0x4024b0+(%rdx &amp; 0xf)</code>

上式等价于:<code>= 0x4024b0+(%cl &amp; 0xf) = 0x4024b0+((%rbx+5) &amp; 0xf)</code>

所有情况见下表。

%rsp+0x10+0

0x4024b0+%rbx+0(0x4024b0 + (第一个输入参数 &amp; 0xf))

%rsp+0x10+1

0x4024b0+%rbx+1(0x4024b0 + (第二个输入参数 &amp; 0xf))

%rsp+0x10+2

0x4024b0+%rbx+2(0x4024b0 + (第三个输入参数 &amp; 0xf))

%rsp+0x10+3

0x4024b0+%rbx+3(0x4024b0 + (第四个输入参数 &amp; 0xf))

%rsp+0x10+4

0x4024b0+%rbx+4(0x4024b0 + (第五个输入参数 &amp; 0xf))

%rsp+0x10+5

0x4024b0+%rbx+5(0x4024b0 + (第六个输入参数 &amp; 0xf))

可以看出<code>%rsp+0x10</code>的赋值是<code>0x4024b0</code>的偏移,偏移量范围为<code>0 ~ 15</code>,也就是嘲讽句的前16个字符,即<code>maduiersnfotvbyl</code>。

我们回到<code>phase_5</code>函数继续分析,利用x/s指令,在<code>&lt;+81&gt;</code>处我们查看<code>x/s 0x40245e</code>,给出一个单词字符串,<code>"flyers"</code>(译为飞翔者们或者考熟的鸭子飞走了)。也就是说,<code>&lt;+86&gt;</code>处的入参必须与<code>flyers</code>相同。

根据<code>maduiersnfotvbyl</code>,取得<code>flyers</code>对应<code>0x4024b0</code>的偏移量是<code>9、15、14、5、6、7</code>,但是只能输入<code>6</code>个字符,如果直接输入<code>91514567</code>,超过<code>6</code>个字符会爆炸。

要解决这个问题,可以先把偏移量转换为二进制<code>1001、1111、1110、0101、0110、0111</code>。因为有<code>%rbx &amp; 0xf</code>的位级操作,那么我们只要保证尾4位相同即可。这时可以联想到用来定义字符的ASCII表,那么答案有许多种,我比较懒,就只找尾4位为<code>1111</code>和<code>1110</code>的字符。分别为<code>/</code>和<code>.</code>,从而得到输入<code>9/.567</code>。

查看<code>phase_6</code>的汇编代码:

好家伙,一个电脑屏幕根本装不下全部代码。我们分成几个部分来看。

注意:在循环开始前会有一些变量准备;<code>0x10 = 16D,0x14 = 20D</code>;<code>flag</code>这个变量命名常用来当作循环是否结束的标志。

Part1,读入了6个整数。

根据<code>phase_2</code>的<code>&lt;read_six_numbers&gt;</code>分析可以知道,这段读入了6个整数,对栈帧<code>%rsp</code>偏移(偏移量为<code>4</code>)即是输入<code>input[i],i取0 ~ 5</code>。如下表所示:

input[0]

input[1]

input[2]

input[3]

input[4]

input[5]

Part2,保证6个输入整数(<code>input[i],i取0 ~ 5</code>)都不相同且范围为1 ~ 6。

loop1确定了<code>input[i]</code>的取值范围为:<code>1 ~ 6</code>。loop2保证6个输入都不相等。

取值范围如何确定的?由<code>+39 ~ +45</code>处得出,<code>input[i] &lt;= 6</code>。同时,因为<code>+45</code>的 <code>jbe</code> 是对无符号数操作,若<code>input[i]</code>取 <code>0</code> ,执行<code>0 - 1 = -1</code>在比较时会出错(溢出),所以不能取<code>0</code>。故只能取<code>1 ~ 6</code>。

Part3,执行算式:<code>input[i] = 7 - input[i]</code>

因此为了方便,我们重新命名:<code>sub_input[i] = 7 - input[i]</code>

Part4,引入链表并改变链表顺序。

首先我们查看<code>0x6032d0</code>,一开始用<code>x/s</code>指令都是乱码,如图所示。

但是注意到<code>node1</code>,说明其很有可能是链表,而不是字符串。

我们用<code>x/24xw</code>指令,按xw格式(十六进制4字节格式)输出24个数据。

如图所示,符合我们的猜测,就是链表,<code>0x6032d0</code>是head指针。

其中,node是节点地址,val是节点值,order是它的序列(一共6个),next是下个节点的地址(能够发现它刚好就是按照序列排列的,最后一个节点的next是<code>0x0即NULL</code>)。

那么,该段循环是怎么改变链表顺序的呢?首先引入栈帧<code>%rsp + 8*i + 0x20,i取0 ~ 5</code>,然后在<code>+130 ~ +137</code>,<code>0x6032d0</code>将随<code>sub_input[i]</code>的改变而改变,最后将改变后的链表逐个赋给栈帧。

即执行执行赋值算式:<code>%rsp + 8*i + 0x20 = $0x6032d0 + (8*(sub_input[i]-1))</code>

这样下来就能够重排链表了。但此处还没有给出改变链表顺序的标准是什么。

同时注意到一个重要的细节问题:改变时0x6032d0的间隔为8D,但我们需要的间隔为0x10 = 16D,如何解决?

事实上,间隔为8也没关系。只要部分+8的地址所存数值恰好为+16的地址即可。(不是所有地址都需要这样,另一部分地址执行两次+8就行了)

上述表述等价于三个等式

<code>*(0x6032d0+8) = 0x6032e0</code>、<code>*(0x6032f0+8) = 0x603200</code>、<code>*(0x603310+8)= 0x603320</code>成立。

我们用p/x 指令打印数值看三个等式是否成立。(这个成立是<code>phase_6</code>已经设置好的,但不查看之前完全不知道)

执行part4后,可以得出如下表格:

rec_node0

rec_node1

rec_node2

rec_node3

rec_node4

rec_node5

%rsp + 0x20

%rsp + 0x28

%rsp + 0x30

%rsp + 0x38

%rsp + 0x40

%rsp + 0x48

说明<code>rec_node1 ~ rec_node6</code>将逐个放入栈帧<code>%rsp + 0x20 ~ %rsp + 0x48</code>中。

注意:因为node0 ~ node5已经随sub_input而重新排列了,但我们还不知道是如何排列的。

所以,不妨命名为:rec_node0 ~ rec_node5,表明链表重组,也就是所谓的改变链表顺序。

(rec:recombination)

同时也可以得出<code>sub_input</code>对应<code>rec_node</code>的表格:

sub_input[0]

sub_input[1]

sub_input[2]

sub_input[3]

sub_input[4]

sub_input[5]

0x6032d0

0x6032e0

0x6032f0

0x603300

0x603310

0x603320

part5,重新连接next。

注意:

<code>0x50(%rsp) = 0 = NULL</code>是next连接完才赋值的。

这里利用栈帧<code>%0x28(%rsp) ~ 0x50(%rsp)</code>逐个赋给<code>%0x20(%rsp) ~ 0x48(%rsp)</code>

改变的是rec_node -&gt;next指向的地址,而不是rec_ndoe本身。也就是所谓的重新连接next。

之所以要重连next,也是因为node已经重排为rec_ndoe了。

part6,说明改变链表顺序的标准:按<code>val</code>降序排列。(从大到小)

之所以按<code>val</code>降序排列,是因为必须满足<code>(0x8+0x20+%rsp+k*8) &gt;= (0x20+%rsp+k*8),k取0~5</code>

即<code>node-&gt;val &gt;= node-&gt;next-&gt;val</code>成立,也就是链表中当前节点值大于下一个节点值。

综上part1 ~ part6可以得出答案:(<code>sub_input</code>模7得到<code>input</code>)

val

0x39c

0x2b3

0x1dd

0x1bb

0x14c

0x0a8

rec_node

sub_input

input

inuput为:<code>4 3 2 1 6 5</code>

ps:如果你仔细观察,你会发现,按val降序排列之后,order其实恰好对应着sub_input。

可恶啊!竟然还有个彩蛋关。什么?你说我怎么发现的,当然是“口口相传”啦。

(事实上可以在编译文件上查找<code>ctrl+f</code>到)

用到了二叉树。

查看<code>phase_defused</code>的汇编代码:(可以知道在phase_4中多输入个DrEvil即<code>0 0 DrEvil</code>才能进入彩蛋关)

我们对一些字符串进行访问,可以知道调用<code>sscanf</code>函数前得输入,其格式为<code>"%d %d %s"</code>但输入内容是空的,这说明我们还没输入。我们需要输入,但,是什么时候输入呢?

要确定输入是何时输入的,我们可以先在<code>*0x4015f5</code>处,即输入处打断点,然后run bomb文件,最后访问<code>0x603870</code>的内容。

此时有<code>0x603870 &lt;input_strings+240&gt;: "0 0 "</code>,这说明输入是在<code>phase_4</code>处进行的。其输入格式为<code>"%d %d %s"</code>,同时有<code>*(0x402622) == DrEvil</code>,之后要进行字符比对且相同才进入secret_phase。那么我们大胆猜测,应该在<code>phase_4</code>处输入<code>0 0 DrEvil</code>才能进入彩蛋关。

开始正式分析,首先查看<code>secret_phase</code>的汇编代码:(说明调用fun7之后,返回值必须为2)

查看<code>fun7</code>的汇编代码:

这是一棵二叉树,思路如图所示:

《深入理解计算机系统》CSAPP_BombLab

方法一:实现eax = 2。(ps:递归调用顺序与返回顺序相反)

下面描述调用过程:(ps:esi是input,每次调用完都会更新rid指向的地址)

B:第一次调用fun7,<code>(rdi) = (0x6030f0) = 0x24</code>。同时输入要不满足<code>(rdi) &lt;= esi</code>,也就说输入小于0x24,才能走路径B。

此次调用结束时更新rdi指向的地址,即:<code>rdi+0x8 = 0x603110</code>。

C:第二次调用fun7,<code>(rdi) = (0x603110) = 0x8</code>。同时输入要满足<code>(rdi) &lt;= esi</code>且不满足<code>edx == esi</code>,也就说输入大于0x8且不等于0x8,才能走路径C。

此次调用结束时更新rdi指向的地址,即:<code>rdi+0x10 = 0x603150</code>。

D:第三次调用fun7,<code>(rdi) = (0x603150) = 0x16</code>。同时输入要满足<code>(rdi) &lt;= esi</code>且满足<code>edx == esi</code>,也就说输入等于0x16即20D,才能走路径D。

三次调用结束,得出input为<code>20</code>。

方法二:实现eax = 2。

key:我们取<code>20</code>这个答案。

无聊地查看一下二叉树内容:(此处对解题不是很有必要)

整理后的图状展示:

But you know, Alan, sometimes it's the very people who no one imagines anything of who do the things no one can imagine. ---The Imitation Game

从9.27到10.11,总算完结了。期间有几天是没有写的。总共是8天,最后两关都用了2天。当然不是一整天,5、6个小时应该是有的。完全靠自己写的是phase_4,奈何没啥基础。前几个都在适应gdb是怎么调试的,后几个难度比较大,phase_5是没联系到ASCII,后两个是链表、二叉树都不熟悉。也参考了不少其它大佬的解答。

自己的汇编阅读能力是提升了不少,后面看都是比较自然,但是联系到数据结构就比较不了解,还是得继续提高姿势水平。

如有谬误,敬请指正。