# 前置任务

  • 对 bomb 进行反汇编
objdump -d bomb > bomb.txt

0
查看反编译后的结果可以知道每个阶段输入答案的指针都存入了 % rdi(函数第一个参数),并将该参数传入 phase_1~6 函数

# Phase 1

1
将 0x402400 移入 % esi 并调用 string_not_equal 函数,而 string_not_equal 函数用于判断两个字符串是否相等,若相等则返回 0,否则返回 1
349 行 string_not_equal 比较输入字符和 0x402400 的内容
2
350~351 行将函数的返回值相与并判断结果是否为 0,由于只有 0&0 的结果才为 0,所以 % rax 不为 0 就会爆炸,即输入字符一定要和 0x402400 内的内容相等
调用 gdb 查看 0x402400:
3
答案即为 Border relations with Canada have never been better.

# Phase 2

4
360 行栈顶指针赋值给 % rsi,361 行调用 read_six_numbers 函数,而 read_six_number 函数读入输入的六个数字并压入栈中
5
362~364 行比较栈顶元素(输入的第一个数)是否和 1 相等,如果不相等则爆炸,所以第一个数一定为 1,跳到 0x400f30:
6
375~377 行栈中第二个元素的指针赋值给 % rbx,并将栈中第 7 个元素的指针(终止条件)赋值给 % rbp,接着跳回 0x400f17 进入循环:
7
% rbx 本存着栈中第二个元素的指针,366 行将指针向顶部移动,即将栈顶元素赋值给 % eax,
367 行将 % eax 元素乘 2,即 % eax 元素变为 2
gdb 断点测试证实推测:
8
368 行将 % eax 和栈中第二个元素比较,如果不相等则爆炸,所以输入的第二个元素应该为 2
跳到 0x400f25:
9
% rbx 中的指针向栈底移动,并和终止条件比较,如果没有到第六个元素则重新返回 0x400f17
归纳可得循环中:
1.% eax 存着栈中前一个元素的值
2.% eax 乘以二
3.% eax 和栈中后一个元素比较,如果相等则继续循环直到结束,否则直接爆炸
答案即为 1 2 4 8 16 32

# Phase 3

10
385~386 行将栈中第个 4 元素的指针赋值给 % rcx(函数第四个参数),第 3 个元素的指针赋值给 % rdx(函数第三个参数)
387~389 行将 0x4025cf 赋值给 % esi(函数第二个参数),将 % eax 置 0,并调用 sscanf 函数
sscanf 函数的第一个参数表示输入源,第二个参数表示 format 参数,返回输入源按照 format 参数进行匹配的个数,并将匹配的字符串依次放入第三和第四个参数中
调用 gdb 查看 0x4025cf:
11
390~392 行如果函数返回值大于 1(等于 2)则不会爆炸,所以答案应为两个整数
12
393 行将栈中第三个元素(输入的第一个数字)和 7 进行比较,如果大于则爆炸,说明第一个数字范围在 0~7
395 行将输入的第一个数字存入 % eax,396 行进入跳转表,跳转到 0x402470+% rax*8
调用 gdb 查看跳转表:
13
根据跳转表查看汇编代码:
14
% rax 从 0 到 7 都有一一对应的跳转代码,说明有 8 种答案
以第一个数字为 0 为例:
根据跳转表跳转到 397 行,而在 397 行将 % eax 赋值为 0xcf,并跳转到 0x400fbe,比较 % eax(0xcf)是否和栈中第四个元素相等,不相等则爆炸,所以当第一个数字为 0 时,第二个数字为 207
根据跳转表可以推断出其余 7 个答案:
1 311
2 707
3 256
4 389
5 206
6 682
7 327

# Phase 4

15
16
前一部分和 phase_3 完全相同,输入应为两个整数,且放于栈中第三个位置和第四个位置中
17
455 行判断输入的第一个数字是否小于等于 14 且非负(无符号),否则爆炸
18
457~460 行 % edx(第三个参数)赋值为 14,% esi(第二个参数)赋值为 0,% edi(第一个参数)赋值为输入的第一个数字,% rcx(第四个参数)之前赋值为输入的第二个数字,调用 func4 函数
先分析 func4 后的代码:
19
461~462 行判断 func4 函数的返回值是否为 0,不为 0 则爆炸
463 行判断输入的第二个数字是否为 0,不为 0 则爆炸,所以输入的第二个数字一定为 0
由于 func4 函数中包含递归,所以选择反汇编为 c 语言:
20

int a = 7, b = 0, c = 14, d = 0, rax;
void func4()
{
    rax = ((int)(c - b + ((unsigned)(c - b) >> 31))) >> 1;
    d = b + rax;
    if (d > a)
    {
        c = d - 1;
        func4();
        rax = rax * 2;
    }
    else if (d < a)
    {
        rax = 0;
        b = d + 1;
        func4();
        rax = rax * 2 + 1;
    }
    else if (d == a)
        rax = 0;
}

发现第一次进入函数中时 d 的值为 7,而当输入的数也为 7 时为最简单的一种情况,即既不会进入(d>a)的递归,也不会进入(d<a)的递归,直接返回 0

除了最简单的 7 0 这一种情况,第一个数也可以为 3 或 1 或 0

# Phase 5

22
472 行将输入字符串的指针赋值给 % rbx,476 行清空 % eax,477 行调用 string_length 函数,478~480 行如果函数返回值不为 6 则爆炸,说明答案为长度为 6 的字符串
23
跳到 0x4010d2 将 % eax 清空,跳回 0x40108b
24
% rax 等于 0,所以 482 行 % ecx 为输入字符串的第一个字符,483 行将第一个字符存入栈顶,484 行~485 行将第一个字符存入 % rdx 并与上 0xf(相当于 mod16),486 行将 % rdx+0x4024b0 重新赋值给 % rdx
调用 gdb 查看 0x4024b0:
25
即 % rdx 中存放着 maduiersnfotvbyl 的第(输入的第一个数 mod16)位的字符
487 行~490 行将 % rdx 重新存入栈中向下 0x10 位,% rax 加 1 并和 6 比较,如果不等于 6 则跳回 0x4024b0 进入循环
归纳可得循环后:从栈中向下 0x10 位起起每个字节(共 6 个字节)存放着 maduiersnfotvbyl 的第(输入的第 n 数 mod16)位的字符
26
491 行将 0x10 起第 7 个字节置 0,492~496 行将 0x40245e 赋值给 % esi(第二个参数),将栈向下 0x10 位地址存入 % rdi(第一个参数)并调用 strings_not_equal 函数,如果返回值不等于 0 则爆炸
调用 gdb 查看 0x40245e:
27
说明栈中存放的内容应该为 flyers,推得输入的字符串 mod16 后应该为 12,18,17,6,7,8,通过 ascii 码反推可得输入的答案应该为 ionefg

# Phase 6

28
518~519 行将栈指针赋值给 % r13 和 % rsi,并调用 read_six_numbers 函数,所以答案应为 6 个整数
read_six_number 函数将输入的数字依次存入栈中
29
521~528 行将栈指针赋值给 % r14 和 % rbp,% r12d 赋值为 0(计数器),将栈顶第一个元素(输入的读一个数字)赋值给 % eax,将 % eax 减 1 并和 5 比较,如果比 5 大就爆炸,说明输入的第一个数字不能大于 6,且必须大于等于 1(jbe 比较的是无符号整数)
30
529~531 行将计数器 % r12d 加上 1 并判断是否等于 6,如果等于 6 则跳出大循环进入 0x401153
如果不等于 6 则继续进入下一步:
31
将计数器赋值给 % ebx(第二个计数器)和 % rax,并将栈中第 % rax 个元素重新赋值给 % eax 并和栈顶元素比较,如果相等则爆炸,如果不相等则进入下一步:
32
538~542 行 % ebx 加上 1 并和 5 比较,如果小于等于 5 则跳回 0x401135 重复比较栈中第 % rax 个元素和栈顶元素,如果大于 5 则将 % r13 加上 4,跳会循环开始 0x401114
由此可以归纳上述二重循环代码:
大循环比较输入的每个数字是否在 1 到 6 之间,小循环比较大循环中中第 i 位(i 表示大循环了几轮)输入和之后输入的数字是否相等
即输入的每个数字是否在 1 到 6 之间且互不相等
33
c 代码:

int input[6] = { 1,2,3,4,5,6 };
void phase_6()
{
    for (int i = 0; i < 6; i++)
    {
        if (input[i] > 6 || input[i] < 1)
        {
            cout << "bomb" << endl;
            break;
        }
        for (int j = i + 1; j <= 5; j++)
        {
            if (input[i] == input[j])
            {
                cout << "bomb" << endl;
                break;
            }
        }
    }
}

35
543~548 行将栈中第七个元素存入 % rsi(循环终止条件),将 % r14(栈指针)赋值给 % rax,将 7 赋值给 % ecx,并将(7 - 栈顶元素)重新赋值回栈中,% rax 向栈底移动一个元素,如果 % rax 不等于 % rsi(终止条件)则循环上述操作(输入的整数取相反数并加 7)
循环跳出后将 % esi(大循环的计数器)清零并跳到 0x401197:
36
% ecx 首先存入栈顶元素,并和 1 比较,如果小于等于 1 则跳到 0x401183:
37
561~562 行将 0x6032d0 存入栈顶向下 0x20 个字节的位置,563 行将 % rsi 加上 1 并和 0x18 比较,如果相等则跳出循环,否则 567 行 % ecx 变为栈中第二个元素,继续和 1 比较
而若栈中目前遍历到的元素大于 1,则跳过跳转代码向下运行
38
% eax(小循环的计数器,同样用作比较)置 1,同样将 0x6032d0 存入 % eax,并跳转到 0x401176:
39
555 行 % rdx 加上 0x8 并将指针相对应的元素重新赋值给 % rdx
调用 gdb 查看 0x6032d0:
40
发现其为链表,而 % rax 加上 0x8 并将指针相对应的元素重新赋值相当于 % rdx+0x10
计数器 % eax 加 1 并和 % ecx(栈中目前遍历到的元素)比较,如果不相等则跳回 0x401176 进行循环直到计数器 % eax 和 % ecx 相等,此时 % rdx 应该等于 0x6032d0+0x10*(栈中目前遍历到的数字 - 1),跳到 0x401188
将 % rdx 的值赋值到栈中 % rsi*2+0x20 位置,并将 % rsi(大循环计数器)加上 0x4,并和 0x18 比较,如果不相等(没有遍历完六个元素)则继续循环,否则跳出循环进入 0x4011ab
由此可以归纳上述二重循环代码:
每次将 0x6032d0+0x10*(栈中目前遍历到的数字 - 1)赋值到栈中(栈中目前遍历到第几个数)*8+0x20 位置
即栈中从栈顶起向下(1)0x20,(2)00x28,(3)0x30,(4)0x38,(5)0x40,(6)0x48 分别保存了 0x6032d0+0x10*(6-ai)
41
42
573~575 行将栈顶起向下 0x20 的元素存入 % rbx,将栈顶起向下 0x28 的地址存入 % rax,将栈顶起向下 0x50 的地址存入 % rsi(循环终止条件)
576~578 行将和 % rcx 栈中遍历到的指针所对应的节点的 next 地址(% rcx+0x8)置换为栈中遍历到的后一个指针(% rdx),% rax 向后移动,并将 % rax 和终止条件比较,如果不相等则 % rcx 也向后移动并继续循环,否则跳到 0x4011d2 将最终的节点的指针置空
(根据栈中存储的指针对链表进行重排)
43
586 行 % ebp 置 5(计数器),% rax 置为栈中第一个元素指针指向的节点的下一个节点指针,% rbx 为栈中第一个元素指针,并将(% rax)低四字节和(% rbx)比较,如果(% rbx)比(% rax)小则爆炸,否则继续循环直到计数器为 0,即从重排序链表的第二个节点开始依次比较前节点和该节点的低四位大小
为了不爆炸,重排序链表应为降序,由此可反推:
第 1 个数 ——>(924 排第 3 位)——>-3+7=4
第 2 个数 ——>(691 排第 4 位)——>-4+7=3
第 3 个数 ——>(477 排第 5 位)——>-5+7=2
第 4 个数 ——>(443 排第 6 位)——>-6+7=1
第 5 个数 ——>(332 排第 1 位)——>-1+7=6
第 6 个数 ——>(168 排第 2 位)——>-2+7=5

# Secret Phase

分析 phase_defused 代码:
44
891 行清空 % rax,892 行判断 0x603760 是否和 6 相等,如果不相等则跳过了 secret_phase 函数,所以 0x603760 一定要等于 6
调用 gdb 在第一阶段结束后加断点,并查看 0x603760:
45
0x603760 中存放的数字为 1,所以 0x603760 存放了输入答案的次数,只有通过了 phase_6 才会进入 secret_phase
46
899 行调用 sscanf,900 行函数返回值如果不等于 3 就会跳过 secret_phase,说明 % edi 一定要和 % esi 匹配
调用 gdb 查看 0x402619(% esi)和 0x603870(% edi):
47
0x603870 中存放了第四阶段的输入,所以第四阶段应再输入一个字符串
48
902 行将 0x402622 赋值给 % esi,903 行将栈中第五个位置指针赋值给 % rdi,904~906 行调用 strings_not_equal 函数,比较 % rdi 和 % esi 所指向的内容字符串是否相等,不相等则跳过 secret_phase 函数,所以两者必须相等
调用 gdb 查看 0x402622:
49
所以第四阶段答案应为 7 0 DrEvil
进入隐藏关卡:
50
查看 secret_phase 函数代码:
51
622 行将输入的字符串转成相应的 10 进制长整型,623 行将长整型赋值给 % rbx,说明应输入一个整数
52
624~627 行如果输入的数字减一后大于 1000 则爆炸,说明输入的数字应该小于等于 1001
53
628 行将 % ebx,即输入的数字赋值给第二个参数,将 0x6030f0 赋值给第一个参数,并调用 fun7
先看 fun7 之后的代码:
54
631~633 行如果 % rax 不等于 2 就会爆炸,说明函数返回值一定是 2
fun7 函数代码:
55
56
首先传入的第一个参数如果等于 0 则会跳到 0x401238 将 % eax 赋值为 0xffffffff,所以第一个参数不能等于 0
由于 fun7 函数中包含递归,所以选择反汇编为 c 语言:

int x[]{36,8,50,22,45,6,107,40,1,99,35,7,20,47,1001};
int rdx, input=20;
int key=0;
void fun7()
{
    if (key != 15)
    {
        rdx = x[key];    //key is address
        if (rdx < input)
        {
            rax = 0;
            key += 2;
            fun7();
            rax = 2 * rax + 1;
        }
        else if (rdx == input)
            rax = 0;
        else if (rdx > input)
        {
            key += 1;
            fun7();
            rax = rax * 2;
        }
    }
    else
        rax = 0xffffffff;
}

57
由于函数最终 % rax 必须等于 2,所以根据 c 语言代码确定 % rax 的最简单的递归顺序应该为 0->1(2*rax+1)->2(rax=rax*2)
调用 gdb 查看 % rdi 内存放的内容:
58
从 0x6030f0 起内存被分成了 15 块节点,每个节点分成三个部分,第一个部分为数值,第二、三部分为指针
可以推断出第一次节点初值为 36,进入 rdx > input 部分,地址 + 0x8 变为 0x603110,值变为 8,输入数字小于 36
第二次进入 rdx < input 部分,地址 + 0x10 变为 0x603150,值变为 22,输入数字大于 8
第三次进入 rdx=input 部分,即输入数字等于 22,且 22 大于 8 且小于 36,所以 22 为答案
59

22 并不是唯一的答案,20 也能通过隐藏关卡