ctf pwn中的缓解机制及其原理

不可执行位 NX Bit

NX(Non-eXecute)位是一种针对 shellcode 执行攻击的保护措施, 意在更有效地识别数据区和代码区。该技术在 1997 年时被首次提出, 2004 年之后广泛应用于 Linux 与 Windows 操作系统上。众所周知, CPU 无法识别内存中的“数据”是指令还是用户数据, 如果 CPU 的 EIP 指向了攻击者控制的区域, 攻击者可以在此部署自己的 shellcode 从而控制程序的行为。而 NX 技术旨在更好地区分“数据”和“代码”.
通过在内存页的标识中增加“执行”位, 可以表示该内存页是否可以执行, 若程序代码的 EIP 执行至不可运行的内存页, 则 CPU 将直接拒绝执行“指令”造成程序崩溃。
在Linux 中, 当装载器把程序装载进内存空间后, 将程序的.text 段标记为可执行, 而其余的数据段(.data, .bss 等)以及栈、堆均不可执行。如此一来, 当攻击者在堆栈上部署自己的 shellcode 并触发时, 只会直接造成程序的崩溃。
但是我们可以注意到, 尽管攻击者无法通过代码注入攻击进行利用, 其仍然无法阻止攻击者通过控制函数指针来劫持控制流的方法。而攻击者可以使用流行的代码重用攻击, 使用现有代码来进一步构造自身所需控制流, 进而完成利用。

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

地址空间布局随机化ASLR

ASLR(Address Space Layout Randomization, 地址空间布局随机化)技术意在将程序的内存布局随机化, 使得攻击者不能轻易地得到数据区的地址来构造攻击载荷。由于程序的堆、栈分配与共享库的装载都是在运行时进行, 系统在程序每次执行时, 随机地分配程序堆栈的地址以及共享库装载的地址。尽管它们之间的相对位置没有改变, 但每次执行的差异仍然是页级的, 攻击者将无法预测自己写入的数据区的确切虚拟地址。
在 32 位系统中由于随机化的位数较少, 一个常用的绕过手段是通过枚举的方式, 猜测相关代码或者数据的位置, 并通过多次发送攻击载荷来不断触发程序运行。而在 64 位系统中, 因随机化位数较多, 使得通过枚举方式来猜测攻击的手段亦不可行。
但由于目前广泛应用在操作系统的地址随机化多为粗粒度的实现方式, 同一模块中的所有代码与数据的相对偏移固定。攻击者只需要通过信息泄露漏洞将某个模块中的任一代码指针或者数据指针泄露, 即可通过计算得出此模块中任意代码或者数据的地址。
内存地址随机化机制(address space layout randomization),有以下三种情况

1
2
3
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。

liunx下关闭ASLR的命令如下,root运行
sudo echo 0 > /proc/sys/kernel/randomize_va_space

位置无关可执行文件 PIE

PIE(Position-Independent Executable, 位置无关可执行文件)技术与 ASLR 技术类似,ASLR 将程序运行时的堆栈以及共享库的加载地址随机化, 而 PIE 技术则在编译时将程序编译为位置无关, 即程序运行时各个段(如代码段等)加载的虚拟地址也是在装载时才确定。这就意味着, 在 PIE 和 ASLR 同时开启的情况下, 攻击者将对程序的内存布局一无所知, 传统的改写
GOT 表项的方法也难以进行, 因为攻击者不能获得程序的.got 段的虚地址。
使用 PIE 技术会很大程度上影响程序和系统的性能, 因此在 Linux 系统中, 除了关键的系统程序以及共享库使用了位置无关技术外, 大部分程序在编译时都直接确定各段加载的虚地址。

重定位只读 RELRO

RELRO(RELocation Read-Only, 重定位只读), 此项技术主要针对 GOT 改写的攻击方式。分为部分RELRO(Partial RELRO)与完全 RELRO(Full RELRO) 两种。

  • 部分 RELRO: 在程序装入后, 将其中一些段(如.dynamic)标记为只读, 防止程序的一些重定位信息被修改。
  • 完全 RELRO: 在部分 RELRO 的基础上, 在程序装入时, 直接解析完所有符号并填入对应的值, 此时所有的 GOT 表项都已初始化, 且不装入link_map与_dl_runtime_resolve的地址(二者都是程序动态装载的重要结构和函数)。

可以看到, 当程序启用完全 RELRO 时, 传统的 GOT 劫持的方式也不再可用。但完全 RELRO 对程序性能的影响也相对较大, 因为其相当于禁用了用于性能优化的动态装载机制, 将程序中可能不会用到的一些动态符号装入, 当程序导入的外部符号很多时, 将带来一定程度的额外开销。

对于攻击者来说, 劫持程序本身 GOT 的利用方式仅仅是破坏函数指针中的一种, 由于系统库大多没有应用重定位只读技术, 结合之前的信息泄露技术, 劫持其他模块中的 GOT 与其他函数指针也是攻击者的手段之一。

canary

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

1
2
3
cc -fno-stack-protector -o test test.c  //禁用栈保护
gcc -fstack-protector -o test test.c //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o test test.c //启用堆栈保护,为所有函数插入保护代码

参考文章

http://yunnigu.dropsec.xyz/2016/10/08/checksec%E5%8F%8A%E5%85%B6%E5%8C%85%E5%90%AB%E7%9A%84%E4%BF%9D%E6%8A%A4%E6%9C%BA%E5%88%B6

http://jcs.iie.ac.cn/ch/reader/create_pdf.aspx?file_no=20180101&year_id=2018&quarter_id=1&falg=1