检查防护措施

pwndbg 自带 checksec

pwndbg> checksec
[*] '/ctf/work/datastore'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

ASLR

概念

ASLR (Address Space Layout Randomization), 地址空间配置随机加载, 简称地址随机化, 是一种针对缓冲区溢出的安全保护技术, 通过对堆、栈、共享库映射等线性区布局的随机化, 通过增加攻击者预测目的地址的难度, 防止攻击者直接定位攻击代码位置, 达到阻止溢出攻击的目的的一种技术。

有以下三种情况

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

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

关闭 ASLR

为了方便我们调试, 可以在自己的系统上关闭ASLR来确认偏移等等

Ubuntu 下:

sudo sysctl -w kernel.randomize_va_space=0

绕过 ASLR

程序信息泄露: 目前广泛应用在操作系统的地址随机化多为粗粒度的实现方式, 同一模块中的所有代码与数据的相对偏移固定。只需要通过信息泄露漏洞将某个模块中的任一代码指针或者数据指针泄露, 即可通过计算得到此模块中任意代码或数据的地址

Canary

Canary

绕过 Canary

程序信息泄露

NX

NX(Non-eXecute)位是一种针对 shellcode 执行攻击的保护措施, 意在更有效地识别数据区和代码区。通过在内存页的标识中增加”执行”位, 可以表示该内存页是否执行, 若程序代码的 EIP 执行至不可运行的内存页, 则 CPU 将直接拒绝执行”指令”造成程序崩溃。

在 Linux 中, 当装载器把程序装载进内存空间后, 将程序的.text段标记为可执行, 而其余的数据段(.data, .bss等)以及栈、堆均不可执行。当攻击者在堆栈上部署自己的 shellcode 并触发时, 只会直接造成程序的崩溃。

工作原理如图:
NX 工作原理

关闭/开启 NX

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

gcc -o test test.c                  // 默认情况下, 开启NX保护
gcc -z execstack -o test test.c // 禁用NX保护
gcc -z noexecstack -o test test.c // 开启NX保护

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

绕过 NX

代码重用攻击, 使用现有代码构造自身所需控制流。

PIE

PIE(Position-Independent Executable, 位置无关可执行文件)技术与 ASLR 技术类似, ASLR 将程序运行时的堆栈以及共享库的加载地址随机化, 而 PIE 技术则在编译时将程序编译为位置无关, 即程序运行时各个段加载的虚拟地址也是在装载时才确定。

关闭 PIE

gcc 编译时加入参数 -no-pie

gcc -no-pie code.c -o code

绕过 PIE

  • 程序信息泄露: 同 ASLR, 通过信息泄露漏洞将某个模块中的任一代码指针或者数据指针泄露, 即可通过计算得到此模块中任意代码或数据的地址

  • 部分写入: PIE 存在一个缺陷, 那就是 PIE 的随机化只能影响到单个内存页。通常来说, 一个内存页大小为 0x1000, 所以最后的3位16进制数是不会变化的, 我们就可以通过程序信息泄露或部分写入来绕过 PIE。

RELRO

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

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

设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号, 从而减少对GOT(Global Offset Table)攻击。RELRO为” Partial RELRO”, 说明我们对GOT表具有写权限。

关闭/开启 RELRO

gcc 编译参数

gcc -o test test.c                  // 默认情况下, 是Partial RELRO
gcc -z norelro -o test test.c // 关闭, 即No RELRO。
gcc -z lazy -o test test.c // 部分开启, 即Partial RELRO
gcc -z now -o test test.c // 全部开启, 即
  • -z now -z norelro, 立即绑定, 但不添加PT_GNU_RELRO段, .got.plt.got 都可写
  • -z relro, 延时绑定, 添加PT_GNU_RELRO段, 只有.got只读, .got.plt依然可写
  • -z now, 立即绑定, 添加PT_GNU_RELRO段, .got只读, .got.plt节取消(plt直接调用.got节地址了)

绕过 RELRO

改写 glibc 中其他函数指针

FORTIFY

fority 用于检查是否存在缓冲区溢出的错误, 是非常轻微的检查。适用情形是程序采用大量的字符串或者内存操作函数, 如memcpy, memset, stpcpy, strcpy, strncpy, strcat, strncat, sprintf, snprintf, vsprintf, vsnprintf, gets以及宽字符的变体。

_FORTIFY_SOURCE设为1, 并且将编译器设置为优化1(gcc -O1), 以及出现上述情形, 那么程序编译时就会进行检查但又不会改变程序功能

_FORTIFY_SOURCE设为2, 有些检查功能会加入, 但是这可能导致程序崩溃。

gcc -D_FORTIFY_SOURCE=1 仅仅只会在编译时进行检查 (特别像某些头文件 #include <string.h>)

gcc -D_FORTIFY_SOURCE=2 程序执行时也会有检查 (如果检查到缓冲区溢出, 就终止程序)

举个例子可能简单明了一些, 下面一段简单的存在缓冲区溢出的C代码

void fun(char *s) {
char buf[0x100];
strcpy(buf, s);
/* Don't allow gcc to optimise away the buf */
asm volatile("" :: "m" (buf));
}

用包含参数-U_FORTIFY_SOURCE编译

08048450 <fun>:
push %ebp ;
mov %esp, %ebp
sub $0x118, %esp ; 将0x118存储到栈上
mov 0x8(%ebp), %eax ; 将目标参数载入eax
mov %eax, 0x4(%esp) ; 保存目标参数
lea -0x108(%ebp), %eax ; 数组buf
mov %eax, (%esp) ; 保存
call 8048320 <strcpy@plt>
leave ;
ret

用包含参数-D_FORTIFY_SOURCE=2编译

08048450 <fun>:
push %ebp ;
mov %esp, %ebp
sub $0x118, %esp ; 将0x118存储到栈上
mov 0x8(%ebp), %eax ; 将目标参数载入eax
mov %eax, 0x4(%esp) ; 保存目标参数
lea -0x108(%ebp), %eax ; 数组buf
mov %eax, (%esp) ; 保存
call 8048320 <strcpy@plt>
leave ;
ret

我们可以看到gcc生成了一些附加代码, 通过对数组大小的判断替换strcpy, memcpy, memset等函数名, 达到防止缓冲区溢出的作用。

总结下就有:

gcc -o test test.c                        // 默认情况下, 不会开这个检查
gcc -D_FORTIFY_SOURCE=1 -o test test.c // 较弱的检查
gcc -D_FORTIFY_SOURCE=2 -o test test.c // 较强的检查

总结

各种安全选择的编译参数如下:

  • NX:-z execstack / -z noexecstack (关闭 / 开启)
  • Canary:-fno-stack-protector /-fstack-protector / -fstack-protector-all (关闭 / 开启 / 全开启)
  • PIE:-no-pie / -pie (关闭 / 开启)
  • ASLR: Ubuntu下 sudo sysctl -w kernel.randomize_va_space=0 进行关闭
  • RELRO:-z norelro / -z lazy / -z now (关闭 / 部分开启 / 完全开启)

参考

linux程序的常用保护机制
Glibc 堆利用的若干方法