Gcc 安全编译选项

1. 简介

在编译器层面,gcc提供了不少安全方面的编译选项,主要有:

item opt descript
NX(DEP) -z execstack // 禁用NX保护
-z noexecstack // 开启NX保护
堆栈禁止执行
RELRO -z norelro // 关闭
-z lazy // 部分开启
-z now // 全部开启
GOT写保护
PIE(ASLR) -fpie -pie // 开启PIE,此时强度为1
-fPIE -pie // 开启PIE,此时为最高强度2
代码段、数据段地址随机化
CANARY -fno-stack-protector // 禁用
-fstack-protector // 开启
-fstack-protector-all // 完全开启
堆栈溢出哨兵
FORTIFY -D_FORTIFY_SOURCE=1 // 较弱的检查
-D_FORTIFY_SOURCE=2 // 较强的检查
常用函数加强检查

还有一个专门的checksec脚本来检测gcc编译出来的可执行文件,开启了哪些安全选项。 下载脚本:

git clone https://github.com/slimm609/checksec.sh/

使用checksec脚本检查:

$ ./checksec.sh/checksec --file=test
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   66) Symbols       No    0               1               test

2. NX(DEP)

NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。

gcc编译选项:

gcc -o test test.c					// 默认情况下,开启NX保护
gcc -z execstack -o test test.c		// 禁用NX保护
gcc -z noexecstack -o test test.c	// 开启NX保护
  • NX
$ gcc -z execstack -o test test.c
$ readelf -l test
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RWE    10           // stack segment增加了可执行属性`E`
$ ./test &
[2] 64014
$ cat /proc/64014/maps 
...
7fff8910f000-7fff89130000 rwxp 00000000 00:00 0                          [stack]    // 运行时堆栈拥有可执行属性`x`
  • no NX
$ gcc -z noexecstack -o test test.c
$ readelf -l test
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10           // stack segment没有了可执行属性`E`
$ ./test &
[2] 64071
$ cat /proc/64071/maps 
...
7ffc72f09000-7ffc72f2a000 rw-p 00000000 00:00 0                          [stack]    // 运行时堆栈没有了可执行属性`x`

3. RELRO

在Linux系统安全领域数据可以写的存储区就会是攻击的目标,尤其是存储函数指针的区域。 所以在安全防护的角度来说尽量减少可写的存储区域对安全会有极大的好处.
GCC, GNU linker以及Glibc-dynamic linker一起配合实现了一种叫做relro的技术: read only relocation。大概实现就是由linker指定binary的一块经过dynamic linker处理过 relocation之后的区域为只读.
设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。RELRO为” Partial RELRO”,说明我们对GOT表具有写权限。

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				// 全部开启,即
  • No RELRO:

可以看到在程序运行以后,数据segment只对应一个vma区域,属性都是rw-

$ gcc -z norelro test.c -o test
$ ./test &
[1] 62725
$ cat /proc/62725/maps 
00400000-00401000 r-xp 00000000 fd:00 5320920                            /home/ipu/uid/test  // 代码segment
00600000-00601000 rw-p 00000000 fd:00 5320920                            /home/ipu/uid/test  // 数据segment
00af1000-00b12000 rw-p 00000000 00:00 0                                  [heap]
...

通过readelf命令读取segment信息,可以详细看到数据segemnt包含了哪些section:.init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 。

$ readelf -l test
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000824 0x0000000000000824  R E    200000
  LOAD           0x0000000000000828 0x0000000000600828 0x0000000000600828
                 0x000000000000023c 0x0000000000000240  RW     200000

 Section to Segment mapping:
  Segment Sections...
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
  • Partial RELRO:

可以看到加上relro编译选项以后,数据segemnt加载到内存分裂成了两个vma:
1、包含.got的vma,在动态链接完成后就被保护起来了,属性设置为r--
2、包含.data.bss的vma,在运行时还需要读写访问的,属性设置为rw-

$ gcc -z lazy test.c -o test
$ cat /proc/63607/maps 
00400000-00401000 r-xp 00000000 fd:00 5401237                            /home/ipu/uid/test  // 代码segment
00600000-00601000 r--p 00000000 fd:00 5401237                            /home/ipu/uid/test  // 数据segment(.init_array .fini_array .jcr .dynamic .got .got.plt)
00601000-00602000 rw-p 00001000 fd:00 5401237                            /home/ipu/uid/test  // 数据segment(.data .bss)
  • Full RELRO:

运行时的效果和Partial RELRO一样:

$ gcc -z now test.c -o test
$ ./test &
[2] 63766
$ cat /proc/63766/maps 
00400000-00401000 r-xp 00000000 fd:00 5401216                            /home/ipu/uid/test
00600000-00601000 r--p 00000000 fd:00 5401216                            /home/ipu/uid/test  // 数据segment(.init_array .fini_array .jcr .dynamic .got .got.plt)
00601000-00602000 rw-p 00001000 fd:00 5401216                            /home/ipu/uid/test  // 数据segment(.data .bss)

4. PIE(ASLR)

为了提升系统的安全,增大漏洞的攻击难度,提出了进程地址空间各区域随机化的措施,称之为ASLR(Address Space Layout Randomization)。ASLR通过随机放置进程关键数据区域的地址空间来防止攻击者能可靠地跳转到内存的特定位置来利用函数。现代操作系统一般都加设这一机制,以防范恶意程序对已知地址进行Return-to-libc攻击。

地址空间随机化分为3个等级:

0 关闭
1 半随机 code&data、stack、mmap、vdso随机化
2 全随机 在1的基础上加上heap随机化

可以通过/proc文件节点查询和配置aslr的等级:

$ cat /proc/sys/kernel/randomize_va_space
2

ALSR其中的code&data随机化需要gcc pie选项的支持。因为打开ASLR以后,支持exe文件code&data的随机化,所以需要把exe文件编译成位置无关代码,这种编译出来的exe文件格式为ET_DYN。可以使用-pie选项来编译ET_DYN类型的exe文件。

gcc编译选项:

gcc -o test test.c				    // 默认情况下,不开启PIE
gcc -fpie -pie -o test test.c		// 开启PIE,此时强度为1
gcc -fPIE -pie -o test test.c		// 开启PIE,此时为最高强度2
gcc -fpic -o test test.c		    // 开启PIC,此时强度为1,不会开启PIE
gcc -fPIC -o test test.c		    // 开启PIC,此时为最高强度2,不会开启PIE

说明:
PIE最早由RedHat的人实现,他在连接起上增加了-pie选项,这样使用-fPIE编译的对象就能通过连接器得到位置无关可执行程序。fPIE和fPIC有些不同。可以参考Gcc和Open64中的-fPIC选项.
gcc中的-fpic选项,使用于在目标机支持时,编译共享库时使用。编译出的代码将通过全局偏移表(Global Offset
Table)中的常数地址访存,动态装载器将在程序开始执行时解析GOT表项(注意,动态装载器操作系统的一部分,连接器是GCC的一部分)。而gcc中的-fPIC选项则是针对某些特殊机型做了特殊处理,比如适合动态链接并能避免超出GOT大小限制之类的错误。而Open64仅仅支持不会导致GOT表溢出的PIC编译。
gcc中的-fpie和-fPIE选项和fpic及fPIC很相似,但不同的是,除了生成为位置无关代码外,还能假定代码是属于本程序。通常这些选项会和GCC链接时的-pie选项一起使用。fPIE选项仅能在编译可执行码时用,不能用于编译库。所以,如果想要PIE的程序,需要你除了在gcc增加-fPIE选项外,还需要在ld时增加-pie选项才能产生这种代码。即gcc -fpie -pie来编译程序。单独使用哪一个都无法达到效果。

  • pie

使能pie编译选项以后胡,编译出来的可执行文件格式为ET_DYN,运行后数据段和代码段的起始地址也被随机化。

$ gcc -fpie -pie -o test test.c
$ file test
test: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=52206a970ad796ff8aa3e9b6e06ecdeaa14d0d8d, not stripped
$ ./test &
[1] 64253
$ cat /proc/64253/maps 
563dd3310000-563dd3311000 r-xp 00000000 fd:00 5401237                    /home/ipu/uid/test  // 代码段起始地址随机化
563dd3510000-563dd3511000 r--p 00000000 fd:00 5401237                    /home/ipu/uid/test  // 数据段起始地址随机化
563dd3511000-563dd3512000 rw-p 00001000 fd:00 5401237                    /home/ipu/uid/test

$ readelf -l test
...
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000a0c 0x0000000000000a0c  R E    200000
  LOAD           0x0000000000000dd8 0x0000000000200dd8 0x0000000000200dd8
                 0x000000000000027c 0x0000000000000280  RW     200000
  • no pie

没有启用pie选项,编译出来的可执行文件格式为ET_EXE,运行后数据段和代码段的起始地址也和链接时的定义一致。

$ gcc -o test test.c
$ file test
test: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=0b8ae05e348ea381550754279eb9973f473ccf7e, not stripped
$ ./test &
[1] 64300
$ cat /proc/64300/maps 
00400000-00401000 r-xp 00000000 fd:00 5355298                            /home/ipu/uid/test  // 代码段起始地址和elf中定义的加载地址一致
00600000-00601000 r--p 00000000 fd:00 5355298                            /home/ipu/uid/test  // 数据段起始地址和elf中定义的加载地址一致
00601000-00602000 rw-p 00001000 fd:00 5355298                            /home/ipu/uid/test

$ readelf -l test
...
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000854 0x0000000000000854  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x000000000000023c 0x0000000000000240  RW     200000

5. CANARY(栈保护)

栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary。
gcc在4.2版本中添加了-fstack-protector和-fstack-protector-all编译参数以支持栈保护功能,4.9新增了-fstack-protector-strong编译参数让保护的范围更广。

gcc编译选项:

gcc -o test test.c						    // 默认情况下,不开启Canary保护
gcc -fno-stack-protector -o test test.c     //禁用栈保护
gcc -fstack-protector -o test test.c        //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o test test.c    //启用堆栈保护,为所有函数插入保护代码

6. FORTIFY

fority其实非常轻微的检查,用于检查是否存在缓冲区溢出的错误。适用情形是程序采用大量的字符串或者内存操作函数,如memcpy,memset,stpcpy,strcpy,strncpy,strcat,strncat,sprintf,snprintf,vsprintf,vsnprintf,gets以及宽字符的变体。
gcc生成了一些附加代码,通过对数组大小的判断替换strcpy, memcpy, memset等函数名,达到防止缓冲区溢出的作用。

gcc编译选项:

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

_FORTIFY_SOURCE设为1,并且将编译器设置为优化1(gcc -O1),以及出现上述情形,那么程序编译时就会进行检查但又不会改变程序功能。
_FORTIFY_SOURCE设为2,有些检查功能会加入,但是这可能导致程序崩溃。
gcc -D_FORTIFY_SOURCE=1 仅仅只会在编译时进行检查 (特别像某些头文件 #include <string.h>)
gcc -D_FORTIFY_SOURCE=2 程序执行时也会有检查 (如果检查到缓冲区溢出,就终止程序)

7. RPATH/RUNPATH

可以在编译时指定程序运行时动态链接库的搜寻路径,防止将一些动态库恶意替换,以达到攻击目的。

gcc –Wl,-rpath              // 指定运行时动态链接库的搜寻路径,硬编码进ELF文件 “RPATH”选项。
LD_RUN_PATH 环境变量         // 指定运行时动态链接库的搜寻路径,硬编码进ELF文件 “RPATH”选项。

查找动态库的过程中,大致的顺序是 RPATHLD_LIBRARY_PATHRUNPATH ,所以,如果使用的是 RPATH 用户将无法进行调整,所以建议使用 RUNPATH

gcc通过以下选项指定使用RPATH还是RUNPATH

-Wl,--disable-new-dtags     // 表明使用 RPATH 
-Wl,--enable-new-dtags      // 标示使用 RUNPATH 
  • RPATH
$ gcc -o test test.c -Wl,-rpath,./
$ readelf -d test

Dynamic section at offset 0xe18 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000f (RPATH)              Library rpath: [./]
 ...
  • RUNPATH
$ gcc -o test test.c -Wl,-rpath,./ -Wl,--enable-new-dtags
$ readelf -d test

Dynamic section at offset 0xe18 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000001d (RUNPATH)            Library runpath: [./]
 ...

参考资料:

1、linux程序的常用保护机制
2、stack canary与绕过的思路
3、PIE与bypass思路
4、Canary保护详解和常用Bypass手段
5、GCC 安全编译选项
6、execve() 详解
7、Linux头文件和库的搜索路径

猜你喜欢

转载自blog.csdn.net/pwl999/article/details/111035160