深入理解计数机系统Bomblab实验报告(超详细)
前言
经过datalab的洗礼后,然后就迫不及待地开始上手bomblab啦,但是我万万没想到啊,这一个实验居然需要用到汇编的知识还要求学会调试器gdb的使用
可是这么点困难这么能湮灭心中的的热情呢,于是我打开 国内著名大学 B站 找了一个又一个的gdb教程,然而这些教程还!不!够!😭
无奈只能在积灰已久的收藏夹中翻出了那个陪伴我失眠的夜老爷子亲传 (助眠) 课程,于是翻到了以前很难理解的部分,发现有了一定的计算机基础知识作铺垫在加上刚学的gdb入门知识再去理解课程就会逐渐顺畅起来😆
于是,肝过视频后就迫不及待地开始实验啦 ~
实验准备
这次的实验几乎不需要环境搭建(可能是我的Linux主机已经安装好了gdb的原因吧)需要用到的前置知识则要比datelab还是更专精些的,下面给刚刚新手推荐的一些前置知识讲解的视频供大家参考:
【精校中英字幕】2015 CMU 15-213 CSAPP 深入理解计算机系统 Lecture5~6
还有就是后知后觉发现真的非常不错的gdb学习文档
然后阅读一下bomb.c文件,可以发现老爷子在为我们构造一个邪恶博士的故事模型帮助我们代入到其中一共有6个关卡,然而除此之外并没有其他线索🤣
正式实验
phase_1
-
先用
gdb将程序运行起来gdb ./bomb -
然后为程序在
phase_1处设置一下断点break phase_1(然后你会发现这意义并不大🤣) -
然后对这个函数进行反编译
disassemble phase_1就可以得到下面的输出1
2
3
4
5
6
7
8
9
10Dump of assembler code for function phase_1:
0x0000000000400ee0 <+0>: sub $0x8,%rsp
0x0000000000400ee4 <+4>: mov $0x402400,%esi
0x0000000000400ee9 <+9>: callq 0x401338 <strings_not_equal>
0x0000000000400eee <+14>: test %eax,%eax
0x0000000000400ef0 <+16>: je 0x400ef7 <phase_1+23>
0x0000000000400ef2 <+18>: callq 0x40143a <explode_bomb>
0x0000000000400ef7 <+23>: add $0x8,%rsp
0x0000000000400efb <+27>: retq
End of assembler dump. -
第一关还是比较好分析的
-
sub $0x8,%rsp是对栈状态寄存器减去一个值,属于栈操作与主要逻辑无关 -
mov $0x402400,%esi是将一个固定地址的下的内容复制到寄存器%esi下 -
callq 0x401338 <strings_not_equal>这里调用了一个子程序看程序名可知是一个字符串判等的函数,于是我们知道了需要输入的值其实是一个字符串 -
test %eax,%eax由寄存器%eax用于存储程序调用的返回值,而整个指令来看就是判断strings_not_equal函数的返回值是否为0 -
je 0x400ef7 <phase_1+23>这个是一个无条件的跳转指令,结合其跳转的位置可知执行到这就跳出函数了 -
callq 0x40143a <explode_bomb>同样是调用了一个函数,有函数名可知是触发爆炸的程序 -
add $0x8,%rsp同样的栈操作 -
retq这里直接就是函数出口了 -
经过上面的分析我们就很容易得到了,这就是一个简单的判断字符是否相等的函数而已,需要避免爆炸(执行到
explode_bomb函数)还要字符相同即可,于是我们打印出0x402400地址处的字符串值于是就有:1
2(gdb) print (char*)0x402400
$1 = 0x402400 "Border relations with Canada have never been better."
-
-
于是第一关的解码字符串为
Border relations with Canada have never been better.
phase_2
-
同样我们查看一下第二关的汇编
disassemble phase_21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27Dump of assembler code for function phase_2:
0x0000000000400efc <+0>: push %rbp
0x0000000000400efd <+1>: push %rbx
0x0000000000400efe <+2>: sub $0x28,%rsp
0x0000000000400f02 <+6>: mov %rsp,%rsi
0x0000000000400f05 <+9>: callq 0x40145c <read_six_numbers>
0x0000000000400f0a <+14>: cmpl $0x1,(%rsp)
0x0000000000400f0e <+18>: je 0x400f30 <phase_2+52>
0x0000000000400f10 <+20>: callq 0x40143a <explode_bomb>
0x0000000000400f15 <+25>: jmp 0x400f30 <phase_2+52>
0x0000000000400f17 <+27>: mov -0x4(%rbx),%eax
0x0000000000400f1a <+30>: add %eax,%eax
0x0000000000400f1c <+32>: cmp %eax,(%rbx)
0x0000000000400f1e <+34>: je 0x400f25 <phase_2+41>
0x0000000000400f20 <+36>: callq 0x40143a <explode_bomb>
0x0000000000400f25 <+41>: add $0x4,%rbx
0x0000000000400f29 <+45>: cmp %rbp,%rbx
0x0000000000400f2c <+48>: jne 0x400f17 <phase_2+27>
0x0000000000400f2e <+50>: jmp 0x400f3c <phase_2+64>
0x0000000000400f30 <+52>: lea 0x4(%rsp),%rbx
0x0000000000400f35 <+57>: lea 0x18(%rsp),%rbp
0x0000000000400f3a <+62>: jmp 0x400f17 <phase_2+27>
0x0000000000400f3c <+64>: add $0x28,%rsp
0x0000000000400f40 <+68>: pop %rbx
0x0000000000400f41 <+69>: pop %rbp
0x0000000000400f42 <+70>: retq
End of assembler dump. -
前面一些框架性的东西就不需要重复了,这个也显然和先前的线性思维的程序不一样,所以我们按程序运行的顺序进行拆解
-
callq 0x40145c <read_six_numbers>根据函数名猜测我们要输入的应该是6个数字,我们进入read_six_numbers查看一下这个函数的汇编1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
170x000000000040145c <+0>: sub $0x18,%rsp
0x0000000000401460 <+4>: mov %rsi,%rdx
0x0000000000401463 <+7>: lea 0x4(%rsi),%rcx
0x0000000000401467 <+11>: lea 0x14(%rsi),%rax
0x000000000040146b <+15>: mov %rax,0x8(%rsp)
0x0000000000401470 <+20>: lea 0x10(%rsi),%rax
0x0000000000401474 <+24>: mov %rax,(%rsp)
0x0000000000401478 <+28>: lea 0xc(%rsi),%r9
0x000000000040147c <+32>: lea 0x8(%rsi),%r8
0x0000000000401480 <+36>: mov $0x4025c3,%esi
0x0000000000401485 <+41>: mov $0x0,%eax
0x000000000040148a <+46>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x000000000040148f <+51>: cmp $0x5,%eax
0x0000000000401492 <+54>: jg 0x401499 <read_six_numbers+61>
0x0000000000401494 <+56>: callq 0x40143a <explode_bomb>
0x0000000000401499 <+61>: add $0x18,%rsp
0x000000000040149d <+65>: retq聚焦
< +41 ~ +56>这段会发现当我们的输入小于6个数字时程序也会发生爆炸 -
然后我们回到原函数的
<+14 ~ +20>这里,我们可以看到首先是(%rsp)和1的一个差关系(cmpl)如果为0则跳转到0x400f30地址处对应了<+52>处(je),否则就会爆炸 -
我们看到
< +52 ~ +57>这里实际上就是将%rsp往后4字节暂存到%rbx中,而4字节就是一个int,所以这里就是将%rsp的下一个int放到%rbx中去,同样的%rbp只是暂存了后面32个字节即%rsp往后6个int -
然后
<+62>其实就是一个简单的无条件跳转指令跳转到的地址对应我们<+27> -
我们看到
<+27 ~ +30>将%rbx的前一个int存入%eax,由上面的分析可知%eax存的值就是1,然后%eax就作了一个倍增,于是%eax中存的值就是2 -
继续往下分析
<+32 ~ +36>这一步其实和前面的很相似就是对%eas和(%rbx)判等,如果不等就会爆炸,于是下一个值就是2 -
然后就是
<+41 ~ +50>这里对%rbx往后移了一个int距离,然后又是判断了%rbp和%rbx是否相等,如果不相等就会跳回到<+27>,否则会跳转到<+64>往后就是程序结束释放栈空间了 -
于是就可以得到这其实就是一个循环,每次循环
$eax都倍增,然后初始还是1所以就可以得到答案序列1 2 4 8 16 32
-
-
测试后发现可以通过,故这一关的答案序列就是
1 2 4 8 16 32
phase_3
-
先上汇编
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38Dump of assembler code for function phase_3:
=> 0x0000000000400f43 <+0>: sub $0x18,%rsp
0x0000000000400f47 <+4>: lea 0xc(%rsp),%rcx
0x0000000000400f4c <+9>: lea 0x8(%rsp),%rdx
0x0000000000400f51 <+14>: mov $0x4025cf,%esi
0x0000000000400f56 <+19>: mov $0x0,%eax
0x0000000000400f5b <+24>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000400f60 <+29>: cmp $0x1,%eax
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39>
0x0000000000400f65 <+34>: callq 0x40143a <explode_bomb>
0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp)
0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106>
0x0000000000400f71 <+46>: mov 0x8(%rsp),%eax
0x0000000000400f75 <+50>: jmpq *0x402470(,%rax,8)
0x0000000000400f7c <+57>: mov $0xcf,%eax
0x0000000000400f81 <+62>: jmp 0x400fbe <phase_3+123>
0x0000000000400f83 <+64>: mov $0x2c3,%eax
0x0000000000400f88 <+69>: jmp 0x400fbe <phase_3+123>
0x0000000000400f8a <+71>: mov $0x100,%eax
0x0000000000400f8f <+76>: jmp 0x400fbe <phase_3+123>
0x0000000000400f91 <+78>: mov $0x185,%eax
0x0000000000400f96 <+83>: jmp 0x400fbe <phase_3+123>
0x0000000000400f98 <+85>: mov $0xce,%eax
0x0000000000400f9d <+90>: jmp 0x400fbe <phase_3+123>
0x0000000000400f9f <+92>: mov $0x2aa,%eax
0x0000000000400fa4 <+97>: jmp 0x400fbe <phase_3+123>
0x0000000000400fa6 <+99>: mov $0x147,%eax
0x0000000000400fab <+104>: jmp 0x400fbe <phase_3+123>
0x0000000000400fad <+106>: callq 0x40143a <explode_bomb>
0x0000000000400fb2 <+111>: mov $0x0,%eax
0x0000000000400fb7 <+116>: jmp 0x400fbe <phase_3+123>
0x0000000000400fb9 <+118>: mov $0x137,%eax
0x0000000000400fbe <+123>: cmp 0xc(%rsp),%eax
0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134>
0x0000000000400fc4 <+129>: callq 0x40143a <explode_bomb>
0x0000000000400fc9 <+134>: add $0x18,%rsp
0x0000000000400fcd <+138>: retq
End of assembler dump. -
先整体看一下这里的汇编代码,可以发现在
<+57 ~ +104>之间有很多的赋值和跳转语句而且所有的跳转都是跳转到<+123>这个比较语句的,这个语句结束后程序就结束了,根据老爷子的上课内容(机器水平编程第2节)猜测这个可能是一个switch case语句然后一个if语句就结束了,然后我们具体分析:-
<+4, +9>将%rsp + 0x8和%rsp + 0xc暂存到两个寄存器中但是通读过后没有发现这两个寄存器有啥用啊,难道是老爷子的烟雾弹?(黑人问号) -
<+14 ~ +19>两个赋值语句,%eax置零还是比较好理解的,然而同样的我们发现%esi在后面的语句中也没有用到它的地方,为了保险起见我们还是打印一下这里出现的值😟1
2(gdb) p 0x4025cf
$1 = 4203983 -
然后就是一个读取数据的函数了,但是这次没有带有明确的函数名标识的包装我们就连要输入啥也不知道啊😅
-
然后查了一下,原来是被我一直忽略的
%esi啊😂ESI/EDI 分别叫做"源/目标索引寄存器"(source/destinationindex),因为在很多字符串操作指令中,DS:ESI指向源串,而ES:EDI指向目标串.
-
于是调整打印格式就得到了下面的结论,也就是我们需要输入两个整数
1
2(gdb) p (char *)0x4025cf
$3 = 0x4025cf "%d %d" -
然后
<+29 ~ +34>是值当%eax的值小于1也就是输入小于2的时候就会爆炸 -
<+39 ~ +44>判断%rsp+0x8了中存的数据是否大于7,大于的话会跳转到<+106>也就是会爆炸,于是猜测这里的%rsp+0x8应该是存放了第一个输入的参数 -
<+46>就是在将%rsp + 0x8的地址存入%eax -
<+50>很可惜的是我们并不知道%rax的值,但是我们已经知道了这个跳转一定是到这个赋值语句里面去然后跳转到<+123>于是我们先确定了第一个数必须小于7,为了方便我们就直接去为0(然后第二个随便取毕竟输入小于2个数字也会爆炸),我们直接打一个断点在<+123>处1
2(gdb) break *0x0000000000400fbe
Breakpoint 2 at 0x400fbe -
<+123 ~ +129>比较了%rsp+0xc中存的数据与%eax的大小关系,如果不相等就会爆炸,否则就是正常退出,所以我们可以知道了%rsp+0xc中存的应该是我们输入的第二个值,我们已经知道了在前面的赋值时%eax会根据%rsp+0x8中的数据跳转到不同的赋值语句中,所以我们就直接打印出现在的%eax就可以了,然后在下一次输入的时候更改为对应的值就好啦1
2
3Breakpoint 2, 0x0000000000400fbe in phase_3 ()
(gdb) print (int)$eax
$1 = 207
-
-
于是就可以得到一组可行的解
0 207,经测试后通过
phase_4
-
汇编如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25Dump of assembler code for function phase_4:
0x000000000040100c <+0>: sub $0x18,%rsp
0x0000000000401010 <+4>: lea 0xc(%rsp),%rcx
0x0000000000401015 <+9>: lea 0x8(%rsp),%rdx
0x000000000040101a <+14>: mov $0x4025cf,%esi
0x000000000040101f <+19>: mov $0x0,%eax
0x0000000000401024 <+24>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000401029 <+29>: cmp $0x2,%eax
0x000000000040102c <+32>: jne 0x401035 <phase_4+41>
0x000000000040102e <+34>: cmpl $0xe,0x8(%rsp)
0x0000000000401033 <+39>: jbe 0x40103a <phase_4+46>
0x0000000000401035 <+41>: callq 0x40143a <explode_bomb>
0x000000000040103a <+46>: mov $0xe,%edx
0x000000000040103f <+51>: mov $0x0,%esi
0x0000000000401044 <+56>: mov 0x8(%rsp),%edi
0x0000000000401048 <+60>: callq 0x400fce <func4>
0x000000000040104d <+65>: test %eax,%eax
0x000000000040104f <+67>: jne 0x401058 <phase_4+76>
0x0000000000401051 <+69>: cmpl $0x0,0xc(%rsp)
0x0000000000401056 <+74>: je 0x40105d <phase_4+81>
0x0000000000401058 <+76>: callq 0x40143a <explode_bomb>
0x000000000040105d <+81>: add $0x18,%rsp
0x0000000000401061 <+85>: retq
End of assembler dump. -
整体看上去应该是一个一些
if else组成的直线型结构,然后我们具体来看:-
<+14 ~ +32>有了前一个实验的铺垫这里就直接一起分析啦,首先打印出源目标索引里面的值,可以发现是两个整数;然后这里的要求应该是严格了一点如果输入值不是两个就会跳转到声明爆炸的那一行1
2(gdb) p (char*)0x4025cf
$2 = 0x4025cf "%d %d" -
<+34 ~ +41>这里就是判断了%rsp+0x8内中存的数据是否小于或等于0xe也就是14,如果不是就会爆炸 -
<+46 ~ +56>就是一些简单的赋值,将%edx置为0xe(14),%esi置为0而%edi存储了刚才的%rsp+0x8 -
<+60 ~ +67>然后就是这里声明了一个函数无法从函数名得到其功能,但是通过<+65>可知函数的返回值要是0,所以我们对这个函数也进行反编译1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25Dump of assembler code for function func4:
0x0000000000400fce <+0>: sub $0x8,%rsp
0x0000000000400fd2 <+4>: mov %edx,%eax
0x0000000000400fd4 <+6>: sub %esi,%eax
0x0000000000400fd6 <+8>: mov %eax,%ecx
0x0000000000400fd8 <+10>: shr $0x1f,%ecx
0x0000000000400fdb <+13>: add %ecx,%eax
0x0000000000400fdd <+15>: sar %eax
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx
0x0000000000400fe2 <+20>: cmp %edi,%ecx
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36>
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx
0x0000000000400fe9 <+27>: callq 0x400fce <func4>
0x0000000000400fee <+32>: add %eax,%eax
0x0000000000400ff0 <+34>: jmp 0x401007 <func4+57>
0x0000000000400ff2 <+36>: mov $0x0,%eax
0x0000000000400ff7 <+41>: cmp %edi,%ecx
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57>
0x0000000000400ffb <+45>: lea 0x1(%rcx),%esi
0x0000000000400ffe <+48>: callq 0x400fce <func4>
0x0000000000401003 <+53>: lea 0x1(%rax,%rax,1),%eax
0x0000000000401007 <+57>: add $0x8,%rsp
0x000000000040100b <+61>: retq
End of assembler dump.<+4 ~ +8>简单赋值和运算%eax赋值为%edx也就是0xe(14),然后%eax减去了一个%esi也就是0这时%eax仍然为0xe,<+10>对%ecx的值进行逻辑右移31位也就是得到了%edc的符号位,得到%edc为0<+13>将%eax加上%ecx得到的结果是%eax仍然为0xe(14)<+15>将%eax算术右移一位也就是%eax = 14>>1 = 7<+17>这里我们并不知道%rax和%rsi的值所以我们打个断点在这个语句的下面,然后打印出%ecx的值就可以了(突然意识到前面的运算也不需要自己手动写直接打印出来就好了🤣)<+20 ~ +36>就是当%edi小于等于7就会跳转到<+36>也就是将%eax置0(这正是我们想要得到的),否则的话会将%edx置为(%rcx)-1然后又调用原函数(递归?)然后对%eax倍增后返回<+41 ~ +53>这个结构和上面的挺像的,当%edi大于等于7 就会跳转到程序结束,否则就将%edx置为(%rcx)+1然后继续调用原函数然后将%eax置为2 * %rax + 1结合前面两个,为了避免节外生枝(无论如何惹到递归去总不会是好主意🤣)其实当
%edi正好为7的时候就可以令%eax为0返回了,又%edi的值是%rsp + 0x8所以其值就是7 -
<+69 ~ +76>这里就是一个简单的比较若%rsp + 0xc的值不为0就会爆炸,否则就顺利退出了
-
-
总结可知正确结果(起码是一种正确的结果)就是
7 0,测试通过
phase_5
-
汇编如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40Dump of assembler code for function phase_5:
0x0000000000401062 <+0>: push %rbx
0x0000000000401063 <+1>: sub $0x20,%rsp
0x0000000000401067 <+5>: mov %rdi,%rbx
0x000000000040106a <+8>: mov %fs:0x28,%rax
0x0000000000401073 <+17>: mov %rax,0x18(%rsp)
0x0000000000401078 <+22>: xor %eax,%eax
0x000000000040107a <+24>: callq 0x40131b <string_length>
0x000000000040107f <+29>: cmp $0x6,%eax
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112>
0x0000000000401084 <+34>: callq 0x40143a <explode_bomb>
0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112>
0x000000000040108b <+41>: movzbl (%rbx,%rax,1),%ecx
0x000000000040108f <+45>: mov %cl,(%rsp)
0x0000000000401092 <+48>: mov (%rsp),%rdx
0x0000000000401096 <+52>: and $0xf,%edx
0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1)
0x00000000004010a4 <+66>: add $0x1,%rax
0x00000000004010a8 <+70>: cmp $0x6,%rax
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41>
0x00000000004010ae <+76>: movb $0x0,0x16(%rsp)
0x00000000004010b3 <+81>: mov $0x40245e,%esi
0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi
0x00000000004010bd <+91>: callq 0x401338 <strings_not_equal>
0x00000000004010c2 <+96>: test %eax,%eax
0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119>
0x00000000004010c6 <+100>: callq 0x40143a <explode_bomb>
0x00000000004010cb <+105>: nopl 0x0(%rax,%rax,1)
0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119>
0x00000000004010d2 <+112>: mov $0x0,%eax
0x00000000004010d7 <+117>: jmp 0x40108b <phase_5+41>
0x00000000004010d9 <+119>: mov 0x18(%rsp),%rax
0x00000000004010de <+124>: xor %fs:0x28,%rax
0x00000000004010e7 <+133>: je 0x4010ee <phase_5+140>
0x00000000004010e9 <+135>: callq 0x400b30 <__stack_chk_fail@plt>
0x00000000004010ee <+140>: add $0x20,%rsp
0x00000000004010f2 <+144>: pop %rbx
0x00000000004010f3 <+145>: retq
End of assembler dump. -
整体看过之后就会感觉是一个反复横跳的循环体,特别是看到这里的
<+66 ~ +74>就会发现感觉起来是个for循环了😆,我们整体分析来看:-
<+5 ~ +22>是一些比较简单的赋值,为了根据前面的经验并不是每一个寄存器里面的值最后都用到了,所有我们就不一一分解开讲啦;值得注意的是<+22>行的这个语句就是对自己取异或就相当于对%eax置0而已 -
<+24 ~ +34>然后声明了一个函数string_length根据函数名可以得到这个大概就是一个返回字符串长度的函数,我们为了以防万一还是将其汇编大致过一下:1
2
3
4
5
6
7
8
9
10
11
12
13Dump of assembler code for function string_length:
0x000000000040131b <+0>: cmpb $0x0,(%rdi)
0x000000000040131e <+3>: je 0x401332 <string_length+23>
0x0000000000401320 <+5>: mov %rdi,%rdx
0x0000000000401323 <+8>: add $0x1,%rdx
0x0000000000401327 <+12>: mov %edx,%eax
0x0000000000401329 <+14>: sub %edi,%eax
0x000000000040132b <+16>: cmpb $0x0,(%rdx)
0x000000000040132e <+19>: jne 0x401323 <string_length+8>
0x0000000000401330 <+21>: repz retq
0x0000000000401332 <+23>: mov $0x0,%eax
0x0000000000401337 <+28>: retq
End of assembler dump.还是挺简单的,
<+0>上来就判断了是否是字符结尾如果是就直接返回0跳出,然后就是一个循环找字符结尾就行了 由于较简单就不细究了然后就是判断返回值是否为
0x6如果不是就会爆炸否则就直接跳到<+112>看到这里我兴奋了一切就要结束了,谁想后面还有一个跳回来的
jmp🤣 -
<+112 ~ +117>这里就是将%eax置0然后就是一个无条件跳转到<+41> -
<+41 ~ +62>一大串的赋值和运算,同样的我们暂时不关心等用到这些寄存器内的值在返回看在哪里发生了修改 -
<+66 ~ +74>在最开头我们说了这个很像一个for循环的过程,当我们跳出循环的时候也就是当%rax等于0x6的时候就是要跳出的时候;我们可以注意到这里的0x6在最早判断字符串长度的时候就出现过,所以猜测这里的循环应该就是和要求我们输入的字符串相关的 -
<+76 ~ +100>这一整段就和第一关非常相似了,我们直接打印出0x40245e内的去看一下就行了1
2(gdb) p (char*)0x40245e
$1 = 0x40245e "flyers"(偷偷去测了一下输入
flyers会不会就过了,结果显然 BOOM 了😂) -
<+119 ~ +135>这里就是,这里是将我们的输入值装入%rax然后来了个异或,我们之前已经知道了当异或的两个值相同的话就会返回0,而当结果为0时就会跳过一个貌似和栈相关的函数结束函数,同样的为了减少麻烦我们尽量凑到跳过这个和栈相关的函数
-
-
一通操作分析下来结果我们连输入存在了哪里我们都还不知道呢😂,但是我们已经发现了整个程序里面最重要的应该就是在那个循环语句里面了,而这个语句应该就是按字符修改过我们的字符串了的,我们返回去看一下赋值和修改过数据的这一部分
-
<+5 ~ +17>这一段唯一难以理解的就是%fs:0x28语句了,查找过资料后发现这原来是一个随机值😅,然后<+5>这里的寄存器%rdi总感觉在哪里见过,翻到前面可以看到ESI/EDI 分别叫做"源/目标索引寄存器"猜测这里的%rdi会不会也是一样的作用啊指向目标索引寄存器也就是这里存了我们输入时的索引,毕竟这里接下来就要做一个判断输入字符串的长度的函数了,所以我们打个断点然后将%rdi中的内容打印出来1
2
3
4
5
6
7
8
9(gdb) break *0x000000000040106a
Breakpoint 3 at 0x40106a
(gdb) c
Continuing.
Breakpoint 3, 0x000000000040106a in phase_5 ()
(gdb) p (char*)$rdi
$3 = 0x6038c0 <input_strings+320> "abcdfe"可以发现就是我们输入的值,所以这里就是将我们输入的值存储到了
%rbx,然后下面的两个寄存器就是存储了个随机值而已没啥需要注意的😆 -
然后就是在循环里面进行的赋值和运算了
<+41 ~ +62><+41>显然一就给我整蒙了,为什么会将%rbx + %rax * 1也就是%rbx + %rax这个随机值存到%ecx中,而且这里还以一个随机值去递增后作为跳出循环的条件,起始按照正常的思维这里的%rax应该是被赋值为一个定值的而且这个定值应该会是0才正常,所以这里还是打印一下%rax的值查看一下比较好,查看果然符合我们的预期(猜测是在string_length函数运行的时候修改了🤔?)这段指令的意思就是取我们输入的字符串的每一位字符然后存到%eax中1
2
3
4
5
6
7
8(gdb) break *0x000000000040108f
Breakpoint 4 at 0x40108f
(gdb) c
Continuing.
Breakpoint 4, 0x000000000040108f in phase_5 ()
(gdb) p (int)$rax
$5 = 0<+45 ~ +48>这里是将%cl的地址装入%rdx这里的(%sp)是个临时变量?同时我们还没用到过%cl这个寄存器,所以打印出来看一下,因为还不知道这里%cl中存了什么类型的数据所以测了几次,第一次得到了int型的97敏感点的应该能猜到这个就是Ascll码中的a,所以当作字符串打印出来但发现好像是不可以的所以直接当做字符打印就行了,所以这里存的也是%ecx里面的值?1
2
3
4
5
6
7
8(gdb) stepi
0x0000000000401092 in phase_5 ()
(gdb) p $cl
$6 = 97
(gdb) p (char*)$cl
$7 = 0x61 <Address 0x61 out of bounds>
(gdb) p (char)$cl
$8 = 97 'a'<+52>这里就是将%edx的值与上0xf也就是取%edx二进制的低4位,然后又不知道%edx是个啥,所以这里再打印出来发现这个值又是a奇奇怪怪,这个值也是我们输入的第一个字符所以这个值又是在啥时候被赋值为%rbx的呢😬?1
2(gdb) p (char)$edx
$10 = 97 'a'<+55>这里的%rdx又一次地不知道是个啥,打印出来就好啦,但是好像没啥意义,然后还赋值给了刚才已经赋过值的%edx😵1
2(gdb) p $rdx
$1 = 1<+62>这里又是将一个奇怪的寄存器里的值赋给了0x10(%rsp,%rax,1)也就是16+%rsp+%rax这里的%rax就是循环的条件,根据我们编程的习惯这里的%rax应该就是我们字符串的索引了而16+%rsp+%rax就是找到字符串的地址(玄学),但是我们还是不知道%dl的值使什么,打印出其值也不知道是从何而来(这里输入的测试字符串是abcdef而一二次得到的值分别是ad)😧1
2
3
4(gdb) p (char)$dl
$16 = 97 'a'
(gdb) p (char)$dl
$17 = 100 'd' -
通读过整个循环语句并没有找到我们希望的对
%rbx修改的部分,但是这里该如何解释判等的时候出现的错误呢?真的不能理解了🤯 -
最终查找资料后发现:
%edx %dl和%rdx是同一个寄存器,%ecx %cl和%rcx是同一个寄存器 -
根据这个线索我们把前面的推导串联起来就得到了:对输入字符串的每一个字符对
0xf取与后得到的值加上0x4024b0作为地址再到对应的字符串再赋值给原值,于是就将0x4024b0作为字符串打印出来1
2(gdb) p (char*) 0x4024b0
$18 = 0x4024b0 <array.3449> "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"原来这里还有一个来自邪恶博士的嘲讽啊🤪
-
回归正题我们需要按照规定规则找到能够与
flyers匹配的字符串,为了避免ascll码中的非可见字符干扰,我们把输入都映射到a~z,我们可以设置程序将所有的映射组都找到就再组合出来就行了1
2
3
4
5
6
7
8
9
10
int main(){
char s[] = "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?";
int i;
for(i = 0; i < 0xf; i++){
printf("%c --> %c\n",'a'+i,s[('a'+i)&0xf]);
}
return 0;
}输出为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15a --> a
b --> d
c --> u
d --> i
e --> e
f --> r
g --> s
h --> n
i --> f
j --> o
k --> t
l --> v
m --> b
n --> y
o --> l -
最后得到一组可行的解为
ionefg,好像后面还有一个赋值和运算的过程并没有啥用啊😂
-
-
经验证可通过
phase_6
-
先上汇编
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88Dump of assembler code for function phase_6:
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
0x0000000000401100 <+12>: mov %rsp,%r13
0x0000000000401103 <+15>: mov %rsp,%rsi
0x0000000000401106 <+18>: callq 0x40145c <read_six_numbers>
0x000000000040110b <+23>: mov %rsp,%r14
0x000000000040110e <+26>: mov $0x0,%r12d
0x0000000000401114 <+32>: mov %r13,%rbp
0x0000000000401117 <+35>: mov 0x0(%r13),%eax
0x000000000040111b <+39>: sub $0x1,%eax
0x000000000040111e <+42>: cmp $0x5,%eax
0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52>
0x0000000000401123 <+47>: callq 0x40143a <explode_bomb>
0x0000000000401128 <+52>: add $0x1,%r12d
0x000000000040112c <+56>: cmp $0x6,%r12d
0x0000000000401130 <+60>: je 0x401153 <phase_6+95>
0x0000000000401132 <+62>: mov %r12d,%ebx
0x0000000000401135 <+65>: movslq %ebx,%rax
0x0000000000401138 <+68>: mov (%rsp,%rax,4),%eax
0x000000000040113b <+71>: cmp %eax,0x0(%rbp)
0x000000000040113e <+74>: jne 0x401145 <phase_6+81>
0x0000000000401140 <+76>: callq 0x40143a <explode_bomb>
0x0000000000401145 <+81>: add $0x1,%ebx
0x0000000000401148 <+84>: cmp $0x5,%ebx
0x000000000040114b <+87>: jle 0x401135 <phase_6+65>
0x000000000040114d <+89>: add $0x4,%r13
0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32>
0x0000000000401153 <+95>: lea 0x18(%rsp),%rsi
0x0000000000401158 <+100>: mov %r14,%rax
0x000000000040115b <+103>: mov $0x7,%ecx
0x0000000000401160 <+108>: mov %ecx,%edx
0x0000000000401162 <+110>: sub (%rax),%edx
0x0000000000401164 <+112>: mov %edx,(%rax)
0x0000000000401166 <+114>: add $0x4,%rax
0x000000000040116a <+118>: cmp %rsi,%rax
0x000000000040116d <+121>: jne 0x401160 <phase_6+108>
0x000000000040116f <+123>: mov $0x0,%esi
0x0000000000401174 <+128>: jmp 0x401197 <phase_6+163>
0x0000000000401176 <+130>: mov 0x8(%rdx),%rdx
0x000000000040117a <+134>: add $0x1,%eax
0x000000000040117d <+137>: cmp %ecx,%eax
0x000000000040117f <+139>: jne 0x401176 <phase_6+130>
0x0000000000401181 <+141>: jmp 0x401188 <phase_6+148>
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 <phase_6+183>
0x0000000000401197 <+163>: mov (%rsp,%rsi,1),%ecx
0x000000000040119a <+166>: cmp $0x1,%ecx
0x000000000040119d <+169>: jle 0x401183 <phase_6+143>
0x000000000040119f <+171>: mov $0x1,%eax
0x00000000004011a4 <+176>: mov $0x6032d0,%edx
0x00000000004011a9 <+181>: jmp 0x401176 <phase_6+130>
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
0x00000000004011c0 <+204>: mov %rdx,0x8(%rcx)
0x00000000004011c4 <+208>: add $0x8,%rax
0x00000000004011c8 <+212>: cmp %rsi,%rax
0x00000000004011cb <+215>: je 0x4011d2 <phase_6+222>
0x00000000004011cd <+217>: mov %rdx,%rcx
0x00000000004011d0 <+220>: jmp 0x4011bd <phase_6+201>
0x00000000004011d2 <+222>: movq $0x0,0x8(%rdx)
0x00000000004011da <+230>: mov $0x5,%ebp
0x00000000004011df <+235>: mov 0x8(%rbx),%rax
0x00000000004011e3 <+239>: mov (%rax),%eax
0x00000000004011e5 <+241>: cmp %eax,(%rbx)
0x00000000004011e7 <+243>: jge 0x4011ee <phase_6+250>
0x00000000004011e9 <+245>: callq 0x40143a <explode_bomb>
0x00000000004011ee <+250>: mov 0x8(%rbx),%rbx
0x00000000004011f2 <+254>: sub $0x1,%ebp
0x00000000004011f5 <+257>: jne 0x4011df <phase_6+235>
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
End of assembler dump. -
我们先整体看一下,可以发现这个程序大概是由多个循环组成的还有很多个带序号的栈的操作,不过貌似只在程序头尾出现问题应该不大(其实我第一眼看过去是在早
callq就怕这个还来几个函数调用那这代码量就…😂)然后再仔细分析-
<+8 ~ +15>在读入数据前的赋值和运算暂且不关注 -
<+18>这里调用了一个函数read_six_numbers我们在phase_2就调用并确认过了就是字面意思要求我们输入6个数字,为了使数据具有标识性我们先暂且将输入定为17 19 23 29 31 37(当然你也可以选择其他的值)这样我们可以通过输出寄存器内的值确认我们的输入存在了哪里 -
<+23 ~ +32>我们可以打断点在<+35>这里然后打印出我们这些寄存器内的值来大致判断寄存器的作用1
2
3
4
5
6
7
8
9
10(gdb) p $r14
$1 = 140737488347856
(gdb) p *$r14
$2 = 17
(gdb) p $rbp
$3 = (void *) 0x7fffffffe2d0
(gdb) p *$rbp
Attempt to dereference a generic pointer.
(gdb) p *$r13
$4 = 17可以发现
%r13 %r14都是保存着我们输入的值的地址的寄存器 -
<+35 ~ +47>我们一段与前一段分割开来分析是因为这里进行赋值的%eax和后面的爆炸有关系,这里将%r13地址所对应的值取出来放进了%eax然后减去了0x1的值要小于或等于0x5否则会爆炸,故我们输入的第一个值应该要小于等于6 -
于是我们修改了输入的值为
3 5 7 11 13 17 -
<+52 ~ +60>这里感觉是个while循环的样子并且应该是循环了5层,以猜测到c的源码大致是长这样的1
while(++i != 6){...} // i --> %r12d
-
<+62 ~ +68>这里将%r12d中的值存到了%rax中,然后将%rsp + 4*%rax地址下的值存入了%eax,在上面的分析中我们已经知道了%r13存放着我们输入的值的第一个值的地址而在<+12>我们可以看到%r13的值来自%rsp所以这里的%rsp存放着我们输入的第一个数的地址,而%rax是我们循环的参数而int占4个字节故我们可以合理猜想这里应该是取了我们输入的值的第二个,我们可以打印出来验证一下1
2
3Breakpoint 2, 0x000000000040113b in phase_6 ()
(gdb) p $eax
$5 = 5 -
<+71 ~ +76>比较了%eax和%rbp的值,相等则爆炸而%rbp存放着我们输入的第一个数值,故这里要求第一个输入值与第二个输入值要不相等 -
<+81 ~ +87>这里的%ebx是%r12d的一个副本,而这里又是一重循环感觉上这里应该会是一个do while循环所以这样的两重循环写成c源码大概长成这样:1
2
3
4
5
6
7
8
9int i = 0;
while(++i != 6){ // i --> %r12d
int j = i; // j --> %ebx
do{
int k = j; // k --> %rax
if(nums[k] == nums[0]) explode_bomb();
//nums[k] --> %eax nums[0] --> %rbp
}while(++j <= 6);
}所以这里的就是其他输入都不能与第一个输入值相同
-
<+89 ~ +93>将%r13向后移动了一个int单位然后又跳到了比较输入是否小于6这一步似乎是一个goto语句(有点像循环语句但是这里是个无条件跳转语句),然后结合前面的分析将<+26 ~ +93>这一部分给化为c源码就长这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14label:
int i = 0; // i --> %r12d
int* rbp = nums; // rbp --> %rbp
if(*rbp-1 <= 5) explode_bomb();
while(++i != 6){
int j = i; // j --> %ebx
do{
int k = j; // k --> %rax
int eax = *(nums+k); // eax --> %eax
if(eax == *rbp) explode_bomb();
}while(++j <= 6);
goto label;
}虽然具体实现不一定是这样但是逻辑大概是这个样子的
-
通过分析到
<+26 ~ +93>我们可以知道了这段大概讲的就是我们输入的6个数都要小于等于6并且互不相等,这里虽然没有限制不能是负数和零但是根据这里大费周章判等和比大小的过程我们大概可以分析出这里要输入的就是1 ~ 6的组合,根据排列组合的原理共720种情况,按照正态分布我们大概会在测试过360此后得到结果,假设我们一秒钟测试一种我们就能在大概6分钟解决最后一个题了而剩下的近三分之二的汇编我们必定无法在6分钟内看完并找到答案,故逐个测试具有完备的合理性(一本正经 胡说八道🤣 其实也未尝不可😜) -
更具已知我们调整一下输入的数字为
2 3 5 1 4 6(只是将质数与非质数分离也可以选择其他序列输入) -
<+95 ~ +112>将栈顶+0x18后的地址装入%rsi由于我们并不知道这里的%rsi是个啥值所以打印出来看看得到的是结果0并看不出啥,然后是%r14可以看出存放的是我们输入的第一个数(其实也可以在<+23>看出来)1
2
3
4
5
6
7
8
9(gdb) p $rsi
$1 = 140737488347880
(gdb) p *$rsi
$2 = 0
(gdb) p $r14
$3 = 140737488347856
(gdb) p *$r14
$4 = 2然后
<+108 ~ +112>一起看就是将7 - nums[0]装入%rax的地址,然后%rax向上移动了一个int单位然后比较了这个与%rsi的大小如果不等就会再次循环于是我们就可以知道了这里的%rsi不是其数值作用的,而是其地址作用的%rsp存的地址是我们输入的第一个数的地址,而0x18 = 24 = 4 * 6也就是指向我们输入的最后一个地址的下一个位置,于是我们可以分析出这里的c源码大概就是:1
2
3
4
5
6int* rsi = nums+6; // rsi --> %rsi
int* rax = nums; //rax --> %rax
int ecx = 7; //ecx --> %ecx
do{
*rax = ecx-*rax;
}while(++rax != rsi);所以这里的结论就是原输入被改成了
7 - nums[i] -
<+123>就是直接将%esi置零 -
<+128>无条件跳转到<+163> -
<+163>将%rsp+%rsi地址内的值放入%ecx由上一个循环的跳出条件可知此时%rsi的值就是0,所以这里的%ecx的值就是第一个输入的数的(经过调整后应该是5)我们可以打印出来验证一下1
2(gdb) p $ecx
$5 = 5 -
<+166 ~ +169>条件跳转当%ecx小于或等于1就跳到<+143>否则就继续,显然我们这里的值不会小于等于1(实际上合理的应该就是我们最开始输入6的位置) -
<+171 ~ +176>将两个寄存器置为常数,%eax置为1,而%edx置为0x6032d0这个其实更像一个地址,所以我们不妨试着将其当做一个地址输出,但好像并不是个有含义的值1
2(gdb) p *0x6032d0
$6 = 332 -
<+181>这里就是直接无条件跳转到<+130>(还想着就要结束了 居然又是个循环😅) -
<+130>这里将%rdx向前移了2个int单位 -
<+134 ~ +139>这里又是一个类似for循环的东西for(int eax = 1; ecx != eax; eax++),又我们之前已经得到了%ecx是常数7也就是我们这里会循环6层即%rdx += 6*8 -
<+141>这里就是一个又是一个无条件跳转语句跳转到<+148>(感觉现在这一块的代码已经快乱麻了😬) -
<+148>这里又是一个赋值将%rdx也就是我上一行出来得到的值交给%rsp+0x20+ 2*%rsi前面已经说了%rsi值为0而0x20 = 32 = 4*8好像和我们的输入并没有关系,但是我们还是输出这里经过循环得到的%rdx的值1
2
3
4(gdb) p $rdx
$7 = 6304528
(gdb) p *$rdx
$8 = 477好像又是无意义的数字,但是两次将这些值当做地址取都是可以的说明这里的值大概率是当做地址来用的
-
<+153 ~ +161>这里将%rsi加上一个int单位后和0x18 (4*6)比较,如果相等就会跳到<+183>(所有可合理猜测为会进行6次循环并且每一次循环都会和输入的数字每一个有关) -
下面就成功进入了循环
<+163>说以这里只有当我们输入的数字是6的那个位置不会有%rdx变化的那部分发生而已,而当处理完6个数据之后按照所给就会跳转到<+183>(感觉这里的部分并没更改什么值🤔) -
<+183 ~ +198>一通赋值并没搞懂在干啥,就都把值打印出来看看:1
2
3
4
5
6(gdb) p $rbx
$9 = 6
(gdb) p $rax
$10 = 5
(gdb) p $rsi
$11 = 0需要注意的是这里的值更具地址并不是我们输入的值,但应该与我们的输入相关
-
<+201 ~ +220>又是循环,聚焦到<+208 ~ +215>我们可以发现这里的%rax在整个循环只在<+208>发生了改变而在%rsi完全没发生改变,而更具前面的赋值这两数的分别赋值了%rsp+0x28和%rsp+0x50这两个的差值都不是8的倍数所以这里会进入死循环?(到这里已经完全蒙了🤯) -
最后屈服了,看了其他人的解法原来在
<+95 ~ +121>这里的常数0x6032d0是一个链表1
2
3
4
5
6
7(gdb) x/24wd 0x6032d0
0x6032d0 <node1>: 332 1 6304480 0
0x6032e0 <node2>: 168 2 6304496 0
0x6032f0 <node3>: 924 3 6304512 0
0x603300 <node4>: 691 4 6304528 0
0x603310 <node5>: 477 5 6304544 0
0x603320 <node6>: 443 6 0 0 -
<+123 ~ +181>这段代码的作用是依次取出栈中存放的6个数(已经被7减过),根据每个数x,从前向后遍历链表到链表的第x个节点,然后将节点的地址依次存入$rsp+0x20处的6个位置。 -
<+230 ~ +257>这里就是通过$rsp+0x20处存入的6个节点的地址取节点值,保证前一个数<后一个数 -
按照链表节点中存放的值从大到小应该是
3 4 5 6 1 2,而这个值是减过7后得到的值,所以这里的得到的最初的输入就是4 3 2 1 6 5
-
-
测试后通过
实验总结
第一次真正上手汇编,在此之前只在计算机组成原理课上提到过一些汇编指令和不同的取址方式
通过这次实验大概地掌握了部分的汇编指令的阅读能力和使用gdb调试c程序的能力并且掌握了部分的机器水平程序开发的技巧
不得不感慨c语言是多么地接近底层,很多时候一条汇编就是一个条c的代码而且其对应关系还是比较明显的,对使用汇编代码实现一些简单的c语句的赋值、运算、if-else语句、switch-case语句、while循环、do-while循环、for循环有了较好的理解
但是本次实验在第六部分实在是被链表给整蒙了,第一次接触如此长的汇编代码加之实验的时间跨度等因素确实完成地不是很理想,希望在下次复盘的时候能彻底解决之前留下的困惑



.jpg)
.jpg)