最近我在学计算机系统时,做到了一个蛮有趣的实验游戏——bomb实验(其实就是一个c程序)。这个实验有六关,每一关需要输入一个字符串(可以称之为密钥),每一关只有输入正确的密钥才能通过,否则“炸弹“将会爆炸。因此,我们需要通过汇编c代码找出汇编文件中藏有的密钥的信息。通过这个实验,我的汇编语言能力获得了极好的锻炼,因此纪录过程以供大家分享并作为纪念。
首先,据实验的要求,我们需要一台linux机器进行gdb调试。我们可以使用linux虚拟机或者租一台linux云主机,在这里我使用的是腾讯云的云主机。在这台主机上,我新建一个文件夹,并将实验所需要的bomb.c文件进行上传。
使用工具:xftp
在上传完成后,我们打开已连接上服务器的xshell或者虚拟机的teminal应用,使用cd命令进入到刚才上传到的文件夹中。之后,我们使用objdump -d bomb > bomb_assembly.S命令生成一个名为bomb_assembly.S的汇编代码文件
使用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的代码文件
关于每一步的解释以# 的注释标在代码后
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被
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
于是,我们可以继续猜想,在调用
0x0000000000400ee4 <+4>: mov $0x402400,%esi # 将0x402400的值作为的参数传入
使用,x/s命令将0x402400中存储的字符串导出。答案如我们所愿。
因此,phase1函数的作用只是单纯的让我们输入一个字符串。再将我们输入的字符串和存储的字符串进行比较而已。
密钥即为“Border relations with Canada have never been better.”,输入即可通关。
打开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的等比数列。
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的语句同read_six_number函数类似,只是具有了更灵活的形式。参数有需要规定的输入形式 ,本语句中以%esi参数寄存器传入。通过该参数,函数可以将输入的合法字符串转换为规定的数字或者字符串。
使用x/s 命令查看,得知是“%d %d”,即需要输入两个整数。
而另外两个参数,分别是栈的+12地址,栈的+8地址。这两个参数用作保存转换的数字。返回值是输入成功的值的个数。这里是两个%d,所以若正常按格式输入两个数字,返回值应大于1。据<+32>得,若不大于1,则炸弹爆炸。
接着是一个典型的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,需输入不同数字。
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这一输入 。
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>语句,
之后通过一系列的操作:%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码得输入。
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)
因此,不同的值对应的不同结果如下。栈中最后会按照原来对应的数字来保存不同的地址
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
于是,我们尝试使用gdb进行端点的调试,查看0x603870处的这个字符串的值。我们在phase6的最后一条语句中设置断点。
接着,我们开始按照刚刚的答案一步步运行程序,到了断点处,程序将自动停止。
接着,我们使用x/s命令查看0x603870处字符串的值
我们可以通过之前6关输入的字符串得知,**第4关的字符串就是触发隐藏关卡的入口。**但是,在第四关处,我们还应该输入某个字符串在"7 0"之后。于是,我们开始对这个字符串进行寻找。
我们发现,<+74>语句对两个字符串进行了比较。而其中一个字符串就是sscanf输入地址参数所指向的字符串。因此,我们了解到需要将与其作比较字符串作为输入。
使用x/s命令查看,我们知道可以通过在第4关输入"7 0 DrEvil"作为通关密钥的同时,开启隐藏关卡。
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
因此,关键在于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 杨政 (转载请声明)