内核池溢出漏洞利用实战之Windows 7篇
发布日期:2024-05-19 浏览次数: 专利申请、商标注册、软件著作权、资质办理快速响应热线:4006-054-001 微信:15998557370
译者:an0nym0u5 一、前言 本文重点围绕HitmanPro独立扫描版(版本3.7.15-build 281)的内核池溢出漏洞(CVE-2017-6008)展开。这个工具是反病毒软件Hitman.Alert解决方案的一部分,并以SophosClean可执行文件的方式集成在了英国公司Sophos的解决方案中。早在2017年2月,Sophos公司就收到了此漏洞的报告并在2017年5月发布3.7.20-Build 286版本更新了补丁。我们使用Ioctfuzzer(https://github.com/Cr4sh/ioctlfuzzer)发现了首次crash。Ioctfuzzer是一款对输入输出请求包(以下简称IRP)进行模糊测试的强大易用的工具,我们利用此工具捕获到了API函数DeviceIoControlFile并利用该函数作为中间人代理。对于收到的每一个IRP包,它都会先行发送几个伪造的IRP包然后再转发原始IRP包。扫描伊始就出现了崩溃,崩溃发生在BAD_POOL_HEADER代码初始化阶段。阅读下文之前,我们强烈建议读者了解一些windows下的IOCTL和IRP知识。MSDN文档提供了大量可以帮助你更好地理解本文的信息。本文将要利用的是64位系统下的情景,这比32位系统下更难利用。 二、详细分析 2.1 崩溃数据分析 首先需要弄清楚BAD_POOL_HEADER错误码的含义,池是内核中动态分配的常见场所,此代码意味着处理池头部时出现了问题。池头是位于块开头的提供块有关信息的结构,如图1所示。 图1 池头结构图 池头很可能已经损坏才导致崩溃,为了验证此设想,我们利用调试器、转储工具还有测试器产生的日志快速找到了有缺陷的IRP包如下: 1 2 3 4 IOCTL Code: 0x0022e100 Outbuff: 0x03ffe1f4, Outsize: 0x00001050 Inbuff : 0x03ffd988, Insize: 0x00000200 //Device/Hitman Pro 37 [/??/C:/Windows/system32/drivers/hitmanpro37.sys] 这里有几点关键信息: C:/Windows/system32/drivers/hitmanpro37.sys:处理IRP的驱动程序。由于池损坏导致了崩溃,因此该驱动一定与崩溃有关。 IOCTL Code: 0x0022e100:该IOCTL代码提供了大量信息,稍后会作分析。通过逆向还可以获知以上驱动是如何处理IRP的。 Outsize / Insize:用来在池中分配一些缓冲区,也可能与池损坏有关。 参考MSDN文档,从IOCTL代码中可以得到如下信息: 1 2 3 4 5 DeviceType = 0x22 Access = 0x3 Function = 0x840 Method = 0x0 Method 0x0=METHOD_BUFFERED “对于METHOD_BUFFERED这种传输类型,IRP提供了一个指向位于Irp->AssociatedIrp.SystemBuffer的缓冲区的指针,该缓冲区代表调用DeviceIoControl和IoBuildDeviceIoControlRequest时的输入缓冲区和输出缓冲区,驱动器就在输入输出缓冲区之间传输数据。 对于输入数据,缓冲区大小由驱动器IO_STACK_LOCATION结构中的DeviceIoControl.InputBufferLength参数指定。 对于输出数据,缓冲区大小由驱动器IO_STACK_LOCATION结构中的DeviceIoControl.OutputputBufferLength参数指定。 系统为单个输入/输出缓冲区分配的空间大小是两个值中较大的那个。“ 最后,为了弄清楚在正常情况下IOCTL是如何发送的,我们逆向了HitmanPro.exe可执行文件,利用IOCTL代码和逆向工具IDA快速定位到了问题函数。 可见,分配给DeviceIoControl的Outsize和Insize与崩溃数据吻合,这种情况下,IRP管理器分配的系统缓冲区大小在正常情况下至少为0x1050字节。 2.2 逆向驱动器 我们已经掌握了很多崩溃有关的信息,是时候逆向驱动器hitmanpro37.sys来看看IOCTL的句柄了。首先,参照IOCTL代码定位调度IRP的函数,通常它是包括一些switch跳转的庞大函数,还好该驱动器并不大我们很快找到了调度器: 跟踪跳转逻辑,我们找到了处理存在漏洞的IOCTL的函数,IRP提供的SystemBuffer首先被传给IoGetDeviceObjectPointer函数的ObjectName参数: 然后, 非常不错进行到这里了,还记得IOCTL用到的METHOD_BUFFERED方法吗? “系统为单个输入/输出缓冲区分配的空间大小是两个值中较大的那个。” 这意味着我们完美控制了SystemBuffer的值,驱动器使用硬编码的值0x1050调用memset,如果SystemBuffer值小于0x1050,调用memset会使池损坏进而导致崩溃,这里我们称之为内核池溢出漏洞。虽然这么说,但是我们到目前为止还没有任何办法控制往此缓冲区写入。它被设置为0然后被DeviceObject中的地址和名字填充,这只有管理员权限才能控制得了,因此此漏洞只会导致操作系统崩溃,该漏洞编号是CVE-2017-6007。 2.3 扭转 到此我们并不甘心,又逆向了更多的处理程序,我们随机挑选了一个处理程序,这真的很有趣: SystemBuffer(我们的输入)参数用在了一个子函数中,如果子函数返回正确的值,一些数据会通过mwmcpy拷贝到SystemBuffer中,此函数的控制码是0x00222000: 1 2 3 4 DeviceType = 0x22 Access = 0x0 Function = 0x0 Method = 0x0 还是利用了同样的方法:METHOD_BUFFERED。 如果我们足够幸运的话,这里可能会有类似的漏洞出现,然而,驱动器的这部分代码非同寻常: a.我们没有在可执行程序HitmanPro中找到任何利用控制码0x0022200发送IRP的函数,因此在驱动器的这个位置下断点不会触发任何异常。 b.我们无法确定这个函数的确切功能,但我们找到了一个漏洞,这已经足够啦。 因此,逆向之旅又开始了。处理驱动后写成了如下伪代码: 驱动器利用SystemBuffer提供的句柄获取到FILE_OBJECT,如果FILE_OBJECT空闲就会调用ObQueryNameString来获取FILE_OBJECT指向的文件名并存放在临时缓冲区,然后从临时缓冲区复制文件名到SystemBuffer。 驱动器通过如下参数调用memcpy: a. dest = SystemBuffer ; 大小由我们控制 b. src = 我们提供的句柄文件名,写入和大小均可控 c. n = src缓冲区的大小; 唯一的限制就是ObQueryNameString函数,该函数是受保护的,如果源太大就不会复制任何内容到目标区域。 由于目标区域是硬编码0x400大小的缓冲区,我们就不能给出大于0x400的文件名,当然,0x400个字节对于利用缓冲区溢出已经足够了。 三、利用 3.1 介绍 既然是内核池溢出漏洞,我们就有很多攻击方式可以利用了。要想利用此漏洞,Tarjei Mandt的文章思路再好不过了,如果你想完全了解下一步发生了什么,它将是你必读的文章。这里我们采用的攻击方式是配额进程指针覆盖,我们选择它是因为这是最优雅的方式之一,32位和64位系统均能实现利用,在此攻击中,我们必须覆盖下一个块的进程指针。 该池头的最后4个字节有一个指向EPROCESS结构的指针,当有池块被释放时,如果PoolType设置了Quota bit,该指针会减小某些与EPROCESS对象有关的值: a. 该对象的引用计数(一个进程是一个对象) b. QuotaBlock字段指向的值 减值之前会有一些检查,我们不可以直接利用对象的ReferenceCount,不过可以伪造一个EPROCESS结构,并在QuotaBlock字段设置任意指针以减随机的值(内核空间也可以哦)。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 kd> dt nt!_EPROCESS +0x000 Pcb : _KPROCESS +0x098 ProcessLock : _EX_PUSH_LOCK +0x0a0 CreateTime : _LARGE_INTEGER +0x0a8 ExitTime : _LARGE_INTEGER +0x0b0 RundownProtect : _EX_RUNDOWN_REF +0x0b4 UniqueProcessId : Ptr32 Void +0x0b8 ActiveProcessLinks : _LIST_ENTRY +0x0c0 ProcessQuotaUsage : [2] Uint4B +0x0c8 ProcessQuotaPeak : [2] Uint4B +0x0d0 CommitCharge : Uint4B +0x0d4 QuotaBlock : Ptr32 _EPROCESS_QUOTA_BLOCK [...] typedef struct _EPROCESS_QUOTA_BLOCK { EPROCESS_QUOTA_ENTRY QuotaEntry[3]; LIST_ENTRY QuotaList; ULONG ReferenceCount; ULONG ProcessCount; } EPROCESS_QUOTA_BLOCK, *PEPROCESS_QUOTA_BLOCK; 3.2 溢出实现 为了实现配额进程指针溢出攻击,我们需要利用我们的溢出覆盖两个东西: a.下一个块的池类型,因为我们需要确定已经设置了Quota bit b.下一块的进程指针,用一个指向伪造的EPROCESS结构的指针替换它 因为我们必须获取到下一块的进程指针,所以无论如何必须要覆盖下一块的整个池头,然而我们不能往池头发随机的数据否则会触发BSOD。 我们必须确定如下字段是正确的: a.块大小 b.前一个块大小 c.池类型 满足此条件的唯一方式是准确获取要覆盖的块,这可以通过池喷射技术来实现。 这里不会详细阐述如何实现池喷射,但基本思路就是获取这种类型的池: 看起来类似这样: 我们的溢出效果: 溢出前: 溢出后: 3.3 Payload 好了,我们可以在任何地址实现减任何值了,下一步做什么呢?我们搜索到了一篇很好的Cesar Cerrudo[4]的文章,文中讲述了几种提权的技术。还有一点也很有趣,在TOKEN结构中有一个Privileges字段: 1 2 3 4 5 6 7 8 9 10 11 typedef struct _TOKEN { [...] /*0x040*/ typedef struct _SEP_TOKEN_PRIVILEGES { UINT64 Present; /*0x048*/ UINT64 Enabled; UINT64 EnabledByDefault; } SEP_TOKEN_PRIVILEGES, *PSEP_TOKEN_PRIVILEGES; [...] }TOKEN, *PTOKEN; 该字段是包含位掩码的结构体,位掩码Enabled定义了进程可执行的操作。该位掩码默认值为0x80000000,具有SeChangeNotifyPrivilege权限,从该位掩码中去掉一位变成了0x7fffffff,就拥有了更大的权限,MSDN文档提供了该位掩码的可用的权限列表: https://msdn.microsoft.com/fr-fr/library/windows/desktop/bb530716(v=vs.85).aspx 但是我们没有_TOKEN结构的地址,我们也不应该有因为那是内核地址。幸运的是,我们可以利用众所周知的NtQuerySystemInformation通过其句柄获取任何对象的内核地址。还可以通过调用OpenProcessToken()函数为我们的token赋予句柄,如果你想更深入了解NtQuerySystemInformation()函数和常见的内核地址溢出你应该参考这里。 我们决定触发这个漏洞以获取SeDebugPrivilege权限,该权限可以实现控制系统所有进程,你可以获取任何你想要的权限。SeDebugPrivilege权限可以允许我们在系统进程中启动线程并反弹一个系统shell。 四、结论 注意,这个exploit不能在windows 8及更高版本系统中使用,毕竟微软在防御内核漏洞方面做了大量工作。实际上,虽然此exploit不能用在windows 8及更高系统版本上并不意味着这些版本不能被攻破,你可以在github上看到我的exploit源代码,windows 10系统下如何利用类似的漏洞这是Nuit du Hack XV大会的主题。 五、参考文献 [1] https://github.com/Cr4sh/ioctlfuzzer– Simple ioctl fuzzer [2]https://msdn.microsoft.com/en-us/library/windows/hardware/ff543023(v=vs.85).aspx – Defining IOCTL code [3] http://www.mista.nu/research/MANDT-kernelpool-PAPER.pdf– Tarjei Mandt paper [4]http://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernel_WP.pdf – Easy local Windows Kernel exploitation by Cesar Cerrudo. [5]https://recon.cx/2013/slides/Recon2013-Alex%20Ionescu-I%20got%2099%20problems%20but%20a%20kernel%20pointer%20ain't%20one.pdf— Leaking Kernel Addresses [6] https://github.com/fishstiqz/poolinfo– This extension is great for investigating the pool state [7] https://github.com/cbayet/Exploit-CVE-2017-6008– Source code of the exploit 本文由 安全客 翻译,转载请注明“转自安全客”,并附上链接。 原文链接:http://trackwatch.com/kernel-pool-overflow-exploitation-in-real-world-windows-7/