第四个炸弹

2025-05-18
1655字

直接看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,根据寄存器用途约定,我们可以把他们分别替换为abcrest,分别代表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/addop2 = op2 +/- op1
  • shr/sar分别是逻辑右移和算数右移,逻辑右移不管符号位整体右移,算数右移会考虑符号位的影响,它们两个对应的C操作符都是>>,具体是逻辑右移还是符号右移,由编译器根据变量的类型决定。
  • cmpop2 - op1,配合jle(Jump if Less or Equal)的语义,if语句的条件应该是t <= a(想反过来也行)。

一条汇编指令写一条C代码太啰嗦了,整理一下上面的代码。

对于t = t >> 31这一行代码,其实t就是c - b的符号位,也就是c >= bt为0,c < bt为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 <= 14res + 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 01 03 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.