The Art of Exploitation

2016/05/10

Categories: Linux Tags: exploitation

1. 栈保护

在函数内有这样一段汇编:

0x000000000040055d <+0>:	push   rbp
0x000000000040055e <+1>:	mov    rbp,rsp
0x0000000000400561 <+4>:	sub    rsp,0x50
.
.
.
0x0000000000400575 <+24>:	mov    rax,QWORD PTR fs:0x28
0x000000000040057e <+33>:	mov    QWORD PTR [rbp-0x8],rax
.
.
.
0x000000000040058f <+50>:	mov    rax,QWORD PTR [rbp-0x8]
0x0000000000400593 <+54>:	xor    rax,QWORD PTR fs:0x28
0x000000000040059c <+63>:	je     0x4005a3 <test_function+70>
0x000000000040059e <+65>:	call   0x400440 <__stack_chk_fail@plt>
0x00000000004005a3 <+70>:	leave  
0x00000000004005a4 <+71>:	ret  

这段代码咋看不懂,实际上是这样的:

中间部分是在函数开始时取 QWORD PTR fs:0x28 一个随机值 canary ,然后赋值到 QWORD PTR [rbp-0x8],
后半部分则是在函数退出前,取 QWORD PTR [rbp-0x8] 与 QWORD PTR fs:0x28 比较是否相等,相等则正常退出,不等则报错。

.
.
|   return address |
|   rbp            |
.
.
|    canary        |
.
.
|    lacal variable |
.
.

这段代码的实际作用是检查栈是否溢出。函数内局部变量居于栈的高地址,寄存器 rbp 保存着return address,在函数栈顶,是低地址。
在栈溢出漏洞中,常利用函数内局部变量插入 shellcode,从该局部变量地址一直覆盖到栈顶,写入合法的 return address 来进行提权。
为了检测这种情况而避免提权,gcc 编译器在编译时设定,函数内从栈顶开始保留部分栈空间,在这部分空间的操作就如之上所述进行。
这里重要假设是,这部分保留空间在栈溢出漏洞利用时肯定会被其它数据覆盖,这样一旦其中数据被发现被篡改了,就一定是栈被破坏了。
这个做法确定也是显而易见的,每个函数都要增加这6、7条指令,还需要保留一定的栈空间,同时只能保护函数的 return address。

gcc 的 stack protector 还做了一件事,将函数内局部变量数组放在高地址,其它局部变量放在低地址。这样当数组溢出时不会影响到低地址的变量。