0x0 介绍
本文记录软件安全课程一项实验内容,为”分析一款编译器的安全特性”,偷懒选了Linux下的gcc,网上有很多相关资料,这里做一实验总结,主要是测试该特性在当前版本Linux平台下是否工作,顺便比较和Windows平台的异同.
另:有更多关于Windows平台下的安全保护机制,但由于windows平台编译器众多(特别是vs开头的),十分依赖编译器和操作系统的配合(虽然在linux平台的实验表明这种程度的安全措施只能依靠编译器和OS的配合,甚至于CPU提供相关的指令集)
本文分析和实验内容如下:
-
a) 分析gcc编译器对安全特性的支持。
-
b) 写程序观察生成可执行代码的差异。
-
c) 分析这些安全特性和程序编写者、编译器、操作系统的关联。
实验环境: ubuntu 18.04 + gcc 7.3
经学习研究,Linux操作系统中gcc相关的安全保护机制有:栈Canaries保护、PIE机制、NX、fortity、relro机制。
0x1 Canaries
在windows操作系统中,这一机制被称为gs机制。实现方式是类似的,即在函数的返回地址前加一个cookie检查,在linux中被称为Canaries。gcc在4.2版本中增加了-fstack-protector来使用这种保护方式,若要禁用栈保护,则应使用-fno-stack-protector。对栈空间的保护在gcc中是默认开启的。
Canaries的产生方式是随机的,即从fs寄存器的0x28偏移处获取,而fs寄存器是被glibc定义存放tls信息的。如图是glibc定义的fs寄存器的数据结构:
图1 fs寄存器的数据结构
可以发现,0x28处存放的即是stark_guard,canaries值。这个值是进程载入时初始化的,生成随机数的代码如下:
图2 canaries的值初始化方式
可以看到,_dl_random是一个随机数,它由_dl_sysdep_start函数从内核获取的。_dl_setup_stack_chk_guard函数负责生成canary值,THREAD_SET_STACK_GUARD宏将canary设置到%fs:0x28位置。
在实验中,我们编写了代码,使用调试程序查看其栈结构和机器码。下图是开启了栈保护的栈结构和机器码:
图3 栈结构
图4 开启栈保护函数的执行代码
可以看到,在初始化函数时。除了push rbp之外,在申请栈空间后又将fs[0x28]压入了栈中,即rbp-0x8。在函数结束前,又检查该处数值是否和fs[0x28]的大小相同,以此保证函数栈未被溢出。该检查如下图所示:
图5 检查canaries的值是否一致
若使用-fno-stack-protector指令进行编译,程序将不被栈保护机制所保护,则编译后的结果如下:
图6 无栈保护的函数栈结构
图7 函数可执行代码
可以看出,rbp值上不再有一个canaries的值,函数在结束时相应的也不会检查。这就使得未经检查的输入有机会发生栈溢出攻击。
另外,当函数在执行过程中发生了栈溢出,系统会返回一个栈帧错误的信息并退出,如图所示:
图8 栈帧错误抛出
0x2 PIE
PIE机制,在windows中被称作ASLR,即地址随机化。PIE在linux中作为内核参数存在,可在/proc/sys/kernel/randomize_va_space中找到其具体的值,0、1、2三个值代表不同的工作强度,具体如下:
- 0 - 表示关闭进程地址空间随机化。
- 1 - 表示将mmap的基址,stack和vdso页面随机化。
- 2 - 表示在1的基础上增加栈(heap)的随机化。
gcc在具体编译时也可选择是否开启PIE,但只有在系统随机功能开启时才有作用。默认开启PIE。
根据此特点,设计了如下的实验:
首先查看当前系统的状态:
图9 当前系统的地址随机化状态
可以看到地址随机化使用了最高的强度。编译运行。结果如下:
图10 PIE值为2时的随机化结果
可以看到,程序的bss段、data段、text段(程序段)、heap段以及stack段全部是随机化过的,系统分配了完全不同的地址空间。
如果在gcc中选择不开启PIE,得到的结果如下:
图11 PIE值为2但gcc不开启PIE
可以发现,bss段、text段、data段不再随机化,且地址大幅度变低,可以推测省区了一部分地址虚拟化的工作,但stack段和heap段仍然是随机化的。gcc的PIE值并没有关闭所有的地址随机化。
更改PIE值为0,如图:
图12 更改PIE值为0
再次编译运行程序,其结果如下:
图13 PIE值为0时的运行结果
可以发现,即使在gcc默认开启了PIE的情况下,所有的地址段都完全没有经过随机化的,这种情况下极容易被猜测到地址情况。
调整PIE值为1,如图:
图14 设置PIE值为1
编译运行,其结果如下:
图15 PIE值为1时的结果
可以看出,地址仍然是被随机化过的。
因此,Linux系统下PIE是否开启决定了段地址是否随机,gcc编译的PIE选项决定了程序的静态段(bss、data、text)是否随机。
0x3 NX
NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。
在windows下,相似的保护措施是DEP。Linux中这个保护特性常被用于和PIE一起使用。
实验中,使用gcc -z execstack关闭NX。通过以下命令执行代码,再进入/proc文件下相应的pid文件查看内存结构:
图16 开启关闭NX机制的方式
内存结构如下:
图17 开启NX的内存结构
图18 关闭NX的内存结构
对比发现,关闭NX后stack段的权限多出了x,可执行。说明若在stack中发生栈溢出时,溢出的代码将可执行。
0x4 Fortity
fority其实非常轻微的检查,用于检查是否存在缓冲区溢出的错误。适用情形是程序采用大量的字符串或者内存操作函数,如memcpy,memset,stpcpy,strcpy,strncpy,strcat,strncat,sprintf,snprintf,vsprintf,vsnprintf,gets以及宽字符的变体。
这种检查是默认不开启的,可以通过
<span style="color:#000000"><span style="color:#000000 !important"><span style="color:#4d4d4c"><code>gcc <span style="color:#000000 !important">-D_FORTIFY_SOURCE</span><span style="color:#000000 !important">=</span>2 <span style="color:#000000 !important">-O1</span>
</code></span></span></span>
开启fortity检查,开启后会替换strcpy等危险函数。
实验中,编写了使用strcpy的函数,使用调试工具查看执行代码如下:
图19 不启用fortity的执行代码
图20 启用fortity的执行代码
对比发现,启用fortity后,程序在执行strcpy函数时,运行了__strcpt_chk函数,这个函数被用于检查是否溢出。检查通过后,这个函数调用strcpy。
fortity的开销较大,所以默认不开启。
0x5 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表具有写权限。
relro功能是默认开启的,可以使用
<span style="color:#000000"><span style="color:#000000 !important"><span style="color:#4d4d4c"><code>gcc <span style="color:#000000 !important">-z</span> norelro
</code></span></span></span>
进行关闭
实验中,分别使用正常编译和norelro编译,运行后观察/proc中对应进程的内存布局,结果如下:
图21 开启relro的内存布局
图22 关闭relro的内存布局
可以发现,关闭relro后,内存布局的唯一区别是text段少了一个没有写权限的段,大小为1000字节,被合并进了之后可写的段。说明这一段内存现在可写,造成了恶意程序有机会更改GOT。
0x6 总结
本实验分析并验证了gcc在linux下的部分安全保护措施,这些安全保护措施极大程度上杜绝了恶意程序的攻击,但大部分情况下有一定缺陷、或需要耗费大量资源。这些保护机制仍需要程序员在操作内存时注意程序的安全问题,如需要严格检查不可信的输入。