Security

2021-01-31, updated 2021-09-12

环境

debian: 10
gcc: 8.3.0

技术简介

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

CANARY

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

下面是一个例子:

#include <stdio.h>

int main() {
    char buf[10];
    scanf("%s", buf);
    return 0;
}

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

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

接下来关闭 CANARY:

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

可以看到当开启 CANARY 的时候,提示检测到栈溢出和段错误,而关闭的时候,只有提示段错误。 下面对比一下反汇编代码上的差异: 开启 CANARY 时:

(gdb) disassemble main
Dump of assembler code for function main:
   0x0000000000001145 <+0>:     push   %rbp
   0x0000000000001146 <+1>:     mov    %rsp,%rbp
   0x0000000000001149 <+4>:     sub    $0x20,%rsp
   0x000000000000114d <+8>:     mov    %fs:0x28,%rax            ;
   0x0000000000001156 <+17>:    mov    %rax,-0x8(%rbp)          ;
   0x000000000000115a <+21>:    xor    %eax,%eax                ; 
   0x000000000000115c <+23>:    lea    -0x12(%rbp),%rax
   0x0000000000001160 <+27>:    mov    %rax,%rsi
   0x0000000000001163 <+30>:    lea    0xe9a(%rip),%rdi        # 0x2004
   0x000000000000116a <+37>:    mov    $0x0,%eax
   0x000000000000116f <+42>:    callq  0x1040 <[email protected]>
   0x0000000000001174 <+47>:    mov    $0x0,%eax
   0x0000000000001179 <+52>:    mov    -0x8(%rbp),%rdx
   0x000000000000117d <+56>:    xor    %fs:0x28,%rdx
   0x0000000000001186 <+65>:    je     0x118d <main+72>
   0x0000000000001188 <+67>:    callq  0x1030 <[email protected]>
   0x000000000000118d <+72>:    leaveq 
   0x000000000000118e <+73>:    retq   
End of assembler dump.

关闭 CANARY 时:

(gdb) disassemble main
Dump of assembler code for function main:
   0x0000000000001135 <+0>:     push   %rbp
   0x0000000000001136 <+1>:     mov    %rsp,%rbp
   0x0000000000001139 <+4>:     sub    $0x10,%rsp
   0x000000000000113d <+8>:     lea    -0xa(%rbp),%rax
   0x0000000000001141 <+12>:    mov    %rax,%rsi
   0x0000000000001144 <+15>:    lea    0xeb9(%rip),%rdi        # 0x2004
   0x000000000000114b <+22>:    mov    $0x0,%eax
   0x0000000000001150 <+27>:    callq  0x1030 <[email protected]>
   0x0000000000001155 <+32>:    mov    $0x0,%eax
   0x000000000000115a <+37>:    leaveq 
   0x000000000000115b <+38>:    retq   
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     Symbols          FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   62) Symbols       No    0               0               a.out

$ gcc -O2 -D_FORTIFY_SOURCE=2 fortify.c 
In file included from /usr/include/string.h:494,
                 from fortify.c:1:
In function ‘strcpy’,
    inlined from ‘main’ at fortify.c:6:5:
/usr/include/x86_64-linux-gnu/bits/string_fortified.h:90: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      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   64) Symbols       Yes   1               1               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 -pie random.c -o open-pie
$ readelf -h open-pie 
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1050
  Start of program headers:          64 (bytes into file)
  Start of section headers:          14688 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         11
  Size of section headers:           64 (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 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x401040
  Start of program headers:          64 (bytes into file)
  Start of section headers:          14552 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         11
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 28

可以看到两者的不同在 Type 和 Entry point address。

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

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

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

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

$ ldd a.out 
        linux-vdso.so.1 (0x00007ffff7fd3000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7dfd000)
        /lib64/ld-linux-x86-64.so.2 (0x00007ffff7fd5000)
$ ldd a.out 
        linux-vdso.so.1 (0x00007ffff7fd3000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7dfd000)
        /lib64/ld-linux-x86-64.so.2 (0x00007ffff7fd5000)

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

# echo 2 > /proc/sys/kernel/randomize_va_space
$ ./a.out 
0x55b2c7719135
$ ./a.out 
0x557cbfbb4135
$ ldd a.out 
        linux-vdso.so.1 (0x00007ffd76f97000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7302bf5000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f7302dc8000)
$ ldd a.out 
        linux-vdso.so.1 (0x00007fffe13f3000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3078465000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f3078638000)

入口地址和动态链接库地址都变得随机。 接下来关闭 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)

RELRO

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

RELOR 有两种形式:

编译参数

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

安全技术 完全开 部分开启 关闭
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

关闭所有保护:

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

保护机制检测

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

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插件)

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

地址空间布局随机化

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

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

words: 2726 tags: gcc