在本文中,我们将以CVE-2020-17382漏洞为例来介绍内核漏洞如何被武器化的。
序言——为何驱动程序仍然是一个有价值的目标?
毫无疑问,内核是一个非常复杂的软件,即使Windows操作系统也不例外。由于缺乏源代码和无正式文档说明的API,它一直是最难审查的对象之一;现在,由于研究社区的巨大努力,相关的资料已经越来越丰富了。令人遗憾的是,近来内核不仅在复杂性上有所增加,同时,其缓解方式也有所改进。那为,攻击者什么要攻击驱动程序呢?除了Microsoft附带的驱动程序之外,第三方驱动程序也是第三方将代码执行权限提升至ring 0级别的唯一且轻松的途径。不过,从Windows 1607 Anniversary Update开始,目前仅允许加载具有WHQL认证进程签名的驱动程序,因此,构造带有“漏洞门”的代码并将其植入内核空间已经难上加难了。有关驱动程序签名过程的更多信息,请参见此处。
在这篇文章中,我们将为大家介绍的是一些“低垂的果实”,即那些隐藏在已签名的、受信任的生产性驱动程序中仍未被发现的漏洞,这些漏洞往往被广泛部署在消费者和企业终端上。
实际上,这些可利用的内核驱动程序漏洞可以将无特权的用户提升至SYSTEM权限,因为漏洞驱动程序可供任何本地用户使用。(好吧,有时易受攻击的驱动程序或内核组件甚至可以被远程利用:EternalBlue,大家还有印象吧?)
在这里,我们的主要目的,就是为大家介绍一套通过IDA和WinDbg完成漏洞的初步分析,然后运行简单的模糊测试,最后自下而上构建exploit的通用方法。
本文构建的exploit是基于CoreSecurity公司的安全研究人员Lucas Dominikow在MSI的Ambient Link驱动程序中发现的一个安全漏洞。虽然我们为Windows 7 SP1构建的exploit已经非常可靠了,但还不够优雅,不过,我们的重点还是Windows 10平台。
下面是我们已经测试过的两个Windows 10版本,分别为1709和2004版本:
19041.1.amd64fre.vb_release.191206-1406
16299.15.amd64fre.rs3_release.170928-1534
不过,我们不打算深入讨论2004版本,因为除了ROP gadget的偏移量之外,它和1709版本几乎一样。另外,PoC可以从我们的Github下载。
驱动程序的组织结构
从某种意义上讲,驱动程序只不过是一个可加载的内核模块,这意味着,除非写成完全独立的程序,否则它免不了要跟用户模式中的代码进行交互;不过,在此之前,用户模式的应用程序必须获得驱动程序的有效句柄,这只是与ring 0级别的代码进行通信的一种安全方式。
设备对象
正如我们很快就会看到的那样,用户应用程序可以通过调用CreateFile API来获取一个有效的句柄。这个函数需要接受一个符号链接,在我们的例子中,这个链接就是一个DEVICE_OBJECT元素。这个设备对象是由驱动程序本身创建的,作为任何用户应用都可以使用的通信通道,如果没有采取适当的访问控制的话,这肯定会增加攻击面——这一点是毋庸置疑的。
实际的设备对象是由Windows I/O管理器进行控制的,一旦收到有效的请求,它就会给用户模式的应用程序返回一个有效的句柄。
获取驱动程序句柄
DriverEntry与Driver对象
DriverEntry是驱动程序的入口点,它存在于每个驱动程序中。我们可以把它看作是驱动程序的main函数,类似于用户模式应用程序中的main函数。
DriverEntry函数接受的第一个参数是DRIVER_OBJECT结构体。这个结构体是由内核创建的,并且会在没有完成初始化的情况下传递给DriverEntry例程。完成加载后,驱动程序将根据自己的功能来填充这个结构体。该函数的第二个参数RegistryPath是指向注册表中的字符串的指针,我们可以通过注册表将配置参数键传递给驱动程序。
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(RegistryPath); NTSTATUS status; // device object UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\uf0DeviceObject"); PDEVICE_OBJECT DeviceObject; NTSTATUS status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject); // symlink UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\uf0SymLink"); status = IoCreateSymbolicLink(&symLink, &devName); }
从上面的片段中,我们还可以注意到DriverEntry例程是如何创建DeviceObject和符号链接的,而符号链接最终是提供给用户应用程序,以便与驱动程序进行通信的。同时,我们还可以注意到,驱动程序使用的两个API都带有前缀Io,即IoCreateDevice和IoCreateSymbolicLink,这表明它们与I/O管理器有关。
Major Functions
实际上,需要由驱动程序初始化的function称为Dispatch Routines,实际上就是DRIVER_OBJECT结构体的MajorFunction成员内部的函数指针数组。该数组描述了驱动程序支持哪些function。下面列出了最常见的function(更多function,请访问这里):
IRP_MJ_CREATE (0) IRP_MJ_CLOSE (2) IRP_MJ_READ (3) IRP_MJ_WRITE (4) IRP_MJ_DEVICE_CONTROL (14) IRP_MJ_INTERNAL_DEVICE_CONTROL (15) IRP_MJ_PNP (31) IRP_MJ_POWER (22)
我们可以把其中的每一个功能看作是用户模式标准函数的内核模式对应物。例如,IRP_MJ_CREATE相当于CreateFile,IRP_MJ_CLOSE相当于CloseFile等等。
由于本文的主旨是开发漏洞利用代码,所以,我们现在要把焦点转移到IRP_MJ_DEVICE_CONTROL上,因为它被用作一个“函数派遣器”来完成大部分内部驱动功能。我们可以注意到,每个Major Function的名称中都有一个IRP前缀:这是由于这些functions是为处理不同类型的IRP而设计的。但到目前为止,我们还没有提到任何关于IRP的东西,对不对?
IRP与IOCTL
正如我们在前面所看到的,通过导出的DLL函数和I/O管理器,可以实现用户模式代码与驱动程序的交互,其中,I/O管理器是管理用户模式代码与驱动程序之间各种请求的终极裁判。在每次收到请求时,I/O管理器都会制作一个I/O请求包(IRP),这是一个不透明的结构体,在MSDN上只有部分记录。驱动程序将寻找地址指向自己的IRP包,并在例程结束后将其发送回IoManager。
正如我们前面所看到的,一旦用户模式应用程序获得了有效的驱动句柄,它就可以开始通过发送IRP与驱动程序进行交互了;IRP最终将提供一个IOCTL代码,以调用特定的驱动例程。整个过程可以简化如下:
上面,我们已经学习了驱动程序运行原理方面的知识,接下来,让我们剖析一个易受攻击的驱动程序,看看我们是否可以通过充分利用其中的漏洞来使其武器化。
深入剖析MSIO64.SYS
下面是在驱动程序中挖掘安全漏洞时的重要检查事项:
· 驱动程序是否允许低权限用户直接与之交互;
· 导入地址表(IAT)中是否存在MmMapIoSpace或ZwMapViewOfSection;
· 是否存在定制的memmove或众所周知的不安全函数。
注:这些只是一个非常基本的起点,大家可以根据自己的经验继续扩充。
为了检查第一个注意事项,我们应该考察驱动程序的DACL(discretionary access control list,DACL),我们可以使用来自OSR的DeviceTree,这是一个非常方便的工具。
我们只需要搜索正确的符号链接,并检查嵌套在DRV选项卡下的DEV选项卡。然后,我们需要弹出Security Attributes窗口,并检查Everyone用户组具有哪些权限。这里有一个好消息:MsIo设备对象允许在其自身和任何类型的用户之间进行任何类型的访问,这意味着即使从低完整性进程也可以访问驱动程序。这是一个很好的前提。
我们只需要搜索正确的符号链接,并检查嵌套在DRV选项卡下的DEV选项卡。从那里我们必须弹出Security Attributes窗口,并检查Everyone组具有何种特权。这里有一个好消息:MsIo设备对象允许在其自身和任何类型的用户之间进行任何类型的访问,这意味着即使从低完整性进程中也可以访问该驱动程序。这是一个很好的开端。
虽然MmMapIoSpace和ZwMapViewOfSection是两个非常关键的函数,但是,它们并没有出现在我们的目标驱动程序中,所以,这里就不深入介绍了。就目前而言,我们只要知道:它们可用于将内核内存映射到用户的进程中(当然,这么做绝对是非常危险的!)即可。
接下来,处理用户模式缓冲区的任何函数也都必须进行边界检查,否则我们将导致经典的缓冲区溢出漏洞,这与影响MSIO64.sys的漏洞几乎一模一样。
驱动程序的逆向分析与调试方法
假装我们对该漏洞公告的内容一无所知,只是我们要处理的是一个普通的缓冲区溢出漏洞,将在特定功能匹配给定的IOCTL代码之后触发。
在跳转到IOCTL之前,我们应该从头开始分析驱动程序,换句话说,就是从DriverEntry函数开始。
我们立即注意到,这个函数只是一个存根,它指向RealDriverEntry函数(这个函数是我们重新命名过的)。那么,让我们跳转至该函数:
在RealDriverEntry函数中,我们可以获取正确的符号链接字符串,在我们的示例中是\\\Device\\MsIo,并将其标记下来。我们还可以注意到,第五行中的rdi的偏移量指向DriverObject。
移动到下一个代码块时,我们看到主函数处理程序sub_113F0的地址被复制到了rax寄存器中,然后被rdi/DriverObject以不同的偏移量(如0x68,0x70,0x80,0xE0)多次引用。我们可以通过WinDBG中的DRIVER OBJECT符号与这些偏移量进行对照,以获得精确的引用:
0: kd> dt _DRIVER_OBJECT nt!_DRIVER_OBJECT ... +0x068 DriverUnload : Ptr64 void +0x070 MajorFunction : [28] Ptr64 long
我们已经可以猜到,0x80和0xE0是MajorFunction例程本身内部的偏移量,而0x70是第一个参数。由于0x80与第一个参数相差16个字节,我们就可以推断出所有相关的派遣例程(dispatch routines):
1: kd> !drvobj MSIO64 2 [...] Dispatch routines: [00] IRP_MJ_CREATE fffff880055b63f0 MSIO64+0x13f0 [...] [02] IRP_MJ_CLOSE fffff880055b63f0 MSIO64+0x13f0 [...] [0e] IRP_MJ_DEVICE_CONTROL fffff880055b63f0 MSIO64+0x13f0
现在,让我们将注意力转移到驱动程序的真正重点上,即sub_113F0,也称为Major Function Handler,我已将其重命名为MsIoDispatch。
值得注意的是,rdi指向的是一个IRP结构体,该结构体在偏移量0x0b8(CurrentStackLocation)和0x38(IoStatus.Information)处被访问。
我们还可以用WinDbg动态地复核这些信息。让我们在MajorFunctionHandler的最开始处放置一个断点。
1: kd> u MSIO64+13f0 MSIO64+0x13f0: fffff802`4b4313f0 488bc4 mov rax,rsp 1: kd> bp MSIO64+13f0
并验证这些信息是否正确:
0: kd> dt nt!_IRP @rdx Tail.Overlay.CurrentStackLocation->* +0x078 Tail : +0x000 Overlay : +0x040 CurrentStackLocation : +0x000 MajorFunction : 0xe '' +0x001 MinorFunction : 0 '' +0x002 Flags : 0x5 '' +0x003 Control : 0 '' +0x008 Parameters : +0x028 DeviceObject : 0xffff8083`f7d97a70 _DEVICE_OBJECT +0x030 FileObject : 0xffff8083`fe9a2ba0 _FILE_OBJECT +0x038 CompletionRoutine : (null) +0x040 Context : (null) 0: kd> dt nt!_IRP @rdx Tail.Overlay.CurrentStackLocation->Parameters.DeviceIoControl. +0x078 Tail : +0x000 Overlay : +0x040 CurrentStackLocation : +0x008 Parameters : +0x000 DeviceIoControl : +0x000 OutputBufferLength : 0 +0x008 InputBufferLength : 0x80 +0x010 IoControlCode : 0x80102040 +0x018 Type3InputBuffer : (null)
从上面的结果中,我们可以看到0x80102040 IoControlCode:注意IOCTL值在调试/模糊测试一个新驱动程序的漏洞时是非常有用的,因为我们可以快速定位目标的内部function。
好了,我想我们对驱动程序的起始部分已经进行了足够的逆向和调试,所以,我们可以看一下我们最关心的部分:存在漏洞的function。但是我们还不知道(或者假装不知道)哪个IOCTL/例程是易受攻击的,对吧?那好,现在让我们来找出它。不过,该怎么找呢?首先,我们需要找到MajorFunction使用的所有IOCTL,然后我们可以使用IDA检查这些functions,或者对其进行模糊测试。
实际上,检索IOCTL列表是非常简单的事情,因为可以使用我最近移植到Python3和IDA Pro 7.5的这个插件。当然,计算IOCTL的方法并不是完美无缺的,但至少可以为合法的IOCTL提供一定的线索:
除了最初的0x2用于通过IRP_MJ_CLOSE终止Major Function外,其余四个似乎都是用于映射内部function的有效的IOCTL代码。
现在,我们已经掌握了所有的IOCTL,接下来,我们可以将这些值输入到这个小型的fuzzer中,它的灵感来自于Jaime Geiger。
这个fuzzer所需的参数是设备符号链接名、逗号分隔的IOCTL值和输入缓冲区长度。为了简化问题,我们可以始终使用单个IOCTL和1000字节的缓冲区大小。
C:\> python3 basic_fuzzer.py -d \\.\MsIo -i 0x80102040 -l 1000
这个简单的测试足以立即触发Bug Check,通过分析调用栈帧,我们看到Major Function的返回地址已经被我们的fuzzer的字符A所覆盖。
2: kd> k # Child-SP RetAddr Call Site 00 ffffa687`18b16608 fffff802`23c12802 nt!DbgBreakPointWithStatus 01 ffffa687`18b16610 fffff802`23c12087 nt!KiBugCheckDebugBreak+0x12 02 ffffa687`18b16670 fffff802`23b768d7 nt!KeBugCheck2+0x937 03 ffffa687`18b16d90 fffff802`23b907db nt!KeBugCheckEx+0x107 04 ffffa687`18b16dd0 fffff802`23b821ce nt!KiDispatchException+0x16202b 05 ffffa687`18b17480 fffff802`23b80234 nt!KiExceptionDispatch+0xce 06 ffffa687`18b17660 fffff802`231816b9 nt!KiGeneralProtectionFault+0xf4 07 ffffa687`18b177f8 41414141`41414141 MSIO64+0x16b9
掌握了哪些IOCTL易受攻击后,我们就可以利用IDA分析相关例程:
最终跟踪到了这个分支,它会调用自定义版本的memmove函数:
这正如Core Security公告中所说明的那样;此外,我们还证明了存储在rdx寄存器中的源缓冲区大小没有进行约束检查,它距离函数返回指针还有72个字节。毫无疑问,我们可以说这将是我们的漏洞利用代码的登陆区。
小结
在本文中,我们将以CVE-2020-17382漏洞为例来介绍内核漏洞如何被武器化的。首先,我们介绍了攻击者为什么会以驱动程序为攻击目标,然后,分析了驱动程序的组成部分,最后,讲解了驱动程序的逆向分析和调试方法。在下一篇文章中,我们将为读者介绍具体的漏洞利用过程。
本文翻译自:https://www.matteomalvica.com/blog/2020/09/24/weaponizing-cve-2020-17382/如若转载,请注明原文地址: