CSAPP-bomblab

CSAPP-bomblab

实验目的

​ 理解汇编语言,学习使用gdb调试器,推导出正确的字符串用于跳过explode_bomb,破解bomb.c生成的炸弹。

实验环境和工具

​ ubuntu 12.04.5 (32位) ;

​ gdb 7.4 ;

实验内容及操作步骤

阅读bomb.c

  • C文件开头的注释如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/***************************************************************************
* Dr. Evil's Insidious Bomb, Version 1.1
* Copyright 2011, Dr. Evil Incorporated. All rights reserved.
*
* LICENSE:
*
* Dr. Evil Incorporated (the PERPETRATOR) hereby grants you (the
* VICTIM) explicit permission to use this bomb (the BOMB). This is a
* time limited license, which expires on the death of the VICTIM.
* The PERPETRATOR takes no responsibility for damage, frustration,
* insanity, bug-eyes, carpal-tunnel syndrome, loss of sleep, or other
* harm to the VICTIM. Unless the PERPETRATOR wants to take credit,
* that is. The VICTIM may not distribute this bomb source code to
* any enemies of the PERPETRATOR. No VICTIM may debug,
* reverse-engineer, run "strings" on, decompile, decrypt, or use any
* other technique to gain knowledge of and defuse the BOMB. BOMB
* proof clothing may not be worn when handling this program. The
* PERPETRATOR will not apologize for the PERPETRATOR's poor sense of
* humor. This license is null and void where the BOMB is prohibited
* by law.
***************************************************************************/
  • 翻译如下:

​ Evil Incorporated博士(PERPETRATOR)特此授予您(VICTIM)使用该炸弹(BOMB)的明确许可。 这是一个有时间限制的许可证,在VICTIM死亡时到期。 PERPETRATOR对损坏,沮丧,精神错乱,虫眼,腕管综合症,睡眠不足或对VICTIM造成的其他伤害不承担任何责任。 除非PERPETRATOR想要获得信誉,否则就是这样。 VICTIM不得将此炸弹源代码分发给PERPETRATOR的任何敌人。 VICTIM不得调试,反向工程,在其上运行“字符串”,反编译,解密或使用任何其他技术来了解和缓解BOMB。 处理此程序时,可能不能穿防弹衣。 PERPETRATOR不会因PERPETRATOR的幽默感而道歉。 在法律禁止BOMB的情况下,此许可无效。

  • bomb.c代码如下:(英文注释做了适当翻译)
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
89
90
91
92
93
94
95
96
97
#include <stdio.h>
#include <stdlib.h>
#include "support.h"
#include "phases.h"

/*
* Note to self: Remember to erase this file so my victims will have no
* idea what is going on, and so they will all blow up in a
* spectaculary fiendish explosion. -- Dr. Evil
*/
//自我提醒:请记住要删除此文件,以使我的受害者不知道发生了什么事,因此他们都会在一场壮观的恶魔般的爆炸中炸毁。 -邪恶博士

FILE *infile;

int main(int argc, char *argv[])
{
char *input;

/* Note to self: remember to port this bomb to Windows and put a
* fantastic GUI on it. */ //自我提醒:记得将炸弹移植到Windows并在上面放上精美的GUI。

/* When run with no arguments, the bomb reads its input lines
* from standard input. */ //当不带参数运行时,炸弹会从标准输入中读取其输入行。
if (argc == 1) {
infile = stdin;
}

/* When run with one argument <file>, the bomb reads from <file>
* until EOF, and then switches to standard input. Thus, as you
* defuse each phase, you can add its defusing string to <file> and
* avoid having to retype it. */
//当使用一个参数<file>运行时,炸弹会从<file>读取直到EOF,然后切换到标准输入。 因此,在对每个阶段进行解压缩时,可以将其解压缩字符串添加到<file>中,而不必重新输入。
else if (argc == 2) {
if (!(infile = fopen(argv[1], "r"))) {
printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]);
exit(8);
}
}

/* You can't call the bomb with more than 1 command line argument. */ //您不能使用超过1个命令行参数来调用炸弹。
else {
printf("Usage: %s [<input_file>]\n", argv[0]);
exit(8);
}

/* Do all sorts of secret stuff that makes the bomb harder to defuse. */ //做各种使炸弹难以化解的秘密工作。
initialize_bomb();

printf("Welcome to my fiendish little bomb. You have 6 phases with\n");
printf("which to blow yourself up. Have a nice day!\n");

/* Hmm... Six phases must be more secure than one phase! */
//嗯...六个阶段必须比一个阶段更安全!
input = read_line(); /* Get input */ //获取输入
phase_1(input); /* Run the phase */ //运行阶段
phase_defused(); /* Drat! They figured it out! //他们想通了!
* Let me know how they did it. */ //让我知道他们是如何做到的。
printf("Phase 1 defused. How about the next one?\n");

/* The second phase is harder. No one will ever figure out
* how to defuse this... */ //第二阶段比较困难。 没有人会想出如何化解这个问题的。
input = read_line();
phase_2(input);
phase_defused();
printf("That's number 2. Keep going!\n");

/* I guess this is too easy so far. Some more complex code will
* confuse people. */ //我想到目前为止这太容易了。 一些更复杂的代码会使人们感到困惑。
input = read_line();
phase_3(input);
phase_defused();
printf("Halfway there!\n");

/* Oh yeah? Well, how good is your math? Try on this saucy problem! */ //哦耶? 好吧,你的数学有多好? 尝试这个棘手的问题!
input = read_line();
phase_4(input);
phase_defused();
printf("So you got that one. Try this one.\n");

/* Round and 'round in memory we go, where we stop, the bomb blows! */ //我们在记忆中走来走去,停下来,炸弹炸毁!
input = read_line();
phase_5(input);
phase_defused();
printf("Good work! On to the next...\n");

/* This phase will never be used, since no one will get past the
* earlier ones. But just in case, make this one extra hard. */
//此阶段将永远不会使用,因为没有人会越过早期的阶段。 但以防万一,使这一操作更加困难。
input = read_line();
phase_6(input);
phase_defused();

/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */ //哇,他们明白了! 但是,是不是……缺少了什么? 也许他们忽略了什么? 哇哈哈哈哈哈!

return 0;
}
  • 总之,有两种方法闯关:
  1. ./bomb,然后在终端输入
  2. ./bomb xxx.txtxxx.txt中按行保存用于过关的字符串

分析汇编代码,找到通关密码

1. phase_1

  • phase_1的汇编代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Dump of assembler code for function phase_1:
    0x08048b50 <+0>: sub $0x1c,%esp
    ;栈指针esp减去0x1c,给当前帧开辟大小为0x1c的空间
    0x08048b53 <+3>: movl $0x804a1a4,0x4(%esp)
    ;将0x804a1a4(原字符串在内存中的地址)赋给M[0x4+esp]。
    ;(从后面调用函数<strings_not_equal>推测其为字符串,通过查看<strings_not_equal>的汇编代码进行验证)
    0x08048b5b <+11>: mov 0x20(%esp),%eax ;将M[0x20+%esp](输入的字符串)赋值给eax
    0x08048b5f <+15>: mov %eax,(%esp)
    ;eax的值赋给M[esp],与M[0x4+esp]共同作为<strings_not_equal>的参数
    0x08048b62 <+18>: call 0x8048fd4 <strings_not_equal>
    ;比较两字符串是否相等,若相等,则返回值%eax=0
    0x08048b67 <+23>: test %eax,%eax ;如果eax=0,则zf=1
    0x08048b69 <+25>: je 0x8048b70 <phase_1+32> ;zf=1时跳转至*(phase_1+32),即下一关
    0x08048b6b <+27>: call 0x80490e6 <explode_bomb> ;否则爆炸
    0x08048b70 <+32>: add $0x1c,%esp
    0x08048b73 <+35>: ret
    End of assembler dump.
    • 其中,<strings_not_equal>的汇编代码如下:

      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
      ·#执行call指令:esp=esp-4
      Dump of assembler code for function strings_not_equal:
      0x08048fd4 <+0>: sub $0x10,%esp ;esp=esp-10
      0x08048fd7 <+3>: mov %ebx,0x4(%esp)
      0x08048fdb <+7>: mov %esi,0x8(%esp)
      0x08048fdf <+11>: mov %edi,0xc(%esp)
      0x08048fe3 <+15>: mov 0x14(%esp),%ebx
      ;此时M[0x14+esp]为输入的字符串*s1,存入ebx
      0x08048fe7 <+19>: mov 0x18(%esp),%esi
      ;此时M[0x18+esp]为原字符串*s0,存入esi
      0x08048feb <+23>: mov %ebx,(%esp)
      0x08048fee <+26>: call 0x8048fbb <string_length>
      ;eax返回值为ebx中字符串的长度s1.length
      0x08048ff3 <+31>: mov %eax,%edi ;存入edi
      0x08048ff5 <+33>: mov %esi,(%esp)
      0x08048ff8 <+36>: call 0x8048fbb <string_length>
      ;eax返回值为esi中字符串的长度s0.length
      0x08048ffd <+41>: mov $0x1,%edx
      0x08049002 <+46>: cmp %eax,%edi ;比较s1.length和s0.length
      0x08049004 <+48>: jne 0x8049039 <strings_not_equal+101>;不相等,则跳转->return 1
      0x08049006 <+50>: movzbl (%ebx),%eax ;s1首地址存入eax
      0x08049009 <+53>: mov $0x0,%dl ;edx=0
      0x0804900b <+55>: test %al,%al
      0x0804900d <+57>: je 0x8049039 <strings_not_equal+101>;if(*s1==0)->return 0
      ;(输入空字符)
      0x0804900f <+59>: mov $0x1,%dl ;edx=1
      0x08049011 <+61>: cmp (%esi),%al
      0x08049013 <+63>: jne 0x8049039 <strings_not_equal+101>;if(*s1!=*s0)->return 1
      0x08049015 <+65>: mov $0x0,%eax ;eax=0(字符串从*s+1开始的偏移量)
      0x0804901a <+70>: jmp 0x8049024 <strings_not_equal+80>
      0x0804901c <+72>: add $0x1,%eax
      0x0804901f <+75>: cmp (%esi,%eax,1),%dl ;if(*s0+(x++)+1!=*s1+x+1)
      0x08049022 <+78>: jne 0x8049034 <strings_not_equal+96> ;return 1
      0x08049024 <+80>: movzbl 0x1(%ebx,%eax,1),%edx
      ;edx存入ebx+(eax存储的偏移量)+1,即*s1+x+1
      0x08049029 <+85>: test %dl,%dl
      0x0804902b <+87>: jne 0x804901c <strings_not_equal+72> ;if(*s1+x+1!=0) 执行循环
      0x0804902d <+89>: mov $0x0,%edx
      0x08049032 <+94>: jmp 0x8049039 <strings_not_equal+101>;否则完成比较,字符串相等,return 0
      0x08049034 <+96>: mov $0x1,%edx
      0x08049039 <+101>: mov %edx,%eax ;if(edx==1) return 1;else return 0
      0x0804903b <+103>: mov 0x4(%esp),%ebx
      0x0804903f <+107>: mov 0x8(%esp),%esi
      0x08049043 <+111>: mov 0xc(%esp),%edi
      0x08049047 <+115>: add $0x10,%esp
      0x0804904a <+118>: ret
      End of assembler dump.
      • <string_length>的汇编代码如下:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        Dump of assembler code for function string_length:
        0x08048fbb <+0>: mov 0x4(%esp),%edx ;edx=[0x4+esp] --字符串首地址
        0x08048fbf <+4>: mov $0x0,%eax ;eax=0 --计数
        0x08048fc4 <+9>: cmpb $0x0,(%edx)
        0x08048fc7 <+12>: je 0x8048fd2 <string_length+23> ;edx=0则退出
        0x08048fc9 <+14>: add $0x1,%eax ;eax++ --偏移量++
        0x08048fcc <+17>: cmpb $0x0,(%edx,%eax,1) ;edx+eax!=0,即当前字符不为空
        0x08048fd0 <+21>: jne 0x8048fc9 <string_length+14> ;则继续循环
        0x08048fd2 <+23>: repz ret ;返回eax则为字符串长度
        End of assembler dump.
  • 所以我们要使输入的字符串和0x804a1a4存储的字符串相等。

  • 查看0x804a1a4存储的字符串:

image-20210510225736415

  • 运行./bomb,输入该字符串:Crikey! I have lost my mojo!,即可得到通过第一关:

image-20210511204851504

2. phase_2

  • phase_2的汇编代码(调用函数<read_six_numbers>前的部分)
1
2
3
4
5
6
7
8
9
10
Dump of assembler code for function phase_2:
0x08048b74 <+0>: push %esi ;调用程序的地址入栈
0x08048b75 <+1>: push %ebx ;被调用者保存寄存器的值入栈
0x08048b76 <+2>: sub $0x34,%esp ;栈指针esp减去0x34,
;--给当前帧开辟大小为0x34的空间
0x08048b79 <+5>: lea 0x18(%esp),%eax ;eax=地址(0x18+esp)
0x08048b7d <+9>: mov %eax,0x4(%esp) ;M[0x4+esp]=eax=地址(0x18+esp)-数组首地址
0x08048b81 <+13>: mov 0x40(%esp),%eax ;eax=M[0x40+esp]
0x08048b85 <+17>: mov %eax,(%esp) ;M[esp]=eax=M[0x40+esp] -输入
0x08048b88 <+20>: call 0x804921b <read_six_numbers> ;调用函数(读入6个数)
  • <read_six_numbers>的汇编代码如下:

    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
    #调用函数 esp=esp-4
    Dump of assembler code for function read_six_numbers:
    0x0804921b <+0>: sub $0x2c,%esp ;栈指针esp减去0x2c
    0x0804921e <+3>: mov 0x34(%esp),%eax ;eax=esp+0x34 [原esp+0x4]
    0x08049222 <+7>: lea 0x14(%eax),%edx
    0x08049225 <+10>: mov %edx,0x1c(%esp) ;读入第六个数
    0x08049229 <+14>: lea 0x10(%eax),%edx
    0x0804922c <+17>: mov %edx,0x18(%esp) ;读入第五个数
    0x08049230 <+21>: lea 0xc(%eax),%edx
    0x08049233 <+24>: mov %edx,0x14(%esp) ;读入第四个数
    0x08049237 <+28>: lea 0x8(%eax),%edx
    0x0804923a <+31>: mov %edx,0x10(%esp) ;读入第三个数
    0x0804923e <+35>: lea 0x4(%eax),%edx
    0x08049241 <+38>: mov %edx,0xc(%esp) ;读入第二个数
    0x08049245 <+42>: mov %eax,0x8(%esp) ;读入第一个数
    0x08049249 <+46>: movl $0x804a3bf,0x4(%esp)
    0x08049251 <+54>: mov 0x30(%esp),%eax ;eax=esp+0x30
    0x08049255 <+58>: mov %eax,(%esp) ;M[esp]=eax=输入
    0x08049258 <+61>: call 0x8048870 <__isoc99_sscanf@plt>
    ;函数__isoc99_sscanf返回输入参数的个数
    0x0804925d <+66>: cmp $0x5,%eax
    0x08049260 <+69>: jg 0x8049267 <read_six_numbers+76> ;输入参数数量大于5则安全退出该函数
    0x08049262 <+71>: call 0x80490e6 <explode_bomb>
    0x08049267 <+76>: add $0x2c,%esp
    0x0804926a <+79>: ret
    End of assembler dump.
    • 函数<__isoc99_sscanf@plt>的汇编代码:

      1
      2
      3
      4
      5
      Dump of assembler code for function __isoc99_sscanf@plt:
      0x08048870 <+0>: jmp *0x804c040
      0x08048876 <+6>: push $0x80
      0x0804887b <+11>: jmp 0x8048760
      End of assembler dump.

      查看__isoc99_sscanf调用的第二个参数,可知可以成功输入6个参数的形式

      image-20210511202402385

  • phase_2的汇编代码(调用函数<read_six_numbers>后的部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
   0x08048b8d <+25>:	cmpl   $0x1,0x18(%esp)					;比较M[0x18+esp]和0x1的大小
0x08048b92 <+30>: je 0x8048b99 <phase_2+37> ;若相等,跳转至<phase_2+37>
0x08048b94 <+32>: call 0x80490e6 <explode_bomb> ;否则爆炸
0x08048b99 <+37>: lea 0x1c(%esp),%ebx ;ebx=地址(0x1c+esp) --第一次
0x08048b9d <+41>: lea 0x30(%esp),%esi ;esi=地址(0x30+esp)
0x08048ba1 <+45>: mov -0x4(%ebx),%eax ;eax=M[ebx-0x4]
;--第一次为M[0x18+esp]=1
0x08048ba4 <+48>: add %eax,%eax ;eax=2M[ebx-0x4]
0x08048ba6 <+50>: cmp %eax,(%ebx) ;比较M[ebx]和2M[ebx-0x4]的大小
;--第一次为M[0x1c+esp]和2M[0x18+esp]
0x08048ba8 <+52>: je 0x8048baf <phase_2+59> ;若相等,跳转至<phase_2+59>
0x08048baa <+54>: call 0x80490e6 <explode_bomb> ;否则爆炸
0x08048baf <+59>: add $0x4,%ebx ;ebx=ebx+0x4
;--第一次变为地址(0x20+esp)
0x08048bb2 <+62>: cmp %esi,%ebx ;比较(0x30+esp)和ebx所存地址的大小
0x08048bb4 <+64>: jne 0x8048ba1 <phase_2+45> ;若不相等则跳转至<phase_2+45>,执行循环
0x08048bb6 <+66>: add $0x34,%esp ;循环结束后,释放栈帧空间
0x08048bb9 <+69>: pop %ebx ;还原ebx
0x08048bba <+70>: pop %esi ;弹出入口地址
0x08048bbb <+71>: ret ;成功结束phase_2
End of assembler dump.
  • 通过上方汇编代码的<+62>、<+64>可知,该函数会执行循环,在每次循环中ebx所存地址(初始地址为0x1c+esp)的值会先加0x4,直到等于0x30+esp。故循环的执行次数为5次。0x1c+esp的初始值为M[0x18+esp]=1的两倍为2,如此循环。可以推知我们输入6个数存储地址分别为上方的(0x18+esp)(0x1c+esp)(0x20+esp)(0x24+esp)(0x28+esp)(0x2c+esp),是以1为首项,2为公比的等比数列。故通关密码为:1 2 4 8 16 32,结果如下:

image-20210511204913739

3. phase_3

  • phase_3的汇编代码(调用<__isoc99_sscanf@plt>前的部分)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Dump of assembler code for function phase_3:
    0x08048bbc <+0>: sub $0x2c,%esp ;栈指针esp减去0x2c
    0x08048bbf <+3>: lea 0x1c(%esp),%eax ;eax=(0x1c+esp)
    0x08048bc3 <+7>: mov %eax,0xc(%esp) ;M[0xc+esp]=(0x1c+esp)
    0x08048bc7 <+11>: lea 0x18(%esp),%eax ;eax=(0x18+esp)
    0x08048bcb <+15>: mov %eax,0x8(%esp) ;M[0x8+esp]=(0x18+esp)
    0x08048bcf <+19>: movl $0x804a3cb,0x4(%esp) ;M[0x4+esp]=0x804a3cb
    0x08048bd7 <+27>: mov 0x30(%esp),%eax ;eax=M[0x30+esp]
    0x08048bdb <+31>: mov %eax,(%esp) ;M[esp]=eax
    0x08048bde <+34>: call 0x8048870 <__isoc99_sscanf@plt>
    • 这里也调用了函数 <__isoc99_sscanf@plt>。查看其第二个参数所在地址$0x804a3cb的内容:

    image-20210511211431900

    • 可知输入应该为两个整型数据,对应的保存地址分别为0x18+esp,0x1c+esp
  • phase_3的汇编代码(剩余部分):

    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
       0x08048be3 <+39>:	cmp    $0x1,%eax					
    ;`<__isoc99_sscanf@plt>的返回值为输入参数的个数,保存在eax中
    0x08048be6 <+42>: jg 0x8048bed <phase_3+49> ;参数个数需大于1,否则爆炸
    0x08048be8 <+44>: call 0x80490e6 <explode_bomb>
    0x08048bed <+49>: cmpl $0x7,0x18(%esp)
    0x08048bf2 <+54>: ja 0x8048c5a <phase_3+158>
    ;第一个整数需要无符号小于等于7,即范围为0-7否则跳转->爆炸
    0x08048bf4 <+56>: mov 0x18(%esp),%eax
    0x08048bf8 <+60>: jmp *0x804a1e0(,%eax,4)
    ;无条件跳转至地址(M[0x804a1e0+4*eax]),即根据第一个整数为跳转的偏移量
    ;根据跳转表,以eax=1为例子如下:
    0x08048bff <+67>: mov $0x0,%eax ;eax=0
    0x08048c04 <+72>: jmp 0x8048c0b <phase_3+79> ;跳转至<phase_3+79>
    0x08048c06 <+74>: mov $0x240,%eax ;(被跳过)
    0x08048c0b <+79>: sub $0x28b,%eax ;eax=eax-0x28b=-0x28b
    0x08048c10 <+84>: jmp 0x8048c17 <phase_3+91> ;跳转至<phase_3+91>
    0x08048c12 <+86>: mov $0x0,%eax ;(被跳过)
    0x08048c17 <+91>: add $0x35a,%eax ;eax=eax+0x35a=0xcf
    0x08048c1c <+96>: jmp 0x8048c23 <phase_3+103> ;跳转至<phase_3+103>
    0x08048c1e <+98>: mov $0x0,%eax ;(被跳过)
    0x08048c23 <+103>: sub $0xc5,%eax ;eax=eax-0xc5=0xa
    0x08048c28 <+108>: jmp 0x8048c2f <phase_3+115> ;跳转至<phase_3+115>
    0x08048c2a <+110>: mov $0x0,%eax ;(被跳过)
    0x08048c2f <+115>: add $0xc5,%eax ;eax=eax+0xc5=0xcf
    0x08048c34 <+120>: jmp 0x8048c3b <phase_3+127> ;跳转至<phase_3+127>
    0x08048c36 <+122>: mov $0x0,%eax ;(被跳过)
    0x08048c3b <+127>: sub $0xc5,%eax ;eax=eax-0xc5=0xa
    0x08048c40 <+132>: jmp 0x8048c47 <phase_3+139> ;跳转至<phase_3+139>
    0x08048c42 <+134>: mov $0x0,%eax ;(被跳过)
    0x08048c47 <+139>: add $0xc5,%eax ;eax=eax+0xc5=0xcf
    0x08048c4c <+144>: jmp 0x8048c53 <phase_3+151> ;跳转至<phase_3+151>
    0x08048c4e <+146>: mov $0x0,%eax ;(被跳过)
    0x08048c53 <+151>: sub $0xc5,%eax ;eax=eax-0xc5=0xa
    0x08048c58 <+156>: jmp 0x8048c64 <phase_3+168> ;(需要此步)跳转至<phase_3+168>,否则爆炸
    0x08048c5a <+158>: call 0x80490e6 <explode_bomb>
    0x08048c5f <+163>: mov $0x0,%eax
    0x08048c64 <+168>: cmpl $0x5,0x18(%esp)
    0x08048c69 <+173>: jg 0x8048c71 <phase_3+181> ;M[0x18+esp]>5则爆炸
    0x08048c6b <+175>: cmp 0x1c(%esp),%eax
    0x08048c6f <+179>: je 0x8048c76 <phase_3+186> ;eax=M[0X1c+esp]时跳转过关,否则爆炸
    0x08048c71 <+181>: call 0x80490e6 <explode_bomb>
    0x08048c76 <+186>: add $0x2c,%esp
    0x08048c79 <+189>: ret
    End of assembler dump.
    • 从该汇编代码的<+156>到结束可知,我们需要经过跳转到达0x08048c58 <+156>,且输入的第一个数的值需要<=5<=5,输入的第二个数需要等于eax。

    • 我们通过x指令,参照jmp *0x804a1e0(,%eax,4) 的跳转表如下:

      image-20210512084515692

    • 上述汇编代码的注释输入的第一个数M[0x18+esp]=1为例,先跳转至<phase_3+67>

      得到最终M[0x1c+esp]需要等于0xa,即十进制的10。故我们可以输入1 10过关:

      image-20210512085729848

      • 同样可推知输入0—7对应的第二个数组合如下:

        0 586 1 10 2 661 3 -197 4 0 5 -197 6 0 7 197

      • 同样输入0~5中的其他数以及经过运算得到的相对应的第二个数,也能过关。

4. phase_4

  • phase_4的汇编代码(调用<__isoc99_sscanf@plt>前的部分)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Dump of assembler code for function phase_4:
    0x08048cd7 <+0>: sub $0x2c,%esp ;栈指针esp减去0x2c
    0x08048cda <+3>: lea 0x18(%esp),%eax ;eax=(0x18+esp)
    0x08048cde <+7>: mov %eax,0xc(%esp) ;M[0xc+esp]=eax=(0x18+esp)
    0x08048ce2 <+11>: lea 0x1c(%esp),%eax ;eax=(0x1c+esp)
    0x08048ce6 <+15>: mov %eax,0x8(%esp) ;M[0x8+esp]=eax=(0x1c+esp)
    0x08048cea <+19>: movl $0x804a3cb,0x4(%esp) ;M[0x4+esp]=0x804a3cb
    0x08048cf2 <+27>: mov 0x30(%esp),%eax ;eax=M[0x30+esp]
    0x08048cf6 <+31>: mov %eax,(%esp) ;M[esp]=eax=M[0x30+esp]
    0x08048cf9 <+34>: call 0x8048870 <__isoc99_sscanf@plt>
    • 和phase_3相似,调用函数<__isoc99_sscanf@plt>将输入以地址0x804a3cb存储的形式保存:

      image-20210512133109861

      可见输入为两个整型数据,分别保存在地址0x1c+esp0x18+esp中。(要注意0x1c+esp是第三个参数,0x18+esp是第四个参数,表示输入数据存储的次序,和phase_3正好相反!)。

      函数返回值为输入数据的个数。

  • phase_4的汇编代码(剩余部分)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
       0x08048cfe <+39>:	cmp    $0x2,%eax				;比较<__isoc99_sscanf@plt>的返回值和2
    0x08048d01 <+42>: jne 0x8048d11 <phase_4+58> ;不相等则爆炸
    0x08048d03 <+44>: mov 0x18(%esp),%eax ;eax=M[0x18+esp] 即输入的第二个整数
    0x08048d07 <+48>: cmp $0x1,%eax ;比较eax和1
    0x08048d0a <+51>: jle 0x8048d11 <phase_4+58> ;eax<=1则爆炸,故第二个整数要大于1
    0x08048d0c <+53>: cmp $0x4,%eax ;比较eax和4
    0x08048d0f <+56>: jle 0x8048d16 <phase_4+63> ;eax<=4则不会爆炸,故第二个整数小于等于4
    0x08048d11 <+58>: call 0x80490e6 <explode_bomb>
    0x08048d16 <+63>: mov 0x18(%esp),%eax ;eax=M[0x18+esp],还是第二个整数
    0x08048d1a <+67>: mov %eax,0x4(%esp) ;M[0x4+esp]=eax=M[0x18+esp]
    0x08048d1e <+71>: movl $0x9,(%esp) ;M[esp]=9
    0x08048d25 <+78>: call 0x8048c7a <func4> ;调用函数<func4>
    0x08048d2a <+83>: cmp 0x1c(%esp),%eax
    ;比较<func4>的返回值和M[esp+0x1c](即输入的第一个整数)的大小
    0x08048d2e <+87>: je 0x8048d35 <phase_4+94> ;相等则过关
    0x08048d30 <+89>: call 0x80490e6 <explode_bomb>
    0x08048d35 <+94>: add $0x2c,%esp
    0x08048d38 <+97>: ret
    End of assembler dump.
    • 故输入的第二个整数(大于1且小于等于4)会进入函数<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
      31
      32
      33
      34
      ;call调用函数,esp=esp-4。观察整个代码,有两个参数,设为x和y(初值为9和输入的第一个整数)
      (gdb) disass func4
      Dump of assembler code for function func4:
      0x08048c7a <+0>: sub $0x1c,%esp ;esp=esp-0x1c,开辟0x1c的栈帧空间
      0x08048c7d <+3>: mov %ebx,0x10(%esp)
      0x08048c81 <+7>: mov %esi,0x14(%esp)
      0x08048c85 <+11>: mov %edi,0x18(%esp) ;以上三行保存被调用寄存器的原值
      0x08048c89 <+15>: mov 0x20(%esp),%esi ;esi=M[0x20+esp]=x
      0x08048c8d <+19>: mov 0x24(%esp),%ebx ;ebx=M[0x24+esp]=y
      0x08048c91 <+23>: test %esi,%esi
      0x08048c93 <+25>: jle 0x8048cc0 <func4+70> ;ZF=1或SF!=OF时跳转
      ;test %esi,%esi后,若esi=0,则ZF=1;或esi为负数,SF!=OF ->return 0
      0x08048c95 <+27>: cmp $0x1,%esi
      0x08048c98 <+30>: je 0x8048cc5 <func4+75> ;x=1时跳转至<func4+75> ->return y
      0x08048c9a <+32>: mov %ebx,0x4(%esp) ;M[0x4+esp]=ebx=y
      0x08048c9e <+36>: lea -0x1(%esi),%eax ;eax=(esi-1)=(x-1)
      0x08048ca1 <+39>: mov %eax,(%esp) ;M[esp]=eax=(esi-1)=(x-1)
      0x08048ca4 <+42>: call 0x8048c7a <func4> ;递归调用func4(x-1,y)
      0x08048ca9 <+47>: lea (%eax,%ebx,1),%edi ;edi=(eax+ebx)=func4(x-1,y)+y;
      0x08048cac <+50>: mov %ebx,0x4(%esp) ;M[0x4+esp]=ebx=y
      0x08048cb0 <+54>: sub $0x2,%esi ;esi=(esi-2)=(x-2)
      0x08048cb3 <+57>: mov %esi,(%esp) ;M[esp]=esi=(x-2)
      0x08048cb6 <+60>: call 0x8048c7a <func4> ;递归调用func4(x-2,y)
      0x08048cbb <+65>: lea (%edi,%eax,1),%ebx ;ebx=(edi+eax)=func4(x-1,y)+y+func4(x-2,y)
      0x08048cbe <+68>: jmp 0x8048cc5 <func4+75>
      ;跳转至<func4+75> ->return func4(x-1,y)+y+func4(x-2,y)
      0x08048cc0 <+70>: mov $0x0,%ebx
      0x08048cc5 <+75>: mov %ebx,%eax ;eax=ebx,即为返回值
      0x08048cc7 <+77>: mov 0x10(%esp),%ebx
      0x08048ccb <+81>: mov 0x14(%esp),%esi
      0x08048ccf <+85>: mov 0x18(%esp),%edi ;以上三行恢复被调用寄存器的值
      0x08048cd3 <+89>: add $0x1c,%esp
      0x08048cd6 <+92>: ret
      End of assembler dump.
      • 可以得到func4函数和主函数的C++代码如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      int func4(int x,int y)
      {
      if(x<=0)
      return 0;
      else if(x==1)
      return y;
      else
      return func4(x-1,y)+y+func4(x-2,y);
      }
      int main()
      {
      cin>>a>>b;
      if(b<=1||b>4)
      explode_bomb();
      else
      {
      if(func4(9,b)!=a)
      explode_bomb();
      }
      return 0;
      }
      • 另写代码输出func4得到的第二个整数(只能为2、3、4)对应的第一个整数:

      image-20210512154349129

    • 输入这3组整数都能过关。

      image-20210512154529595

      image-20210512155152270

      image-20210512155207546

5. phase_5

  • phase_5的汇编代码如下:

    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
    Dump of assembler code for function phase_5:
    0x08048d39 <+0>: push %ebx ;保存ebx的原值
    0x08048d3a <+1>: sub $0x18,%esp ;esp=esp-0x18,预留0x18的栈帧空间
    0x08048d3d <+4>: mov 0x20(%esp),%ebx ;ebx=M[0x20+esp],为输入的字符串
    0x08048d41 <+8>: mov %ebx,(%esp) ;M[esp]=ebx
    0x08048d44 <+11>: call 0x8048fbb <string_length>
    ;调用函数<string_length>(phase_1中出现过),返回值为输入字符串的长度
    0x08048d49 <+16>: cmp $0x6,%eax
    0x08048d4c <+19>: je 0x8048d53 <phase_5+26> ;若eax=6则跳转
    0x08048d4e <+21>: call 0x80490e6 <explode_bomb> ;否则爆炸
    0x08048d53 <+26>: mov $0x0,%edx ;edx=0
    0x08048d58 <+31>: mov $0x0,%eax ;eax=0
    0x08048d5d <+36>: movsbl (%ebx,%eax,1),%ecx ;ecx=M(ebx+eax),高位用符号位补齐
    0x08048d61 <+40>: and $0xf,%ecx ;保留ecx的低四位(字符的ASCII码对16取余)
    0x08048d64 <+43>: add 0x804a200(,%ecx,4),%edx ;edx=edx+M[0x804a200+4*ecx]
    0x08048d6b <+50>: add $0x1,%eax ;eax=eax+1
    0x08048d6e <+53>: cmp $0x6,%eax
    0x08048d71 <+56>: jne 0x8048d5d <phase_5+36> ;eax不等于6则跳转至<phase_5+36>,执行循环
    0x08048d73 <+58>: cmp $0x2b,%edx
    0x08048d76 <+61>: je 0x8048d7d <phase_5+68> ;若edx=0x2b 则过关
    0x08048d78 <+63>: call 0x80490e6 <explode_bomb>
    0x08048d7d <+68>: add $0x18,%esp
    0x08048d80 <+71>: pop %ebx
    0x08048d81 <+72>: ret
    End of assembler dump.
    • 可见输入的字符串长度为6。在循环中,依次取字符串的1个字符,ASCII码对16取余,保存于ecx中。每次edx加上M[0x804a200+4*ecx],相当于0x804a200为数组,ecx为数组的索引。

    • 循环执行次数为6次。结束循环后,edx的值需为0x2b(即43)。

      • 查看首地址为0x804a200的数组保存的数字,数组长度为16,如下:

        image-20210512202614163

        • 需要在其中取6个数,和为43。可选择:13、10、12、4、3、1。(不唯一)
        • 对应索引分别为:15、1、4、8、7、3,分别对应字符ASCII码的余数。
      • 对应字符可以为:O、A、D、H、G、C。(答案不唯一)

  • 输入OADHGC,过关:

    image-20210512203929992

6. phase_6

  • phase_6的汇编代码(分为多个部分)如下:

  • 第一部分:

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
Dump of assembler code for function phase_6:
0x08048d82 <+0>: push %esi ;esi入栈
0x08048d83 <+1>: push %ebx ;ebx入栈
0x08048d84 <+2>: sub $0x44,%esp ;esp=esp-0x44,栈帧预留0x44的空间
0x08048d87 <+5>: lea 0x10(%esp),%eax ;eax=(esp+0x10)
0x08048d8b <+9>: mov %eax,0x4(%esp) ;M[0x4+esp]=eax
0x08048d8f <+13>: mov 0x50(%esp),%eax ;eax=M[0x50+esp]=输入
0x08048d93 <+17>: mov %eax,(%esp) ;M[esp]=eax=输入
0x08048d96 <+20>: call 0x804921b <read_six_numbers>
;调用函数<read_six_numbers>(phase_2中出现过),读入6个数,相当于读入首地址为(esp+0x10)的长度为6的int型数组 a[]
0x08048d9b <+25>: mov $0x0,%esi ;esi=0
0x08048da0 <+30>: mov 0x10(%esp,%esi,4),%eax ;eax=M[0x10+(esp+4*esi)]
0x08048da4 <+34>: sub $0x1,%eax ;eax=eax-1=M[eax]-1
0x08048da7 <+37>: cmp $0x5,%eax
0x08048daa <+40>: jbe 0x8048db1 <phase_6+47> ;若eax<=5(无符号),则跳转至<phase_6+47>
0x08048dac <+42>: call 0x80490e6 <explode_bomb> ;否则爆炸
0x08048db1 <+47>: add $0x1,%esi ;esi=esi+1
0x08048db4 <+50>: cmp $0x6,%esi
0x08048db7 <+53>: je 0x8048dd4 <phase_6+82> ;esi=6则跳转至<phase_6+82>
0x08048db9 <+55>: mov %esi,%ebx ;ebx=esi
0x08048dbb <+57>: mov 0x10(%esp,%ebx,4),%eax ;eax=M[0x10+(esp+4*ebx)]
0x08048dbf <+61>: cmp %eax,0xc(%esp,%esi,4)
0x08048dc3 <+65>: jne 0x8048dca <phase_6+72>
;若M[0xc+(esp+4*esi)]和eax不相等,则跳转至<phase_6+72>
0x08048dc5 <+67>: call 0x80490e6 <explode_bomb> ;否则爆炸
0x08048dca <+72>: add $0x1,%ebx ;ebx=ebx+1
0x08048dcd <+75>: cmp $0x5,%ebx
0x08048dd0 <+78>: jle 0x8048dbb <phase_6+57>
;若ebx<=5,则跳转至<phase_6+57> -> eax=M[0x10+(esp+4*ebx)]=a[ebx]
0x08048dd2 <+80>: jmp 0x8048da0 <phase_6+30>
;否则跳转至<phase_6+30> -> eax=M[0x10+(esp+4*esi)]=a[esi]
  • 在读入6个数后,根据汇编代码可以推得对应的C++代码(数组为a[]):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    for(int i=0;i<=5;i++)
    {
    if(a[i]-1>5||a[i]-1<0)
    explode_bomb();
    else
    for(int j=i+1;j<=5;j++)
    if(a[j]==a[i])
    explode_bomb();
    }
    //结束后跳转至<phase_6+82>
    • 这个双重循环确保数组a[]中的6个整数不相等且范围为[1,6]。
  • 第二部分:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    0x08048dd4 <+82>:	lea    0x10(%esp),%eax				;eax=(0x10+esp)
    0x08048dd8 <+86>: lea 0x28(%esp),%ebx ;ebx=(0x28+esp)
    0x08048ddc <+90>: mov $0x7,%ecx ;ecx=7
    0x08048de1 <+95>: mov %ecx,%edx ;edx=ecx=7
    0x08048de3 <+97>: sub (%eax),%edx ;edx=edx-M[eax]=7-M[eax]
    0x08048de5 <+99>: mov %edx,(%eax) ;M[eax]=edx
    0x08048de7 <+101>: add $0x4,%eax ;eax=eax+4
    0x08048dea <+104>: cmp %ebx,%eax
    0x08048dec <+106>: jne 0x8048de1 <phase_6+95> ;若eax!=ebx,则回跳至<phase_6+95>
    • 这部分汇编代码写成C++代码如下:

      1
      2
      3
      4
      for(int i=0;a[i]!=a[6];i++)
      {
      a[i]=7-a[i];
      }
  • 第三部分:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    0x08048dee <+108>:	mov    $0x0,%ebx					;ebx=0;
    0x08048df3 <+113>: jmp 0x8048e0b <phase_6+137> ;跳转至<phase_6+137>
    0x08048df5 <+115>: mov 0x8(%edx),%edx ;edx=M[0x8+edx]
    0x08048df8 <+118>: add $0x1,%eax ;eax=eax+1
    0x08048dfb <+121>: cmp %ecx,%eax
    0x08048dfd <+123>: jne 0x8048df5 <phase_6+115> ;若eax!=ecx,回跳至<phase_6+115>
    0x08048dff <+125>: mov %edx,0x28(%esp,%esi,4) ;M[0x28+esp+4*esi]=edx
    0x08048e03 <+129>: add $0x1,%ebx ;ebx=ebx+1
    0x08048e06 <+132>: cmp $0x6,%ebx
    0x08048e09 <+135>: je 0x8048e22 <phase_6+160> ;若ebx=6,跳转至<phase_6+160>(跳出循环)
    0x08048e0b <+137>: mov %ebx,%esi ;esi=ebx
    0x08048e0d <+139>: mov 0x10(%esp,%ebx,4),%ecx ;ecx=M[0x10+esp+4*ebx]=a[ebx]
    0x08048e11 <+143>: mov $0x1,%eax ;eax=1;
    0x08048e16 <+148>: mov $0x804c13c,%edx ;edx=0x804c13c
    ;以上4行相当于初始化寄存器
    0x08048e1b <+153>: cmp $0x1,%ecx
    0x08048e1e <+156>: jg 0x8048df5 <phase_6+115> ;若a[ebx]>1,跳转至<phase_6+115>
    0x08048e20 <+158>: jmp 0x8048dff <phase_6+125> ;否则跳转至<phase_6+125>
    • edx=0x804c13cedx=M[0x8+edx] 为突破口,先查看0x804c13c存放的东西:

      image-20210512225255740

      • 由node联想到结点类(结构体),且地址0x8+edx存储的为指向下一结点的地址。
    • 依次查看结构体的信息如下,共有6个结构体:

      image-20210512233328889

      • 可以观察到结构体有一个值(val)、编号(id)、下一节点的地址(*next)构成,形成链表结构。
    • 可推成C++代码帮助理解,如下:

      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
      struct node
      {
      int val;
      int id;
      node *next;
      };
      node *n[6];
      ebx=0;
      do
      {
      esi=ebx;
      ecx=a[ebx];
      eax=1;
      edx=0x804c13c //头结点的首地址
      if(a[ebx]>1) //数组a[]的范围为[1,6],只有a[ebx]等于1时不执行if内的循环。
      {
      do
      {
      edx=M[0x8+edx]; //下一结点地址
      eax++;
      }while(eax!=ecx); //直到eax=a[ebx]时跳出循环,故循环次数为a[ebx]的值
      }
      node[esi]=edx; //故第i次循环是为了让原先的第a[i-1]个结点变为node[i-1]。 (i=ebx+1,esi=ebx)
      //其中a[i-1]=(7-输入的第i个值)
      ebx++;
      }while(ebx<6) //共执行循环6次
      • 故根据数组a[]的值,给6个node节点重新排列:让原先的第a[i-1]个结点变为node[i-1]。其中a[i-1]=(7-输入的第i个值)。
  • 第四部分

    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
       0x08048e22 <+160>:	mov    0x28(%esp),%ebx			;ebx=M[0x28+esp]	--node[0]的地址
    0x08048e26 <+164>: mov 0x2c(%esp),%eax ;eax=M[0x2c+esp] --node[1]的地址
    0x08048e2a <+168>: mov %eax,0x8(%ebx) ;M[ebx+0x8]=eax --node[0].next=node[1]
    ;以下至<+196>都为连接结点、形成链表的操作
    0x08048e2d <+171>: mov 0x30(%esp),%edx
    0x08048e31 <+175>: mov %edx,0x8(%eax)
    0x08048e34 <+178>: mov 0x34(%esp),%eax
    0x08048e38 <+182>: mov %eax,0x8(%edx)
    0x08048e3b <+185>: mov 0x38(%esp),%edx
    0x08048e3f <+189>: mov %edx,0x8(%eax)
    0x08048e42 <+192>: mov 0x3c(%esp),%eax
    0x08048e46 <+196>: mov %eax,0x8(%edx)
    0x08048e49 <+199>: movl $0x0,0x8(%eax) ;M[0x8+eax]=0,node[5].next=0(空)
    0x08048e50 <+206>: mov $0x5,%esi ;esi=5
    0x08048e55 <+211>: mov 0x8(%ebx),%eax ;eax=M[0x8+ebx] --下一节点的地址
    0x08048e58 <+214>: mov (%eax),%edx ;edx=M[eax] --下一结点的值
    0x08048e5a <+216>: cmp %edx,(%ebx)
    0x08048e5c <+218>: jge 0x8048e63 <phase_6+225>
    ;若M[ebx]>=edx,即某结点的值不小于其后一个结点的值,则不会爆炸
    0x08048e5e <+220>: call 0x80490e6 <explode_bomb> ;否则爆炸
    0x08048e63 <+225>: mov 0x8(%ebx),%ebx ;ebx=M[ebx+0x8] --下一个结点的地址
    0x08048e66 <+228>: sub $0x1,%esi ;esi=esi-1
    0x08048e69 <+231>: jne 0x8048e55 <phase_6+211> ;当esi!=0时,回跳至<phase_6+211>,执行循环
    ;故执行<+211>到<+231>的次数为6。
    0x08048e6b <+233>: add $0x44,%esp ;恢复栈帧,过关。
    0x08048e6e <+236>: pop %ebx
    0x08048e6f <+237>: pop %esi
    0x08048e70 <+238>: ret
    End of assembler dump.
    • 故phase_6要求输入6个数,根据这6个数,对带值(val)的6个结点进行重新排序,使得结点的值降序排列。

    • 根据之前查看的结点信息:

      image-20210512233328889

      可以列表如下:

      原结点编号 1 2 3 4 5 6
      原值 0xdf 0x252 0x376 0xea 0x2fb 0xf3
      目标编号 6 3 1 5 2 4
    • 让原先的第a[i]个结点变为node[i],故数组a[6]={3,5,2,6,4,1}

    • 故输入(7-a[i])4 2 5 1 3 6,过关截图如下:

      image-20210513114343306

7. phase_defused (含secret_phase)

  • 看似结束了。但回到bomb.c,在生成炸弹后,和输入有关的函数中,我们还有两个函数还没查看汇编代码,分别为read_line()
    phase_defused()。何况邪恶博士Dr. Evil还留下了这样一句话,好似幸灾乐祸:

    1
    2
    3
    /* Wow, they got it!  But isn't something... missing?  Perhaps
    * something they overlooked? Mua ha ha ha ha! */
    //哇,他们明白了! 但是,是不是……缺少了什么? 也许他们忽略了什么? 哇哈哈哈哈哈!
  • 先看了看read_line(),就是对一行输入进行花里胡哨的处理后保存在指定地址,没有什么特别之处。

  • phase_defused()的汇编代码如下:

    • 第一部分:
    1
    2
    3
    4
    5
    6
    7
    Dump of assembler code for function phase_defused:
    0x0804926b <+0>: sub $0x8c,%esp
    0x08049271 <+6>: mov %gs:0x14,%eax ;取gs:0x14处的值
    0x08049277 <+12>: mov %eax,0x7c(%esp) ;置于M[0x7c+esp]
    0x0804927b <+16>: xor %eax,%eax ;ZF=1
    0x0804927d <+18>: cmpl $0x6,0x804c3cc ;比较M[0x804c3cc]和6的值
    0x08049284 <+25>: jne 0x80492f8 <phase_defused+141> ;不相等则跳过secret_phase,故我们要让其相等
    • 猜测6和我们的关卡数量有关。故在进入每个关卡的函数的位置设断点,观察0x804c3cc的变化情况:

      • 进入关卡1前,M[0x804c3cc]=0

        image-20210513200430515

      • 进入关卡1后,M[0x804c3cc]=1:

        image-20210513200542728

      • 进入关卡2后,M[0x804c3cc]=2:

        image-20210513200748969

      • 进入关卡3后,M[0x804c3cc]=3:

        image-20210513200806250

      • 进入关卡4后,M[0x804c3cc]=4:

        image-20210513201243594

      • 进入关卡5后,M[0x804c3cc]=5:

        image-20210513201259639

      • 进入关卡6后,M[0x804c3cc]=6:

        image-20210513201315080

    • 显然只有在第6关之后,M[0x804c3cc]=6,才可能进入secret_phase。

    • 第二部分:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      0x08049286 <+27>:	lea    0x2c(%esp),%eax					;eax=(0x2c+esp)
      0x0804928a <+31>: mov %eax,0x10(%esp) ;M[0x10+esp]=eax=(0x2c+esp)
      0x0804928e <+35>: lea 0x28(%esp),%eax ;eax=(0x28+esp)
      0x08049292 <+39>: mov %eax,0xc(%esp) ;M[0xc+esp]=eax=(0x28+esp)
      0x08049296 <+43>: lea 0x24(%esp),%eax ;eax=(0x24+esp)
      0x0804929a <+47>: mov %eax,0x8(%esp) ;M[0x8+esp]=eax=(0x24+esp)
      0x0804929e <+51>: movl $0x804a3d1,0x4(%esp) ;M[0x4+esp]=eax=0x804a3d1
      0x080492a6 <+59>: movl $0x804c4d0,(%esp) ;M[esp]=0x804c4d0--从该位置读入
      0x080492ad <+66>: call 0x8048870 <__isoc99_sscanf@plt>
      0x080492b2 <+71>: cmp $0x3,%eax ;输入参数个数应等于3
      0x080492b5 <+74>: jne 0x80492ec <phase_defused+129> ;否则不会到达scret_phase
      • 调用<__isoc99_sscanf@plt>从0x804c4d0读入参数。根据函数的参数可知,共读入3个参数分别保存在(0x24+esp)(0x28+esp)(0x2c+esp)。输入参数的格式为地址0x804a3d1所示:

        image-20210513203029685

    • 第三部分:

      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
         0x080492b7 <+76>:	movl   $0x804a3da,0x4(%esp)				;M[0x4+esp]=0x804a3da
      0x080492bf <+84>: lea 0x2c(%esp),%eax ;eax=(0x2c+esp)
      0x080492c3 <+88>: mov %eax,(%esp) ;M[esp]=eax=(0x2c+esp)
      0x080492c6 <+91>: call 0x8048fd4 <strings_not_equal>
      ;0x804a3da存储的字符串和(0x2c+esp)中的字符串的字符串进行比较。若相等则eax=0。
      ;0x804a3da存储的字符串为 "DrEvil"
      ;(0x2c+esp)存储的为<__isoc99_sscanf@plt>读入的第三个参数%s对应的字符串。
      0x080492cb <+96>: test %eax,%eax
      0x080492cd <+98>: jne 0x80492ec <phase_defused+129> ;如果eax!=0,则会跳过secret_phase
      0x080492cf <+100>: movl $0x804a2a0,(%esp) ;M[esp]=0x804a2a0
      0x080492d6 <+107>: call 0x8048800 <puts@plt>
      ;查看0x804a2a0存储的值,得输出的字符串:"Curses, you've found the secret phase!"”
      0x080492db <+112>: movl $0x804a2c8,(%esp) ;;M[esp]=0x804a2c8
      0x080492e2 <+119>: call 0x8048800 <puts@plt>
      ;查看0x804a2c8存储的值,得输出的字符串:"But finding it and solving it are quite different..."
      0x080492e7 <+124>: call 0x8048ec2 <secret_phase> ;secret_phase:顾名思义,秘密关卡!
      0x080492ec <+129>: movl $0x804a300,(%esp)
      0x080492f3 <+136>: call 0x8048800 <puts@plt>
      0x080492f8 <+141>: mov 0x7c(%esp),%eax ;取出原%gs:0x14的值
      0x080492fc <+145>: xor %gs:0x14,%eax ;与现值异或
      0x08049303 <+152>: je 0x804930a <phase_defused+159>
      ;异或结果为0,栈保护者canary未被非法篡改,安全退出程序
      0x08049305 <+154>: call 0x80487d0 <__stack_chk_fail@plt>
      ;否则ZF=0,调用__stack_chk_fail,退出程序
      0x0804930a <+159>: add $0x8c,%esp
      0x08049310 <+165>: ret
      End of assembler dump.
      • 此时需要找到地址0x804c4d0存储的是什么,哪来的%d %d %s

      • 故在进入每个关卡的函数的位置设断点,观察地址0x804c4d0存储值的变化,进行调试:

        • 进入前三关,皆无变化:

          image-20210513204703675

        • 进入第4关,变化来了:

          image-20210513204722687

        • 0x804c4d0存储的是第4关的输入。

      • 我们在第4关的输入后增加字符串"DrEvil" ,即可打开秘密关卡secret_phase:

        image-20210513205050635

        image-20210513205105128

    secret_phase
    • 汇编代码如下:

      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
      Dump of assembler code for function secret_phase:
      0x08048ec2 <+0>: push %ebx ;ebx入栈
      0x08048ec3 <+1>: sub $0x18,%esp ;esp=esp-0x18,开辟栈帧空间
      0x08048ec6 <+4>: call 0x804910d <read_line> ;读取一行,保存在eax中
      0x08048ecb <+9>: movl $0xa,0x8(%esp) ;M[0x8+esp]=0xa=10 --strtol的第3个参数
      0x08048ed3 <+17>: movl $0x0,0x4(%esp) ;M[0x4+esp]=0 --strtol的第2个参数
      0x08048edb <+25>: mov %eax,(%esp) ;M[esp]=eax --strtol的第1个参数
      0x08048ede <+28>: call 0x80488e0 <strtol@plt>
      ;strtol将eax存储的字符串转化为10进制整数(使结果没有""),结束符为空字符。
      ;返回值10进制整数保存在eax
      0x08048ee3 <+33>: mov %eax,%ebx ;ebx=eax
      0x08048ee5 <+35>: lea -0x1(%eax),%eax ;eax=eax-1;
      0x08048ee8 <+38>: cmp $0x3e8,%eax ;比较eax和0x3e8
      0x08048eed <+43>: jbe 0x8048ef4 <secret_phase+50> ;若eax<=0x3e8,则不爆炸
      0x08048eef <+45>: call 0x80490e6 <explode_bomb>
      0x08048ef4 <+50>: mov %ebx,0x4(%esp) ;M[0x4+esp]=ebx
      0x08048ef8 <+54>: movl $0x804c088,(%esp) ;M[esp]=0x804c088
      0x08048eff <+61>: call 0x8048e71 <fun7>
      ;调用函数fun7(0x804c088,ebx),返回值保存在eax
      0x08048f04 <+66>: cmp $0x4,%eax
      0x08048f07 <+69>: je 0x8048f0e <secret_phase+76> ;若eax=4,则不爆炸且过关
      0x08048f09 <+71>: call 0x80490e6 <explode_bomb>
      0x08048f0e <+76>: movl $0x804a240,(%esp) ;M[esp]=0x804a240
      0x08048f15 <+83>: call 0x8048800 <puts@plt>
      ;输出:"Wow! You've defused the secret stage!"
      0x08048f1a <+88>: call 0x804926b <phase_defused> ;退出秘密关卡
      0x08048f1f <+93>: add $0x18,%esp
      0x08048f22 <+96>: pop %ebx
      0x08048f23 <+97>: ret
      End of assembler dump.
      • 其中,fun7的汇编代码如下:

        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
        Dump of assembler code for function fun7:
        0x08048e71 <+0>: push %ebx ;ebx入栈
        0x08048e72 <+1>: sub $0x18,%esp ;esp=esp-0x18,开辟栈帧空间
        0x08048e75 <+4>: mov 0x20(%esp),%edx ;edx=M[0x20+esp] --第一个参数
        0x08048e79 <+8>: mov 0x24(%esp),%ecx ;ecx=M[0x24+esp] --第二个参数
        0x08048e7d <+12>: test %edx,%edx
        0x08048e7f <+14>: je 0x8048eb8 <fun7+71> ;如果edx=0 跳转->return 0xffffffff
        0x08048e81 <+16>: mov (%edx),%ebx ;ebx=M[edx]
        0x08048e83 <+18>: cmp %ecx,%ebx
        0x08048e85 <+20>: jle 0x8048e9a <fun7+41> ;如果ebx<=ecx(有符号数),跳转至<fun7+41>
        0x08048e87 <+22>: mov %ecx,0x4(%esp) ;M[0x4+esp]=ecx
        0x08048e8b <+26>: mov 0x4(%edx),%eax ;eax=M[0x4+edx]
        0x08048e8e <+29>: mov %eax,(%esp) ;M[esp]=eax=M[0x4+edx]
        0x08048e91 <+32>: call 0x8048e71 <fun7> ;递归调用函数fun7(M[0x4+edx],ecx)
        0x08048e96 <+37>: add %eax,%eax ;eax=2*fun7(M[0x4+edx],ecx)
        0x08048e98 <+39>: jmp 0x8048ebd <fun7+76> ;跳转-> return 2*fun7(M[0x4+edx],ecx);
        0x08048e9a <+41>: mov $0x0,%eax ;eax=0
        0x08048e9f <+46>: cmp %ecx,%ebx
        0x08048ea1 <+48>: je 0x8048ebd <fun7+76> ;如果ebx=ecx,跳转-> return 0;
        0x08048ea3 <+50>: mov %ecx,0x4(%esp) ;M[0x4+esp]=ecx
        0x08048ea7 <+54>: mov 0x8(%edx),%eax ;eax=M[0x8+edx]
        0x08048eaa <+57>: mov %eax,(%esp) ;M[esp]=eax
        0x08048ead <+60>: call 0x8048e71 <fun7> ;递归调用函数fun7(M[0x8+edx],ecx)
        0x08048eb2 <+65>: lea 0x1(%eax,%eax,1),%eax;return eax=2*fun7(M[0x8+edx],ecx)+1
        0x08048eb6 <+69>: jmp 0x8048ebd <fun7+76>
        0x08048eb8 <+71>: mov $0xffffffff,%eax
        0x08048ebd <+76>: add $0x18,%esp
        0x08048ec0 <+79>: pop %ebx
        0x08048ec1 <+80>: ret
        End of assembler dump.
        • 显然fun7是个含2个参数的递归函数,C++代码可如下表示:

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          #include<iostream>
          using namespace std;
          int fun7(int *p,int m)
          {
          if(p==0)
          return -1;
          else
          {
          if(*p<m)
          return 2*fun7(*(p+2),m)+1;
          else if(*p==m)
          return 0
          else
          return 2*fun7(*(p+1),m);
          }
          }
          • 这个递归的返回值为0时结束递归,对于每个返回值,根据与m的大小关系,有两个递归分支,分别是2fun7((p+2),m)+12*fun7(*(p+2),m)+12fun7((p+1),m)2*fun7(*(p+1),m)

          • fun7最终的返回值为4,故0->1->2->4为递归返回的路径,则递归调用时地址的增量为0x4,0x4,0x8。

            image-20210514111858675

          • m=7,使得在最后一层递归返回0。整个过程如下:

            1. *p=24>m,return 2*fun7(*(p+1),m)
            2. *p=8>m,return 2*fun7(*(p+1),m)
            3. *p=6<m,return 2*fun7(*(p+1),m)+1
            4. *p=7=m,return 0
    • 输入7,fun7得到返回值4,通关:

      image-20210514112742119

实验结果

image-20210514112928319

image-20210514112906727

实验心得

  1. 这次实验的各个关卡对汇编语言实现的功能各有侧重:

    phase_1:字符串在内存地址中的存放、访问,字符串长度的获取与字符串的比较。

    phase_2:利用循环得到存储等比数列的数组。

    phase_3:实现switch语句,需要查看跳转表。

    phase_4:递归函数的实现。

    phase_5:数组及其索引。

    phase_6:链表及其重组。

    secret_phase:通过含指针参数的递归函数实现地址跳转。

    通过学习、理解汇编代码在该实验中实现的各种功能,我对汇编代码的各种指令,如比较、跳转、调用指令等有了更深的理解。当遇到难以直接明白其功能的地方,我会尝试把汇编语言转换成自己编写的C++代码帮助理解,从而对汇编的功能实现有了更全面的认识。

  2. 在实验过程中难免需要对汇编代码进行调试,查看某个寄存器或在某个地址内存的值及其变化。所以gdb工具的使用是不可或缺的。正是通过完成这次实验,我对gdb工具的使用愈发熟练。

  3. 在进行函数调用时,栈帧的变化格外重要。通过这次实验对汇编代码的调试,思考在何处保存函数的参数、何处保存临时变量等,我对栈帧的结构有了更深刻的认识。

  4. 进行实验,细心和耐心也是很重要的品质。有时候会因为不够细心而耽误时间,如phase_4中我看反了两个输入参数的位置,但好在能够及时发现并改正。在遭遇比较复杂的结构,如phase_6中重构链表的双重循环,则需要耐心分析汇编语句。有了细心和耐心的加持,才能更好地闯关一个个关卡,收获知识,提升技能。


本博客所有文章除特别声明外,均为博客作者本人编写整理,转载请联系作者!