目录

Bomb lab 实验总结

我看见网上非常多的教程里面是一边使用gdb调试(过程中可能引爆炸弹),一边探索结果的。他们可能多次启动了GDB来完成实验。但是我在bomb.c中看到了如下指示:

text

No VICTIM may debug, reverse-engineer, run "strings" on, decompile, decrypt, or use any other technique to gain knowledge of and defuse the BOMB.

并且在 CSAPP 3e 的 handout 中也写的很清楚,不应该多次启动 gdb 来通过输入随机序列的方式来探测密码,毕竟解引的机会只有一次。如果引爆炸弹,autolab 的分数会掉。所以正确的方法应该是对汇编程序进行逆向分析,然后推算炸弹的密码,而不是通过多次输入字符串来利用炸弹进行测试。

不要一开始就开gdb运行phase,可以先启动gdb,在main函数部分打断点,因为很多地方需要从gdb中获取信息,但是我们又不能直接开始做题,因为一做题就无法二次启动gdb了(会扣实验分)。最好是把每个phase都分析透彻,得出确定的答案了,再在gdb中运行,因为中间没有空闲给你打断点和运行,直接进入gets输入了。

6 道题的答案为(如果用文件输入的话,记得最后有个换行符,否则炸弹爆炸):

text

Border relations with Canada have never been better.
1 2 4 8 16 32
0 207
0 0 DrEvil
IONEFG
4 3 2 1 6 5
20

首先我们使用wget命令从实验官网下载tar文件,使用tar xvf bomb.tar解压文件。

打开文件夹,里面有 2 个重要文件,分别是bomb.cbomb二进制文件。我们使用objdump -d bomb > bomb.s生成二进制可执行文件的反汇编并存储在bomb.s中。bomb.c中只有main函数的部分,main函数调用的那些函数只在二进制文件和汇编代码中存在。

我们首先来看bomb.c,其中含有 6 个 phase 的炸弹,我们需要依次解开。在函数最开始处作者声明可以有多种输入格式,可以从stdin获取输入,也可以从文件获取输入。每个 phase 都有如下两行:

c

phase_n(input);      /* Run the phase               */
phase_defused();     /* Drat!  They figured it out!

phase_1的汇编代码如下:

text

0000000000400ee0 <phase_1>:
  400ee0:	48 83 ec 08          	sub    $0x8,%rsp
  400ee4:	be 00 24 40 00       	mov    $0x402400,%esi
  400ee9:	e8 4a 04 00 00       	callq  401338 <strings_not_equal>
  400eee:	85 c0                	test   %eax,%eax
  400ef0:	74 05                	je     400ef7 <phase_1+0x17>
  400ef2:	e8 43 05 00 00       	callq  40143a <explode_bomb>
  400ef7:	48 83 c4 08          	add    $0x8,%rsp
  400efb:	c3                   	retq

其过程如下:

  1. 留出 8 个字节的空间
  2. %esi = 0x402400
  3. 调用strings_not_equal函数
  4. 测试%eax是否等于 0
  5. 若返回值为 0,则恢复栈帧,退出
  6. 否则引爆炸弹

这里推测%esi是给strings_not_equal提供的参数,且string_not_equal返回的是个int类型整数。

我们打开gdb,打好断点并运行,输入call strings_not_equal("a","a")发现返回 0。输入call string_length("a")发现返回 1。那么strings_not_equal的作用就是判断两字符串是否不相等,string_length的作用就是计算字符串的长度。

我们的目的是让strings_not_equal返回 0,也就是说我们需要传入两个相同的字符串。那么问题在于我们传入的字符串存储在哪里呢?我们看一下strings_not_equal的反汇编代码:

asm

0000000000401338 <strings_not_equal>:
seg1:
  401338:	41 54                	push   %r12
  40133a:	55                   	push   %rbp
  40133b:	53                   	push   %rbx
  40133c:	48 89 fb             	mov    %rdi,%rbx
  40133f:	48 89 f5             	mov    %rsi,%rbp
  401342:	e8 d4 ff ff ff       	callq  40131b <string_length>
  401347:	41 89 c4             	mov    %eax,%r12d
  40134a:	48 89 ef             	mov    %rbp,%rdi
  40134d:	e8 c9 ff ff ff       	callq  40131b <string_length>
  401352:	ba 01 00 00 00       	mov    $0x1,%edx

  401357:	41 39 c4             	cmp    %eax,%r12d
  40135a:	75 3f                	jne    40139b <strings_not_equal+0x63>
  40135c:	0f b6 03             	movzbl (%rbx),%eax

  40135f:	84 c0                	test   %al,%al
  401361:	74 25                	je     401388 <strings_not_equal+0x50>

  401363:	3a 45 00             	cmp    0x0(%rbp),%al
  401366:	74 0a                	je     401372 <strings_not_equal+0x3a>
  401368:	eb 25                	jmp    40138f <strings_not_equal+0x57>
seg2:
  40136a:	3a 45 00             	cmp    0x0(%rbp),%al
  40136d:	0f 1f 00             	nopl   (%rax)
  401370:	75 24                	jne    401396 <strings_not_equal+0x5e>
seg3:
  401372:	48 83 c3 01          	add    $0x1,%rbx
  401376:	48 83 c5 01          	add    $0x1,%rbp
  40137a:	0f b6 03             	movzbl (%rbx),%eax

  40137d:	84 c0                	test   %al,%al
  40137f:	75 e9                	jne    40136a <strings_not_equal+0x32>
  401381:	ba 00 00 00 00       	mov    $0x0,%edx
  401386:	eb 13                	jmp    40139b <strings_not_equal+0x63>
seg4:
  401388:	ba 00 00 00 00       	mov    $0x0,%edx
  40138d:	eb 0c                	jmp    40139b <strings_not_equal+0x63>
seg5:
  40138f:	ba 01 00 00 00       	mov    $0x1,%edx
  401394:	eb 05                	jmp    40139b <strings_not_equal+0x63>
seg6:
  401396:	ba 01 00 00 00       	mov    $0x1,%edx
seg7:
  40139b:	89 d0                	mov    %edx,%eax
  40139d:	5b                   	pop    %rbx
  40139e:	5d                   	pop    %rbp
  40139f:	41 5c                	pop    %r12
  4013a1:	c3                   	retq

其 C 语言代码分析如下:

c

seg2()
{
    if(al != *rbp) {
        seg6();
    }
    else{
        seg3();
    }
}

seg3()
{
    rbx += 1;
    rbp += 1;
    eax = *rbx;
    if(al != 0) {
        // seg2
        seg2();
    }
    else {
        edx = 0;
        seg7();
    }
}

seg4()
{
    edx = 0;
    seg7();
}

seg5()
{
    edx = 1;
    seg7();
}

seg6() {
    edx = 1;
    seg7();
}

seg7()
{
    eax = edx;
    //还原rbx rbp r12
    return eax;
}

// 传入参数有 rdi rsi,返回值在eax中
int strings_not_equal(char* str)
{
    // seg1
    // 保存r12 rbp rbx的值
    rbx = rdi
    rbp = rsi
    call string_length
    r12d = eax
    rdi = rbp
    call string_length
    edx = 0x1;

    if(r12d != eax) {
        seg7();
    }
    else {
        eax = *rbx;
        if(al == 0) {
            seg4();
        }
        else {
            if(al == *rbp) {
                seg3();
            }
            else {
                seg5();
            }
        }
    }
}

我们对调用的函数string_length反汇编看一下:

text

000000000040131b <string_length>:
  40131b:	80 3f 00             	cmpb   $0x0,(%rdi)
  40131e:	74 12                	je     401332 <string_length+0x17>
  401320:	48 89 fa             	mov    %rdi,%rdx
  401323:	48 83 c2 01          	add    $0x1,%rdx
  401327:	89 d0                	mov    %edx,%eax
  401329:	29 f8                	sub    %edi,%eax
  40132b:	80 3a 00             	cmpb   $0x0,(%rdx)
  40132e:	75 f3                	jne    401323 <string_length+0x8>
  401330:	f3 c3                	repz retq
  401332:	b8 00 00 00 00       	mov    $0x0,%eax
  401337:	c3                   	retq

其 C 语言转化如下:

c

// 传入参数为 rdi,推测其为字符串的首地址
int string_length()
{
    if(*rdi == 0x0) {
        rax = 0;
        return rax;
    }
    rdx = rdi;
    do{
        rdx += 0x1;
        eax = edx;
        eax -= edi;
    }while(*rdx != 0x0);
    return rax;
}

因为string_length中传入的应该是字符串首地址,存储在寄存器%rdi中。而strings_not_equal中我们使用了两个寄存器%rdi%rsi,那么%rsi中保存的应该是第二个字符串的地址。而在phase_1中,我们让%rsi = 0x402400,也就是说0x402400这里有个字符串,我们要让输入的字符串和这里的字符串相同,才能让%rax = 0

输入x/s 0x402400,得到如下结果:

text

(gdb) x/s 0x402400
0x402400:       "Border relations with Canada have never been better."

这就是我们应该在phase_1中输入的字符串了!让我们来试试!结果如下:

text

(gdb)
Phase 1 defused. How about the next one?
81          printf("Phase 1 defused. How about the next one?\n");

成功!现在准备解决phase_2的问题,gdb先不管。

phase_2的反汇编如下:

text

0000000000400efc <phase_2>:
  400efc:	55                   	push   %rbp
  400efd:	53                   	push   %rbx
  400efe:	48 83 ec 28          	sub    $0x28,%rsp
  400f02:	48 89 e6             	mov    %rsp,%rsi
  400f05:	e8 52 05 00 00       	callq  40145c <read_six_numbers>
  400f0a:	83 3c 24 01          	cmpl   $0x1,(%rsp)
  400f0e:	74 20                	je     400f30 <phase_2+0x34>
  400f10:	e8 25 05 00 00       	callq  40143a <explode_bomb>

  400f15:	eb 19                	jmp    400f30 <phase_2+0x34>

  400f17:	8b 43 fc             	mov    -0x4(%rbx),%eax
  400f1a:	01 c0                	add    %eax,%eax
  400f1c:	39 03                	cmp    %eax,(%rbx)
  400f1e:	74 05                	je     400f25 <phase_2+0x29>
  400f20:	e8 15 05 00 00       	callq  40143a <explode_bomb>

  400f25:	48 83 c3 04          	add    $0x4,%rbx
  400f29:	48 39 eb             	cmp    %rbp,%rbx
  400f2c:	75 e9                	jne    400f17 <phase_2+0x1b>
  400f2e:	eb 0c                	jmp    400f3c <phase_2+0x40>

  400f30:	48 8d 5c 24 04       	lea    0x4(%rsp),%rbx
  400f35:	48 8d 6c 24 18       	lea    0x18(%rsp),%rbp
  400f3a:	eb db                	jmp    400f17 <phase_2+0x1b>

  400f3c:	48 83 c4 28          	add    $0x28,%rsp
  400f40:	5b                   	pop    %rbx
  400f41:	5d                   	pop    %rbp
  400f42:	c3                   	retq

我们将其转化为 C 语言试试:

c

     // // 400f17
     // eax = *(rbx - 0x4);
     // eax *= 2;
     // if(*rbx == eax) {
     //     rbx += 4;
     //     if(rbx != rbp)
     //         //400f17
     //     else
     //         //400f3c
     //         //恢复栈空间和rbp rbx
     //         return eax;
     // }else {
     //     call explode_bomb
     // }
int phase_2()
{
    //保存rbp rbx
    //rsp留出40个字节
    rsi = rsp;
    call read_six_numbers
    if(*rsp == 0x1) {
        // 400f30
        rbx = rsp + 0x4;
        rbp = rsp + 24;

        // 400f17改写为循环
        do {
            eax = *(rbx - 0x4);
            eax *= 2;
            if(*rbx != eax)
                call explode_bomb
            rbx += 4;
        }while(rbx != rbp)
        return eax;
    }
    else {
        call explode_bomb
    }
}

我们对read_six_numebrs反汇编一下看看:

text

000000000040145c <read_six_numbers>:
  40145c:	48 83 ec 18          	sub    $0x18,%rsp
  401460:	48 89 f2             	mov    %rsi,%rdx
  401463:	48 8d 4e 04          	lea    0x4(%rsi),%rcx
  401467:	48 8d 46 14          	lea    0x14(%rsi),%rax
  40146b:	48 89 44 24 08       	mov    %rax,0x8(%rsp)
  401470:	48 8d 46 10          	lea    0x10(%rsi),%rax
  401474:	48 89 04 24          	mov    %rax,(%rsp)
  401478:	4c 8d 4e 0c          	lea    0xc(%rsi),%r9
  40147c:	4c 8d 46 08          	lea    0x8(%rsi),%r8
  401480:	be c3 25 40 00       	mov    $0x4025c3,%esi
  401485:	b8 00 00 00 00       	mov    $0x0,%eax
  40148a:	e8 61 f7 ff ff       	callq  400bf0 <__isoc99_sscanf@plt>
  40148f:	83 f8 05             	cmp    $0x5,%eax
  401492:	7f 05                	jg     401499 <read_six_numbers+0x3d>
  401494:	e8 a1 ff ff ff       	callq  40143a <explode_bomb>
  401499:	48 83 c4 18          	add    $0x18,%rsp
  40149d:	c3                   	retq

这里也有explode_bomb,有玄机啊~

先分析read_six_numbers,这里就一个跳转,整个过程容易分析:

text

1. 保留 24 个字节的位置
2. rdx = rsi
3. rcx = rsi + 0x4
4. rax = rsi + 20
5. *(rsp + 8) = rax
6. rax = rsi + 16
7. *rsp = rax
8. r9 = rsi + 12
9. r8 = rsi + 8
10. esi = 0x4025c3
11. eax = 0
12. call sccanf 获取输入
13. if(eax > 5) 恢复栈,返回 eax
14. 否则爆炸

这里传入的应该只有一个参数,就是%rsi寄存器中的值,在前面phase_2里面%rsi = %rsp。那么输入的值应该是从%rsp开始存储。我们使用如下命令打印0x4025c3处的值,得到如下结果:

text

(gdb) x/s 0x4025c3
0x4025c3:       "%d %d %d %d %d %d"

这里%eax中存储的应该是sscanf读入的数据个数,如果%eax > 5的话,read_six_numbers顺利过关,否则爆炸。

那么数组应该是保存在%rsi指向的位置(也就是一开始%rsp指向的位置),通过分析phase_2的代码,我们可以总结出以下要求:

  1. 输入的第 1 个数必须是 1
  2. 下一个数是前一个数的两倍
  3. 输入 6 个数字,而且是整数,不是浮点数

那么了解了要求后,我们需要输入1 2 4 8 16 32这 6 个数,看看结果:

text

(gdb)
That's number 2.  Keep going!
88          printf("That's number 2.  Keep going!\n");

phase_2破解成功!我们继续phase_3

phase_3的反汇编代码如下:

text

0000000000400f43 <phase_3>:
  400f43:	48 83 ec 18          	sub    $0x18,%rsp
  400f47:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
  400f4c:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx
  400f51:	be cf 25 40 00       	mov    $0x4025cf,%esi
  400f56:	b8 00 00 00 00       	mov    $0x0,%eax
  400f5b:	e8 90 fc ff ff       	callq  400bf0 <__isoc99_sscanf@plt>
  400f60:	83 f8 01             	cmp    $0x1,%eax
  400f63:	7f 05                	jg     400f6a <phase_3+0x27>
  400f65:	e8 d0 04 00 00       	callq  40143a <explode_bomb>

  400f6a:	83 7c 24 08 07       	cmpl   $0x7,0x8(%rsp)
  400f6f:	77 3c                	ja     400fad <phase_3+0x6a>
  400f71:	8b 44 24 08          	mov    0x8(%rsp),%eax
  400f75:	ff 24 c5 70 24 40 00 	jmpq   *0x402470(,%rax,8)
  400f7c:	b8 cf 00 00 00       	mov    $0xcf,%eax
  400f81:	eb 3b                	jmp    400fbe <phase_3+0x7b>
  400f83:	b8 c3 02 00 00       	mov    $0x2c3,%eax
  400f88:	eb 34                	jmp    400fbe <phase_3+0x7b>
  400f8a:	b8 00 01 00 00       	mov    $0x100,%eax
  400f8f:	eb 2d                	jmp    400fbe <phase_3+0x7b>
  400f91:	b8 85 01 00 00       	mov    $0x185,%eax
  400f96:	eb 26                	jmp    400fbe <phase_3+0x7b>
  400f98:	b8 ce 00 00 00       	mov    $0xce,%eax
  400f9d:	eb 1f                	jmp    400fbe <phase_3+0x7b>
  400f9f:	b8 aa 02 00 00       	mov    $0x2aa,%eax
  400fa4:	eb 18                	jmp    400fbe <phase_3+0x7b>
  400fa6:	b8 47 01 00 00       	mov    $0x147,%eax
  400fab:	eb 11                	jmp    400fbe <phase_3+0x7b>

  400fad:	e8 88 04 00 00       	callq  40143a <explode_bomb>
  400fb2:	b8 00 00 00 00       	mov    $0x0,%eax
  400fb7:	eb 05                	jmp    400fbe <phase_3+0x7b>
  400fb9:	b8 37 01 00 00       	mov    $0x137,%eax

  400fbe:	3b 44 24 0c          	cmp    0xc(%rsp),%eax
  400fc2:	74 05                	je     400fc9 <phase_3+0x86>
  400fc4:	e8 71 04 00 00       	callq  40143a <explode_bomb>

  400fc9:	48 83 c4 18          	add    $0x18,%rsp
  400fcd:	c3                   	retq

这段代码较长,我们将其转化为 C 语言试试

我们先不转化成 C 语言,先来看一下让炸弹爆炸的几个条件。炸弹爆炸出现在如下几个地方:

text

  400f65:	e8 d0 04 00 00       	callq  40143a <explode_bomb>
  400fad:	e8 88 04 00 00       	callq  40143a <explode_bomb>
  400fc4:	e8 71 04 00 00       	callq  40143a <explode_bomb>

对第一处,我们先进行分析:

text

  400f43:	48 83 ec 18          	sub    $0x18,%rsp
  400f47:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
  400f4c:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx
  400f51:	be cf 25 40 00       	mov    $0x4025cf,%esi
  400f56:	b8 00 00 00 00       	mov    $0x0,%eax
  400f5b:	e8 90 fc ff ff       	callq  400bf0 <__isoc99_sscanf@plt>
  400f60:	83 f8 01             	cmp    $0x1,%eax
  400f63:	7f 05                	jg     400f6a <phase_3+0x27>
  400f65:	e8 d0 04 00 00       	callq  40143a <explode_bomb>

这段代码首先分配了 24 个字节的栈空间,随后做了如下步骤:

  1. rcx = rsp + 12
  2. rdx = rsp + 8
  3. esi = 0x4025cf
  4. eax = 0
  5. call sscanf
  6. 将 eax 与 1 进行比较
  7. 如果 eax > 1,跳转到400f6a
  8. 否则炸弹爆炸

这段代码和我们上一段看到的sscanf之前的代码非常相似,0x4025cf多半是一个让你输入一串东西的指令地址,我们看一下%esi(0x4025cf)附近的值等于多少:

text

(gdb) x/s 0x4025cf
0x4025cf:       "%d %d"

这里说明我们需要输入 2 个数字,如果不是输入 2 个数字的话,炸弹就会爆炸。第一个要求明确了。

再来看第二处炸弹的位置前后的代码,它的上一条代码是个jmp,那么就不可能从那儿执行到这儿来,因为炸弹爆炸了,所以他后面的代码也不可能执行,所以我们只需要看看哪些代码会跳转到这一条(400fad)上来。

text

  400f6a:	83 7c 24 08 07       	cmpl   $0x7,0x8(%rsp)
  400f6f:	77 3c                	ja     400fad <phase_3+0x6a>

这段代码出现在我们获取了两个整数之后。这段代码比较了*(rsp + 8)0x7的值。为了不跳转到400fad,我们需要让%rsp + 8 <= 0x7。第二个要求明确了。

最后一个炸弹前的代码如下:

text

  400fb9:	b8 37 01 00 00       	mov    $0x137,%eax

  400fbe:	3b 44 24 0c          	cmp    0xc(%rsp),%eax
  400fc2:	74 05                	je     400fc9 <phase_3+0x86>
  400fc4:	e8 71 04 00 00       	callq  40143a <explode_bomb>

这段代码中实现如下操作:

  1. eax = 0x137
  2. 如果eax != *(rsp + 12),那么炸弹爆炸,也就是说输入的第二个参数最后必须等于0x137

现在问题来了,有几个代码通过跳转来到了400fbe位置,我们需要分析其情况,看看其是否对%rsp + 12处的值做了操作。

text

  400f7c:	b8 cf 00 00 00       	mov    $0xcf,%eax ->eax = 0xcf
  400f81:	eb 3b                	jmp    400fbe <phase_3+0x7b>
  400f83:	b8 c3 02 00 00       	mov    $0x2c3,%eax -> eax = 0x2c3
  400f88:	eb 34                	jmp    400fbe <phase_3+0x7b>
  400f8a:	b8 00 01 00 00       	mov    $0x100,%eax -> eax = 0x100
  400f8f:	eb 2d                	jmp    400fbe <phase_3+0x7b>
  400f91:	b8 85 01 00 00       	mov    $0x185,%eax -> eax = 0x185
  400f96:	eb 26                	jmp    400fbe <phase_3+0x7b>
  400f98:	b8 ce 00 00 00       	mov    $0xce,%eax -> eax = 0xce
  400f9d:	eb 1f                	jmp    400fbe <phase_3+0x7b>
  400f9f:	b8 aa 02 00 00       	mov    $0x2aa,%eax -> eax = 0x2aa
  400fa4:	eb 18                	jmp    400fbe <phase_3+0x7b>
  400fa6:	b8 47 01 00 00       	mov    $0x147,%eax -> eax = 0x147
  400fab:	eb 11                	jmp    400fbe <phase_3+0x7b>

  400fad:	e8 88 04 00 00       	callq  40143a <explode_bomb>
  400fb2:	b8 00 00 00 00       	mov    $0x0,%eax -> eax = 0
  400fb7:	eb 05                	jmp    400fbe <phase_3+0x7b>
  400fb9:	b8 37 01 00 00       	mov    $0x137,%eax -> eax = 0x137

  400fbe:	3b 44 24 0c          	cmp    0xc(%rsp),%eax
  400fc2:	74 05                	je     400fc9 <phase_3+0x86>
  400fc4:	e8 71 04 00 00       	callq  40143a <explode_bomb>

要注意的是指令400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8)中实现的是间接跳转(因为加了*),那么我们需要找到0x402470处的 8 字节地址,以它为跳转的基地址。

text

(gdb) x/wx 0x402470
0x402470:       0x00400f7c

这个地址就是0x400f7c。我们前面把输入的第一个数字移到了%eax,那么我们需要通过0x400f7c + 8 * %eax来确定我们的跳转目标。跳转以后会赋给%eax相应的值,而后我们需要保证我们输入的第二个数等于%eax

这样逻辑就捋顺了,整个代码应该是一个switch_case的结构,而0x400f7c就是这个跳转表的首地址,%eax是对应的索引。这个phase的要求就是:

  1. 输入 2 个整数
  2. 第一个整数必须小于等于 0x7,因为最多只有 7 个case
  3. 输入的第二个数必须和其case中赋给%eax的值相同

我们设计第一个数是0,那么第二个数就是0xcf(207),我们试一试:

text

main (argc=<optimized out>, argv=<optimized out>) at bomb.c:90
90          /* I guess this is too easy so far.  Some more complex code will
(gdb) n
91           * confuse people. */
(gdb) n
Halfway there!
94          phase_defused();

成功!

首先还是对phase_4进行了反汇编:

text

000000000040100c <phase_4>:
  40100c:	48 83 ec 18          	sub    $0x18,%rsp
  401010:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
  401015:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx
  40101a:	be cf 25 40 00       	mov    $0x4025cf,%esi
  40101f:	b8 00 00 00 00       	mov    $0x0,%eax
  401024:	e8 c7 fb ff ff       	callq  400bf0 <__isoc99_sscanf@plt>
  401029:	83 f8 02             	cmp    $0x2,%eax
  40102c:	75 07                	jne    401035 <phase_4+0x29>

  40102e:	83 7c 24 08 0e       	cmpl   $0xe,0x8(%rsp)
  401033:	76 05                	jbe    40103a <phase_4+0x2e>
  401035:	e8 00 04 00 00       	callq  40143a <explode_bomb>

  40103a:	ba 0e 00 00 00       	mov    $0xe,%edx
  40103f:	be 00 00 00 00       	mov    $0x0,%esi
  401044:	8b 7c 24 08          	mov    0x8(%rsp),%edi
  401048:	e8 81 ff ff ff       	callq  400fce <func4>
  40104d:	85 c0                	test   %eax,%eax
  40104f:	75 07                	jne    401058 <phase_4+0x4c>
  401051:	83 7c 24 0c 00       	cmpl   $0x0,0xc(%rsp)
  401056:	74 05                	je     40105d <phase_4+0x51>

  401058:	e8 dd 03 00 00       	callq  40143a <explode_bomb>
  40105d:	48 83 c4 18          	add    $0x18,%rsp
  401061:	c3                   	retq

在本代码中进行了如下过程:

  1. 预留出 24 个字节的空间
  2. rcx = rsp + 12
  3. rdx = rsp + 8
  4. esi = 0x4025cf
  5. eax = 0
  6. sscanf 调用
  7. 下面是带分支部分:

c

if(eax != 0x2) {
    call bomb_explode
}
if(arg3 > 0xe) {
    call bomb_explode
}
edx = 0xe;
esi = 0x0;
edi = arg3
ret = func4(arg1,arg2,arg3)
if(eax != 0)
    call bomb_explode
if(arg2 == 0)
    return eax;
else
    call bomb_explode

这里出现了非常熟悉的sscanf调用,我们先看看0x4025cf处是啥(如果不出意外应该是%d %d

text

(gdb) x/s 0x4025cf
0x4025cf:       "%d %d"

果然!果然!是让我们输入 2 个整数,如果不是两个的话就爆炸!此外,一开始的arg3应该小于等于0xe,否则爆炸,此外函数func4返回的eax应该为 0,否则爆炸。此外,*(rsp + 12)(如果func4里面没改变这个值的话应该还是arg2)必须等于 0。

那么我们来分析一下func4

text

0000000000400fce <func4>:
  400fce:	48 83 ec 08          	sub    $0x8,%rsp
  400fd2:	89 d0                	mov    %edx,%eax
  400fd4:	29 f0                	sub    %esi,%eax
  400fd6:	89 c1                	mov    %eax,%ecx
  400fd8:	c1 e9 1f             	shr    $0x1f,%ecx
  400fdb:	01 c8                	add    %ecx,%eax
  400fdd:	d1 f8                	sar    %eax
  400fdf:	8d 0c 30             	lea    (%rax,%rsi,1),%ecx
  400fe2:	39 f9                	cmp    %edi,%ecx
  400fe4:	7e 0c                	jle    400ff2 <func4+0x24>

  400fe6:	8d 51 ff             	lea    -0x1(%rcx),%edx
  400fe9:	e8 e0 ff ff ff       	callq  400fce <func4>
  400fee:	01 c0                	add    %eax,%eax
  400ff0:	eb 15                	jmp    401007 <func4+0x39>

  400ff2:	b8 00 00 00 00       	mov    $0x0,%eax
  400ff7:	39 f9                	cmp    %edi,%ecx
  400ff9:	7d 0c                	jge    401007 <func4+0x39>

  400ffb:	8d 71 01             	lea    0x1(%rcx),%esi
  400ffe:	e8 cb ff ff ff       	callq  400fce <func4>
  401003:	8d 44 00 01          	lea    0x1(%rax,%rax,1),%eax

  401007:	48 83 c4 08          	add    $0x8,%rsp
  40100b:	c3                   	retq

看起来有点长,但是不要惊慌!这里只有 3 个跳转,而且其中两个都是跳转到最后恢复栈的位置。其主要步骤如下:

  1. 分配 8 个字节的栈空间
  2. eax = edx
  3. eax -= esi -> eax = edx - esi
  4. ecx = eax
  5. ecx » 31 -> ecx = eax » 31(eax 符号位拉满)
  6. eax += ecx
  7. eax » 1 -> eax = (eax + ecx) » 1
  8. ecx = rax + rsi
  9. 下面进入一段分支:

c

if(ecx <= edi) {
    //400ff2
    eax = 0;
    if(ecx >= edi) {
        // 401007
        return rax;
    }
    else{
        esi = rcx + 0x1;
        ret = func4(arg1,arg2,arg3)
        eax = rax + rax + 1;
        return rax;
    }
}
else {
    edx = rcx - 0x1;
    ret = func4(arg1,arg2,arg3)
    eax = eax * 2;
    return rax;
}

分析func4,我们不难发现,该函数的传入参数是%edx%esi,此外该函数是个递归函数。函数总的过程应该如下:

c

eax = edx - esi
ecx = eax >> 31 (逻辑右移)
eax = (eax + ecx) >> 1 (算数右移)
ecx = rax + rsi
if(ecx <= edi) {
   eax = 0;
   if(ecx >= edi) {
       // 401007
       return rax;
   }
   else{
       esi = rcx + 0x1;
       ret = func4(arg1,arg2,arg3)
       eax = rax * 2 + 1;
       return rax;
   }
}
else {
   edx = rcx - 0x1;
   ret = func4(arg1,arg2,arg3)
   eax = eax * 2;
   return rax;
}

经过变量调整重写的 C 版本如下:

c

ret = arg3 - arg2
mid = ret >> 31
ret = (ret + ret >> 31) / 2;
mid = ret + arg2
if(mid <= arg1) {
   ret = 0;
   if(mid >= arg1) {
       return ret;
   }
   else{
    //    arg2 = mid + 1;
       return func4(arg1,mid + 1,arg3) * 2 + 1;
   }
}
else {
//    arg3 = mid - 1;
   return func4(arg1,arg2,mid - 1) * 2;
}

我们看看什么情况下eax = 0。(这到底是什么东西啊摔!)真没看懂是什么东西,试了个0 0,通过了,先 mark 一下,后面再来检验:

text

(gdb)
0 0
95          printf("Halfway there!\n");

text

(gdb)
97          /* Oh yeah?  Well, how good is your math?  Try on this saucy problem! */

数学不好呜呜呜。

phase_5 的反汇编代码如下:

text

0000000000401062 <phase_5>:
seg1:
  401062:	53                   	push   %rbx
  401063:	48 83 ec 20          	sub    $0x20,%rsp
  401067:	48 89 fb             	mov    %rdi,%rbx
  40106a:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
  401071:	00 00
  401073:	48 89 44 24 18       	mov    %rax,0x18(%rsp)
  401078:	31 c0                	xor    %eax,%eax
  40107a:	e8 9c 02 00 00       	callq  40131b <string_length>
  40107f:	83 f8 06             	cmp    $0x6,%eax
  401082:	74 4e                	je     4010d2 <phase_5+0x70>
  401084:	e8 b1 03 00 00       	callq  40143a <explode_bomb>

  401089:	eb 47                	jmp    4010d2 <phase_5+0x70>
seg2:
  40108b:	0f b6 0c 03          	movzbl (%rbx,%rax,1),%ecx
  40108f:	88 0c 24             	mov    %cl,(%rsp)
  401092:	48 8b 14 24          	mov    (%rsp),%rdx
  401096:	83 e2 0f             	and    $0xf,%edx
  401099:	0f b6 92 b0 24 40 00 	movzbl 0x4024b0(%rdx),%edx
  4010a0:	88 54 04 10          	mov    %dl,0x10(%rsp,%rax,1)
  4010a4:	48 83 c0 01          	add    $0x1,%rax
  4010a8:	48 83 f8 06          	cmp    $0x6,%rax
  4010ac:	75 dd                	jne    40108b <phase_5+0x29>
  4010ae:	c6 44 24 16 00       	movb   $0x0,0x16(%rsp)
  4010b3:	be 5e 24 40 00       	mov    $0x40245e,%esi
  4010b8:	48 8d 7c 24 10       	lea    0x10(%rsp),%rdi
  4010bd:	e8 76 02 00 00       	callq  401338 <strings_not_equal>
  4010c2:	85 c0                	test   %eax,%eax
  4010c4:	74 13                	je     4010d9 <phase_5+0x77>
  4010c6:	e8 6f 03 00 00       	callq  40143a <explode_bomb>

  4010cb:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)
  4010d0:	eb 07                	jmp    4010d9 <phase_5+0x77>
seg3:
  4010d2:	b8 00 00 00 00       	mov    $0x0,%eax
  4010d7:	eb b2                	jmp    40108b <phase_5+0x29>
seg4:
  4010d9:	48 8b 44 24 18       	mov    0x18(%rsp),%rax
  4010de:	64 48 33 04 25 28 00 	xor    %fs:0x28,%rax
  4010e5:	00 00
  4010e7:	74 05                	je     4010ee <phase_5+0x8c>
  4010e9:	e8 42 fa ff ff       	callq  400b30 <__stack_chk_fail@plt>
seg5:
  4010ee:	48 83 c4 20          	add    $0x20,%rsp
  4010f2:	5b                   	pop    %rbx
  4010f3:	c3                   	retq

整个过程还原为 C 语言如下:

c

// 这个函数的参数是: %rdi
int phase5(char* buf)
{
    // 保存 rbx
    // 留出 32 个字节的空间
    rbx = rdi // buf 的地址保存在 rbx 中
    *(rsp + 24) = canary
    eax = 0 // 返回值 = 0
    call string_length // 返回字符串长度
    if(eax == 0x6) {
		eax = 0

		// //seg2
		// ecx = *(rbx + rax)
		// *rsp = cl
		// rdx = *rsp
		// edx = edx & 0xf
		// edx = *(rdx + 0x4024b0)
		// *(rsp + rax + 16) = dl
		// rax = rax & 0x1
		// if(rax != 0x6)
		// 	jump to seg2

		// seg2 应该是个循环,其格式如下:
		do {
			ecx = *(rbx + rax) -> (str+i)指向的4个字节赋给ecx
			*rsp = cl -> rsp指向处存储ecx的最低一个字节
			rdx = *rsp -> rdx = 最低的那个字节
			edx = edx & 0xf -> edx = 最低4
			edx = *(rdx + 0x4024b0) -> edx = *(最低四位 + 0x4024b0)
			*(rsp + rax + 16) = dl -> rsp[i + 16] = edx的最低四位
			rax = rax + 0x1 -> rax 递增
		}while(rax != 0x6);

		*(rsp + 22) = 0 -> rsp[22] = 0 // 字符串的末尾是`\0`
		esi = 0x40245e
		rdi = rsp + 16
		call strings_not_equal // 判断 esi处的字符串和 rsp + 16处的6个字节的字符串是否相等
		if(eax == 0) {
			// seg4
			rax = *(rsp + 24) // canary
			// compare rax and canary
			if(rax == canary) {
				//seg5
				// release the stack
				return rax;
			}
			else {
				call __stack_chk_fail
			}
		}else {
			call bomb_explode
		}
    }
	else {
		call bomb_explode
	}
}

0x40245e处的字符串如下:

text

(gdb) x/s 0x40245e
0x40245e:       "flyers"

从上面的内容中我们可以看出:

  1. 必须输入 6 个字符长度的字符串,否则炸弹爆炸
  2. rsp + 16处的字符串和0x40245e处的字符串flyers相同。
  3. 我们需要计算一下 seg2 那段到底对字符串做了什么操作:edx的值 = *(0x4024b0 + str[i]的最低4位),然后str[i] = edx最低四位
  4. 最后str需要等于flyers,也就是说*(0x4024b0 + str[i]的最低4位)的最低4位 = flyers

其中flyers的编码为:0x40245e: 0x66 0x6c 0x79 0x65 0x72 0x73 0x00,最后一个0x00在程序中手动设置了,所以无需自己设置。

打印一下0x4024b0开始的字符串:maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?

因此,本题的目的是在0x4024b0开始的字符串中拼凑出flyers,然后把每个字符相对于开头的偏移量计算出来。其几个字符的地址分别是:

  1. f:0x09
  2. l:0x0f
  3. y:0x0e
  4. ers:0x05 0x06 0x07

综合起来是:0x09 0x0f 0x0e 0x05 0x06 0x07。我们来试一下*(0x4024b0 + str[i]的最低4位)是否等于flyers。测试出来是这样的。但是这 6 个字节不是字符,为了凑成字符,我们对每个数字加上 64(因为在计算的时候只取最低 4 位,所以对一个字节加上 64 不影响最后的偏移量),得到73, 79,78, 69, 70, 71(这里是 10 进制),其对应的 ASCII 码是IONEFG

phase_6 反汇编如下(哎哟我滴妈呀咋个这么长啊):

text

00000000004010f4 <phase_6>:
seg1:
  4010f4:	41 56                	push   %r14
  4010f6:	41 55                	push   %r13
  4010f8:	41 54                	push   %r12
  4010fa:	55                   	push   %rbp
  4010fb:	53                   	push   %rbx
  4010fc:	48 83 ec 50          	sub    $0x50,%rsp
  401100:	49 89 e5             	mov    %rsp,%r13
  401103:	48 89 e6             	mov    %rsp,%rsi
  401106:	e8 51 03 00 00       	callq  40145c <read_six_numbers>
  40110b:	49 89 e6             	mov    %rsp,%r14
  40110e:	41 bc 00 00 00 00    	mov    $0x0,%r12d
seg2:
  401114:	4c 89 ed             	mov    %r13,%rbp
  401117:	41 8b 45 00          	mov    0x0(%r13),%eax
  40111b:	83 e8 01             	sub    $0x1,%eax
  40111e:	83 f8 05             	cmp    $0x5,%eax
  401121:	76 05                	jbe    401128 <phase_6+0x34>
  401123:	e8 12 03 00 00       	callq  40143a <explode_bomb>
seg3:
  401128:	41 83 c4 01          	add    $0x1,%r12d
  40112c:	41 83 fc 06          	cmp    $0x6,%r12d
  401130:	74 21                	je     401153 <phase_6+0x5f>
  401132:	44 89 e3             	mov    %r12d,%ebx
seg4:
  401135:	48 63 c3             	movslq %ebx,%rax
  401138:	8b 04 84             	mov    (%rsp,%rax,4),%eax
  40113b:	39 45 00             	cmp    %eax,0x0(%rbp)
  40113e:	75 05                	jne    401145 <phase_6+0x51>
  401140:	e8 f5 02 00 00       	callq  40143a <explode_bomb>
seg5:
  401145:	83 c3 01             	add    $0x1,%ebx
  401148:	83 fb 05             	cmp    $0x5,%ebx
  40114b:	7e e8                	jle    401135 <phase_6+0x41>
  40114d:	49 83 c5 04          	add    $0x4,%r13
  401151:	eb c1                	jmp    401114 <phase_6+0x20>
seg6:
  401153:	48 8d 74 24 18       	lea    0x18(%rsp),%rsi
  401158:	4c 89 f0             	mov    %r14,%rax
  40115b:	b9 07 00 00 00       	mov    $0x7,%ecx
seg7:
  401160:	89 ca                	mov    %ecx,%edx
  401162:	2b 10                	sub    (%rax),%edx
  401164:	89 10                	mov    %edx,(%rax)
  401166:	48 83 c0 04          	add    $0x4,%rax
  40116a:	48 39 f0             	cmp    %rsi,%rax
  40116d:	75 f1                	jne    401160 <phase_6+0x6c>
  40116f:	be 00 00 00 00       	mov    $0x0,%esi
  401174:	eb 21                	jmp    401197 <phase_6+0xa3>
seg8:
  401176:	48 8b 52 08          	mov    0x8(%rdx),%rdx
  40117a:	83 c0 01             	add    $0x1,%eax
  40117d:	39 c8                	cmp    %ecx,%eax
  40117f:	75 f5                	jne    401176 <phase_6+0x82>
  401181:	eb 05                	jmp    401188 <phase_6+0x94>
seg9:
  401183:	ba d0 32 60 00       	mov    $0x6032d0,%edx
seg10:
  401188:	48 89 54 74 20       	mov    %rdx,0x20(%rsp,%rsi,2)
  40118d:	48 83 c6 04          	add    $0x4,%rsi
  401191:	48 83 fe 18          	cmp    $0x18,%rsi
  401195:	74 14                	je     4011ab <phase_6+0xb7>
seg11:
  401197:	8b 0c 34             	mov    (%rsp,%rsi,1),%ecx
  40119a:	83 f9 01             	cmp    $0x1,%ecx
  40119d:	7e e4                	jle    401183 <phase_6+0x8f>
  40119f:	b8 01 00 00 00       	mov    $0x1,%eax
  4011a4:	ba d0 32 60 00       	mov    $0x6032d0,%edx
  4011a9:	eb cb                	jmp    401176 <phase_6+0x82>
seg12:
  4011ab:	48 8b 5c 24 20       	mov    0x20(%rsp),%rbx
  4011b0:	48 8d 44 24 28       	lea    0x28(%rsp),%rax
  4011b5:	48 8d 74 24 50       	lea    0x50(%rsp),%rsi
  4011ba:	48 89 d9             	mov    %rbx,%rcx
seg13:
  4011bd:	48 8b 10             	mov    (%rax),%rdx
  4011c0:	48 89 51 08          	mov    %rdx,0x8(%rcx)
  4011c4:	48 83 c0 08          	add    $0x8,%rax
  4011c8:	48 39 f0             	cmp    %rsi,%rax
  4011cb:	74 05                	je     4011d2 <phase_6+0xde>
  4011cd:	48 89 d1             	mov    %rdx,%rcx
  4011d0:	eb eb                	jmp    4011bd <phase_6+0xc9>
seg14:
  4011d2:	48 c7 42 08 00 00 00 	movq   $0x0,0x8(%rdx)
  4011d9:	00
  4011da:	bd 05 00 00 00       	mov    $0x5,%ebp
seg15:
  4011df:	48 8b 43 08          	mov    0x8(%rbx),%rax
  4011e3:	8b 00                	mov    (%rax),%eax
  4011e5:	39 03                	cmp    %eax,(%rbx)
  4011e7:	7d 05                	jge    4011ee <phase_6+0xfa>
  4011e9:	e8 4c 02 00 00       	callq  40143a <explode_bomb>
seg16:
  4011ee:	48 8b 5b 08          	mov    0x8(%rbx),%rbx
  4011f2:	83 ed 01             	sub    $0x1,%ebp
  4011f5:	75 e8                	jne    4011df <phase_6+0xeb>
  4011f7:	48 83 c4 50          	add    $0x50,%rsp
  4011fb:	5b                   	pop    %rbx
  4011fc:	5d                   	pop    %rbp
  4011fd:	41 5c                	pop    %r12
  4011ff:	41 5d                	pop    %r13
  401201:	41 5e                	pop    %r14
  401203:	c3                   	retq   s

我们将其分为 16 个 seg,然后绘制一下函数流程图试试,总体过程如图

图

然后,我们为了减小每步分析的工作量,根据跳转分支将其分为 6 个过程,如图

图

下面我们对这 6 个过程来进行分析:

首先进入的是 seg1,其做的工作是:

  1. 压入r14 r13 r12 rbp rbx
  2. 留出 80 个字节的空间
  3. r13 = rsp = rsi
  4. 调用call_six_numbers,经前文分析,read_six_numbers的传入参数为%rsi,返回值在%eax中,其存储了读入的数字的个数
  5. r14 = rsp = rsi = r13r12d = 0

下面进入 seg 2,其做的工作是:

  1. rbp = r13,那么现在r13 = r14 = rsi = rsp = rbp
  2. eax = r13所指向的值
  3. 判断eax - 1是否小于等于 5,如果eax - 1 > 5,那么炸弹爆炸,也就是说eax必须小于等于 6
  4. 正常情况下,进入 seg 3

进入 seg 3 分析:

  1. r12d += 1
  2. r12d与 6 比较,如果r12d != 6,那么ebx = r12d,随后进入 seg 4(proc 2 中)
  3. 否则就进入 seg 6(proc 3 中)

这里我们分析得到的结果是:

  1. seg 1 中进行了一些初始化的工作,让几个指针都指向输入数字的起始位置,并且让计数值r12d = 0
  2. seg 2 中让rbp 指向 r13所指向的位置(一开始r13 = r14 = rsp = rsi,因为后面还有进入seg 2的部分,所以不确定r13的指向会不会改变,先看看),让eax = r13所指向的值,并且保证其必须小于等于 6
  3. seg 3 中让r12d计数值增加,判断r12d是否等于6。如果不等于 6,就让其等于ebx,然后进入seg 4;否则进seg 6(看流程图可以看出,seg 6 离终点要更进一步,而 seg 4 在进入后做一些循环的工作后又会回到 seg 2,故我猜测这里 seg 4 以后的步骤是做了一些遍历的工作,但是在遍历的而过程中是读还是修改暂不清楚)

接下来我们进入 seg 4:

  1. 我们首先让rax = ebx(注意前面ebx = r12d,此时r12d已经递增到下一个位置了),那么这里rax = ebx = r12d
  2. eax = *(rsp + 4 * rax),因为 rsp 是输入数字的起始位置,那么 rax 应该是一个索引,因为输入的是int类型,所以每次要 * 4。假定输入的数字的字符串叫str,那么这里就是eax = str[rax]
  3. 判断rbp 指向的值是否等于eax,前文中我们可以看到rbp指向的是r13所指向的位置,也就是让你判断r13指向位置的值是否等于eax,也就是前面的str[rax]
  4. 如果不相等,进入 seg 5,否则炸弹爆炸(下面那个图中的有点错误,菱锌框中应该是不相等),我们可以初步判断,这里两个相邻的值应该是不能相等的(其他不相邻的能不能相等等会儿再看,如果r13改变的话,那么不相邻的值应该也不能像等)

就接下来我们进入 seg 5:

  1. ebx += 1(还记得ebx是什么吗?ebx = r12d!在前文中r12d已经向前跃进了一个位置,那么现在ebx还要比r12d再进一个位置)
  2. ebx 和 0x5相比较,如果ebx <= 0x5,也就是说如果之前的ebx <= 0x6的话,继续回到 seg 4。否则r13 += 4r13果然改变了,向前跃进了 4 个字节,也就是一个 int 类型的大小),然后回到 seg 2

这个过程我们分析得到的结果是:

  1. seg 4 让rax = ebx = r12d,然后让rax = str[rax],判断rbp/r13指向的值是否等于eax。相等就爆炸,不相等就进 seg 5
  2. seg 5 判断之前的ebx是否小于等于 0x6,r12debx小 1。如果ebx <= 0x6的话,回到 seg 4。seg 4 中还要判断下一个位置的数字和r13/rbp所指向的数字是否相等,相等就爆炸。否则r13指向下一个数字的位置,回到 seg 2。seg 2 中又要判断下一个数字是否小于等于 6,以及其和后面的数字是否相等。

到这里我们可以初步判断,r13/rbp 是用于指向被比较的基准数,是指针。ebx/r12d 是用来递增的数组索引,是数字。获取到的数字通常被赋给 rax。输入的数字不能有相等的,也必须都小于等于 6。Proc 1 和 Proc 2 主要执行了一些检查的工作。

从前文我们可以判断出,当进入 seg 6 的时候,r12d 肯定是等于 6 了,r13/rbp 也指向了最后一个数字。

在 seg 6 中:

  1. 我们让rsi指向最后一个数字的下一个位置,让rax = r14,ecx = 7r14一开始是指向rsp的,这两个值是什么含义这里还没有完全清楚)

在 seg 7 中:

  1. 我们让edx = ecxedx = ecx = 7
  2. edx = edx - *rax,这里rax指向数组的开头,也就是让edx作为 7 和rax指向的当前值的差
  3. *rax = edx,也就是让rax指向的值 = 这个差
  4. rax指向下一个位置,比较raxrsi,也就是判断是否到达了数组的末尾
  5. 如果没有到达(rax还没有到达数组末尾rsi),就回到 seg 7,继续让每个数组的值 = 7 - 它的值
  6. 否则 esi = 0,跳到seg 11

在这个子过程中,我们让每个数组的值都等于 7-它原本的值,遍历完成后 rax 指向数组末尾,r14 和 rsp 还在开头,esi = 0

这个过程有点复杂,要慢慢来

在 seg 11 中:

  1. 我们让ecx的值 = *(rsp + rsi)
  2. ecx0x1 比较,如果ecx <= 0x1的话,跳转到 seg 9
  3. 否则 eax = 1, edx = 0x6032d0,跳转到 seg 8

因为 seg 9 离终点更近,因此我们首先分析 seg 8,也就是7 - str[i] > 1 -> value < 6的情况。

  1. rdx = *(rdx + 8),也就是说rdx的值 = 离rdx8 个字节的位置的值(大概率是个指针,因为需要解引,而且指针是 8 个字节对齐)
  2. eax += 1,将ecxeax比较(这里 eax 已经从 2 开始了),如果eax还不等于ecx,那么回到 seg 8,否则到 seg 10

seg 8 的含义为,从第一个差开始,如果当前这个差的值 > 0x1 的话,一直寻找这个差的值的位置,保存在rdx中,并且这个差的值在rax中。

同时我们来分析 seg 9,seg 9 中edx = 0x6032d0,直接进入 seg 10

seg 10 的操作是:

  1. *(rsp + rsi * 2 + 32) = rdx,这里 rsi 的值 = 之前计算出的索引值(最开始是 0),rsp 的值 = 输入数字的位置
  2. rsi = rsi + 4
  3. rsi和 24 比较,如果rsi != 24,即所有数字还没遍历完,退回到 seg 11
  4. 否则进入 seg 12

那么总结来说,这三个段的含义如下:

  1. seg 11 让ecx = str[rsi],将其和0x1作比较
  2. ecx <= 0x1时,跳转到 seg 9,令edx = 0x6032d0,随后进入 seg 10
  3. ecx > 0x1时,eax = 1, edx = 0x6032d0,跳转到 seg 8,seg 8 中从第一个值开始一直寻找第ecx个值的位置,并保存在rdx中,进入 seg 10
  4. seg 10 中我们把该数字的地址都存储在*(起始地址 + 2 * 索引 + 32)位置处,如果数字遍历完了,就进入 seg 12,否则就回退到 seg 11

大概明白了,这里作者可能是想构建一个链表,他把链表的所有结点的起始地址放在一个数组里了!放置的顺序是我们输入的数字串的顺序!比如rsi = 0时,读入第ecx个数字,那么根据这个数字找到的edx就存储在地址数组的第一行。每个数字的地址都存储在起始地址 + 32 + 2 * 索引的位置。(为什么是这个还不太理解)

到这里结束,6 个数字的位置分别存储的rsi是:0 4 8 12 16 20 24,其偏移量是0 8 16 25 32 40 48,加上 32 后相距rsp的偏移量是:32 40 48 57 72 80

我们从0x6032d0开始,每次打印 16 个字节(小端序),前 8 个字节是数字的值,后 8 个字节是下一个结点的地址,其顺序如下:

第一个结点从0x6032d0开始:

text

(gdb) x/16xb 0x6032d0
0x6032d0 <node1>:       0x4c    0x01    0x00    0x00    0x01    0x00    0x00    0x00
0x6032d8 <node1+8>:     0xe0    0x32    0x60    0x00    0x00    0x00    0x00    0x00

那么第二个结点的地址应该是0x6032e0

text

(gdb) x/16xb 0x6032e0
0x6032e0 <node2>:       0xa8    0x00    0x00    0x00    0x02    0x00    0x00    0x00
0x6032e8 <node2+8>:     0xf0    0x32    0x60    0x00    0x00    0x00    0x00    0x00

第三个结点的地址应该是0x6032f0

text

(gdb) x/16xb 0x6032f0
0x6032f0 <node3>:       0x9c    0x03    0x00    0x00    0x03    0x00    0x00    0x00
0x6032f8 <node3+8>:     0x00    0x33    0x60    0x00    0x00    0x00    0x00    0x00

第四个结点的地址应该是0x603300

text

(gdb) x/16xb 0x603300
0x603300 <node4>:       0xb3    0x02    0x00    0x00    0x04    0x00    0x00    0x00
0x603308 <node4+8>:     0x10    0x33    0x60    0x00    0x00    0x00    0x00    0x00

第五个结点的地址应该是0x603310

text

0x603310 <node5>:       0xdd    0x01    0x00    0x00    0x05    0x00    0x00    0x00
0x603318 <node5+8>:     0x20    0x33    0x60    0x00    0x00    0x00    0x00    0x00

第六个结点的地址应该是0x603320

text

(gdb) x/16xb 0x603320
0x603320 <node6>:       0xbb    0x01    0x00    0x00    0x06    0x00    0x00    0x00
0x603328 <node6+8>:     0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00

在这个子过程中,我们首先进入 seg12,经过一个看起来是循环的东西,再从 seg 14 出去。

seg 12 的过程如下:

  1. rbx = *(rsp + 32) // rbx = 起始位置的值(链表第一个结点的地址)
  2. rax = rsp + 40 // 第 2 个值(链表第二个结点的地址)的位置
  3. rsi = rsp + 80 // 最后一个数(最后一个结点的地址)的位置
  4. rcx = rbx // rcx = rbx = 起始位置的值(第一个结点的地址)

随后进入 seg 13,seg 13 的过程如下:

  1. rdx = *rax // rdx = rax 位置的值
  2. *(rcx + 8) = rdx // rcx 指向的下一个位置的值是 rdx
  3. rax += 8 // rax = 下一个位置
  4. 如果 rax == rsi,进入 seg 14,否则rcx = rdx,重新回到 seg 13

这里把链表中的结点都串起来啦!

seg 14 的过程如下:

  1. *(rdx + 8) = 0 // 把链表最后一个节点的 next 域置为 0
  2. ebp = 5

随后进入 seg 15。

现在 rbx 的值等于链表第一个结点的地址,rcx 的值等于链表倒数第二个结点的地址,rdx 的值等于链表最后一个结点的地址,rsi 和 rax 指向链表的最后,ebp = 5

seg 15 中完成了以下步骤:

  1. rax = *(rbx + 8)
  2. eax = *rax
  3. 如果 *rbx >= eax,进入 seg 16
  4. 否则炸弹爆炸

这里说明第一个结点的值必须大于等于第二个结点,否则炸弹爆炸。

seg 16 中执行以下步骤:

  1. rbx = *(rbx + 8)
  2. ebp -= 1
  3. 如果 ebp != 0,进入 seg 15
  4. 否则栈恢复

这段说明链表的值从开始到后面必须严格递减。我们在前期已经获得了链表各个结点的值,我们只需要从大到小排列这些值即可,炸弹的密码是排列的顺序。这组值是0x014c 0x00a8 0x039c 0x02b3 0x01dd 0x01bb,翻译成十进制就是332 168 924 691 477 443(可以使用x/96dh 0x6032d0打印halfword),gdb 的 x 命令打印的字节数如下:

text

b - byte
h - halfword (16-bit value)
w - word (32-bit value)
g - giant word (64-bit value)

所以数字从大到小的顺序应该是3 4 5 6 1 2,但是因为ecx的值是7 - 原来的值,所以原来的值应该是4 3 2 1 6 5。成功解除炸弹!

重点:

  1. 这个过程就是第ecx个数的rdx(链表结点地址)存储在地址数组的第rsi位,实现了链表结点的重排序!
  2. 最后记得第ecx个数 = 7 - 原来的值,所以我们解出了链表中元素的值后,还要用 7 - 它 来获得我们原来输入的数

secret phase执行反汇编得到如下结果:

text

0000000000401242 <secret_phase>:
seg1:
  401242:	53                   	push   %rbx
  401243:	e8 56 02 00 00       	callq  40149e <read_line>
  401248:	ba 0a 00 00 00       	mov    $0xa,%edx
  40124d:	be 00 00 00 00       	mov    $0x0,%esi
  401252:	48 89 c7             	mov    %rax,%rdi
  401255:	e8 76 f9 ff ff       	callq  400bd0 <strtol@plt>
  40125a:	48 89 c3             	mov    %rax,%rbx
  40125d:	8d 40 ff             	lea    -0x1(%rax),%eax
  401260:	3d e8 03 00 00       	cmp    $0x3e8,%eax
  401265:	76 05                	jbe    40126c <secret_phase+0x2a>
  401267:	e8 ce 01 00 00       	callq  40143a <explode_bomb>
seg2:
  40126c:	89 de                	mov    %ebx,%esi
  40126e:	bf f0 30 60 00       	mov    $0x6030f0,%edi
  401273:	e8 8c ff ff ff       	callq  401204 <fun7>
  401278:	83 f8 02             	cmp    $0x2,%eax
  40127b:	74 05                	je     401282 <secret_phase+0x40>
  40127d:	e8 b8 01 00 00       	callq  40143a <explode_bomb>
seg3:
  401282:	bf 38 24 40 00       	mov    $0x402438,%edi
  401287:	e8 84 f8 ff ff       	callq  400b10 <puts@plt>
  40128c:	e8 33 03 00 00       	callq  4015c4 <phase_defused>
  401291:	5b                   	pop    %rbx
  401292:	c3                   	retq

其过程如下:

  1. 压入%rbx
  2. 读入一行 -> 估计结果存储在 rax 中,也就是字符串的起始位置
  3. edx = 0xaesi = 0rdi = rax
  4. 调用strtol
  5. rbx = rax -> rbx = 字符串转化出的数值
  6. eax = rax - 1
  7. 如果eax <= 0x3e8,跳转到 seg 2;否则炸弹爆炸
  8. esi = ebxedi = 0x6030f0,调用fun7
  9. 如果eax == 0x2,跳转到 seg 3(炸弹解除),否则炸弹爆炸

将其转化为 C 语言:

c

int secret_phase(char* buf)
{
    // 压入rbx
    rax = call read_line
    edx = 0xa
    esi = 0
    rdi = rax
    call strtol
    rbx = rax
    eax = rax - 1 //输入数字必须小于等于 1000
    if(eax <= 0x3e8) {
        // seg2
        esi = ebx
        edi = 0x6030f0
        call fun7
        if(eax == 0x2) {
            return eax;
        }
        else {
            call bomb_explode
        }
    }
    else {
        call bomb_explode
    }
}

我们首先解读一下secret_phase

一开始几个寄存器的值如下:

text

rax - read line 返回的字符串地址
edx - 0xa
esi - 0
rdi - read line 返回的字符串地址

调用strtol后几个寄存器的值如下:

text

rax - strtol 返回的字符串转化出的数值
rbx - strtol 返回的字符串转化出的数值
eax - 数值 - 1

如果该数值 - 1 > 0x3e8(1000),炸弹爆炸,否则进入 seg 2。

seg 2 中完成了如下工作:

  1. esi = strtol 返回的字符串转化出的数值(这个数必须小于等于 1001)
  2. edi = 0x6030f0(一个地址)
  3. 调用fun7
  4. 如果fun7返回 2,退出,否则炸弹爆炸

这其中调用了一个叫fun7的函数,其反汇编结果如下:

text

0000000000401204 <fun7>:
seg1:
  401204:	48 83 ec 08          	sub    $0x8,%rsp
  401208:	48 85 ff             	test   %rdi,%rdi
  40120b:	74 2b                	je     401238 <fun7+0x34>
  40120d:	8b 17                	mov    (%rdi),%edx
  40120f:	39 f2                	cmp    %esi,%edx
  401211:	7e 0d                	jle    401220 <fun7+0x1c>
  401213:	48 8b 7f 08          	mov    0x8(%rdi),%rdi
  401217:	e8 e8 ff ff ff       	callq  401204 <fun7>
  40121c:	01 c0                	add    %eax,%eax
  40121e:	eb 1d                	jmp    40123d <fun7+0x39>
seg2:
  401220:	b8 00 00 00 00       	mov    $0x0,%eax
  401225:	39 f2                	cmp    %esi,%edx
  401227:	74 14                	je     40123d <fun7+0x39>
  401229:	48 8b 7f 10          	mov    0x10(%rdi),%rdi
  40122d:	e8 d2 ff ff ff       	callq  401204 <fun7>
  401232:	8d 44 00 01          	lea    0x1(%rax,%rax,1),%eax
  401236:	eb 05                	jmp    40123d <fun7+0x39>
seg3:
  401238:	b8 ff ff ff ff       	mov    $0xffffffff,%eax
seg4:
  40123d:	48 83 c4 08          	add    $0x8,%rsp
  401241:	c3                   	retq

根据反汇编可以看出%rdi(0x6030f0)和%esi(输入的数值)是传入参数,%eax是返回值。其中还包括了一次递归调用。fun7的 C 语言实现如下:

c

int fun7()
{
    // rsp留出8个字节的空间
    if(rdi == 0) {
        // seg3
        eax = 0xffffffff
    }
    else {
        edx = *rdi
        if(edx <= esi) {
            // seg2
            eax = 0
            if(edx != esi) {
                rdi = *(rdi + 16)
                call fun7
                eax = 2 * rax + 1
            }
        }
        else {
            rdi = *(rdi + 8)
            call fun7
            eax = eax * 2
        }
    }
    // seg4
    // rsp栈空间恢复
    return eax;
}

fun7的内容翻译成 C 语言是如下内容:

  1. 如果传入的rdi = 0,那么返回0xffffffff
  2. 否则,edx的值为rdi指向地址的值。
  3. 如果edx > 传入的esi,rdi 跳转到下一个位置(左节点的值)(这里大概率又是个链表),返回2 * fun7()
  4. 否则,eax = 0,如果edx == 传入的esi,返回 0;否则就跳转到下下个位置(右节点的值),返回2 * fun7() + 1

为什么能推测出来这里的左节点和右节点呢?我们在0x6030f0打印 32 个字节的值看看:

text

(gdb) x/32bx 0x6030f0
0x6030f0 <n1>:          0x24    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x6030f8 <n1+8>:        0x10    0x31    0x60    0x00    0x00    0x00    0x00    0x00
0x603100 <n1+16>:       0x30    0x31    0x60    0x00    0x00    0x00    0x00    0x00

这里有 n+8 和 n+16,可以看出来这也是两个地址。

这里对fun7的要求是必须在传入地址的值为0x6030f0,输入的%esi <= 1000的情况下返回 2。

我们打印一下这个链表各个结点的值:

text

(gdb) x/32bx 0x6030f0
0x6030f0 <n1>:          0x24    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x6030f8 <n1+8>:        0x10    0x31    0x60    0x00    0x00    0x00    0x00    0x00
0x603100 <n1+16>:       0x30    0x31    0x60    0x00    0x00    0x00    0x00    0x00

我们打印它左节点(node 21):

text

(gdb) x/32bx 0x603110
0x603110 <n21>: 0x08    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x603118 <n21+8>:       0x90    0x31    0x60    0x00    0x00    0x00    0x00    0x00
0x603120 <n21+16>:      0x50    0x31    0x60    0x00    0x00    0x00    0x00    0x00
0x603128:       0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00

再打印右节点(node 22):

text

(gdb) x/32bx 0x603130
0x603130 <n22>: 0x32    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x603138 <n22+8>:       0x70    0x31    0x60    0x00    0x00    0x00    0x00    0x00
0x603140 <n22+16>:      0xb0    0x31    0x60    0x00    0x00    0x00    0x00    0x00
0x603148:       0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00

现在的迷惑之处在于fun7到底返回的是什么东西。从fun7的内容来看,其主要目的是一直找到esi的值在二叉树中的位置。如果esi < edx,就找左边那颗二叉树,返回2 * fun7(),否则就找右边那颗二叉树,返回2 * fun7() + 1。从0x6030f0这个位置开始找,最后fun7()必须返回 2。从我们之前学习数据结构的经验来说,如果一个结点的标号为 n,那么它的左孩子就是 2 _ n,右孩子就是 2 _ n + 1。

二叉树的结构如下:

fun7

那么首先edx的值为根节点的地址,我们从根开始遍历,一直到%esi的值所在的位置。我们对每个结点进行排查,那么能让最后返回 2 的%esi只有 0x16 和 0x14。

但是secret_phase在哪里启动呢?我们来看看!欸,在phase_defused中启动!我们看到在callq secret_phase前有这样一段代码:

text

  401621:	bf 20 25 40 00       	mov    $0x402520,%edi
  401626:	e8 e5 f4 ff ff       	callq  400b10 <puts@plt>
  40162b:	b8 00 00 00 00       	mov    $0x0,%eax
  401630:	e8 0d fc ff ff       	callq  401242 <secret_phase>

这段汇编首先把一个看起来像地址的0x402520放进了%edi寄存器,然后调用puts,将eax设置为 0。我们来看看0x402520处放了啥:

text

(gdb) x/s 0x402520
0x402520:       "But finding it and solving it are quite different..."

这个地址是否出现在别的地方呢?好像没有。但是这段代码后面还有一段:

text

  401626:	e8 e5 f4 ff ff       	callq  400b10 <puts@plt>
  40162b:	b8 00 00 00 00       	mov    $0x0,%eax
  401630:	e8 0d fc ff ff       	callq  401242 <secret_phase>
  401635:	bf 58 25 40 00       	mov    $0x402558,%edi
  40163a:	e8 d1 f4 ff ff       	callq  400b10 <puts@plt>
  40163f:	48 8b 44 24 68       	mov    0x68(%rsp),%rax
  401644:	64 48 33 04 25 28 00 	xor    %fs:0x28,%rax

这里又出现了一个新地址,0x402558,我们来看看这里是啥:

text

(gdb) x/s 0x402558
0x402558:       "Congratulations! You've defused the bomb!"

额,是恭喜我们拆除炸弹。。。

我想这么看可能是看不出来的,那么我们来把phase_defused详细拆解看看(nop 指令省去):

text

00000000004015c4 <phase_defused>:
seg1:
  4015c4:	48 83 ec 78          	sub    $0x78,%rsp -> 留出栈位置
  4015c8:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
  4015cf:	00 00
  4015d1:	48 89 44 24 68       	mov    %rax,0x68(%rsp) -> *(rsp + 104) = rax
  4015d6:	31 c0                	xor    %eax,%eax -> 将eax置0
  4015d8:	83 3d 81 21 20 00 06 	cmpl   $0x6,0x202181(%rip)        # 603760 <num_input_strings>
  4015df:	75 5e                	jne    40163f <phase_defused+0x7b>
  4015e1:	4c 8d 44 24 10       	lea    0x10(%rsp),%r8
  4015e6:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
  4015eb:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx
  4015f0:	be 19 26 40 00       	mov    $0x402619,%esi
  4015f5:	bf 70 38 60 00       	mov    $0x603870,%edi
  4015fa:	e8 f1 f5 ff ff       	callq  400bf0 <__isoc99_sscanf@plt>
  4015ff:	83 f8 03             	cmp    $0x3,%eax
  401602:	75 31                	jne    401635 <phase_defused+0x71>
  401604:	be 22 26 40 00       	mov    $0x402622,%esi
  401609:	48 8d 7c 24 10       	lea    0x10(%rsp),%rdi
  40160e:	e8 25 fd ff ff       	callq  401338 <strings_not_equal>
  401613:	85 c0                	test   %eax,%eax
  401615:	75 1e                	jne    401635 <phase_defused+0x71>
  401617:	bf f8 24 40 00       	mov    $0x4024f8,%edi
  40161c:	e8 ef f4 ff ff       	callq  400b10 <puts@plt>
  401621:	bf 20 25 40 00       	mov    $0x402520,%edi
  401626:	e8 e5 f4 ff ff       	callq  400b10 <puts@plt>
  40162b:	b8 00 00 00 00       	mov    $0x0,%eax
  401630:	e8 0d fc ff ff       	callq  401242 <secret_phase>
seg2:
  401635:	bf 58 25 40 00       	mov    $0x402558,%edi
  40163a:	e8 d1 f4 ff ff       	callq  400b10 <puts@plt>
seg3:
  40163f:	48 8b 44 24 68       	mov    0x68(%rsp),%rax
  401644:	64 48 33 04 25 28 00 	xor    %fs:0x28,%rax
  40164b:	00 00
  40164d:	74 05                	je     401654 <phase_defused+0x90>
  40164f:	e8 dc f4 ff ff       	callq  400b30 <__stack_chk_fail@plt>
seg4:
  401654:	48 83 c4 78          	add    $0x78,%rsp
  401658:	c3                   	retq

其中 seg 3 和 4 主要是一些收尾工作(检查canary的值之类的),seg2 是打印恭喜字符串,其核心部分在 seg 1。

seg 1 的工作如下:

  1. 留出栈的位置,放置canary

  2. 获取输入的字符串的个数,如果不等于 6,跳转到收尾工作

  3. 设置以下几个值:

    text

     4015e1:	4c 8d 44 24 10       	lea    0x10(%rsp),%r8 // r8 = rsp + 16
     4015e6:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx // rcx = rsp + 12
     4015eb:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx // rdx = rsp + 8
     4015f0:	be 19 26 40 00       	mov    $0x402619,%esi // esi = 0x402619
     4015f5:	bf 70 38 60 00       	mov    $0x603870,%edi // edi = 0x603870

    这里0x402619sscanf的格式化输入的字符串的保存地址:

    text

    0x402619:       "%d %d %s"

    0x603870是输入的字符串的保存地址:

    text

    (gdb) x/s 0x603870
     0x603870 <input_strings+240>:   ""
  4. 调用 sscanf 获取输入字符串,其个数存储在%eax中,判断其是否等于 3,如果不是进入收尾工作

  5. 接下来我们需要判断0x402622处的字符串和输入的字符串是否相等,该处的字符串为DrEvil,如果相等的话,打印如下指令,然后进入 secret phase:

text

(gdb) x/s 0x4024f8
0x4024f8:       "Curses, you've found the secret phase!"
(gdb) x/s 0x402520
0x402520:       "But finding it and solving it are quite different..."

但是什么时候会sscanf0x603870处呢?只有phase_3phase_4调用了sscanf,且输入的字符串的地址都存储在$rdi中,我们对他们打断点进行分析看看:

text

Breakpoint 1, 0x0000000000400f43 in phase_3 ()
(gdb) p $rdi
$1 = 6305824
(gdb) p /x $rdi
$2 = 0x603820
(gdb) n
Single stepping until exit from function phase_3,
which has no line number information.
main (argc=<optimized out>, argv=<optimized out>) at bomb.c:90
warning: Source file is more recent than executable.
90          /* I guess this is too easy so far.  Some more complex code will
(gdb)
91           * confuse people. */
(gdb)
Halfway there!
94          phase_defused();
(gdb)
0 207
95          printf("Halfway there!\n");
(gdb) n

Breakpoint 2, 0x000000000040100c in phase_4 ()
(gdb) p /x $rdi
$3 = 0x603870

phase_4$rdi = 0x603870,这个字符串是在这里输入的!那么这里应该输入第三个字符串来触发 secret_phase,也就是DrEvil,并且 secret phase 的答案是 0x16 和 0x14。

最后成功啦!

text

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.
Good work!  On to the next...
Curses, you've found the secret phase!
But finding it and solving it are quite different...
Wow! You've defused the secret stage!
Congratulations! You've defused the bomb!
  1. Phase 1 - 3 还好
  2. Phase 3那个需要传入 10 进制数,而不是 16 进制,写答案的时候不要写错了
  3. Phase 4那个func没看出来是啥,蒙了个0 0过了,需要搞懂
  4. Phase 5一开始没想到给那几个 16 进制的值加偏移量,使其为合法的ASCII码字符,是后来加查了 1 个题解晓得的。
  5. Phase 6费大力气终于解决啦!当然我一开始没留意到是第ecx个数的rdx存储在地址数组的第rsi位,所以还以为链表的结点都是顺序排列的。解除 phase6 的经历也告诉我,看见汇编不要一开始上来就将它转化成 C 语言,要首先根据跳转目标对其进行分段,用流程图分析他的跳转,然后再将其拆解为各个小的过程(拆解的过程中尽量把循环放在一起,控制一下过程的规模),一步步的分析他,不要一上来就看题解/暴力反汇编。而且要留意题意!比如第 5 题那个 ASCII 码字符,第 6 题要把顺序转化为7 - 原来的值,这都是容易做错的地方。
  6. secret_phase顺利完结!二叉树的结构精妙极了!但是secret_phase的入口不好找,因为输入的字符串都在%rdi中,需要通过gdb查看哪个 phase 的 rdi 寄存器的值 = 0x603870,这点很难。

相关内容