程序员学完深入理解计算机系统,深入理解计算机系统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

05adfe5eb2b959260cbb66e8bc525c45.png

生成汇编代码

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

a7826eec0603af81dc7ff8131b68757a.png

进入gdb调试模式

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

1d542dbd589725b8e30c49e553ea180b.png

获取主要函数的汇编代码

首先,我们打开已经生成好的汇编语言文件(可以在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的汇编代码。

52d20ed0f20766dd4d8c4391a8a3bcb7.png

重复上述步骤,依次快速复制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中存储的字符串导出。答案如我们所愿。

730f3dcf0e74b726d08750430f9634ea.png

因此,phase1函数的作用只是单纯的让我们输入一个字符串。再将我们输入的字符串和存储的字符串进行比较而已。

通关密钥

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

使用run命令执行bomb.c程序,再输入第一关密钥

c158f8c1e7607c870eeff21603e19b62.png

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的等比数列。

通关密钥

0f8598309367f54d2b5ecade21fe0628.png

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”,即需要输入两个整数。

7fb84d59acbfa18a6b30fd4ed9265381.png

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

swith-case语句

接着是一个典型的swith-case语句。首先在中,将第一个数字与7进行无符号的小于比较。这是在规定输入的第一个数字必须是0-6之间的第一个数(包含0,6)。然后,是一个经典的跳转表形式。

b9deefa3616b2dd04c631351a640ef68.png

通过语句,第一个数字成为了跳转表的参数。等语句,分别对应的输入第一个数字为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为例)

3b04dad9d94ec3a65573e036b928740e.png

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这一输入 。

通关密钥

aa25931b9ffb87817854e5ba619ca9d9.png

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的值改写到这个字符串对应的结尾字符串,所以最后有'

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值