保护手段

参考:

Canary

简单地说:

  • 函数在调用时,在返回地址与局部变量之间设置了一个用户不可知的 Canary
  • 在函数结束调用时,函数检查这个 Canary 是否被修改,否则抛出异常;

gcc 在编译时可以控制以下的几个参数控制栈的保护程度:

1
2
3
4
5
6
7
8
# 禁用栈帧保护
$ gcc -fno-stack-protector a.c

# 开启栈帧保护
$ gcc -fstack-protector a.c

# 启用栈帧保护,为所有函数插入保护代码
$ gcc -fstack-protector-all a.c

Fortify

Fortifygcc 的一个检测工具,它会在编译时检测危险的字符串操作函数,比如 memcpystrcpy 等。

它有弱强两种使用模式:

  1. D_FORTIFY_SOURCE 设置为 1 进行较弱的检查:

    • 程序编译时就会进行检查,但是不会改变程序的功能;
    • 程序仅仅在编译时进行检查,运行时不会检查;
    1
    
    $ gcc -D_FORTIFY_SOURCE=1 t.c
    
  2. D_FORTIFY_SOURCE 设置为 2 进行较强的检查:

    • 程序编译时可能会改变程序执行的函数,可能会导致程序崩溃;
    • 程序在运行时会进行检查,检查到缓冲区溢出会终止程序;
    1
    
    $ gcc -D_FORTIFY_SOURCE=2 t.c
    

NX (DEP)

这一技术的基本原理是将数据所在的内存页标注为不可执行,程序试图在数据上执行代码时,CPU 会抛出异常。

gcc 默认开启了 NX 选项,可以通过以下的方式控制 NX 保护:

1
2
3
4
5
# 禁用 NX 保护
$ gcc -z execstack t.c

# 开启 NX 保护
$ gcc -z noexecstack t.c

PIE (ASLR)

PIE: Position Independent Executable(位置独立可执行)。gcc 通过以下的方式控制是否开启 PIE

1
2
3
4
5
# PIE 默认开启
$ gcc --enable-default-pie t.c

# 关闭 PIE
$ gcc -no-pie t.c

但是在 Linux 平台下,即使文件开启了 PIE 保护,还需要系统开启 ASLR 才会打乱基址,否则程序仍然会加载在一个固定的基址上(只不过和不开启 PIE 不一样)。

我们可以通过控制文件 /proc/sys/kernel/randomize_va_space 控制是否开启 ASLR:

  • 0:表示关闭进程地址空间随机化。
  • 1:表示将 mmap 的基址,stack 和 vdso 页面随机化。
  • 2:表示在 1 的基础上增加堆区基址的随机化(下图中的 Random brk offset)。

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/figure/program_virtual_address_memory_space.png

Relro

gccgnu linkerglic-dynamic-linker 一起配合实现了一种叫做 Relro 的技术(Read Only Relocation)。

实现方式就是通过 linker 指定文件的的部分区域标记为只读区域。

gcc 中指定以下的参数可以控制编译的方式:

1
2
3
4
5
$ gcc -z norelro t.c		# 关闭

$ gcc -z lazy t.c			# 部分开启,可写

$gcc -z now t.c				# 全部开启,只读