这里主要介绍一下Linux下ELF文件的一些安全防御机制及其原理,其在二进制安全和Pwn上经常会碰到,至于各个类型的绕过技巧后面会补充。

NX(DEP)

NX即No-eXecute(不可执行)的意思,NX(即Windows上类似的DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。

通常开启了NX后,即使有栈溢出漏洞也执行不了写在栈上的shellcode,但是可通过ROP方式来绕过NX跳转至其他地方执行。

gcc编译器默认开启了NX选项,如果需要关闭NX选项,可以给gcc编译器添加-z execstack参数。例如:gcc -z execstack -o test test.c

在Windows下,类似的概念为DEP(数据执行保护),在最新版的Visual Studio中默认开启了DEP编译选项。

原理如图:

NX

网上找的示例图:

NX

PIE(ASLR)

(1)ASLR(Address Space Layout Randomization):地址随机化,通常用来防御return2libc攻击,有以下3种配置:

​ 0 - 表示关闭进程地址空间随机化。

​ 1 - 表示将mmap的基址,stack和vdso页面随机化。

​ 2 - 表示在1的基础上增加栈(heap)的随机化。

(2)PIE(Position-Independent Executables):位置无关的可执行文件,和Windows下的ASLR(Address Space Layout Randomization)机制类似,PIE enabled表示程序开启地址随机化选、意味着程序每次运行的时候地址都会变化。主要是为了解决二进制本身地址已知的问题,可用来防御return2elf和其他已知地址读写问题。

默认情况下,PIE未开启,x86加载的基地址为0x8048000,而x64加载的基址为0x400000。

开启PIE后,elf中相对偏移不变,但加载机制每次均变化。

一般情况下NX(DEP)和PIE(ASLR)会同时工作。

PIE机制有以下三种情况:

​ 0 - 表示关闭进程地址空间随机化。

​ 1 - 表示将mmap的基址,stack和vdso页面随机化。

​ 2 - 表示在1的基础上增加栈(heap)的随机化。

可以防范基于Ret2libc方式的针对DEP的攻击。ASLR和DEP配合使用,能有效阻止攻击者在堆栈上运行恶意代码。

Built as PIE:位置独立的可执行区域PIE。这样使得在利用缓冲溢出和移动操作系统中存在的其他内存崩溃缺陷时采用面向返回的编程ROP(return-oriented programming)方法变得难得多。

liunx下关闭PIE的命令如下:sudo -s echo 0 > /proc/sys/kernel/randomize_va_space

RELRO

RELRO(Relocation Read Only)重定向只读,实现就是由linker指定binary的一块经过dynamic linker处理过 relocation之后的区域为只读,用来防御hijack GOT攻击。通过设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,保护库函数的调用不受攻击者重定位的影响,从而减少对GOT(Global Offset Table)攻击。

动态函数在第一次懒加载过程中,会使用到重定位表格进行符号搜索,并将搜索到的函数信息保存到重定位项中,第二次直接跳转到重定位项中。而攻击者可以修改重定位表格或重定位项来实现hijack GOT表攻击。

RELRO有Partial RELRO和FULL RELRO两个选项,如果开启FULL RELRO,意味着无法修改GOT表;如果为Partial RELRO,说明对GOT表具有写权限。在Linux下默认开启状态。

  • Partial RELRO:重定位表格只读,重定位项可读写;
  • FULL RELRO:重定位表格和重定位项均为只读(但会导致符号懒加载失效,同时会带来启动时的效率下降);

可通过ROP绕过。

FORTIFY

Fortify 技术是GCC在编译源码时判断程序的哪些buffer会存在可能的溢出,在buffer大小已知的情况下,GCC会把 strcpy、memcpy,、memset等函数自动替换成相应的 __strcpy_chk(dst, src, dstlen)等函数,达到防止缓冲区溢出的作用。

FORTIFY_SOURCE机制对格式化字符串有两个限制:

​ (1)包含%n的格式化字符串不能位于程序内存中的可写地址;

​ (2)当使用位置参数时,必须使用范围内的所有参数。例如要使用%4$x,则必须同时使用1、2、3。

GCC中-D_FORTIFY_SOURCE=2是默认开启的,但是只有开启O2或以上优化的时候,这个选项才会被真正激活。

如果指定-D_FORTIFY_SOURCE=1,那同样也要开启O1或以上优化,这个选项才会被真正激活。

可以使用-U_FORTIFY_SOURCE或者-D_FORTIFY_SOURCE=0来禁用。

如果开启了-D_FORTIFY_SOURCE=2,那么调用__printf_chk函数的时候会检查format string中是否存在%n,如果存在%n 而且format string是在一个可写的segment中的(不是在read-only内存段中),那么程序会报错并终止。如果是开启-D_FORTIFY_SOURCE=1,那么就不会报错。

CANARY(Stack)

Stack,栈溢出检查,用Canary是否变化来检测,其中Canary found表示开启。

Canary是一种缓冲区溢出攻击缓解手段:启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux将cookie信息称为Canary。

如图:

在GCC中开启Canary有3中选项:

  • 禁用Canary:gcc -fno-stack-protector -o test test.c
  • 启用Canary,但只为局部变量中含有char数组的函数插入保护代码:gcc -fstack-protector -o test test.c
  • 启用Canary,为所有函数插入保护代码:gcc -fstack-protector-all -o test test.c

如果栈中开启Canary found,那么就不能用直接用溢出的方法覆盖栈中返回地址,而且要通过改写指针与局部变量、leak canary、overwrite canary的方法来绕过。

参考

Pwn基础知识笔记

软件常用安全防护手段 checksec 总结