怎样攻进操作系统内核?
这是一个很有意思也很硬核的问题。
黑客通过应用程序的漏洞(如Java、PHP、Apache、IE、Chrome、Adobe、office等)获得执行代码能力后,由于操作系统安全方面的设定,很多情况下都是在沙盒或者低权限进程中运行,许多操作都无法进行。要想做更多高权限的事情,黑客通常会使用工具来提权。
x86
arm
操作系统的安全防线是建立在内核至高权限掌控的基础之上,无论是杀毒软件,沙箱,还是防火墙都运行在内核态。要突破安全包围,必须获得内核级权限的执行能力,才能和这些安全防护正面PK。
我们常常听到的Android系统ROOT和iOS系统的越狱就是内核攻击的典型应用。
获得内核权限以后,攻击者可安装rootkit级木马病毒,实现文件隐藏、进程隐藏、通信隐藏等高级木马功能,对系统危害极为严重。
内核0day漏洞,在APT攻击中是核武器级别的存在,地下网络安全交易市场价值巨大。
进入内核的四种方式
实际上,我们的程序每时每刻都在往返于用户空间和内核空间,只不过这些进出的大门都被操作系统提前安排好了,进入内核后该去哪里执行什么代码,是操作系统说了算,由不得我们自己的程序做主。
从用户态空间进入内核,有四种方式:
中断:
中断分为两种:硬中断和软中断
硬中断:硬件设备向CPU发起的中断信号
软中断:CPU执行int指令触发,早期的操作系统中实现系统调用就是通过这种方式,如Windows上的 int 2e,Linux上的 int 80
不管是硬中断还是软中断,CPU遇到以后都会保留当前执行的现场上下文,进入内核去执行中断处理函数。这些函数记录在中断描述符表IDT中,由操作系统初始化系统的时候预先安排。
异常:
异常是CPU在执行指令的过程中出现的问题,如除法指令的除数为0,访问的内存地址无效等等。
异常和中断处理逻辑类似,也是通过记录在IDT中的异常处理函数来执行,同样由操作系统初始化系统的时候预先安排。
系统调用:
系统调用大家应该就很熟悉了,我们要实现文件系统访问、网络I/O、进程线程使用、内存分配释放等等行为,都需要借助操作系统提供的编程接口来实现,这些接口叫做:系统调用。
前面提到,早期的x86架构下的CPU,没有专门的系统调用机制,操作系统们都使用软中断的形式来进入内核完成系统调用。
后来,因为系统调用是一个很高频的需求,软中断的方式效率有些低下,CPU加入了专门的系统调用机制,这包括一些专用的寄存器和一些专用的系统调用指令。如sysenter(x86)、syscall(x64)、swi(arm)。
通过系统调用进入内核后,该转向哪里执行也是操作系统提前安排好了,由不得应用程序做主。
开发驱动程序:
最后一种进入内核的方式就是开发驱动程序了,但加载驱动本身就需要极高的权限,所以这一点就不详述了。
以上就是通过正规途径,让我们的程序进入内核态运行的方式,可见,一旦进入内核态,执行流就进入了操作系统提前设置好的代码,攻击者没办法胡来。
有正规途径,当然就有不正规途径,也就是通过形形色色的漏洞攻击系统内核,从而使我们的程序进入内核态执行,这也是这篇文章的重点。
下面列举一些常见的攻击手法。
零地址攻击
学过C语言的朋友都知道,零地址,也就是NULL,在C语言中代表着空指针。
一些没有经验的程序员在写一些接口函数时,往往容易忘记检查指针参数是否是NULL,而导致程序的崩溃异常。
以32位操作系统为例,进程的地址空间是:
- 0x00000000~0xFFFFFFFF,
在x86架构上,内存一般以4KB页面单元进行管理。
你有没有想过,如果进程的地址空间中,以零地址(也就是NULL)开始的第一个4KB页面如果被分配了,会出现什么事情?
假设在内核中,有一段代码忘记对空指针的检查,就通过这个指针来调用函数。而攻击者提前将零地址页面分配准备好数据,就像这样:
这会发生什么后果?
后果就是,攻击者的代码将会在内核态下执行!
然而假设不只是假设,它曾真实发生过,就算强如微软的程序大佬,也会有忘记检查空指针的时候。
典型漏洞案例:CVE-2014-4113 Windows
释放后使用:UAF
除了空指针,悬在C/C++程序员头顶的还有一把利刃,这就是悬空指针。
悬空指针的意思是忘记对已经释放的内存/对象指针即时置空,而在后面又去使用这个指针,但此时对应的内存已被回收,引发不可预期的后果。
哎,这个指针可真是害人不浅啊!
你有没有想过,假如在对象释放后忘记对指针及时置空,后面又继续使用这个指针,就在这两个动作发生之间的那一段时间里,不怀好意的人去把原来释放的那块内存空间给“占领”了,布置好恶意的数据代码,会发生什么后果?
这就是大名鼎鼎的释放后使用UAF攻击!
UAF意思是Use After Free。
来看一个简单的例子:
两个对象,一个真,一个假,它俩对象占据的内存空间一样大。
下面这段代码,在原始对象释放后,忘记对obj指针置空,随后分配一个FakeObject,由于堆分配算法的原因,这俩对象一样大小,很大概率新的对象就会分配到刚刚释放的那片内存上去。
此时再通过原来已经悬空的指针来调用函数,实际上调用到了新的对象的函数,劫持了执行流程。
这只是一个简单的示例,真实环境中比这要复杂得多,但原理是一致的。
而这种事情一旦发生在系统内核,那后果就严重了,应用程序可以劫持内核空间的执行流程,执行自己的代码。
典型漏洞案例:CVE-2016-0728 Linux
整数溢出 + 数组越界
在操作系统中,有很多函数地址以表格的形式存储了起来,如:
- 系统调用表:SSDT/sys_call_table
- 中断描述符表:IDT
假如有办法能修改这些表格中的函数地址,改写成攻击者的代码地址,不就能有办法让我们的代码在内核模式下运行了吗?
道理是这么个道理,但这些表格本身就位于内核空间,普通应用程序别说去改写了,连读取都费劲。
那真的没办法了吗?
还是有的!
假如内核中某段代码在向某个数组中某个元素写入数据,又恰巧忘记了检查数组的下标是不是越界,再恰好这个下标可以通过应用程序来控制,那岂不是可以越界写?一不小心写到了前面那些函数表格里去了咋办?
你可能已经意识到了,这不是假设,是真实案例!
典型漏洞案例:CVE-2013-2094 Linux
这是一个Linux内核任意地址写入漏洞,通过精准控制系统调用的参数,实现改写IDT中的函数地址为恶意代码地址,实现在内核态执行恶意代码!
下面是一些内核攻击的真实案例:
安全防御
魔高一尺,道高一丈,形形色色的攻击也推进了操作系统和CPU的安全能力建设:
- Intel? 2011年在第三代Core处理器禁止内核状态下去执行用户地址空间指令 SMEP(supervisor mode execute prevention),设置CR4寄存器的bit20位为1开启
- ARM从armv7开始加入PXN技术,原理同SMEP
- Windows 8.1禁止使用零页地址内存
- Linux 2.6.26开始使用vm.mmap_min_addr限制地址空间最小值,防止使用零页内存
- ······
空指针、悬空指针、数组越界、整数溢出···这些一个个看起来不起眼的编程问题,如果发生在操作系统内核之中,那造成的后果便是灾难性的!可见,养成一个好的编程习惯有多重要!
连开发操作系统的大神程序员们都会犯错误,何况我们呢?
本文转载自微信公众号「编程技术宇宙」,可以通过以下二维码关注。转载本文请联系编程技术宇宙公众号。