4.4 GCC 堆栈保护技术

技术简介

Linux 中有各种各样的安全防护,其中 ASLR 是由内核直接提供的,通过系统配置文件控制。NX,Canary,PIE,RELRO 等需要在编译时根据各项参数开启或关闭。未指定参数时,使用默认设置。

CANARY

启用 CANARY 后,函数开始执行的时候会先往栈里插入 canary 信息,当函数返回时验证插入的 canary 是否被修改,如果是,则说明发生了栈溢出,程序停止运行。

下面是一个例子:

#include <stdio.h>
void main(int argc, char **argv) {
    char buf[10];
    scanf("%s", buf);
}

我们先开启 CANARY,来看看执行的结果:

$ gcc -m32 -fstack-protector canary.c -o f.out
$ python -c 'print("A"*20)' | ./f.out
*** stack smashing detected ***: ./f.out terminated
Segmentation fault (core dumped)

接下来关闭 CANARY:

可以看到当开启 CANARY 的时候,提示检测到栈溢出和段错误,而关闭的时候,只有提示段错误。

下面对比一下反汇编代码上的差异:

开启 CANARY 时:

关闭 CANARY 时:

FORTIFY

FORTIFY 的选项 -D_FORTIFY_SOURCE 往往和优化 -O 选项一起使用,以检测缓冲区溢出的问题。

下面是一个简单的例子:

开启优化 -O2 后,编译没有检测出任何问题,checksec 后 FORTIFY 为 No。当配合 -D_FORTIFY_SOURCE=2(也可以 =1)使用时,提示存在溢出问题,checksec 后 FORTIFY 为 Yes。

NX

No-eXecute,表示不可执行,其原理是将数据所在的内存页标识为不可执行,如果程序产生溢出转入执行 shellcode 时,CPU 会抛出异常。

在 Linux 中,当装载器将程序装载进内存空间后,将程序的 .text 段标记为可执行,而其余的数据段(.data、.bss 等)以及栈、堆均为不可执行。因此,传统利用方式中通过修改 GOT 来执行 shellcode 的方式不再可行。

但这种保护并不能阻止攻击者通过代码重用来进行攻击(ret2libc)。

PIE

PIE(Position Independent Executable)需要配合 ASLR 来使用,以达到可执行文件的加载时地址随机化。简单来说,PIE 是编译时随机化,由编译器完成;ASLR 是加载时随机化,由操作系统完成。ASLR 将程序运行时的堆栈以及共享库的加载地址随机化,而 PIE 在编译时将程序编译为位置无关、即程序运行时各个段加载的虚拟地址在装载时确定。开启 PIE 时,编译生成的是动态库文件(Shared object)文件,而关闭 PIE 后生成可执行文件(Executable)。

我们通过实际例子来探索一下 PIE 和 ASLR:

可以看到两者的不同在 TypeEntry point address

首先我们关闭 ASLR,使用 -pie 进行编译:

我们虽然开启了 -pie,但是 ASLR 被关闭,入口地址不变。

可以看出动态链接库地址也不变。然后我们开启 ASLR:

入口地址和动态链接库地址都变得随机。

接下来关闭 ASLR,并使用 -no-pie 进行编译:

入口地址和动态库都是固定的。下面开启 ASLR:

入口地址依然固定,但是动态库变为随机。

所以在分析一个 PIE 开启的二进制文件时,只需要关闭 ASLR,即可使 PIE 和 ASLR 都失效。

ASLR(Address Space Layout Randomization)

关闭:# echo 0 > /proc/sys/kernel/randomize_va_space

部分开启(将 mmap 的基址,stack 和 vdso 页面随机化):# echo 1 > /proc/sys/kernel/randomize_va_space

完全开启(在部分开启的基础上增加 heap的随机化:# echo 2 > /proc/sys/kernel/randomize_va_space

RELRO

RELRO(ReLocation Read-Only)设置符号重定向表为只读或在程序启动时就解析并绑定所有动态符号,从而减少对 GOT(Global Offset Table)的攻击。

RELOR 有两种形式:

  • Partial RELRO:一些段(包括 .dynamic)在初始化后将会被标记为只读。

  • Full RELRO:除了 Partial RELRO,延迟绑定将被禁止,所有的导入符号将在开始时被解析,.got.plt 段会被完全初始化为目标函数的最终地址,并被标记为只读。另外 link_map_dl_runtime_resolve 的地址也不会被装入。

编译参数

各种安全技术的编译参数如下:

安全技术
完全开启
部分开启
关闭

Canary

-fstack-protector-all

-fstack-protector

-fno-stack-protector

NX

-z noexecstack

-z execstack

PIE

-pie

-no-pie

RELRO

-z now

-z lazy

-z norelro

关闭所有保护:

开启所有保护:

  • FORTIFY

    • -D_FORTIFY_SOURCE=1:仅在编译时检测溢出

    • -D_FORTIFY_SOURCE=2:在编译时和运行时检测溢出

保护机制检测

有许多工具可以检测二进制文件所使用的编译器安全技术。下面介绍常用的几种:

checksec

peda 自带的 checksec

地址空间布局随机化

最后再说一下地址空间布局随机化(ASLR),该技术虽然不是由 GCC 编译时提供的,但对 PIE 还是有影响。该技术旨在将程序的内存布局随机化,使得攻击者不能轻易地得到数据区的地址来构造 payload。由于程序的堆栈分配与共享库的装载都是在运行时进行,系统在程序每次执行时,随机地分配程序堆栈的地址以及共享库装载的地址。使得攻击者无法预测自己写入的数据区的虚拟地址。

针对该保护机制的攻击,往往是通过信息泄漏来实现。由于同一模块中的所有代码和数据的相对偏移是固定的,攻击者只要泄漏出某个模块中的任一代码指针或数据指针,即可通过计算得到此模块中任意代码或数据的地址。

Last updated

Was this helpful?