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:

$ gcc -m32 -fno-stack-protector canary.c -o fno.out
$ python -c 'print("A"*20)' | ./fno.out
Segmentation fault (core dumped)

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

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

开启 CANARY 时:

gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x000005ad <+0>:	lea    ecx,[esp+0x4]
   0x000005b1 <+4>:	and    esp,0xfffffff0
   0x000005b4 <+7>:	push   DWORD PTR [ecx-0x4]
   0x000005b7 <+10>:	push   ebp
   0x000005b8 <+11>:	mov    ebp,esp
   0x000005ba <+13>:	push   ebx
   0x000005bb <+14>:	push   ecx
   0x000005bc <+15>:	sub    esp,0x20
   0x000005bf <+18>:	call   0x611 <__x86.get_pc_thunk.ax>
   0x000005c4 <+23>:	add    eax,0x1a3c
   0x000005c9 <+28>:	mov    edx,ecx
   0x000005cb <+30>:	mov    edx,DWORD PTR [edx+0x4]
   0x000005ce <+33>:	mov    DWORD PTR [ebp-0x1c],edx
   0x000005d1 <+36>:	mov    ecx,DWORD PTR gs:0x14				; 将 canary 值存入 ecx
   0x000005d8 <+43>:	mov    DWORD PTR [ebp-0xc],ecx			; 在栈 ebp-0xc 处插入 canary
   0x000005db <+46>:	xor    ecx,ecx
   0x000005dd <+48>:	sub    esp,0x8
   0x000005e0 <+51>:	lea    edx,[ebp-0x16]
   0x000005e3 <+54>:	push   edx
   0x000005e4 <+55>:	lea    edx,[eax-0x1940]
   0x000005ea <+61>:	push   edx
   0x000005eb <+62>:	mov    ebx,eax
   0x000005ed <+64>:	call   0x450 <__isoc99_scanf@plt>
   0x000005f2 <+69>:	add    esp,0x10
   0x000005f5 <+72>:	nop
   0x000005f6 <+73>:	mov    eax,DWORD PTR [ebp-0xc]				; 从栈中取出 canary
   0x000005f9 <+76>:	xor    eax,DWORD PTR gs:0x14					; 检测 canary 值
   0x00000600 <+83>:	je     0x607 <main+90>
   0x00000602 <+85>:	call   0x690 <__stack_chk_fail_local>
   0x00000607 <+90>:	lea    esp,[ebp-0x8]
   0x0000060a <+93>:	pop    ecx
   0x0000060b <+94>:	pop    ebx
   0x0000060c <+95>:	pop    ebp
   0x0000060d <+96>:	lea    esp,[ecx-0x4]
   0x00000610 <+99>:	ret
End of assembler dump.

关闭 CANARY 时:

gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x0000055d <+0>:	lea    ecx,[esp+0x4]
   0x00000561 <+4>:	and    esp,0xfffffff0
   0x00000564 <+7>:	push   DWORD PTR [ecx-0x4]
   0x00000567 <+10>:	push   ebp
   0x00000568 <+11>:	mov    ebp,esp
   0x0000056a <+13>:	push   ebx
   0x0000056b <+14>:	push   ecx
   0x0000056c <+15>:	sub    esp,0x10
   0x0000056f <+18>:	call   0x59c <__x86.get_pc_thunk.ax>
   0x00000574 <+23>:	add    eax,0x1a8c
   0x00000579 <+28>:	sub    esp,0x8
   0x0000057c <+31>:	lea    edx,[ebp-0x12]
   0x0000057f <+34>:	push   edx
   0x00000580 <+35>:	lea    edx,[eax-0x19e0]
   0x00000586 <+41>:	push   edx
   0x00000587 <+42>:	mov    ebx,eax
   0x00000589 <+44>:	call   0x400 <__isoc99_scanf@plt>
   0x0000058e <+49>:	add    esp,0x10
   0x00000591 <+52>:	nop
   0x00000592 <+53>:	lea    esp,[ebp-0x8]
   0x00000595 <+56>:	pop    ecx
   0x00000596 <+57>:	pop    ebx
   0x00000597 <+58>:	pop    ebp
   0x00000598 <+59>:	lea    esp,[ecx-0x4]
   0x0000059b <+62>:	ret
End of assembler dump.

FORTIFY

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

下面是一个简单的例子:

#include<string.h>
void main() {
    char str[3];
    strcpy(str, "abcde");
}
$ gcc -O2 fortify.c
$ checksec --file a.out
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH  FORTIFY Fortified Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   No  0 0 a.out

$ gcc -O2 -D_FORTIFY_SOURCE=2 fortify.c
In file included from /usr/include/string.h:639:0,
                 from fortify.c:1:
In function ‘strcpy’,
    inlined from ‘main’ at fortify.c:4:2:
/usr/include/bits/string3.h:109:10: warning: ‘__builtin___memcpy_chk’ writing 6 bytes into a region of size 3 overflows the destination [-Wstringop-overflow=]
   return __builtin___strcpy_chk (__dest, __src, __bos (__dest));
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ checksec --file a.out
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH  FORTIFY Fortified Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   Yes 2 2 a.out

开启优化 -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:

#include<stdio.h>
void main() {
    printf("%p\n", main);
}
$ gcc -m32 -pie random.c -o open-pie
$ readelf -h open-pie
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x400
  Start of program headers:          52 (bytes into file)
  Start of section headers:          6132 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         9
  Size of section headers:           40 (bytes)
  Number of section headers:         30
  Section header string table index: 29
$ gcc -m32 -no-pie random.c -o close-pie
$ readelf -h close-pie
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8048310
  Start of program headers:          52 (bytes into file)
  Start of section headers:          5964 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         9
  Size of section headers:           40 (bytes)
  Number of section headers:         30
  Section header string table index: 29

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

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

# echo 0 > /proc/sys/kernel/randomize_va_space
# gcc -m32 -pie random.c -o a.out
# checksec --file a.out
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	FORTIFY	Fortified Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   No	0		2	a.out

# ./a.out
0x5655553d
# ./a.out
0x5655553d

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

# ldd a.out
  linux-gate.so.1 (0xf7fd7000)
  libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
  /lib/ld-linux.so.2 (0xf7fd9000)
# ldd a.out
  linux-gate.so.1 (0xf7fd7000)
  libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
  /lib/ld-linux.so.2 (0xf7fd9000)

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

# echo 2 > /proc/sys/kernel/randomize_va_space
# ./a.out
0x5665353d
# ./a.out
0x5659753d
# ldd a.out
  linux-gate.so.1 (0xf7727000)
  libc.so.6 => /usr/lib32/libc.so.6 (0xf7529000)
  /lib/ld-linux.so.2 (0xf7729000)
# ldd a.out
  linux-gate.so.1 (0xf77d6000)
  libc.so.6 => /usr/lib32/libc.so.6 (0xf75d8000)
  /lib/ld-linux.so.2 (0xf77d8000)

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

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

# echo 0 > /proc/sys/kernel/randomize_va_space
# gcc -m32 -no-pie random.c -o b.out
# checksec --file b.out
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH  FORTIFY Fortified Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   No  0   2 b.out

# ./b.out
0x8048406
# ./b.out
0x8048406
# ldd b.out
  linux-gate.so.1 (0xf7fd7000)
  libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
  /lib/ld-linux.so.2 (0xf7fd9000)
# ldd b.out
  linux-gate.so.1 (0xf7fd7000)
  libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
  /lib/ld-linux.so.2 (0xf7fd9000)

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

# echo 2 > /proc/sys/kernel/randomize_va_space
# ./b.out
0x8048406
# ./b.out
0x8048406
# ldd b.out
  linux-gate.so.1 (0xf7797000)
  libc.so.6 => /usr/lib32/libc.so.6 (0xf7599000)
  /lib/ld-linux.so.2 (0xf7799000)
# ldd b.out
  linux-gate.so.1 (0xf770a000)
  libc.so.6 => /usr/lib32/libc.so.6 (0xf750c000)
  /lib/ld-linux.so.2 (0xf770c000)

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

所以在分析一个 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 的地址也不会被装入。

编译参数

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

关闭所有保护:

gcc hello.c -o hello -fno-stack-protector -z execstack -no-pie -z norelro

开启所有保护:

gcc hello.c -o hello -fstack-protector-all -z noexecstack -pie -z now
  • FORTIFY

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

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

保护机制检测

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

checksec

$ checksec --file /bin/ls
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	FORTIFY	Fortified Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   Yes	5		15	/bin/ls

peda 自带的 checksec

$ gdb /bin/ls
gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : ENABLED
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

地址空间布局随机化

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

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

Last updated