前言:
本人(TJUer)之前做拆弹实验的时候在网上搜了很久,也没有找到十分详细的汇编语句解析的文章,看了感觉似懂非懂,于是在这篇文章中,我几乎将每一句有用的汇编语句都做了注释,并且配图,希望能对做这个拆弹实验学弟学妹们有所帮助。
《手把手教你拆解 CSAPP 的 炸弹实验室 BombLab》是一个很好的文章(我当时就跟着他做的),涵盖的面很广。
(本文前置知识包括GDB调试工具的使用)
每个phase中,内容布局:图片->汇编语句分析->综述
下面就是实验报告了
实验3:拆弹专家
进⼀步掌握程序的机器级表示⼀章的知识。理解程序控制、过程调⽤的汇编级实现,熟练掌握 汇编语⾔程序的阅读。
程序bomb是⼀个电⼦炸弹,当该程序运⾏时,需要按照⼀定的顺序输⼊⼝令,才能阻 ⽌炸弹的引爆。当输⼊错误的密码时,炸弹将会引爆。此时控制台将会产⽣如下输出,并结束 程序
在炸弹程序中,你需要输⼊多组⼝令,且每⼀组⼝令都正确才能够防⽌引爆。
⽬前已知的内容只有炸弹程序的⼆进制可执⾏⽂件bomb(⽬标平台为:x86-64)和 bomb的main函数框架代码,⻅main.c。其他的细节均不会以c语⾔的⽅式呈现。 你的任务是:利⽤现有的资源以及相关的⼯具,猜出炸弹的全部⼝令,并输⼊⾄炸弹程序中,以完成最终的拆弹⼯作。
1)在Unbuntu18.04LTS操作系统下,按照实验指导说明书,使用gdb和objdump等工具,以反向工程方式完成Bomb拆弹。
2)需提交:拆弹口令文本文件、电子版实验报告全文。
(拆弹phase1到phase6的拆弹分析过程,可以包括典型的汇编码片段、拆弹思路、拆弹过程中的结果截图。可选项:隐藏卡的破解过程分析)
Phase 1
先分析,再综述。
图一
图二
图三:
Phase 1主要用到了一下三个函数
0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp栈帧
400ee4: be 00 24 40 00 mov $0x402400,%esi这是将某数据的地址放进来
我们可以使用x/s 加上这个地址的方式直接访问这段地址中的内容,得到的是:Border relations with Canada have never been better.这一字符串,见图一
并且可以看出这个地址被放在了%rsi中
400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal>调用函数
400eee: 85 c0 test %eax,%eax
400ef0: 74 05 je 400ef7 <phase_1+0x17>从这里可以看出string_not_equal函数必须返回0才可以,否则会爆炸
400ef2: e8 43 05 00 00 callq 40143a <explode_bomb>
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 retq
0000000000401338 <strings_not_equal>:此时%rsi中是传入的一个地址,放的是一个字符串,%rdi可以通过测试得知,存放的是我们输入的字符串的地址,见图二
401338: 41 54 push %r12这是保护寄存器的数据
40133a: 55 push %rbp保护
40133b: 53 push %rbx
40133c: 48 89 fb mov %rdi,%rbx
40133f: 48 89 f5 mov %rsi,%rbp
401342: e8 d4 ff ff ff callq 40131b <string_length>调用length函数
401347: 41 89 c4 mov %eax,%r12d这是将刚刚测得的rdi中指向的string(我自己输入的string)的长度存到%r12d中
40134a: 48 89 ef mov %rbp,%rdi 这是完成了rdi 和rsi 的交换,为了使用同一个测长度的函数来测量用来匹配的string
40134d: e8 c9 ff ff ff callq 40131b <string_length>
401352: ba 01 00 00 00 mov $0x1,%edx
401357: 41 39 c4 cmp %eax,%r12d这是将测得的匹配string长度和我自己输入的string的长度进行比较,
40135a: 75 3f jne 40139b <strings_not_equal+0x63>如果两个string的长度不一样,就会爆炸
40135c: 0f b6 03 movzbl (%rbx),%eax将此时的string的第一个字符放进rax
40135f: 84 c0 test %al,%al al 就是rax(8 bit)看看是不是结尾
401361: 74 25 je 401388 <strings_not_equal+0x50>如果是结尾,再加上长度相同,那就表示两个string一样,返回0,如果不是结尾,才进入到下方的一个一个测试的循环中
401363: 3a 45 00 cmp 0x0(%rbp),%al 把待匹配的两个string的首位先进行比较,rbp中是他的,al 中是我的string
401366: 74 0a je 401372 <strings_not_equal+0x3a>相同则跳转,继续
401368: eb 25 jmp 40138f <strings_not_equal+0x57>不同就返回1,会引起爆炸
40136a: 3a 45 00 cmp 0x0(%rbp),%al这是循环的起点
40136d: 0f 1f 00 nopl (%rax)
401370: 75 24 jne 401396 <strings_not_equal+0x5e>
401372: 48 83 c3 01 add $0x1,%rbx
401376: 48 83 c5 01 add $0x1,%rbp rbx和rbp分别是指向对方和我方字符串的指针,一开始指向首位,每执行一次这两个语句,会同时向后移动一个字符
40137a: 0f b6 03 movzbl (%rbx),%eax 跟上面一样,进行比较
40137d: 84 c0 test %al,%al 比,看看是不是到结尾了
40137f: 75 e9 jne 40136a <strings_not_equal+0x32> 如果不是结尾,那就重复这个循环
401381: ba 00 00 00 00 mov $0x0,%edx 到结尾了,若中途没有失配,那就返回0,表示匹配成功
401386: eb 13 jmp 40139b <strings_not_equal+0x63>返回0
401388: ba 00 00 00 00 mov $0x0,%edx
40138d: eb 0c jmp 40139b <strings_not_equal+0x63>返回0
40138f: ba 01 00 00 00 mov $0x1,%edx
401394: eb 05 jmp 40139b <strings_not_equal+0x63>返1(爆)
401396: ba 01 00 00 00 mov $0x1,%edx
40139b: 89 d0 mov %edx,%eax
40139d: 5b pop %rbx
40139e: 5d pop %rbp
40139f: 41 5c pop %r12
4013a1: c3 retq
000000000040131b <string_length>可知测的是%rdi中的地址指向的字符串的长度
40131b: 80 3f 00 cmpb $0x0,(%rdi) 看当前rdi指向的内存中的第一个值是不是‘\0’,如果是 那就表示长度是零,所以对应接下来的返回值是0的操作,(这是进入循环前先判断了一下)
40131e: 74 12 je 401332 <string_length+0x17>
401320: 48 89 fa mov %rdi,%rdx到这里说明长度>0
401323: 48 83 c2 01 add $0x1,%rdx
401327: 89 d0 mov %edx,%eax
401329: 29 f8 sub %edi,%eax//上面三个语句是把rdx这个指针向后移动了一个,并且使得rax变成了1,这是一个循环,每一次循环都会让rax的值增加1
40132b: 80 3a 00 cmpb $0x0,(%rdx)这句和下面一句是循环的结束条件,当读取到string末尾的反斜杠0时,就停止,然后会返回string的长度
40132e: 75 f3 jne 401323 <string_length+0x8>
401330: f3 c3 repz retq
401332: b8 00 00 00 00 mov $0x0,%eax返回0
401337: c3 retq
分析一遍可以知道,phase1就是把一个准备好的string和我输入的string进行比较,如果两者一致,那就不会爆炸。所以我们将刚刚读取到的0x402400中的那个string拿出来,输入,然后就成功了。(见图三)
Phase 2
图一:
图二:
图三:
图四:
图五:
图六:
0000000000400efc <phase_2>:
400efc: 55 push %rbp
400efd: 53 push %rbx
400efe: 48 83 ec 28 sub $0x28,%rsp 保护和开辟栈帧
400f02: 48 89 e6 mov %rsp,%rsi把这时的栈顶给rsi
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers> 调用函数,输入六个数
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp)此时的rsp是读入的第一个数,()原因在后面已经说了)这条语句和下面一条表示的是,第一个数必须是1,否则爆炸
400f0e: 74 20 je 400f30 <phase_2+0x34>
400f10: e8 25 05 00 00 callq 40143a <explode_bomb>
400f15: eb 19 jmp 400f30 <phase_2+0x34>
400f17: 8b 43 fc mov -0x4(%rbx),%eax 这是循环的开始
这里是将rbx-4(因为刚刚加四了)的位置的值放进rax,注意此时rbx没有变化
400f1a: 01 c0 add %eax,%eax 变成二倍
400f1c: 39 03 cmp %eax,(%rbx) 比较,相同则不爆炸
400f1e: 74 05 je 400f25 <phase_2+0x29>
400f20: e8 15 05 00 00 callq 40143a <explode_bomb>
400f25: 48 83 c3 04 add $0x4,%rbx
400f29: 48 39 eb cmp %rbp,%rbx
400f2c: 75 e9 jne 400f17 <phase_2+0x1b>
400f2e: eb 0c jmp 400f3c <phase_2+0x40>
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx把rsp的地址加上4后给rbx,也就是后移了一个数,或者说每运行到这里rbx都会加四
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp
400f3a: eb db jmp 400f17 <phase_2+0x1b>
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 retq 返回
000000000040145c <read_six_numbers>:
40145c: 48 83 ec 18 sub $0x18,%rsp 开辟栈帧
401460: 48 89 f2 mov %rsi,%rdx
401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx
401467: 48 8d 46 14 lea 0x14(%rsi),%rax
40146b: 48 89 44 24 08 mov %rax,0x8(%rsp)
401470: 48 8d 46 10 lea 0x10(%rsi),%rax
401474: 48 89 04 24 mov %rax,(%rsp)
401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9
40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8 一系列对刚刚开辟的栈帧的地址计算和存储
401480: be c3 25 40 00 mov $0x4025c3,%esi 注意这个地址,读取一下是“d d d d d d”可以知道传入的是六个int类型的数据,见图一。
401485: b8 00 00 00 00 mov $0x0,%eax
40148a: e8 61 f7 ff ff callq 400bf0 <__isoc99_sscanf@plt>
40148f: 83 f8 05 cmp $0x5,%eax
401492: 7f 05 jg 401499 <read_six_numbers+0x3d> scanf函数会返回读取的数据的个数,放到rax里边(图二),这条跳转语句的意思是,必须读取大于5个数,否则爆炸
401494: e8 a1 ff ff ff callq 40143a <explode_bomb>
401499: 48 83 c4 18 add $0x18,%rsp
40149d: c3 retq
这个函数把刚刚读入的六个数从下向上放在栈帧中,可以通过读取栈顶的数据来验证,栈顶地址加四后就是第二个数,见图二,图三
这个phase是考循环的,输入六个数,第一个数强制要求是1,并且每次倍增,自然而然,得出这六个数是1 2 4 8 16 32,输入验证见图六
Phase 3
图一:
图二:
图三:
图四:
图五:
图六:
0000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp 分配栈帧
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
400f51: be cf 25 40 00 mov $0x4025cf,%esi 在这里有一个地址0x4025cf 我用x命令读取这个内存 ,得到“%d %d”,可见这是输入了两个数字(图一)
400f56: b8 00 00 00 00 mov $0x0,%eax 让返回值是0
400f5b: e8 90 fc ff ff callq 400bf0 <__isoc99_sscanf@plt>
400f60: 83 f8 01 cmp $0x1,%eax 返回了输入的数字的个数,和1比较,根据下面的一个指令,可以看出,如果返回值大于一,则会跳转继续,否则爆炸,说明我们至少输入两个数
400f63: 7f 05 jg 400f6a <phase_3+0x27>
400f65: e8 d0 04 00 00 callq 40143a <explode_bomb>
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp)
上面这一句指令是将%rsp中的地址+8处的值和7比较,如果大于7,就会爆炸。
根据测试发现在这个%rsp值+8地址处的值就是我输入的第一个数(图二)
在图二中,我输入的第一个数是12321,图中可见rsp中的值是0x7fffffffdfe0,于是我访问0x7fffffffdfe8,使用十进制输出,输出的正好是我输入的第一个值。
400f6f: 77 3c ja 400fad <phase_3+0x6a> 大了就会爆炸,所以我们输入的第一个值必须<=7,我们以输入0为例
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax 把第一个输入的数放进%rax
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8)
这条语句是无条件跳转到*(0x402470+8*%rax),即跳转到0x402470+8*%rax这个地址中存放的那个地址。这个跳转方式和跳转表很符合。
我们打印一下在0x402470处的跳转表(图三 ),可见其中的前四行是我们要找的跳转表,
以第一个0x00400f7c为例,这一个地址值占了4B,而刚刚的跳转语句是每八个字节为一个单位,正好和这个跳转表是对应的。
以我们的输入的第一个数字是0为例,这时候我们会跳转到0x402470+8*0处存放的地址值,观察跳转表可以知道,这个位置是0x00400f7c
使用si指令,单步执行,可以观察到指令进行到了0x00400f7c处(图四)
然后我们直接从这个位置继续看
400f7c: b8 cf 00 00 00 mov $0xcf,%eax(刚刚跳转到了这里)把0xcf(207)放到了%rax里面
400f81: eb 3b jmp 400fbe <phase_3+0x7b>
400f83: b8 c3 02 00 00 mov $0x2c3,%eax
400f88: eb 34 jmp 400fbe <phase_3+0x7b>
400f8a: b8 00 01 00 00 mov $0x100,%eax
400f8f: eb 2d jmp 400fbe <phase_3+0x7b>
400f91: b8 85 01 00 00 mov $0x185,%eax
400f96: eb 26 jmp 400fbe <phase_3+0x7b>
400f98: b8 ce 00 00 00 mov $0xce,%eax
400f9d: eb 1f jmp 400fbe <phase_3+0x7b>
400f9f: b8 aa 02 00 00 mov $0x2aa,%eax
400fa4: eb 18 jmp 400fbe <phase_3+0x7b>
400fa6: b8 47 01 00 00 mov $0x147,%eax
400fab: eb 11 jmp 400fbe <phase_3+0x7b>
400fad: e8 88 04 00 00 callq 40143a <explode_bomb>
400fb2: b8 00 00 00 00 mov $0x0,%eax
400fb7: eb 05 jmp 400fbe <phase_3+0x7b>
400fb9: b8 37 01 00 00 mov $0x137,%eax
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax
将%rsp+0xc处的值和刚刚放进%rax中的207比较,如果相同就不会爆炸,猜测%rsp中放的就是我们输入的第二个数字,为了证明,我们输入第二个数是333,访问这个地址的值,也是333(图五)。
400fc2: 74 05 je 400fc9 <phase_3+0x86> 相等就不会炸
400fc4: e8 71 04 00 00 callq 40143a <explode_bomb> 爆炸
400fc9: 48 83 c4 18 add $0x18,%rsp 回收栈帧
400fcd: c3 retq 返回
根据上面的分析可以知道,这是一个Switch语句类似的跳转表的结构,输入的第一个数字决定了后面跳到哪里,也就决定了第二个数字是多少。第一个数字可以是0~7的任意一个数字,我们可以选0,对应的是0xcf (207),输入这两个数就可以不引爆炸弹。(图六)
综上,此题多解,而且只要前两个数是对的,再多输入几个数也不影响,比如可以输0 207 2
Phase 4
图一:
图二:
图三:
图四:
000000000040100c <phase_4>:
40100c: 48 83 ec 18 sub $0x18,%rsp 开辟栈帧
401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx 存放rsp+0xc的值(存第二个输入的数)
401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx 存放rsp+0x8的值(存第一个输入的数)
40101a: be cf 25 40 00 mov $0x4025cf,%esi 这个地址用x/s读内存得到“%d %d ”可见是输入两个数字(图一)
40101f: b8 00 00 00 00 mov $0x0,%eax 把0放进%rax
401024: e8 c7 fb ff ff callq 400bf0 <__isoc99_sscanf@plt>
401029: 83 f8 02 cmp $0x2,%eax 和上一个题略有不同,这道题必须输入两个数,多一个数也不行
40102c: 75 07 jne 401035 <phase_4+0x29>
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp) %rsp+8放的是第一个输入的数字,根据这条指令和下面一个指令,可知第一个数必须小于等于0xe(十进制的14)
401033: 76 05 jbe 40103a <phase_4+0x2e>
401035: e8 00 04 00 00 callq 40143a <explode_bomb>
40103a: ba 0e 00 00 00 mov $0xe,%edx把14放进%edx
40103f: be 00 00 00 00 mov $0x0,%esi 把0放%rsi
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi 把第一个输入的数放到%rdi
401048: e8 81 ff ff ff callq 400fce <func4> 开始递归
40104d: 85 c0 test %eax,%eax 可知这个递归函数的返回值必须是0才可以
40104f: 75 07 jne 401058 <phase_4+0x4c>
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp) 可知第二个输入的数必须是0才不爆炸
401056: 74 05 je 40105d <phase_4+0x51>
401058: e8 dd 03 00 00 callq 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp 回收栈帧
401061: c3 retq 返回
0000000000400fce <func4>: 这是一个递归
400fce: 48 83 ec 08 sub $0x8,%rsp 分配栈帧
400fd2: 89 d0 mov %edx,%eax
400fd4: 29 f0 sub %esi,%eax
400fd6: 89 c1 mov %eax,%ecx
400fd8: c1 e9 1f shr $0x1f,%ecx 取符号位
400fdb: 01 c8 add %ecx,%eax
400fdd: d1 f8 sar %eax 算数右移1bit,相当于除以二
400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx
400fe2: 39 f9 cmp %edi,%ecx 第一个分支点
400fe4: 7e 0c jle 400ff2 <func4+0x24>
400fe6: 8d 51 ff lea -0x1(%rcx),%edx
400fe9: e8 e0 ff ff ff callq 400fce <func4>
400fee: 01 c0 add %eax,%eax
400ff0: eb 15 jmp 401007 <func4+0x39>
400ff2: b8 00 00 00 00 mov $0x0,%eax 赋返回值为0
400ff7: 39 f9 cmp %edi,%ecx
400ff9: 7d 0c jge 401007 <func4+0x39>
400ffb: 8d 71 01 lea 0x1(%rcx),%esi
400ffe: e8 cb ff ff ff callq 400fce <func4>
401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax rax变成1了(图四)
只有在输入的第一个值是大于7小于等于14时会在第一层调用中由于这个原因返回1导致爆炸。
401007: 48 83 c4 08 add $0x8,%rsp 回收栈帧
40100b: c3 retq
用图表示递归的结构是
其中,可以最快走出递归且让返回值为 0的一条道路可以是一直走图中最左侧的道路,即第一次调用就满足%ecx<=%edi且%ecx>=%edi也就是%ecx=%edi,追踪寄存器可以知道此时的%ecx=%edi=7(其实这是由于调用函数之前放里边的14经过算数右移1位得到的),当func4运行到结尾时,rax中的值还是0,满足条件。
此题是多解的,若不使用第一个数是7的方法,继续分析,从左向右数第二条路中,会继续递归func4函数且将返回值赋值为1,故此路不通。
其他情况下,只有走最右侧道路是合理的,即第一个传入的数(此时在%rdi里边)必须满足小于7,又因为函数递归中的 shr $0x1f,%ecx这一条指令是取符号位,当负数的时候就和正数的时候不同,所以我们可以在0~6这七个数中直接试正确性。经试验,得知6, 3, 1, 0 等都可被接受。
故,其中一种可行的答案是7 0(图三)
Phase 5
图一:
图二:
图三:
图四:
0000000000401062 <phase_5>:
401062: 53 push %rbx 调用者保护
401063: 48 83 ec 20 sub $0x20,%rsp 开辟栈帧
401067: 48 89 fb mov %rdi,%rbx
40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 金丝雀值传到%rax里
401071: 00 00
401073: 48 89 44 24 18 mov %rax,0x18(%rsp) 将金丝雀放在栈帧所在位置上面18字节的地方
401078: 31 c0 xor %eax,%eax 将rax全清零
40107a: e8 9c 02 00 00 callq 40131b <string_length> 测string的长度
40107f: 83 f8 06 cmp $0x6,%eax 长度是6才不会爆炸,跳转
401082: 74 4e je 4010d2 <phase_5+0x70>
401084: e8 b1 03 00 00 callq 40143a <explode_bomb>
401089: eb 47 jmp 4010d2 <phase_5+0x70>
40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx 这是循环的开始
注意,%rbx里存放的是我输入的六个字符长度string的地址,证:我输入的string是Yonefg,使用x指令访问%rbx中的地址值也是Yonefg,见图一
Movzbl是移动一个字节长度的内容到两个字节的位置,以无符号的方式。
上面这一条语句的含义是将我输入的string的第rax位置的一个字符(因为是读一个字节)放进rcx中,放到低三十二位。
40108f: 88 0c 24 mov %cl,(%rsp)
这条指令是将%rcx的低八位(一个字节)放到栈顶位置
401092: 48 8b 14 24 mov (%rsp),%rdx
将刚刚放到栈顶位置的数据拿出来放进%rdx
401096: 83 e2 0f and $0xf,%edx
And 是与操作,目的是将%rdx的高60bit全赋值为0,仅剩低4bit是有效的值。
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx
这条语句是将0x4024b0这个地址加上%rdx中存储的偏移量后,取出一个字节的内容,放进%rdx中,覆盖掉之前的数据。
我们不防读取一下这个地址的内容,得到的是"maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1)
这是将刚刚拿出来的%rdx中的数据的低32bit放到栈顶+10字节+%rax的偏移量的位置(这个循环每循环一次,%rax的值加一)
4010a4: 48 83 c0 01 add $0x1,%rax %rax自增一
4010a8: 48 83 f8 06 cmp $0x6,%rax 这是上面这个循环的结束条件,当已经循环六次,退出循环
4010ac: 75 dd jne 40108b <phase_5+0x29> 没有足够六次,去往循环开始的位置
4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp) 这时已经退出了循环,从栈顶+10的位置一直到栈顶+15的位置,可以看做是六个char型的数据,上面这一条指令是在string的末尾加’\0’表示结尾
4010b3: be 5e 24 40 00 mov $0x40245e,%esi 注意这个地址,是用来匹配的string,是目标,打印这个地址存储的string是“flyers”(图二)
4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi 将刚刚的string的开头的地址放进%rdi,作为参数传入下方的string_not_equal中,与目标string相匹配
4010bd: e8 76 02 00 00 callq 401338 <strings_not_equal>
4010c2: 85 c0 test %eax,%eax 返回值是0时说明是匹配的 不会炸
4010c4: 74 13 je 4010d9 <phase_5+0x77>
4010c6: e8 6f 03 00 00 callq 40143a <explode_bomb>
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
4010d0: eb 07 jmp 4010d9 <phase_5+0x77>
4010d2: b8 00 00 00 00 mov $0x0,%eax 将rax清零
4010d7: eb b2 jmp 40108b <phase_5+0x29>
4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax 这是开头部分放置的金丝雀值,将其放进%rax中等待检测
4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax 做异或操作,相等时结果是零,才不会爆炸,若不是零,说明栈被破坏
4010e5: 00 00
4010e7: 74 05 je 4010ee <phase_5+0x8c>
4010e9: e8 42 fa ff ff callq 400b30 <__stack_chk_fail@plt> 终止
4010ee: 48 83 c4 20 add $0x20,%rsp 回收栈帧
4010f2: 5b pop %rbx 恢复
4010f3: c3 retq 返回
看完一遍上面的汇编指令可以知道,这个函数的原理是根据我输入的字符串的每一个char的ASCII码的最后4bit,将这4bit看做一个数,作为已给的string的下标,挑出这个string中合适位置的字符,拼凑成为一个新的string,使其和带匹配的string相同。
已知待匹配字符串是“flyers”
原料字符串是“maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?”
想要拼出flyer可以分别取原料字符串的第(十六进制表示)9 f e 5 6 7个位置的字符,
即用4bit表示这六个数是 1001;1111;1110;0101;0110;01111;
查询ASCII码表可以得到后4bit对应的六个字符(不唯一)
1001->i I y Y 9 )
1111->? / _ o O
1110->> n N .
0101->u U e E %
0110->f F v V &
0111->’ 7 g G w W
上面字符任意组合即可,以“)?>%&’”为例,输入后可见正确。(图四)
其他答案例如ionUfG 9oN%V7 y_.E&’都是可以的
Phase 6 链表,栈
图一:
图二:
图三:
图四(该图片来源于https://zhuanlan.zhihu.com/p/451623574):
图五:
图六:
图七:
00000000004010f4 <phase_6>:
4010f4: 41 56 push %r14
4010f6: 41 55 push %r13
4010f8: 41 54 push %r12
4010fa: 55 push %rbp
4010fb: 53 push %rbx 寄存器数据保护
4010fc: 48 83 ec 50 sub $0x50,%rsp 开辟50字节的栈帧
401100: 49 89 e5 mov %rsp,%r13 %r13存放栈顶 地址
401103: 48 89 e6 mov %rsp,%rsi 栈顶地址做第二个参数
401106: e8 51 03 00 00 callq 40145c <read_six_numbers>
读入六个数字,这六个数字从栈顶向上排列(从低到高),我们可以通过依次读取%rsp+0,%rsp+4,%rsp+8,%rsp+c,%rsp+0x10,%rsp+0x14来验证。(图一中我输入的前三个数是4 3 2)
40110b: 49 89 e6 mov %rsp,%r14 栈顶
40110e: 41 bc 00 00 00 00 mov $0x0,%r12d %r12d置零
下面一部分是一个嵌套循环
401114: 4c 89 ed mov %r13,%rbp 栈顶 外层循环(保证大于零小于六)
:每循环一次,%r13d会向后移动一个数,这是外层循环中变化的下标,遍历六个数
401117: 41 8b 45 00 mov 0x0(%r13),%eax 将第一个数放进%rax
40111b: 83 e8 01 sub $0x1,%eax 第一个数减一
40111e: 83 f8 05 cmp $0x5,%eax 然后和5比较
401121: 76 05 jbe 401128 <phase_6+0x34> (无符号数)小于等于五就不会爆炸,即传入的第一个数<=6且不能是比1小的数,即1~6
401123: e8 12 03 00 00 callq 40143a <explode_bomb>
401128: 41 83 c4 01 add $0x1,%r12d
40112c: 41 83 fc 06 cmp $0x6,%r12d
401130: 74 21 je 401153 <phase_6+0x5f>这是外层循环结束判定条件,棕色的%r12d是计数器,当加到6的时候会结束循环
401132: 44 89 e3 mov %r12d,%ebx
401135: 48 63 c3 movslq %ebx,%rax内循环(查重作用),其中%rbx是循环的计数器,每循环一次加一
401138: 8b 04 84 mov (%rsp,%rax,4),%eax 当前是循环的第几轮,就把栈中的第几个数(输入的第几个数)放进%rax中,或者这是根据%ebx选择第几个数,然后比较
40113b: 39 45 00 cmp %eax,0x0(%rbp) 并将这个数和栈顶比较,相等就会爆炸,不等就跳转继续
40113e: 75 05 jne 401145 <phase_6+0x51>
401140: e8 f5 02 00 00 callq 40143a <explode_bomb>
401145: 83 c3 01 add $0x1,%ebx %rbx中是刚刚的轮次数,从1开始
401148: 83 fb 05 cmp $0x5,%ebx %rbx<=6(有符号)跳转
40114b: 7e e8 jle 401135 <phase_6+0x41>内循环:为了保证当前的这个数和其他所有的数不一样,直到%rbx到了最后一个数比完了结束,从外循环当前处理的数的位置开始,到最后一个数结束
40114d: 49 83 c5 04 add $0x4,%r13
每循环一次,%r13向后移动一个数
401151: eb c1 jmp 401114 <phase_6+0x20> 外层循环 401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi 运行到这里,嵌套的循环结束
对于刚刚的这个循环,如果使用c语言描述,请见图二
上面这条指令是为了将%rsi指向最后一个输入的数,用来做下面循环的结束条件
接下来一部分是用7减去输入的数,放在原来的位置
401158: 4c 89 f0 mov %r14,%rax 在0x40110b已知%r14中存放的是当前栈顶的地址
40115b: b9 07 00 00 00 mov $0x7,%ecx 7->rcx
401160: 89 ca mov %ecx,%edx rcx->rdx循环,(用7减)
401162: 2b 10 sub (%rax),%edx 这是用7减去当前rax中指向的值,放在rdx中
401164: 89 10 mov %edx,(%rax) 然后把rdx放回rax指向的内存
401166: 48 83 c0 04 add $0x4,%rax rax中存放的地址加4,向后移动一个数据
40116a: 48 39 f0 cmp %rsi,%rax 结束循环的条件,当rax==rsi说明这六个数都已经处理完了
40116d: 75 f1 jne 401160 <phase_6+0x6c>
经历完这个循环,最初输入的a b c d e f 已经变成了7-a 7-b …… 7-f,存放在%rsp到%rsp+0x18内存中,每个数据占了4byte
打印这些值,发现确实已经变成了被7减后的值(图三)
下面一段(0x40116f~0x4011a9)是根据我输入的数字将对应的链表的节点的地址放在stack中对应的顺序存储的位置上(流程图见图四)
40116f: be 00 00 00 00 mov $0x0,%esi 首先%rsi赋初值为0,注意,这里的%rsi中的值每次循环会加四,相当于数组的下标向后移动,遍历这输入的六个数字
401174: eb 21 jmp 401197 <phase_6+0xa3>
401176: 48 8b 52 08 mov 0x8(%rdx),%rdx 循环开始,(找到目标节点)
此时,%rdx是第一个节点的地址,这是将rdx变成当前节点的下一个节点的地址
40117a: 83 c0 01 add $0x1,%eax将%rax+1
可以看出,%rdx和%rax的关系是,%rdx中存放的是第%rax个节点的地址
40117d: 39 c8 cmp %ecx,%eax 看看是否已经循环到了要找的节点
40117f: 75 f5 jne 401176 <phase_6+0x82> 没找到,继续找
401181: eb 05 jmp 401188 <phase_6+0x94> 已经到了,跳转
401183: ba d0 32 60 00 mov $0x6032d0,%edx
注意:这里出现了一个地址值,打印后发现这是六个节点,每个节点占用了十六个字节,前八个字节是数据域,后八个字节显然是指针域,指向了下一个节点的地址。(图五)
因为是从输入的数是6(7-6=1)的情况下才跳转过来的,所以这里直接把第一个节点的地址放进%edx中
401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2)
这是把找到的对应的节点的地址放到0x20(%rsp,%rsi,2)的位置中,也可以看出,这些地址从%rsp+0x20开始存储,每次偏移2*%rsi字节,也可推论出,%rsi每次循环后至少改变4个字节。
40118d: 48 83 c6 04 add $0x4,%rsi 自增4
401191: 48 83 fe 18 cmp $0x18,%rsi 当rsi到0x18说明已经循环了六次了,该结束了
401195: 74 14 je 4011ab <phase_6+0xb7>退出当前的循环
401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx 将当前要处理的数放在%exc中,或说继续处理下一个数
40119a: 83 f9 01 cmp $0x1,%ecx
40119d: 7e e4 jle 401183 <phase_6+0x8f>根据前面分析已知,输入的值不可能小于1,所以,当%ecx中的数是1,才会跳转(为了简化找到对应地址的过程,避免了循环)
40119f: b8 01 00 00 00 mov $0x1,%eax 如果当前处理的数不是1,会到这里执行,把1放进%rax
4011a4: ba d0 32 60 00 mov $0x6032d0,%edx 把第一个节点的地址放到%rdx
4011a9: eb cb jmp 401176 <phase_6+0x82>
执行完上面一段指令后,栈中的数据如图(图六)
下边这一段指令的内容是更新节点指针域。
把第一个数对应的节点的指针域指向第二个节点,以此类推。
4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx 第一个数对应的节点的地址放进%rbx
4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax 第二个数对应的节点的地址在栈中的地址放进%rax
4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi 将不存在的第七个数对应节点的地址在栈中的地址放在%rsi,作为循环结束的终止条件
4011ba: 48 89 d9 mov %rbx,%rcx 第一个数对应的节点的地址放进%rcx
4011bd: 48 8b 10 mov (%rax),%rdx 第二个数对应的节点的地址放进%rdx
4011c0: 48 89 51 08 mov %rdx,0x8(%rcx) 把第二个数对应的节点的地址放进第一个数对应的节点的指针域中,使得第一个数对应的节点指向第二个数对应的节点在内存中的位置(不是栈中)
4011c4: 48 83 c0 08 add $0x8,%rax
%rax从第二个数对应的节点的地址在栈中的地址变成第三个数对应的节点的地址在栈中的地址
4011c8: 48 39 f0 cmp %rsi,%rax当%rsi已经指向第七个数对应的那个地址,结束该循环
4011cb: 74 05 je 4011d2 <phase_6+0xde>
4011cd: 48 89 d1 mov %rdx,%rcx
4011d0: eb eb jmp 4011bd <phase_6+0xc9> 循环,将这六个节点全部链接
4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx) 此时的%rdx是第六个数对应的节点的地址,此节点的指针域赋值为NULL
4011d9: 00
下边最后一段指令的内容是要第一个数对应的节点的数据域的后4bit大于第二个的
,第二个的大于第三个的,以此类推。
4011da: bd 05 00 00 00 mov $0x5,%ebp 计数器,一共比五次,每次减一
4011df: 48 8b 43 08 mov 0x8(%rbx),%rax 第1~5个数对应节点的指针域的内容,循环开始
4011e3: 8b 00 mov (%rax),%eax 第2~6个数的数据域的内容
4011e5: 39 03 cmp %eax,(%rbx) 第2~6个数的数据域的内容和第1~5个数的数据域的内容相比较 ,而且只比较后面四个字节的内容
4011e7: 7d 05 jge 4011ee <phase_6+0xfa> 当大于等于,不爆
4011e9: e8 4c 02 00 00 callq 40143a <explode_bomb>
4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx 将前一个节点的地址变成后一个节点的地址,相当于p=p->next;
4011f2: 83 ed 01 sub $0x1,%ebp 更改计数器的值
4011f5: 75 e8 jne 4011df <phase_6+0xeb>
当计数器变成零则跳出循环
4011f7: 48 83 c4 50 add $0x50,%rsp 回收栈帧
4011fb: 5b pop %rbx 恢复数据
4011fc: 5d pop %rbp
4011fd: 41 5c pop %r12
4011ff: 41 5d pop %r13
401201: 41 5e pop %r14
401203: c3 retq 返回
这道题考察链表,循环,stack。
分析一遍后可知,我们要输入六个数(互不相同,在1~6这里面),空格隔开,并且根据这六个数(相当于索引),必须满足前一节点数据域后4bit大于后一节点数据域后4bit的节点顺序。
此题需要倒推,根据打印出来的节点的数据域的值,可以看出
这些节点必须按照node3 node4 node5 node6 node1 node2的顺序才可以
即被七减完后stack中的值应该是(从%rsp+0x0到%rsp+0x14)3 4 5 6 1 2
也就是说,输入的六个数应该是 4 3 2 1 6 5
验证结果如图(图七)
secret_phase
图一:
图二:
图三:
图四:
图五:
图六:
图七:
图八:
浏览反汇编的.s文件发现在phase_6下方有一个fun7函数,顺藤摸瓜可以发现还有两个函数,phase_defused和secret_phase
他们的调用关系是phase_defused->secret_phase-> fun7->fun7……
观察主函数main(部分)可以发现
下面是main函数的一部分
400e2d: e8 de fc ff ff callq 400b10 <puts@plt>
400e32: e8 67 06 00 00 callq 40149e <read_line>
400e37: 48 89 c7 mov %rax,%rdi
400e3a: e8 a1 00 00 00 callq 400ee0 <phase_1>
400e3f: e8 80 07 00 00 callq 4015c4 <phase_defused>
400e44: bf a8 23 40 00 mov $0x4023a8,%edi
400e49: e8 c2 fc ff ff callq 400b10 <puts@plt>
400e4e: e8 4b 06 00 00 callq 40149e <read_line>
400e53: 48 89 c7 mov %rax,%rdi
400e56: e8 a1 00 00 00 callq 400efc <phase_2>
400e5b: e8 64 07 00 00 callq 4015c4 <phase_defused>
400e60: bf ed 22 40 00 mov $0x4022ed,%edi
400e65: e8 a6 fc ff ff callq 400b10 <puts@plt>
400e6a: e8 2f 06 00 00 callq 40149e <read_line>
400e6f: 48 89 c7 mov %rax,%rdi
400e72: e8 cc 00 00 00 callq 400f43 <phase_3>
400e77: e8 48 07 00 00 callq 4015c4 <phase_defused>
400e7c: bf 0b 23 40 00 mov $0x40230b,%edi
400e81: e8 8a fc ff ff callq 400b10 <puts@plt>
400e86: e8 13 06 00 00 callq 40149e <read_line>
400e8b: 48 89 c7 mov %rax,%rdi
400e8e: e8 79 01 00 00 callq 40100c <phase_4>
400e93: e8 2c 07 00 00 callq 4015c4 <phase_defused>
400e98: bf d8 23 40 00 mov $0x4023d8,%edi
400e9d: e8 6e fc ff ff callq 400b10 <puts@plt>
400ea2: e8 f7 05 00 00 callq 40149e <read_line>
400ea7: 48 89 c7 mov %rax,%rdi
400eaa: e8 b3 01 00 00 callq 401062 <phase_5>
400eaf: e8 10 07 00 00 callq 4015c4 <phase_defused>
400eb4: bf 1a 23 40 00 mov $0x40231a,%edi
400eb9: e8 52 fc ff ff callq 400b10 <puts@plt>
400ebe: e8 db 05 00 00 callq 40149e <read_line>
400ec3: 48 89 c7 mov %rax,%rdi
400ec6: e8 29 02 00 00 callq 4010f4 <phase_6>
400ecb: e8 f4 06 00 00 callq 4015c4 <phase_defused>
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
观察主函数可以发现,在调用phase_1~6后,都会调用一次phase_defused函数,猜想是否进入phase中的secret_phase和phase_1~6的输入有关
下面我们来解析phase_defused函数
00000000004015c4 <phase_defused>:
4015c4: 48 83 ec 78 sub $0x78,%rsp 分配栈帧
4015c8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 金丝雀
4015cf: 00 00
4015d1: 48 89 44 24 68 mov %rax,0x68(%rsp)
4015d6: 31 c0 xor %eax,%eax 清零
4015d8: 83 3d 81 21 20 00 06 cmpl $0x6,0x202181(%rip)
# 603760 <num_input_strings>
注释的意思是输入的string的个数,结合上面这个指令可知,如果输入的string的个数不是六,那就会跳转到该函数末尾,无法调用secret_phase函数
4015df: 75 5e jne 40163f <phase_defused+0x7b>
4015e1: 4c 8d 44 24 10 lea 0x10(%rsp),%r8
4015e6: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
4015eb: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
4015f0: be 19 26 40 00 mov $0x402619,%esi "%d %d %s"(图一)
这意味着输入的值必须是“数字,数字,字符串”类型的,观察已知的前六个题的密码,只有phase_3、phase_4是符合的,也就是说要在三、四的密码的后边加上一个字符串
4015f5: bf 70 38 60 00 mov $0x603870,%edi ""
4015fa: e8 f1 f5 ff ff callq 400bf0 <__isoc99_sscanf@plt>
4015ff: 83 f8 03 cmp $0x3,%eax 这里也说明输入值是3个才有可能调用secret_phase,否则会跳转到结尾
401602: 75 31 jne 401635 <phase_defused+0x71>
401604: be 22 26 40 00 mov $0x402622,%esi "DrEvil"(图一)
401609: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
40160e: e8 25 fd ff ff callq 401338 <strings_not_equal>调用了strings_not_euqal函数,结合前边几个phase的经验,可以知道,这是rdi和rsi的对比,也就是说,待输入的string就是%esi中的"DrEvil",将三、四题的答案后加上这个string,可以进入secret_phase(图二)
经测试,在四题输入0 0 时才会进入secret_phase
401613: 85 c0 test %eax,%eax
401615: 75 1e jne 401635 <phase_defused+0x71>
401617: bf f8 24 40 00 mov $0x4024f8,%edi "Curses, you've found the secret phase!" (图一)
40161c: e8 ef f4 ff ff callq 400b10 <puts@plt>
401621: bf 20 25 40 00 mov $0x402520,%edi "But finding it and solving it are quite different..." (图一)
401626: e8 e5 f4 ff ff callq 400b10 <puts@plt>
40162b: b8 00 00 00 00 mov $0x0,%eax
401630: e8 0d fc ff ff callq 401242 <secret_phase>
401635: bf 58 25 40 00 mov $0x402558,%edi "Congratulations! You've defused the bomb!" (图一)
40163a: e8 d1 f4 ff ff callq 400b10 <puts@plt>
40163f: 48 8b 44 24 68 mov 0x68(%rsp),%rax
401644: 64 48 33 04 25 28 00 xor %fs:0x28,%rax 检验stack是否被破坏
40164b: 00 00
40164d: 74 05 je 401654 <phase_defused+0x90>
40164f: e8 dc f4 ff ff callq 400b30 <__stack_chk_fail@plt>
401654: 48 83 c4 78 add $0x78,%rsp
401658: c3 retq
401659: 90 nop
40165a: 90 nop
40165b: 90 nop
40165c: 90 nop
40165d: 90 nop
40165e: 90 nop
40165f: 90 nop
0000000000401242 <secret_phase>:
401242: 53 push %rbx 开栈帧
401243: e8 56 02 00 00 callq 40149e <read_line> 读入数据
401248: ba 0a 00 00 00 mov $0xa,%edx
40124d: be 00 00 00 00 mov $0x0,%esi
401252: 48 89 c7 mov %rax,%rdi
401255: e8 76 f9 ff ff callq 400bd0 <strtol@plt>
Strtol是string to long ,将字符串变成一个长整型的数。
40125a: 48 89 c3 mov %rax,%rbx 返回值是我输入的数(图五)
40125d: 8d 40 ff lea -0x1(%rax),%eax
401260: 3d e8 03 00 00 cmp $0x3e8,%eax 这是要求刚刚调用的函数的返回值(就是我输入的那个数)要小于等于0x3e8+1,这是为了避免访问内存越界等情况
401265: 76 05 jbe 40126c <secret_phase+0x2a>满足条件跳转
401267: e8 ce 01 00 00 callq 40143a <explode_bomb>
40126c: 89 de mov %ebx,%esi
40126e: bf f0 30 60 00 mov $0x6030f0,%edi
出现了一个地址,使用x命令看看是什么(图六),发现是一堆节点,仔细观察可以发现,一个节点由以下几个部分构成:data(4 字节)、0(4 字节)、add(8 字节)、add(8 字节)、0(8 字节)。每个节点有两个地址,看做左子树和右子树的指针,猜测这是一个二叉树。(图六)
并且在图中可以看到,每个节点前边的0x1,0x21,0x22,0x32,0x33,0x31,0x34…可以正好表示该节点是树的第几层的第几个节点。
于是我把树的结构和节点的值画出来,见图七
此时rsi中存放的是我输入的那个数字,rdi中是树的根节点
401273: e8 8c ff ff ff callq 401204 <fun7>调用函数
401278: 83 f8 02 cmp $0x2,%eax 可见,返回值必须是2
40127b: 74 05 je 401282 <secret_phase+0x40>
40127d: e8 b8 01 00 00 callq 40143a <explode_bomb>
401282: bf 38 24 40 00 mov $0x402438,%edi 这是拆弹成功后的输出内容
401287: e8 84 f8 ff ff callq 400b10 <puts@plt>
40128c: e8 33 03 00 00 callq 4015c4 <phase_defused>
401291: 5b pop %rbx
401292: c3 retq
401293: 90 nop
401294: 90 nop
401295: 90 nop
401296: 90 nop
401297: 90 nop
401298: 90 nop
401299: 90 nop
40129a: 90 nop
40129b: 90 nop
40129c: 90 nop
40129d: 90 nop
40129e: 90 nop
40129f: 90 nop
0000000000401204 <fun7>:
401204: 48 83 ec 08 sub $0x8,%rsp 开辟栈帧
401208: 48 85 ff test %rdi,%rdi 如果%rdi==0,那就表示这个传入的根节点是个空指针,直接返回
40120b: 74 2b je 401238 <fun7+0x34>
40120d: 8b 17 mov (%rdi),%edx 得节点的data值 放rdx中。
40120f: 39 f2 cmp %esi,%edx 将目前这个节点的data值和我输入的值进行比较
401211: 7e 0d jle 401220 <fun7+0x1c> 如果我输入的值大于等于等于节点的(有符号),那就跳转
401213: 48 8b 7f 08 mov 0x8(%rdi),%rdi 如果我输入的值小于节点的值,那就将指向当前节点的指针变成当前节点指向的位置加8的地方存储的值(是指向左子树的值)
401217: e8 e8 ff ff ff callq 401204 <fun7> 然后继续调用,传入的是左子树的指针
40121c: 01 c0 add %eax,%eax 然后将返回值乘二
40121e: eb 1d jmp 40123d <fun7+0x39> 跳转到结尾
401220: b8 00 00 00 00 mov $0x0,%eax 将rax赋值为0
401225: 39 f2 cmp %esi,%edx 继续比较
401227: 74 14 je 40123d <fun7+0x39> 如果相等,那就返回当前rax中的值
401229: 48 8b 7f 10 mov 0x10(%rdi),%rdi 将指右子树的地址放进%rdi,接下来在右子树中查找我输入的值
40122d: e8 d2 ff ff ff callq 401204 <fun7>
401232: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax 然后rax=rax*2+1
401236: eb 05 jmp 40123d <fun7+0x39>
401238: b8 ff ff ff ff mov $0xffffffff,%eax 空树,返回-1
40123d: 48 83 c4 08 add $0x8,%rsp回收栈帧
401241: c3 retq 返回
最后在树中查找这一段可以简要地总结为,进入左子树就将返回值乘二,进入右子树就将返回值乘二再加一,返回值初始是零。这一过程见图四
这一环节等价于C语言的:
int fun7(int cmp, Node* addr){
if(addr == 0){
return -1;
}
int v = addr->value;
if (v == cmp){
return 0;
}else if( v < cmp){
return 1 + 2*fun7(cmp, addr->right);
}else{
return 2*func7(cmp, addr->left);
}
}
显然,要让返回值先乘二加一再乘二就可以得到二了,即先进入左子树再右子树(反着的),即0x16,所以答案输入22即可。(图八)
实验总结及心得体会
首先学习linux终端的基本使用方法,比如cd(更改工作路径)、pwd(查看当前路径)、whoami(查当前用户)、gdb(调试工具)、tree(显示文件结构)、..(返回上一级)、touch(新建或刷新)、vi(编辑器)、gcc(编译)等命令。
其次,学会熟练使用gdb调试工具,其中我学会的一部分命令有:
Si单步执行、disas显示当前运行到的位置、info reg显示寄存器状态、info b显示设置的断点的状态、layout r同时显示命令,汇编,寄存器、x/指令用来显示内存中数据,r 开始运行、r+文件名 带参数运行、b+行号或函数名是设置断点、clear b是清除所有断点、c继续运行到断点或结尾……
Gdb是十分有力的调试工具,让我感受到了计算机底层的繁琐且有序。
实验中遇到的问题:
- 对汇编语言阅读不熟练,多加练习才能逐步熟悉汇编语言。
- 虚拟机遇到问题,我自己在win上搭建虚拟机,独立完成了配置环境,学习了C文件编辑并编译等操作。
- 实验难度大,难以攻克,需要坚定信心。
拆弹过程中,遇到问题我会上网求解,找相似的文章看一看,解决了问题,也开拓了视野。
总结一下这7个phase:难度由简到难,层层递进。分别的考点是:内存中的字符串匹配,对栈的理解、跳转表的结构、递归与结束条件、文本加密破解、链表中结点排序以及最后的树以及树中查找数据。
解决这7个问题虽然耗时很长,但是一点一点攻克总能够解出来的,可谓:世上无难事只怕有心人。