HNU-2024计算机系统实验-BombLab

写在前面:
BombLab实验是计算机系统所有实验中难度最高、最有趣也最能有所收获的实验,实验目的比较简单,就是根据可执行文件bomb反汇编得到的汇编代码,分析每个关卡炸弹被引爆的条件并防止这些炸弹爆炸。实验涵盖了所有课程中出现的情况:例如switch-case的跳转表、函数递归调用、结构体等等,甚至还有更难的复杂数据结构,如:二叉树、链表等等。认真完成这个实验的话,最后一个BufLab实验就可以很轻松的完成了,对汇编代码的理解也会更加深刻。

一、实验项目一

1.1 项目名称

BombLab实验

1.2 实验目的

1) 加强学生对汇编代码的理解和分析能力

2) 提高学生对逆向工程的能力,加强理解C语言和汇编代码之间的联系

3) 提高学生对gdb调试工具的使用

1.3 实验资源

1) BombLab实验文件以及wsl2子系统(搭载ubuntu20.04)
BombLab实验文件由可执行文件bomb、bomb-quiet以及代码文件bomb.c
bomb是实验的主要文件,通过执行这个文件依次进入每一关,输入一串字符串之后炸弹引爆或者进入下一关:
在这里插入图片描述
bomb.c给出了bomb执行的原理:

/***************************************************************************
 * 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.
 ***************************************************************************/

#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. */

    /* 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. */
    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 [<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;
}

main函数依次读入命令行中输入的字符,然后以输入的字符串作为参数传入每一关对应的函数,并在函数中根据这个字符串判断炸弹是否引爆,只有当该关卡的炸弹没有被引爆时,才会进入下一关。

2) 9.2版本gdb

在这里插入图片描述

3) 2.34版本objdump

在这里插入图片描述

2 实验任务

使用objdump -d bomb >b.txt可以反汇编得到bomb文件的汇编代码,将其保存在b.txt文件中方便分析和注释。

2.1 实验任务A:phase_1

汇编代码:

在这里插入图片描述

观察汇编代码可以发现,调用函数的返回值%eax等于0的情况下可以完成跳转,不会引发爆炸,因此考虑本题的关键在于函数string_not_equal,从函数名字可以推测这是一个字符串比较的函数,分析可以发现传入该函数的第二个参数是一个地址,使用gdb查看地址内容:

在这里插入图片描述

再查看函数string_not_equal的汇编代码:

在这里插入图片描述

分析可知这个函数的功能是比较两个字符串是否相等,如果相等返回0,否则返回1,实现原理是传入两个字符串,首先比较字符串的长度(通过函数string_length获取)是否相等,若不相等直接返回1,然后依次比较每一个字符是否相等,直到读到结束符‘\0’结束,如果中途存在两个字符串中相同位置的某两个字符不相等,则直接返回1,若所以输入我们利用gdb得到的字符串,顺利过关:

在这里插入图片描述

通过逆向工程得到的C语言代码:

int string_length(const char *str){
    int length = 0;
    while(str[length] != '\0'){
        length++;
    }
    return length;
}
int strings_not_equal(const char *s1, const char *s2){
    if (string_length(s1) != string_length(s2)){
        return 1;
    }

    while (*s1 != '\0' && *s1 == *s2){
        s1++;
        s2++;
    }
    if(*s1=='\0') return 0;
    return 1;
}
void phase_1(const char *input){
    if (strings_not_equal(input, "And they have no disregard for human life.")){
        explode_bomb();
    }
}

2.2 实验任务B:phase_2

汇编代码:

在这里插入图片描述

在这里插入图片描述

分析汇编代码可知:调用函数read__six_numbers函数按照字符串“%d %d %d %d %d %d”依次读取六个数字,并将其存入栈中:

在这里插入图片描述

起始地址为:0x18+%esp,分析循环条件可知,要使第二个炸弹不被引爆,需要使得存入栈中的六个数字满足:第二个数字等于第一个数字加一,第三个数字等于第二个数字加二,以此类推,第一个数字不能为负数,其余没有要求,因此这里取第一个数字为1,最后答案为1 2 4 7 11 16:

在这里插入图片描述

通过逆向工程得到的C语言代码:

void phase_2(const char *input){
    int numbers[6];
    read_six_numbers(input, numbers);
    if (numbers[0] < 0){
        explode_bomb();
    }
    for (int i = 1; i < 6; i++) {
        if (numbers[i] != numbers[i-1]+i){
            explode_bomb();
        }
    }
}

2.3 实验任务C:phase_3

汇编代码:

在这里插入图片描述

首先通过sscanf函数读入两个参数,并进行判断,如果读入的数字少于两个则爆炸:

在这里插入图片描述

分析汇编代码可以理解,这是一个switch-case结构,限制输入的数字为0、1、2、3、4、5、6、7,大于7的数字直接引爆,我们首先查看跳转表:

在这里插入图片描述

综上,本题的目的在于将输入的第二个参数与第一个参数通过switch-case得到的值进行比较,若不相等则爆炸,分析汇编代码可以得到每一个索引值跳转到对应位置得到的值:

0->255(0xFF) 1->487(0x1E7) 2->836(0x344) 3->847(0x34F) 4->283(0x11B) 5->373(0x175) 6->921(0x399) 7->68 (0x44)

取1的情况:

在这里插入图片描述

通过逆向工程得到的C语言代码:

void phase_3(const char *input) {
    int n1, n2;
    int result = sscanf(input, "%d %d", &n1, &n2);
    if (result < 2) {
        explode_bomb();
        return;
    }

    switch (n1) {
        case 0:
            if (n2 != 0xff) explode_bomb();
            break;
        case 1:
            if (n2 != 0x1e7) explode_bomb(); 
            break;
        case 2:
            if (n2 != 0x344) explode_bomb();
            break;
        case 3:
            if (n2 != 0x34f) explode_bomb();
            break;
        case 4:
            if (n2 != 0x11b) explode_bomb();
            break;
        case 5:
            if (n2 != 0x175) explode_bomb();
            break;
        case 6:
            if (n2 != 0x399) explode_bomb();
            break;
        case 7:
            if (n2 != 0x44) explode_bomb();
            break;
        default:
            explode_bomb();
    }
}

2.4 实验任务D:phase_4

汇编代码:

在这里插入图片描述

观察汇编代码可以发现,本函数调用sscanf函数读入两个参数,放入地址%esp+0x18以及%esp+0x1c,如果输入参数不等于2则爆炸,设这两个参数为x,y,通过分析汇编代码可知,x的范围必须在[0,14]之间,若不满足此区间则直接爆炸,同时y等于35(0x23),同时调用函数func4,传入三个参数,分别为:x、0、0xe,返回的参数必须满足等于35(0x23),故分析函数func4,根据返回值来推测传入参数x:

在这里插入图片描述

分析这段汇编代码可知,func4实现的是通过递归实现一个二分搜索树,每个节点储存本次查找的区间的中间值,根节点储存整型7,根据最初的区间范围为[0,14],如果传入的值比中间值大,则返回右节点的路径,如果传入的值比中间值小,则返回左节点的路径,依次将0-14的不同值作为传入参数进行模拟,得到最终的路径值为35时,对应传入的参数x为8(根据后面的分析,本题是进入隐藏关卡的入口):

二分搜索树:

在这里插入图片描述

通过本题:

在这里插入图片描述

通过逆向工程得到的C语言代码:

int func4(int a, int b, int c) {
    int mid=(b+c)>>1;
    if (mid<a) return func4(a,mid+1,c)+mid;
    else if(mid>a) return func4(a,b,mid-1)+mid;
    else return mid;
}

void phase_4(const char *input) {
    int num1, num2;
    if (sscanf(input, "%d %d", &num1, &num2) != 2) {
        explode_bomb();
        return;
    }
    if (num1 < 0 || num1 > 14) {
        explode_bomb();
        return;
    }
    int result = func4(num1,0,14);
    if (result != 35) {
        explode_bomb();
        return;
    }
    if (num2 != 35){
        explode_bomb();
        return;
    }
}

2.5 实验任务E:phase_5

汇编代码:

在这里插入图片描述

观察汇编代码,可以发现本题传入的参数是一个字符串,首先判断字符串的长度,若不等于6则引发爆炸,因此我们可以得到本关应输入一串含有6个字符的字符串,继续分析,发现将六个字符依次取出,并与0xF求与,得到每个字符对应ASCII码的低四位,以此作为偏移量,从基址0x804a228处进行偏移,将每个字符对应的偏移地址处的字符取出,依次放入从地址空间%esp+0x15处,最后将地址%esp+0x15作为字符串的首地址,与首地址为0x804a1fe的字符串进行比较,若完全相等则不引发爆炸,利用objdump查看0x804a1fe处的字符串:

在这里插入图片描述

再查看基址0x804a228往后的地址中的字符:

在这里插入图片描述

我们可以很容易得到需要的字符对应的偏移量(从0开始):

o->10 i->4 l->15 e->5 r->>6 s->7

所以我们输入字符串对应的ASCII码的低四位即为上述偏移量,这里取JDOEFG:

在这里插入图片描述

通过逆向工程得到C语言代码:

void phase_5(const char *input) {
    if (string_length(input) != 6){ 
        explode_bomb();
    }
    char mapped[7];
    const char *mapping = "maduiersnfotvbyl";
    for (int i = 0; i < 6; i++){ 
        int idx = input[i] & 0xF;
        mapped[i] = mapping[idx];
    }
    mapped[6] = '\0';
    if (strings_not_equal(mapped, "oilers")) {
        explode_bomb();
    }
}

2.6 实验任务F:phase_6

汇编代码:

在这里插入图片描述

分析这一关的汇编代码,可以看到调用了read_six_numbers这个函数,所以同样输入六个参数,汇编代码前半段是一个双重循环,外层循环不发生爆炸的条件是每个参数减一均小于6,内层循环不发生爆炸的条件是这六个数的值互不相等,同时每个参数的值应该大于0,所以这六个参数是1 2 3 4 5 6的排列组合,接着分析汇编代码的后半段,发现在重复进行一个操作:即mov 0x8(%edx) %edx,同时最初的%edx的值为:0x804c13c,利用gdb调试查看内存地址0x804c13c的值:

在这里插入图片描述

不难看出这个地址起始存放了一个含有6个节点的链表,每个节点的元素依次为:整型val、整型n(表示第n个节点),下一个节点的地址,分析汇编代码可知,重复进行的操作是将链表按照我们输入的数字进行重新排列,如输入:2 3 4 1 5 6,则按照n等于2的节点->n等于三的结点->n等于四的节点…以此排列,然后依次遍历链表的所有节点,判断是否满足节点的值单调递增,不满足则发生爆炸,从上表可以很容易看出,单调递增排列的节点为:4 1 3 5 6 2:

在这里插入图片描述

通过逆向工程得到的C语言代码:

void phase_6(const char *input){
    int numbers[6];
    Node* nodes[6];
    read_six_numbers(input, numbers);
    int i=0;
    while(1){
        if(numbers[i]>1) explode_bomb();
        i++;
        if(i==6) break;
        for(int j=i;j<=5;j++){
            if(numbers[j]==numbers[i]) explode_bomb();
        }
    }
    Node* head = (Node *)0x804c13c;
    for (int i = 0;i<6;i++) {  
        int index = numbers[i];
        Node *node = head;
        for (int j = 1;j<index;j++) {
            node = node->next;
        }
        nodes[i] = node;
    }
    Node *new_head = nodes[0];
    Node *cur = new_head;
    for (int i = 1; i < 6; ++i) {
        cur->next = nodes[i];
        cur = cur->next;
    }
    cur->next = NULL;

    cur = new_head;
    int prev = cur->value;
    while ((cur = cur->next) != NULL) {
        if (cur->value <= prev) {
            explode_bomb();
        }
        prev = cur->value;
    }
}

2.7 实验任务G:secret_phase

通过汇编代码,我们发现在phase_6下面还有一个隐藏关卡:secret_phase:

在这里插入图片描述

但是在main函数中,我们并没有发现调用secret_phase,经过查找发现secret_phase在函数phase_defused中被调用:

在这里插入图片描述

分析汇编代码,利用gdb调试查看地址0x804c3cc的值,分析read_line函数可知此处存放的是已经输入的字符串次数,因此需要通过所有6个关卡才能最后进入到隐藏关:

在这里插入图片描述

继续分析汇编代码可以发现:要想进入隐藏关卡,必须调用sscanf从0x804c4d0中解析出三个参数:前两个是整数,第三个是字符串,同时第三个字符串要与0x804a3aa地址处的字符串完全相同,利用gdb调试:

在这里插入图片描述

所以可以确定我们在0x804c4d0地址处输入的第三个字符串参数为DrEvil,接下来寻找输入字符串的位置,通过分析read_line函数可以发现,每次调用phase函数,输入字符串的地址都满足:0x804c3e0+80n,(n是地址0x804c3cc处存放的值),根据上文分析可知,n表示已经输入的字符串次数,解线性方程0x804c3e0+80n=0x804c4d0,n=3,所以进入隐藏关卡的地址输入字符串的地址对应之前已经输入3次字符串,所以是在第四关输入两个整数之后额外输入DrEvil可以进入隐藏关:

在这里插入图片描述

在这里插入图片描述

进入隐藏关后,接下来就分析secret_phase:

在这里插入图片描述

通过分析可知,secret_phase函数传入的参数是一个字符串,然后通过strtol字符串进行解析,读取其中的整数片段,若该整数大于0x3e8则爆炸,然后将该整数以及一个地址0x804c088传入函数func7,利用gdb调试查看地址内容:

在这里插入图片描述

可以发现此地址应该是一个二叉搜索树的根节点,每个节点的参数依次为:整数val、左子结点、右子节点,同时每个节点的值大于其左子结点的值,小于右子节点的值,继续分析可以发现,调用完func7的返回值必须等于5,否则产生爆炸,进而分析func7:

做出二叉树(图中数字均为十六进制表示):

在这里插入图片描述

func7:

在这里插入图片描述

可以看出,func7函数是对二叉搜索树进行递归搜索,直到找到与传入参数值相同的节点,若当前节点的值大于传入参数,则返回两倍调用左子节点的路径,若当前节点小于传入参数则返回两倍调用右子节点的路径加一,若相等则返回零,通过模拟输入每个节点的值来求出不同的路径,可以发现输入值为47,即从根节点开始,先访问右节点,再访问左节点,再访问右节点可以得到返回值5:

在这里插入图片描述

因为隐藏关卡是从输入的字符串解析整数,所以可以整活:

在这里插入图片描述

加上字符串“IloveHNU”依然能够顺利过关

通过逆向工程得到的C语言代码:

typedef struct TreeNode{
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
}TreeNode;
int func7(TreeNode* node,int value){
    if(node==NULL) return -1;
    int node_val = node->val;
    if(node_val>value) return 2*func7(node->left,value);
    else if(node_val<value) return 2*func7(node->right,value)+1;
    else return 0;
}
void secret_phase(){
    char *input = read_line();
    long num = strtol(input, NULL, 10);
    if (num < 1 || num > 0x3e9) {
        explode_bomb();
    }
    if (fun7((TreeNode *)0x804c088, num) != 5) {
        explode_bomb();
    }
    puts("Congratulations! You've defused the secret stage!");
    phase_defused();
}

3 总结

3.1 实验中出现的问题

  1. 在刚开始做这个实验的时候,对汇编代码有些不太熟悉,尤其是read_six_numbers函数中调用的sscanf函数,平时学习过程中也少有此函数的使用,为了搞清楚这个函数的使用方法和规则,查阅了较多的资料,以及在自己本地的编译环境多次尝试,最后终于熟练掌握sscanf的使用;

  2. 刚开始做本实验时,对gdb调试的使用不太熟练,导致花费了很多额外的不必要的时间浪费,通过自学许多gdb调试技巧,如:layout asm实时显示代码运行位置的指令等,提高了分析汇编代码的效率。

3.2 心得体会

1) 历经15个小时以上的时间完成本实验,从最开始第一题都费劲,到能够比较顺利地理解复杂数据结构的汇编代码,通过这个实验,我非常深入地进行了一次汇编代码的分析学习,看着一个个关卡的顺利通过,直到最后一个隐藏关卡结束,让人很有成就感,也很有收获;

2) 通过这个实验我了解到了许多新的gdb调试指令,利用这些指令能够更加高效地开展汇编代码的分析,为我后期更深入地学习汇编代码打下基石;

3) 通过这个实验我理解了链表、二叉树等复杂数据结构在内存中的存储形式、函数递归调用、深度优先搜索、switch-case跳转表的汇编表示,以及简单常见函数:strlen等的汇编表示,加深了对汇编代码的理解。

  • 47
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值