Linux二进制漏洞利用
——突破系统防御的关键技术
2018-09-25曾永瑞
曾永瑞 李 喆
(北京天融信网络安全技术有限公司阿尔法实验室 北京 100085) (zeng_yongrui@topsec.com.cn)
近年来,随着攻防技术不断发展,二进制漏洞相关的资料也越来越多,但国内的环境导致了目前大家对Windows系统上的二进制漏洞有更多的关注,多数介绍二进制漏洞的书籍资料都是基于Windows平台的,而对于Linux二进制漏洞的研究的相关资料只能在网络上一些个人博客、论坛中找到,缺乏有效的总结和整理, 也没能很好地引起人们的重视.本文的目的就是对Linux平台上的二进制漏洞利用作一个浅显的梳理.
本文完成了以下几个方面的工作:
1) 介绍了目前Linux系统上常见的二进制漏洞类型以及其基本原理.
2) 整理了这些漏洞经典的利用方法、相关的文献资料;接着,介绍了Linux系统中针对这些利用方法而出现的安全机制;最后,深入介绍了几个目前绕过这些安全机制的方法.
3) 总结了文中提到的漏洞特点以及共性, 并针对它们的本质特征提出进一步的防护方案.
1 常见的Linux二进制漏洞
1.1 栈溢出漏洞
缓冲区溢出攻击的概念最早可以追溯到20世纪70年代,但直到1996年,AlphaOne[1]才首次公开展示了栈缓冲区溢出漏洞的利用原理,并提出了“shellcode”这一概念.
若程序在接受用户输入时,不对输入数据进行边界检查,直接将其保存到栈上,且写入目标数据结构的数据大小超过为该结构分配的内存大小时,会发生栈缓冲区溢出.这将导致栈上相邻数据的损坏,通常在错误触发溢出的情况下,会导致程序崩溃.但需要注意的是,栈包含有函数调用的返回地址,在现在主流的各种缓解措施没有出现之前,攻击者只需简单地将可执行代码注入正在运行的程序缓冲区中,再将返回地址覆盖为指向注入代码的地址,就能轻易获取未经授权系统的访问权限.
即便到了今天,存在各种缓解措施的情况下,每年缓冲区溢出漏洞在各种类型漏洞中所占比例一直都是居高不下,所以栈溢出漏洞依然值得警惕.
1.2 堆漏洞
相对于栈而言,堆内存的管理机制要复杂得多,在各种系统上的实现也不相同,甚至不同平台中堆内存管理器也有不同的实现,例如dlmalloc,ptmalloc,tcmalloc,jemalloc等等,甚至有些程序还会使用自己创建的内存池来管理内存.
1.2.1堆溢出
堆溢出的原理和栈溢出类似,当程序向堆块(chunk)中写入的数据超过了堆块本身可使用的字节数时,溢出的数据将会覆盖到物理相邻的下一个堆块.
1999年,w00w00安全小组的Conover[2]首次详细介绍了堆溢出的原理和利用方式,从这时起,漏洞利用的目标逐渐扩大到了堆上.
1.2.2释放重引用漏洞
释放重引用漏洞UAF(use after free)如其字面意思,当一个内存块被释放之后,再次使用指向这块内存的指针所引发的漏洞.
许多程序中,内存被分配以用于存储对象实例.这些对象使用完后,程序会释放分配的内存,以节省系统资源.然后,需要将指向该对象的指针设为NULL,如果一个指针没有指向适当类型的有效对象,则称这类指针为“悬挂指针”(dangling pointer).通常来说导致“悬挂指针”有如下2个原因:
1) 程序利用了已被释放的C++对象,从而访问无效的内存位置;
2) 一个程序返回了指向它的本地变量的指针,因为这个变量只在该函数内部有效,一旦函数结束,这个指针将变为一个无效的指针.
在某些情况下,程序可能会使用指向已被释放对象的指针.如果发生这种情况,程序将进入意外的执行流程,这可能导致程序崩溃或甚至更危险的后果,有关UAF漏洞的利用方法,将在2.3节介绍.
1.2.3双重释放漏洞
双重释放(double free)[3]漏洞本质上可以算是UAF漏洞的一个子集,即对1块内存释放2次,这会产生不确定的后果.
双重释放漏洞的利用原理也很好理解:堆块释放后,物理相邻的前、后堆块若为空闲状态,会进行合并操作,然后利用Unlink机制将该空闲堆块从(unsorted bin)中取下.如果用户精心构造的假堆块被Unlink,很容易导致一次固定地址写,然后转换为任意地址读写,从而控制程序的执行.
1.3 整数溢出漏洞
C语言中存在3种基本的整型数据类型——short,int和long,这3种类型又可分为有符号(signed)和无符号(unsigned),每种数据类型都有各自的大小范围[4].但是对有符号类型和无符号类型的区分只在编译器层面,底层汇编指令处理的只是二进制数据.当程序中的数据超过其数据类型的范围,则会造成整数溢出.这种溢出本身不会造成任意代码执行,但却可能绕过程序的边界检查,从而间接地导致栈或堆的缓冲区溢出,如图1所示:
图1 一个整数溢出漏洞的例子
上述代码第4行中的strlen()的返回类型是size_t(unsigned int),其返回值存储在unsigned char(范围是0~255)数据类型中.因此,任何大于unsigned char的表示范围的值都会导致整数溢出.当用户输入的密码长度在260~264 B之间时,变量passwd_len实际上为4~8,绕过了第5行的边界检查,进而导致了一个栈的缓冲区溢出漏洞.
1.4 格式化字符串漏洞
格式化字符串函数是一种特别的函数,它可以传入可变数量的参数,并将第1个参数作为格式化字符串,来解析之后的参数.通俗来说,格式化字符串函数就是将计算机内存中表示的数据转化为人类可读的字符串格式.几乎所有的CC++程序都会利用这类函数来输出信息,或处理字符串.
关于格式化字符串漏洞的原理,可以先看看下面这条语句:
printf(″Color %s, Number %d, Float %4.2f″).
这条语句中没有为格式化字符串提供参数,程序依然可以编译成功并正常运行,但会将栈上格式化字符串地址上面的3个变量分别解析为:
1) 其地址对应的字符串;
2) 其内容对应的整型值;
3) 其内容对应的浮点值.
对于2)和3)来说并没有太大的问题,但是对于1)来说,如果提供了一个不可访问地址,程序就会因此而崩溃.通过格式化字符串,用户可以使用%s,%x从栈输出数据;也可以用%n向任意地址写入数据,这就造成了内存泄露的问题.表1中列出了常见的几种格式化字符串函数.
表1 常见输出格式化字符串函数
1.5 竞争条件漏洞
竞争条件(race condition)[5]是指多个线程或进程在读写1个共享数据时结果依赖于它们执行的相对时间.竞争条件发生在多个进程或者线程读写数据时,其最终的结果依赖于多个进程的指令执行顺序.由于目前的系统中大量采用并发编程,经常对资源进行共享,往往会产生竞争条件漏洞.而且由于在并发时,执行流的不确定性很大,竞争相对难察觉,并且在漏洞的复现和调试方面会比较困难.这给修复竞争条件漏洞也带来了不小的困难.
2016年Linux内核内存子系统在处理写入时复制COW(copy-on-write)产生的竞争条件漏洞,可以说是近年来最有名的竞争条件漏洞.这个被称为“脏牛”(DirtyCow)的漏洞在发现时已经存在于Linux内核9年之久,可见竞争条件漏洞的隐蔽性.
2 传统利用手段和技巧
堆和栈中的漏洞曾经乃至现在都是二进制漏洞中所占比例最大的,而整数溢出漏洞最终实现利用,也是通过间接转化为堆栈溢出漏洞.所以本节主要介绍在各种漏洞缓解措施未普及之前,栈溢出漏洞和堆相关漏洞的经典利用方法.
2.1 面向导向编程
最古老的栈溢出漏洞利用,只需简单地将函数的返回地址覆盖为需要执行的shellcode的地址即可,随着不可执行位NX(no-execute)[6]技术的应用,直接向栈或者堆上直接注入shellcode的方式难以继续发挥效果.直到2007年,加州大学圣迭戈分校的Shacham[7]在其论文中提出了面向导向编程ROP(return oriented programming)技术,其主要思想是在栈缓冲区溢出的基础上,依靠借用程序中多个以返回指令结尾的代码块(gadgets),组成1个ROP链,它可以实现任何逻辑功能,从而控制程序的执行流程,其原理如图2所示:
图2 ROP技术原理
ROP技术是图灵完备的,换句话说,它为攻击者提供了一种功能齐全的“编程语言”,可以使用它来执行任何所需的操作.此外,gadget可以选择的位置和利用方式也多种多样,表2中列出了一些Linux下常见的ROP技术:
表2 常见的ROP技术
2.2 堆溢出的利用
关于堆漏洞利用的传统方法,可以参考Phantasmagoria[8]于2005年提出的5种堆漏洞利用方法,他将这些方法命名为“The House of xxx”,具体如下:
1) The House of Prime;
2) The House of Mind;
3) The House of Force;
4) The House of Lore;
5) The House of Spirit.
这几种堆溢出利用的首要思路就是通过覆盖物理相邻的下一堆块的元数据来破坏堆的数据结构,进而执行恶意代码.然而随着ASLR等技术的出现,文中所提到的方法在大多数情况下都无法应用,但是现如今又出现了一系列新的名为“The House of xxx”的利用方法,和文献[8]中所提到的方法已有较大的不同.
2.3 UAF漏洞利用
关于UAF漏洞的利用,Watchfire的安全研究人员Afek等人[9]在2007年的Black Hat USA大会上,结合一个Microsoft IIS 6服务器的漏洞,完整地讲述了UAF漏洞的原理和利用技巧.
UAF最常用的利用方式就是通过申请与重用对象大小相等的堆块,使恶意数据被分配到已释放对象的内存位置,从而覆盖旧对象的内存空间.
了解编译器实现对象的方式对于能够使用适当的数据覆盖原始对象至关重要.以32位程序的UAF漏洞为例,一个C++对象开头是一个指向虚函数表的指针,如图3所示.
若将恶意数据的第1个DWORD大小的内容设置为包含一个特殊地址,该地址将取代对象的虚函数表.内存的其余部分布置为将要执行的shellcode.当使用call [eax+offset]执行虚函数调用时,再将程序的执行流程转移到shellcode中.整个过程的原理如图4所示.
图3 对象虚函数的实现方式
图4 UAF漏洞利用的最常见方式
2.4 堆喷射
堆喷射(heap spray)技术的出现最早可以追溯到2001年,奥地利著名安全小组TESO发布的一个针对BSD telnet服务器攻击程序中[10],首次利用了堆喷射技术.当时该技术并没有确切的名称,其目的也只是为了提供一个稳定的存放shellcode的地方,然后在栈溢出劫持返回地址进行跳转.
随着后来ASLR技术的出现,这个古老技术又成为了辅助绕过ASLR的绝佳手段.尤其是对于一些能内嵌执行脚本的程序,为攻击者提供了动态分配内存的途径,对于不同的程序来说往往有如下这些堆喷射方法:
1) 利用JavaScript进行堆喷射;
2) 利用ActionScript进行堆喷射;
3) 利用HTML5进行堆喷射;
4) 利用VBScript进行堆喷射;
5) 直接将堆喷射数据构造进文件中,例如图片、视频等.
3 现代防护措施
3.1 应用级加固措施
3.1.1地址空间布局随机化
在第2节的一些漏洞利用方法中,最后都要将程序流程指向内存中已布置好的shellcode的地址,为了防止这些内存损坏漏洞,地址空间布局随机化(address space layout randomization, ASLR)[11]技术应运而生.
为了防止攻击者每次都能可靠地跳转到内存中特定的地址,ASLR随机排列进程的关键数据区域的地址.
3.1.2不可执行位
计算机将指令当成数据的概念,使得汇编语言、编译器与其他自动编程工具得以实现,但也造成一些缺陷,缓存溢出就是一个典型例子.NX技术的出现,就是为了弥补这一缺陷,用于区分内存中指令集与数据.任何标记了NX位的区块代表仅供存储数据使用而不是存储处理器的指令集,处理器不会将此处的数据作为代码执行,这种技术可防止多数的缓存溢出攻击.
3.2 编译器加固措施
3.2.1位置无关可执行程序
前面提到的ASLR是操作系统的功能选项,作用于可执行文件装入内存运行时,因而只能随机化stack,heap和libraries的基址;而位置无关可执行程序(position independent executables, PIE)[12]是编译器的功能选项(-fPIE),作用于可执行文件的编译过程,其随机化了可行性文件装载内存的基址(代码段,PLT,GOT,数据段等共同的基址).
PIE需要在源代码级别遵循一套特定的语义,并且需要编译器的支持.使用了绝对内存地址的指令,会被替换为相对寻址指令.这些间接处理过程可能导致PIE的运行效率下降,但是因为大多数处理器对位置无关可执行程序都有很好的支持,使得这一点点效率的下降基本可以忽略.
3.2.2重定位只读
要理解重定位只读(relocation read-only, RELRO)[13],首先需要了解ELF文件的动态链接过程.一个程序在引用了某个模块中的函数时,因为不知道模块加载位置,需要将相关代码地址抽出,放在数据段中的全局偏移表(global offset table, GOT)中.
例如调用func函数,假设该函数定义在glibc共享库中,若要找到该函数的地址,链接器需要生成一段额外的代码,放入代码段中一个名为过程链接表(procedure link table, PLT)的位置,通过这段代码获取func函数地址,并完成对它的调用.其过程如图5所示.
图5 ELF文件动态链接过程
由于GOT表是可写的,假设攻击者得到了一个任意地址写入的机会,把GOT表中的函数地址覆盖为shellcode地址,在程序进行调用这个函数时就会执行shellcode.而开启RELRO后,可以在程序解析符号时,将此过程中使用的重要数据结构标记为只读,从而防止攻击者修改这些重要结构.
RELRO又可以分为部分重定位只读(partial RELRO)和完全重定位只读(full RELRO)2种模式.这2种模式最大的区别在于,前者GOT表是可写的,而后者GOT表是只读的,但是因为Full RELRO严重影响了程序运行的性能,所以一般只在重要的程序中才会开启.
3.2.3Canary
Canary[14]是一种用于缓解缓冲区溢出的安全机制,这个名称源于曾经矿井中用来预警瓦斯气体的金丝雀(Canary),因为金丝雀瓦斯气体十分敏感,当瓦斯含量超过一定限度,人类还毫无察觉时,金丝雀却早已毒发身亡.
同样的道理,在栈上返回地址之前添加一个Canary值,函数返回时检查该值有没有被改变,便可知道是否发生了溢出.在GCC中,通过-fstack-protector选项和-fstack-protector-all选项以支持该功能.
图6 TLS结构
以x64平台为例,Canary是从fs:0x28偏移位置获取的,fs寄存器用于存放线程局部存储( thread local storage, TLS)信息,该结构在Glibc中的实现如图6所示:
Canary值由glibc产生并保存在tcbhead_t结构中,程序返回时,将栈上的Canary与该结构中保存的值比较,当检查失败时,执行glibc的__stack_chk_fail函数,并终止进程.
3.2.4FORTIFY_SOURCE
该技术源于2004年由RedHat的工程师提交的一个GCC和glibc补丁[15],其目的是提供一种轻量级的缓冲区溢出和格式化字符串漏洞防护机制 .它可以通过编译时定义FORTIFY_SOURCE标志来配置,在目前主流的Linux发行版甚至安卓平台中都能够见到它的身影.
对内存拷贝型函数,在用户调用过程中,FORTIFY_SOURCE将对图7中几种行为进行判定:
图7 FORTIFY_SOURCE的4种判断方式
1) 正确,不需要在编译或运行时检查.
2) 编译器不知道实际复制进buf中数据的长度,于是将此类不安全的函数替换为相对安全的__memcpy_chk和__strcpy_chk函数,如果发生溢出,就会调用内置的chk_fail ()函数,如图8所示:
图8 glibc-2.24中__memcpy_chk的实现
3) 编译器在编译的过程中会检测到溢出.
4) 在此类调用的方式下,编译器无从得知缓冲区的长度,在编译时不作检查,运行时无法检查,这种调用往往可能导致缓冲区的溢出.GCC中可以通过设置D_FORTIFY_SOURCE选项为1或2这2种不同的级别,前者认为错,而后者认为安全.
上述4类是FORTIFY_SOURCE对内存拷贝类函数的判断标准.
此外,FORTIFY_ SOURCE还有针对格式化字符串的保护,glibc中的printf等格式化字符串函数默认可以使用%n参数,并且能够任意指定格式化串的参数,例如%4$p,在FORTIFY_SOURCE=2的编译条件下,glibc启动了FORTIFY_SOURCE的相关保护.glibc在调用printf时,更改为调用__printf_chk函数,如图9所示:
图9 glibc-2.24中___printf_chk的实现
3.3 系统级加固措施
3.3.1内核地址空间布局随机化
内核地址空间布局随机化(kernel address space layout randomization, KASLR),顾名思义也就是内核地址的随机化,这项技术将在开机引导时将内核代码加载到随机位置,在Linux内核3.14版本中引入了该特性.
3.3.2内核页表隔离
内核页表隔离(kernel page-table isolation, KPTI)是Linux内核中的一种安全技术,它将现在被用户空间和内核空间共享使用的这张表分成2部分,内核空间和用户空间各自使用1个,从而解决页表泄露的问题.
3.3.3管理模式访问保护管理模式执行保护
在很多内核漏洞的利用过程中,通常会将内核指针重定向到用户空间,这种利用方式被称为ret2usr.为了抵御这种攻击,管理模式访问保护(supervisor mode access prevention, SMAP)和管理模式执行保护(supervisor mode execution prevention, SMEP)被提出.两者的作用分别是禁止内核访问用户空间的数据、禁止内核执行用户空间的代码.Linux内核从3.0开始支持SMEP,从3.7开始支持SMAP.
4 现代利用手段
4.1 SROP方法
SROP(sigreturn oriented programming)这种方法由阿姆斯特丹自由大学的Bosman等人[16]提出,该技术利用了Linux系统信号处理机制存在一个设计缺陷.和传统的ROP攻击相比显得更加简单、可靠、可移植.
了解Linux的信号处理机制是使用SROP技术的前提,这里可以参考图10,简要地了解一下Linux信号处理机制:
图10 Linux信号处理机制
当有中断或异常产生时,内核向进程发出一个Signal,此时进程被挂起,系统切换到内核态.内核执行do_signal()函数,并且最终调用setup_frame()函数,向用户栈中push一个保存有全部寄存器的值和Signal信息(定义在名为sigcontext的结构中),另外还会push一个sigruturn()系统调用的地址.此时的用户栈布局如图11所示.
图11 setup_frame()调用后用户栈
其中User Context和Signal Info这2部分被称为Signal Frame.之后,跳转到注册过的Signal handler中处理相应的Signal.Signal handler返回后,内核执行sigreturn系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新pop回对应的寄存器,最后恢复进程的执行.
观察上面信号处理的流程,可以发现主要的变动都在Signal Frame中.而Signal Frame是保存在用户的地址空间中,所以用户是可以读写的.
由于内核不会去保存这个Signal对应的Signal Frame,所以当执行sigreturn系统调用时,此时的Signal Frame并不一定是之前内核为用户进程保存的Signal Frame.到这里SROP技术的基本思想一目了然,它通过控制调用堆栈,将伪造的sigcontext结构置于调用栈上,当执行完sigreturn,伪造的sigcontext结构内容被恢复到寄存器中,便可控制程序流程.一个最简单的例子如图12所示.
2) 将rax设置为execve的系统调用号.
3) 将rip设置为syscalll指令的地址.
sigreturn系统调用将这些值恢复相应寄存器,最后恢复进程执行时,将会得到一个shell.
如果需要执行一系列的函数,就像构造一个ROP链一样,只需要作2处修改即可:
1) 修改栈指针指向下一个伪造的sigcontext.
2) 将程序指针设置为syscall;ret这个指令序列的地址,如图13所示.
SROP通常只需要一个gadget即可成功实施此攻击,使得这种攻击变得简单而有效,在文献[16]中,作者还给出了“syscall; ret”这个gadget在不同内核版本中出现的位置,有兴趣的读者可以去自行查阅.
图12 伪造的sigcontext结构
图13 SROP链
4.2 Blind ROP
Blind ROP技术由斯坦福大学的Bittau等人[17]提出,该技术可以在没有二进制程序和源码的情况下同时绕过NX,ASLR和Canary的安全保护,但必须符合以下条件:
1) 目标程序存在栈溢出漏洞;
2) 目标程序进程在崩溃之后会重新启动,且进程加载基址与原来相同,即使用fork重新创建子进程,但未采用execve执行磁盘上的程序(fork-only without execve).
BROP的主要思想就是首先通过不断的枚举,得到栈上正确的Canary,然后枚举出可用的Gadgets来构造write系统调用、远程dump内存,最终实现漏洞利用,具体过程下面将详细介绍.
1) 暴力枚举
不断增大输入缓冲区数据的长度,直到发现目标程序刚好崩溃,得到Canary之前栈空间的长度.
2) Stack Reading
因为Canary的变化只是在每个字节上,所以文献[17]中提出了一种名为Stack Reading的方法来快速枚举出正确的Canary值,原理如图14所示:
图14 通过Stack Reading 枚举出正确的Canary
首先只覆盖Canary的第1个字节,然后从0x00到0xFF枚举,当枚举到一个正确的数值时,服务器进程并不会崩溃,记录下这个字节,继续覆盖第2个字节,然后不断重复前面的过程,直到将Canary逐字节全部枚举出来.
0x00到0xFF有256种可能,所以在32位系统中,最多需要尝试4×256=1 024次,64位中最多需要尝试8×256=2 048次,整个过程只需几秒.
3) 寻找Stop Gadgets
接下来需要找到构造ROP链,从远程dump漏洞程序的内存.文献[17]提出了Stop Gadget这一概念,它寻找其他Gadgets取到了至关重要的作用.为了找到这种Gadgets,需要将返回地址覆盖为某个代码段的地址,然后通过不断枚举寻找(代码段的地址可以在上一步Stack Reading中获得,对于没有PIE的程序也可以从可执行文件默认的加载基址开始),当程序的执行流跳到这时,程序并不会崩溃,而是进入了无限循环,攻击者能一直保持连接状态.
4) 寻找Useful Gadgets
接下来,需要找到其他具有某些功能而不是会造成Crash的Gadget.这类Gadgets称为“Useful Gadgets”.
同样还是采用上一步枚举的方法,将返回地址覆盖为某个代码段的地址,返回地址之后填充多个Stop Gadgets,如图15所示.
当正在枚举的地址是一个无效的Gadget时,程序将会崩溃,如图16所示.
但是如果是一个有效的Gadget,程序将进入一个无限循环的状态,如图17所示.
以上是寻找Useful Gadgets的原理,而要从远程dump内存,可以通过构造write(int sock, void *buf, int len)来实现,接下来的关键就是如何找到相关的Gadgets.构造这个调用的4个Gadgets可以在图18所示的位置找到.
图15 枚举有效的Gadgets
图16 无效的Gadget导致程序崩溃
图17 有效的Gadget使程序保持运行状态
图18 通过Stack Reading 枚举出正确的Canary
在这之后,便可以通过dump下的内存,构造漏洞利用程序.
4.3 Stack Pivot
Stack Pivot[18]出现的时间已经很久了,但到目前为止,一直是漏洞利用中比较重要的辅助技术.目的是将栈劫持到一个攻击者能够控制的内存上去,在该位置再进行ROP.
假设攻击者控制了栈上部分区域,但是中间有一段不可控制的内存,这时攻击者需要控制栈指针跳转到可控部分,继续执行ROP指令,图19只是使用该技术的最简单情况,要想劫持栈指针还有很多方法,任何可以修改栈指针的Gadget都可以用于Stack Pivot.
图19 Stack Pivot
4.4 绕过full RELRO
在第4节中曾提到过,对于partial RELRO的情况,GOT表仍然是可写的,仍可以进行GOT表覆写,所以本节中将重点介绍绕过full RELRO的方法[19],此外这里假设读者已了解Linux的动态重定位原理,就不再对其进行详细介绍了.
当使用full RELRO时,所有的重定位将在加载时完成,GOT表被设置只读,不会有惰性解析(lazy binding)的过程,并且link_map结构的地址和_dl_runtime_resolve地址也不会初始化.在.dynamic段中的Elf_Dyn结构中,有一个DT_DEBUG条目,它的值是程序加载时,由动态加载器设置好的,指向堆中保存的一个r_debug类型的数据结构.此外,该结构的r_map域保存着一个指向link_map链表的指针,因此可以通过这个结构来恢复link_map的值,如图20所示.
图20 从DT_DEBUG条目恢复link_map
得到link_map的位置后,需要伪造一个重定位项(因为.got.plt是只读的).为此需要覆盖掉原来l_info结构的DT_JMPREL域的内容,使其指向一个伪造的动态条目,而这个动态条目则指向一个重定位项.这个重定位项引用了已经存在的函数符号,而r_offset则指向一块可写的内存区域,如图21所示.
图21 覆盖DT_JMPREL伪造动态条目
接着,还需要恢复_dl_runtime_resolve函数的指针,因为GOT表中已经没有_dl_runtime_resolve函数的指针了.解引用l_info域中的第1个link_map结构取得描述第1个共享库的link_map,而这个共享库是不被完全RELRO保护的.攻击者通过l_info[DT_PLTGOT]域来得到对应的动态条目(右侧的.dynamic),接着是.plt.got段(总是在右侧),其中的第2个条目里就有_dl_runtime_resolve的地址,如图22所示:
图22 _dl_runtime_resolve
最后,将link_map结构作为第1个参数,将一个新的.dynsym偏移作为第2个参数,就可以调用_dl_runtime_resolve函数了.但这里存在一个问题问题——_dl_runtime_resolve不仅会调用目标函数,还会尝试将目标函数的地址写到正确的GOT项中,但因为GOT是不可写的,程序会因此崩溃.为了解决这个问题,将原本指向.rel.dyn段的DT_JMPREL指向攻击者控制的一块内存区域,并在那块位置伪造一个Elf_Rel结构,且其r_offset域指向一块可写的内存区域,其r_info指向目标符号.所以,当一个库被解析时,它的地址将会被写到一个可写的位置,程序就不会崩溃,而且请求的函数也将会被执行.
5 总 结
近几年,二进制漏洞分析与利用技术已经发展得相当成熟,很多技术也被黑产所利用,也正因如此,软件安全领域得到了极大的发展,对个人终端、服务器的攻击越来越困难.从上面介绍的利用方法来看,虽然有能绕过各种安全机制的途径,但往往利用条件都有一些限制.如果程序开启了全部安全机制(NX,PIE,ASLR,Full RELRO等),那么就需要通过泄露地址等方式才能完成利用.
所以未来的二进制漏洞的发展趋势,可能会逐渐向其他方面转移.例如卡巴斯基实验室最近发布报告指出,近几年来,路由器等网络设备已成为高级持续性威胁(APT)常用的攻击渠道,结合目前的行业趋势,笔者预测未来Linux相关二进制攻防博弈主要集中在下面3个领域.
1) 移动终端
移动互联网时代早已到来,移动终端上每年发现的漏洞正在呈倍增长.特别是基于Linux的Android系统,有很多Linux内核漏洞也会影响到它.而对于iOS系统漏洞,其漏洞数量也是保持持续上升趋势.虽然由于iOS的封闭性和相关安全研究者较少,但是随着这几年相关安全书籍和文章增加,iOS设备的漏洞也会越来越多.
2) 物联网设备
物联网的诞生,势必将改变人们未来的生活,根据相关估算,到2020年,物联网设备的数量将会超过200亿.此外根据统计,暴露在公网上的物联网设备中,大部分都是使用Linux操作系统,根据目前相关资料以及近年来出现的与物联网设备相关的蠕虫病毒信息,针对物联网设备的攻击手段及技术已经很成熟.这些攻击手段与传统安全攻击手段类似,但更偏重于系统底层.同时由于物联网设备的系统特殊性和封闭性,都对这种攻击提供了很好的保护作用.
相对其他领域而言,物联网可以算是才刚刚起步,物联网设备的安全更是没有引起很多厂商的重视.作为和人们生活息息相关的物品,如果物联网产品存在安全问题,那就有可能直接影响到个人财产安全,甚至人身安全.例如曾经Black Hat上展示的关于心脏起搏器、胰岛素泵(注射胰岛素的设备,当注入过量时可导致患者昏迷)的入侵等等.
3) 云计算平台
云计算平台的架构可以简单地分为软件即服务(SaaS)、平台即服务(PaaS)、基础设施即服务(IaaS),它们分别为用户提供了应用软件、系统平台和IT基础设施资源等.
在SaaS层上,传统的Web漏洞、软件漏洞都可能会出现,而此层的漏洞风险更大,也是外部最容易触及到的.
在PaaS层上,由于大多数服务器采用Linux系统,Linux上的Web服务器漏洞、系统提权等漏洞依然需要人们重视.
在IaaS层上,则存在虚拟机漏洞、数据存储缺陷等安全问题.如果利用虚拟机逃逸漏洞,进而控制云平台主系统,那么造成的后果也是不堪设想的.