深入理解计算机系统bomb实验

深入理解计算机系统Bomb实验

  • 前言
  • 准备阶段
    • 上传bomb.c文件
    • 生成汇编代码
    • 进入gdb调试模式
    • 获取主要函数的汇编代码
  • 实验阶段
    • Phase1
      • 实验探究
      • 输入字符串首地址的保存
      • 继续phase1的研究
      • 通关密钥
    • Phase2
      • 实验探究
      • 通关密钥
    • Phase3
      • 实验探究
      • sscanf语句
      • swith-case语句
      • 通关密钥
    • Phase4
      • 实验探究
      • 通关密钥
    • Phase5
      • 实验探究
      • 通关密钥
    • Phase6
      • 实验探究
      • 通关密钥
    • 秘密关卡
      • 实验探究
        • 找到隐藏关卡入口
        • secret_phase探究
  • 小结

前言

最近我在学计算机系统时,做到了一个蛮有趣的实验游戏——bomb实验(其实就是一个c程序)。这个实验有六关,每一关需要输入一个字符串(可以称之为密钥),每一关只有输入正确的密钥才能通过,否则“炸弹“将会爆炸。因此,我们需要通过汇编c代码找出汇编文件中藏有的密钥的信息。通过这个实验,我的汇编语言能力获得了极好的锻炼,因此纪录过程以供大家分享并作为纪念。

准备阶段

上传bomb.c文件

首先,据实验的要求,我们需要一台linux机器进行gdb调试。我们可以使用linux虚拟机或者租一台linux云主机,在这里我使用的是腾讯云的云主机。在这台主机上,我新建一个文件夹,并将实验所需要的bomb.c文件进行上传。
使用工具:xftp
深入理解计算机系统bomb实验_第1张图片

生成汇编代码

在上传完成后,我们打开已连接上服务器的xshell或者虚拟机的teminal应用,使用cd命令进入到刚才上传到的文件夹中。之后,我们使用objdump -d bomb > bomb_assembly.S命令生成一个名为bomb_assembly.S的汇编代码文件
生成汇编代码

进入gdb调试模式

使用gdb bomb命令进入bomb.c文件的调试模式
进入gdb调试

获取主要函数的汇编代码

首先,我们打开已经生成好的汇编语言文件(可以在linux的文件管理器中打开或者通过xftp打开),找到main函数,将其复制下来,单独保存至一个文件中,方便查看,

0000000000400da0 
: # ...省略了以上的部分汇编代码 400e32: e8 67 06 00 00 callq 40149e 400e37: 48 89 c7 mov %rax,%rdi 400e3a: e8 a1 00 00 00 callq 400ee0 400e3f: e8 80 07 00 00 callq 4015c4 400e44: bf a8 23 40 00 mov $0x4023a8,%edi 400e49: e8 c2 fc ff ff callq 400b10 400e4e: e8 4b 06 00 00 callq 40149e 400e53: 48 89 c7 mov %rax,%rdi 400e56: e8 a1 00 00 00 callq 400efc 400e5b: e8 64 07 00 00 callq 4015c4 400e60: bf ed 22 40 00 mov $0x4022ed,%edi 400e65: e8 a6 fc ff ff callq 400b10 400e6a: e8 2f 06 00 00 callq 40149e 400e6f: 48 89 c7 mov %rax,%rdi 400e72: e8 cc 00 00 00 callq 400f43 400e77: e8 48 07 00 00 callq 4015c4 400e7c: bf 0b 23 40 00 mov $0x40230b,%edi 400e81: e8 8a fc ff ff callq 400b10 400e86: e8 13 06 00 00 callq 40149e 400e8b: 48 89 c7 mov %rax,%rdi 400e8e: e8 79 01 00 00 callq 40100c 400e93: e8 2c 07 00 00 callq 4015c4 400e98: bf d8 23 40 00 mov $0x4023d8,%edi 400e9d: e8 6e fc ff ff callq 400b10 400ea2: e8 f7 05 00 00 callq 40149e 400ea7: 48 89 c7 mov %rax,%rdi 400eaa: e8 b3 01 00 00 callq 401062 400eaf: e8 10 07 00 00 callq 4015c4 400eb4: bf 1a 23 40 00 mov $0x40231a,%edi 400eb9: e8 52 fc ff ff callq 400b10 400ebe: e8 db 05 00 00 callq 40149e 400ec3: 48 89 c7 mov %rax,%rdi 400ec6: e8 29 02 00 00 callq 4010f4 400ecb: e8 f4 06 00 00 callq 4015c4 400ed0: b8 00 00 00 00 mov $0x0,%eax 400ed5: 5b pop %rbx 400ed6: c3 retq 400ed7: 90 nop 400ed8: 90 nop 400ed9: 90 nop 400eda: 90 nop 400edb: 90 nop 400edc: 90 nop 400edd: 90 nop 400ede: 90 nop 400edf: 90 nop # ...省略了以下phase3-phase6的汇编代码

我们发现,phase1-phase6函数似乎就恰好对应题目中给出的第一关到第六关。于是,在gdb中,我们可以快速地使用disas命令进入这些函数的汇编代码中一探究竟。

  400e3a:	e8 a1 00 00 00       	callq  400ee0   #找到函数名前的函数开始指令的地址
  400e3f:	e8 80 07 00 00       	callq  4015c4 

进入gdb模式,使用disas 0x400ee0进入phase1的汇编代码。
disas 命令
重复上述步骤,依次快速复制phase1到phase6的代码,将代码分别复制到不同文件中。

实验阶段

Phase1

实验探究

打开复制下来的phase1的代码文件
关于每一步的解释以# 的注释标在代码后

   0x0000000000400ee0 <+0>:	sub    $0x8,%rsp  # 在栈中开辟一个8字节的临时空间
   0x0000000000400ee4 <+4>:	mov    $0x402400,%esi  # 将0x402400的值作为的参数传入
   0x0000000000400ee9 <+9>:	callq  0x401338 
   0x0000000000400eee <+14>:	test   %eax,%eax   # 测试该函数返回值
   0x0000000000400ef0 <+16>:	je     0x400ef7    # 若返回值为0则跳过炸弹爆炸函数
   0x0000000000400ef2 <+18>:	callq  0x40143a   # 返回值为1,炸
   0x0000000000400ef7 <+23>:	add    $0x8,%rsp  # 恢复栈
   0x0000000000400efb <+27>:	retq  

由字面意义可知,函数起到了一个比较字符串是否相等的作用。于是,猜测该函数具有两个参数——一个是我们输入的字符串的首地址,另一个是待比较的字符串的首地址

输入字符串首地址的保存

返回main函数保存的文件中,查看调用phase1函数之前的几句代码,发现确实如此。参数寄存器%rdi被函数的返回值赋值。因此,猜测函数用于读取一行字符串,将返回值保存于%rax,而被赋值的%rdi中存储的就是输入的字符串的首地址

  400e32:	e8 67 06 00 00       	callq  40149e 
  400e37:	48 89 c7             	mov    %rax,%rdi
  400e3a:	e8 a1 00 00 00       	callq  400ee0 

再观察其他的phase函数调用前的语句,我们都可以发现类似情况。因此,我们认为这些%rdi寄存器在phase函数的一开始,起到的是存储输入字符串首地址的作用

  400e4e:	e8 4b 06 00 00       	callq  40149e 
  400e53:	48 89 c7             	mov    %rax,%rdi
  400e56:	e8 a1 00 00 00       	callq  400efc 

继续phase1的研究

于是,我们可以继续猜想,在调用函数之前的%esi寄存器中是否也存储了一个待比较的字符串首地址。

  0x0000000000400ee4 <+4>:	mov    $0x402400,%esi  # 将0x402400的值作为的参数传入

使用,x/s命令将0x402400中存储的字符串导出。答案如我们所愿。
x/s 0x402400
因此,phase1函数的作用只是单纯的让我们输入一个字符串。再将我们输入的字符串和存储的字符串进行比较而已。

通关密钥

密钥即为“Border relations with Canada have never been better.”,输入即可通关。

使用run命令执行bomb.c程序,再输入第一关密钥
深入理解计算机系统bomb实验_第2张图片

Phase2

实验探究

打开phase2汇编代码被保存的文件

   0x0000000000400efc <+0>:	push   %rbp
   0x0000000000400efd <+1>:	push   %rbx
   0x0000000000400efe <+2>:	sub    $0x28,%rsp  # 产生一块40字节大小的临时空间
   0x0000000000400f02 <+6>:	mov    %rsp,%rsi  # 将栈指针赋值给参数寄存器
   0x0000000000400f05 <+9>:	callq  0x40145c 
   0x0000000000400f0a <+14>:	cmpl   $0x1,(%rsp)  # 比较1和栈顶元素的大小
   0x0000000000400f0e <+18>:	je     0x400f30   
   0x0000000000400f10 <+20>:	callq  0x40143a   # 若不相等则炸
   0x0000000000400f15 <+25>:	jmp    0x400f30 
   0x0000000000400f17 <+27>:	mov    -0x4(%rbx),%eax  # 将栈中的上一个元素值赋值
   0x0000000000400f1a <+30>:	add    %eax,%eax  # 栈中的上一个元素值*2后保存
   0x0000000000400f1c <+32>:	cmp    %eax,(%rbx)  # 将上一个元素值的2倍与%rbx对应的值(现元素值)进行比较
   0x0000000000400f1e <+34>:	je     0x400f25 
   0x0000000000400f20 <+36>:	callq  0x40143a 
   0x0000000000400f25 <+41>:	add    $0x4,%rbx   # 将%rbx的+=4
   0x0000000000400f29 <+45>:	cmp    %rbp,%rbx  # 比较是否等于尾指针
   0x0000000000400f2c <+48>:	jne    0x400f17 
   0x0000000000400f2e <+50>:	jmp    0x400f3c 
   0x0000000000400f30 <+52>:	lea    0x4(%rsp),%rbx  # 将栈指针加4的赋值
   0x0000000000400f35 <+57>:	lea    0x18(%rsp),%rbp  # 将栈的尾指针赋值
   0x0000000000400f3a <+62>:	jmp    0x400f17 
   0x0000000000400f3c <+64>:	add    $0x28,%rsp
   0x0000000000400f40 <+68>:	pop    %rbx
   0x0000000000400f41 <+69>:	pop    %rbp
   0x0000000000400f42 <+70>:	retq 

首先函数将栈指针传入参数寄存器,考虑到紧挨的read_six_number函数。猜测栈指针作为参数用于保存数字,而另一参数%rdi(上文提到)给出输入字符串地址。因此,函数read_six_number函数用于将输入的字符串转换为6个数字。 得知,本轮需要输入6个数字作为密钥。

接下来从<+14>语句中得知,第一个输入的数字是1。


<+41>(偏移量++)
<+57>(尾指针的赋值)
<+45> (偏移量等于尾值)
判断出,这函数当中存在一个循环。循环中%rbx依次保存栈中的所有元素的地址。而又由<+27>-<+32>语句中可知,栈中元素满足这样的排列:栈中每一元素是它上一元素的两倍 ,即需输入一个首项为1,公比为2,项数为6的等比数列。

通关密钥

在这里插入图片描述

Phase3

实验探究

   0x0000000000400f43 <+0>:	sub    $0x18,%rsp  #栈指针减24用来存放3个临时变量(看大小决定个数)
   0x0000000000400f47 <+4>:	lea    0xc(%rsp),%rcx  #%rcx=栈指针+12(参数)
   0x0000000000400f4c <+9>:	lea    0x8(%rsp),%rdx  #%rdx=栈指针+8(参数)
   0x0000000000400f51 <+14>:	mov    $0x4025cf,%esi  #某个参数的传递
   0x0000000000400f56 <+19>:	mov    $0x0,%eax  #对返回值赋值0,为sscanf语句做准备
   0x0000000000400f5b <+24>:	callq  0x400bf0 <__isoc99_sscanf@plt>#按格式读入输入
   0x0000000000400f60 <+29>:	cmp    $0x1,%eax    #将返回值与1进行比较
   0x0000000000400f63 <+32>:	jg     0x400f6a   #若返回值大于1(说明scanf的参数大于1),jump39
   0x0000000000400f65 <+34>:	callq  0x40143a    #否则炸
   0x0000000000400f6a <+39>:	cmpl   $0x7,0x8(%rsp)  #比较第一个数字与7的大小
   0x0000000000400f6f <+44>:	ja     0x400fad   #若>7,跳转106,炸;并且是无符号数的比较。
   0x0000000000400f71 <+46>:	mov    0x8(%rsp),%eax    #把第一个数字的值给%eax
   0x0000000000400f75 <+50>:	jmpq   *0x402470(,%rax,8)   #这是属于跳转表的形式,
   0x0000000000400f7c <+57>:	mov    $0xcf,%eax   #以下就是把某一个值放到%eax中在做<+123>的过程,就是switch-case语句
   0x0000000000400f81 <+62>:	jmp    0x400fbe   
   0x0000000000400f83 <+64>:	mov    $0x2c3,%eax
   0x0000000000400f88 <+69>:	jmp    0x400fbe 
   0x0000000000400f8a <+71>:	mov    $0x100,%eax  
   0x0000000000400f8f <+76>:	jmp    0x400fbe 
   0x0000000000400f91 <+78>:	mov    $0x185,%eax
   0x0000000000400f96 <+83>:	jmp    0x400fbe 
   0x0000000000400f98 <+85>:	mov    $0xce,%eax
   0x0000000000400f9d <+90>:	jmp    0x400fbe 
   0x0000000000400f9f <+92>:	mov    $0x2aa,%eax
   0x0000000000400fa4 <+97>:	jmp    0x400fbe 
0x0000000000400fa6 <+99>:	mov    $0x147,%eax
   0x0000000000400fab <+104>:	jmp    0x400fbe 
   0x0000000000400fad <+106>:	callq  0x40143a 
   0x0000000000400fb2 <+111>:	mov    $0x0,%eax
   0x0000000000400fb7 <+116>:	jmp    0x400fbe 
   0x0000000000400fb9 <+118>:	mov    $0x137,%eax
   0x0000000000400fbe <+123>:	cmp    0xc(%rsp),%eax #都是在拿rsp+12的地址对应的值与eax进行比较
   0x0000000000400fc2 <+127>:	je     0x400fc9   #若等就会结束,成功;不等,就会炸
   0x0000000000400fc4 <+129>:	callq  0x40143a 
   0x0000000000400fc9 <+134>:	add    $0x18,%rsp
   0x0000000000400fcd <+138>:	retq

sscanf语句

在该汇编语言中,使用了sscanf格式化输入。sscanf的语句同read_six_number函数类似,只是具有了更灵活的形式。参数有需要规定的输入形式 ,本语句中以%esi参数寄存器传入。通过该参数,函数可以将输入的合法字符串转换为规定的数字或者字符串。

使用x/s 命令查看,得知是“%d %d”,即需要输入两个整数。
%d %d

而另外两个参数,分别是栈的+12地址,栈的+8地址。这两个参数用作保存转换的数字。返回值是输入成功的值的个数。这里是两个%d,所以若正常按格式输入两个数字,返回值应大于1。据<+32>得,若不大于1,则炸弹爆炸。

swith-case语句

接着是一个典型的swith-case语句。首先在<+44>中,将第一个数字与7进行无符号的小于比较。这是在规定输入的第一个数字必须是0-6之间的第一个数(包含0,6)。然后,是一个经典的跳转表形式。
在这里插入图片描述
通过<+46>语句,第一个数字成为了跳转表的参数<+50>。<+57><+64>等语句,分别对应的输入第一个数字为0-6情况的不同跳转。

在跳转后,将某个值(每个跳转对应的值均不同)存入%eax寄存器中。接着统一跳转至<+123>,0-6对应的不同的%eax的结果与第二个数字进行比较。若相等,方可通过。

第一个数字 对应的case语句下取出的值
0 0xcf=207
1 0x137=311
2 0x2c3=707
3 0x100=256
4 0x185=389
5 0xce=206
6 0x2aa

通关密钥

因此,本局关卡需要输入两个数字。第一个数字必须是0-6中的一个,而第二个数字通过swith-case语句对应0-6,需输入不同数字。

通关实例 (以第一个数字0为例)
在这里插入图片描述

Phase4

实验探究

   0x000000000040100c <+0>:	sub    $0x18,%rsp     
   0x0000000000401010 <+4>:	lea    0xc(%rsp),%rcx  //要用的参数,放入参数寄存器中给scanf存
   0x0000000000401015 <+9>:	lea    0x8(%rsp),%rdx  //要用的参数
   0x000000000040101a <+14>:	mov    $0x4025cf,%esi    //这个也是“%d %d”
   0x000000000040101f <+19>:	mov    $0x0,%eax
   0x0000000000401024 <+24>:	callq  0x400bf0 <__isoc99_sscanf@plt>
   0x0000000000401029 <+29>:	cmp    $0x2,%eax  //不是正常的两个参数就炸
   0x000000000040102c <+32>:	jne    0x401035  
   0x000000000040102e <+34>:	cmpl   $0xe,0x8(%rsp)  //这个值和14
   0x0000000000401033 <+39>:	jbe    0x40103a   //低于或者相等
   0x0000000000401035 <+41>:	callq  0x40143a 
   0x000000000040103a <+46>:	mov    $0xe,%edx
   0x000000000040103f <+51>:	mov    $0x0,%esi
   0x0000000000401044 <+56>:	mov    0x8(%rsp),%edi
   0x0000000000401048 <+60>:	callq  0x400fce 
   0x000000000040104d <+65>:	test   %eax,%eax  //返回0才是正确做法
   0x000000000040104f <+67>:	jne    0x401058 
   0x0000000000401051 <+69>:	cmpl   $0x0,0xc(%rsp)   //再比较第二个输入值和0的关系
   0x0000000000401056 <+74>:	je     0x40105d   //需等于0,否则炸
   0x0000000000401058 <+76>:	callq  0x40143a 
   0x000000000040105d <+81>:	add    $0x18,%rsp
   0x0000000000401061 <+85>:	retq   

该函数简单明了,同Phase3,同样使用了一个sscanf语句,同样是"%d %d"格式输入。因此,密钥仍为两个数字。其次,0x8(%rsp)作为第一个数字,应该满足<+34>语句,即低于或者小于14。最后,func4的返回值必须是0,而func4的参数在<+46>-<+56>中给出。

于是,我们通过disas 命令获取func4的汇编代码。(这里不再示例gdb的使用)

   0x0000000000400fce <+0>:	sub    $0x8,%rsp
   0x0000000000400fd2 <+4>:	mov    %edx,%eax   # result=14
   0x0000000000400fd4 <+6>:	sub    %esi,%eax # result-=0,不变
   0x0000000000400fd6 <+8>:	mov    %eax,%ecx  
   0x0000000000400fd8 <+10>:	shr    $0x1f,%ecx  # %ecx逻辑右移31位,补0,取最高位之意
   0x0000000000400fdb <+13>:	add    %ecx,%eax  # 拿自己的最高位加上result;(负数加1正数加0)14+0=0
   0x0000000000400fdd <+15>:	sar    %eax  # 算术右移,单操作数是只移动一位的意思  7
   0x0000000000400fdf <+17>:	lea    (%rax,%rsi,1),%ecx  # 7+0=%ecx
   0x0000000000400fe2 <+20>:	cmp    %edi,%ecx  # 比较第一个输入值和%ecx的关系
   0x0000000000400fe4 <+22>:	jle    0x400ff2 
   0x0000000000400fe6 <+24>:	lea    -0x1(%rcx),%edx  
   0x0000000000400fe9 <+27>:	callq  0x400fce 
   0x0000000000400fee <+32>:	add    %eax,%eax
   0x0000000000400ff0 <+34>:	jmp    0x401007 
   0x0000000000400ff2 <+36>:	mov    $0x0,%eax  # 给出0
   0x0000000000400ff7 <+41>:	cmp    %edi,%ecx  # 再比较一次
   0x0000000000400ff9 <+43>:	jge    0x401007  #  大于等于
   0x0000000000400ffb <+45>:	lea    0x1(%rcx),%esi
   0x0000000000400ffe <+48>:	callq  0x400fce 
   0x0000000000401003 <+53>:	lea    0x1(%rax,%rax,1),%eax
   0x0000000000401007 <+57>:	add    $0x8,%rsp
   0x000000000040100b <+61>:	retq   

根据注释,我们可以得出:当返回值为0时,第一个数字需为7。 再回到函数phase4中来,我们看到有<+69>语句,该语句规定了第二个数字需为0这一输入

通关密钥

在这里插入图片描述

Phase5

实验探究

   0x0000000000401062 <+0>:	push   %rbx
   0x0000000000401063 <+1>:	sub    $0x20,%rsp  //开辟一个32字节的空间
   0x0000000000401067 <+5>:	mov    %rdi,%rbx    //rdi是输入字符串数组地址 
   0x000000000040106a <+8>:	mov    %fs:0x28,%rax  //  栈溢出保护
   0x0000000000401073 <+17>:	mov    %rax,0x18(%rsp)  //把返回值存储到栈临时内存中
   0x0000000000401078 <+22>:	xor    %eax,%eax  //异或自己,置零
   0x000000000040107a <+24>:	callq  0x40131b   
   0x000000000040107f <+29>:	cmp    $0x6,%eax  //字符串长度与6比较
   0x0000000000401082 <+32>:	je     0x4010d2   //等于的话跳转,否则炸
   0x0000000000401084 <+34>:	callq  0x40143a 
   0x0000000000401089 <+39>:	jmp    0x4010d2 
   0x000000000040108b <+41>:	movzbl (%rbx,%rax,1),%ecx  //将字符依次赋值
   0x000000000040108f <+45>:	mov    %cl,(%rsp)   //%rcx的最低字节(依次的元素的值)给栈顶内存存储
   0x0000000000401092 <+48>:	mov    (%rsp),%rdx  //将这个字符赋值给%rdx
   0x0000000000401096 <+52>:	and    $0xf,%edx   //使得%edx高位的值被0覆盖掉,只剩0-15
   0x0000000000401099 <+55>:	movzbl 0x4024b0(%rdx),%edx  // 将(0x4024b0+%rdx)对应内存的值给了%edx
   0x00000000004010a0 <+62>:	mov    %dl,0x10(%rsp,%rax,1)   //再将%edx的低位保存在栈中
   0x00000000004010a4 <+66>:	add    $0x1,%rax  
   0x00000000004010a8 <+70>:	cmp    $0x6,%rax
   0x00000000004010ac <+74>:	jne    0x40108b //似乎是一个循环
   0x00000000004010ae <+76>:	movb   $0x0,0x16(%rsp)  //把0的值改写到这个字符串对应的结尾字符串,所以最后有'\0'做结尾
   0x00000000004010b3 <+81>:	mov    $0x40245e,%esi  //这个是需要比较的字符串地址
   0x00000000004010b8 <+86>:	lea    0x10(%rsp),%rdi  //这个是输入字符串地址
   0x00000000004010bd <+91>:	callq  0x401338   
   0x00000000004010c2 <+96>:	test   %eax,%eax  //是0就是两字符串相等
   0x00000000004010c4 <+98>:	je     0x4010d9 
   0x00000000004010c6 <+100>:	callq  0x40143a 
   0x00000000004010cb <+105>:	nopl   0x0(%rax,%rax,1) //对齐作用
   0x00000000004010d0 <+110>:	jmp    0x4010d9 
   0x00000000004010d2 <+112>:	mov    $0x0,%eax  //返回值后32位置0
   0x00000000004010d7 <+117>:	jmp    0x40108b  
   0x00000000004010d9 <+119>:	mov    0x18(%rsp),%rax
   0x00000000004010de <+124>:	xor    %fs:0x28,%rax  //看是否被改写,否则出现大问题,这可不是炸的问题了
   0x00000000004010e7 <+133>:	je     0x4010ee 
   0x00000000004010e9 <+135>:	callq  0x400b30 <__stack_chk_fail@plt>
   0x00000000004010ee <+140>:	add    $0x20,%rsp
   0x00000000004010f2 <+144>:	pop    %rbx
   0x00000000004010f3 <+145>:	retq   

首先,根据<+29>语句,函数的返回值,即输入字符串的长度必须等于6。 紧接着,跳转至<+112>语句将%eax置0,然后才返回<+41>语句继续进行。<+41>语句将字符依次赋值给%ecx 。我们注意到,%rax在<+66>处++,并且在<+74>处跳转回去形成一个循环,而在这个循环中%rax是每次都改变的。因此每一次循环都使得%ecx获得到的是下一个的新字符。因此,称之为依次

之后通过一系列的操作:%ecx->栈顶->%rdx,将这个元素的值赋给%rdx。接着对%rdx使用0xf掩码,使其只剩低4位有效。(注意,这使得后面可以免去输入低位ASCLL码的麻烦

我们接着看到<+55>语句,这是关键;这一语句将不同的%rbx值作为偏移量,对0x4024b0的地址进行偏移,从而获得不同的字符放入%edx寄存器中。然后再将%edx寄存器中的值放如栈中保存。在循环语句退出后(循环执行6次,我们可以得知应输入6个字符),将第7个字符设为/0标志字符串的结尾。然后是一个简单字符串比较函数(看保存在栈中的字符串与0x40245e作为首地址的字符串是否相等——<+81>中给出参数)。

所以,Phase5的接题关键是,输入的字符作为偏移量,可以刚好使得0x4024b0作为首地址偏移所对应得新的字符与0x40245e对应得字符依次相等

因此,我们分别使用x/s 0x4024b0以及x/s 0x40245e命令查看两个字符串。
在这里插入图片描述
在这里插入图片描述
得到,偏移量应该为 9 15 14 5 6 7 ,才能依次对应“f l y e r s”。
由于前面使用了0xf作为掩码,所以可以使用字符串“9?>567”代替低ascll码得输入。

通关密钥

在这里插入图片描述

Phase6

实验探究

Section1 准备工作

  0x00000000004010f4 <+0>:	push   %r14
   0x00000000004010f6 <+2>:	push   %r13
   0x00000000004010f8 <+4>:	push   %r12
   0x00000000004010fa <+6>:	push   %rbp
   0x00000000004010fb <+7>:	push   %rbx
   0x00000000004010fc <+8>:	sub    $0x50,%rsp   //开辟80字节的临时空间
   0x0000000000401100 <+12>:	mov    %rsp,%r13  //将栈指针保存到被调用者寄存器r13
   0x0000000000401103 <+15>:	mov    %rsp,%rsi   //将栈指针传入参数,用于接受那6个数字
   0x0000000000401106 <+18>:	callq  0x40145c 
   0x000000000040110b <+23>:	mov    %rsp,%r14   //r14同样用来保存栈指针
   0x000000000040110e <+26>:	mov    $0x0,%r12d  

由上述代码可知,需要输入6个数字作为密钥。

Section2


   0x0000000000401114 <+32>:	mov    %r13,%rbp   //将栈指针的值赋给%rbp保存(这里,每一次循环都会+4%r13)
   0x0000000000401117 <+35>:	mov    0x0(%r13),%eax  //将栈指针指向的数字(%eax也是32位的)赋给%eax
   0x000000000040111b <+39>:	sub    $0x1,%eax  //数字的值--
   0x000000000040111e <+42>:	cmp    $0x5,%eax  //减完以后和5进行比较
   0x0000000000401121 <+45>:	jbe    0x401128    //低于或者相等才可(也就是说,每一个数字都是要低于等于5的才行)(也不能是负数)
   0x0000000000401123 <+47>:	callq  0x40143a //否则炸
   0x0000000000401128 <+52>:	add    $0x1,%r12d  //0+1
   0x000000000040112c <+56>:	cmp    $0x6,%r12d  //比较和6比较大小,因此猜测是在一个循环中
   0x0000000000401130 <+60>:	je     0x401153 //这是跳出外层循环
   0x0000000000401132 <+62>:	mov    %r12d,%ebx   //将这个会变化的值(第一次是1)赋值给一个被调用者寄存器
   0x0000000000401135 <+65>:	movslq %ebx,%rax  //有符号数的低字节到高字节赋值,%rax被改变
   0x0000000000401138 <+68>:	mov    (%rsp,%rax,4),%eax  //将这个值对应的元素(每一次循环给一个)赋值给%eax
   0x000000000040113b <+71>:	cmp    %eax,0x0(%rbp)  //将这些数字与(%rbp进行比较)%rbp在外面其实一直在被递增(所以比较的始终是这个元素和它的上一个元素)
   0x000000000040113e <+74>:	jne    0x401145   //不等于才是对的
   0x0000000000401140 <+76>:	callq  0x40143a 
   0x0000000000401145 <+81>:	add    $0x1,%ebx  //再将这个计数器值加1
   0x0000000000401148 <+84>:	cmp    $0x5,%ebx  //将这个值与5进行比较
   0x000000000040114b <+87>:	jle    0x401135   //这是一个嵌套循环
   0x000000000040114d <+89>:	add    $0x4,%r13                  //将%r13+4
   0x0000000000401151 <+93>:	jmp    0x401114   

在这一部分,由注释可以得出,这一部分是一个嵌套循环。该嵌套循环有两层,外层循环作用是,确定输入这几个数字均在1-6之间(包含1,6)(见<+45>)。内层循环作用是,确定这几个数字互不相等(见<+71>)。

Section3

   0x0000000000401153 <+95>:	lea    0x18(%rsp),%rsi   //将指针+0x18地址对应的值赋值给%rsi参数
   0x0000000000401158 <+100>:	mov    %r14,%rax  //将栈指针的值传递给返回值寄存器
   0x000000000040115b <+103>:	mov    $0x7,%ecx  //将7赋值给第二个参数
   0x0000000000401160 <+108>:	mov    %ecx,%edx  //将第二个参数赋值给第三个参数    发现,第一个循环后%ecx不受影响,这是一个定值
   0x0000000000401162 <+110>:	sub    (%rax),%edx  //让7-%rax指向的数字
   0x0000000000401164 <+112>:	mov    %edx,(%rax)  //将这个结果赋值给这个数字
   0x0000000000401166 <+114>:	add    $0x4,%rax //让它指向第二个数字
   0x000000000040116a <+118>:	cmp    %rsi,%rax  //比较%rsi尾指针地址是否不同,这应该是最后一个数字,说明这是一个循环
---Type  to continue, or q  to quit---
   0x000000000040116d <+121>:	jne    0x401160 

该部分的作用即,将输入的数字分别转换为7-该数字。如1变为6…。其中栈指针对应的是栈顶元素。栈指针+8对应的是栈中的第二个元素。即栈中的每一个元素之间的地址间隔8个字节。

Section4

   0x000000000040116f <+123>:	mov    $0x0,%esi  //赋值0给%esi
   0x0000000000401174 <+128>:	jmp    0x401197 
   0x0000000000401176 <+130>:	mov    0x8(%rdx),%rdx   //将某个值给取出来赋值给%rdx==6304480
   0x000000000040117a <+134>:	add    $0x1,%eax  //将这个值+1
   0x000000000040117d <+137>:	cmp    %ecx,%eax  //这里是比较%ecx(每一个数字)和%eax(第一次是2)
   0x000000000040117f <+139>:	jne    0x401176   //若不等,跳转回到130,这是一个循环
   0x0000000000401181 <+141>:	jmp    0x401188  
   0x0000000000401183 <+143>:	mov    $0x6032d0,%edx            
   0x0000000000401188 <+148>:	mov    %rdx,0x20(%rsp,%rsi,2)  //将这个值存起来
   0x000000000040118d <+153>:	add    $0x4,%rsi   //将计数器++
   0x0000000000401191 <+157>:	cmp    $0x18,%rsi  //计数器退出条件
   0x0000000000401195 <+161>:	je     0x4011ab 
   0x0000000000401197 <+163>:	mov    (%rsp,%rsi,1),%ecx   //这一看又是一个循环,目的,将不同的数字给依次取出
   0x000000000040119a <+166>:	cmp    $0x1,%ecx   //比较这些数字和1的大小关系     
   0x000000000040119d <+169>:	jle    0x401183   //如果是小于等于1就直接到143
   0x000000000040119f <+171>:	mov    $0x1,%eax  //继续执行  将1赋值给%eax
   0x00000000004011a4 <+176>:	mov    $0x6032d0,%edx  //将这个值赋值给%edx(复原)
   0x00000000004011a9 <+181>:	jmp    0x401176 

这一部分起到了关键作用。首先将栈中元素取出<+163>(这里同样是依次取出),置于%ecx。接着是一个判断语句<+143>,我们首先考虑栈顶元素等于1(这是经过了section3后的,原值是6)的情况——这时,栈顶元素被覆盖为0x6032d0 <+148>。

那么,我们继续考虑栈中元素大于1的情况。此时均会跳转至<+130>处。在<+130>到<+141>之间是一个循环。若栈顶元素是2,则只执行一次<+130>语句后退出。若是3,则执行两次,依此类推。

而<+130>语句,实际上是对%rdx+8这一地址取值后,赋值给%rbx自己(可以看成是一个链表:p=p->next)

因此,不同的值对应的不同结果如下。栈中最后会按照原来对应的数字来保存不同的地址
深入理解计算机系统bomb实验_第3张图片
Section5
在这里,我们为方便叙述,我们将栈中存的地址称为地址元素

   0x00000000004011ab <+183>:	mov    0x20(%rsp),%rbx   //给定开始地址元素
   0x00000000004011b0 <+188>:	lea    0x28(%rsp),%rax     //这是下一元素的栈地址
   0x00000000004011b5 <+193>:	lea    0x50(%rsp),%rsi   //这个是末尾元素的栈地址
   0x00000000004011ba <+198>:	mov    %rbx,%rcx    //开始地址元素的赋值
   0x00000000004011bd <+201>:	mov    (%rax),%rdx       //将栈中的下一个地址元素赋值给%rdx(中转站)
   0x00000000004011c0 <+204>:	mov    %rdx,0x8(%rcx)   //这是将栈中的下一个地址元素赋值给(上一地址元素+8)对应的内存中
   0x00000000004011c4 <+208>:	add    $0x8,%rax  //将%rax+8(下一栈地址)
   0x00000000004011c8 <+212>:	cmp    %rsi,%rax  //退出条件:下一栈地址等于末尾元素栈地址(所以只循环五次)此时%rdx为第五个地址元素
   0x00000000004011cb <+215>:	je     0x4011d2 
   0x00000000004011cd <+217>:	mov    %rdx,%rcx   //中转站中的值赋值给%rcx(这里是把第二个地址元素赋给(原来的开始元素),副本间的赋值)
   0x00000000004011d0 <+220>:	jmp    0x4011bd 

该部分的作用:把栈中所有的下一个地址元素赋值给,其上一个地址元素+8对应的内存中,见<+204>。但是,栈本身存的地址元素并没有被改变。而且,这些地址元素指向的值确实也没有被改变,因为改变的是地址元素+8对应的内存值,而我们不是地址元素对应的内存值。(注:地址元素所对应的值的大小只占8字节的大小

Section6

   0x00000000004011d2 <+222>:	movq   $0x0,0x8(%rdx)   //将0值赋给(最后一个地址元素+8)对应内存中
   0x00000000004011da <+230>:	mov    $0x5,%ebp
   0x00000000004011df <+235>:	mov    0x8(%rbx),%rax  //这里是(现地址元素+8对应的内存值)对应的内存(注意,即下一个地址元素)
   0x00000000004011e3 <+239>:	mov    (%rax),%eax   //将这个地址元素再解引用得到(就是下一个地址元素指向的值!!注意,这个值是没有被改变的!)
   0x00000000004011e5 <+241>:	cmp    %eax,(%rbx)   //将这个值与上一个元素地址对应的内存(均没有被改变!)进行比较
   0x00000000004011e7 <+243>:	jge    0x4011ee   上一个元素地址对应的值需要大于等于下一个的
   0x00000000004011e9 <+245>:	callq  0x40143a 
   0x00000000004011ee <+250>:	mov    0x8(%rbx),%rbx  //将%rbx++
   0x00000000004011f2 <+254>:	sub    $0x1,%ebp  //计数器,五次循环,比较只需要五次就可以比完
   0x00000000004011f5 <+257>:	jne    0x4011df   //若不等于则回去
---Type  to continue, or q  to quit---
   0x00000000004011f7 <+259>:	add    $0x50,%rsp
   0x00000000004011fb <+263>:	pop    %rbx
   0x00000000004011fc <+264>:	pop    %rbp
   0x00000000004011fd <+265>:	pop    %r12
   0x00000000004011ff <+267>:	pop    %r13
   0x0000000000401201 <+269>:	pop    %r14
   0x0000000000401203 <+271>:	retq   

在这部分,我们使用一个循环将栈中所有的地址元素对应的值,和它的下一个地址元素(见<+235><+239>)对应的值进行比较(见<+241>)。而比较的目的是,让下一个元素地址对应的值均大于现地址元素对应的值。

因此,我们使用x/g 语句,对于0x6032d0-0x603320对应的值查询。

地址
0x6032d0 332
0x6032e0 168
0x6032f0 924
0x603300 691
0x603310 477
0x603320 443

再对应上一张表,我们可以得出使得对应元素值从大到小数字串是"4 3 2 1 6 5"

通关密钥

在这里插入图片描述

秘密关卡

实验探究

我们注意到,每一个函数后面都存在一个phase_defused函数。而当我们使用disas命令汇编这一代码时,却意外的发现位于代码<+108>语句下的secret_phase关卡

   0x00000000004015c4 <+0>:	sub    $0x78,%rsp
   0x00000000004015c8 <+4>:	mov    %fs:0x28,%rax  
   0x00000000004015d1 <+13>:	mov    %rax,0x68(%rsp) #将phase函数的返回值放入栈中保存
   0x00000000004015d6 <+18>:	xor    %eax,%eax  # 置0
   0x00000000004015d8 <+20>:	cmpl   $0x6,0x202181(%rip)        # 0x603760 
   0x00000000004015df <+27>:	jne    0x40163f 
   0x00000000004015e1 <+29>:	lea    0x10(%rsp),%r8 # sscanf参数,下同
   0x00000000004015e6 <+34>:	lea    0xc(%rsp),%rcx
   0x00000000004015eb <+39>:	lea    0x8(%rsp),%rdx
   0x00000000004015f0 <+44>:	mov    $0x402619,%esi  # “%d %d %s”
   0x00000000004015f5 <+49>:	mov    $0x603870,%edi
   0x00000000004015fa <+54>:	callq  0x400bf0 <__isoc99_sscanf@plt>
   0x00000000004015ff <+59>:	cmp    $0x3,%eax  # 参数有3
   0x0000000000401602 <+62>:	jne    0x401635 
   0x0000000000401604 <+64>:	mov    $0x402622,%esi
   0x0000000000401609 <+69>:	lea    0x10(%rsp),%rdi  # %rdi中存储的是字符串
   0x000000000040160e <+74>:	callq  0x401338  # 比较两字符串
   0x0000000000401613 <+79>:	test   %eax,%eax
   0x0000000000401615 <+81>:	jne    0x401635 
   0x0000000000401617 <+83>:	mov    $0x4024f8,%edi   # 输出函数提示
   0x000000000040161c <+88>:	callq  0x400b10 
   0x0000000000401621 <+93>:	mov    $0x402520,%edi
   0x0000000000401626 <+98>:	callq  0x400b10 
   0x000000000040162b <+103>:	mov    $0x0,%eax  # 将返回值置0
---Type  to continue, or q  to quit---
   0x0000000000401630 <+108>:	callq  0x401242 
   0x0000000000401635 <+113>:	mov    $0x402558,%edi
   0x000000000040163a <+118>:	callq  0x400b10 
   0x000000000040163f <+123>:	mov    0x68(%rsp),%rax
   0x0000000000401644 <+128>:	xor    %fs:0x28,%rax
   0x000000000040164d <+137>:	je     0x401654 
   0x000000000040164f <+139>:	callq  0x400b30 <__stack_chk_fail@plt>
   0x0000000000401654 <+144>:	add    $0x78,%rsp
   0x0000000000401658 <+148>:	retq   

找到隐藏关卡入口
  • 首先,在<+20><+27>语句中,使用了一个 0x202181(%rip) 的值,这个值从汇编代码自带的提示中可以猜想得到,是一个当前已经输入的关卡数,即字符串数。而该比较语句的含义是,只有在已完成关卡6的情况下才能进入到secret_phase。否则,将会直接跳到<+123>语句,失去进入<+108>秘密关卡函数的机会。
  • 其次,是一个sscanf语句。然而,sscanf语言的参数相比于前面的参数要多了一个。通过x/s语句查看,输入格式为"%d %d %s",那么照理而言除了三个地址作为参数外,不应该有0x603870作为参数。
  • 于是,我们想到sscanf语句与scanf语句的不同之处。sscanf语句可以有一个字符串参数,用于指定sscanf所需输入的字符串的源。(默认源是标准输入)
  • 而这个字符串应当由我们输入触发隐藏关卡才对,为什么会由汇编语言给出?这就说明应该是我们前面的输入,保存到了这个地方。使得隐藏函数的以触发。

于是,我们尝试使用gdb进行端点的调试,查看0x603870处的这个字符串的值。我们在phase6的最后一条语句中设置断点。
在这里插入图片描述
接着,我们开始按照刚刚的答案一步步运行程序,到了断点处,程序将自动停止。
深入理解计算机系统bomb实验_第4张图片
接着,我们使用x/s命令查看0x603870处字符串的值
在这里插入图片描述
我们可以通过之前6关输入的字符串得知,**第4关的字符串就是触发隐藏关卡的入口。**但是,在第四关处,我们还应该输入某个字符串在"7 0"之后。于是,我们开始对这个字符串进行寻找。

我们发现,<+74>语句对两个字符串进行了比较。而其中一个字符串就是sscanf输入地址参数所指向的字符串。因此,我们了解到需要将与其作比较字符串作为输入。

使用x/s命令查看,我们知道可以通过在第4关输入"7 0 DrEvil"作为通关密钥的同时,开启隐藏关卡。
在这里插入图片描述

secret_phase探究
   0x0000000000401242 <+0>:	push   %rbx
   0x0000000000401243 <+1>:	callq  0x40149e 
   0x0000000000401248 <+6>:	mov    $0xa,%edx  # 按十进制转换
   0x000000000040124d <+11>:	mov    $0x0,%esi  # 将字符串要保存到的地址设置为空地址
   0x0000000000401252 <+16>:	mov    %rax,%rdi  # 要转换的字符串是标准输入进来的
   0x0000000000401255 <+19>:	callq  0x400bd0 
   0x000000000040125a <+24>:	mov    %rax,%rbx  # 保存该转换后的数字
   0x000000000040125d <+27>:	lea    -0x1(%rax),%eax  # 将该数字-=1
   0x0000000000401260 <+30>:	cmp    $0x3e8,%eax # 将已经减过1的数字与0x3e8进行比较
   0x0000000000401265 <+35>:	jbe    0x40126c  # 若低于或者等于则跳转
   0x0000000000401267 <+37>:	callq  0x40143a 
   0x000000000040126c <+42>:	mov    %ebx,%esi  # 将原数字赋值给该参数
   0x000000000040126e <+44>:	mov    $0x6030f0,%edi
   0x0000000000401273 <+49>:	callq  0x401204 
   0x0000000000401278 <+54>:	cmp    $0x2,%eax  # 若返回值为2,那么该函数拆弹成功
   0x000000000040127b <+57>:	je     0x401282 
   0x000000000040127d <+59>:	callq  0x40143a 
   0x0000000000401282 <+64>:	mov    $0x402438,%edi
   0x0000000000401287 <+69>:	callq  0x400b10 
   0x000000000040128c <+74>:	callq  0x4015c4 
   0x0000000000401291 <+79>:	pop    %rbx
   0x0000000000401292 <+80>:	retq   

  • 根据C库函数strtol的定义,我们得知这是一个转换字符串为数字的字符串。所以,我们需要输入一个数字。
  • 根据<+27><+30>,该数字需要低于0x3e8。
  • 在进入到func7函数后,若返回值为2,则跳转到<+64>打印出拆弹成功的字符串。

因此,关键在于func7函数。注意,初次调用func7时,我们的参数分别是输入的数字(%esi),以及0x6030f0(%rdi)。

fun7函数

   0x0000000000401204 <+0>:	sub    $0x8,%rsp
   0x0000000000401208 <+4>:	test   %rdi,%rdi  # 测试rdi
   0x000000000040120b <+7>:	je     0x401238  # 若为%rdi为0则跳转,返回0xffffffff,无法获得返回值为2的正确结果
   0x000000000040120d <+9>:	mov    (%rdi),%edx  # 获取该地址对应的值
   0x000000000040120f <+11>:	cmp    %esi,%edx  # 与输入数字进行比较,
   0x0000000000401211 <+13>:	jle    0x401220 
   0x0000000000401213 <+15>:	mov    0x8(%rdi),%rdi
   0x0000000000401217 <+19>:	callq  0x401204 
   0x000000000040121c <+24>:	add    %eax,%eax
   0x000000000040121e <+26>:	jmp    0x40123d 
   0x0000000000401220 <+28>:	mov    $0x0,%eax
   0x0000000000401225 <+33>:	cmp    %esi,%edx
   0x0000000000401227 <+35>:	je     0x40123d 
   0x0000000000401229 <+37>:	mov    0x10(%rdi),%rdi
   0x000000000040122d <+41>:	callq  0x401204 
   0x0000000000401232 <+46>:	lea    0x1(%rax,%rax,1),%eax
   0x0000000000401236 <+50>:	jmp    0x40123d 
   0x0000000000401238 <+52>:	mov    $0xffffffff,%eax
   0x000000000040123d <+57>:	add    $0x8,%rsp
   0x0000000000401241 <+61>:	retq   

这一部分的代码比较短,也比较好读,下面调用了两次fun7说明这也是一个递归程序,而且观察得到%rsi的值在整个递归的过程中没有变化过,起到的只是一个比较的作用。
一开始还检测了一下%rdi是否为0,后面设置递归参数的时候用mov 0x8(%rdi),%rdi,自身加上一个偏移量的间接寻址代替自身,基本可以确定%rdi是一个指针,%rdi+0x8和%rdi+0x10同样也是一个指针,看到这里基本已经猜出这个数据结构就是二叉树了,之后的寻找答案也就不难了,顺着左右儿子找一下就得到答案了:

答案是0x16,也就是22。

小结

这一次的bomb实验,包含了计算机系统中第三章汇编语言的几乎所有知识点。通过本次的练习,我的汇编语言能力获得了很好的锻炼,对于一些重要知识点(如跳转表,循环)的知识点,掌握的更加牢靠。而本实验中包含的许多有趣实用的汇编语言技巧(如一些精巧的中间变量的使用、灵活的jump跳转指令的运用)使我更加注意编程技巧的学习。汇编语言的学习无疑是一件重中之重的学习任务,学习之途任重而道远,今发此文,与诸君共勉。

——ECNU 杨政 (转载请声明)

你可能感兴趣的:(计算机系统)