Windows 10 内核漏洞利用防护及其绕过方法

0x00 引⾔

本⽂介绍了 Windows 10 1607 和 1703 版本中引⼊的针对内核漏洞利⽤的防护措施,在此基础上将给出相应的绕过⽅案,从⽽使我们能够重新获得内核态下的 RW primitives,并进⼀步实现 KASLR 绕过以及内核中 shellcode 的执⾏。尽管微软对于 Windows 10 内核的保护在不断提升,我们还是有可能找到办法来进⾏绕过,不过这对安全研究者的技术要求来说变得越来越⾼了。


0x01 关于内核态 RW primitives

为了应对 Windows 10 内核的漏洞利⽤缓解⽅案,研究者们借鉴了 ring3 层浏览器 Exp 中提及的内存 RW primitives 概念,即接下来我们讨论的 ring0 层任意内存读写能⼒,⽽在获取内核态 RW primitives 时最常借助的两个对象是 Bitmap和 tagWND 。

⾸先来看 bitmap primitive,此技术利⽤了 GDI 对象 Bitmap ,在内核中⼜被称为 Surface 对象。我们可通过 CreateBitmap 函数创建 Surface 对象以构造出两个内存中相邻的 Bitmap ,再借助 WWW(Write-What-Where)漏洞对第⼀个 Surface 对象的⼤⼩进⾏修改,此⼤⼩(也就是 bitmap 位图的⻓宽)由对象中偏移 0x20 处的成员变量 sizlBitmap 所控制。

当 bitmap 位图的⼤⼩扩增后,我们可修改第⼆个 Surface 对象中指向其 bitmap 位图内容的指针[6],通过 API 函数 SetBitmapBits 和 GetBitmapBits 能获得内核中任意内存读写的能⼒。具体的实现如下:

⽽若想利⽤ WWW 漏洞实现前述的重写操作,我们必须要能够定位内核中的 Surface 对象,⼜因为要求在 Low Integrity级别下也能完成该操作,所以不能使⽤ NtQuerySystemInformation 这类 API 函数。好在我们可以通过 PEB 中的 GdiSharedHandleTable 结构来定位 Surface 对象的地址,此结构包含所有的 GDI 对象,当然也就包括了 Surface对象,借助⽤户态下 Bitmap 对象句柄可找到正确的表⼊⼝,从⽽得到相应的内核地址。

接着我们看下基于 tagWND 对象的内核态 RW primitives,同 bitmap primitive 技术类似,这⾥需要借助两个窗⼝对象,对应的内核对象称为 tagWND ,它们在内存中彼此相邻。

在 tagWND 对象中包含⼀块叫 ExtraBytes 的可变区域,其⼤⼩由 cbWndExtra变量控制,通过破坏内存中的此变量我们可以实现越界(OOB)修改相邻tagWND 对象,即借助 SetWindowLongPtr 函数来修改相邻的 tagWND 对象。其中, StrName 变量是指向窗⼝标题名的指针,通过修改此变量,再借助⽤户态下的 InternalGetWindowText 和 NtUserDefSetText 函数则可实现任意内核地址读写[7]。此技术中 Write primitive

的实现如下:

同样,我们也需要获取内核中 tagWND 对象的地址。这可以借助 User32.dll 模块导出结构 gSharedInfo 中的 UserHandleTable 来得到,该表涵盖了内核桌⾯堆(Desktop Heap)上的全部对象,因⽽通过⽤户态窗⼝对象句柄可以查找到对应的内核 tagWND 对象地址,相关代码如下 :

此外,我们知道⻚表包含有虚拟内存的元信息,如表示⻚⾯是否可执⾏以及⻚⾯是否属于 ring0 的⽐特位。所以为了解决ring0 下内存⻚不可执⾏的问题,研究者们普遍都采⽤⼀项称作 PTE(Page Table Entry)覆写的技术,其思路是先在ring3 下分配 shellcode 所需的内存空间,⽽后得到相应的 PTE 地址并修改指向的元数据信息。

借助内核态 Write primitive 对所分配内存⻚的 PTE 内容进⾏覆写,我们可实现⻚⾯执⾏属性和特权级的修改,从⽽可以将⽤户态内存转换为内核态内存,以此绕过 SMEP 保护。在 Windows 10 1507 和 1511 版本中,⻚表的基址是固定的,可通过如下算法来获取特定内存的 PTE 地址:

通过 PTE 覆写技术也能改变 ring0 下内存⻚的执⾏属性: 

⽽在很多情况下,我们需要获取 ntoskrnl.exe 模块的基址。由于不能再借助 NtQuerySystemInformation 函数,我们采⽤另⼀种很有效的⽅式,也就是利⽤ HAL Heap [8],其通常被分配到固定地址上(0xFFFFFFFFFD00000)且偏移0x448 处包含有指向 ntoskrnl.exe 模块的指针。我们先利⽤内核态 Read primitive 读取 0xFFFFFFFFFD00448 处的指针,然后按照查找 “MZ” 头部的⽅法即可定位出 ntoskrnl.exe 模块的基址,具体实现如下: 

需要注意的是,本部分内容适⽤于 Windows 10 1507 和 1511 版本。 


0x02 Windows 10 1607 中新增的内核漏洞利⽤防护

接着我们看下在 Windows 10 周年更新版本(即 Windows 10 1607 版本)中引⼊的缓解内核漏洞利⽤的新防护措施。⾸先,⻚表基址在启动时进⾏了随机化处理,这使得早前从虚拟地址到 PTE 地址的转换算法不再有效[9],该措施能缓解⼤部分内核 Exp 中⽤来创建 ring0 下可执⾏内存的⽅法。

其次, GdiSharedHandleTable 表中 GDI 对象的内核地址被移除了,这意味着我们不能再借助查找 GdiSharedHandleTable 的⽅法来定位内核中 Surface 对象的地址,从⽽也就⽆法修改 Surface 对象的⼤⼩,即内核中的 bitmap primitive 变得不再有效。

最后,当借助 InternalGetWindowText 和 NtUserDefSetText 函数操作 tagWND 对象时,其中的 strName变量必须为指向桌⾯堆(Desktop Heap)的指针[10],这直接限制了原有 tagWND primitive 技术中内核地址的读写范围。


0x03 Windows 10 1607 内核态 RW primitives

此部分内容将讨论如何绕过现有保护来重拾内核态 RW primitives。⾸先看下 bitmap primitive,我们要解决的问题是如何获取内核中 Surface 对象的地址。对于 Surface 对象,如果其⼤⼩⼤等于 0x1000 字节,那么它会被分配到 Large PagedPool 中,⽽如果⼤⼩恰好为 0x1000 字节,那么相应分配到的内存⻚是私有的。

另外,若⼀次性分配许多⼤⼩为 0x1000 字节的 Surface 对象,则它们所在的内存⻚将会是连续的。因⽽只要能定位到其中的⼀个 Surface 对象,就⾃然能找到相邻的多个 Surface 对象,这在获取内核态 RW primitives 时是必要的。 LargePaged Pool 的基址在启动时经过了随机化处理,不过我们可以借助内核地址信息泄露来得到,可以观察到 TEB中 Win32ThreadInfo 字段的内容如下: 

该泄露地址正是我们所期望的,只需移除低位的⽐特即可得到 Large Paged Pool 基址。如果我们创建的 Surface 对象⼤⼩⾮常⼤,那么它距此基址的偏移是可被预测的,如下为相关代码: 

⽽由固定偏移 0x16300000 对 Win32ThreadInfo 指针进⾏转换后得到的地址可造成 Surface 对象的信息泄露: 

在上述 Surface 对象分配完成后,可以观察到 leakPool 函数返回地址对应的内存分布如下: 

虽然显示的是 bitmap 位图内容,但确实说明该地址指向 Surface 对象。分析可知该指针⼏乎总是指向第⼆个 Surface 对象,我们将此对象释放掉,释放空间⽤⼤⼩正好是 0x1000 字节的 Surface 对象再次填充,如下例⼦中我们填充了⼏乎近10000 个的 Surface 对象: 

再次观察泄露地址处的内存分布,可以看到此时对应的是⼀个 Surface 对象: 

由于 sizlBitmap 变量落在可预测的地址上,因⽽我们能够再次利⽤ WWW 型漏洞来修改 Surface 对象的⼤⼩。

接着再来看 tagWND primitive,当调⽤ InternalGetWindowText 和NtUserDefSetText 函数时, tagWND 对象中的 strName 指针必须指向桌⾯堆(Desktop Heap),此限制是由新引⼊函数 DesktopVerifyHeapPointer 进⾏检测的,相关代码⽚段如下: 

可以看到,保存 strName 指针的寄存器 RDX 先后与桌⾯堆的基址以及最⼤地址进⾏⽐较,即检测 strName 指针是否位于桌⾯堆中,任何⼀个⽐较条件不满⾜都会触发错误。⽽由分析可知,桌⾯堆的地址范围是由 tagDESKTOP 对象所确定的,指向该对象的指针取⾃ tagWND 对象,⼆者的对应关系如下: 

即⽤作⽐较的 tagDESKTOP 对象其指针取⾃ tagWND 对象的 0x18 偏移处。虽然我们⽆法避开这些检测,但是函数中并没有就 tagDESKTOP 对象指针的有效性进⾏校验,因⽽存在伪造 tagDESKTOP 对象的可能。当借助 SetWindowLongPtr 函数修改 strName 指针时,我们也将相应修改 tagDESKTOP 对象的指针。如下代码可⽤于伪造 tagDESKTOP 对象: 

借助伪造的 tagDESKTOP 对象,我们可以控制 Exp 中桌⾯堆的基址和最⼤地址,使之恰能满⾜相应 strName 指针的检测条件,具体实现如下: 

按照本部分讨论的绕过⽅法,我们最终得以重获基于 Bitmap 和 tagWND 对象的内核态 RW primitives。 


0x04 Windows 10 1703 中新增的内核漏洞利⽤防护

我们继续来看 Windows 10 创意者更新版本或称为 Windows 10 1703 版本,该版本进⼀步增强了内核防护。针对tagWND primitive 的缓解措施主要体现在两个⽅⾯,⾸先 User32.dll 模块 gSharedInfo 结构中的 UserHandleTable 表发⽣了变化,原先包含的桌⾯堆(Desktop Heap)中对象的内核地址信息都被移除了。

如下为 Windows 10 1607 UserHandleTable 表的内容: 

对应到 Windows 10 1703 中:

与 Windows 10 1607移除GdiSharedHandleTable表中的内核地址是同⼀道理,这意味着我们不能再借助之前的⽅法来定位 tagWND 对象了。其次,对于SetWindowLongPtr 函数来说,所写⼊的 ExtraBytes 区域不再位于内核中了,可以知道指向 ExtraBytes 区域的指针取⾃ tagWND 对象的 0x180 偏移处,如下图所示:

通过调试,我们看到 R14 中的数值 0xFFFFF78000000000 被写⼊到 RCX 表示的地址中,该地址为⽤户态下的地址: 

这使得我们⽆法对第⼆个 tagWND 对象的 strName 指针进⾏修改。

此外,该版本中还有其它两点变化,其⼀, Surface 对象的头部⼤⼩变了,增加了 8 个字节,虽然只是⼩变化,但我们还是要给予考虑,否则会导致分配时的对⻬操作失败。其⼆, HAL Heap 进⾏了随机化处理,这意味着我们⽆法再经由地址0xFFFFFFFFFD00448 找到指向 ntoskrnl.exe 模块的指针。 


0x05 Windows 10 1703 内核态 RW primitives

伴随 Windows 10 1703 新引⼊的这些防护策略,原先的内核态 RW primitives 都变得不再有效。不过对于 bitmapprimitive,其改动较⼩,只需简单修改位图⼤⼩以确保 Bitmap 对象仍占 0x1000 字节即可。相对来说,重拾 tagWNDprimitive 要复杂得多,接下去我们将讨论这部分内容。 

由分析可知, TEB 中的 Win32ClientInfo 结构同样也变了,原先在其 0x28 偏移处表示的内容为 ulClientDelta ,即内核桌⾯堆与其在⽤户态下映射间的 delta 值,现在的内容则为: 

可以看到 0x28 偏移处的内容被⼀个⽤户态指针替代了,该指针直接就是相应⽤户态映射的起始地址,所示如下: 

此例中,这两块区域的内容完全相同,桌⾯堆的起始地址为0xFFFFBD2540800000。虽然 UserHandleTable 表中基于句柄查询的元信息被移除了,但真实数据仍然会进⾏⽤户态下的映射操作。通过⼿动搜索⽤户态下的映射内容是有可能定位到句柄的,进⽽可以计算出对象在内核中的地址。如下代码实现了⽤户态映射地址查找以及计算与桌⾯堆间的 delta值: 

⽽定位 tagWND 对象内核地址的代码则如下: 

这使得我们可以绕过针对 tagWND primitive 的第⼀点防护,不过就算重新定位到了 tagWND 对象,仍还有⼀个问题需要解决,因为在 manager/worker 对象组合中我们⽆法再借助 SetWindowLongPtr 函数来修改第⼆个 tagWND 对象的 strName 指针了,所以还是不能实现任意内核地址读写。

我们已经知道在 tagWND 对象中 ExtraBytes 区域的⼤⼩由 cbWndExtra 变量所控制,通过 RegisterClassEx函数注册窗⼝类时会对其进⾏赋值,⽽在初始化 WNDCLASSEX 结构过程中,另⼀个称作 cbClsExtra 的变量引起了我们的注意,所示如下:

它表示的是 tagCLS 对象中 ExtraBytes 区域的⼤⼩,该对象与 tagWND 对象存在关联。分析可知 tagCLS 对象同样被分配到桌⾯堆中,且由于相应窗⼝类是在创建 tagWND 对象前注册的,这使得 tagCLS 对象正好被分配到了 tagWND 对象之前,在完成第⼆个 tagWND 对象的分配操作后,将得到如下的内存布局: 

通过重写 tagCLS 对象的 cbClsExtra 值⽽⾮ tagWND1 对象的 cbWndExtra 值,我们得到了与之前相类似的情形, SetClassLongPtr 函数可⽤于修改 tagCLS 对象的 ExtraBytes 区域,该函数所写⼊的区域仍然位于桌⾯堆中,因⽽我们⼜能对 tagWND2 对象的 strName 指针进⾏修改了。

实现任意地址写操作的代码如下: 

同理可以实现任意地址读的功能,⾄此,我们就完整绕过了 Windows 10 1703 版本中引⼊的针对本⽂所讨论内核态 RWprimitives 的防护策略。


0x06 KASLR 保护绕过

下⾯我们讨论 KASLR 的绕过, Windows 10 1607 和 1703 版本中引⼊的防护措施能够缓解所有已知的内核信息泄露。此类漏洞通常是因为设计上的问题,例如最近的这两个 KASLR 绕过漏洞就是由于 HAL Heap 未随机化以及 SIDT 汇编指令问题导致的, Windows 10 1703 和 1607 版本中分别对此给予了修复。

然⽽ Exp 的编写经常需要⽤到驱动的内核地址,因此我们有必要找寻新的可以导致内核信息泄露的设计缺陷。这⾥采⽤的策略是把 KASLR 绕过和特定内核态 Read primitive 结合起来,因此,我们将分别针对 bitmap primitive 和 tagWNDprimitive 给出相应的 KASLR 绕过⽅法。

我们先讨论与 bitmap primitive 有关的绕过思路。在 REACTOS 项⽬中(即 Windows XP 系统逆向部分)有给出内核对象Surface 的定义:

其中, hdev 成员的描述如下: 

这⾥问题就落到 PDEVOBJ 结构上,还好 REACTOS 项⽬中也给出了该结构的定义: 

上述 PFN 类型成员为函数指针,我们可借此得到指向内核驱动的地址,因⽽具体思路就是经由 hdev 成员来读取PDEVOBJ 结构中的函数指针。然⽽通过查看内存中的 Surface 对象,我们却发现 hdev 的值为 NULL: 

分析可知,借助 CreateBitmap 函数来创建 Bitmap 对象时不会对 hdev 成员进⾏赋值,不过另⼀ API 函数,即 CreateCompatibleBitmap 函数,也能⽤于创建 Surface 对象,借助该函数创建的 Bitmap 对象中 hdev 指针是有效的: 

其 0x6F0 偏移处的指针指向了驱动模块 cdd.dll 中的 DrvSynchronizeSurface 函数: 

为了得到 hdev 指针,我们需要进⾏以下操作。⾸先获取距 leakPool 函数返回地址 0x3000 偏移处 Bitmap 对象的句柄,⽽后将此 Surface 对象释放并借助 CreateCompatibleBitmap 函数重新分配多个 Bitmap 对象,实现代码如下 :

执⾏过后, leakPool 泄露地址 0x3030 偏移处即为要找的 hdev 指针,进⽽可得到指向 DrvSynchronizeSurface 函数的指针。分析可知, DrvSynchronizeSurface 函数中 0x2B 偏移处包含的调⽤最终指向了 ntoskrnl.exe 模块中的ExEnterCriticalRegionAndAcquireFastMutexUnsafe 函数,所示如下 :

基于这个指向 ntoskrnl.exe 模块的指针,再配合 “MZ” 头部查找法,即每次以 0x1000 字节间距往回搜索,我们可定位到相应的基址。完整的 ntoskrnl.exe 模块基址查找过程如下: 

接下来我们给出与 tagWND primitive 有关的 KASLR 绕过思路,所⽤⽅法和前⾯讨论的很类似。借助 REACTOS 项⽬中Windows XP 系统的结构说明⽂档,我们知道 tagWND 对象的 head 成员是⼀ THRDESKHEAD 结构体,其中包含另⼀称作 THROBJHEAD 的结构体,⽽ THROBJHEAD 结构体中⼜包含⼀个指向 THREADINFO 结构体的指针,它们的对应关系如下,先是 tagWND 结构:

然后是 THRDESKHEAD 和 THROBJHEAD 结构: 

最后是 THREADINFO 结构,其中包含称作 W32THREAD 的结构体: 

⽽在 W32THREAD 结构起始处是⼀指向 KTHREAD 对象的指针: 

虽然此过程经历了多次结构间辗转且⽂档资料也较⽼了,但就算是Windows 10 1703 版本, KTHREAD 对象在其 0x2A8偏移处仍旧包含指向 ntoskrnl.exe 模块的指针,因⽽借助给定的 tagWND 对象内核地址我们能够得到 ntoskrnl.exe 模块的基址。通过分析 64 位 Windows 10 系统中相应的结构,我们知道 tagWND 对象 0x10 偏移处的指针指向的是 THREADINFO 对象,经由该指针能得到 KTHREAD 对象的地址,所示如下: 

我们将上述的 KASLR 绕过步骤封装到单个函数中,⽽借助指向notoskrnl.exe 模块的指针来查找基址的⽅法跟前⾯相同,最终的实现代码如下: 

0x07 函数动态查找

接着我们讨论如何查找特定驱动函数的地址,这在内核漏洞利⽤中是很重要的。对于不同版本的系统,借助固定偏移进⾏函数定位的⽅法可能并不通⽤,更好的⽅法是借助内核态 Read primitive 来动态定位函数。

⽬前为⽌我们所实现的 Read primitive 只能读取 8 字节的内容,但不论是基于 Bitmap 对象还是 tagWND 对象的primitive 都可进⼀步修改成任意字节的读取。就 bitmap primitive 来说,这和 bitmap 位图的⼤⼩有关,通过修改相应字段可以达到任意字节读取的⽬的,实现代码如下:

可以看到,代码对 bitmap 位图⼤⼩进⾏了修改且⽤于保存最终 GetBitmapBits 返回内容的缓冲区⼤⼩也变了。我们可以借此将 ring0 下完整的驱动或其相关部分 dump 到 ring3 空间中,以便进⾏后续的查找操作。

这⾥我们借助哈希值来定位函数地址,其中哈希值的计算也⽐较简单,仅仅是将对应地址处 4 个相互间隔 4 字节的QWORD 值相加。虽然没有考虑冲突处理,但结果表明此算法还是很有效的,具体实现如下: 

0x08 ⻚表基址的随机化

继续往下来看⻚表基址随机化的问题。前⾯我们提到过获取 Windows 10 系统 ring0 层可执⾏内存的最常⽤⽅法是修改⻚⾯(其中包含 shellcode )的 PTE(Page Table Entry,⻚表项)信息,早于 Windows 10 1607 的版本都可通过如下算法得到给定⻚⾯的 PTE 地址: 

⽽到了 Windows 10 1607 和 1703 版本,原先的基址 0xFFFFF68000000000 被随机化处理了,这使得我们⽆法再简单计0x08 ⻚表基址的随机化算出给定⻚⾯的 PTE 地址。不过虽然⻚表基址被随机化了,但是我们知道内核必然还要经常查询 PTE 的内容,因此肯定存在⽤于获取 PTE 地址的 ring0 层 API,例如 ntoskrnl.exe 模块中的 MiGetPteAddress 函数。我们在 IDA 中查看该函数,发现⻚表基址并未被随机化 :

然⽽内存中的基址却是随机化处理过的: 

所以我们的绕过思路就是先找到 MiGetPteAddress 函数的地址,⽽后读取随机化后的基址并⽤其替换掉原先算法中的固定值 0xFFFFF68000000000。此过程需要⽤到内核态 Read primitive 以及上节讨论的函数动态查找法,在 MiGetPteAddress 函数地址的 0x13 字节偏移处即为对应的⻚表基址,如下为获取该基址的实现代码: 

在替换掉固定基址后,原先的算法即可重新⽤于获取给定⻚⾯的 PTE 地址: 

例如,对于地址 0xFFFFF78000000000(KUSER_SHARED_DATA 结构的内存地址)来说,相应的 PTE 地址为0xFFFFCF7BC0000000: 

⽽如果 shellcode 被写⼊到 KUSER_SHARED_DATA 结构的 0x800 偏移处,那么将正好位于地址 0xFFFFF78000000000对应的⻚⾯中。我们可以通过覆写 PTE来移除 NX 位,即最⾼⽐特位,以此修改内存⻚的保护属性,代码如下: 

接着可借助已知的⽅法来触发 shellcode 执⾏,例如覆盖 HalDispatchTable 中的函数指针: 

因此我们能够绕过⻚表的随机化保护并得以重拾 PTE 覆写技术。


0x09 Ring0 下可执⾏空间的分配

最后我们讨论如何在 Windows 10 1703 内核中直接分配可执⾏的内存空间,虽然也可以借助修改包含 shellcode ⻚⾯的PTE 信息来间接获取,但前者明显要简洁得多。

⼤部分内核池(kernel pool)的分配操作都是通过 ntoskrnl.exe 模块中的 ExAllocatePoolWithTag 函数完成的,按照 MSDN 上的定义,该函数包含 3 个参数,分别为池类型、分配⼤⼩以及 Tag 值: 

调⽤成功则返回指向新分配内存的指针。另外,虽然Windows 10内核中普遍采⽤的是NonPagedPoolNX池类型,但下列类型仍然还是存在的:

如果所选池类型的数值为 0,那么分配到的池内存其属性将是可读、可写、可执⾏的。要实现对 ExAllocatePoolWithTag 函数的调⽤,我们可借鉴前⾯经由 ring3 层 NtQueryIntervalProfile 函数调⽤来触发 ring0 层 shellcode 执⾏的思路,即函数调⽤栈传递与 hook 技术(HalDispatchTable 函数指针覆盖)相配合,但由于还需要考虑参数的传递,所以⽆法借助 HalDispatchTable 来实现。我们需要寻找另外的函数表,经过分析,内核模块 win32kbase.sys 中的 gDxgkInterface 函数表引起了我们的注意,所示如下: 

许多函数都会⽤到这个表,这其中我们所要找的函数需满⾜以下条件: 1)可在 ring3 下调⽤; 2)⾄少有 3 个参数是我们可控的且在随后的调⽤栈上保持不变; 3)⼏乎不被操作系统或守护进程调⽤,以避免覆盖函数表后出现错误调⽤。

分析可知, ring3 下的 NtGdiDdDDICreateAllocation 函数恰好满⾜这些要求,它会⽤到上表 0x68 偏移处的函数指针,即对应内核模块 dxgkrnl 中的 DxgkCreateAllocation 函数。不过其并⾮导出函数,只在 win32u.dll 模块中包含相关的系统调⽤,因此我们直接通过系统调⽤的⽅式来使⽤该函数,代码如下: 

当 NtGdiDdDDICreateAllocation 函数被调⽤后,执⾏流会从 win32k.sys 模块转移到 win32kfull.sys 模块,接着再转移到 win32kbase.sys 模块,最后获取 gDxgkInterface 表 0x68 偏移处的函数指针并调⽤之,整个过程如下: 

可以看到,该过程仅是简单的参数传递,并未对参数进⾏修改,因此满⾜第⼆点要求。此外,在测试中我们尝试覆盖上述的 DxgkCreateAllocation 函数指针,结果没有出现异常的问题,最后⼀点要求也满⾜了。

不过要想利⽤ “ NtGdiDdDDICreateAllocation 函数 + gDxgkInterface 函数表” 组合来实现对 ExAllocatePoolWithTag 函数的调⽤,还需满⾜⼀个条件,即 gDxgkInterface 函数表必须是可写的,我们可以通过如下⽅式获取该表的 PTE 信息: 

⼀般来说 PTE 所含内容很难直接看出来,我们按照 _MMPTE_HARDWARE 结构进⾏打印,如下所示该函数表是可写的: 

原则上到这⾥所有条件也就都具备了,我们的思路是先⽤ ExAllocatePoolWithTag 函数指针覆盖掉位于 gDxgkInterface 函数表 0x68 偏移处的 DxgkCreateAllocation 函数指针,⽽后再调⽤ NtGdiDdDDICreateAllocation 函数,选⽤的池类型参数为 NonPagedPoolExecute。剩下的问题就是如何定位 gDxgkInterface 函数表了,虽然在 KASLR 绕过部分我们讨论过如何定位 ntoskrnl.exe 模块基址,但到⽬前为⽌,还没有办法能够定位其它内核模块。

分析发现我们可以借助 ntoskrnl.exe 模块中的 PsLoadedModuleList 结构来获取内核中所有模块的基址,因此问题也就迎刃⽽解了,如下为遍历内核模块所⽤到的双向链表结构: 

继续遍历该链表直到 0x58 偏移处出现正确的模块名,则相应模块基址可在 0x30 偏移处得到。

不过早前的函数动态查找算法并不能直接⽤于定位 PsLoadedModuleList 结构,因为它不是⼀个函数,好在很多函数都⽤到了这个结构,我们可以从中找到指向 PsLoadedModuleList 结构的指针。例如 ntoskrnl.exe 模块中的 KeCapturePersistentThreadState 函数就⽤到了该结构:

我们先通过查找算法定位 KeCapturePersistentThreadState 函数,再间接获取 PsLoadedModuleList 结构的地址,进⽽可以获取内核中任意模块的基址。

由于 win32kbase.sys 模块基址也能得到了,因此 gDxgkInterface 表的定位问题就和定位 ntoskrnl.exe 模块中的 PsLoadedModuleList 结构很类似了。其思路同样是先找到⼀个使⽤了 gDxgkInterface 表的函数,再从中读取出相应地址。

这⾥我们将借助 win32kfull.sys 模块中的 DrvOcclusionStateChangeNotify 函数,其反汇编结果如下:

通过该函数指针我们能够得到 gDxgkInterface 表的地址,接着可对表中的函数指针进⾏覆盖,从⽽能够实现对 ExAllocatePoolWithTag 函数的调⽤,亦即实现了内核中可执⾏空间的分配,相关代码如下: 

在完成池内存的分配后,我们可借助内核态 Write primitive 来写⼊shellcode。最后我们再将 gDxgkInterface 函数表0x68 偏移处的指针覆盖为 shellcode 起始地址并再次调⽤ NtGdiDdDDICreateAllocation 函数: 

可以看到 NtGdiDdDDICreateAllocation 函数的调⽤参数中包括了 DxgkCreateAllocation 函数指针及其原先在函数表中的位置,以便我们能在 shellcode 中对 gDxgkInterface 函数表进⾏恢复,避免后续调⽤可能造成的系统崩溃。 


*参考部分详⻅原⽂

译者注:仅对所述中 Windows 10 的内容做了翻译


原⽂链接: 

https://www.blackhat.com/docs/us-17/wednesday/us-17-Schenk-Taking-Windows-10-Kernel-Exploitation-ToThe-Next-Level%E2%80%93Leveraging-Write-What-Where-Vulnerabilities-In-Creators-Update-wp.pdf


本文由看雪论坛『Pwn』版主 BDomne编译

来源:Morten Schenk [email protected]

转载请注明来自看雪社区


更多阅读:

[原创]Unity3D游戏引擎Android下mono模式源码保护升级版

[原创]尝试着实现了一个 ART Hook

[原创]反调试之检测类名与标题名

[原创]PE加载器的简单实现

你可能感兴趣的:(Windows 10 内核漏洞利用防护及其绕过方法)