深入理解计算机系统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
生成汇编代码
在上传完成后,我们打开已连接上服务器的xshell或者虚拟机的teminal应用,使用cd命令进入到刚才上传到的文件夹中。之后,我们使用objdump -d bomb > bomb_assembly.S命令生成一个名为bomb_assembly.S的汇编代码文件
进入gdb调试模式
使用gdb bomb命令进入bomb.c文件的调试模式
获取主要函数的汇编代码
首先,我们打开已经生成好的汇编语言文件(可以在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的汇编代码。
重复上述步骤,依次快速复制phase1到phase6的代码,将代码分别复制到不同文件中。
实验阶段
Phase1
实验探究
打开复制下来的phase1的代码文件
关于每一步的解释以# 的注释标在代码后
0x0000000000400ee0 :sub $0x8,%rsp # 在栈中开辟一个8字节的临时空间
0x0000000000400ee4 :mov $0x402400,%esi # 将0x402400的值作为的参数传入
0x0000000000400ee9 :callq 0x401338
0x0000000000400eee :test %eax,%eax # 测试该函数返回值
0x0000000000400ef0 :je 0x400ef7 # 若返回值为0则跳过炸弹爆炸函数
0x0000000000400ef2 :callq 0x40143a # 返回值为1,炸
0x0000000000400ef7 :add $0x8,%rsp # 恢复栈
0x0000000000400efb :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 :mov $0x402400,%esi # 将0x402400的值作为的参数传入
使用,x/s命令将0x402400中存储的字符串导出。答案如我们所愿。
因此,phase1函数的作用只是单纯的让我们输入一个字符串。再将我们输入的字符串和存储的字符串进行比较而已。
通关密钥
密钥即为“Border relations with Canada have never been better.”,输入即可通关。
使用run命令执行bomb.c程序,再输入第一关密钥
Phase2
实验探究
打开phase2汇编代码被保存的文件
0x0000000000400efc :push %rbp
0x0000000000400efd :push %rbx
0x0000000000400efe :sub $0x28,%rsp # 产生一块40字节大小的临时空间
0x0000000000400f02 :mov %rsp,%rsi # 将栈指针赋值给参数寄存器
0x0000000000400f05 :callq 0x40145c
0x0000000000400f0a :cmpl $0x1,(%rsp) # 比较1和栈顶元素的大小
0x0000000000400f0e :je 0x400f30
0x0000000000400f10 :callq 0x40143a # 若不相等则炸
0x0000000000400f15 :jmp 0x400f30
0x0000000000400f17 :mov -0x4(%rbx),%eax # 将栈中的上一个元素值赋值
0x0000000000400f1a :add %eax,%eax # 栈中的上一个元素值*2后保存
0x0000000000400f1c :cmp %eax,(%rbx) # 将上一个元素值的2倍与%rbx对应的值(现元素值)进行比较
0x0000000000400f1e :je 0x400f25
0x0000000000400f20 :callq 0x40143a
0x0000000000400f25 :add $0x4,%rbx # 将%rbx的+=4
0x0000000000400f29 :cmp %rbp,%rbx # 比较是否等于尾指针
0x0000000000400f2c :jne 0x400f17
0x0000000000400f2e :jmp 0x400f3c
0x0000000000400f30 :lea 0x4(%rsp),%rbx # 将栈指针加4的赋值
0x0000000000400f35 :lea 0x18(%rsp),%rbp # 将栈的尾指针赋值
0x0000000000400f3a :jmp 0x400f17
0x0000000000400f3c :add $0x28,%rsp
0x0000000000400f40 :pop %rbx
0x0000000000400f41 :pop %rbp
0x0000000000400f42 :retq
首先函数将栈指针传入参数寄存器,考虑到紧挨的read_six_number函数。猜测栈指针作为参数用于保存数字,而另一参数%rdi(上文提到)给出输入字符串地址。因此,函数read_six_number函数用于将输入的字符串转换为6个数字。 得知,本轮需要输入6个数字作为密钥。
接下来从语句中得知,第一个输入的数字是1。
由
(偏移量++)
(尾指针的赋值)
(偏移量等于尾值)
判断出,这函数当中存在一个循环。循环中%rbx依次保存栈中的所有元素的地址。而又由-语句中可知,栈中元素满足这样的排列:栈中每一元素是它上一元素的两倍 ,即需输入一个首项为1,公比为2,项数为6的等比数列。
通关密钥
Phase3
实验探究
0x0000000000400f43 :sub $0x18,%rsp #栈指针减24用来存放3个临时变量(看大小决定个数)
0x0000000000400f47 :lea 0xc(%rsp),%rcx #%rcx=栈指针+12(参数)
0x0000000000400f4c :lea 0x8(%rsp),%rdx #%rdx=栈指针+8(参数)
0x0000000000400f51 :mov $0x4025cf,%esi #某个参数的传递
0x0000000000400f56 :mov $0x0,%eax #对返回值赋值0,为sscanf语句做准备
0x0000000000400f5b :callq 0x400bf0 <__isoc99_sscanf>#按格式读入输入
0x0000000000400f60 :cmp $0x1,%eax #将返回值与1进行比较
0x0000000000400f63 :jg 0x400f6a #若返回值大于1(说明scanf的参数大于1),jump39
0x0000000000400f65 :callq 0x40143a #否则炸
0x0000000000400f6a :cmpl $0x7,0x8(%rsp) #比较第一个数字与7的大小
0x0000000000400f6f :ja 0x400fad #若>7,跳转106,炸;并且是无符号数的比较。
0x0000000000400f71 :mov 0x8(%rsp),%eax #把第一个数字的值给%eax
0x0000000000400f75 :jmpq *0x402470(,%rax,8) #这是属于跳转表的形式,
0x0000000000400f7c :mov $0xcf,%eax #以下就是把某一个值放到%eax中在做的过程,就是switch-case语句
0x0000000000400f81 :jmp 0x400fbe
0x0000000000400f83 :mov $0x2c3,%eax
0x0000000000400f88 :jmp 0x400fbe
0x0000000000400f8a :mov $0x100,%eax
0x0000000000400f8f :jmp 0x400fbe
0x0000000000400f91 :mov $0x185,%eax
0x0000000000400f96 :jmp 0x400fbe
0x0000000000400f98 :mov $0xce,%eax
0x0000000000400f9d :jmp 0x400fbe
0x0000000000400f9f :mov $0x2aa,%eax
0x0000000000400fa4 :jmp 0x400fbe
0x0000000000400fa6 :mov $0x147,%eax
0x0000000000400fab :jmp 0x400fbe
0x0000000000400fad :callq 0x40143a
0x0000000000400fb2 :mov $0x0,%eax
0x0000000000400fb7 :jmp 0x400fbe
0x0000000000400fb9 :mov $0x137,%eax
0x0000000000400fbe :cmp 0xc(%rsp),%eax #都是在拿rsp+12的地址对应的值与eax进行比较
0x0000000000400fc2 :je 0x400fc9 #若等就会结束,成功;不等,就会炸
0x0000000000400fc4 :callq 0x40143a
0x0000000000400fc9 :add $0x18,%rsp
0x0000000000400fcd :retq
sscanf语句
在该汇编语言中,使用了sscanf格式化输入。sscanf的语句同read_six_number函数类似,只是具有了更灵活的形式。参数有需要规定的输入形式 ,本语句中以%esi参数寄存器传入。通过该参数,函数可以将输入的合法字符串转换为规定的数字或者字符串。
使用x/s 命令查看,得知是“%d %d”,即需要输入两个整数。
而另外两个参数,分别是栈的+12地址,栈的+8地址。这两个参数用作保存转换的数字。返回值是输入成功的值的个数。这里是两个%d,所以若正常按格式输入两个数字,返回值应大于1。据得,若不大于1,则炸弹爆炸。
swith-case语句
接着是一个典型的swith-case语句。首先在中,将第一个数字与7进行无符号的小于比较。这是在规定输入的第一个数字必须是0-6之间的第一个数(包含0,6)。然后,是一个经典的跳转表形式。
通过语句,第一个数字成为了跳转表的参数。等语句,分别对应的输入第一个数字为0-6情况的不同跳转。
在跳转后,将某个值(每个跳转对应的值均不同)存入%eax寄存器中。接着统一跳转至,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 :sub $0x18,%rsp
0x0000000000401010 :lea 0xc(%rsp),%rcx //要用的参数,放入参数寄存器中给scanf存
0x0000000000401015 :lea 0x8(%rsp),%rdx //要用的参数
0x000000000040101a :mov $0x4025cf,%esi //这个也是“%d %d”
0x000000000040101f :mov $0x0,%eax
0x0000000000401024 :callq 0x400bf0 <__isoc99_sscanf>
0x0000000000401029 :cmp $0x2,%eax //不是正常的两个参数就炸
0x000000000040102c :jne 0x401035
0x000000000040102e :cmpl $0xe,0x8(%rsp) //这个值和14
0x0000000000401033 :jbe 0x40103a //低于或者相等
0x0000000000401035 :callq 0x40143a
0x000000000040103a :mov $0xe,%edx
0x000000000040103f :mov $0x0,%esi
0x0000000000401044 :mov 0x8(%rsp),%edi
0x0000000000401048 :callq 0x400fce
0x000000000040104d :test %eax,%eax //返回0才是正确做法
0x000000000040104f :jne 0x401058
0x0000000000401051 :cmpl $0x0,0xc(%rsp) //再比较第二个输入值和0的关系
0x0000000000401056 :je 0x40105d //需等于0,否则炸
0x0000000000401058 :callq 0x40143a
0x000000000040105d :add $0x18,%rsp
0x0000000000401061 :retq
该函数简单明了,同Phase3,同样使用了一个sscanf语句,同样是"%d %d"格式输入。因此,密钥仍为两个数字。其次,0x8(%rsp)作为第一个数字,应该满足语句,即低于或者小于14。最后,func4的返回值必须是0,而func4的参数在-中给出。
于是,我们通过disas 命令获取func4的汇编代码。(这里不再示例gdb的使用)
0x0000000000400fce :sub $0x8,%rsp
0x0000000000400fd2 :mov %edx,%eax # result=14
0x0000000000400fd4 :sub %esi,%eax # result-=0,不变
0x0000000000400fd6 :mov %eax,%ecx
0x0000000000400fd8 :shr $0x1f,%ecx # %ecx逻辑右移31位,补0,取最高位之意
0x0000000000400fdb :add %ecx,%eax # 拿自己的最高位加上result;(负数加1正数加0)14+0=0
0x0000000000400fdd :sar %eax # 算术右移,单操作数是只移动一位的意思 7
0x0000000000400fdf :lea (%rax,%rsi,1),%ecx # 7+0=%ecx
0x0000000000400fe2 :cmp %edi,%ecx # 比较第一个输入值和%ecx的关系
0x0000000000400fe4 :jle 0x400ff2
0x0000000000400fe6 :lea -0x1(%rcx),%edx
0x0000000000400fe9 :callq 0x400fce
0x0000000000400fee :add %eax,%eax
0x0000000000400ff0 :jmp 0x401007
0x0000000000400ff2 :mov $0x0,%eax # 给出0
0x0000000000400ff7 :cmp %edi,%ecx # 再比较一次
0x0000000000400ff9 :jge 0x401007 # 大于等于
0x0000000000400ffb :lea 0x1(%rcx),%esi
0x0000000000400ffe :callq 0x400fce
0x0000000000401003 :lea 0x1(%rax,%rax,1),%eax
0x0000000000401007 :add $0x8,%rsp
0x000000000040100b :retq
根据注释,我们可以得出:当返回值为0时,第一个数字需为7。 再回到函数phase4中来,我们看到有语句,该语句规定了第二个数字需为0这一输入 。
通关密钥
Phase5
实验探究
0x0000000000401062 :push %rbx
0x0000000000401063 :sub $0x20,%rsp //开辟一个32字节的空间
0x0000000000401067 :mov %rdi,%rbx //rdi是输入字符串数组地址
0x000000000040106a :mov %fs:0x28,%rax // 栈溢出保护
0x0000000000401073 :mov %rax,0x18(%rsp) //把返回值存储到栈临时内存中
0x0000000000401078 :xor %eax,%eax //异或自己,置零
0x000000000040107a :callq 0x40131b
0x000000000040107f :cmp $0x6,%eax //字符串长度与6比较
0x0000000000401082 :je 0x4010d2 //等于的话跳转,否则炸
0x0000000000401084 :callq 0x40143a
0x0000000000401089 :jmp 0x4010d2
0x000000000040108b :movzbl (%rbx,%rax,1),%ecx //将字符依次赋值
0x000000000040108f :mov %cl,(%rsp) //%rcx的最低字节(依次的元素的值)给栈顶内存存储
0x0000000000401092 :mov (%rsp),%rdx //将这个字符赋值给%rdx
0x0000000000401096 :and $0xf,%edx //使得%edx高位的值被0覆盖掉,只剩0-15
0x0000000000401099 :movzbl 0x4024b0(%rdx),%edx // 将(0x4024b0+%rdx)对应内存的值给了%edx
0x00000000004010a0 :mov %dl,0x10(%rsp,%rax,1) //再将%edx的低位保存在栈中
0x00000000004010a4 :add $0x1,%rax
0x00000000004010a8 :cmp $0x6,%rax
0x00000000004010ac :jne 0x40108b //似乎是一个循环
0x00000000004010ae :movb $0x0,0x16(%rsp) //把0的值改写到这个字符串对应的结尾字符串,所以最后有'