第四个炸弹
直接看phase_4
汇编代码:
1sub $0x18,%rsp ;40100c
2lea 0xc(%rsp),%rcx ;401010
3lea 0x8(%rsp),%rdx ;401015
4mov $0x4025cf,%esi ;40101a
5mov $0x0,%eax ;40101f
6call 400bf0 <__isoc99_sscanf@plt> ;401024
7cmp $0x2,%eax ;401029
8jne 401035 <phase_4+0x29> ;40102c
秒了,读取两个数字放在0x8(%rsp)
和0xc(%rsp)
,甚至格式化字符串地址都和phase_3
一样。jne
的意思是Jump if Not Equal,和之前的校验sscanf
返回值大同小异。
接下来三行:
9cmpl $0xe,0x8(%rsp) ;40102e
10jbe 40103a <phase_4+0x2e> ;401033
11call 40143a <explode_bomb> ;401035
jbe
的意思是Jump if Below or Equal,也就是第一个数字小于等于14才会跳过炸弹引爆函数。
剩余的部分:
12mov $0xe,%edx ;40103a
13mov $0x0,%esi ;40103f
14mov 0x8(%rsp),%edi ;401044
15call 400fce <func4> ;401048
16test %eax,%eax ;40104d
17jne 401058 <phase_4+0x4c> ;40104f
18cmpl $0x0,0xc(%rsp) ;401051
19je 40105d <phase_4+0x51> ;401056
20call 40143a <explode_bomb> ;401058
21add $0x18,%rsp ;40105d
22ret ;401061
以第一个数字、0、14为实参调用func4
,如果返回值为0,跳过引爆。然后校验第二个数字是否是0,是0则跳过引爆。
所以接下来的重点是func4
函数:
1sub $0x8,%rsp ;400fce
2mov %edx,%eax ;400fd2
3sub %esi,%eax ;400fd4
4mov %eax,%ecx ;400fd6
5shr $0x1f,%ecx ;400fd8
6add %ecx,%eax ;400fdb
7sar %eax ;400fdd
8lea (%rax,%rsi,1),%ecx ;400fdf
9cmp %edi,%ecx ;400fe2
10jle 400ff2 <func4+0x24> ;400fe4
11lea -0x1(%rcx),%edx ;400fe6
12call 400fce <func4> ;400fe9
13add %eax,%eax ;400fee
14jmp 401007 <func4+0x39> ;400ff0
15mov $0x0,%eax ;400ff2
16cmp %edi,%ecx ;400ff7
17jge 401007 <func4+0x39> ;400ff9
18lea 0x1(%rcx),%esi ;400ffb
19call 400fce <func4> ;400ffe
20lea 0x1(%rax,%rax,1),%eax ;401003
21add $0x8,%rsp ;401007
22ret ;40100b
先略过细节,看12行的19行:call 400fce <func4>
,嗯,这是个递归。如果还按指令顺序一条一条的看,会陷入这个递归泥沼,解决这个炸弹比较好的方法是写出对应的C代码。
首先我们看整个函数只用到了%edi
、%rsi/%esi
、%edx
、%rax/%eax
和%ecx
,根据寄存器用途约定,我们可以把他们分别替换为a
、b
、c
、res
和t
,分别代表func4
的三个参数、返回值和临时变量。
另外还需要注意,401007
是函数的出口,跳转到这里就意味着执行return res;
这行代码,而19-22这几行代码合起来对应的C代码就是:
res = func4(a, b, c);
return res + res - 1; // 0x1(%rax,%rax,1) → (%rax) + 0x1 + (%rax) * 1,前面学过的。
12-14行同理:
res = func4(a, b, c);
return res + res;
接下来开始分析对应的C代码。
第2-9行:
int res = c;
res = res - b;
int t = res;
t = t >> 31;
res = res + t;
res = res > 1;
t = res + b;
if (t <= a) {} else {}
说明:
sub
/add
是op2 = op2 +/- op1
。shr
/sar
分别是逻辑右移和算数右移,逻辑右移不管符号位整体右移,算数右移会考虑符号位的影响,它们两个对应的C操作符都是>>
,具体是逻辑右移还是符号右移,由编译器根据变量的类型决定。cmp
是op2 - op1
,配合jle
(Jump if Less or Equal)的语义,if语句的条件应该是t <= a
(想反过来也行)。
一条汇编指令写一条C代码太啰嗦了,整理一下上面的代码。
对于t = t >> 31
这一行代码,其实t
就是c - b
的符号位,也就是c >= b
是t
为0,c < b
时t
为1。
我们可以借助这个让代码更有可读性:
int res = c >= b ? (c - b) / 2 : (c - b + 1) / 2;
int t = res + b;
if (t <= a) {} else {}
如果你对二分查找比较熟悉的话,立马就可以想到,这就是一个防溢出版本的求两个int平均数的做法。如果不熟悉,可以分两种情况代入验证一下。
%rax
在这里显然是用户保存中间计算结果的,后面肯定会被覆盖掉,因此到目前为止的代码可以精简为(不考虑溢出):
int func4(int a, int b, int c) {
int res;
int t = (b + c) / 2;
if (t <= a) {} else {}
}
对于条件为真的分支,跳转到了第15行;条件为假的分支则顺序执行第11行。代码都没什么难点,可以轻松写出对应的C代码:
int func4(int a, int b, int c) {
int res;
int t = (b + c) / 2;
if (t <= a) {
res = 0;
if (t >= a) {
return res;
} else {
res = func4(a, t + 1, c);
return res + res - 1;
}
} else {
res = func4(a, b, t - 1);
return res + res;
}
}
好像有点别扭,保持逻辑不变修改一下:
int func4(int a, int b, int c) {
int res;
int t = (b + c) / 2;
if (t < a) {
res = func4(a, b, t - 1);
return res + res;
} else if (t > a) {
res = func4(a, t + 1, c);
return res + res - 1;
} else {
return 0;
}
}
我们代入b = 0, c = 14
分析一下这个函数(其实就是一个二分查找函数):
如果7 < a <= 14
,res + res - 1
是肯定不等于0的,也就是如果初始入参为大于7的数,肯定会引爆炸弹。
如果a = 7
,第一轮递归就返回0,不会引爆炸弹。
如果a < 0
,函数会陷入死循环。
如果0 <= a < 7
,嗯,直接跑代码吧:
0->0 1->0 2->4 3->0 4->2 5->2 6->6
所以这个炸弹的密码可以是0 0
、1 0
、3 0
或者7 0
中的一组(这里的0是第二个数字),选一个填入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!
Halfway there!
So you got that one. Try this one.
BOOM!!!
The bomb has blown up.