CSAPPのBomb Lab,超详细指南

  • 先导知识

  • 基本Linux命令行代码
  • 一定的汇编语言阅读能力
  • 对gdb的简单了解(gdb指令可查阅:GDB Cheat Sheet (whu.edu.cn)
  • 如果你好奇炸弹炸了是什么样子,请不要尝试hhh,因为我已经给你们试过了:

准备过程

解压你获得到的tar文件(tar -xvf bombX.tar)得到一个目录./bombXXX,其中包含如下文件:

  • README:标识该bomb和所有者。
  • bomb:bomb的可执行程序。
  • bomb.c:bomb程序的main函数。

然后在该文件目录下打开终端,就可以开始拆炸弹啦!

开始拆炸弹!——phase_1

phase1的考点就是字符串:进行字符串比较,如果输入的字符串与预设的字符串不相等,则”炸弹爆炸“。

具体解法:

  1. gdb bomb 使用 gdb 运行 bomb
  2. b explode_bomb 设置断点,即使当我们错误输入时也能阻止炸弹爆炸
  3. b phase_1 从 bomb.c 中可以知道,phase_1(input) 处理输入字符串,故我们在此函数入口处设置断点
  4. r 运行程序
  5. 随便输入一些字符
  6. layout asm 显示反汇编窗口

可以看到,调用了 strings_not_equal 函数,b strings_not_equal 在该函数入口处设置断点

c 继续运行

又调用了 string_length 函数,b string_length 设置断点,c 继续运行

通过阅读这段代码,我们可以知道:

string_length 函数实现:将输入字符串的长度存在寄存器 %rax 中返回

回到 strings_not_equal 函数,我们能够知道它首先得到我们输入字符串长度,然后得到正确答案字符串长度,进行比较,二者不相等则在 %eax 中存 1 返回;若二者长度相等,则逐个比较二者字符串内容,若全部相等则在 %rax 中存 0 返回,若有任意一个不相等则在 %eax 中存 1 返回

回到 phase_1 函数,我们现在就能知道 0x402500 处就是答案字符串地址,使用 x/s 0x402500 查看该处字符串

复制下来这句话,使用 q 命令退出 gdb,gdb bomb 重新进入调试模式,b phase_2 在 phase_2 入口处打上断点,r 运行,输入刚才复制的这句话,phase_1 的炸弹成功拆除:

开始拆炸弹!——phase_2

phase_2的考点:

  • 数组
  • 循环

其功能简单来说就是:

  • 将在字符串解析为6个数字的整数数组array
  • array第三个个整数为0、1、1
  • array中每个元素必须是前两个个元素之和

具体解法:

  1. 从 phase_1 结尾处继续
  2. b explode_bomb 设置断点,即使当我们错误输入时也能阻止炸弹爆炸
  3. 随便输入一些字符,回车,进入 phase_2 断点,layout asm 显示反汇编窗口,可以看到首先调用 read_six_numbers 函数,这提示我们需要输入六个数

b read_six_numbers 在 read_six_numbers 函数入口处打上断点,c 继续执行

首先栈顶指针寄存器 %rsp 减去 0x18 ,在内存中开辟了一块 24 字节的栈空间
在第 2 步中的图中我们能看到,调用 read_six_numbers 前先将栈顶指针减去了 0x28 ,然后把栈顶指针值赋给 %rsi

大致的栈变化和寄存器变化如下图所示(括号代表栈变化)

我们注意到在0x4015f3位置出现了一个地址:0x4027f5,使用 x/s 尝试打印一下内容

再结合前面的函数名为 read_six_numbers ,可以知道我们需要输入六个整型数,且用空格分开,故我们先使用 q 退出当前调试,重新输入六个整数

Tips:

当我们想重新开始调试拆解 phase_2 时,就不得不把 phase_1 的拆弹密码再输入一遍,可以想象,将来我们调试拆解 phase_6 时,前面 5 个密码每次都要输一遍,这实在是太浪费时间了。幸运的是,作者考虑到了这种情况,我们可以在当前目录下新建一个文本文件,其中存有我们的密码,在运行或调试时带上参数即可

Vim 编辑器的具体使用不在此介绍,我们在此只需要知道:

Vim 首先进入的是命令模式,按 i 进入编辑模式,输入完成后按两次 esc 键退出编辑模式。之后按冒号:,输入 wq,按 enter 键,表示保存并退出。

接着gdb bomb 进入调试,b phase_2 打断点,r key.txt 带参数运行

帮我们省去了重复输入字符串的功夫

上图若干条指令对照内存走一遍大致明白就是栈顶处为整型数 0

栈顶指针 +4 (整型数占用 4 个字节)指向的下一个整数应该1

第三个整数(栈顶指针 +8)为前两个数之和(1+0=1)即为1

可以看出cmp rbp rbx这附近的一段是一个循环

所以第四个及以后的整数也为前两个数之和,分别是2,3,5

回头看我们知道了第 3 步中那些操作是通过栈传递参数,我们输入的六个数是逆序入栈,第一个数最后入栈,为栈顶。所以我们输入的字符串应该是:0 1 1 2 3 5

将其存入key文件然后 q 退出当前调试,gdb bomb 重新进入,b phase_3 在 phase_3 入口处打上断点,r key 运行

开始拆炸弹!——phase_3

phase_3的考点:

  • switch-case

phase_3的功能总的来说:

  • 将字符串解析为两个数字x, y
  • x不能大于7
  • 通过switch-case可以将x的值映射为一个num值
  • y必须等于num

具体解法:

随便输入一些字符,回车,进入 phase_3 断点,layout regs 显示反汇编和寄存器窗口

可以看到在 <phase_3+14> 处出现一个内存地址 0x402801,我们来用x/s 查看一下

结合在 phase_2 中的经验,我们可以猜测phase_3 的输入是两个数字,以空格隔开

然后程序调用一个什么sscanf程序,这次我们不直接打断点去看它的汇编

新方法!

我们根据之前的假设输入任意两个数字进key

接着stop再run key:

接着我们stepi一步一步来看,一直step到sscanf这个函数

这里我们也可以看出它需要的参数是两个整数。接下来我们finish:

看到这个return value,联系我们的已有知识,我们不难猜这里就是在判断我们输入了几个数字。

之后代码判断返回值是否大于1,如果大于,跳转至phase_3+39,很好,我们又跳过了一个炸弹!

接下来一连串比较&跳转,我们慢慢来看:

首先是比较0xc(%rsp)和7的大小,大于则跳转phase_3+106

我们一看,好的,又是炸弹。说明,0xc(%rsp)不能大于7

那么这个0xc(%rsp)是什么呢,我们打出来看看

不要忘记我们之前在key中输入的值,这个2就是我输入的第一个数。这说明我们输入的第一个数不能大于7。接着我们继续来看,代码将这个数传给了eax,再根据eax的值跳转

那我们stepi一下,看看根据我们现在输入的第一个数跳转到哪里去了

这里是把0x117赋给了eax,然后又跳转:

这里很明显我们可以看出,就是比较0x8(%rsp)和eax,相等就跳转,不相等就爆炸。

那我们赶紧把key中的第二个数字设为279(0x117的十进制数),再重新运行看看(别忘了改一下断点位置)

很好,我们已经解决了一半的炸弹!

开始拆炸弹!——phase_4

phase_4考点:

  • 递归

从 phase_3 结尾处继续,随便输入一个字符,回车,进入 phase_4 断点。一样的,我们layout asm

开头与之前类似,我们同样关心这个0x402801是什么,打印出来看看

这个参数形式,我们同样猜测这次的输入也是两个整数,结合从开头一直到<phase_4+32>的代码,我们基本上可以肯定这个猜测。

然后继续看,程序将0xe(十进制14)与0xc(%rsp)比较,如果小于等于,就跳过下一个炸弹。

同样的,我们输入两个数到key里面,然后更改断点位置,再stepi测试一下

注意这里要加个*号

然后我们开始stepi一步一步来看,首先我们打印一下0xc(%rsp)的值

说明是我们输入的第一个值

然后继续,我们进入了func4,和之前一样,我们直接查看返回值

还不是很理解这里的返回值15是怎么得来的。先看看后面的代码,我们知道,第二个数要是0x2d(十进制的45),于是我们换个输入值再试试。

同样的,我们再次进入func4

这里我不详细说明func4的运行流程,不过值得注意的是sar只有一个参数的时候是什么意思:

func4大致是根据输入参数计算结果,两者成正相关,最后根据自己推导出的原函数计算,得出要使eax结果为45,输入参数应为14

那我们更改key里的参数,再次运行

很好,我们又解决了一个炸弹!

开始拆炸弹!——phase_5

phase_5考点:

  • 数组
  • for循环

其功能可总结为:

  • 在数组里跳转
  • 根据输入的第一个值x确定跳转次数
  • 将跳转到的所有数字求和,即输入的第二个数字y

又是熟悉的开头

我们同样猜测这里是要输入两个整数,测试一下,果然没问题

接下来,我们看以上这段代码(假设输入的两个数字分别为x y):

eax=x

eax与1111(二进制)相与,即取eax低四位

将修改后的eax传回给栈

将eax与1111相比较,若相等,则爆炸

若不等,ecx=0;edx=0;

edx=edx+1;

之后一个比较少见的汇编代码cltq:把eax符号扩展到rax(猜测:为了下文取值,因为基址寄存器必须是64位)

根据rax值进行赋值,eax=0x4025a0+4*rax

ecx=ecx+eax

比较1111和eax,若不等,跳转至phase_5+65

若相等,x=1111

比较edx(1)和1111,若不等,爆炸

比较y和ecx,若相等则成功结束,不等则爆炸。

因此,我们发现关键要让edx=1111(15),在这种循环次数下,得到ecx,就是我们第二个输入的数字。

于是我们打印出整个数组的内容,推导x应等于几。我们发现rax=6,时,eax=1111,反推15次循环,6、14、2、1、10、0、8、4、9、13、11、7、3、12、5。因此,我们知道了x=5,从而ecx=115。

由于edx=1111是一个比较大的数,其实,我们还可以采用穷举法():

当x=0时,edx=6;x=1时,edx=4;x=2时,edx=3;x=3时,edx=13;x=4时,edx=8; 

当x=5时,edx=15,这时我们再查看ecx的值

因此我们更改我们的key文件,再次运行

令人兴奋的消息~

开始拆炸弹!——phase_6

总的来说就是将输入的数据n对应到node_n,然后根据输入数据顺序将node重排,重排要求每个node递减。

从 phase_5 结尾处继续,随便输入一些字符,回车,进入 phase_6 断点,layout asm 显示反汇编窗口

虽然这个比前面 5 个都要长上不少,但不要畏惧,这并不复杂

首先同样是从 read_six_numbers 可知,我们需要输入的依旧是六个以空格隔开的数字,因此我们随意输入六个数来测试一下

确实是六个数字

接下来我们逐步看这个汇编,之后的代码行数n我都是以汇编中的+n来称呼

   0x00000000004010dd <+0>:	push   %r13
   0x00000000004010df <+2>:	push   %r12
   0x00000000004010e1 <+4>:	push   %rbp
   0x00000000004010e2 <+5>:	push   %rbx
    #以上均为对调用者寄存器保存的过程

   0x00000000004010e3 <+6>:	sub    $0x58,%rsp
   0x00000000004010e7 <+10>:	lea    0x30(%rsp),%rsi
   0x00000000004010ec <+15>:	callq  0x4015cf <read_six_numbers>
    #读取输入并检查输入数字个数

   0x00000000004010f1 <+20>:	lea    0x30(%rsp),%r13 #r13=rsp+30
   0x00000000004010f6 <+25>:	mov    $0x0,%r12d #r12d=0

   0x00000000004010fc <+31>:	mov    %r13,%rbp #rbp=r13=rsp+30 
    #:rbp:第二个数
   0x00000000004010ff <+34>:	mov    0x0(%r13),%eax #eax=(r13)输入的第一个数
    #第二个数
   0x0000000000401103 <+38>:	sub    $0x1,%eax #eax=eax-1
   0x0000000000401106 <+41>:	cmp    $0x5,%eax #eax和5比较
   0x0000000000401109 <+44>:	jbe    0x401110 <phase_6+51> #eax须<=5
    #第一个数小于等于6
    #第二个数小于等于6
##其实就是每个数小于等于6
   0x000000000040110b <+46>:	callq  0x401599 <explode_bomb>

   0x0000000000401110 <+51>:	add    $0x1,%r12d #r12d=r12d+1 #r12d=1
    #r12d=2
   0x0000000000401114 <+55>:	cmp    $0x6,%r12d #r12d和6比较
   0x0000000000401118 <+59>:	jne    0x401121 <phase_6+68> #r12d不等于6,跳转
   0x000000000040111a <+61>:	mov    $0x0,%esi #if r12d=6, then esi=0
   0x000000000040111f <+66>:	jmp    0x401163 <phase_6+134> 
##整个循环一直到100行,如果判断完了所有输入的数,则从这跳转出去到134行

   0x0000000000401121 <+68>:	mov    %r12d,%ebx #ebx=r12d(初值为1)
    #ebx=2
   0x0000000000401124 <+71>:	movslq %ebx,%rax #rax=ebx (初值为1)2 3 4 5
    #rax=2
   0x0000000000401127 <+74>:	mov    0x30(%rsp,%rax,4),%eax #eax=(rsp+4rax+30)#即输入数据,按rax的值为几就是第n+1个
   0x000000000040112b <+78>:	cmp    %eax,0x0(%rbp) #比较eax和rbp,rbp初值为输入数据第一个值的地址)
   0x000000000040112e <+81>:	jne    0x401135 <phase_6+88>
   0x0000000000401130 <+83>:	callq  0x401599 <explode_bomb>

   0x0000000000401135 <+88>:	add    $0x1,%ebx #ebx=ebx+1(初值为1)(加完为2)2 3 4 5 6
   0x0000000000401138 <+91>:	cmp    $0x5,%ebx #比较5和ebx (2 3 4 5 6
   0x000000000040113b <+94>:	jle    0x401124 <phase_6+71>
   0x000000000040113d <+96>:	add    $0x4,%r13 #r13原为输入的第一个数,加完变成第二个数
   0x0000000000401141 <+100>:	jmp    0x4010fc <phase_6+31>
##实际上就是每个数的值不能相等
##第一个输入判断的大逻辑结束
  

以下代码中出现了一个地址,我们读其中的内容:

x/128x 0x6042f0

它的名字叫node,从这里我们可以进行一些推测这里与节点和树(图)相关。 并且我们惊奇地发现这也是六个节点。接下来我们来看看汇编代码。

   0x0000000000401143 <+102>:	mov    0x8(%rdx),%rdx #node的下一个值赋给rdx
   0x0000000000401147 <+106>:	add    $0x1,%eax
   0x000000000040114a <+109>:	cmp    %ecx,%eax
   0x000000000040114c <+111>:	jne    0x401143 <phase_6+102>
   0x000000000040114e <+113>:	jmp    0x401155 <phase_6+120>
#根据输入数据,若等于n,跳转,此时为node_n

   0x0000000000401150 <+115>:	mov    $0x6042f0(node1),%edx #edx=node1
   0x0000000000401155 <+120>:	mov    %rdx,(%rsp,%rsi,2) #(rsp+2rsi)=rdx=x(rsi初值0)
##输入数据为n,则赋值node_n
   0x0000000000401159 <+124>:	add    $0x4,%rsi #rsi+=4
   0x000000000040115d <+128>:	cmp    $0x18,%rsi #rsi与24比较,即判断1是输入的第几个数
   0x0000000000401161 <+132>:	je     0x401178 <phase_6+155> #若是最后一个数,则去155

##进入新的阶段,这里是入口
   0x0000000000401163 <+134>:	mov    0x30(%rsp,%rsi,1),%ecx#rsi初值0,ecx:输入的第一个值
    #ecx为输入的第二个值
   0x0000000000401167 <+138>:	cmp    $0x1,%ecx #ecx与1比较
   0x000000000040116a <+141>:	jle    0x401150 <phase_6+115> #ecx小于等于1跳转至115行
    #根据前面的判断条件,我们可以知道这里只有一个输入可以小于等于1
   0x000000000040116c <+143>:	mov    $0x1,%eax #eax=1
   0x0000000000401171 <+148>:	mov    $0x6042f0,%edx #这个地址我们发现是node1
   0x0000000000401176 <+153>:	jmp    0x401143 <phase_6+102>

##这一部分结束,总的来说,逻辑就是遍历每一个输入,若输入为n,则找到node_n赋值,并赋值到从rsp开始的栈内,每个占八字节

因此我们需要来看看,node的每个值是多少

然后,聪明的我们发现,每个node可以分成两部分,后面八个字节是下一个node的地址!因此我们推翻之前的猜测,这里的node更应该是链表。接下来,我们继续来看汇编代码

   0x0000000000401178 <+155>:	mov    (%rsp),%rbx #rbx=(rsp),第一个node的地址
   0x000000000040117c <+159>:	lea    0x8(%rsp),%rax #rax=rsp+8,下一个node在栈内的地址
   0x0000000000401181 <+164>:	lea    0x30(%rsp),%rsi #rsi=rsp+30,我们输入的数的开始地址
   0x0000000000401186 <+169>:	mov    %rbx,%rcx #rcx=rbx

   0x0000000000401189 <+172>:	mov    (%rax),%rdx #rdx=(rax)
   0x000000000040118c <+175>:	mov    %rdx,0x8(%rcx) #
   0x0000000000401190 <+179>:	add    $0x8,%rax
   0x0000000000401194 <+183>:	cmp    %rsi,%rax
   0x0000000000401197 <+186>:	je     0x40119e <phase_6+193>
   0x0000000000401199 <+188>:	mov    %rdx,%rcx
   0x000000000040119c <+191>:	jmp    0x401189 <phase_6+172>
   0x000000000040119e <+193>:	movq   $0x0,0x8(%rdx)
##总的来说,这是把节点链接起来,即第一个数对应的结点的指针域(后8字节)存储第二个数对应节点的地址,依此类推,最后让数6对应节点的指针域指向 Null

   0x00000000004011a6 <+201>:	mov    $0x5,%ebp
   0x00000000004011ab <+206>:	mov    0x8(%rbx),%rax
   0x00000000004011af <+210>:	mov    (%rax),%eax
   0x00000000004011b1 <+212>:	cmp    %eax,(%rbx) #rbx里的值要小于等于eax
   0x00000000004011b3 <+214>:	jle    0x4011ba <phase_6+221>
   0x00000000004011b5 <+216>:	callq  0x401599 <explode_bomb>
   0x00000000004011ba <+221>:	mov    0x8(%rbx),%rbx
   0x00000000004011be <+225>:	sub    $0x1,%ebp
   0x00000000004011c1 <+228>:	jne    0x4011ab <phase_6+206>
##最后这里就是让数1对应节点的内容(取4个字节)小于数 2 对应节点的内容,数2对应节点的内容小于数3对应节点的内容,以此类推。
#之后就是很平常的结束phase_6调用的代码啦,就不放在这里占地方了。

 因此我们需要将每个node的值都给出来:

node_10x20a
node_20x7a
node_30x358
node_40x1b8
node_50x1a4
node_60x35b

因此,node:6>3>1>4>5>2;所以我们的key应该是2 5 4 1 3 6!

让我们来试试!

好耶~= ̄ω ̄=

当然...事情没有那么简单——secret_phase

隐藏关卡的解法就不在这里详细写了hhhh毕竟是隐藏关卡

tips:想找到它,可以去看看phase_defused函数!

最终的key文件:

然后!解决!

  • 19
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值