漫谈Linux系统安全缺陷缓解机制
现代Linux发行版提供了一些缓解技术,使更难稳定利用软件漏洞。 例如只读重定位(RELRO,RELocation Read-Only),禁止执行(NX,NoExecute),栈保护(Canaries),地址空间布局随机化(ASLR,Address Space Layout Randomization)和位置无关可执行文件(PIE,Position Independent Executables)之类的缓解措施,已经使稳定利已知漏洞的挑战的更加困难。
1 用户空间强化(Userspace Hardening)
通过用于构建软件包的默认编译器标志以及Ubuntu内核,可以启用许多安全特性。 注意:Ubuntu的编译器强化不仅适用于其官方发布的程序,也适用于使用其编译器在Ubuntu上构建的任何内容。
1.1 栈的保护(Stack Protector)
很久之前,矿工在煤井中挖煤时,经常遇到瓦斯泄露的安全事故。后来有人发现一种鸟–金丝雀对瓦斯很敏感。于是矿工就用它作为安全报警装置。
Stack Protector又名canary、stack cookie等,gcc中的-fstack-protector参数提供了一个随机的栈金丝雀(stack canary),类似于Windows平台下Visual Studio中的GS。
栈保护是一种针对栈缓冲区溢出攻击的缓解机制,当函数存在栈缓冲区溢出漏洞时,攻击者可以通过覆盖栈上的返回地址来执行shellcode。当启用栈保护后,调用函数前先往栈里存入一个cookie。在Linux中我们把这个cookie称为canary。当函数返回后再验证cookie是否和之前的值一致,如果不一致就终止进程。因为攻击者在覆盖返回地址的时候往往也会覆盖cookie,导致栈保护检查失败从而阻止执行任意代码。
在编译时可以选择是否启用栈保护以及程度,例如:
1 | gcc -o test test.c // 默认情况下,不开启 Canary 保护 |
绕过栈保护机制的常见方法有:泄露canary、暴破canary、劫持__stack_chk_fail、伪造canary、SSP泄露。
详见:
https://www.anquanke.com/post/id/177832
https://zhakul.top/archives/216
1.2 堆的保护(Heap Protector)
GNU C库堆保护机制(通过ptmalloc和手动两种方式自动进行)为glibc堆内存管理器(在glibc 2.3.4中首次引入)提供了对损坏列表/断开链接/双重释放/溢出( corrupted-list/unlink/double-free/overflow)的防护。 这将防止通过堆内存溢出破坏malloc堆内存区域的控制结构来执行任意代码的能力。
1.3 指针混淆(Pointer Obfuscation)
glibc中存储的某些指针会通过glibc内部的PTR_MANGLE/PTR_UNMANGLE宏进行混淆,以防止libc函数指针在运行时被覆盖。
1.4 不可执行内存(Non-Executable Memory)
大多数现代CPU都可以防止执行不可执行的内存区域(堆,堆栈等)。 这被称为“非eXecute(NX)”或“ eXecute-Disable(XD)”,某些BIOS制造商默认情况下会不必要地禁用它,因此请检查BIOS设置。 这种保护减少了攻击者可以用来执行任意代码执行的区域。 它要求内核使用“ PAE”寻址(也允许对3GB以上的物理地址进行寻址)。 64位和32位-server和-generic-pae内核使用PAE寻址进行编译。 从Ubuntu 9.10开始,针对在32位内核(带有或不带有PAE的内核)上运行的缺少NX的处理器,部分模拟了这种保护。
NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标记为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。
gcc编译器默认开启了NX选项,如果需要关闭NX选项,可以给gcc编译器添加-z execstack参数。
例如:
1 | gcc -o test test.c // 默认情况下,开启NX保护 |
在Windows下,类似的概念为DEP(数据执行保护),在最新版的Visual Studio中默认开启了DEP编译选项。
绕过NX防护手段的方法主要是ROP,详见:
https://www.cnblogs.com/ichunqiu/p/11196219.html
1.5 地址空间布局随机化(Address Space Layout Randomisation)
ASLR由内核和ELF加载器通过随机化内存分配(栈、堆、共享库等)的位置来实现。 这使得攻击者在尝试进行内存破坏利用时,内存地址更难预测。ASLR在系统范围内受/ proc / sys / kernel / randomize_va_space的值控制。 在Ubuntu 8.10之前,此选项默认为“ 1”(上)。 在包含brk ASLR的更高发行版中,它默认为“ 2”(在brk ASLR上启用)。
ASLR(Address space layout randomization,地址空间布局随机化)通过随机放置数据区域的地址空间来防止攻击者跳转到内存的特定位置。在 Windows 上 ASLR 主要包括堆栈随机化、PEB与TEB随机化、映像随机化,windows系统上虽然xp时代就提出来了,但是从vista开始ASLR才真正发挥作用。在linux上ASLR主要包括栈地址随机化、LIBS/MMAP随机化、EXEC随机化、BRK随机化、VDSO随机化。在没有ASLR的情况下让程序跳转到一个已经存在的系统函数的漏洞利用方式被称为ret2libc。
一般情况下NX(Windows平台上称其为DEP)和地址空间布局随机化(ASLR)会同时工作。可以防范基于Ret2libc方式的针对DEP的攻击。ASLR和DEP配合使用,能有效阻止攻击者在堆栈上运行恶意代码。
地址空间布局随机化,有以下三种情况:
1 | 0 - 表示关闭进程地址空间随机化。 |
1.5.1 栈的ASLR
每次执行程序都会有不同的栈内存空间布局。 这使得很难在内存中定位要攻击或传递可执行攻击有效负载的位置。
1.5.2 LIBS/MMAP的ASLR
每次执行程序的都会产生不同的mmap内存空间布局(这会使动态加载库每次都加载到不同的内存地址)。 这使得很难在内存中找到类似“return to libc”攻击的跳转位置。
可以这么理解,LIBS/MMAP随机化相当于windows中dll的随机化,而EXEC随机化相当于windows中exe的随机化。
1.5.3 EXEC的ASLR
使用“ -fPIE -pie”参数构建的程序的每次执行时,都将被加载到不同的内存位置。 这使得基于内存破坏的攻击,很难在内存中定位到要攻击或跳转的位置。
1.5.4 BRK的ASLR
与exec ASLR相似,brk ASLR调整exec内存区域和brk内存区域之间的相对内存位置(对于small malloc)。 在2.6.26(Ubuntu 8.10)中添加了来自exec内存的brk偏移量的随机化,尽管brk ASLR的一些影响可以在Ubuntu 8.04 LTS中的PIE程序中看到,因为exec是ASLR,并且brk在执行后立即分配 区域(因此从技术上讲是随机的,但直到8.10才相对于文本区域随机化)。
linux系统中brk和mmap这两个系统调用用来分配内存。当brk ASLR关闭的时候,start_brk和brk都是指向bss段的尾部的;当brk ASLR开启的时候,start_brk和brk初始位置是bss段的尾部加一个随机的偏移。
1.5.5 VDSO的ASLR
程序的每次执行都会导致一个随机的vdso位置。 这样可以防止跳入系统调用(jump-into-syscall)攻击。
VDSO(Virtual Dynamically-linked Shared Object,虚拟动态共享库)将内核态的调用映射到用户态的地址空间中,使得调用开销更小,路径更好。拿x86下的系统调用举例,传统的int 0x80有点慢,Intel和AMD分别实现了sysenter/sysexit和syscall/sysret,即所谓的快速系统调用指令,使用它们更快,但是也带来了兼容性的问题。于是linux实现了vsyscall,程序统一调用vsyscall,具体的选择由内核来决定,vsyscall的实现就在VDSO中。执行ldd /bin/sh,会发现有个linux-vdso.so.1的动态文件,而系统中却找不到它,它就是VDSO。
1.6 编译时强化(Built with)
1.6.1 编译时使用PIE参数(Built as PIE)
使用“ -fPIE -pie”参数构建的位置无关可执行文件(PIE)都可以利用exec ASLR。 这样可以防止“返回文本(return-to-text)”,并且通常会阻止内存破坏攻击。 在构建整个程序包时,需要对编译器选项进行统一更改。 PIE在只有较少数量的通用寄存器(例如x86)的体系结构上会造成很大的性能损失(5-10%),因此它最初仅用于选定数量的安全关键软件包。在64位体系结构上的PIE没有相同的缺陷,并且已将其设为默认值。
前面说了EXEC的随机化,实际上更准确的说法是PIE(Position Independent Executables,位置无关可执行文件)。PIE只有在系统开启ASLR和编译时开启-fpie -pie选项这两个条件同时满足时才会生效。
- PIE:
-no-pie
/-pie
(关闭 / 开启)
位置无关的可执行文件(PIE,Position Independent Executables)。这样使得在利用缓冲溢出和移动操作系统中存在的其他内存崩溃缺陷时采用面向返回的编程(return-oriented programming)方法变得难得多。
liunx下关闭PIE的命令如下:
1 | sudo -s echo 0 > /proc/sys/kernel/randomize_va_space |
gcc编译命令
1 | gcc -o test test.c // 默认情况下,不开启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
https://www.cnblogs.com/ichunqiu/p/11350476.html
1.6.2 编译时使用Fortify Source参数(Built with Fortify Source)
使用“ -D_FORTIFY_SOURCE = 2”(以及-O1或更高版本)构建的程序在glibc中启用了几种编译时和运行时保护:
- 当已知目的缓冲区的大小时,将对“ sprintf”,“ strcpy”的无长度限定的调用扩展为它们有长度限制的表亲函数(防止内存溢出)。
- 当格式化字符串位于可写内存段中时,阻止使用格式字化符串“%n”的攻击。
- 检查重要的函数返回值和参数(例如system,write,open)。
- 创建新文件时需要指定文件掩码。
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代码
1 | void fun(char *s) { |
用包含参数-U_FORTIFY_SOURCE编译
1 | 08048450 <fun>: |
用包含参数-D_FORTIFY_SOURCE=2编译
1 | 08048470 <fun>: |
我们可以看到gcc生成了一些附加代码,通过对数组大小的判断替换strcpy, memcpy, memset等函数名,达到防止缓冲区溢出的作用。
总结下就有:
1 | gcc -o test test.c // 默认情况下,不会开这个检查 |
1.6.3 编译时使用RELRO参数(Built with RELRO)
在Linux系统安全领域数据可以写的内存区域就会是攻击的目标,尤其是存储函数指针的地方。所以从安全防护的角度来说,尽量减少可写的内存区域对安全会有极大的好处。
通过使加载程序将重定位表的任何区域标记为只读,以在加载时解析任何符号(“只读重定位”),从而使ELF程序防范加载程序内存区覆盖写的攻击。这减少了GOT覆盖写形式(GOT-overwrite-style)的内存破坏攻击的范围。
RELRO(RELocation Read-Only,只读重定位)让加载器将重定位表中加载时解析的符号标记为只读,这减少了GOT覆盖写的攻击面。RELRO可以分为部分RELRO(Partial RELRO)和完全RELRO(Full RELRO)。开启Partial RELRO的话GOT表是可写的;开启FULL RELRO的话GOT表是只读的。开启-Wl,-z,relro选项即可开启Partial RELRO;开启-Wl,-z,relro,-z,now选项即可开启Full RELRO。
设置符号重定位表为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。当RELRO为” Partial RELRO”时,说明我们对GOT表具有写权限;如果开启FULL RELRO,意味着我们无法修改GOT表。
gcc编译:
1 | gcc -o test test.c // 默认情况下,是 Partial RELRO |
1.6.4 编译时使用BIND_NOW参数(Built with BIND_NOW)
标记ELF程序在启动时就解析所有动态符号(而不是”按需形式“(延时绑定),也称为“立即绑定”),以便可以将GOT完全设为只读(与上面的RELRO结合使用时)。
1.6.5 编译时使用fstack-clash参数(Built with -fstack-clash-protection)
在可变长度栈内存分配(通过alloca()或gcc可变长度数组等)周围添加额外的指令,以在分配时探测内存的每一页。 通过确保所有栈内存分配均有效(或通过引发分段错误(如果无效)并把可能的代码执行攻击转变为拒绝服务),这可以缓解栈冲突(stack-clash)攻击。
1.6.6 编译时使用-fcf-protection参数(Built with -fcf-protection)
指示编译器生成指令以支持英特尔的控制流强制技术(CET, Control-flow Enforcement Technology)。
2 内核强化(Kernel Hardening)
内核本身启用的保护功能,使其更难以受到威胁。
2.1 零地址保护(0-address protection)
由于内核和用户空间共享虚拟内存地址,因此需要对“ NULL”内存地址进行保护,以使用户空间mmap的内存无法从地址0开始,从而阻止” NULL解引用”(“NULL dereference”)内核攻击。
2.2 /dev/mem保护(/dev/mem protection)
某些应用程序(Xorg)需要从用户空间直接访问物理内存。 存在特殊文件/ dev / mem来提供此访问。 过去,如果攻击者具有root用户访问权限,则可以从该文件查看和更改内核内存。 引入了CONFIG_STRICT_DEVMEM内核选项以阻止对非设备存储器的访问。
2.3 禁用/dev/kmem(/dev/kmem disabled)
/ dev / kmem不再是现代用户,攻击者无法使用它来加载内核rootkit。 CONFIG_DEVKMEM设置为“ n”。 尽管/ dev / kmem设备节点通过Ubuntu 9.04在Ubuntu 8.04 LTS中仍然存在,但实际上并没有附加到内核中的任何内容。
2.4 禁止模块加载(Block module loading)
在Ubuntu 8.04 LTS及更早版本中,可以从系统范围的功能边界集中删除CAP_SYS_MODULES,这将阻止加载任何新的内核模块。 这是阻止安装内核rootkit的另一层保护。 2.6.25 Linux内核(Ubuntu 8.10)更改了边界集的工作方式,该功能消失了。 从Ubuntu 9.10开始,现在可以通过在/ proc / sys / kernel / modules_disabled中设置“ 1”来再次阻止模块加载。
2.5 只读数据节(Read-only data sections)
这样可以确保将某些内核数据段标记为禁止修改。 这有助于防止某些类的内核rootkit。 通过CONFIG_DEBUG_RODATA选项启用。
2.6 栈的保护(Stack protector)
类似于ELF程序用户空间中的栈保护器,内核也可以保护其内部栈。 通过CONFIG_CC_STACKPROTECTOR选项启用。
2.7 模块只读或不可执行(Module RO/NX)
此功能扩展了CONFIG_DEBUG_RODATA,对内核中已加载模块也有类似限制。 这可以帮助抵御依赖已加载模块中各种内存区域的内核攻击。通过CONFIG_DEBUG_MODULE_RONX选项启用。
2.8 限制内核地址显示(Kernel Address Display Restriction)
当攻击者试图开发通用性更强的漏洞利用程序时,他们通常需要知道内核结构的位置。 通过将内核地址视为敏感信息,常规本地用户无法看到那些地址。 从Ubuntu 11.04开始,/ proc / sys / kernel / kptr_restrict设置为“ 1”,以阻止报告已知的内核地址泄漏。 此外,只有root用户才能读取各种文件和目录:/boot/vmlinuz、/boot/System.map、/sys/kernel/debug/、/proc/slabinfo
2.9 内核地址空间布局随机化(Kernel Address Space Layout Randomisation)
内核地址空间布局随机化(kASLR)旨在通过随机化内核的基地址来使某些内核利用更加难以实现。依赖于内核符号位置的漏洞利用必须发现随机基址。
kASLR从Ubuntu 14.10开始可用,但默认情况下未启用。 在内核命令行上指定“ kaslr”选项以使用kASLR。
注意:启用kASLR将会禁用休眠模式。
2.10 冷门协议黑名单(Blacklist Rare Protocols)
通常,内核允许通过MODULE_ALIAS_NETPROTO(PF _…)宏按需自动加载所有网络协议。由于这些协议中的许多协议对于旧版Ubuntu用户来说都是旧的,稀有的或通常很少使用的,并且可能包含未发现的可利用漏洞,因此自Ubuntu 11.04起,它们已被列入黑名单。 其中包括:ax25,netrom,x25,rose,decnet,econet,rds和af_802154。 如果需要任何协议,则可以通过modprobe专门加载它们,或者可以更新/etc/modprobe.d/blacklist-rare-network.conf文件以删除黑名单条目。
2.11 过滤系统调用(Syscall Filtering)
程序可以使用seccomp_filter接口过滤掉内核syscall的可用性。 这是在容器或沙箱中完成的,这些容器或沙箱希望在潜在运行不受信任的软件时进一步限制对内核接口的访问。
seccomp 是 secure computing 的缩写,其是 Linux kernel 从2.6.23版本引入的一种简洁的 sandboxing 机制。在 Linux 系统里,大量的系统调用(system call)直接暴露给用户态程序。但是,并不是所有的系统调用都被需要,而且不安全的代码滥用系统调用会对系统造成安全威胁。seccomp安全机制能使一个进程进入到一种“安全”运行模式,该模式下的进程只能调用4种系统调用(system call),即 read(), write(), exit() 和 sigreturn(),否则进程便会被终止。
CTF中的seccomp详见:
https://www.jianshu.com/p/969219ce9050
https://blog.csdn.net/tan6600/article/details/80967853
2.12 限制dmesg(dmesg restrictions)
当攻击者试图开发通用性更强的漏洞利用程序时,他们经常会使用dmesg输出。通过将dmesg输出视为敏感信息,攻击者无法使用此输出。从Ubuntu 12.04 LTS开始,可以将/ proc / sys / kernel / dmesg_restrict设置为“ 1”,以将dmesg输出视为敏感内容。 Ubuntu Touch内核默认情况下启用了此功能。
2.13 禁止kexec(Block kexec)
从Ubuntu 14.04 LTS开始,现在可以通过sysctl禁用kexec。在Ubuntu中启用了CONFIG_KEXEC,因此最终用户可以根据需要使用kexec,新的sysctl允许管理员禁用kexec_load。 例如,这在设置CONFIG_STRICT_DEVMEM和modules_disabled的环境中是理想的。
2.14 UEFI安全启动(UEFI Secure Boot)
从Ubuntu 12.04 LTS开始,UEFI安全启动在引导加载程序的强制模式和内核的非强制模式下实现。 使用此配置,无法验证的内核将只能在未启用UEFI兼容模式(quirks)下启动。Ubuntu 18.04 LTS的Ubuntu 18.04.2版本为引导加载程序和内核启用了强制模式,因此验证失败的内核不会启动,而且验证失败的内核模块也不会加载。
2.15 内核页表隔离(Kernel PageTable Isolation)
今年年初的CPU漏洞让内核页表隔离(KPTI, Kernel PageTable Isolation)进入了人们的视野。进程地址空间被分成了内核地址空间和用户地址空间,其中内核地址空间映射到了整个物理地址空间,而用户地址空间只能映射到指定的物理地址空间。内核地址空间和用户地址空间共用一个页全局目录表。为了彻底防止用户程序获取内核数据,可以令内核地址空间和用户地址空间使用两组页表集。linux内核从4.15开始支持KPTI,windows上把这个叫KVA Shadow,原理类似。更多细节请见参考资料。
2.16 SMAP/SMEP
SMAP(Supervisor Mode Access Prevention,管理模式访问保护)和SMEP(Supervisor Mode Execution Prevention,管理模式执行保护)的作用分别是禁止内核访问用户空间的数据和禁止内核执行用户空间的代码。arm里面叫PXN(Privilege Execute Never)和PAN(Privileged Access Never)。SMEP类似于前面说的NX,不过一个是在内核态中,一个是在用户态中。和NX一样SMAP/SMEP需要处理器支持,可以通过cat /proc/cpuinfo查看,在内核命令行中添加nosmap和nosmep禁用。windows系统从win8开始启用SMEP,windows内核枚举哪些处理器的特性可用,当它看到处理器支持SMEP时通过在CR4寄存器中设置适当的位来表示应该强制执行SMEP,可以通过ROP或者jmp到一个RWX的内核地址绕过。linux内核从3.0开始支持SMEP,3.7开始支持SMAP。
在没有SMAP/SMEP的情况下把内核指针重定向到用户空间的漏洞利用方式被称为ret2usr。physmap是内核管理的一块非常大的连续的虚拟内存空间,为了提高效率,该空间地址和RAM地址直接映射。RAM相对physmap要小得多,导致了任何一个RAM地址都可以在physmap中找到其对应的虚拟内存地址。另一方面,我们知道用户空间的虚拟内存也会映射到RAM。这就存在两个虚拟内存地址(一个在physmap地址,一个在用户空间地址)映射到同一个RAM地址的情况。也就是说,我们在用户空间里创建的数据,代码很有可能映射到physmap空间。基于这个理论在用户空间用mmap()把提权代码映射到内存,然后再在physmap里找到其对应的副本,修改EIP跳到副本执行就可以了。因为physmap本身就是在内核空间里,所以SMAP/SMEP都不会发挥作用。这种漏洞利用方式叫ret2dir。
3 checksec
Checksec是一个用于检查可执行文件的正在使用的标准Linux OS或PaX的安全特性(例如PIE,RELRO,PaX,Canaries,ASLR,Fortify Source)的bash脚本。 它最初是由Tobias Klein编写,自2011年的v1.5版本之后,不再维护。新版本来自github.com/slimm609/的开源项目。
[^PaX]: PaX是针对Linux Kernel的一个加固版本的补丁,它让Linux内核的内存页受限于最小权限原则,是一个”有效防御系统级别0DAY”的方案。
checksec的使用方法:
1 | $checksec --file=/bin/ls |
一般来说,也可以使用gdb中peda插件自带的checksec功能,如下图所示:
上图所示的CANARY: disabled表示没有开启栈保护特性。
4 参考和引用
https://introspelliam.github.io/2017/09/30/linux%E7%A8%8B%E5%BA%8F%E7%9A%84%E5%B8%B8%E7%94%A8%E4%BF%9D%E6%8A%A4%E6%9C%BA%E5%88%B6/
https://www.infoq.cn/article/Linux-PaX-Grsecurity/
https://blog.csdn.net/zsj2102/article/details/78734981
https://bbs.pediy.com/thread-226696.htm
https://wiki.ubuntu.com/Security/Features
https://manybutfinite.com/post/anatomy-of-a-program-in-memory/