前言
我们都知道任何软件都有漏洞可以利用,软件又都运行在操作系统上,所以操作系统当然就有义务保护这些程序。本文将简单介绍Linux平台下的一些常见保护机制。
- CANNARY(栈保护)
- FORTIFY
- NX(DEP)
- PIE/PIC与ASLR
- RELRO
cannary
cannary是一项非常古老的栈保护机制,直到现在都是操作系统安全的第一道防线。canary 不管是实现还是设计思想都比较简单高效, 就是插入一个值, 在 stack overflow 发生的 高危区域的尾部, 当函数返回之时检测 canary 的值是否经过了改变, 以此来判断 stack/buffer overflow 是否发生。 这种方法很像windows下的启用GS选项 。
当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary。
开启/关闭
gcc -o test test.c // 默认情况下,开启Canary保护
gcc -fno-stack-protector -o test test.c //禁用栈保护
gcc -fstack-protector -o test test.c //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o test test.c //启用堆栈保护,为所有函数插入保护代码
实现原理
开启 Canary 保护的 stack 结构大概如下
当程序启用 Canary 编译后,在函数序言部分会取 fs 寄存器 0x28 处的值,存放在栈中 %ebp-0x8 的位置。 这个操作即为向栈中插入 Canary 值,代码如下:
mov rax, qword ptr fs:[0x28]
mov qword ptr [rbp - 8], rax
在函数返回之前,会将该值取出,并与 fs:0x28 的值进行异或。如果异或的结果为 0,说明 canary 未被修改,函数会正常返回,这个操作即为检测是否发生栈溢出。
mov rdx,QWORD PTR [rbp-0x8]
xor rdx,QWORD PTR fs:0x28
je 0x4005d7 <main+65>
call 0x400460 <__stack_chk_fail@plt>
如果 canary 已经被非法修改,此时程序流程会走到 __stack_chk_fail
。
绕过技术
-
泄露栈中的 Canary
Canary 设计为以字节
\x00
结尾,本意是为了保证 Canary 可以截断字符串。 泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分。 这种利用方式需要存在合适的输出函数,并且可能需要第一溢出泄露 Canary,之后再次溢出控制执行流程。 -
one-by-one 爆破 Canary
对于 Canary,不仅每次进程重启后的 Canary 不同 (相比 GS,GS 重启后是相同的),而且同一个进程中的每个线程的 Canary 也不同。 但是存在一类通过 fork 函数开启子进程交互的题目,因为 fork 函数会直接拷贝父进程的内存,因此每次创建的子进程的 Canary 是相同的。我们可以利用这样的特点,彻底逐个字节将 Canary 爆破出来。 在著名的 offset2libc 绕过 linux64bit 的所有保护的文章中,作者就是利用这样的方式爆破得到的 Canary: 这是爆破的 Python 代码:
print "[+] Brute forcing stack canary " start = len(p) stop = len(p)+8 while len(p) < stop: for i in xrange(0,256): res = send2server(p + chr(i)) if res != "": p = p + chr(i) #print "\t[+] Byte found 0x%02x" % i break if i == 255: print "[-] Exploit failed" sys.exit(-1) canary = p[stop:start-1:-1].encode("hex") print " [+] SSP value is 0x%s" % canary
-
劫持__stack_chk_fail 函数
已知 Canary 失败的处理逻辑会进入到
__stack_chk_fail
ed 函数,__stack_chk_fail
ed 函数是一个普通的延迟绑定函数,可以通过修改 GOT 表劫持这个函数。 -
覆盖 TLS 中储存的 Canary 值
已知 Canary 储存在 TLS 中,在函数返回前会使用这个值进行对比。当溢出尺寸较大时,可以同时覆盖栈上储存的 Canary 和 TLS 储存的 Canary 实现绕过。
FORTIFY
fortify是非常轻微的检查,用于检查是否存在缓冲区溢出的错误。适用情形是程序采用大量的字符串或者内存操作函数,如memcpy,memset,stpcpy,strcpy,strncpy,strcat,strncat,sprintf,snprintf,vsprintf,vsnprintf,gets以及宽字符的变体。
gcc -D_FORTIFY_SOURCE=1 仅仅只会在编译时进行检查 (特别像某些头文件 #include <string.h>)
gcc -D_FORTIFY_SOURCE=2 程序执行时也会有检查 (如果检查到缓冲区溢出,就终止程序)
NX
NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。
在Windows下,类似的概念为DEP(数据执行保护),在最新版的Visual Studio中默认开启了DEP编译选项。
开启/关闭
gcc -o test test.c // 默认情况下,开启NX保护
gcc -z execstack -o test test.c // 禁用NX保护
gcc -z noexecstack -o test test.c // 开启NX保护
工作原理
PIE/PIC和ASLR
- ASLR全称 Address Space Layout Randomization 地址空间布局随机化
- PIE 全称 Position-Independent Executable 地址无关可执行文件
-
Linux 平台通过 PIE 机制来负责代码段和数据段的随机化工作,而不是 ASLR。 但是只有在开启 ASLR 之后,PIE 才会生效。
- 在 Linux 平台上,堆空间的分配是通过 mmap() 以及 brk() 这两个系统调用完成的。
- 当 ASLR 等级为 1 时,通过 mmap() 分配的堆空间将被随机化,但并不包括 brk() 分配的堆空间
- 在等级为 2 时,通过 brk() 分配的内存空间也将被随机化,即此时堆空间被完全随机化。
因为ASLR技术的出现,攻击者在ROP或者向进程中写数据时不得不先进行leak,或者干脆放弃堆栈,转向bss或者其他地址固定的内存块。而PIE技术就是一个针对代码段.text, 数据段.*data,.bss等固定地址的一个防护技术。同ASLR一样,应用了PIE的程序会在每次加载时都变换加载基址,从而使位于程序本身的gadget也失效。
地址无关代码
为了能够使共享对象在任意地址装载,我们在链接时对所有绝对地址的引用不做重定位,而把这一步推迟到装载时再完成。一旦模块装载地址确定,那么系统就对程序中所有的绝对地址引用进行重定位。这种重定位叫做装载时重定位。
装载时重定位能解决动态模块中的绝对地址引用问题,但是它无法使指令部分在多个进程之间共享,这样就失去了动态链接节省内存的一大优势。
我们希望程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变。所以我们就把指令中那些需要被修改的部分分离出来,跟数据部分放在一起。这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本。这就是目前被称为地址无关代码(PIC,Position-independent Code)的技术。
-fpic和-fPIC
两者都是GCC的参数,用来生成地址无关代码。
-fpic:在一些平台上有硬件相关的限制,但是代码较小、较快。
-fPIC:没有限制。
地址无关可执行文件
同样的技术用在共享对象上就是地址无关代码,而用在可执行文件上,它就是地址无关可执行文件。
一个以地址无关方式编译的可执行文件被称作地址无关可执行文件(PIE,Position-Independent Executable)
-fpie和-fPIE
两者都是GCC的参数,用来生成地址无关代码。
-fpie:在一些平台上有硬件相关的限制,但是代码较小、较快。
-fPIE:没有限制。
开启/关闭
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。
sudo sh -c 'echo 0 > /proc/sys/kernel/randomize_va_space' //关闭
sudo sh -c 'echo 2 > /proc/sys/kernel/randomize_va_space' //开启
gcc编译命令
gcc -o test test.c // 默认情况下,开启PIE
gcc -fpie -pie -o test test.c // 开启PIE,此时强度为1
gcc -fPIE -pie -o test test.c // 开启PIE,此时为最高强度2
gcc -fpic -o test test.c // 开启PIC,此时强度为1,不会开启PIE
gcc -fPIC -o test test.c // 开启PIC,此时为最高强度2,不会开启PIE
RELRO
在Linux系统安全领域数据可以写的存储区就会是攻击的目标,尤其是存储函数指针的区域. 所以在安全防护的角度来说尽量减少可写的存储区域对安全会有极大的好处.
GCC, GNU linker以及Glibc-dynamic linker一起配合实现了一种叫做relro的技术: relocation read only .大概实现就是由linker指定binary的一块经过dynamic linker处理过 relocation之后的区域为只读.
设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。
RELRO为” Partial RELRO”,说明我们对GOT表具有写权限。
开启/关闭
gcc -o test test.c // 默认情况下,是Full RELRO
gcc -z norelro -o test test.c // 关闭,即No RELRO
gcc -z lazy -o test test.c // 部分开启,即Partial RELRO
gcc -z now -o test test.c // 全部开启,即Full RELRO