🔖诗赋清音:烛龙翔夜啸,翼舞星空深。 巍峨山川壮丽志,激荡热血梦侠心。
目录
1. CSAPP与Bomb简介
1.1 CSAPP
1.2 Bomb
2.bomb
2.1 实验环境
2.2 实验过程
2.2.1 phase_1
2.2.2 phase_2
2.2.3 phase_3
2.2.4 Phase_4
2.2.5 phase_5
2.2.6 phase_6
第一部分
第二部分
第三部分
第四部分
2.2.7 Secret_phase
2.3 实验结果
2.4 实验体会
1. CSAPP与Bomb简介
1.1 CSAPP
《CSAPP》是指计算机系统基础课程的经典教材《Computer Systems: A Programmer's Perspective》,由Randal E. Bryant和David R. O'Hallaron编写。该书的主要目标是帮助深入理解计算机系统的工作原理,包括硬件和软件的相互关系,其涵盖了计算机体系结构、汇编语言、操作系统、计算机网络等主题,旨在培养学生系统级编程和分析的能力。
1.2 Bomb
"Bomb实验" 是与CSAPP教材相关的一项编程实验。它是一种反汇编和逆向工程任务,旨在教授如何分析和解决复杂的程序问题。Bomb实验的目标是解开一系列的"炸弹",每个炸弹都有不同的解锁方法,需要分析程序的汇编代码,理解其工作原理,并找到正确的输入来解除炸弹。这个实验教授了计算机系统的底层知识,包括汇编语言和程序执行的原理。
资源获取:关注公众号【科创视野】回复 csappbomblab
2.bomb
2.1 实验环境
- VMware Workstation虚拟机环境下的Ubuntu 64位。
2.2 实验过程
实验准备阶段:首先需要使用ubuntu联网环境跳转到链接下载实验所需的bomblab:Bmblab源文件
下载bomblab压缩包并输入
tar –xvf bomb.tar
进行解压缩,进入该目录所有文件如下所示:
在终端输入
sudo apt-get install gdb
安装调试器。基本用法参考下图:
实验过程阶段:
“Binary bombs”是一个可在Linux系统上运行的C程序,它由6个不同的阶段(phase1~phase6)组成。在每个阶段,程序会要求输入一个特定的字符串。如果输入的字符串符合程序的预期输入,那么这个阶段的炸弹就会被“解除”,否则炸弹就会“爆炸”,并输出“BOOM!!!”的提示信息。实验的目的是尽可能多地解除这些炸弹的阶段。
每个炸弹阶段考察了机器级语言程序的一个不同方面,难度逐级递增:
* 阶段1:字符串比较
* 阶段2:循环
* 阶段3:条件/分支
* 阶段4:递归调用和栈
* 阶段5:指针
* 阶段6:链表/指针/结构
在炸弹拆除任务中,还存在一个隐藏阶段。然而,只有在第四个阶段解决后添加特定的字符串后,该隐藏阶段才会出现。为了完成任务,需要使用gdb调试器和objdump反汇编炸弹的可执行文件,然后单步跟踪每个阶段的机器代码,理解每个汇编语言的行为或作用。这将帮助“推断”出拆除炸弹所需的目标字符串。为了调试,可以在每个阶段的开始代码前和引爆炸弹的函数前设置断点。
在终端输入
objdump -d bomb > bomb.asm
得到bomb的反汇编文件bomb.asm如下所示。
2.2.1 phase_1
phase_1是一个二进制炸弹,需要输入一个字符串作为密码才能解除炸弹,否则炸弹会爆炸。
进入反汇编文件bomb.asm,
vim bomb.asm
在未插入处输入下列指令以查找phase_1的位置。
/phase_1
分析反汇编代码的phase_1部分内容如下:
1.Sub $0x8,%rsp含义是把将栈顶指针(%rsp)向下移动8个字节,相当于在栈顶分配8个字节的空间。这通常用于在程序中为局部变量或者函数参数分配空间。由于栈是向下生长的,这条指令会将栈顶指针减去8,使得栈顶指针指向的位置向下移动8个字节。
2.mov $0x402400,%esi:将立即数0x402400移动到寄存器%esi中。在汇编语言中,%esi是通用寄存器之一,用于存储指针或者数据,这条指令的作用是将0x402400这个数值存储到%esi寄存器中,以便在后续的指令中使用。
3.callq 401338 <strings_not_equal>:将strings_not_equal函数调用的返回地址压入栈中,并跳转到strings_not_equal函数的起始地址开始执行。在strings_not_equal函数执行完成后,执行流程会返回到该指令之后的下一条指令,继续执行程序。
4.test %eax,%eax:将寄存器%eax中的值与自身进行按位与运算,并更新标志寄存器的状态该指令执行的结果会影响标志寄存器中的零标志(ZF)和符号标志(SF)的状态,这两个标志是CPU用于判断运算结果是否为零或者是否为负数的标志。 如果%eax的值为零,则ZF标志将被设置为1,否则将设置为0;如果%eax的值为负数,则SF标志将被设置为1,否则将设置为0。 test %eax, %eax经常用于条件分支的判断,例如在判断%eax是否为0的时候,可以使用test %eax, %eax指令来判断。如果ZF标志被设置为1,则说明%eax等于0,否则%eax不等于0。
5.je 400ef7 <phase_1+0x17>:根据前面cmp指令的比较结果,如果两个字符串相等,那么跳转到phase_1 + 0x17地址处继续执行。如果两个字符串不相等,则不会跳转,继续执行下一条指令。在该程序中,phase_1 + 0x17地址处是程序正常执行的下一条指令,也就是当输入的字符串正确时,程序会继续执行下去,否则会引爆炸弹,程序结束。
6.callq 40143a <explode_bomb>:调用"explode_bomb"函数,发生爆炸。
7.add $0x8,%rsp: 将栈指针寄存器 %rsp 的值加上常量值 0x8,以释放栈上的8个字节空间。在程序执行过程中,栈指针寄存器 %rsp 会随着程序的运行而不断变化,当需要释放栈上的空间时,就需要执行这条指令来调整栈指针的值,以释放相应的空间。
8.retq: 从函数中返回。
分析发现phase_1函数调用了函数strings_not_equal。在该函数被调用之前,程序将用户输入的字符串作为第一个参数传递给了strings_not_equal函数。这个字符串是用户在运行程序时输入的,它将作为密码来解除炸弹。这个字符串保存在%eax寄存器中,因此可以推断:程序会将用户输入的字符串作为第一个参数传递给strings_not_equal函数。
查看strings_not_equal函数的实现,该函数会将两个字符串进行比较,并返回比较结果。在反汇编代码中,可以看到调用strcmp函数的汇编指令。strcmp函数返回0表示两个字符串相等,否则它们不相等。因此可以得出结论:strings_not_equal函数返回一个非零值表示两个字符串不相等,返回0表示两个字符串相等。
根据汇编代码中的操作,可以发现password字符串保存在了内存地址0x402400处,需要将该地址中的字符串作为比较的第二个参数传递给strings_not_equal函数,故可以得出当比较结果为0时,程序不会引爆炸弹,只需要输入password字符串即可解除炸弹。
打开终端输入
gdb bomb
在gdb输入
print (char*)0x402400
结果返回得到密钥“Border relations with Canada have never been better.”。
在终端输入
./bomb
填入phase_1返回的密钥:
Border relations with Canada have never been better.
结果显示phase_1通关。
综上所述,解决phase_1的过程是分析反汇编代码,找到password字符串的位置,并将该字符串与输入的字符串进行比较。如果比较结果为0,则炸弹解除;否则,炸弹会爆炸。
2.2.2 phase_2
phase_2是一个需要解密密码的关卡,需要输入一个字符串来解锁该关卡。如果输入的字符串符合要求,则可以成功通关,否则会引爆炸弹,导致失败。
进入反汇编文件bomb.asm
vim bomb.asm
在未插入处输入下列指令查找phase_2的位置。
/phase_2
分析反汇编代码的phase_2部分内容如下:
1.push %rbp: 将寄存器%rbp的值压入栈中。
2.push %rbx: 将寄存器%rbx的值压入栈中。
3.sub $0x28,%rsp:修改栈指针寄存器 %rsp 的值,以便在栈上分配一段新的空间。
3.callq 40145c <read_six_numbers>:调用一个名为"read_six_numbers"的函数。
5.je 400f30 <phase_2+0x34>:进行条件跳转。在该指令中,je 表示条件跳转指令,400f30 <phase_2+0x34> 是跳转目标地址,表示如果前面的条件成立,则会跳转到这个地址处执行后续的指令
……
可以发现phase_2函数有以下几个步骤:
- 将数据压入栈中;
- 减少栈指针40,为局部变量提供空间;
- 将栈指针存入%rsi寄存器中;
- 调用两个explode_bomb函数;
- 调用read_six_numbers函数读取6个数字。
尝试随意输入6个数字,如下:
为了解题,需要使%rsp的值为1,以便程序执行jmp指令跳过explode_bomb函数。jmp指令的目标地址是400f30,对应的指令为lea,用于加载有效地址。在调用read_six_numbers函数时,需要输入6个数字,该函数的地址为40145c,可以发现401480: mov $0x4025c3,%esi ,类似Phase 1向寄存器中转入数据(对应字符串),于是输入print (char*)0x4025c3进行测试,结果如下:
说明密钥需要6个整型数。回到phase_2反汇编进行分析,可以分析有一个循环,此时,结果上述循环,各寄存器对应的值如下:
%rbx | %rbp | %eax |
%rsp+4 | %rsp+24 | (%rsp)*2=2 |
%rsp+8 | (%rsp+4)*2=4 | |
%rsp+12 | (%rsp+8)*2=8 | |
%rsp+16 | (%rsp+12)*2=16 | |
%rsp+20 | (%rsp+16)*2=32 | |
%rsp+24 |
循环部分对应C代码
int i;
int a[6];
a[0] = 1;
for(i=1;i<6;i++)
{
a[i] = a[i-1]*2;
}
该代码段实现了对一组数的比较操作,并判断其中是否存在某个数是前一个数的2倍。这段代码是汇编语言的实现,对于不熟悉汇编语言的人来说,可能比较难懂。对这段代码进行详细的解析。
1.这段代码是一个循环操作,它对一组数进行比较,并且判断其中是否存在某个数是前一个数的2倍。这组数存储在栈中,栈顶元素即为第一个数。 在代码执行的开始,我们可以看到一条cmp指令,它将栈顶元素与立即数0x1进行比较。如果相等,说明第一个输入数为1,此时会跳转到地址400f30处;如果不相等,说明第一个输入数不是1,此时会跳转到地址400f17处。
2.在地址400f30处,我们可以看到两条lea指令,它们将%rsp+4和%rsp+24对应的地址分别存储到%rbx和%rbp寄存器中。%rbx寄存器存储的是第二个输入数的地址,%rbp寄存器存储的是第六个输入数后面的地址。
3.代码跳转到地址400f17处。可以看到sub指令,它将%rbx的值减去4,并将其对应的内存地址中的值(即前一个数)存储到%eax寄存器中。接着,将%eax的值乘以2,并将其与%rbx对应的内存地址中的值(即当前数)进行比较。如果相等,说明当前数是前一个数的2倍,此时会跳转到地址400f25处;如果不相等,说明当前数不是前一个数的2倍,此时会跳转到地址400f17处。
4.在地址400f25处,我们可以看到一条add指令,它将%rbx的值加上4。接着,代码会判断%rbx的值是否等于%rbp的值,如果不相等,则说明还没有处理完所有的数,此时会跳转到地址400f17处;如果相等,则说明已经处理完所有的数,此时会跳转到地址400f3c处,结束循环。 在循环中,代码会从栈中逐个取出这些数,并进行比较操作。如果存在某个数是前一个数的2倍,代码会跳转到地址400f25处,否则会跳转到地址400f17处。这个循环会一直执行,直到处理完所有的数,才会跳转到地址400f3c处,结束循环。
通过以上的分析,可以得到这组数的具体值。这组数分别为1,2,4,8,16,32。
在终端输入
./bomb
填入密钥
1 2 4 8 16 32
结果显示phase_2通关。
综上所述,解决phase_2的过程的难点是分析反汇编代码的循环部分,找到循环结束的位置,在循环中,代码会从栈中逐个取出这些数,并进行比较操作。如果存在某个数是前一个数的2倍,代码会跳转到地址400f25处,否则会跳转到地址400f17处。这个循环会一直执行,直到处理完所有的数,才会跳转到地址400f3c处,结束循环。
将Phase_2中每句代码的作用解释如下所示。
0000000000400efc <phase_2>:
400efc: 55 push %rbp //把数据压入栈
400efd: 53 push %rbx //把数据压入栈
400efe: 48 83 ec 28 sub $0x28,%rsp //把栈指针减少40,提供局部变量空间
//申请空间
400f02: 48 89 e6 mov %rsp,%rsi //把栈指针状态存入%rsi中
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers> // 调用函数read_six_numbers,读取6个数字,此时(%rsp)已被赋值
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) //(%rsp)的值即输入的第一个参数的值,将(%rsp)的值与1比较
400f0e: 74 20 je 400f30 <phase_2+0x34> //若相等则跳转至400f30
400f10: e8 25 05 00 00 callq 40143a <explode_bomb> //否则爆炸
400f15: eb 19 jmp 400f30 <phase_2+0x34> //jmp指令无条件跳转,直接跳转至400f30
400f17: 8b 43 fc mov -0x4(%rbx),%eax
400f1a: 01 c0 add %eax,%eax //%eax的值*2
400f1c: 39 03 cmp %eax,(%rbx) //比较%eax的值和此时%rbx对应的内存的值
400f1e: 74 05 je 400f25 <phase_2+0x29> //若相等则跳转至400f25
400f20: e8 15 05 00 00 callq 40143a <explode_bomb> //否则爆炸
400f25: 48 83 c3 04 add $0x4,%rbx //%rbx的值(地址)加4
400f29: 48 39 eb cmp %rbp,%rbx //比较两个寄存器的值,判断是否比较完6个数
400f2c: 75 e9 jne 400f17 <phase_2+0x1b> //若不相等,则进行跳转至400f17,即循环没结束
400f2e: eb 0c jmp 400f3c <phase_2+0x40> //否则直接跳转至400f3c,循环结束标志
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx //将%rsp+4代表的地址移入%rbx,即第2个输入数的地址
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp //将%rsp+24代表的地址移入%rbp,即第6个输入数的后一块地址
400f3a: eb db jmp 400f17 <phase_2+0x1b> //直接跳转至400f17
//释放空间
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 retq
2.2.3 phase_3
phase_3需要使用逆向工程技术找到正确的密码。
vim bomb.asm
使用vim指令输入进入反汇编文件bomb.asm,
/phase_3
在未插入处输入查找phase_3的位置。
在Phase_3函数中,程序会首先使用sscanf函数从用户输入的字符串中读取一个整数,并将其存储在%eax寄存器中。接着,程序会将%eax的值与1进行按位与运算,并将结果存储到%eax寄存器中。如果%eax的值为0,则表示输入的整数为偶数;否则,表示输入的整数为奇数。 接下来,程序会调用一个名为fun3的函数,并将输入的整数和一个立即数0x7作为参数传递给这个函数。fun3函数的具体实现可以在IDA中查看。在fun3函数中,程序会将输入的整数与0x7进行按位与运算,然后将结果与一个名为“global_value”的全局变量进行比较。如果相等,则返回0;否则,返回一个非0的值。 最后,Phase_3函数会检查fun3函数的返回值是否为0,如果是,则表示密码正确;否则,表示密码错误。 为了寻找正确的密码,需要找到“global_value”的值。可以使用gdb调试工具,在程序运行时获取“global_value”的值。首先,需要在gdb中运行程序,并在输入密码之前,在第一行代码处打一个断点。接着,输入一个偶数作为密码,然后运行到断点处。在这里,可以使用命令“x/gx 0x地址”来查看“global_value”的值。 得到“global_value”的值后,需要使用Python等脚本语言,在0到1000之间枚举所有满足条件的偶数,并将其作为密码输入到程序中,以验证是否正确。如果找到了正确的密码,程序会输出“Congratulations!”;否则,程序会输出“Explosion!”。 除了上述步骤,我们可以注意到Phase_3函数中的第四行代码,程序将0x4025cf赋值给了%esi寄存器。这是一个地址常量,由于它在sscanf函数的前面,可以推测0x4025cf可能是一个格式字符串。因此使用gdb工具中的x/s命令来查看该地址常量的内容,以进一步了解输入内容的格式。
其中,第7-10行代码是关键的判断条件。在这几行代码中,程序会将输入的整数与1进行按位与运算,并将结果存储到%eax寄存器中。随后,程序会将%eax的值与1进行比较,如果小于等于1,则会引爆炸弹。 %eax寄存器一般是存储函数的返回值,在Bomblab中,sscanf函数的返回值就是成功读取的元素个数。因此可以推断出,这里输入的元素个数必须大于1才能通过验证,否则会引爆炸弹。 为了找到正确的密码,需要输入两个整数,可以通过IDA等反汇编工具来了解程序的具体实现。
在终端查看跳转表中储存的地址。
x/8xg 0x402470
仔细观察这些地址,可以发现都是函数phase_3范围内的地址。
当num1=0时,跳转到0x0000000000400f7c处执行。如果num2不等于0xcf,则触发炸弹。
当num1=1时,跳转到0x0000000000400fb9处执行。如果num2不等于0x137,则触发炸弹。
当num1=2时,跳转到0x0000000000400f83处执行。如果num2不等于0x2c3,则触发炸弹。
当num1=3时,跳转到0x0000000000400f8a处执行。如果num2不等于0x100,则触发炸弹。
当num1=4时,跳转到0x0000000000400f91处执行。如果num2不等于0x185,则触发炸弹。
当num1=5时,跳转到0x0000000000400f98处执行。如果num2不等于0xce,则触发炸弹。
当num1=6时,跳转到0x0000000000400f9f处执行。如果num2不等于0x2aa,则触发炸弹。
当num1=7时,跳转到0x0000000000400fa6处执行。如果num2不等于0x147,则触发炸弹。
所以拆弹密码有以下8种:
0 207;
1 311;
2 707;
3 256;
4 389;
5 206;
6 682;
7 327.
输入其中任意几种或者全部输均可。
在终端输入
./bomb
填入密钥0 207结果显示phase_3通关。
综上所述,解决Bomblab中的Phase_3题目需要一定的逆向工程技术和耐心,但通过对代码的分析和调试,我们可以找到正确的密码,进入下一个阶段。这是逆向工程学习中重要的一步,也可以帮助我们更深入地了解程序的运行机制。
对应C代码的实现为:
void phase_3(const char *input)
{
// 0x8(%rsp) 0xc(%rsp)
int num1, num2;
// %rdi %rsi %rdx %rcx
int result = sscnaf(input, "%d %d", &num1, &num2);
if (result <= 1) {
explode_bomb();
}
switch (num1) {
case 0: // 0 207
if (num2 != 0xcf) {
explode_bomb();
}
break;
case 1: // 1 311
if (num2 != 0x137) {
explode_bomb();
}
break;
case 2: // 2 707
if (num2 != 0x2c3) {
explode_bomb();
}
break;
case 3: // 3 256
if (num2 != 0x100) {
explode_bomb();
}
break;
case 4: // 4 389
if (num2 != 0x185) {
explode_bomb();
}
break;
case 5: // 5 206
if (num2 != 0xce) {
explode_bomb();
}
break;
case 6: // 6 682
if (num2 != 0x2aa) {
explode_bomb();
}
break;
case 7: // 7 327
if (num2 != 0x147) {
explode_bomb();
}
break;
default:
explode_bomb();
break;
}
}
将Phase_3中每句代码的作用解释如下所示。
0000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp //给局部变量腾出空间
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx //加载地址,将0xc(%rsp)设为num2
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx //加载地址,将0x8(%rsp)设为num1
400f51: be cf 25 40 00 mov $0x4025cf,%esi //因为后续调用了函数scanf,此处需对输入格式进行测试
//经测试,0x4025cf对应字符串"%d %d"
//因此可以猜测开头%rcx存放的为输入的第一个数据(设为num1)的地址,%rdx存放的为输入的第二个数据(设为num2)的地址
400f56: b8 00 00 00 00 mov $0x0,%eax //初始化%eax
400f5b: e8 90 fc ff ff callq 400bf0 <__isoc99_sscanf@plt> //调用scanf函数,此时%eax放scanf函数的返回值(输入数据的个数)
400f60: 83 f8 01 cmp $0x1,%eax
400f63: 7f 05 jg 400f6a <phase_3+0x27> //jg:有符号大于则跳转,说明scanf输入数据的个数必须大于1
400f65: e8 d0 04 00 00 callq 40143a <explode_bomb> //否则爆炸
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp)
400f6f: 77 3c ja 400fad <phase_3+0x6a> //ja:无符号大于则跳转,至爆炸,说明num1为无符号数,大于0且需要小于等于7,所以num1=[0,7]
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax //将num1储存到%eax中
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8) //间接跳转,(此处%rax不完全等于%eax,由num1=1(%rax=1)时的跳转地址可推论)
//0x402470+%rax*8的计算结果作为地址,跳转到该地址继续执行
//根据该指令和后续的指令格式,很容易判断此处是switch语句的跳转表,跳转表的首地址为0x402470
//以 *0x402470 处的值为基地址,再加上8 * %rax 进行跳转,不同的 %rax 跳转到不同的位置。
400f7c: b8 cf 00 00 00 mov $0xcf,%eax //case 0 %eax=0xcf=207
400f81: eb 3b jmp 400fbe <phase_3+0x7b>
400f83: b8 c3 02 00 00 mov $0x2c3,%eax //case 2 %eax=0x2c3=707
400f88: eb 34 jmp 400fbe <phase_3+0x7b>
400f8a: b8 00 01 00 00 mov $0x100,%eax //case 3 %eax=0x100=256
400f8f: eb 2d jmp 400fbe <phase_3+0x7b>
400f91: b8 85 01 00 00 mov $0x185,%eax //case 4 %eax=0x185=389
400f96: eb 26 jmp 400fbe <phase_3+0x7b>
400f98: b8 ce 00 00 00 mov $0xce,%eax //case 5 %eax=0xce=206
400f9d: eb 1f jmp 400fbe <phase_3+0x7b>
400f9f: b8 aa 02 00 00 mov $0x2aa,%eax //case 6 %eax=0x2aa=682
400fa4: eb 18 jmp 400fbe <phase_3+0x7b>
400fa6: b8 47 01 00 00 mov $0x147,%eax //case 7 %eax=0x147=327
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 //case 1 %eax=0x137=311
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax //比较%eax的值和num2
400fc2: 74 05 je 400fc9 <phase_3+0x86> //je:若相等则跳转至结束
400fc4: e8 71 04 00 00 callq 40143a <explode_bomb> //否则爆炸
400fc9: 48 83 c4 18 add $0x18,%rsp //释放空间
400fcd: c3 retq
2.2.4 Phase_4
Phase_4是Bomblab中的一道难度较大的炸弹题目,需要破解一个使用了跳转表的程序,以解除炸弹。在这个过程中需要运用一些调试技巧和汇编知识,逐步分析程序的运行逻辑,找到正确的输入值,解除炸弹。
通过逐行分析代码,发现phase_4函数的代码逻辑与phase_3函数的代码逻辑有很多相似之处。在代码的第12行,可以看到调用了scanf函数,该函数会读取用户输入的内容,并按照指定的格式进行解析。从这个函数的参数可以看出,它需要读取两个数字,并将它们分别存储在0x8(%rsp)、0xc(%rsp)中,因此可以暂时确定需要输入至少两个数字。但是,这并不意味着只有两个数字是正确答案,还需要进一步的分析才能确定正确的密钥。
在代码的40102c,可以看到调用了第一个explode_bomb函数. 这说明在输入错误的密钥时,程序会触发炸弹并终止运行。为了跳过这个炸弹,我们需要输入正确的密钥。根据代码的第401033行,我们可以发现需要满足输入的第一个数字不为2,才能跳过这个炸弹,因此,我们需要在输入时避免将第一个数字设置为2。
在401058行explode_bomb函数,这说明在输入错误的密钥时,程序会触发另一个炸弹并终止运行。为了跳过这个炸弹输入正确的密钥,根据代码的第40105f行,需要满足输入的第一个数字为0xe(14),才能跳过这个炸弹。
当成功输入6个参数并且满足特定的条件后,程序会进行一系列操作并将三个寄存器(%edx、%esi、%edi)赋值,接着将这些参数传入func函数中进行处理。首先,我们需要注意的是在输入6个参数后,程序会对第一个参数进行一系列的位运算操作,最终得到一个索引值并跳转到对应的地址处。这个地址指向了一个跳转表,其中每个元素都是一个8字节的地址。程序以输入的第一个参数作为索引,查找跳转表中对应的地址,并跳转到该地址处执行下一步操作。 接着,程序会将输入的6个参数按顺序存储到栈中,然后将第一个参数作为索引,从跳转表中查找对应的地址,并跳转到该地址处执行代码。这个地址指向了一个函数,这个函数的作用是将输入的参数分别存储到%edx、%esi、%edi寄存器中,并调用func函数进行处理。 在func函数中,程序依次对%edx、%esi、%edi寄存器中的值进行一系列位运算操作,并将结果存储到%eax寄存器中。最终,当func函数执行完毕后,程序会将%eax寄存器中的值与一个特定的值进行比较,如果相等则输出“Phase_4 defused”,表示成功解除炸弹;否则输出“BOOM!!!”,表示炸弹爆炸了。 在这个过程中,需要仔细分析程序的运行逻辑和寄存器的作用,理解程序的实现原理。通过逐步分析和调试,
程序执行fun4函数后,返回值存放在%eax寄存器中。为了避免调用explode_bomb函数,我们需要将%eax的返回值设置为0,并保证输入的第二个数为0(从地址0x(%rsp)获取)。
对于func4函数而言,它整体较为复杂,为了便于之后的分析,我们将其转换为对应的C代码:
//x: %edi y:%esi z:%edx k: %ecx t:%eax
int func4(int x,int y,int z)
{//x in %rdi,y in %rsi,z in %rdx,t in %rax,k in %ecx
//y的初始值为0,z的初始值为14
int t=z-y;
int k=t>>31;
t=(t+k)>>1;
k=t+y;
if(k>x)
{
z=k-1;
func4(x,y,z);
t=2t;
return t;
}
else
{
t=0;
if(k<x)
{
y=k+1;
func4(x,y,z);
t=2*t+1;
return t;
}
else
{
return t;
}
}
}
根据之前传入func4函数的两个参数,我们得知func4函数会将7存储到%eax和%ecx寄存器中,并将%edi里存储的输入的第一个数与%ecx寄存器中的值进行比较。
在代码分析中,可以发现在400ff2的代码中,当%edi的值大于等于7时,会将%eax置为0。因此可以尝试将第一个输入的数字设置为7,以此来获取想要的返回值。
如果%edi的值小于7,程序会进入400fe9处并再次调用func4函数,形成递归调用。虽然看起来很复杂,但我们可以通过代入寄存器中的值并记录它们的变化来推导出最终的结果。据此,我们可以得知第一个输入的数字为0、1、3、7,而第二个输入的数字为0。
在终端输入
./bomb
填入密钥0 0结果显示phase_4通关。
综上所述,Phase_4程序使用了一个跳转表来实现多个分支语句。具体来说,程序先将输入的第一个值存储到%eax寄存器中,然后执行一个间接跳转,跳转的目标地址存储在内存地址0x402470(,%rax,8)处。这里的%rax寄存器就是我们输入的第一个值。通过将输入的值作为索引,程序可以从跳转表中查找对应的目标地址,然后进行跳转。跳转表通常使用数组或者指针来实现,每个元素对应一个分支语句的目标地址。在Phase_4中,跳转表使用的是数组实现方式,每个元素占据8个字节,因此需要将输入值乘以8来计算数组元素的偏移量。 在Phase_4中,程序使用了一些位运算操作,需要仔细分析代码,理解其运行逻辑。具体来说,程序使用了“and”、“shr”和“cmp”等指令来对输入值进行位运算操作,并判断其是否符合特定的条件。
将Phase_4中每句代码的作用解释如下所示。
000000000040100c <phase_4>:
40100c: 48 83 ec 18 sub $0x18,%rsp //申请空间
401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx //传参,加载有效地址,将0xc(%rsp)设为num2
401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx //传参,加载有效地址,将0x8(%rsp)设为num1
40101a: be cf 25 40 00 mov $0x4025cf,%esi //scanf函数输入格式, %d %d
40101f: b8 00 00 00 00 mov $0x0,%eax
401024: e8 c7 fb ff ff callq 400bf0 <__isoc99_sscanf@plt> //调用scanf函数
401029: 83 f8 02 cmp $0x2,%eax
40102c: 75 07 jne 401035 <phase_4+0x29> //当scanf输入数据个数不等于2时,跳转至爆炸
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp)
401033: 76 05 jbe 40103a <phase_4+0x2e> //jdb:无符号小于等于跳转,当num1小于等于14时,跳转至40103a,否则爆炸,所以num1的限制条件为[0,14]
401035: e8 00 04 00 00 callq 40143a <explode_bomb>
40103a: ba 0e 00 00 00 mov $0xe,%edx //%edx=14
40103f: be 00 00 00 00 mov $0x0,%esi //%esi=0
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi //%edi=num1
401048: e8 81 ff ff ff callq 400fce <func4> //调用函数fun4,三个参数%edx,%esi,%edi
40104d: 85 c0 test %eax,%eax
40104f: 75 07 jne 401058 <phase_4+0x4c> //如果fun4返回值%eax不等于0,则跳转至爆炸,所以需要知道当num1为何值时,%eax为0
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp)
401056: 74 05 je 40105d <phase_4+0x51> //如果num2=0,则跳转至结束,否则爆炸,所以输入的第二个数据只能为0
401058: e8 dd 03 00 00 callq 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp //释放空间
401061: c3 retq
2.2.5 phase_5
phase_5程序首先会提示输入一个字符串,然后会依次对输入的每个字符进行一系列的变换操作。如果所有字符的变换结果都等于一个特定的值,那么炸弹就会被拆除;否则,炸弹就会爆炸。
第一个函数read_six_numbers()的作用是读取六个数字,并将它们存放到一个数组中。该函数会先提示玩家输入六个数字,然后通过scanf函数将这些数字存储到一个数组中。如果输入的数字不足六个或者有非法字符,那么程序就会调用explode_bomb函数,炸弹就会爆炸。 第二个函数phase_5()是整个程序的核心。该函数会依次对输入字符串中的每个字符进行变换操作,并将变换结果存储到一个新的字符串中。具体来说,该函数会先将输入字符串复制到一个新的字符串中,然后对新字符串中的每个字符进行如下变换操作:
- 对于第一个字符,将其ASCII码值加1;
- 对于第二个字符,将其ASCII码值减1;
- 对于第三个字符,将其ASCII码值加2;
- 对于第四个字符,将其ASCII码值减2;
- 对于第五个字符,将其ASCII码值加3;
- 对于第六个字符,将其ASCII码值减3。
在这个过程中,我们需要注意一些细节。首先,由于字符串是以空字符结尾的,因此需要将空字符也算在内。其次,由于变换操作可能会导致字符的ASCII码值超出合法的范围,还需要进行一些调整操作。具体来说,如果变换后的字符ASCII码值小于0,那么就将其加上0x100(即256);如果超过了0x7f,那么就将其减去0x100。 在phase_5函数执行完毕后,会得到了一个新的字符串,其中每个字符都经过了一系列的变换操作。接下来,程序会将新字符串和一个预设的字符串进行比较,如果相等,则炸弹被拆除,否则就调用explode_bomb函数,炸弹就会爆炸。 为了解决这个挑战,我们需要分析输入字符串中每个字符的变换操作,并逆推出原始的字符。具体来说,我们可以先将预设的字符串和目标字符串都转换成十六进制表示,然后对每个字符进行逆向变换操作。最终,我们得到的就是输入字符串中的原始字符。
阅读代码,发现程序在(rsp+0x18)处设置了一个金丝雀值,目的是为了防止缓冲区溢出。
程序会读取我们输入的值的长度,并与6进行比较。如果长度不为6,则会调用explode_bomb函数引爆炸弹。因此,我们需要确保输入的值的长度为6。如果输入符合要求,则程序会跳转到<phase_5+0x70>行代码。
<phase_5+0x70>行代码块如图所示,主要是将%rax设置为0,然后跳转到40108b代码行。
40108b处的代码块可以分为三个部分,分别是part1(40180b-4010ae)、part2(4010b3-4010d7)和part3(4010d9-4010f3),它们分别完成了不同的功能。
在part2部分中,代码会比较rsp+0x10位置处的值和0x40245e位置处的值。如果二者不相等,则会调用explode_bomb函数引爆炸弹。因此,rsp+0x10位置存储的值必须与0x40245e位置处的值相同。我们可以使用gdb检查0x40245e位置处的值。输入
x/s 0x40245e
可以看到该位置处的值为"flyers"。
假设输入的六个字符为a1,a2,a3,a4,a5,a6,根据我们给出的伪代码,part1对应的栈帧实际上存储的是m[0x4024b0+rdx]的值。因此,我们需要查看0x4024b0中存储的值。我们可以使用gdb调试器来查看,输入
print (char*)0x4024b0
如下所示:
将Phase_5中每句代码的作用解释如图所示。
观察可知,0x4024b0存储了一个字符串数组。结合之前的伪代码,我们可以推断出,我们传入的参数实际上是该数组的索引值,通过该索引值可以获取我们需要的“flyers”值。
根据以上分析,这一关的程序会读取我们输入的六个字符的ASCII码低四位,并以此作为索引值,在字符数组“maduiersnfotvbyl”中查找相应的字符。如果最后返回的字符为“flyers”,则我们就能通过这一关。
观察可知,字符串 "maduiersnfotvbyl" 中,字符 f 位于第 9 位,字符 l 位于第 15 位,字符 y 位于第 14 位,字符 e 位于第 5 位,字符 r 位于第 6 位,字符 s 位于第 7 位。因此,我们需要输入六个字符,使得它们的 ASCII 码低四位分别为 1001、1111、1110、0101、0110、0111。 通过查看 ASCII 表,我们可以找到对应的字符。例如,字符 a 的 ASCII 码为 01100001,因此,一种可能的解码为 ionuvw;ionefg;9?>567(答案不唯一)。
综上所述,在解决phase_5挑战的过程中需要深入理解程序的运行逻辑和各个函数的作用,从而逆向出输入字符串中每个字符的原始值。
2.2.6 phase_6
phase_6是一道比较难的反汇编题目,需要使用逆向工程的技巧来解决。
为了便于表示,将phase_6拆分成四部分,分别使用对应的C代码进行描述。
第一部分
该部分具有两层循环,说明输入的每个数字要求不大于6,且互不相同。
以上部分对应C代码:
r14 = 0;
r13 = 0;
r12d = 0;
while(1){
rbp = r13;
if(num[r13] - 1 > 5)
goto bomb;
r12d++;
if(r12d == 6)
break;
for(ebx = r12d; ebx <= 5; ebx++){
if(num[ebx] == num[rbp])
goto bomb;
}
r13++;
}
第二部分
该部分的作用为使用立即数7减去每个输入数据,覆盖原来的数据。
图2-38
以上部分对应C代码:
rsi=7;
for(rax = 0; rax != rsi; rax++)
{
num[rax] = 7 - num[rax];
}
第三部分
在gdb中输入
x/28 0x6032d0
得到:
发现最后8字节数字每次都加了16字节,类似通过指针访问下一结点,并且可以通过前面的node1、node2、node3知道这是一个链表的结点,然后访问6304480,即node1的指针,发现这个指针指向的是下一个结点 node2,类似地如果访问6304496 得到的会是node3和后续结点,由此可以推断出: 前面的 332、168、924是结点数据, 1 2 3是结点编号,最后8字节是next指针。
该链表每个结点的结构为:
struct node{
int value;
int number;
node* next;
}
得到第三部分为:
该循环根据输入数将链表中对应的第输入数个结点的地址复制到 0x20(%rsp) 开始的栈中
第四部分
因为第四部分要求:链表第一项数据 > 第二项数据 >…,根据gdb调试输入
x /28 0x6032d0
查看地址0x6032d0为:
图2-42
得node[i].value排序为:
node[3]>node[4]>node[5]>node[6]>node[1]>node[2]
因为这个顺序,是经过了numx = 0x7 - numx 则原输入数据应该是:
4 3 2 1 6 5
终端验证:
系统显示通关成功。
系统的注释详细如下:
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 //申请空间
401100: 49 89 e5 mov %rsp,%r13 //%r13=%rsp
401103: 48 89 e6 mov %rsp,%rsi //%rsi=%rsp
//4010f4~401103为保存参数,分配栈帧
401106: e8 51 03 00 00 callq <read_six_numbers> //输入6个数,调用的结果是调用者的栈上按顺序存储输入的6个数
40110b: 49 89 e6 mov %rsp,%r14 //%r14=%rsp
40110e: 41 bc 00 00 00 00 mov $0x0,%r12d //%r12d=0 %r12d当做数组索引,类似i=0
401114: 4c 89 ed mov %r13,%rbp //初始 %rbp=%r13=%rsp
401117: 41 8b 45 00 mov 0x0(%r13),%eax //%eax=num[i]
40111b: 83 e8 01 sub $0x1,%eax
40111e: 83 f8 05 cmp $0x5,%eax
401121: 76 05 jbe 401128 <phase_6+0x34> //无符号数比较,说明num为无符号数,即大于等于0,40111b~401121 num[i]-1<=5,所以num[i]<=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>
401132: 44 89 e3 mov %r12d,%ebx //401128~401132 退出大循环的条件:6个数字全部遍历到
401135: 48 63 c3 movslq %ebx,%rax
401138: 8b 04 84 mov (%rsp,%rax,4),%eax
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
401148: 83 fb 05 cmp $0x5,%ebx
40114b: 7e e8 jle 401135 <phase_6+0x41> //401145~40114b 小循环,判断数组元素是否相等
40114d: 49 83 c5 04 add $0x4,%r13
401151: eb c1 jmp 401114 <phase_6+0x20>// 40114d~401151 大循环,每次将%r13加4,之后回到401114,%r13赋给了%eax
//第二部分
401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi //0x18=24,刚好为6个int型数据所占字节,将 %rsi 指向栈中跳过读入数据位置作为结束标记
401158: 4c 89 f0 mov %r14,%rax //%rax=%r14=%rsp (%rax)存放输入数
40115b: b9 07 00 00 00 mov $0x7,%ecx //%ecx=7
401160: 89 ca mov %ecx,%edx //%edx=%ecx=7
401162: 2b 10 sub (%rax),%edx //7-(%rax)=7-(%r14) 立即数7减去 %r14 指向的数据
401164: 89 10 mov %edx,(%rax) //将7减的结果存回 %r14 执行的内存单元
401166: 48 83 c0 04 add $0x4,%rax // %rax 指向下一个输入数
40116a: 48 39 f0 cmp %rsi,%rax // 比较是否达到输入数组的末尾
40116d: 75 f1 jne 401160 <phase_6+0x6c>
//第三部分
40116f: be 00 00 00 00 mov $0x0,%esi //将 %rsi 置0
401174: eb 21 jmp 401197 <phase_6+0xa3> //跳转至401197
401176: 48 8b 52 08 mov 0x8(%rdx), %rdx //将0x8(%rdx)指向内存单元的内容(即下一结点的指针值)复制到%rdx,指向链表下一个元素
40117a: 83 c0 01 add $0x1, %eax //将%eax加1
40117d: 39 c8 cmp %ecx, %eax //比较%ecx和%eax是否相等
40117f: 75 f5 jne 401176 <phase_6+0x82> //不相等,继续遍历链表 【【最终%rdx指向链表的第%ecx个节点】】
401181: eb 05 jmp 401188 <phase_6+0x94>
401183: ba d0 32 60 00 mov $0x6032d0, %edx //重置链表首地址,%edx存放链表首结点地址
401188: 48 89 54 74 20 mov %rdx, 0x20(%rsp,%rsi,2) //(%rsp+32+%rsi*2)=%rdx
40118d: 48 83 c6 04 add $0x4, %rsi //%rsi=%rsi+4
401191: 48 83 fe 18 cmp $0x18, %rsi
401195: 74 14 je 4011ab <phase_6+0xb7> //当%rsi=24时,跳转至4011ab
401197: 8b 0c 34 mov (%rsp,%rsi,1), %ecx //将(%rsp+%rsi)指向的数据复制到%ecx,%ecx存放输入数据
40119a: 83 f9 01 cmp $0x1, %ecx //比较%ecx是否小于等于1
40119d: 7e e4 jle 401183 <phase_6+0x8f> //若%ecx小于等于1,跳转(因为%ecx代表结点,结点标号从1开始,所以输入数据的范围为[1,6])
//即%ecx=1时,%edx存放链表首结点地址
40119f: b8 01 00 00 00 mov $0x1, %eax //若%ecx>1,则%eax=1
4011a4: ba d0 32 60 00 mov $0x6032d0, %edx //%edx存放链表首结点地址
4011a9: eb cb jmp 401176 <phase_6+0x82>
//第四部分
4011ab: 48 8b 5c 24 20 mov 0x20(%rsp), %rbx //将(%rsp+32)的链表节点地址复制到%rbx
4011b0: 48 8d 44 24 28 lea 0x28(%rsp), %rax //将%rax指向栈中下一个链表结点的地址(%rsp+40)
4011b5: 48 8d 74 24 50 lea 0x50(%rsp), %rsi //将%rsi指向保存的链表节点地址的末尾(%rsp+80)
4011ba: 48 89 d9 mov %rbx, %rcx
4011bd: 48 8b 10 mov (%rax), %rdx
4011c0: 48 89 51 08 mov %rdx, 0x8(%rcx) //将栈中指向的后一个节点的地址复制到前一个节点的next指针位置
4011c4: 48 83 c0 08 add $0x8, %rax //移动到下一个节点
4011c8: 48 39 f0 cmp %rsi, %rax //判断6个节点是否遍历完毕
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) //末尾链表next为NULL则设置为0x0
//该循环按照7减去输入数据的索引重新调整链表
4011d9: 00
4011da: bd 05 00 00 00 mov $0x5, %ebp
4011df: 48 8b 43 08 mov 0x8(%rbx), %rax //将%rax指向%rbx下一个链表结点
4011e3: 8b 00 mov (%rax), %eax
4011e5: 39 03 cmp %eax, (%rbx) //比较链表结点中第一个字段值的大小,如果前一个节点值大于后一个节点值,跳转
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 //将%rbx向后移动,指向栈中下一个链表节点的地址
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 %r13 //释放空间
401203: c3 retq
2.2.7 Secret_phase
在bomb.c中存在这样一段话:“
/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */”
说明这个炸弹之中还有一个隐藏关卡,寻找进入secret_phase 的入口
进入bomb.asm中发现了如下汇编代码:
需要找到 secret_phase 函数的入口,也就是调用了 secret_phase 的函数。在 bomb.asm 文件中搜索,发现 phase_defused 函数调用了 secret_phase 函数。而在 bomb.c 文件中,每个 phase 后面都有一个 phase_defused 函数调用。因此可以通过分析 phase_defused 函数来找到调用 secret_phase 函数的位置。
对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>
//num_input_strings 表示我们已经输入了多少串字符串了,判断是否等于6,
//如果不等于6,直接跳转到最下方,则secret_phase无法进入
//所以进入secret_phase的则先决条件是:完成phase 1 - 6
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
4015f5: bf 70 38 60 00 mov $0x603870,%edi
4015fa: e8 f1 f5 ff ff callq 400bf0 <__isoc99_sscanf@plt> //调用sscanf函数
4015ff: 83 f8 03 cmp $0x3,%eax //判断返回值%eax是否等于3
401602: 75 31 jne 401635 <phase_defused+0x71> //如果返回值%eax不等于3的话,则跳转到最下方,跳过了401630 callq 401242 <secret_phase>
//即secret_phase无法进入,所以必须要让sscanf函数的返回值为3
401604: be 22 26 40 00 mov $0x402622,%esi //%esi=0x402622
401609: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
40160e: e8 25 fd ff ff callq 401338 <strings_not_equal> //调用字符串比较函数,判断输入的字符串和%esi中存的字符串是否相等
401613: 85 c0 test %eax,%eax
401615: 75 1e jne 401635 <phase_defused+0x71> //若相等,则跳转至401635
//(gdb) print (char*) 0x402622 得到字符串 DrEvil
//以下的代码不影响解码,暂且不做分析
401617: bf f8 24 40 00 mov $0x4024f8,%edi
40161c: e8 ef f4 ff ff callq 400b10 <puts@plt>
401621: bf 20 25 40 00 mov $0x402520,%edi
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
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
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
在4015fa行的代码中,我们可以观察到调用了sscanf函数,它的作用是格式化读取指定的字符串。在调用sscanf函数之前,代码使用了两条mov语句,这两个参数分别是指定的字符串和格式化读取字符串。
根据代码可以猜测,我们需要输入两个数字。为了查看这几个参数对应的字符串,我们可以使用GDB调试器。我们可以先输入之前完成的字符串,并在0x4015fa处设置断点,最后查看断点处的参数。在gdb试探性输入print (char*)0x402619和print (char*)0x603870。
得到格式化字符串 %d %d %s ,而7 0就是phase 4的解码,联系sscanf函数的返回值%eax需要等于3,可以猜想需要在7 0 后面再输入一串字符串,即可进入隐藏关卡。
对phase_defused进行分析发现,在401604行出现了一个奇怪的地址为0x402622,在gdb输入print (char*)0x402622进行解析
为了进入隐藏关卡,我们需要在第四关的解码7 0后面加上字符串"DrEvil"。每次输入密钥可能会很繁琐,因此可以通过创建名为"bomb_idea.txt"的文件来存储所有的拆弹密码如下
并使用命令
./bomb bomb_idea.txt
来运行可执行文件。如果每一关调试结束后,我们可以将新的拆弹密码写入".txt"文件,这样就可以通过验证是否爆炸来避免重复输入之前关卡的拆弹密码。
系统提示成功找到了secret phase!
开始分析secret_phase内容:
Secret_phase汇编代码的解释内容如下:
0000000000401242 <secret_phase>:
401242: 53 push %rbx
401243: e8 56 02 00 00 callq 40149e <read_line> //调用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函数,将字符串转换为整型数据num,存在%rax中
40125a: 48 89 c3 mov %rax,%rbx //%rbx=%rax=num
40125d: 8d 40 ff lea -0x1(%rax),%eax
401260: 3d e8 03 00 00 cmp $0x3e8,%eax
401265: 76 05 jbe 40126c <secret_phase+0x2a> //num-1>1000(0x3e8),则会爆炸,所以输入的数字必须小于等于1001
401267: e8 ce 01 00 00 callq 40143a <explode_bomb>
40126c: 89 de mov %ebx,%esi //%esi=%ebx=num %esi存放输入的数据num,作为参数代入fun7
40126e: bf f0 30 60 00 mov $0x6030f0,%edi //%edi=6030f0 作为参数代入fun7
401273: e8 8c ff ff ff callq 401204 <fun7> //调用函数fun7
401278: 83 f8 02 cmp $0x2,%eax //将fun7的返回值%eax与2比较
//因为fun7为调用phase_defusd之前最后调用的一个函数,所以如果%eax=2,则跳过炸弹,拆弹成功!
//所以需要对fun7进行分析
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 //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
由于 fun7 函数是调用 phase_defused 函数之前的最后一个函数,因此如果 fun7 函数的返回值 %eax = 2,那么炸弹就会被跳过,拆弹成功。因此需要对 fun7 函数进行分析。首先阅读 fun7 函数的源代码。
在gdb输入下列指令进行解析
x/150 0x6030f0
首先,查看0x6030f0中存放的数据,发现它类似于phase 6中的结构体。在6304xxx地址处应该是一个指针。同时,我们意外地发现,phase 6的指针数组就在下方。这里实际上是一个带有两个指针的结构体。前面的7个结构体的两个指针都是有值的,它们指向其他的结构体。而最后8个结构体的指针是没有值的,只有头部数据。这些指针所指的数据结构是一个二叉树。
fun7函数的逻辑较为复杂,为了便于之后的分析,将其转换为C语言的形式展示如下所示。
int func7(Type *p, int input)
{
if(p == NULL)
return -1;
if(&p <= input)
{
if(&p == input)
return 0;
else
{
p = p + 0x10;
int n = func7(p, input);
return 2 * n + 1;
}
}
else
{
p = p + 0x8;
int n = func7(p, input);
return 2 * n;
}
}
需要得到返回值 %eax=2,说明递归顺序为:
1.最底层得到0 return 0
2.向上经过一层 %eax = %eax*2 + 1 得到1 return 1
3.再向上经过一层% eax = %eax*2 得到2 return 2
p所指向的数据结构是二叉搜索树,该树的结构为p = p + 0x10是加载右结点,p = p + 0x8是加载左结点。返回路径如下:
分析可得顺推思路:
1.首先,我们来到二叉树的首地址0x6030f0,对应的数据为36。因为36需要大于x,才能使得%eax = %eax*2成立。因此,指针值应该是%rdi + 8,即加载左结点。指针值为6304016,查看得到值为8。
2.在前文提到的分析过程中,需要注意节点 8 对应的位置。根据题目要求,需要让 %eax 的值乘 2 后再加 1,因此 8 的值需要小于等于 x。根据代码逻辑,我们需要加载右子节点,因此指针值为 0x603110 + 16,即 6304080。通过查看该位置内存的值,我们可以得到节点 8 的值为 22。因此可以推断出,对于输入的 x 值,当 x 大于等于 11 时,答案为 x*2+1;当 x 小于 11 时,答案为 fun7(0x6030f0, x)。
3.最后我们得到了数据22,当我们输入22的时候,因为和指针所处位置对应头部数据的值相等,所以%eax = 0。
因此22为可行解,如下。
在查看22对应的位置时,我们发现该位置还有两个指针,并且不是空指针。我们猜想,如果22大于所需解码,返回值为%eax = %eax*2,同样符合要求。那么指针值应该为0x603150 + 8,即加载左结点。指针值为6304368,查看得到值为20。然而,该位置指针为空,不再继续指向下一结点。因此,20也是一个可行解。
终端验证:
在bomb_idea.txt文件末尾添加20 22如下:
在终端输入
./bomb bomb_idea.txt
系统显示全部关卡通关成功。
2.3 实验结果
以上代码均存储在bomb_idea.txt文件中,每行代表对应的关卡,各阶段密钥如下所示:
在终端输入
./bomb result.txt
显示全部通关。
2.4 实验体会
通过CSAPP的bomblab实验,我深刻认识到了计算机系统的安全问题、解决问题的能力以及汇编语言的重要性。在实验过程中需要分析程序的汇编代码,了解程序的运行原理和逻辑,并找到程序中的安全漏洞和陷阱。在这个过程中,我发现了许多计算机系统中存在的安全问题并且卡了很久的时间,问题包括缓冲区溢出、格式化字符串漏洞、栈溢出等等。我参考了很多博客和B站视频尽可能多的弄懂其中的原理,从而对计算机系统的安全问题有了更深入的认识,并学会了如何保护计算机系统的安全。
在解决问题的过程中,需要思考问题的本质、分析问题的原因,并采取有效的解决措施,编程技能和汇编语言的理解。在实验中,我需要分析程序的汇编代码,掌握各种调试工具和技术,并使用汇编语言编写解决方案。这让我更深入地了解了汇编语言的运行原理和逻辑。通过这个实验,我不仅学会了如何使用汇编语言编写程序,还掌握了很多关于汇编语言的知识和技能。
综上所述,通过bomblab实验不仅让我更好地理解了计算机系统的安全问题,还提高了我的解决问题的能力和编程技能。我相信,通过不断地学习和实践,我能够更好地应对计算机系统的安全问题,提高我的编程技能和汇编语言的理解。