CSAPP: BombLab 拆炸弹谜题题解(x86 环境)

慈云数据 6个月前 (05-13) 技术支持 40 0

【写在前面】

这是一个仍然需要修改和更新的 CSAPP : BombLab 的解题教程,本篇大多数情况下都在分析汇编代码,操作部分讲解的不多,我会在后期更新简化的操作教程。这是一篇本人在学习 IA-32 汇编指令并完成学校实验过程中一点浅薄的见解,现在将其整理出来与君分享。学识尚浅,高手勿喷。(2023.12.5)

2023.12.31 更新了隐藏关卡的破解过程,并修正了一些文字上的小错误。

 一、基本要求

  • 基本的 Linux 命令行使用,一些基本指令
  • 基本的汇编语言阅读能力
  • 基本的 gdb调试指令,本文会把使用到的 gdb 命令列出。

    二、题目要求

    运行一个二进制文件 bomb,它包括六个"阶段(phase)",每个阶段要求学生通过 stdin 输入一个特定的字符串。如果输入了预期的字符串,那么该阶段被"拆除",进入下一个阶段,直到所有炸弹被成功"拆除"。否则,炸弹就会"爆炸",打印出"BOOM!!!",并通知教师端。

    三、拆弹方法

            1.使用 objdump 对炸弹文件进行反汇编;

            2.理解汇编语言代码的行为或作用;

            3.使用 gdb 调试器单步跟踪调试每一阶段的机器代码,找出拆除炸弹所需的字符串;

            4.提交结果,撰写实验报告。

    四、配置拆弹环境

    4.1 安装 VMWare

    下面的链接共有 3 个不同版本的 VMWare,根据自己的操作系统选择合适的版本:

    链接:https://pan.baidu.com/s/1f6MXhyiMqGYpezjZQhSeAA

    提取码:bdo6

    PS:V16.0.0 只支持 Win10 64 位;Win7 或配置较低的电脑,建议用 v12.5.9 老版本;XP 或 32 位系统只能用 v10.0.7 经典版。

    (注意: Win 11 请安装最新版,到官网下载,激活方法网上找激活码啥的,自己查阅资料)

    4.2 安装 Linux 虚拟机

    (1)下载 Linux 镜像文件,一定要确保是 32 位 Linux。Linux 推荐使用 Ubuntu 16.04 或 14.04 版本。官方下载地址:

    16.04.6:Ubuntu 16.04.7 LTS (Xenial Xerus)

    14.04.6:Ubuntu 14.04.6 LTS (Trusty Tahr)

    注:Linux iso文件名上带有 i386 字样的为 32 位 Linux,带有 amd64 字样的为 64 位 Linux。

    (2)在 VMware 下,点击 文件-新建虚拟机,安装 Linux 虚拟机,各种设置按默认即可。

    确保已安装 VMWare Tools,否则无法设置共享文件夹。如果已安装 VMWare Tools,则不需要重新安装。安装方法参照《实验室电脑配置说明》。如果 VMWare 下的 虚拟机-安装 VMWare Tools 按钮是灰色的,导致无法安装,则转第 3 条进行处理;VMWware Tools 安装完成之后,转第5条。

    4.3 VMWare Tools 按钮灰显

    (1)点击虚拟机-设置-CD/DVD(SATA),选中“使用ISO影像文件”,同时选中“已连接”和“启动时连接”。

    (2)点击浏览,选择 VMWare 安装目录下的 linux.iso 文件,如:C:\Program Files (x86)\VMware\ VMware Workstation\linux.iso,点击打开,如下图所示:

    (3)此时应自动展开 VMWare Tools 虚拟光驱目录,或出现下面的 DVD 图标,用鼠标点击该图标。就可以安装 VMWare Tools 了,具体的安装步骤请自己查阅资料(限于篇幅)。

    (4)此时应自动展开 VMWare Tools 虚拟光驱目录,或出现下面的 DVD 图标,用鼠标点击该图标。就可以安装 VMWare Tools 了。

    4.4 设置共享文件夹的方法

    在 VMware 下,点击 虚拟机-设置-选项-共享文件夹-总是启用-添加-浏览-选择 D:\share(在 Windows 下提前建好此文件夹)-下一步-完成。

    如果共享文件夹设置好之后,在/mnt/hgfs目录下没有 share 目录,则重新安装 VMWare Tools,一般可解决问题。

    在做实验二时,要登录 ***,能够访问学校内网,才能连接服务器。如果登录了 ***,仍然不能连接服务器,可以在获得拆弹密码之后,用其他同学的电脑把结果提交到服务器。(我们作业是在线版本的,如果个人完成,则不需要配置网络)

    在做实验二时,要确保自己的电脑主机已联网,并将虚拟机设置为“NAT模式”进行联网,否则可能导致无法连接服务器。设置方法:在 VMWare 的 虚拟机-设置-网络适配器 中,选择“NAT模式”,如下图所示。

    4.5 独立下载炸弹

    如果读者不是通过学校课程作业获取到的炸弹文件,那么请按照以下教程自行下载离线版炸弹文件。

    使用 wget 下载炸弹文件,需要先 cd 到你想存放炸弹的文件夹,然后使用下面的指令下载文件

    wget csapp.cs.cmu.edu/3e/bomb.tar

    通过下面的指令可以解压文件

    tar xvf bomb.tar

    这会生成一个 bomb 目录,包含三个文件:bomb, bomb.c, README

    五、了解 GDB 常用指令

    5.1 变量

    查看变量信息:

    (gdb) p

    5.2 寄存器

    对于调试来说寄存器中的值也很重要,可以查看到当前正在执行的指令的地址等。具体操作如下:

    (gdb) info reg:显示所有寄存器。可以简写为:i r。

    如果要查看具体的寄存器可以这样:i $ebx

    (gdb) p $eax:显示eax寄存器内容

    (gdb) p/c $eax:用字符显示 eax 寄存器内容

    反斜杠后面的是显示格式,可使用的格式见下表:该表在显示内存内容的 x 命令中也是通用的

    格式说明
    x显示为十六进制数
    d显示为十进制数
    u显示为无符号十六进制数
    o显示为八进制数
    t显示为二进制数
    a显示为地址
    c显示为字符(ASCII)
    f显示为浮点数
    s显示为字符串
    i显示为汇编代码(仅在显示内存的x命令中可用)

    5.3 栈

    查看栈信息:

    (gdb) bt:显示所有栈帧

    (gdb) bt 10:显示前面10个栈帧

    (gdb) bt -10:显示后面10个栈帧

    (gdb) bt full:显示栈帧以及局部变量

    (gdb) bt full 10:显示前面10个栈帧以及局部变量

    (gdb) bt full -10:显示后面10个栈帧以及局部变量

    (gdb) frame :进入指定的栈帧中,然后可以查看当前栈帧中的局部变量,以及栈帧内容等信息

    (gdb) info frame :可以查看指定栈帧的详细信息

    (gdb) up:进入上层栈帧

    (gdb) down:进入下层栈帧

    5.4 内存

    可以查看具体内存地址中的内容,比如:目前执行的汇编指令,以及栈中的内容等。

    (gdb) x $pc:显示程序指针指向位置的内容

    (gdb) x/i $pc:显示程序当前位置的汇编指令

    (gdb) x/10i $pc:显示程序当前位置开始往后的10条汇编指令

    5.5 设置断点

    (gdb) break :对当前正在执行的文件中的指定函数设置断点。可简写为:(gdb) b

    (gdb) break :对当前正在执行的文件中的特定行设置断点。可简写为:(gdb) b

    (gdb) break :对指定文件的指定行设置断点。最常用的设置断点方式。可简写为:(gdb) b

    (gdb) break :对指定文件的指定函数设置断点。C++类中的方法似乎不好使。可简写为:(gdb) b

    (gdb) break :当前指令行+/-偏移量出设置断点。可简写为:b

    (gdb) break :指定地址处设置断点。可简写为:b

    5.6 查看/删除断点

    (gdb) info break:显示所有断点以及监视点。可简写为:(gdb) i b

    (gdb) delete :删除编号指向的断电或者监视点。可简写为:(gdb) d

    (gdb) clear :删除该行的断点

    (gdb) clear :删除该行的断点

    5.7 设置无效、有效断点

    (gdb) disble :当前断点设置为无效

    (gdb) enable:当前断点设置为有效

    5.8 监视点

    可以监视某个变量,在变量被访问或者被修改时程序会在当前点进入断点。删除,查看监视点的方式与断点相同。设置监视点方式如下:

    (gdb) watch :表达式发生变化时暂停

    (gdb) awatch :表达式访问或者改变时暂停

    (gdb) rwatch :表达式被访问时暂停

    5.9 条件断点

    在调试程序过程中,有时候我们只想在某个条件下停止程序,然后进行单步调试,而条件断点就是为此而设计。下面是条件断点的操作方式:

    (gdb) b if  : 例如:b main.cpp:8 if x=10 && y=10

    (gdb) condition :删除该断点的条件。

    (gdb) condition :修改断点条件。例如:condition 1 x=10 && y=10

    6.0 断点命令

    每次断点发生时候,想要查看的变量很多时,如果每个变量都手动 print 需要浪费很多时间。断点命令可以在断点发生时批量执行GDB命令。下面是断点命令的设置方式:

    (gdb) commands

    (gdb) >print x

    (gdb) >print y

    (gdb) >end

    首先输入 GDB 命令 commands 然后回车,这时候会出现> 提示符。出现> 提示符后可以输入断点发生时需要执行的GDB命令,每行一条,全部输入完成后输入end结束断点命令。

    6.1 设置变量值

    对变量的值进行控制,可以更快的调试自己的程序。下面就是设置变量值的方法:

    (gdb) set variable = :将变量的值设定为指定表达式的值。例如 set variable x=10

    六、分析各个关卡

    前置:

    首先我们注意到我们的 jar 解压后一共有三个文件,一个是 bomb.c 还有一个就是炸弹程序 bomb,以及不是很重要的 README 文件。 bomb.c 源码文件是 main 函数的源代码,这有助于我们调试,但是我想先对里面的一些英文注释做翻译:

    /********************************85/2000000000000000000*******************************************
     * 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.
     * // 中文版:
     * 邪恶博士的阴险炸弹,1.1 版
     * 版权所有 2011,邪恶集结者博士。保留所有权利。
     * 许可证:
     * 邪恶博士公司(犯罪者)特此明确允许您(受害者)使用此炸弹。
     * 这是一个有时间限制的许可证,在受害者死亡时到期。犯罪者对受害者的伤害
     * 、沮丧、精神错乱、虫眼、腕管综合征、睡眠不足或其他伤害不承担任何责任。
     * 除非犯罪者想获得荣誉,否则受害者不得将此炸弹源代码分发给犯罪者的任何敌人。
     * 任何受害者都不得调试、反向工程、运行“字符串”、反编译、解密或使用
     * 任何其他技术来获取和拆除炸弹。处理此程序时可能不穿防炸弹的衣服。
     * 犯罪者不会为犯罪者缺乏幽默感而道歉。
     * 在法律禁止使用炸弹的情况下,此许可证无效。
     ***************************************************************************/
    #include 
    #include 
    #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 , the bomb reads from  
         * until EOF, and then switches to standard input. Thus, as you 
         * defuse each phase, you can add its defusing string to  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. */
    	// 你不能用一个以上的命令行参数调用炸弹。只能最多指定一个文件路径作为参数
        else {
    	printf("Usage: %s []\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! */
    	// 六个阶段必须比一个阶段更安全!(暗示至少有6个阶段)
        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. */
    	/* Drat!他们想通了!
           让我知道他们是怎么做到的。*/
        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;
    }
    

    注释几乎暗含了每个关卡的方向。下面就是破解和分析时间了(以下内容是干货(错误)比较多的,内容这是从我实验报告上几乎原封不动复制过来的,高手勿喷。准备寒假完善隐藏关卡的解释,并写一个单纯的攻略操作版本,有空再研究一下 x64 的炸弹)

    6.1 phase_1

    6.1.1 相关汇编代码

     08048b33 :

     8048b33: 83 ec 14                         sub    $0x14,%esp

     8048b36: 68 84 a1 04 08               push   $0x804a184

     8048b3b: ff 74 24 1c                      pushl  0x1c(%esp)

     8048b3f:  e8 ba 04 00 00               call   8048ffe

     8048b44: 83 c4 10                         add    $0x10,%esp

     8048b47: 85 c0                              test   %eax,%eax

     8048b49: 74 05                              je     8048b50

     8048b4b: e8 eb 06 00 00               call   804923b

     8048b50: 83 c4 0c                         add    $0xc,%esp

     8048b53: c3                                   ret   

    6.1.2 破解过程与结果

    在 函数中:

    1.

    sub  $0x14,%esp

    将栈顶指针减去0x14字节来为局部变量和临时数据腾出空间。

    2.

    push  $0x804a184

    将绝对地址 0x804a184 处获数据并压栈,根据上下文猜测是在准备下面 strings_not_equal 函数的参数。

    3.

    pushl  0x1c(%esp)

    从栈上偏移量 0x1c 处获取一个32位值,然后将其压入栈中。

    4.

    call  8048ffe

    调用 strings_not_equal 函数来解析输入数据。

    再看 main 函数中调用 phase_1 函数的上下文:

     8048a79: e8 35 08 00 00              call   80492b3

     8048a7e:  89 04 24                       mov  %eax,(%esp)

     8048a81:  e8 ad 00 00 00             call   8048b33

     8048a86:  e8 21 09 00 00             call   80493ac

    这里 ` mov  %eax,(%esp)` 将寄存器 %eax 的值移动(存储)到栈顶 (sp)。也就是读入了一行字符串,然后将其首地址从 eax(read_line返回值)存储到 esp 。

    于是,我们可以知道 地址 0x804a184 处的字符串用于与输入字符串进行比较,继续分析phase_1的下文:

    5.

    test  %eax,%eax

    这条指令测试 %eax 寄存器中的值。它将 %eax 与自身进行按位逻辑与(AND)运算,并根据结果设置标志寄存器(FLAGS)。

    6.

    je  8048b50

    如果上一条指令产生的结果为零(也就是 %eax 的值为零),则跳转到地址 0x8048b50 处执行,否则继续执行下一条指令。而下一条指令是 `call 804923b ` 该函数使得炸弹爆炸,拆弹失败。所以当两条字符串相等时,strings_not_equal 返回值是 0,炸弹拆除成功;否则为非零值,炸弹拆除失败。

    于是,只需利用 gdb 的 x/s 指令查看 0x804a184 位置对应内存存的字符串即可:

    也可以使用 readelf 的readelf -x .rodata bomb 列出整个 rodata 节的数据:

    重新运行一个实例,并输入 I was trying to give Tina Fey more material. 可以发现成功通关:

    6.2 phase_2

    6.2.1 相关汇编代码

     08048b54 :

     8048b54: 53                                  push   %ebx

     8048b55: 83 ec 30                        sub    $0x30,%esp

     8048b58: 65 a1 14 00 00 00         mov    %gs:0x14,%eax

     8048b5e:  89 44 24 24                  mov    %eax,0x24(%esp)

     8048b62: 31 c0                             xor    %eax,%eax

     8048b64: 8d 44 24 0c                   lea    0xc(%esp),%eax

     8048b68: 50                                  push   %eax

     8048b69: ff 74 24 3c                     pushl  0x3c(%esp)

     8048b6d: e8 06 07 00 00              call   8049278

     8048b72: 83 c4 10                        add    $0x10,%esp

     8048b75: 83 7c 24 04 00              cmpl   $0x0,0x4(%esp)

     8048b7a:  79 05                            jns    8048b81

     8048b7c:  e8 ba 06 00 00             call   804923b

     8048b81: bb 01 00 00 00              mov    $0x1,%ebx

     8048b86: 89 d8                             mov    %ebx,%eax

     8048b88: 03 04 9c                        add    (%esp,%ebx,4),%eax

     8048b8b: 39 44 9c 04                   cmp    %eax,0x4(%esp,%ebx,4)

     8048b8f:  74 05                             je     8048b96

     8048b91: e8 a5 06 00 00              call   804923b

     8048b96: 83 c3 01                        add    $0x1,%ebx

     8048b99: 83 fb 06                         cmp    $0x6,%ebx

     8048b9c:  75 e8                            jne    8048b86

     8048b9e:  8b 44 24 1c                  mov    0x1c(%esp),%eax

     8048ba2:  65 33 05 14 00 00 00       xor    %gs:0x14,%eax

     8048ba9:  74 05                            je     8048bb0

     8048bab:  e8 e0 fb ff ff                  call   8048790

     8048bb0: 83 c4 28                        add    $0x28,%esp

     8048bb3: 5b                                  pop    %ebx

     8048bb4: c3                                  ret   

     08049278 :

     8049278: 83 ec 0c                        sub    $0xc,%esp

     804927b: 8b 44 24 14                   mov    0x14(%esp),%eax

     804927f:  8d 50 14                        lea    0x14(%eax),%edx

     8049282: 52                                  push   %edx

     8049283: 8d 50 10                        lea    0x10(%eax),%edx

     8049286: 52                                  push   %edx

     8049287: 8d 50 0c                        lea    0xc(%eax),%edx

     804928a:  52                                 push   %edx

     804928b: 8d 50 08                        lea    0x8(%eax),%edx

     804928e:  52                                 push   %edx

     804928f:  8d 50 04                        lea    0x4(%eax),%edx

     8049292: 52                                  push   %edx

     8049293: 50                                  push   %eax

     8049294: 68 05 a4 04 08              push   $0x804a405

     8049299: ff 74 24 2c                     pushl  0x2c(%esp)

     804929d: e8 6e f5 ff ff                   call   8048810

     80492a2:  83 c4 20                       add    $0x20,%esp

     80492a5:  83 f8 05                        cmp    $0x5,%eax

     80492a8:  7f 05                             jg     80492af

     80492aa:  e8 8c ff ff ff                   call   804923b

     80492af:  83 c4 0c                        add    $0xc,%esp

     80492b2: c3                                  ret   

    6.2.2 破解过程与结果

    【逐行分析】

    1.

    push %ebx

    将 %ebx 寄存器的值推入栈中,保存现场, esp1 = esp0 - 4。

    2.

    sub $0x30,%esp

    在栈上为局部变量和缓冲区开辟空间。 esp2 = esp1 – 48 = esp0 - 52

    3.  

      mov    %gs:0x14,%eax

      mov    %eax,0x24(%esp).

    获取操作系统提供的安全机制(通常称为"canary")并放置到栈上以防止缓冲区溢出攻击。根据上下文计算esp0 - 4 - 0x30(48) + 0x24(36) = esp0 – 16,是将gs:0x14对应地址放到了esp0 - 16 处。

    4.

    xor    %eax,%eax

    清空 eax 寄存器的值。

    5.

    lea    0xc(%esp),%eax

    push   %eax

    将 %eax 设置为栈顶地址加上偏移量 12(即 %esp + 0xC 的地址),即esp0 – 40 处作的值压栈,先将将栈顶指针 – 4,即esp3 = esp0 – 56, 然后将值压入到新的栈顶esp0-56处作为后续函数调用的参数。

    6.

    pushl  0x3c(%esp)

    将当前 esp 加上偏移量 0x3c 即 esp0 – 56 + 60 = esp0 + 4 处的值,作为调用 read_six_numbers 函数的参数,由于调用了 push, 这个参数是放在现在的esp4 = esp0 – 60 处。

    7.

    call   8049278

    将前面计算得到的esp0-52处参数2(数组首地址)和esp0 + 4 的栈上数据(源字符串首地址)作为参数1,用这两个参数来调用函数 read_six_numbers。


    重难点:观察 mian 函数调用 phase_2 的上下文:

     8048a97: e8 17 08 00 00             call   80492b3

     8048a9c:  89 04 24                      mov    %eax,(%esp)

     8048a9f:  e8 b0 00 00 00             call   8048b54

     8048aa4:  e8 03 09 00 00            call   80493ac

    在调用 phase_2 函数之前,先调用了 read_line 函数,字面理解就是读取了一行输入,返回值是字符串首地址,存放在 eax 寄存器中,然后调用 mov    %eax,(%esp) 将 eax 中的地址放到当前的栈顶,即esp 处。可以发现 传递给 phase_2 的参数在传递给 read_six_numbers 时,上面分析的是 esp0 + 4 处的值,为什么两者看似“不等”呢?

    【分析】

    因为调用 call phase_2指令时,call 指令实际上做了两件事:将PC+偏移量计算得到的地址压入栈顶,并将 esp – 4, 所以我们进入 phase_2 的栈帧时 esp0 = esp – 4。

    综上,read_six_numbers 的第一个参数其实就是 main 函数传递给 phase_2 的唯一参数,而不是其他偏移处的值。


    8.

    add    $0x10,%esp

    恢复堆栈指针,这里是恢复调用 read_six_numbers 前为其分配参数所占用的空间,将栈帧移动到数组第一个元素地址(esp0-40)的顶部,也就是说,现在的esp5 = esp4 + 16 = esp0 - 44。

    9.

      cmpl   $0x0,0x4(%esp)

      jns    8048b81

    0x4(%esp),即 esp5 + 4 = esp0 – 44 + 4 = esp0 - 40,根据前面分析,这个值是read_six_numbers参数格式化的第一个数的地址,即6个数的数组的首地址。这两条指令检查第一个输入数值是否大于等于零。如果是负数,则会向下执行,引爆炸弹。

    【小结】:需要确保第一个输入数字不为负数。可以通过输入非负整数来绕过这个检查。

    10.

    call   804923b

    此行代码对应一种异常情况,即第一个输入数字为负数,会导致炸弹爆炸。

    11.

    mov    $0x1,%ebx

    将寄存器 %ebx 设置为 1,用作循环计数器,即for循环的i 。

    11.

    mov    %ebx,%ea

    将 %ebx 的值移动到 %eax 寄存器中,即 %eax = %ebx = 1,用于后续计算。

    12.

    add    (%esp,%ebx,4),%eax

    将栈上地址为 (%esp + 4 * %ebx) 处的数值加到 %eax 中。这是一个累加操作, 而eax 用于存放ebx(索引 i)和a[i-1]的和,第一轮是eax = i = 1,经过这个操作后理解为 eax = 1 + a[i - 1]。由于esp5 + 4 = esp0 - 40 是数组的首地址,ebx 作为 i 并初始化为1, (%esp,%ebx,4)就是指 %esp + 4 + (%ebx - 1) * 4,在遍历过程中可以理解为这行指令是依次取数组中元素的意思,即 a[i - 1]且 i=1:n。现在 eax 中的值就是取出的 a[i-1]。

    13.

    cmp    %eax,0x4(%esp,%ebx,4)

    将 %eax 和栈上另一个地址 (%esp + 4 * %ebx + 4) 处的数值比较, (%esp + 4 * %ebx + 4)比(%esp,%ebx,4)多加一个4,所以对于每一次循环,这个值代表 a[i],这行指令意思是把eax和a[i]进行比较。

    14.

    je     8048b96

    如果相等,则跳转到 8048b96 处执行下一条指令。而8048b96 处的指令为 add    $0x1,%ebx,将 %ebx 加1,增加循环计数器的值。

    15.

    call   804923b

    此行代码对应一种异常情况,即累加结果与所比较的数不相等,会引爆炸弹。

    16.

       cmp    $0x6,%ebx

       jne    8048b86

    如果 %ebx 不等于 6,则跳转到 8048b86 处,我们再看一下调转的位置:

     8048b86: mov    %ebx,%eax

     8048b88: add    (%esp,%ebx,4),%eax

     8048b8b: cmp    %eax,0x4(%esp,%ebx,4)

    可以看出,这里是将之前累加过1的新的ebx 赋值给eax,再看eax 其实每次循环 eax 都会变成当前的索引 i 所以,我们进一步理解了此处的循环到底在做什么,即:每次循环比较 a[i] 和 a[i-1] + i 是否相等,如果不等,就会爆炸。

    17.

    mov    0x1c(%esp),%eax

    将栈上地址为 (%esp + 0x1C) = esp5 + 28 = esp0 – 44 + 28 = esp0 - 16 的值移动到 %eax 中根据上面的分析,这里存储的是 gs:14 生成的校验码,用于下一步的检测缓冲区溢出的操作。(如果数组 a 没有发生栈溢出,则不会覆盖esp0 -16,这个位于它上方的值,如果溢出,则会覆盖掉这个值,导致下一步异或失败)。

    18.

    xor    %gs:0x14,%eax

    通过与内存中的gs:0x14值异或操作,检查是否有缓冲区溢出。本行代码是对安全机制(canary)进行验证,略过它即可继续。

    19.

    je     8048bb0

    如果没有缓冲区溢出,则跳转至 8048bb0 进行返回前的堆栈平衡处理。

    否则,发生溢出时,会继续向下执行 call   8048790 ,此行代码对应一种异常情况,即发现了堆栈错误,程序崩溃退出。

    19.

    8048bb0: add    $0x28,%esp

    恢复堆栈指针到,esp0 – 16即堆栈检测的校验码处。

    20.

    pop    %ebx

    从栈中弹出phase_2为调用者(main)维护的 %ebx 值,恢复寄存器状态。

    21.

    ret

    返回 %eax 最后的值到调用者,也就是堆栈检测异或的结果。

    根据以上分析可以尝试画出 phase_2 函数返回前的栈帧结构图:

    (P.S. 由于当时实验时候时间紧的原因,栈帧理解有一个地方出错的,就是 call 压入的是返回地址,用于回到调用者,图中文本错了)

    综上所述,本关卡的破解建议是输入六个非负整数,以满足每次循环 a[i-1] + 1与a[i]相等(i从1开始)。如果在执行期间触发异常条件,则会引爆炸弹,导致游戏失败。

    所以,如果以 a[0] = 1,则后面的数为:2 4 7 11 16。

    将结果保存至 anx.txt 并作为运行参数,通关结果如下:

    6.3 phase_3

    (1)相关汇编代码

     08048bb5 :

     8048bb5: 83 ec 1c                             sub    $0x1c,%esp

     8048bb8: 65 a1 14 00 00 00              mov    %gs:0x14,%eax

     8048bbe:  89 44 24 0c                       mov    %eax,0xc(%esp)

     8048bc2:  31 c0                                 xor    %eax,%eax

     8048bc4:  8d 44 24 08                       lea    0x8(%esp),%eax

     8048bc8:  50                                      push   %eax

     8048bc9:  8d 44 24 08                       lea    0x8(%esp),%eax

     8048bcd:  50                                      push   %eax

     8048bce:  68 11 a4 04 08                  push   $0x804a411

     8048bd3: ff 74 24 2c                          pushl  0x2c(%esp)

     8048bd7: e8 34 fc ff ff                        call   8048810

     8048bdc:  83 c4 10                            add    $0x10,%esp

     8048bdf:  83 f8 01                              cmp    $0x1,%eax

     8048be2:  7f 05                                  jg     8048be9

     8048be4:  e8 52 06 00 00                  call   804923b

     8048be9:  83 7c 24 04 07                  cmpl   $0x7,0x4(%esp)

     8048bee:  77 3c                                 ja     8048c2c

     8048bf0:  8b 44 24 04                        mov    0x4(%esp),%eax

     8048bf4:  ff 24 85 e0 a1 04 08           jmp    *0x804a1e0(,%eax,4)

     8048bfb:  b8 c0 03 00 00                   mov    $0x3c0,%eax

     8048c00:  eb 3b                                 jmp    8048c3d

     8048c02:  b8 c6 02 00 00                  mov    $0x2c6,%eax

     8048c07:  eb 34                                 jmp    8048c3d

     8048c09:  b8 fd 02 00 00                   mov    $0x2fd,%eax

     8048c0e:  eb 2d                                 jmp    8048c3d

     8048c10:  b8 c4 00 00 00                  mov    $0xc4,%eax

     8048c15:  eb 26                                 jmp    8048c3d

     8048c17:  b8 a6 02 00 00                  mov    $0x2a6,%eax

     8048c1c:  eb 1f                                  jmp    8048c3d

     8048c1e:  b8 f9 02 00 00                   mov    $0x2f9,%eax

     8048c23:  eb 18                                 jmp    8048c3d

     8048c25:  b8 81 03 00 00                  mov    $0x381,%eax

     8048c2a:  eb 11                                 jmp    8048c3d

     8048c2c:  e8 0a 06 00 00                  call   804923b

     8048c31:  b8 00 00 00 00                  mov    $0x0,%eax

     8048c36:  eb 05                                 jmp    8048c3d

     8048c38:  b8 a1 00 00 00                  mov    $0xa1,%eax

     8048c3d:  3b 44 24 08                       cmp    0x8(%esp),%eax

     8048c41:  74 05                                 je     8048c48

     8048c43:  e8 f3 05 00 00                   call   804923b

     8048c48:  8b 44 24 0c                       mov    0xc(%esp),%eax

     8048c4c:  65 33 05 14 00 00 00        xor    %gs:0x14,%eax

     8048c53:  74 05                                 je     8048c5a

     8048c55:  e8 36 fb ff ff                       call   8048790

     8048c5a:  83 c4 1c                            add    $0x1c,%esp

     8048c5d:  c3                                      ret   

    (2)破解过程与结果

    1.

    sub $0x1c,%esp

    栈顶指针减去 28 个字节,为局部变量和临时数据腾出空间。

    2.

    mov %gs:0x14,%eax

    调用了内核函数,这是一种栈溢出保护手段。

    3.

    mov %eax,0xc(%esp)

    保存 eax 旧的值,便于调用返回时恢复现场;使用 mov 指令从 %gs:0x14 处加载一个值到 %eax 寄存器中,并通过 mov 指令将其拷贝到 %esp + 0xc 的位置。

    4.

    xor %eax,%eax

    将 %eax 寄存器与自身进行异或操作,相当于将其清零。

    5.

    准备 sscanf 函数的参数:

    lea 0x8(%esp),%eax

    将倒数第一个参数传递给 eax 寄存器。(格式化的两个数中的第二个数)

     push   %eax

    将%eax寄存器的值压入栈中。

     lea    0x8(%esp),%eax

     push   %eax

    重复两条指令,将倒数第二个参数压入栈中。(第一个数)

     push   $0x804a411

    压入绝对地址的字符串,gdb x/s -> "%d %d"; 进一步印证了sscanf 格式化输入两个整数

    pushl  0x2c(%esp)

    局部变量(位于 phase_3 入口时的 esp + 0xC 处),这个是 sscanf 的第一个参数,也就是源字符串的首地址,是 main函数调用 phase_3 时传入的参数。

    6.

    call   8048810

    调用 sscanf 格式化输入多参数。

    所以,整理一下就是:

    int ret = sscanf(p1, "%d %d", x , y);

    7.

    add    $0x10,%esp

    这里实现了esp + 16。在函数调用结束时,需要恢复栈的原始状态,以确保函数参数和局部变量在栈上的正确分配与释放。从栈上移除了刚刚传递给__isoc99_sscanf 函数的参数所占据的空间。这些参数可能被压入栈上作为函数调用的参数列表,并且不再需要它们,因此使用这个指令清理栈空间。push 了4次,刚好16字节。

    8.

    cmp    $0x1,%eax

    sscanf 函数返回值和立即数 1 进行比较,sscanf 返回值表示实际格式化成功的参数个数。

     jg     8048be9    ; %eax 大于 1,跳转到0x8048be9处执行,如果 %eax 小于等于 1,程序向下执行直接爆炸。结合上下文,意思是输入整数个数必须大于等于2个。

    整理一下就是:

    if ret > 1:
        goto 0x8048be9;
    else  explode_bomb();

    9.

    8048be9: cmpl   $0x7,0x4(%esp)

    8048bee:  ja     8048c2c

    8048bf0:  mov    0x4(%esp),%eax

    8048bf4:  jmp    *0x804a1e0(,%eax,4)

    这部分代码比较了位于栈上偏移esp + 4的值与7的大小关系,即判断第一个输入的数和7的大小关系,如果输入的数大于 7或者小于0,则跳转到地址8048c2c,引爆炸弹。注意点:为什么小于 0 的也会引爆?因为 jg是无符号运算大于的时候会跳转,负数采用补码参与运算时,始终比无符号的7要大,这就会导致直接跳转到爆炸处。

    8048c2c:    call   804923b

    否则,将输入的第一个数复制到寄存器 %eax中,并通过间接跳转指令jmp *0x804a1e0(,%eax,4)进行跳转,跳转的位置取决于输入的参数1即现在的 eax 的值,0x804a1e0是一个存放地址值的数组的首地址。

    这里我们需要借助工具知道该标签数组存放的是什么数据:

    方法一:gdb:x/12w 0x804a1e0

    方法二:readelf -x .rodata bomb,并定位0x804a1e0处:

    明显,这是一个利用数组存放跳转地址的代码,结合课本已经学过的知识,不难猜测这是一个 switch-case 的“跳转表”,即根据变量(第一个输入的整数)的具体值,到数组中取得地址,然后进行相应的跳转。下面我们理一下跳转的具体过程:

    输入的第一个数的数值

    跳转后第一个指令

    eax最终的值(十进制)

    0

    mov $0xa1,%eax

    161

    1

    mov $0x3c0,%eax

    960

    2

    mov $0x2c6,%eax

    710

    3

    mov $0x2fd,%eax

    765

    4

    mov $0xc4,%eax

    196

    5

    mov $0x2a6,%eax

    678

    6

    mov $0x2f9,%eax

    761

    7

    mov $0x381,%eax

    897

    > 7或者

    call explode_bomb

    爆炸

    然后我们再看一下这段代码的上下文:

    明显看出这里的跳转表都是执行了 mov 指令将 eax 的值改变掉,然后都跳转到0x8048c3d 处,即下一条指令是 cmp 0x8(%esp),%eax 指令,我们知道 esp + 4 是用户输入的第一个整数,那么esp + 8 则是用户输入的第二个数,这里比较了eax 转换后的值和第二个数是否相等,比较的结果会改变标志寄存器(FLAGS),具体的操作需要看下面的条件跳转指令。

    10.

    je 8048c48

    条件跳转指令,如果相等则跳转到0x8048c48 处,如果不相等,就会继续执行,然后执行 explode_bomb,引发爆炸。

    11.

    8048c48: mov    0xc(%esp),%eax

    8048c4c:  xor    %gs:0x14,%eax

    8048c53:  je     8048c5a

    8048c55:  call   8048790

    8048c5a:  add    $0x1c,%esp

    8048c5d:  ret

    以上代码就是之前条件跳转的地址,这里负责检查缓冲区是否溢出,并进行堆栈平衡,最后 ret 返回函数。

    综上,要想通过本关卡,我们需要输入两个整数,第一个数在 0~7之间,然后第二个数必须等于对应的eax 计算得到的值,这一关卡有多组正解,这个在上面的跳转表中已经分析过,这里以“0 161”作为第一种正确的解去尝试,发现成功通关:

    6.4 phase_4

    (1)相关汇编代码

    08048c5e :

     8048c5e:  56                             push   %esi

     8048c5f:  53                              push   %ebx

     8048c60:  83 ec 04                   sub    $0x4,%esp

     8048c63:  8b 54 24 10              mov    0x10(%esp),%edx

     8048c67:  8b 74 24 14              mov    0x14(%esp),%esi

     8048c6b:  8b 4c 24 18              mov    0x18(%esp),%ecx

     8048c6f:  89 c8                         mov    %ecx,%eax

     8048c71:  29 f0                         sub    %esi,%eax

     8048c73:  89 c3                        mov    %eax,%ebx

     8048c75:  c1 eb 1f                    shr    $0x1f,%ebx

     8048c78:  01 d8                        add    %ebx,%eax

     8048c7a:  d1 f8                         sar    %eax

     8048c7c:  8d 1c 30                    lea    (%eax,%esi,1),%ebx

     8048c7f:  39 d3                         cmp    %edx,%ebx

     8048c81:  7e 15                        jle    8048c98

     8048c83:  83 ec 04                   sub    $0x4,%esp

     8048c86:  8d 43 ff                     lea    -0x1(%ebx),%eax

     8048c89:  50                             push   %eax

     8048c8a:  56                             push   %esi

     8048c8b:  52                             push   %edx

     8048c8c:  e8 cd ff ff ff                call   8048c5e

     8048c91:  83 c4 10                   add    $0x10,%esp

     8048c94:  01 d8                        add    %ebx,%eax

     8048c96:  eb 19                        jmp    8048cb1

     8048c98:  89 d8                        mov    %ebx,%eax

     8048c9a:  39 d3                        cmp    %edx,%ebx

     8048c9c:  7d 13                        jge    8048cb1

     8048c9e:  83 ec 04                   sub    $0x4,%esp

     8048ca1:  51                             push   %ecx

     8048ca2:  8d 43 01                   lea    0x1(%ebx),%eax

     8048ca5:  50                             push   %eax

     8048ca6:  52                             push   %edx

     8048ca7:  e8 b2 ff ff ff               call   8048c5e

     8048cac:  83 c4 10                   add    $0x10,%esp

     8048caf:  01 d8                         add    %ebx,%eax

     8048cb1:  83 c4 04                   add    $0x4,%esp

     8048cb4:  5b                             pop    %ebx

     8048cb5:  5e                             pop    %esi

     8048cb6:  c3                             ret   

    08048cb7 :

     8048cb7:  83 ec 1c                    sub    $0x1c,%esp

     8048cba:  65 a1 14 00 00 00     mov    %gs:0x14,%eax

     8048cc0:  89 44 24 0c               mov    %eax,0xc(%esp)

     8048cc4:  31 c0                         xor    %eax,%eax

     8048cc6:  8d 44 24 08               lea    0x8(%esp),%eax

     8048cca:  50                              push   %eax

     8048ccb:  8d 44 24 08               lea    0x8(%esp),%eax

     8048ccf:  50                               push   %eax

     8048cd0:  68 11 a4 04 08          push   $0x804a411

     8048cd5:  ff 74 24 2c                 pushl  0x2c(%esp)

     8048cd9:  e8 32 fb ff ff               call   8048810

     8048cde:  83 c4 10                    add    $0x10,%esp

     8048ce1:  83 f8 02                     cmp    $0x2,%eax

     8048ce4:  75 07                         jne    8048ced

     8048ce6:  83 7c 24 04 0e          cmpl   $0xe,0x4(%esp)

     8048ceb:  76 05                         jbe    8048cf2

     8048ced:  e8 49 05 00 00          call   804923b

     8048cf2:  83 ec 04                     sub    $0x4,%esp

     8048cf5:  6a 0e                          push   $0xe

     8048cf7:  6a 00                          push   $0x0

     8048cf9:  ff 74 24 10                  pushl  0x10(%esp)

     8048cfd:  e8 5c ff ff ff                 call   8048c5e

     8048d02: 83 c4 10                     add    $0x10,%esp

     8048d05: 83 f8 0a                     cmp    $0xa,%eax

     8048d08: 75 07                         jne    8048d11

     8048d0a:  83 7c 24 08 0a         cmpl   $0xa,0x8(%esp)

     8048d0f:  74 05                         je     8048d16

     8048d11:  e8 25 05 00 00         call   804923b

     8048d16: 8b 44 24 0c               mov    0xc(%esp),%eax

     8048d1a:  65 33 05 14 00 00 00       xor    %gs:0x14,%eax

     8048d21: 74 05                         je     8048d28

     8048d23: e8 68 fa ff ff               call   8048790

     8048d28: 83 c4 1c                    add    $0x1c,%esp

     8048d2b: c3                              ret   

    (2)破解过程与结果

    首先,第四关和第三关输入函数的调用代码类似,简单概括如下图:

    我们接下来分析不一样的地方:

    1.

    8048ce6: cmpl   $0xe,0x4(%esp)

    8048ceb:  jbe    8048cf2

    8048ced: call   804923b

    这三行代码检查了格式化输入后的第一个整数,看第一个整数是否不大于0xe,(jbe是条件跳转,当无符号数整数比较A不大于B 时,发生跳转)。如果大于 0xe(十进制的14)或者为负值,则不跳转,继续执行下一条指令,造成炸弹爆炸。

    所以,以上代码可以理解为如下伪代码:

    unsigned int v1;
    int v2;
    if ( __isoc99_sscanf(p1, "%d %d", &v1, &v2) != 2 || v1 > 14 )
      explode_bomb();// 输入的数不是两个,或第一个数比14大,或第一个数是负数,则都会爆炸。

    尤其要注意无符号数比较的问题,不能为负值,负值就爆炸。

    2.

    8048cf5:  push   $0xe

    8048cf7:  push   $0x0

    8048cf9:  pushl  0x10(%esp)

    8048cfd:  call   8048c5e

    这段代码首先准备了函数调用的参数,一共有三个参数,第一个数是我们输入的第一个数,第二个为数0,第三个为数14。然后我们调用call func4 指令,进入 func4 函数。

    接着,分析 func4 的功能:

    1). 首先这段代码实现了将调用者的 esi 和ebx 压栈,保存现场。然后又预分配了4个字节的空间:

    8048c5e: push   %esi

    8048c5f:  push   %ebx

    8048c60:  sub    $0x4,%esp

    2). 这三条指令是计算了三个局部变量的值:

    8048c63:  mov    0x10(%esp),%edx    ; SrcV:指定的目标数字

    8048c67:  mov    0x14(%esp),%esi      ; 0  -> st:表示搜索范围的起始数字

    8048c6b:  mov    0x18(%esp),%ecx     ; 14 -> ed:表示搜索范围的结尾数字

    3). 这段指令序列实现了用 ebx 保存当前 ed – st 的结果。

    8048c6f:  mov    %ecx,%eax ; 临时拷贝 ed 到 eax 寄存器

    8048c71:  sub     %esi,%eax  ; ed - st,结果保存在 eax

    8048c73:  mov    %eax,%ebx ; 结果转移到 ebx

    4). 这段指令序列实现了将 ebx 中的数值除以2,有符号数不能够整除时向零取整。并将结果继续保存到 ebx 中,所以 %ebx = (ed - st) / 2

    8048c75:  shr    $0x1f,%ebx   ; 逻辑右移 31 位,相当于取符号位

    8048c78:  add    %ebx,%eax  ; 对于被除数为负数且不能被整除的情况做向零取整,需要加上1,正好符号位是1,所以利用直接加符号位,将不影响正数的计算(这是一个技巧)

    8048c7a:  sar    %eax      ; eax 的值算数右移 1 位,相当于有符号数除法,除数为 2.

    8048c7c:  lea    (%eax,%esi,1),%ebx   ; 用 st 加上 st 和 ed 之间距离的一半,计算结果是中位数,放到 ebx 中

    自此,我们可以用伪代码概括一下上面一段指令的功能:

    int mid = (ed - st) / 2 + st;  // 求中位数

    5). 根据上面计算的中位数(mid)和目标数(SrcV)之间的大小关系,对函数自身进行递归调用,通过分析参数的准备过程不难推断出 func4 的算法是二分,即目标数小于中间数的时候,目标数只能在中位数左侧的搜索区间内,相反只能在右侧的搜索区间内,从而缩小搜索范围。

    8048c7f:  cmp    %edx,%ebx ; 要查找的数 SrcV(在 edx 上),和 ebx 也就是中位数比较

    8048c81:  jle    8048c98 ; 如果 中位数 = 目标数,结合前面的条件中位数 > 1; if (mid > x) return mid + func4(x, st, mid - 1); if (mid 14 >> 2 >> 1 >> 10 >> 0 >> 8 >> 4 >> 4 >> 9 >> 13 >> 11 >> 7 >> 3 >> 12 >> 5。所以第一个数是数字 5 。而第二个数则要等于累加和(不包括第一次输入的数),也就是 12 + 3 + 7 + 11 + 13 + 9 + 4 + 8 + 0 + 10 + 1 + 2 + 14 + 6 + 15 = 115 。

    所以本题答案是 “5 115”,到测试环境下验证通过:

    下面为了总结汇编代码的功能,给出还原出来的C语言代码。

    概念验证代码(C 语言):

    #include 
    int array_3250[] = {
    	10, 2, 14, 7, 8, 12, 15, 11,
    	0, 4, 1, 13, 3, 9, 6, 5
    };
    int d_phase_5(int a) {
    	int count = 0, sum = 0;
    	int index = a;
    	do {
    		count++;
    		index = array_3250[index];
    		sum += index;
    	} while ( index != 15 );
    	if ( count != 15)// 判断是否满足返回后不爆炸的条件,如果爆炸,则返回 -1
    		return -1;
    	else
    		return sum;
    }
    int main() {
    	for (int i = 1; i  
    

    编译运行结果如下:

    6.6 phase_6

    (1)相关汇编代码

     08048db9 :

     8048db9: 56                                    push   %esi       ; 准备环节

     8048dba:  53                                   push   %ebx

     8048dbb: 83 ec 4c                           sub    $0x4c,%esp

     8048dbe:  65 a1 14 00 00 00          mov    %gs:0x14,%eax

     8048dc4:  89 44 24 44                    mov    %eax,0x44(%esp)

     8048dc8:  31 c0                              xor    %eax,%eax

     8048dca:  8d 44 24 14                    lea    0x14(%esp),%eax

     8048dce:  50                                   push   %eax

     8048dcf:  ff 74 24 5c                       pushl  0x5c(%esp)

     8048dd3: e8 a0 04 00 00                call   8049278

     8048dd8: 83 c4 10                          add    $0x10,%esp         ; 第一个部分

     8048ddb: be 00 00 00 00                mov    $0x0,%esi

     8048de0:  8b 44 b4 0c                    mov    0xc(%esp,%esi,4),%eax

     8048de4:  83 e8 01                         sub    $0x1,%eax

     8048de7:  83 f8 05                          cmp    $0x5,%eax

     8048dea:  76 05                              jbe    8048df1

     8048dec:  e8 4a 04 00 00               call   804923b

     8048df1:  83 c6 01                          add    $0x1,%esi

     8048df4:  83 fe 06                           cmp    $0x6,%esi

     8048df7:  74 1b                               je     8048e14

     8048df9:  89 f3                                mov    %esi,%ebx

     8048dfb:  8b 44 9c 0c                     mov    0xc(%esp,%ebx,4),%eax

     8048dff:   39 44 b4 08                     cmp    %eax,0x8(%esp,%esi,4)

     8048e03:  75 05                              jne    8048e0a

     8048e05:  e8 31 04 00 00               call   804923b

     8048e0a:  83 c3 01                         add    $0x1,%ebx

     8048e0d:  83 fb 05                          cmp    $0x5,%ebx

     8048e10:  7e e9                              jle    8048dfb

     8048e12:  eb cc                              jmp    8048de0

     8048e14:  8d 44 24 0c                    lea    0xc(%esp),%eax         ; 第二个部分

     8048e18:  8d 5c 24 24                    lea    0x24(%esp),%ebx

     8048e1c:  b9 07 00 00 00               mov    $0x7,%ecx

     8048e21:  89 ca                              mov    %ecx,%edx

     8048e23:  2b 10                              sub    (%eax),%edx

     8048e25:  89 10                              mov    %edx,(%eax)

     8048e27:  83 c0 04                         add    $0x4,%eax

     8048e2a:  39 c3                              cmp    %eax,%ebx

     8048e2c:  75 f3                               jne    8048e21

     8048e2e:  bb 00 00 00 00               mov    $0x0,%ebx         ; 第三个部分

     8048e33:  eb 16                              jmp    8048e4b

     8048e35:  8b 52 08                         mov    0x8(%edx),%edx

     8048e38:  83 c0 01                         add    $0x1,%eax

     8048e3b:  39 c8                              cmp    %ecx,%eax

     8048e3d:  75 f6                               jne    8048e35

     8048e3f:  89 54 b4 24                     mov    %edx,0x24(%esp,%esi,4)

     8048e43:  83 c3 01                         add    $0x1,%ebx

     8048e46:  83 fb 06                          cmp    $0x6,%ebx

     8048e49:  74 17                              je     8048e62

     8048e4b:  89 de                              mov    %ebx,%esi

     8048e4d:  8b 4c 9c 0c                    mov    0xc(%esp,%ebx,4),%ecx

     8048e51:  b8 01 00 00 00              mov    $0x1,%eax

     8048e56:  ba 54 d1 04 08              mov    $0x804d154,%edx ; 结点的首地址

     8048e5b:  83 f9 01                         cmp    $0x1,%ecx

     8048e5e:  7f d5                              jg     8048e35

     8048e60:  eb dd                             jmp    8048e3f

     8048e62:  8b 5c 24 24                   mov    0x24(%esp),%ebx ; 第四个部分

     8048e66:  8d 44 24 24                   lea    0x24(%esp),%eax

     8048e6a:  8d 74 24 38                   lea    0x38(%esp),%esi

     8048e6e:  89 d9                             mov    %ebx,%ecx

     8048e70:  8b 50 04                        mov    0x4(%eax),%edx

     8048e73:  89 51 08                        mov    %edx,0x8(%ecx)

     8048e76:  83 c0 04                        add    $0x4,%eax

     8048e79:  89 d1                             mov    %edx,%ecx

     8048e7b:  39 c6                             cmp    %eax,%esi

     8048e7d:  75 f1                              jne    8048e70

     8048e7f:  c7 42 08 00 00 00 00     movl   $0x0,0x8(%edx)

     8048e86:  be 05 00 00 00              mov    $0x5,%esi      ; 最后一个部分

     8048e8b:  8b 43 08                        mov    0x8(%ebx),%eax

     8048e8e:  8b 00                             mov    (%eax),%eax

     8048e90:  39 03                             cmp    %eax,(%ebx)

     8048e92:  7d 05                             jge    8048e99

     8048e94:  e8 a2 03 00 00              call   804923b

     8048e99:  8b 5b 08                        mov    0x8(%ebx),%ebx

     8048e9c:  83 ee 01                         sub    $0x1,%esi

     8048e9f:  75 ea                               jne    8048e8b

     8048ea1:  8b 44 24 3c                    mov    0x3c(%esp),%eax

     8048ea5:  65 33 05 14 00 00 00     xor    %gs:0x14,%eax

     8048eac:  74 05                              je     8048eb3

     8048eae:  e8 dd f8 ff ff                    call   8048790

     8048eb3:  83 c4 44                         add    $0x44,%esp

     8048eb6:  5b                                   pop    %ebx

     8048eb7:  5e                                   pop    %esi

     8048eb8:  c3                                   ret   

    (2)破解过程与结果

    1.下面这段指令序列开辟栈空间并为堆栈检测准备数据(和前面第二关卡类似):

     8048db9:  push     %esi

     8048dba:  push     %ebx

     8048dbb:  sub       $0x4c,%esp

     8048dbe:  mov      %gs:0x14,%eax

     8048dc4:  mov      %eax,0x44(%esp)

     8048dc8:  xor        %eax,%eax

    2.这段指令序列实现读入六个数字

     8048dca:   lea        0x14(%esp),%eax

     8048dce:   push     %eax         ; 数组的首地址

     8048dcf:    pushl    0x5c(%esp)    ; 输入字符串的首地址

     8048dd3:   call       8049278

     8048dd8:   add       $0x10,%esp   ; 恢复栈指针位置

    我们也可以通过 gdb 来分析 read_six_numbers 具体做了什么(这种方法是我们前面没详细提过的):

    首先在终端中使用 gdb bomb 启动调试实例:

    然后依次打下几个关键断点:

    b phase_6                            # 在本关卡入口断下

    b read_six_numbers            # 断下读入数字函数(可选)

    b __isoc99_sscanf@plt       # 在 sscanf 函数处断下(用于知道输入格式)

    b explode_bomb                  # 防止炸弹爆炸

    然后,填入前几关的Code 到文本文件,并使用 r 指令运行炸弹程序。

    由于每一关都会调用 sscanf 函数,程序运行到断点处就会被断下,所以需要手动输入 continue 继续运行,直到第六关开始,我们首先输入一串数字,并在随后触发断点sscanf ,此时就不需要继续过断点了。而是使用stepi 单步步入指令,可以让我们知道 sscanf 的参数:

    显然,sscanf 函数读取了6个整形数字,即 int 类型的数据,这和前面第二关静态分析的结果一致。

    3. 这段指令序列首先利用 esi 存储数组的索引变量 i ,并初始化为 0 。然后利用 (%esp + 0xc) + 4 * i 计算索引为 i 的数组元素的地址,并利用 mov 指令将该地址上的数据传递给 eax 寄存器,据此,%eax = a[i]。然后将 eax 自减 1 ,并将结果和立即数 5 进行比较,jbe 是无符号比较不大于时跳转,即 a[i] – 1

微信扫一扫加客服

微信扫一扫加客服

点击启动AI问答
Draggable Icon