Bomblab 记录

最近计算机系统基础课程要求完成这项作业,故记录一下完成过程。

  • bomb.c

    首先,出题者给我们提供了bomb的原型,通过bomb.c,大致可以了解这个bomb的执行流程:从键盘读入一行,然后执行phase_(x)然后解除炸弹。总共有6?个phase等待我们取解开。

    bomblab_bomb.c
  • 环境准备

    这里,我是用了radare2 作为分析工具。(然而第6题太给力,用了IDA)

    首先,执行r2 -A bomb加载二进制文件并执行分析。

    bomblab_prepare

    然后,执行afl看一下函数列表,我们发现了几个包含phase的函数,也就是我们要重点关注的东西了。同时也注意到列表里包含sym.secret_phase,估计这是个bonus

    bomblab_functions
  • Phase_1

    先执行s sym.phase_1跳转到phase_1,然后执行pdf查看反汇编代码。

    bomblab_phase1

    phase_1的汇编代码比较简洁。

    我们的输入存放在rdi中,然后phase_1中将Border relations with Canada have never been better.这句话放入esi中作为参数调用strings_not_equalstrings_not_equal函数负责判断两个字符串是否相等。接下来是test指令判断eax的值是否为0,不为0则引爆炸弹。

    所以phase_1的答案就是Border relations with Canada have never been better.

  • Phase_2

    一样的跳转到phase_2然后查看反汇编代码。

    bomblab_phase2

    可以看到,在phase_2中程序读入了6个数字,然后判断第一个数字是否为1,不为1则爆炸。

    若为1则跳转到0x00400f30处,这里radare2qword [local_4h]表示rsp+4,也就说local_4hlocal_18h刚好是第二个数到第六个数和“第七个数”(用于判断循环结束)(也刚好应证了开头sub rsp, 0x28开辟的7个空间)。将一头一尾分别保存到rbxrbp后,一个JMP直接跳到了0x00400f17开始执行循环。

    在循环体中,程序先取出前一个值(rbx-4),然后将这个值乘2,在与当前值比较(rbx)。如果相等那么将rbx+4也就是循环迭代下一个值,如果刚好到最后一个值了那么就跳出循环。

    所以这个循环就是判断这六个数是否是以1为首项,2为公比的等比数列。

    phase_2的答案是1 2 4 8 16 32

  • Phase_3

    同样的,跳转到phase_3然后查看反汇编代码。

    bomblab_phase3

    这里先调用sscanf读取读取两个数("%d %d"),然后判断返回值是否大于1。查阅资料后得知sscanf的返回值的含义应该是:

    如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF。

    然后我们根据调用的约定可以得出local_ch存放第二个数,local_8h存放第一个数。

    然后有个CMP比较(0x00400f6a),可以得出第一个数不能大于7,同时又由接下来的ja得出第一个数应该是无符号的,也就第一个数还需大于0。

    然后进入了一个switch-case语句。首先是jmp qword [rax*8 + 0x402470],这里可以想到应该是通过switch-case的跳转表确定结果。由于前面已经推出第一个数只可能是0到7共8个值,故执行px/8xg 0x402470查看那8个case的跳转表。

    bomblab_phase3

    所以设第一个数为x,那么x的跳转表如下:

    地址 x
    0x00400f7c 0
    0x00400fb9 1
    0x00400f83 2
    0x00400f8a 3
    0x00400f91 4
    0x00400f98 5
    0x00400f9f 6
    0x00400fa6 7

    对于每一个case都修改了eax的值,然后跳转到0x00400fbe与第二个数的值进行比较,如果相等就成功拆弹,否则就失败。故本题答案有多组,设第二个数位y,本题答案如下:

    x y
    0 207(0xcf)
    1 311(0x137)
    2 707(0x2c3)
    3 256(0x100)
    4 389(0x185)
    5 206(0xce)
    6 682(0x2aa)
    7 327(0x147)
  • Phase_4

    同样的,跳转到phase_4然后查看反汇编代码。

    phase_4phase_3开头很像,同样是读入了两个数(设为xy)。

    bomblab_phase4

    完成输入后,在0x0040102ex进行比较,其中x<=14

    接着将edx赋值为14,将esi赋值为0,edi赋值为x,再进入fun4

    bomblab_phase4

    这里是一个递归调用,我们尝试逆向出C语言代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    int func4(int edx,int esi,int edi) {
    int eax = edx - esi;
    int ecx = eax >> 31; // 符号位
    eax += ecx;
    eax /= 2;
    ecx = eax + esi;
    if(ecx<=edi) {
    if(ecx>=edi) {
    return 0;
    } else {
    esi = eax+1;
    eax = func4(edx,esi,edi);
    return 2*eax+1;
    }
    } else {
    edx = ecx-1;
    eax = func4(edx,esi,edi);
    return 2*eax;
    }
    }

    这里可以大致看出是一个二分算法,稍微修改一下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    int func4(int r, int l, int t)
    {
    int mid = (r + l) >> 1; // ecx,同时假定r>l
    if (mid == t)
    { // 找到目标
    return 0;
    }
    else if (mid < t)
    { // mid < t 中值小于目标x,扩大左边界
    l = ((r - l) >> 1) + 1;
    return func4(r, l, t) * 2 + 1;
    }
    else
    { // mid > t 中值大于目标x,缩小右边界至mid-1
    r = mid - 1;
    return func4(r, l, t) * 2;
    }
    }

    phase_4中执行了func4(14,0,x)其返回值要求为0,同时第二个输入也必须为0(??)才能过关。

    关于func4不妨写一个程序遍历一遍(x取值在0到14之间),得到的结果如下:

    x func4
    0 0
    1 0
    2 4
    3 0
    4 2
    5 2
    6 segmentation fault
    7 0
    8 1
    9 1
    10 segmentation fault
    11 1
    12 segmentation fault
    13 segmentation fault
    14 segmentation fault

    这里比较有意思的是segmentation fault,估计是模拟给出的C语言代码有些问题吧!同时实际测试发现bomb没有给出segmentation fault而是直接引爆了炸弹。

    所以综上,本题答案为0 01 03 07 0

  • Phase_5

    同样的,跳转到phase_5然后查看反汇编代码。

    bomblab_phase5

    其实这段汇编有些内容我还不清楚,但是不影响解题。

    2020-4-27:

    关于fs:[0x28]

    这里实际上是通过段寻址方式获取了一个“金丝雀”(canary)值并把这个值送入栈中,这个动作是一种栈溢出保护手段。

    先将这个特殊的值压栈,当函数结束后再判断栈中的这个值是否被修改过,以此来验证是否出现栈溢出。

    关于0x28这个偏移量:

    这个bomb是64位,并且与glibc链接。fs寄存器被glibc定义为存放TLS(Thread Local Storage)信息。

    Github上的一份glibcmirror中我们可以找到tls的定义:Github

    其中stack_guard存放的就是这里偏移为0x28的金丝雀值。

    这段代码的大意是:

    1. 输入一个字符串,长度为6
    2. 遍历输入的每个字符c
    3. 获取c的ASCII二进制码的后四位,将这个数作为索引从maduiersnfotvbylSo_you_think_you_can_stop_the_bomb_with_ctrl_c__do_you中取出字母拼接一个新的字符串。
    4. 比较拼接出的字符串是否是flyers

    这里由于是4位二进制串故我们只用考虑在前16位(maduiersnfotvbyl)中找字母就可以了。

    其中f-9l-15y-14e-5r-6s-7。这里由于只考虑后4位二进制,所以可以直接printf出几个不可见字符或者找可见字符后四位满足要求的。比如:

    1
    2
    3
    printf("%c%c%c%c%c%c",9,15,14,5,6,7);
    printf("IONEFG");
    ...
  • Phase_6

    同样的,跳转到phase_6然后查看反汇编代码。

    bomblab_phase6

    这一题汇编代码比较长。一开始就分配了20个4字的栈空间。

    这道题静态分析不好整(我太菜了),所以使用IDA进行动态调试。

    bomblab_phase6

    首先定位到phase_6

    其中左半边的比较好研究,先看左半边。

    bomblab_phase6

    左半边有两重循环,其中左半边的逻辑大至如下:

    1. 首先读入六个数字
    2. 遍历这六个数字,记当前数字为a
    3. 如果a比6大则爆炸
    4. a开始遍历以后的数字,如果存在与a相等的则爆炸
    5. 完成第3,4步后迭代a

    也就是说这1-5步就是确定这六个数字互不相等且都小于等于6(大于等于1)

    执行完以上步骤之后又是一个循环:

    bomblab_phase6

    在这个循环内,用7减去输入的每个数字。

    接着,到了最为复杂的部分。

    bomblab_phase6

    首先是一个神秘常数node1(应该是IDA自动命名的)。因为一些神神秘秘的原因,我无法直接获取它的值,所以我在动态调试时使用python脚本来获取。

    bomblab_phase6 bomblab_phase6

    最后我们拿到了六个数所对应的神秘数值:

    node 7-x x
    332 1 6
    168 2 5
    924 3 4
    691 4 3
    477 5 2
    443 6 1

    接着程序按照我们输入的值将node的值对应按顺序存入RBX中,最后检查RBX中的值是否是递减的。

    bomblab_phase6

    所以我们手动给这几个数排序,也就是924>691>477>443>332>168对应成我们要输入的数就是4 3 2 1 6 5

  • 最后

    其实还有一个secret_phase没分析,就等下一次填坑吧!

    总结一下,这6个phase从简单到难,的确是很考验基础知识的。

    UPDATE:来填坑了

  • secret_phase

    关于这个secret_phase如不是题目明确指明,我或许都不会去想有这一关。

    开启secret_phase的机关在phase_defused中:

    bomblab_secret_phase

    在这个函数里,先判断是否完成了六次输入,如果完成了就会判断是否触发隐藏关。

    在这里调用了sscanf来进行输入,而需要转换的源字符串是什么,这里通过动态调试,得知在执行到这儿后,第四次输入的内容将被传送到这。

    所以触发隐藏关就是在第四次输入后多加一个DrEvil

    下面就来看secret_phase

    bomblab_secret_phase

    secret_phase中首先读入了一个十进制的数字字符串,然后通过strtol函数把它转化为整数。

    如果这个数小于等于1001则可以继续,反之引爆炸弹。

    接着,调用函数fun7,传入两个参数,一个是”$”(?)一个是转换后的那个数,如果返回值为2则顺利通关。

    接着分析fun7

    bomblab_secret_phase

    这里尝试还原成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
    int fun7(long long *edi, long long esi)
    {
    if (!edi)
    {
    return 0xffffffff;
    }
    if (*edi <= esi)
    {
    if (*edi == esi)
    {
    return 0;
    }
    else
    {
    edi += 16;
    return 2 * fun7(edi, edi) + 1;
    }
    }
    else
    {
    edi += 8;
    return 2 * fun7(edi, esi);
    }
    }

    稍微整理一下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int fun7(long long *x, long long y)
    {
    if (!x)
    return 0xffffffff; // -1
    if (*x > y)
    return 2 * fun7(x + 1, y);
    if (*x == y)
    return 0;
    if (*x < y)
    return 2 * fun7(x + 2, y) + 1;
    }

    然后回到最初传入的那个地址,执行px/8xg 0x6030f0查看一下存放的值:

    bomblab_secret_phase

    然后再反向推这个fun7,得到结果2的步骤应该是0->2*0+1->2*(2*0+1)

    1. 那么第一次输入要先满足*x>y,而第一次输入时*x=36y<36
    2. 然后取x的下一个数,也就是0x603110指向的内容,即8。此时要满足*x<y,即y>8
    3. 接着取x的下两个数,也就是0x603150指向的内容,即22。此时要满足*x==y,即y=22

    所以secret_phase的答案是22

    UPDATE-2020-4-29:

    其实这个隐藏关还有另一个答案20

    我们不妨认为x+1就是左子树,x+2就是右子树,x对应的就是当前节点的值,可以画出以可二叉树:

    bomblab_secret_phase

    关于另一个答案20,可以这样推出:我们知道,最后找到相等时返回的是0,所以在找到0的上一层可以嵌套多个*x>y(0*2还是0)。那么按照上面画的二叉树来判断就是在22的左子树上也就是20(比22的多遍历一次)