第二个炸弹
接下来把断点打在第82行,开始拆除第二个炸弹。
run answer.txt
之后,还是mov %rax,%rdi
,然后调用phase_2
。
我们可以直接查看bomb.s
中的汇编代码,必要时再在gdb中操作。
后续列出的汇编代码进行了格式化,指令地址放在了;
开头的注释中。
phase_2 上
phase_2
汇编代码如下:
1push %rbp ;400efc
2push %rbx ;400efd
3sub $0x28,%rsp ;400efe
4mov %rsp,%rsi ;400f02
5call 40145c <read_six_numbers> ;400f05
6cmpl $0x1,(%rsp) ;400f0a
7je 400f30 <phase_2+0x34> ;400f0e
8call 40143a <explode_bomb> ;400f10
9jmp 400f30 <phase_2+0x34> ;400f15
10mov -0x4(%rbx),%eax ;400f17
11add %eax,%eax ;400f1a
12cmp %eax,(%rbx) ;400f1c
13je 400f25 <phase_2+0x29> ;400f1e
14call 40143a <explode_bomb> ;400f20
15add $0x4,%rbx ;400f25
16cmp %rbp,%rbx ;400f29
17jne 400f17 <phase_2+0x1b> ;400f2c
18jmp 400f3c <phase_2+0x40> ;400f2e
19lea 0x4(%rsp),%rbx ;400f30
20lea 0x18(%rsp),%rbp ;400f35
21jmp 400f17 <phase_2+0x1b> ;400f3a
22add $0x28,%rsp ;400f3c
23pop %rbx ;400f40
24pop %rbp ;400f41
25ret ;400f42
前3行和后3行在Introduction中已经介绍过了,第3行开辟了40字节的临时空间。
第4行的mov %rsp,%rsi
将%rsp
作为函数的第2个参数,而第5行的函数名是read_six_numbers
,似乎意味着在read_six_numbers
内,直接读取了6个数字放在了phase_2
的栈帧空间内。
这样有些武断,还是看下read_six_numbers
的汇编代码。
read_six_numbers
1sub $0x18,%rsp ;40145c
2mov %rsi,%rdx ;401460
3lea 0x4(%rsi),%rcx ;401463
4lea 0x14(%rsi),%rax ;401467
5mov %rax,0x8(%rsp) ;40146b
6lea 0x10(%rsi),%rax ;401470
7mov %rax,(%rsp) ;401474
8lea 0xc(%rsi),%r9 ;401478
9lea 0x8(%rsi),%r8 ;40147c
10mov $0x4025c3,%esi ;401480
11mov $0x0,%eax ;401485
12call 400bf0 <__isoc99_sscanf@plt> ;40148a
13cmp $0x5,%eax ;40148f
14jg 401499 <read_six_numbers+0x3d> ;401492
15call 40143a <explode_bomb> ;401494
16add $0x18,%rsp ;401499
17ret ;40149d
前2条指令都比较好理解(注意此时%rsi
指向phase_2
的栈顶)。
lea 0x4(%rsi),%rcx
这条指令的意思是,将%rsi
的值加上0x4
,结果保存到%rcx
中。
mov %rax,0x8(%rsp)
这条指令的意思是,将%rax
的内容放到首地址为(%rsp) + 0x8
的内存块中,其中(%rsp)
表示对%rsp
取内容。
所以在执行mov $0x4025c3,%esi
之前,相关寄存器和栈应该长这样:

图中紫色的是phase_2
的栈帧(注意里面的数字是十进制的),箭头表示这些寄存器和两个橙色的内存块(每个8 Bytes)是作为指针使用的,分别指向这些地方。所以我们之前判断的没错,phase_2
栈帧最后的24个字节,是作为6个参数(第3参数到第8参数)传入到sscanf
函数(<__isoc99_sscanf@plt>
)中保存读取到的6个数字的。
接下来的两条指令mov $0x4025c3,%esi
将0x4025c3
这个地址作为sscanf
的第2个参数,$0x0,%eax
预先将返回值设置为0。
那sscanf
的第一个参数呢?%rdi
的值没有改动过,所以它还是read_line
函数的返回值,也就是answer.txt
的第二行。
sscanf
的函数签名是int sscanf(const char *str, const char *format, ...);
,所以第二个参数0x4025c3
指向的就是格式化字符串,用x /s 0x4025c3
查看它的值:
%d %d %d %d %d %d
所以第二个炸弹的密码应该是6个用空格分隔的数字。
执行完sscanf
之后,还进行了参数检查:
cmp $0x5,%eax
:等价于执行(%eax) - 0x5
,但是不保存结果,而是用结果来更新标志寄存器。jg 401499
:Jump if Greater。
也就是如果sscanf
的返回值大于5就会跳过炸弹引爆函数。
phase_2 下
通过前面的分析我们已经知道,read_six_numbers
读取了6个数字保存在phase_2
的栈帧中。
接下来的几条指令:
6cmpl $0x1,(%rsp) ;400f0a
7je 400f30 <phase_2+0x34> ;400f0e
8call 40143a <explode_bomb> ;400f10
9jmp 400f30 <phase_2+0x34> ;400f15
判断第一个数字是不是1,如果不是1直接引爆炸弹,如果不是跳转到400f30
这条指令。
10mov -0x4(%rbx),%eax ;400f17
11add %eax,%eax ;400f1a
12cmp %eax,(%rbx) ;400f1c
13je 400f25 <phase_2+0x29> ;400f1e
14call 40143a <explode_bomb> ;400f20
15add $0x4,%rbx ;400f25
16cmp %rbp,%rbx ;400f29
17jne 400f17 <phase_2+0x1b> ;400f2c
18jmp 400f3c <phase_2+0x40> ;400f2e
19lea 0x4(%rsp),%rbx ;400f30
20lea 0x18(%rsp),%rbp ;400f35
21jmp 400f17 <phase_2+0x1b> ;400f3a
400f30
开始的这3条指令,计算了两个地址,然后跳转到了上方400f17
,如果一切顺利,指令从400f17
执行到了400f2c
,又会跳转回400f17
。
所以这是一个典型的循环结构,400f30
开始的这三条指令定义了循环的初始条件:%rbx
指向第2个数字,%rbp
指向第6个数字之上的位置。
然后看一下这个循环做了什么:
- 第10行:把前一个数字放到
%eax
。 - 第11行:等价于
%eax = (%eax) + (%eax)
,%eax
内的值变为原来的两倍。 - 第12-14行:如果
%rbx
(当前数字)和%eax
(前一个数字的两倍)相等,就跳过炸弹引爆函数。 - 第15-18行:将
%rbx
原地加4(移到下一个数字),并判断是否和%rbp
相等,相等说明6个数字已经循环完毕,需要跳出循环。
综上所述,这个炸弹的密码是第1个数字是1、公比为2、长度为6的等比数列,也就是1 2 4 8 16 32
,写入answer.txt
并运行:
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!
BOOM!!!
The bomb has blown up.
没有问题。
总结
这个炸弹主要让你了解在汇编层面循环是如何实现的,解密过程也更复杂一些。