Windows 10 1809内核ASLR绕过技术的演变
在内核地址空间布局随机化(KASLR)保护机制得以正确实现的情况下,直接获取内核驱动程序的基地址是不现实的,这给Windows内核的漏洞利用带来了极大的困难。为了绕过该防御机制,研究人员过去的研究重点通常都放在以下几种途径上面:通过内核地址泄漏来检索这些地址,或者通过特定的内核内存泄漏漏洞,或者使用通过池溢出(pool overflow)或write-what-where之类的内核漏洞来创建内核模式读取原语。不过,本文的重点则是使用内核模式读取原语绕过KASLR,因为这种类型的方法更加通用。
实际上,在KASLR安全机制的绕过与反绕过方面,一直是个猫鼠游戏:
在撰写本文时,已经有一年时间没见过新型的通用Windows内核利用技术问世了,这意味着Windows10的当前版本中的KASLR已经非常强大了。
从表面上看,这似乎是一件好事,但当我开始着手准备一个新的安全课程时,却让我有点为难:既然是新课程,如果还在介绍一年前就开始使用的、针对老版操作系统的老技术的话,好像有点说不过去。
所以,我开始在当前版本的Windows 10(代号为Redstone 5)上挖掘新型的KASLR绕过方法。功夫不负有心人,经过一番折腾,我终于找到了一个。
在这篇文章中,我将向您介绍在挖掘这种绕过技术过程中的一些关键步骤,正是借助于这些步骤,才帮我发现了尚未公布的KASLR绕过技术。
踏上征程
首先,请注意,通过使用众所周知的EnumDeviceDrivers和NtQuerySystemInformation API,攻击者能够以中等完整性级别来发动攻击,从而轻易绕过KASLR。因此,对于本文的其余部分,将假设执行的上下文是在低完整性水平下,或是在应用程序沙箱中进行的。
在以前版本的Windows 10中,可以使用多种通用方法来强制内核指针泄漏,包括使用来自线程环境块(TEB)的Win32ThreadInfo指针,泄漏兼容位图或利用桌面堆(Desktop heap)。对于这里介绍的绕过方法来说,我们将专注于使用桌面堆。
在Windows 10的Redstone 2版本中,微软删除了存放在桌面堆上分配内存的所有对象的内核模式地址的UserHandleTable,但幸运的是,桌面堆本身仍然是以用户模式映射的,使得我们仍然可以对其进行搜索并查找特定的对象。另外,由于像tagWnd这样的桌面堆对象通常包含一个头部,我们可以借助特定的dereference链,通过这个头部来获取一个KTHREAD结构,因此,我们可以设法触发内核模式指针地址泄漏,进而绕过KASLR。下面的清单1给出了相应的代码:
清单1:适用于Windows 10 Redstone 2版本的KASLR绕过技术
为了在Windows 10 Redstone5版本下进行测试,我们通过CreateWindowEx API创建了一个窗口,从TEB中的偏移0x828处获取用户模式映射桌面堆的地址,然后通过暴力搜索,在用户模式映射的桌面堆上查找Window句柄,以获取相应的偏移量——这实际上就是我在2017年的Black Hat USA大会上展示的一种技术。
图1:用户模式映射的桌面堆中的窗口句柄和位置
在Redstone 5之前,图1中的地址tagWnd将指向tagWnd内核对象的直接副本,该对象包含tagWnd对象的直接副本,该对象包含所有相关的内核模式指针,包括threadInfo指针。但是,如清单2所示,实际结果相去甚远:
清单2:用户模式映射桌面堆内的Windows对象
在Redstone 2版本中,清单2中突出显示的值将包含窗口对象名称的UNICODE_STRING结构。这意味着映射到上面突出显示的值的值通常包含窗口名称的UNICODE_STRING结构,该结构位于偏移量8处,即映射到地址0x20a177ab800的值通常包含指向该UNICODE字符串的内核模式指针。但事实并非如此。很明显,微软已经彻底修改了用户模式映射的桌面堆。
此时,为了更好地理解具体发生了什么变化,我不得不在内核空间中执行一些交叉检查。为此,我尝试查找桌面堆内核模式的基地址。在Redstone 5之前的版本,我们可以在桌面堆头部的偏移量0x28处找到该指针,并且,该指针可以从用户模式映射的桌面堆中读取。因此,我尝试通过调试器从TEB中偏移量0x828处手动获取该地址:
清单3:用户模式映射的桌面堆的头部
从上面的列表中我可以看到,现在用户模式映射桌面堆中偏移量为0x28处的QWORD的值为NULL。不过,我们可以在偏移量0x80处得到桌面堆的内核基地址,所以,我继续使用该偏移量进行分析。
清单4:桌面堆和Window内核模式地址的Delta值
由于用户模式映射的桌面堆是由SYSTEM线程进行更新的,所以,我预计内核空间中桌面堆的内容会与用户空间中显示的内容略有不同。但是,在检查两个桌面堆的内存(见清单3)时,WinDBG显示了完全相同(未经过滤)的内容!这意味着Microsoft对Desktop堆和相关API进行了彻底的修改。
更多的堆
为了弄清楚微软具体做了哪些修改,并确定攻击向量是否可行,我需要了解更多关于内核模式API如何在修改后的桌面堆上操作的相关信息。由于窗口名称(作为Unicode字符串存储在tagWnd对象中)通常比较容易识别(并且是用户可控的),因此,我调用了NTUserDefSetText API,试图更新该名称,并获得其实际的内核模式地址。为了跟踪执行情况,我在Win32KFull.sys中的NTUserDefSetText上设置了一个断点:
清单5:在NtUserDefSetText的prologue中看到的ValidateHwnd
在这里,我注意到了对ValidateHwnd的调用,它使用窗口句柄来定位内核模式tagWnd对象的地址。通常情况下,ValidateHwnd将返回内核桌面堆上的tagWnd的内存地址,但这又带来了另一个惊喜:
清单6:包含tagWnd对象的第二个桌面堆
如清单6所示,从ValidateHwnd接收的内核模式地址指向一个句柄为0x30502的对象,该对象与更新后的POC在用户空间中返回的Window句柄完全一致。所以,该对象包含的是内核模式指针,偏移量0x28处包含的是指向具有单独内存空间的指针,该内存空间也包含相同的窗口句柄。
由此得出的结论是,微软已经创建了多个桌面堆,并将对象内容存放到多个堆中,同时,只将内核模式指针的free版本映射到用户模式。这是一种非常好的方法,可以缓解用户模式中内核模式对象的滥用,从而加固KASLR。好了,这方面的内容就不多讲了,毕竟本文的主题是KASLR绕过技术。
与ASLR绕过所有方法一样,这里的关键也是要弄清楚在进程重启过程中随机化了哪些内容。在完成这些调查的过程中,我发现了一些重要的信息。首先,映射到用户模式的桌面堆的基地址始终以0x1400000结尾,重新启动后,只对高36位的内容进行了随机化处理。
其次,包含内核模式对象指针的桌面堆的地址也只是高36位(如清单7中的绿色所示)进行了随机化处理,这与映射到用户模式的桌面堆的地址的处理方式是一致的。之后的4 bit的内容始终等于0x4或0x6(见红色部分),而低24位则进行了随机化(以蓝色显示),这样就无法预测对象的仅内核模式桌面堆(kernel-mode only Desktop heap)的地址了:
清单7:仅内核模式桌面堆中的tagWnd对象的头部
这种区分似乎是合理的,只有内核模式的桌面堆包含指向用户模式桌面堆的指针。
固定值的使用似乎非常值得关注,因为它表明随机化是不彻底的。幸运的是,我在重新启动时发现了一个更加重要的线索。清单7中显示的tagWnd对象的仅内核模式的桌面堆部分表明偏移0x50处的指针指向另一个仅内核模式桌面堆,其内容是未知对象:
清单8:单独的桌面堆中的未知对象
这个对象让人感兴趣的是,低28位在重新启动时是一个固定值,即0x08308c0(以蓝色显示),而高36位是与用户模式共享的桌面堆(以绿色显示)。实际上,我们可以泄漏并计算包含内核模式指针的对象的绝对地址!
更有价值的是,从该对象的开头偏移0x10处的值是指向threadInfo结构的指针,这使我们可以使用原来的tagWnd KASLR绕过技术。threadInfo结构的偏移量0x0处包含指向KTHREAD的指针,其在偏移量02A8处包含指向ntoskrnl的指针。Dereference过程如清单9所示:
清单9:从未知对象指向ntoskrnl的指针
小结
在Windows10发布的Redstone 5版本中,微软完成了一项令人印象深刻的工程任务:将桌面堆分割成多个堆,并只允许用户模式查看与内核无关的内容。但是,它的随机化在实现方面存在使用静态值的缺陷,所以,如果攻击者能够读取任意内核模式内存的话,仍然能够绕过KASLR安全机制。
该KASLR绕过技术之所以能够得逞,是因为攻击者可以泄露内核模式对象地址中已经随机化处理后的36位,而低28位是静态的。与内核模式读取原语一起使用的内核模式对象的位置将指针泄漏给NTOSKRNL。
尽管原理比较复杂,但是要想绕过Windows 10 的Redstone 5版本中的KASLR防御机制,只要一条命令就行了:
清单10:利用一条WinDBG命令绕过KASLR
由于这种类型的安全问题与任何MicrosoftBug报告程序或指南都不匹配,因此,我尚未报告该问题。但是,我已经确认,由于某些设计的更改,这个问题在即将发布的19H1版本中将得到修复。
打赏我,让我更有动力~
© 2016 - 2024 掌控者 All Rights Reserved.