..
2025-07-29 20:29:54 +08:00
2025-07-29 20:29:54 +08:00
2025-07-31 23:09:17 +08:00
2025-07-31 23:09:17 +08:00
2025-07-29 20:29:54 +08:00
2025-08-01 21:11:21 +08:00

bomblab

输入命令 objdump -s -S -d -M att bomb > bomb.s 进行反汇编。

Phase 1

phase_1 进行逆向:

void phase_1(char str[]) {
  if (strings_not_equal(str, "Border relations with Canada have never been better.") != 0) {
    explode_bomb();
  }
}

explode_bomb 函数将炸弹引爆,我们不希望它被调用;而 strings_not_equal 接受两个字符串作为参数,在两者不相等时返回 1,否则返回 0

显然,输入应为 Border relations with Canada have never been better.

Phase 2

在这个 phase 中,我们看到编译器在处理循环时使用了奇怪的策略,这使得生成的汇编混乱并且难以理解。对此,我们选择出较为混乱的代码,调整指令的顺序并相应改变的跳转,使之符合原书中描述的几种形式。

在这里,我们将 400f0a400f3a 部分整理如下:

  cmpl   $0x1,(%rsp)
  je     .L1
  call   explode_bomb
.L1:
  lea    0x4(%rsp),%rbx
  lea    0x18(%rsp),%rbp
.L2:
  mov    -0x4(%rbx),%eax
  add    %eax,%eax
  cmp    %eax,(%rbx)
  je     .L3
  call   explode_bomb
.L3:
  add    $0x4,%rbx
  cmp    %rbp,%rbx
  jne    .L2

基于此逆向得到:

void phase_2(char str[]) {
  int x[6];
  read_six_numbers(x);

  if (x[0] != 1) {
    explode_bomb();
  } else {
    for (int i = 1; i < 6; i++) {
      if (x[i] != x[i - 1] * 2) {
        explode_bomb();
      }
    }
  }
}

read_six_numbers(int x[]) 读入 6 个整数,并依次存贮到 x[0]x[5]

由此可知,输入应为 1 2 4 8 16 32

Phase 3

这个 phase 包含一个 switch 语句。

void phase_3(char str[]) {
  int x, y;
  if (sscanf(str, "%d %d", &x, &y) <= 1) {
    explode_bomb();
  }
  
  if (x > 7 || x < 0) {
    explode_bomb();
  }
  int z;
  switch (x) {
    case 0:
      z = 0xcf;
      break;
    case 2:
      z = 0x2c3;
      break;
    case 3:
      z = 0x100;
      break;
    case 4:
      z = 0x185;
      break;
    case 5:
      z = 0xce;
      break;
    case 6:
      z = 0x2aa;
      break;
    case 7:
      z = 0x147;
      break;
    default:
      z = 0x137;
  }
  if (y != z) {
    explode_bomb();
  }
}

由此可知,输入的第 1 个数必须在 0 到 7 之间,第二个数与之相应即可。

Phase 4

400fd6400fdd 部分使用了原书 2.3.7 中提到的方法实现除以 2。

int func4(int x, int y, int z) {
  int mid = y + (z - y) / 2;
  if (mid <= x) {
    if (mid >= x) {
      return 0;
    } else {
      return 2 * func4(x, mid + 1, z) + 1;
    }
  } else {
    return 2 * func4(x, y, mid - 1);
  }
}

void phase_4(char str[]) {
  int x, y;
  if (sscanf(str, "%d %d", &x, &y) != 2) {
    explode_bomb();
  }

  if (x > 15 || x < 0) {
    explode_bomb();
  }
  if (func4(x, 0, 15) != 0 || y != 0) {
    explode_bomb();
  }
}

结合线段树知识,输入的第 1 个数可以是 0137,第二个数是 0

Phase 5

40107f4010c6 部分整理如下:

  cmp    $0x6,%eax
  je     .L1
  call   explode_bomb
.L1:
  mov    $0x0,%eax
.L2:
  movzbl (%rbx,%rax,1),%ecx
  mov    %cl,(%rsp)
  mov    (%rsp),%rdx
  and    $0xf,%edx
  movzbl 0x4024b0(%rdx),%edx
  mov    %dl,0x10(%rsp,%rax,1)
  add    $0x1,%rax
  cmp    $0x6,%rax
  jne    .L2
  movb   $0x0,0x16(%rsp)
  mov    $0x40245e,%esi
  lea    0x10(%rsp),%rdi
  call   strings_not_equal
  test   %eax,%eax
  je     .L3
  call   explode_bomb
.L3:

基于此逆向得到:

void phase_5(char str[]) {
  if (string_length(str) != 6) {
    explode_bomb();
  }

  char str2[7];
  for (int i = 0; i < 6; i++) {
    str2[i] = "maduiersnfotvbyl"[str[i] & 0xF];
  }
  str2[6] = 0;

  if (strings_not_equal(str2, "flyers") != 0) {
    explode_bomb();
  }
}

一个可行的输入是 9?>567

Phase 6

4011764011a9 部分整理如下:

  mov    $0x0,%esi
.L1:
  mov    $0x6032d0,%edx
  mov    (%rsp,%rsi,1),%ecx
  cmp    $0x1,%ecx
  jle    .end
  mov    $0x1,%eax
.L2:
  mov    0x8(%rdx),%rdx
  add    $0x1,%eax
  cmp    %ecx,%eax
  jne    .L2
.end:
  mov    %rdx,0x20(%rsp,%rsi,2)
  add    $0x4,%rsi
  cmp    $0x18,%rsi
  jne     .L1

为了理解代码对以 0x6032d0 开头的一段内存的读取与写入,我们还需研究其中数据的组织方式,例如:

 6032d0 4c010000 01000000 e0326000 00000000  L........2`.....

结合汇编代码,不难猜到这块区域依次存储了两个 int 和一个指针,组成一个结构。我们给出它的声明:

struct chain_node {
  int val;
  int id;
  struct chain_node *next;
};

综合以上,逆向得到:

struct chain_node {
  int val;
  int id;
  struct chain_node *next;
} c[6] = {{0x014c, 1, &c[1]}, 
          {0x00a8, 2, &c[2]}, 
          {0x039c, 3, &c[3]}, 
          {0x02b3, 4, &c[4]}, 
          {0x01dd, 5, &c[5]},
          {0x01bb, 6}};

void phase_6(char str[]) {
  int x[6];
  read_six_numbers(x);
  
  for (int i = 0; ; i++) {
    if (x[i] <= 0 || x[i] > 6) {
      explode_bomb();
    }
    if (i == 5) {
      break;
    }
    for (int j = i + 1; j < 6; j++) {
      if (x[i] == x[j]) {
        explode_bomb();
      }
    }
  }

  for (int i = 0; i < 6; i++) {
    x[i] = 7 - x[i];
  }

  struct chain_node *y[6];
  for (int i = 0; i < 6; i++) {
    chain_node *p = c[0];
    for (int j = 1; j < x[i]; j++) {
      p = p->next;
    }
    y[i] = p;
  }

  for (int i = 1; i < 6; i++) {
    y[i - 1]->next = y[i];
  }
  y[5]->next = NULL;

  chain_node *p = y[0];
  for (int i = 5; i > 0; i--) {
    if (p->val < p->next->val) {
      explode_bomb();
    }
    p = p->next;
  }
}

结合链表知识,输入应为 4 3 2 1 6 5

进入 Secret Phase

唯一对 secret_phase 的调用位于 phase_defused 中。观察 phase_defused,在 num_input_strings(每次调用 read_line 都会使它加 1等于 6即完成 Phase 6 后的调用时,该函数从以 0x603870 开头的字符串中先后提取了两个整数和一个字符串,并检查提取的字符串是否与 DrEvil 相等,若相等,则调用 secret_phase

0x603870 这个地址并没有在其他任何地方出现过,但是在 skipread_line 中,出现了地址 0x603780。对这两个函数逆向,大致如下:

char input[MAXLEN]; // 0x603780
int num_input_strings = 0;
FILE *infile;

int skip() {
  fgets(input + 90 * num_input_strings, 90, infile);
  ...
}

char* read_line() {
  ...
  char *start = input + 90 * num_input_strings;
  ...
  num_input_strings++;
  ...
  return start;
}

由此可知,以 0x603870 开头的字符串就是 phase 4 时输入的字符串,而在 phase 4 中恰好要输入两个整数,在它们之后再输入 MrEvil,我们就能够进入 Secret Phase。

Secret Phase

struct tree_node {
  int val;
  struct tree_node *ls, *rs;
  long place_holder;
} t[15] = {{0x024, &t[1], &t[2]},
           {0x008, &t[5], &t[3]},
           {0x032, &t[4], &t[6]},
           {0x016, &t[12], &t[10]},
           {0x02d, &t[7], &t[13]},
           {0x006, &t[8], &t[11]},
           {0x06b, &t[9], &t[14]},
           {0x028, NULL, NULL},
           {0x001, NULL, NULL},
           {0x063, NULL, NULL},
           {0x023, NULL, NULL},
           {0x007, NULL, NULL},
           {0x014, NULL, NULL},
           {0x02f, NULL, NULL},
           {0x3e9, NULL, NULL}};

int fun7(struct tree_node *x, int y) {
  if (x == NULL) {
    return -1;
  } else {
    if (x->val <= y) {
      if (x->val == y) {
        return 0;
      } else {
        return 2 * fun7(x->rs, y) + 1;
      }
    } else {
      return 2 * fun7(x->ls, y);
    }
  }
}

void secret_phase() {
  long x = strtol(read_line(), NULL, 10);

  if (x > 1001 || x < 1) {
    explode_bomb();
  }

  if (fun7(t, x) != 2) {
    explode_bomb();
  }
  ...
}

结合二叉搜索树知识,输入应为 22