CSAPP Lab:Bomb Lab

发布于 2022-12-24  46 次阅读


在勉强读完第四章后(流水线后面的HCL细节实现都略过没看了),决定开始尝试传说中的lab,datalab与进制表示的骚操作相关,有点无趣,于是直接开始lab2 bomb(二进制炸弹实验)

Bomb Lab 介绍

这个 lab 给了一个名为 bomb 的程序文件,还有一个名为 bomb.c 的文件是题目要求和 bomb 实现的代码框架,代码是不完整的,因此无法编译。题目要求是运行 bomb 后输入六个 phase ,输入正确 bomb 程序才能继续运行,输入错误就会 bomb!

实验材料可以从这里获取,找到 Self-Study Handout 就可以下载材料了,而 Writeup 并不是题解,是实验的要求说明

📦bomb.tar

📄bomblab.pdf

下载后:

tar -xvf bomb.tar
cd bomb/

image-20221223195605340

可以随便玩玩炸弹(bushi),根据材料可以知道 CMU 的学生是在实验机房完成实验,并且每引爆一次炸弹就会在实验成绩上 -0.5 (上限是20),从这个角度上讲,也就是说我们做实验最好也少触发炸弹

首先是基本操作:

objdump -d ./bomb > ./bomb.asm

image-20221224143157130

此外,接下来实验会多次使用到 GDB 调试工具,附上 CSAPP 的 GDB 学习材料(笔者接下来也是从 0 开始尝试使用 GDB)

第一关

phase_1 很 easy,直接 IDA F5 出结果

image-20221224143706585

也可以尝试使用 GDB,

image-20221223203100636

rdi 是传入 phase_1 的第一个参数,也是传入 string_not_equal 的第一个参数,因此这段汇编里面没有使用 rdi,而 esi 则是传入 string_not_equal 的第二个参数,因此我们去读 esi 指向的地址,就得到了想要的字符串

image-20221223204234673

第二关

第二关就嗯读汇编了,IDA 逆出来的也看不懂,read_six_numbers 提示需要读入 6 个数

image-20221223204859156

读入后,把 (rsp)0x1 比较,必须相等才不会引爆炸弹,之后 rbp 赋值为 rsp + 18rbxrsp + 4add eax, eax 操作会使每次的数翻倍,rbx 每次 +4 后移。其实就循环,要求 rsp 里面存的数必须依次是 1, 2, 4, 8, 16, 32

image-20221223213207960

一开始我尝试的输入是 32, 16, 8, 4, 2, 1, GDB 观察发现反了,再换成 1, 2, 4, 8, 16, 32,在 GDB 调试的时候却死活告诉我只读入了 5 个数,懵逼

无奈之下,只好把输入文件的数改成 1, 2, 4, 8, 16, 32, 64,在 GDB 下提示通过了关卡2。

(后来根据经验我发现如果使用文件输入的话,最后一个字符后需要加一个换行,否则可能读不进去,不懂为啥)

image-20221223214439729

芜湖,终于解出第二关,GDB 搞了好久

第三关

第3关应该是个跳表,switch 结构,IDA F5 后如下:

__int64 __fastcall phase_3(__int64 a1)
{
  __int64 result; // rax
  int v2; // [rsp+8h] [rbp-10h] BYREF
  int v3; // [rsp+Ch] [rbp-Ch] BYREF

  if ( (int)__isoc99_sscanf(a1, "%d %d", &v2, &v3) <= 1 )
    explode_bomb();
  switch ( v2 )
  {
    case 0:
      result = 207LL;
      break;
    case 1:
      result = 311LL;
      break;
    case 2:
      result = 707LL;
      break;
    case 3:
      result = 256LL;
      break;
    case 4:
      result = 389LL;
      break;
    case 5:
      result = 206LL;
      break;
    case 6:
      result = 682LL;
      break;
    case 7:
      result = 327LL;
      break;
    default:
      explode_bomb();
  }
  if ( (_DWORD)result != v3 )
    explode_bomb();
  return result;
}

其实从上面已经可以看出答案了

分析汇编,可以知道需要输入2个数,并且要求输入的第一个数不能大于7,于是先尝试输入

1 255

然后调试观察调到哪里

(这里好像就是因为末尾没有换行,所以输入的255变成了25,怪)

image-20221223223609932

观察到往 eax 里面写入值后,就把值打印出来,可以发现,它需要的值是 311

image-20221223223911797

输入 1 311,成功通过了第三关

第四关

第四关是递归函数,顿时头皮发麻,IDA F5 后如下:

__int64 __fastcall phase_4(__int64 a1)
{
  __int64 result; // rax
  unsigned int v2; // [rsp+8h] [rbp-10h] BYREF
  int v3; // [rsp+Ch] [rbp-Ch] BYREF

  if ( (unsigned int)__isoc99_sscanf(a1, "%d %d", &v2, &v3) != 2 || v2 > 0xE )
    explode_bomb();
  result = func4(v2, 0LL, 14LL);
  if ( (_DWORD)result || v3 )
    explode_bomb();
  return result;
}

汇编代码如下:

000000000040100c <phase_4>:
  40100c:   48 83 ec 18             sub    $0x18,%rsp
  401010:   48 8d 4c 24 0c          lea    0xc(%rsp),%rcx
  401015:   48 8d 54 24 08          lea    0x8(%rsp),%rdx
  40101a:   be cf 25 40 00          mov    $0x4025cf,%esi
  40101f:   b8 00 00 00 00          mov    $0x0,%eax
  401024:   e8 c7 fb ff ff          call   400bf0 <__isoc99_sscanf@plt>
  401029:   83 f8 02                cmp    $0x2,%eax
  40102c:   75 07                   jne    401035 <phase_4+0x29>
  40102e:   83 7c 24 08 0e          cmpl   $0xe,0x8(%rsp)
  401033:   76 05                   jbe    40103a <phase_4+0x2e>
  401035:   e8 00 04 00 00          call   40143a <explode_bomb>
  40103a:   ba 0e 00 00 00          mov    $0xe,%edx
  40103f:   be 00 00 00 00          mov    $0x0,%esi
  401044:   8b 7c 24 08             mov    0x8(%rsp),%edi
  401048:   e8 81 ff ff ff          call   400fce <func4>
  40104d:   85 c0                   test   %eax,%eax
  40104f:   75 07                   jne    401058 <phase_4+0x4c>
  401051:   83 7c 24 0c 00          cmpl   $0x0,0xc(%rsp)
  401056:   74 05                   je     40105d <phase_4+0x51>
  401058:   e8 dd 03 00 00          call   40143a <explode_bomb>
  40105d:   48 83 c4 18             add    $0x18,%rsp
  401061:   c3                      ret  

无论是分析 IDA 的 c代码还是汇编代码,都可以知道,需要读入两个数,第2个数不能大于 0xE,然后调用 func4(v1, 0, 14),传入参数在汇编里写得很明白

image-20221224151510832

调用的 func4 是递归函数,IDA F5后,改了点变量名后结果如下:

image-20221223233920013

汇编的最后告诉我们最后返回值一定得为0,并且读入的第二个数也得是0,于是 7 0 便是一组可行解

image-20221223233703415

第五关

第五关我做的最顺利,IDA F5 后改了点变量名,得到如下:

unsigned __int64 __fastcall phase_5(char *input)
{
  __int64 i; // rax
  char v3[8]; // [rsp+10h] [rbp-18h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-10h]

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

逻辑很清晰,把输入的字符串 input[i] & 0xF 得到 array_3449 的下标,最后要求 v3flyersarray_3449 如下:

image-20221224115328758

对照着顺序写了个简单的 exp.c,用于看 ASCII 码的低 16 位

image-20221224115402400

草稿纸上就是 flyers ---> 9FE567 ---> ionefg 的过程,尝试解出的答案

Nice!!!一次通过 phase_5

image-20221224114743558

第六关

第六关是 Boss,涉及到链表、排序、结构体,太难了,看了一些题解也是比较模糊,汇编代码也很长,下面是 IDA F5 后尽力得到的结果,从 67 行开始的内容就不太明白了:

__int64 __fastcall phase_6(char *input)
{
  int *a; // r13
  int cnt; // er12
  int v3; // ebx
  int *c; // rax
  unsigned __int64 i; // rsi
  _QWORD *v6; // rdx
  int v7; // eax
  int v8; // ecx
  __int64 v9; // rbx
  char *v10; // rax
  __int64 j; // rcx
  __int64 v12; // rdx
  int v13; // ebp
  __int64 result; // rax
  int b[6]; // [rsp+0h] [rbp-78h] BYREF
  char v16; // [rsp+18h] [rbp-60h] BYREF
  __int64 v17; // [rsp+20h] [rbp-58h]
  char v18; // [rsp+28h] [rbp-50h] BYREF
  char v19; // [rsp+50h] [rbp-28h] BYREF

  a = b;
  read_six_numbers((__int64)input, (__int64)b);
  cnt = 0;
  while ( 1 )                                   // 读入的b[6]要求两两不同,且<=6
  {
    if ( (unsigned int)(*a - 1) > 5 )
      explode_bomb();
    if ( ++cnt == 6 )
      break;
    v3 = cnt;
    do
    {
      if ( *a == b[v3] )
        explode_bomb();
      ++v3;
    }
    while ( v3 <= 5 );
    ++a;
  }
  c = b;
  do
  {
    *c = 7 - *c;                                // b[i] = 7 - b[i]
    ++c;
  }
  while ( c != (int *)&v16 );
  for ( i = 0LL; i != 24; i += 4LL )
  {
    v8 = b[i / 4];
    if ( v8 <= 1 )
    {
      v6 = &node1;
    }
    else
    {
      v7 = 1;
      v6 = &node1;
      do
      {
        v6 = (_QWORD *)v6[1];
        ++v7;
      }
      while ( v7 != v8 );
    }
    *(__int64 *)((char *)&v17 + 2 * i) = (__int64)v6;
  }
  v9 = v17;
  v10 = &v18;
  for ( j = v17; ; j = v12 )
  {
    v12 = *(_QWORD *)v10;
    *(_QWORD *)(j + 8) = *(_QWORD *)v10;
    v10 += 8;
    if ( v10 == &v19 )
      break;
  }
  *(_QWORD *)(v12 + 8) = 0LL;
  v13 = 5;
  do
  {
    result = **(unsigned int **)(v9 + 8);
    if ( *(_DWORD *)v9 < (int)result )
      explode_bomb();
    v9 = *(_QWORD *)(v9 + 8);
    --v13;
  }
  while ( v13 );
  return result;
}

大概是读入6个数,要求是6的排列,然后每个数 x 变成 6 - x

image-20221224134639986

image-20221224140607063

然后进行了一通我不想看的排序等操作,最后要求链表从大到小排序,上面查看了链表里面的值,从大到小是:

3 4 5 6 1 2

再 7 - x 一下,得到答案

4 3 2 1 6 5

至此,成功通过了实验

image-20221224140708361

实验的最后结果 ,一种可能的input.txt 如下:

Border relations with Canada have never been better.
1 2 4 8 16 32
1 311
7 0
ionefg
4 3 2 1 6 5

小结

  • 感觉文件末尾需要多一个换行,否则读不到数
  • IDA技术不够,虽然翻了一下IDA的使用技巧和手册还杯水车薪,不过庆幸的是 IDA 里面的很多“单词”大概能看懂了,比学 x86-64 汇编之前好很多
  • 使用 GDB 可以在没有源代码的情况下动态调试,像跳表、结构体里面的内容等好像不调试你也就看不到。IDA据说也有动态调试功能,可惜我还不会
  • 还有个彩蛋,隐藏关卡,以后再说吧

参考资料

https://wdxtub.com/csapp/thick-csapp-lab-2/2016/04/16/

https://github.com/LRL52/CSAPP-Labs/blob/master/notes/bomb.md

https://zhuanlan.zhihu.com/p/28422249


昨日的月光悄然退场,曦阳已经渐亮