【实战🚀】手摸手教你Bomb Lab

手摸手教你Bomb Lab

阅读须知: 网上关于CMU配套教材开源的版本的bomb的教程已经很多了,比如我科学长的这两篇写得相当好

深入理解计算机系统(CS:APP) - Bomb Lab详解, 深入理解计算机系统BombLab实验报告. 我也不想做重复的劳动,去写别人已经写过的东西,这一篇博客是基于学校给我发的bomb版本来写的, 相关的可执行文件等实验结束后会上传到我的github上面, 如果你想要一个其他版本的bomb来练习的话,那么可以用我的这一份.

这个实验一共有6个阶段以及一个隐藏阶段,我目前没有做那个隐藏阶段,如果老师说做隐藏阶段加分的话,我或许会补上^_^! ps:写博客比做实验花时间多系列QwQ

首先搬运一下指导书上面的实验简介:

本实验中,你要使用课程所学知识拆除一个“binary bombs”来增强对程序的机器级表示、汇编语言、调试器和逆向工程等方面原理与技能的掌握。

一个“binary bombs”(二进制炸弹,下文将简称为炸弹)是一个Linux可执行C程序,包含了6个阶段(phase1~phase6)。炸弹运行的每个阶段要求你输入一个特定的字符串,若你的输入符合程序预期的输入,该阶段的炸弹就被“拆除”,否则炸弹“爆炸”并打印输出 “BOOM!!!”字样。实验的目标是拆除尽可能多的炸弹层次。

每个炸弹阶段考察了机器级语言程序的一个不同方面,难度逐级递增:

* 阶段1:字符串比较

* 阶段2:循环

* 阶段3:条件/分支

* 阶段4:递归调用和栈

* 阶段5:指针

* 阶段6:链表/指针/结构

另外还有一个隐藏阶段,但只有当你在第4阶段的解之后附加一特定字符串后才会出现。

为了完成二进制炸弹拆除任务,你需要使用gdb调试器和objdump来反汇编炸弹的可执行文件,并单步跟踪调试每一阶段的机器代码,从中理解每一汇编语言代码的行为或作用,进而设法“推断”出拆除炸弹所需的目标字符串。这可能需要你在每一阶段的开始代码前和引爆炸弹的函数前设置断点,以便于调试。

实验语言:C语言

实验环境:linux

然后对实验中用到的一些文件做一个简单的说明:

  • bomb:bomb的可执行程序。
  • bomb.c:bomb程序的main函数。

「运行./bomb」./bomb是一个可执行程序,需要0或1个命令行参数(详见bomb.c源文件中的main()函数)。如果运行时不指定参数,则该程序打印出欢迎信息后,期待你按行输入每一阶段用来拆除炸弹的字符串,并根据你当前输入的字符串决定你是通过相应阶段还是炸弹爆炸导致任务失败。

你也可将拆除每一阶段炸弹的字符串按行组织在一个文本文件中(比如:result.txt),然后作为运行程序时的唯一一个命令行参数传给程序(./bomb result.txt),程序会自动读取文本文件中的字符串,并依次检查对应每一阶段的字符串来决定炸弹拆除成败。

正文开始

首先将可执行程序bomb进行反汇编:

1
复制代码objdump -d bomb > disassemble.asm

然后用你喜欢的编辑器打开这个文件, 我用的是VsCode

可以装这个插件来高亮显示代码


主函数比较长,反正我是没看,直接Ctrl + F搜索phase_1进入阶段1

阶段一

阶段1比较简单,我分配到的题目如下所示:

1
2
3
4
5
6
7
8
9
10
11
复制代码08048b33 <phase_1>:
8048b33: 83 ec 14 sub $0x14,%esp
8048b36: 68 a4 9f 04 08 push $0x8049fa4
8048b3b: ff 74 24 1c pushl 0x1c(%esp)
8048b3f: e8 5e 04 00 00 call 8048fa2 <strings_not_equal>
8048b44: 83 c4 10 add $0x10,%esp
8048b47: 85 c0 test %eax,%eax
8048b49: 74 05 je 8048b50 <phase_1+0x1d>
8048b4b: e8 49 05 00 00 call 8049099 <explode_bomb>
8048b50: 83 c4 0c add $0xc,%esp
8048b53: c3 ret

我们来分析一下这个代码,可以看到有两个push操作,然后有一个call操作,所以猜测这两个push操作是在为这个strings_not_equal函数传递参数, 我们可以用gdb调试一下看看是不是这样:

1
2
3
4
5
复制代码gdb bomb
b phase_1 //打断点
b explode_bomb
run
layout asm //提供图形界面, 可以更好的观察执行到哪一条语句了

我输入的字符串是hello, 然后通过调试可以看到,0x1c(%esp)压进栈中的字符串就是hello

1
复制代码$1 = 0x804c3e0 <input_strings> "hello"

所以我们可以猜测我们输入的字符串需要与0x8049fa4处的字符串进行比对,如果相等,那么就可以成功拆除炸弹了,现在查看这个地址处的字符串

1
复制代码x /s 0x8049fa4

执行后获得以下的输出, All your base are belong to us.即为正确答案

1
2
复制代码(gdb) x /s 0x8049fa4
0x8049fa4: "All your base are belong to us."

总的来说,这个任务还算是非常简单的.

阶段二

阶段2考察的是循环,代码量相对于阶段1要大很多,首先贴一下阶段二完整的代码

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
复制代码08048b54 <phase_2>:
8048b54: 56 push %esi
8048b55: 53 push %ebx
8048b56: 83 ec 2c sub $0x2c,%esp
8048b59: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048b5f: 89 44 24 24 mov %eax,0x24(%esp)
8048b63: 31 c0 xor %eax,%eax
8048b65: 8d 44 24 0c lea 0xc(%esp),%eax
8048b69: 50 push %eax
8048b6a: ff 74 24 3c pushl 0x3c(%esp)
8048b6e: e8 4b 05 00 00 call 80490be <read_six_numbers>
8048b73: 83 c4 10 add $0x10,%esp
8048b76: 83 7c 24 04 01 cmpl $0x1,0x4(%esp)
8048b7b: 74 05 je 8048b82 <phase_2+0x2e>
8048b7d: e8 17 05 00 00 call 8049099 <explode_bomb>
8048b82: 8d 5c 24 04 lea 0x4(%esp),%ebx
8048b86: 8d 74 24 18 lea 0x18(%esp),%esi
8048b8a: 8b 03 mov (%ebx),%eax
8048b8c: 01 c0 add %eax,%eax
8048b8e: 39 43 04 cmp %eax,0x4(%ebx)
8048b91: 74 05 je 8048b98 <phase_2+0x44>
8048b93: e8 01 05 00 00 call 8049099 <explode_bomb>
8048b98: 83 c3 04 add $0x4,%ebx
8048b9b: 39 f3 cmp %esi,%ebx
8048b9d: 75 eb jne 8048b8a <phase_2+0x36>
8048b9f: 8b 44 24 1c mov 0x1c(%esp),%eax
8048ba3: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048baa: 74 05 je 8048bb1 <phase_2+0x5d>
8048bac: e8 df fb ff ff call 8048790 <__stack_chk_fail@plt>
8048bb1: 83 c4 24 add $0x24,%esp
8048bb4: 5b pop %ebx
8048bb5: 5e pop %esi
8048bb6: c3 ret

开始的几行就是一些准备工作,不用去研究它是干什么的,爷也不想研究.

下面从第8行开始看起,第8行用了一条lea语句,这是获得0xc(%esp)处的地址, 然后就把这个地址压栈了, 可以猜测这个地址是用来存放读取的6个数字的,后面的一个pushl是干啥的我也8太清楚.然后调用read_six_numbers函数读取6个数字.

第13行, cmpl $0x1,0x4(%esp)这一条语句是比较读入的第一个参数和1的大小关系,至于为什么是第一个参数,可以通过自己模拟栈的push pop知道. 这里第一个参数必须等于1, 否则就会引爆炸弹. 然后跳转到16行进行执行,.

16, 17行分别将指向第1个数和第6个数的指针存进了ebx 和 esi寄存器中. 从这里可以看出, esi寄存器的值可以看做循环终止的条件.

18, 19行两行是将ebx寄存器中的数字变为原来的两倍后放进了eax寄存器中,然后比较ebx寄存器中数字的后一个数字是否与eax寄存器中的值相等, 也就是比较后一个数字是否是前一个数字的两倍, 如果不是, 那么引爆炸弹, 否则循环继续, 指针往后挪一位, 知道挪到了最后一个.

所以最后的答案就是1 2 4 8 16 32, 这一题也比较简单

阶段三

阶段三考察的是条件与分支, 也比较简单, 先来看一看阶段三的源代码

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
复制代码 8048bb7:	83 ec 1c             	sub    $0x1c,%esp
8048bba: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048bc0: 89 44 24 0c mov %eax,0xc(%esp)
8048bc4: 31 c0 xor %eax,%eax
8048bc6: 8d 44 24 08 lea 0x8(%esp),%eax
8048bca: 50 push %eax
8048bcb: 8d 44 24 08 lea 0x8(%esp),%eax
8048bcf: 50 push %eax
8048bd0: 68 6f a1 04 08 push $0x804a16f
8048bd5: ff 74 24 2c pushl 0x2c(%esp)
8048bd9: e8 32 fc ff ff call 8048810 <__isoc99_sscanf@plt>
8048bde: 83 c4 10 add $0x10,%esp
8048be1: 83 f8 01 cmp $0x1,%eax
8048be4: 7f 05 jg 8048beb <phase_3+0x34>
8048be6: e8 ae 04 00 00 call 8049099 <explode_bomb>
8048beb: 83 7c 24 04 07 cmpl $0x7,0x4(%esp)
8048bf0: 77 3c ja 8048c2e <phase_3+0x77>
8048bf2: 8b 44 24 04 mov 0x4(%esp),%eax
8048bf6: ff 24 85 00 a0 04 08 jmp *0x804a000(,%eax,4)
8048bfd: b8 f3 01 00 00 mov $0x1f3,%eax
8048c02: eb 3b jmp 8048c3f <phase_3+0x88>
8048c04: b8 c0 00 00 00 mov $0xc0,%eax
8048c09: eb 34 jmp 8048c3f <phase_3+0x88>
8048c0b: b8 57 01 00 00 mov $0x157,%eax
8048c10: eb 2d jmp 8048c3f <phase_3+0x88>
8048c12: b8 9b 01 00 00 mov $0x19b,%eax
8048c17: eb 26 jmp 8048c3f <phase_3+0x88>
8048c19: b8 5c 03 00 00 mov $0x35c,%eax
8048c1e: eb 1f jmp 8048c3f <phase_3+0x88>
8048c20: b8 38 02 00 00 mov $0x238,%eax
8048c25: eb 18 jmp 8048c3f <phase_3+0x88>
8048c27: b8 67 01 00 00 mov $0x167,%eax
8048c2c: eb 11 jmp 8048c3f <phase_3+0x88>
8048c2e: e8 66 04 00 00 call 8049099 <explode_bomb>
8048c33: b8 00 00 00 00 mov $0x0,%eax
8048c38: eb 05 jmp 8048c3f <phase_3+0x88>
8048c3a: b8 83 02 00 00 mov $0x283,%eax
8048c3f: 3b 44 24 08 cmp 0x8(%esp),%eax
8048c43: 74 05 je 8048c4a <phase_3+0x93>
8048c45: e8 4f 04 00 00 call 8049099 <explode_bomb>
8048c4a: 8b 44 24 0c mov 0xc(%esp),%eax
8048c4e: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048c55: 74 05 je 8048c5c <phase_3+0xa5>
8048c57: e8 34 fb ff ff call 8048790 <__stack_chk_fail@plt>
8048c5c: 83 c4 1c add $0x1c,%esp
8048c5f: c3 ret

从第5行开始看, 这里的两个lea操作很显然就是传地址, 类似与&a, 然后供sacnf函数使用

这里注意第9行push $0x804a16f, 这里压入了一个地址常量, 根据后面调用的scanf猜测这个应该是格式字符串, 我们通过gdb查看这个字符串的值

1
2
复制代码(gdb) x /s 0x804a16f
0x804a16f: "%d %d"

从输出可以看到, scanf接受的是两个int类型的整数.

接着看第13行, 有一个eax寄存器与1的比较操作, 一般eax寄存器存储函数的返回值, 而sacnf函数的返回值就是成功读取的元素个数,于是可知,这里读取的元素个数小于等于1的话就会引爆炸弹.

接着看第16行, 是比较读入的第一个参数和7的大小关系, 注意后面一行的ja是无符号比较, 所以可知第一个参数的1值必须在0~7之间, 否则就会引爆炸弹

接着的18, 19行, 有意思的地方来了, 这里是一个典型的跳转表的实现,比如中断向量表, switch都是用的这种思想.比如说我输入的第一个数字是1, 那么我跳转到的地址需要在gdb中通过如下方式获得:

1
2
复制代码(gdb) print /x *(int*)(0x804a000+4)
$1 = 0x8048bfd

然后再在19~36行中寻找对应的地址跳转过去就行, 比如0x8048bfd是第20行,对应的代码是

1
2
复制代码8048bfd:	b8 f3 01 00 00       	mov    $0x1f3,%eax
8048c02: eb 3b jmp 8048c3f <phase_3+0x88>

所以eax的值就是0x1f3, 也就是499

通过第38行的代码

1
2
3
复制代码 8048c3f:	3b 44 24 08          	cmp    0x8(%esp),%eax
8048c43: 74 05 je 8048c4a <phase_3+0x93>
8048c45: e8 4f 04 00 00 call 8049099 <explode_bomb>

可以知道, 我们需要输入的第二个数与eax寄存器中的值相等,不然就会引爆炸弹, 于是一个可能的答案就是1 499, 当然, 还有其他的答案.

阶段四

阶段四需要你进行逆向出对应的C代码, 这样解决起来会简单许多,这一题我很快就逆向出C代码了,但是没想到被scanf函数坑了, 它的第一个参数是你输入的第二个, 第二个参数是你输入的第一个, 搞得我还一直以为我逆向错了,后来在车大牛的提醒下, 单步调试终于发现了这个坑爹的BUG

下面我就来讲一讲我是如何逆向的, 以及我是如何调试的

首先我们先贴上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
25
26
27
28
29
30
31
32
33
34
复制代码08048ca3 <phase_4>:
8048ca3: 83 ec 1c sub $0x1c,%esp
8048ca6: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048cac: 89 44 24 0c mov %eax,0xc(%esp)
8048cb0: 31 c0 xor %eax,%eax
8048cb2: 8d 44 24 04 lea 0x4(%esp),%eax
8048cb6: 50 push %eax
8048cb7: 8d 44 24 0c lea 0xc(%esp),%eax
8048cbb: 50 push %eax
8048cbc: 68 6f a1 04 08 push $0x804a16f
8048cc1: ff 74 24 2c pushl 0x2c(%esp)
8048cc5: e8 46 fb ff ff call 8048810 <__isoc99_sscanf@plt>
8048cca: 83 c4 10 add $0x10,%esp
8048ccd: 83 f8 02 cmp $0x2,%eax
8048cd0: 75 0c jne 8048cde <phase_4+0x3b>
8048cd2: 8b 44 24 04 mov 0x4(%esp),%eax
8048cd6: 83 e8 02 sub $0x2,%eax
8048cd9: 83 f8 02 cmp $0x2,%eax
8048cdc: 76 05 jbe 8048ce3 <phase_4+0x40>
8048cde: e8 b6 03 00 00 call 8049099 <explode_bomb>
8048ce3: 83 ec 08 sub $0x8,%esp
8048ce6: ff 74 24 0c pushl 0xc(%esp)
8048cea: 6a 09 push $0x9
8048cec: e8 6f ff ff ff call 8048c60 <func4>
8048cf1: 83 c4 10 add $0x10,%esp
8048cf4: 3b 44 24 08 cmp 0x8(%esp),%eax
8048cf8: 74 05 je 8048cff <phase_4+0x5c>
8048cfa: e8 9a 03 00 00 call 8049099 <explode_bomb>
8048cff: 8b 44 24 0c mov 0xc(%esp),%eax
8048d03: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048d0a: 74 05 je 8048d11 <phase_4+0x6e>
8048d0c: e8 7f fa ff ff call 8048790 <__stack_chk_fail@plt>
8048d11: 83 c4 1c add $0x1c,%esp
8048d14: c3 ret

从第6行开始看起, 这里是熟悉的lea, 熟悉的push, 熟悉的scanf, 然后用相同的伎俩可以知道格式化字符串还是%d %d, 于是我们知道这里也是读入两个数字, 但是, 这里先push进去的是低地址的那一个…

14行比较了一下返回值, 不是2就引爆炸弹, 也再一次证明了要读两个数

16-18行比较了scanf的第一个参数, 也就是我们输入的第二个数减2后与2的大小关系, 这里需要特别注意19行是jbe, 是无符号数比较, 所以我们输入的第二个参数的值必须在2~4之间.

21~23行是在为调用func4准备参数, 查看func4的源代码后我们可以知道,先push进去的是右边的参数

假设我们输入的两个数是b a, 那么这里的函数调用就是func4(9, a), 至于为什么你自己画一下这里的栈就知道了, 电脑上画图太麻烦, 我就不画了.(对了, 在自己分析栈时要注意函数调用时会将返回地址压栈)

我们先不看func4, 先接着看phase_4, 由26到28行我们可以看到我们需要我们输入的第一个参数的值等于调用func4后的返回值, 不然就会引爆炸弹, 于是, 接下来, 我们逆向func4

先上func4的源代码:

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
复制代码08048c60 <func4>:
8048c60: 57 push %edi
8048c61: 56 push %esi
8048c62: 53 push %ebx
8048c63: 8b 5c 24 10 mov 0x10(%esp),%ebx
8048c67: 8b 7c 24 14 mov 0x14(%esp),%edi
8048c6b: 85 db test %ebx,%ebx
8048c6d: 7e 2b jle 8048c9a <func4+0x3a>
8048c6f: 89 f8 mov %edi,%eax
8048c71: 83 fb 01 cmp $0x1,%ebx
8048c74: 74 29 je 8048c9f <func4+0x3f>
8048c76: 83 ec 08 sub $0x8,%esp
8048c79: 57 push %edi
8048c7a: 8d 43 ff lea -0x1(%ebx),%eax
8048c7d: 50 push %eax
8048c7e: e8 dd ff ff ff call 8048c60 <func4>
8048c83: 83 c4 08 add $0x8,%esp
8048c86: 8d 34 07 lea (%edi,%eax,1),%esi
8048c89: 57 push %edi
8048c8a: 83 eb 02 sub $0x2,%ebx
8048c8d: 53 push %ebx
8048c8e: e8 cd ff ff ff call 8048c60 <func4>
8048c93: 83 c4 10 add $0x10,%esp
8048c96: 01 f0 add %esi,%eax
8048c98: eb 05 jmp 8048c9f <func4+0x3f>
8048c9a: b8 00 00 00 00 mov $0x0,%eax
8048c9f: 5b pop %ebx
8048ca0: 5e pop %esi
8048ca1: 5f pop %edi
8048ca2: c3 ret

前面几行是在为函数调用准备, 不做分析.

5, 6两行是从栈中取出参数, 如果前面的栈你自己分析了, 那么这里也不难分析得出ebx放的是第一个参数x1, edi放的是第二个参数x2.

第7, 8两行的作用就是判断x1是否≤0, 经过分析不难得出这几句汇编代码对应的C语言代码, 注意:eax寄存器存储的是函数的返回值

1
复制代码if (x1 <= 0) return 0;

接下来第9行将eax寄存器赋值为x2, 然后又对x1做了一个判断, 如果等于0, 那么跳转到第27行执行, 于是我们可以据此逆向出下面的C语言代码:

1
复制代码if (x1 == 1) return x2;

接下来调用func4(x1 - 1, x2), 由18行可知然后将返回值加上x2赋值给了esi

19~21行又在为调用func4函数准备参数, 稍加分析可以知道接下来调用的是func4(x1 -2, x2)

最后由24行可知, 将第二次调用func4的返回值加上esi的值得到最终的返回值, 于是总的逆向程序代码如下所示:

1
2
3
4
5
6
复制代码int func(int x1, int x2) {
if (x1 <= 0) return 0;
if (x1 == 1) return x2;

return func(x1 - 1, x2) + x1 + func(x1 - 2, x2);
}

非常的优雅.

当然了, 如果你对上面的有些难以理解, 那么我可以给出另一份比较笨, 但容易理解的代码, 我们注意到, 在调用的过程中eax寄存器的值是没有保存的, 所以, 我们可以把它看做全局变量, 令它为变量eax, 于是有下面的程序, 完全对应汇编语句的丑陋的C程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码int eax;

void func(int x1, int x2) {
if (x1 <= 0) {
eax = 0;
return;
}

eax = x2;
if (x1 == 1) return;
eax = x1 - 1;
func(eax, x2);
int temp = x2 + eax;
x1 -= 2;
func(x1, x2);
eax += temp;
return;
}

int main() {
printf("%d", eax);
}

如果你仔细对比的话, 可以看出这个程序的每一条语句的行为都是和汇编一一对应的, 可能更容易理解一点.

最后的结果是264 3

下面讲一下我在一开始被scanf坑到时是如何进行调试的.

首先gdb进入调试界面, 然后在explode_bomb处打一个断点, phase_4处也打一个断点, 然后单步调试, 然后观察寄存器的值, 以及是在哪里炸的, 好吧, 我在说废话, 不这么调试还怎么调试, gdb相关命令直接看文档就行, 不用背.

阶段五

你可能注意到了, 前面的程序我都没有任何的注释, 因为确实也不需要注释, 但是从阶段五开始, 我开始写注释了, 不然自己都不知道自己在看什么

不过阶段五难度还好, 我不到一个小时就做出来了.

先上阶段五的源代码:

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
复制代码08048d15 <phase_5>:
8048d15: 53 push %ebx
8048d16: 83 ec 14 sub $0x14,%esp
8048d19: 8b 5c 24 1c mov 0x1c(%esp),%ebx
8048d1d: 53 push %ebx
8048d1e: e8 60 02 00 00 call 8048f83 <string_length>
8048d23: 83 c4 10 add $0x10,%esp
8048d26: 83 f8 06 cmp $0x6,%eax
8048d29: 74 05 je 8048d30 <phase_5+0x1b>
8048d2b: e8 69 03 00 00 call 8049099 <explode_bomb>
8048d30: 89 d8 mov %ebx,%eax;eax指向了字符串的开始
8048d32: 83 c3 06 add $0x6,%ebx;ebx现在指向了字符串的结尾的后一个,象征着结束标志
8048d35: b9 00 00 00 00 mov $0x0,%ecx
8048d3a: 0f b6 10 loop: movzbl (%eax),%edx;取出了eax指向的那个字符,放到了edx中,而且是0扩展
8048d3d: 83 e2 0f and $0xf,%edx;将低四位保留,高四位清零了
8048d40: 03 0c 95 20 a0 04 08 add 0x804a020(,%edx,4),%ecx;这个有点switch的感觉
8048d47: 83 c0 01 add $0x1,%eax
8048d4a: 39 d8 cmp %ebx,%eax
8048d4c: 75 ec jne 8048d3a <phase_5+0x25>
8048d4e: 83 f9 2c cmp $0x2c,%ecx;需要ecx加6次等于0x2c 10 + 10 + 10 + 10 + 2 + 2
8048d51: 74 05 je 8048d58 <phase_5+0x43>
8048d53: e8 41 03 00 00 call 8049099 <explode_bomb>
8048d58: 83 c4 08 add $0x8,%esp
8048d5b: 5b pop %ebx
8048d5c: c3 ret

这一题考察的是指针, 不做点注释我都不知道我自己指到哪里去了.

首先看到第6行, 这个函数名一看就知道是计算字符串的长度, 而这个字符串肯定就是我们输的字符串, 不要问我为什么, 男人的直觉, 就是这么准.

第8行中, 比较了eax与6的大小,eax的值就是string_length函数的返回值, 不信的话你可以自己调试看看. 所以从这里我们可以获得一个重要信息, 这个字符串的长度是6

对了, 第5行也有重要信息, 这个push语句很显然是在为调用string_length函数准备参数, 所以ebx寄存器中存储的就是指向字符串首字符的指针, 有内味了!

然后第11行的赋值让eax也指向了字符串首, 由于字符串长度为6, 所以由12行可知, 这里指向了串尾的后一个, 很显然是我们常用的str[i] != 0的结束标志

14行15行注释说的很清楚了, 就是对字符做了一些处理

第16行就是将ecx加上对应地址处的值, 这里又出现了前面的那个跳转表有木有, 我们可以在gdb中看一看那个地址处的值是啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码(gdb) print (int)*(0x804a020)
$2 = 2
(gdb) print (int)*(0x804a024)
$3 = 10
(gdb) print (int)*(0x804a028)
$4 = 6
(gdb) print (int)*(0x804a02c)
$5 = 1
(gdb) print (int)*(0x804a030)
$6 = 12
(gdb) print (int)*(0x804a034)
$7 = 16
(gdb) print (int)*(0x804a038)
$8 = 9
(gdb) print (int)*(0x804a03c)
$9 = 3
(gdb) ...

17~19行我们可以知道一共要加6次

20行中我们可以看出加了6次后的值要等于0x2c

所以一个可能的结果就是10 + 10 + 10 + 10 + 2 + 2

10代表对应的字符低四位为1, 2代表低四位的值为0, 这个是由跳转表地址以及对应值的关系得到的,所以一个可能的答案是111100

最后, 到了这次实验中最难的一题, 我写了不少注释才看懂这一题, 这一题大概花了我两个多小时

阶段六

这一题的代码巨长无比, 如果不是因为要交作业, 我是不会去做这个的QwQ

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
88
复制代码08048d5d <phase_6>:
8048d5d: 56 push %esi
8048d5e: 53 push %ebx
8048d5f: 83 ec 4c sub $0x4c,%esp
8048d62: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048d68: 89 44 24 44 mov %eax,0x44(%esp)
8048d6c: 31 c0 xor %eax,%eax
8048d6e: 8d 44 24 14 lea 0x14(%esp),%eax
8048d72: 50 push %eax
8048d73: ff 74 24 5c pushl 0x5c(%esp)
8048d77: e8 42 03 00 00 call 80490be <read_six_numbers>
8048d7c: 83 c4 10 add $0x10,%esp
8048d7f: be 00 00 00 00 mov $0x0,%esi
8048d84: 8b 44 b4 0c mov 0xc(%esp,%esi,4),%eax
8048d88: 83 e8 01 sub $0x1,%eax
8048d8b: 83 f8 05 cmp $0x5,%eax
8048d8e: 76 05 jbe 8048d95 <phase_6+0x38>
8048d90: e8 04 03 00 00 call 8049099 <explode_bomb>
8048d95: 83 c6 01 add $0x1,%esi;这里就是限定了数的取值范围
8048d98: 83 fe 06 cmp $0x6,%esi
8048d9b: 74 1b je 8048db8 <phase_6+0x5b>
8048d9d: 89 f3 mov %esi,%ebx
8048d9f: 8b 44 9c 0c mov 0xc(%esp,%ebx,4),%eax
8048da3: 39 44 b4 08 cmp %eax,0x8(%esp,%esi,4)
8048da7: 75 05 jne 8048dae <phase_6+0x51>
8048da9: e8 eb 02 00 00 call 8049099 <explode_bomb>
8048dae: 83 c3 01 add $0x1,%ebx;这里限定了数字两两不能相等
8048db1: 83 fb 05 cmp $0x5,%ebx
8048db4: 7e e9 jle 8048d9f <phase_6+0x42>
8048db6: eb cc jmp 8048d84 <phase_6+0x27>
8048db8: 8d 44 24 0c lea 0xc(%esp),%eax;指向第1个数
8048dbc: 8d 5c 24 24 lea 0x24(%esp),%ebx;指向第6个数的后一个,也就是end标志
8048dc0: b9 07 00 00 00 mov $0x7,%ecx
8048dc5: 89 ca mov %ecx,%edx;an = 7 - an, 后面记得还要再变回来
8048dc7: 2b 10 sub (%eax),%edx
8048dc9: 89 10 mov %edx,(%eax)
8048dcb: 83 c0 04 add $0x4,%eax;指向下一个
8048dce: 39 c3 cmp %eax,%ebx
8048dd0: 75 f3 jne 8048dc5 <phase_6+0x68>;;
8048dd2: bb 00 00 00 00 mov $0x0,%ebx


8048dd7: eb 16 jmp 8048def <phase_6+0x92>
8048dd9: 8b 52 08 mov 0x8(%edx),%edx;大于1跳到这里, 这里目测是链表的 p = p -> next操作
8048ddc: 83 c0 01 add $0x1,%eax
8048ddf: 39 c8 cmp %ecx,%eax
8048de1: 75 f6 jne 8048dd9 <phase_6+0x7c>
8048de3: 89 54 b4 24 mov %edx,0x24(%esp,%esi,4);小于等于1跳到这里,这里的操作就是把链表指针依次放到这6个数的后面


8048de7: 83 c3 01 add $0x1,%ebx
8048dea: 83 fb 06 cmp $0x6,%ebx
8048ded: 74 17 je 8048e06 <phase_6+0xa9>;这里的处理完后,这6个数后面的链表指针的大小1排序就对应着这6个数的排序
`8048def: 89 de mov %ebx,%esi
8048df1: 8b 4c 9c 0c mov 0xc(%esp,%ebx,4),%ecx;从输入后处理完的第一个数开始遍历
8048df5: b8 01 00 00 00 mov $0x1,%eax
8048dfa: ba 3c c1 04 08 mov $0x804c13c,%edx;这个是链表的首地址
8048dff: 83 f9 01 cmp $0x1,%ecx;这个数和1进行比较
8048e02: 7f d5 jg 8048dd9 <phase_6+0x7c>;大于跳转
8048e04: eb dd jmp 8048de3 <phase_6+0x86>;小于或者等于跳转
8048e06: 8b 5c 24 24 mov 0x24(%esp),%ebx;这个值就是这6个数后面的第一个链表指针的值
8048e0a: 8d 44 24 24 lea 0x24(%esp),%eax;指向了后面第一个链表指针
8048e0e: 8d 74 24 38 lea 0x38(%esp),%esi;指向最后一个链表指针
8048e12: 89 d9 mov %ebx,%ecx
8048e14: 8b 50 04 mov 0x4(%eax),%edx;指向栈上第二个链表指针
8048e17: 89 51 08 mov %edx,0x8(%ecx);让栈上第一个链表指针指向栈上第二个链表指针
8048e1a: 83 c0 04 add $0x4,%eax
8048e1d: 89 d1 mov %edx,%ecx
8048e1f: 39 c6 cmp %eax,%esi
8048e21: 75 f1 jne 8048e14 <phase_6+0xb7>;这个循环就是让栈上这6个链表节点按栈上面的顺序建立连接关系
8048e23: c7 42 08 00 00 00 00 movl $0x0,0x8(%edx);最后一个指向null
8048e2a: be 05 00 00 00 mov $0x5,%esi
8048e2f: 8b 43 08 mov 0x8(%ebx),%eax
8048e32: 8b 00 mov (%eax),%eax;现在eax指向了第二个节点了
8048e34: 39 03 cmp %eax,(%ebx);比较这两个节点的值?
8048e36: 7d 05 jge 8048e3d <phase_6+0xe0>;前一个需要大于或者等于后面一个
8048e38: e8 5c 02 00 00 call 8049099 <explode_bomb>
8048e3d: 8b 5b 08 mov 0x8(%ebx),%ebx
8048e40: 83 ee 01 sub $0x1,%esi
8048e43: 75 ea jne 8048e2f <phase_6+0xd2>
8048e45: 8b 44 24 3c mov 0x3c(%esp),%eax
8048e49: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048e50: 74 05 je 8048e57 <phase_6+0xfa>
8048e52: e8 39 f9 ff ff call 8048790 <__stack_chk_fail@plt>
8048e57: 83 c4 44 add $0x44,%esp
8048e5a: 5b pop %ebx
8048e5b: 5e pop %esi
8048e5c: c3 ret`

写的有点累了, 这个不太想细讲了, 注释也写了蛮多了, 注意, 注释中说的链表指的是栈上面接在那6个数后面的部分.

从第8行开始看起, 这里又是熟悉的lea操作, 熟悉的push和call操作, 这个函数读了六个数字到栈上的指定地址.

后面用了很多代码就是进行了两个很无聊的操作, 一是限定了6个数的取值范围必须在1~6, 二是限定了6个数互不相同

然后进行了变换的操作

56行注意到有一个地址常量赋值的操作, 这个地址值就是链表头所在的地址, 通过gdb我们可以看到如下的信息


每个节点占12个字节, 第一个字节就是值, 第二个字节是id号, 第三个字节就是下一个节点的地址.

后面进行了将链表的节点按一定顺序放到栈上那6个数后面的操作, 然后还进行了栈上的链表的连接关系的建立操作, 也就是重新规定了每一个节点的下一个节点是谁(是它在栈上的后一个),最后有一个重大的信息, 就是这6个节点的顺序是按照值从大到小的,, 于是我们通过上图可以知道节点的id按照节点的值从大到小的顺序为6 2 4 3 1 5, 最后执行an = 7 - an的还原操作得到最终答案 1 5 3 4 6 2!

完结撒花!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%