Tip

CASPP 的 LAB 放在 Lab Assignments 上,在 “Self-Study Handout” 出下载对应 LAB 的资源。

课程 Lab 说明:“二进制炸弹” 是一个提供给学生的可执行文件。运行时,它会提示用户输入 6 个不同的字符串。如果这些字符串中有任何一个不正确,炸弹就会 “爆炸” 并打印错误信息。学生必须通过反汇编和逆向自己的炸弹来确定这 6 个字符串应该是什么。这个实验让学生理解汇编语言,并强迫他们学习如何使用调试器(GDB)。这是一个你可以自己尝试的 Linux/x86-64 二进制炸弹。

我们选择的是 “自学” 模块,没有远程服务端检查和扣分机制,可以随意调试。但是,CMU 的同学就会有这些机制,输错了就扣分,所以需要小心调试。

解压 tar 包,得到三个文件 bombbomb.c 和一个 READMEREADME 只说了这是一个 x86-64 程序。

bomb.c 解读

bomb.c 应该就是 bomb 的主代码了。

代码引入了两个自定义库 support.hphases.h,也就是说我们想要的字符串必须从程序中得到。

然后是命令行参数要求,总结如下:

  1. bomb:标准的按行输入
  2. bomb <file>:先从 <file> 中读取输入,结束后再转到标准输入
  3. 不可提供更多命令行参数,否则打印用法

然后就是打印提示信息并要求输入了。整个代码解读完毕,现在转向程序。

bomb 逆向 - GDB

在 Linux 上使用包管理器安装 GDB,使用 gdb bomb 开始调试

基本操作:

bomb 逆向 - IDA

这个 Lab 是需要在 Linux 环境下运行的,不过我们在 Windows 上,所以先用 IDA 分析一下。这里可以用 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
int __fastcall main(int argc, const char **argv)
{
  const char *line; // rax
  const char *v3; // rax
  const char *v4; // rax
  const char *v5; // rax
  const char *v6; // rax
  const char *v7; // rax

  if ( argc == 1 )
  {
    infile = (FILE *)stdin;
  }
  else
  {
    if ( argc != 2 )
    {
      __printf_chk(1, "Usage: %s [<input_file>]\n", *argv);
      exit(8);
    }
    infile = fopen(argv[1], "r");
    if ( !infile )
    {
      __printf_chk(1, "%s: Error: Couldn't open %s\n", *argv, argv[1]);
      exit(8);
    }
  }
  initialize_bomb();
  puts("Welcome to my fiendish little bomb. You have 6 phases with");
  puts("which to blow yourself up. Have a nice day!");
  line = read_line();
  phase_1(line);
  phase_defused();
  puts("Phase 1 defused. How about the next one?");
  v3 = read_line();
  phase_2(v3);
  phase_defused();
  puts("That's number 2.  Keep going!");
  v4 = read_line();
  phase_3(v4);
  phase_defused();
  puts("Halfway there!");
  v5 = read_line();
  phase_4(v5);
  phase_defused();
  puts("So you got that one.  Try this one.");
  v6 = read_line();
  phase_5((__int64)v6);
  phase_defused();
  puts("Good work!  On to the next...");
  v7 = read_line();
  phase_6(v7);
  phase_defused();
  return 0;
}

Phase1

先来看 Phase1,点进 phase_1(line),即可得到内容:

1
result = strings_not_equal(line, "Border relations with Canada have never been better.");

Phase1 就是 Border relations with Canada have never been better.

Phase2

接下来是 Phase2。有了 Phase1 的经验,我们点开 phase_2(v5)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall phase_2(__int64 a1)
{
  __int64 result; // rax
  char *v2; // rbx
  int v3; // [rsp+0h] [rbp-38h] BYREF
  char v4; // [rsp+4h] [rbp-34h] BYREF
  char v5; // [rsp+18h] [rbp-20h] BYREF

  read_six_numbers(a1, &v3);
  if ( v3 != 1 )
    explode_bomb();
  v2 = &v4;
  do
  {
    result = (unsigned int)(2 * *((_DWORD *)v2 - 1));
    if ( *(_DWORD *)v2 != (_DWORD)result )
      explode_bomb();
    v2 += 4;
  }
  while ( v2 != &v5 );
  return result;
}

先看第一个读取函数 read_six_numbers(a1, &v3)

1
2
3
4
5
6
7
8
9
__int64 __fastcall read_six_numbers(__int64 a1, __int64 a2)
{
  __int64 n5; // rax

  n5 = __isoc99_sscanf(a1, "%d %d %d %d %d %d", a2, a2 + 4, a2 + 1, a2 + 12, a2 + 2, a2 + 20);
  if ( (int)n5 <= 5 )
    explode_bomb();
  return n5;
}

核心就是 __isoc99_sscanf。查询函数定义 [1] 可知,这里是从我们输入的 Phase2 中读取格式化的 “%d %d %d %d %d %d” 并写入 v3[i]

我们可以在这里修复一下函数定义,修复后的 phase_2(v5)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __fastcall phase_2(const char *a1)
{
  int result; // eax
  int *v2; // rbx
  int v3[6]; // [rsp+0h] [rbp-38h] BYREF
  int v4; // [rsp+18h] [rbp-20h] BYREF

  read_six_numbers(a1, v3);
  if ( v3[0] != 1 )
    explode_bomb();
  v2 = &v3[1];
  do
  {
    result = 2 * *(v2 - 1);
    if ( *v2 != result )
      explode_bomb();
    ++v2;
  }
  while ( v2 != &v4 );
  return result;
}

可以知道,v3[0] ,即第一个输入值必然是 1v2 对应第二个输入值的地址。

然后进入一个 do-while 循环,循环条件是 v2 不等于 v4地址

循环体的逻辑是计算 2 * v2[i-1] 的值,然后和 v2[i] 进行比较。如果不相等,就引爆炸弹。然后移动到下一个元素。

v4 和我们的输入无关且分配内存时接在 v3 之后,基于内存连续性,我们认为数组 v3 结束于地址 v4

v3 应当是一个初始值为 1,公比为 2 的等比数列,即 1, 2, 4, 8, 16, 32

Phase2 就是 1 2 4 8 16 32

Phase3

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
  if ( (int)__isoc99_sscanf(a1, "%d %d", &v2, &n207_1) <= 1 )
    explode_bomb();
  switch ( v2 )
  {
    case 0:
      n207 = 207;
      break;
    case 1:
      n207 = 311;
      break;
    case 2:
      n207 = 707;
      break;
    case 3:
      n207 = 256;
      break;
    case 4:
      n207 = 389;
      break;
    case 5:
      n207 = 206;
      break;
    case 6:
      n207 = 682;
      break;
    case 7:
      n207 = 327;
      break;
    default:
      explode_bomb();
  }
  if ( (_DWORD)n207 != n207_1 )
    explode_bomb();
  return n207;
}

要求输入的数值对符合 switch-case 定义的 8 种情况,否则炸弹爆炸。

一个合法的 Phase3 示例为 0 207

Phase4

1
2
3
4
5
6
if ( (unsigned int)__isoc99_sscanf(a1, "%d %d", &n0xE, &v3) != 2 || n0xE > 14 )
  explode_bomb();
result = func4(n0xE, 0, 14);
if ( (_DWORD)result || v3 )                   // v3 = 0
  explode_bomb();
return result;

输入的数值对中,第一个输入的值不能大于 14,第二个值必须为 0。

result,即 func 的返回值必须为 0。

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall func4(signed int n0xE, __int64 a2, int n14)
{
  signed int _0xE_1___7; // ecx
  __int64 result; // rax

  _0xE_1___7 = (n14 - (int)a2) / 2 + a2;        // 0xE_1 = 7
  if ( _0xE_1___7 > n0xE )
    return 2 * (unsigned int)func4(n0xE, a2, _0xE_1___7 - 1);
  result = 0;
  if ( _0xE_1___7 < n0xE )
    return 2 * (unsigned int)func4(n0xE, (unsigned int)(_0xE_1___7 + 1), n14) + 1;
  return result;
}

函数的核心逻辑:

  • 计算中点:_0xE_1___7 = (n14 - a2) / 2 + a2,即区间 [a2, n14] 的中间值

  • 如果中点 > 目标值:递归搜索左半区间 [a2, mid-1],返回值乘以 2

  • 如果中点 < 目标值:递归搜索右半区间 [mid+1, n14],返回值乘以 2 再加 1

  • 如果中点 == 目标值:返回 0

由于返回值要求 0,因此我们的目标值就需要一次等于计算出来的中点,即 7。

Phase4 为 7 0

Phase5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned __int64 __fastcall phase_5(const char *line)
{
  __int64 i; // rax
  char line_; // [rsp+10h] [rbp-18h] BYREF
  char v4; // [rsp+16h] [rbp-12h]
  unsigned __int64 v5; // [rsp+18h] [rbp-10h]

  v5 = __readfsqword(0x28u);
  if ( (unsigned int)string_length(line) != 6 )
    explode_bomb();
  for ( i = 0; i != 6; ++i )
    *(&line_ + i) = array_3449[line[i] & 0xF];
  v4 = 0;
  if ( (unsigned int)strings_not_equal(&line_, "flyers") )
    explode_bomb();
  return __readfsqword(0x28u) ^ v5;
}

需要输入长度为 6 的字符串。每个字符和 0xF 相与后在 array_3449 寻找对应下标字符,再回写。最终字符串要等于 flyers

array_3449: maduiersnfotvbyl

Phase5 为 9?>567

Phase6

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
__int64 __fastcall phase_6(const char *a1)
{
  int *filted_input_1; // r13
  int n5_2; // r12d
  int n5; // ebx
  int *filted_input_2; // rax
  unsigned __int64 i; // rsi
  _QWORD *p_node1; // rdx
  int v7; // eax
  int v8; // ecx
  __int64 j_3; // rbx
  char *v10; // rax
  __int64 j; // rcx
  __int64 j_2; // rdx
  int n5_1; // ebp
  __int64 result; // rax
  int filted_input[6]; // [rsp+0h] [rbp-78h] BYREF
  char v16; // [rsp+18h] [rbp-60h] BYREF
  __int64 j_1; // [rsp+20h] [rbp-58h]
  char v18; // [rsp+28h] [rbp-50h] BYREF
  char v19; // [rsp+50h] [rbp-28h] BYREF

  filted_input_1 = filted_input;
  read_six_numbers(a1, filted_input);
  n5_2 = 0;
  while ( 1 )
  {
    if ( (unsigned int)(*filted_input_1 - 1) > 5 )// if(filted_input[i] > 6)
      explode_bomb();
    if ( ++n5_2 == 6 )
      break;
    n5 = n5_2;
    do
    {
      if ( *filted_input_1 == filted_input[n5] )// the input should be diff from each other
        explode_bomb();
      ++n5;
    }
    while ( n5 <= 5 );
    ++filted_input_1;
  }
  filted_input_2 = filted_input;
  do
  {
    *filted_input_2 = 7 - *filted_input_2;      // f_input[i] = 7 - f_input[i]
    ++filted_input_2;
  }
  while ( filted_input_2 != (int *)&v16 );
  for ( i = 0; i != 24; i += 4LL )
  {
    v8 = filted_input[i / 4];
    if ( v8 <= 1 )
    {
      p_node1 = &node1;
    }
    else
    {
      v7 = 1;
      p_node1 = &node1;
      do
      {
        p_node1 = (_QWORD *)p_node1[1];
        ++v7;
      }
      while ( v7 != v8 );
    }
    *(__int64 *)((char *)&j_1 + 2 * i) = (__int64)p_node1;
  }
  j_3 = j_1;
  v10 = &v18;
  for ( j = j_1; ; j = j_2 )
  {
    j_2 = *(_QWORD *)v10;
    *(_QWORD *)(j + 8) = *(_QWORD *)v10;
    v10 += 8;
    if ( v10 == &v19 )
      break;
  }
  *(_QWORD *)(j_2 + 8) = 0;
  n5_1 = 5;
  do
  {
    result = **(unsigned int **)(j_3 + 8);
    if ( *(_DWORD *)j_3 < (int)result )
      explode_bomb();
    j_3 = *(_QWORD *)(j_3 + 8);
    --n5_1;
  }
  while ( n5_1 );
  return result;
}

首先,需要输入 6 个数字,每个数字都不相同,且在 1~6 之间(无符号数,小于 1 就溢出了)。

然后对输入进行变换,将每个数字 x 替换为 7 - x,即原来的 1~6 变成 6~1。

根据变换后的输入选择 node<x>,再将 node<x> 链接成一个链表,检查此链表的节点值是否按降序排序。如果按降序排序,那么原始输入就是合法的。

node 结构大概是:

1
2
3
4
5
struct node {
    int value;
    int id;
    struct node *next;
};

我们可以通过查看内存布局得到各个 node 的具体数据。

排序后发现降序顺序为:node3 → node4 → node5 → node6 → node1 → node2

对应的 id 为:3 4 5 6 1 2

Phase6 为 4 3 2 1 6 5

Secret Phase


  1. C 库函数 int sscanf ↩︎