在Windows 11内部预览版中,KUSER_SHARED_DATA结构体发生了哪些新变化?(上)
2022-3-13 11:50:0 Author: www.4hou.com(查看原文) 阅读量:34 收藏

简介

不久前,我看到了一条有趣的推文,其中谈到了Windows 11的内部预览版本中KUSER_SHARED_DATA即将发生的一些变化。

1.png

这引起了我的极大兴趣,因为KUSER_SHARED_DATA是一个位于静态虚拟内存空间的结构体,在传统的Windows内核中,它位于0xfffff78000000000处。从漏洞利用的角度来看,由于其静态特性,攻击者经常通过它来攻击系统内核,特别是在远程入侵内核的时候。虽然KUSER_SHARED_DATA结构体既没有包含指向ntoskrnl.exe的指针,也不可执行,但有一段内存与KUSER_SHARED_DATA结构体位于同一内存页内,并且该页中没有包含任何数据,因此,它可用作具有静态地址的代码洞。

撰写本文时,在最新版本的windows 11 内部预览版中,KUSER_SHARED_DATA结构体的长度为0x738字节。

1.png

在Windows上,一个给定的内存“页面”的长度通常为0x1000字节,即4KB。由于KUSER_SHARED_DATA结构体的长度为0x738字节,所以,内存页中仍有0x8C8字节的内存空间可供攻击者滥用。因此,这些未使用的字节仍然具有与KUSER_SHARED_DATA结构体其他部分相同的内存权限,即RW,或读/写权限。这意味着: “KUSER_SHARED_DATA代码洞”不仅是一个可读可写的代码洞,而且,还具有静态地址。实际上,Morten Schenk在BlackHat 2017的演讲中早就讲过这种技术,我之前也写过一篇文章,就滥用这种结构体来执行代码的漏洞进行了简单介绍。

如果这个代码洞得到了适当的处理,攻击者就需要在内存中找到另一个位置来存放其shellcode。另外,具有读/写原语的攻击者可以破坏对应于KUSER_SHARED_DATA的页表项(PTE),从而使内存页变为可写的。然而,为了实现这一点,攻击者需要绕过kASLR并将一个原语写入内存——这意味着:攻击者基本上已经完全控制了系统。缓解这一代码漏洞的方法,就是迫使攻击者首先得绕过kASLR,然后,才能将恶意代码写入内存,从而提高漏洞利用的门槛。如果攻击者无法直接写入静态地址,则需要定位其他内存区域。因此,我们可以将其归类为一种更小、更专用的缓解措施。无论如何,我仍然觉得这是一个有趣的研究课题。

最后,在开始之前,本文探讨的内容都是在ntoskrnl.exe的上下文中进行的;当启用基于虚拟化的安全特性(VBS)时,这些内容并不适用于VTL1级别安全内核。正如Saar Amar所指出的,这种结构体的地址,在VTL1中实际上是随机化的。

0xfffff78000000000现在变为只读的了

对于KUSER_SHARED_DATA可能的变化,我的第一个想法是内存地址最终(不知何故)将被完全随机化。为了验证这一点,我将KUSER_SHARED_DATA结构体的静态地址传递给了WinDbg中的dt命令,令我惊讶的是,该结构体在解析后仍然位于0xfffff78000000000处。

1.png

我的下一个想法是,尝试以0x800为偏移量,对KUSER_SHARED_DATA结构体进行写入操作,以查看是否发生任何意外行为。执行该操作后,通过检查PTE,我们发现KUSER_SHARED_DATA现在变成只读的了。

下面提供的地址0xfffffe7bc0000000是与虚拟地址0xfffff78000000000或KUSER_SHARED_DATA结构体关联的PTE的虚拟地址。在您的系统上,可以使用Windbg的!pte 0xfffff78000000000命令来查找该地址。为了提高可读性,这里省略了这些命令,不过,我们将告诉读者哪些地址对应于哪些结构体,以及如何在自己的系统上面查找这些地址。

1.png

后来,有次跟同事Yarden Shafir聊天,他告诉我KUSER_SHARED_DATA中有一些东西(例如SystemTime成员)会不断更新,同时,他还鼓励我继续深挖,因为很明显,KUSER_SHARED_DATA结构体肯定是通过只读PTE进行写入/更新的。正如我后来发现的那样,这也是有意义的,因为与KUSER_SHARED_DATA对应的PTE的Dirty位被设置为0,这意味着该内存页还没有被写入。那么,这到底是怎么发生的呢?

带着这些信息,我开始借助IDA寻找任何有趣的东西。

nt!MmWriteableUserSharedData来救场了!

在IDA中搜索了0xFFFF78000000000或“usershared”之类的关键词后,我偶然发现了一个我以前从未见过的符号——nt!mmwriteableusershareddata。在IDA中,这个符号似乎被定义为0xFFFF78000000000。

1.png

然而,在查看实时内核调试会话时,我注意到地址似乎有所不同。不仅如此,在重启之后,这个地址也发生了变化!

1.png

我们还可以看到,0xFFFF78000000000静态地址和新符号都指向相同的内存内容。

1.png

然而,是否存在这种情况:两个单独的内存页面指向两个单独的结构体,并且其中包含相同的内容?或者它们是以某种方式交织在一起的?在查看了这两个PTE之后,我确认了这两个虚拟地址虽然不同,但都使用了相同的页帧号(PFN)。此外,我们可以通过以下命令找到“静态”KUSER_SHARED_DATA结构体和新符号nt!MmWriteableSharedUserData的PTE:

    !pte 0xfffff78000000000

    !pte poi(nt!MmWriteableSharedUserData)

如上所述,与“静态”KUSER_SHARED_DATA结构体相对应的PTE的地址是0xfffffe7bc000000。而地址0xffffcc340c47010正好是与nt!MmWriteableSharedUserData的PTE相对应的虚拟地址。

1.png

PFN乘以页的大小(在Windows上通常为0x1000)将得到相应虚拟地址的物理地址(就PTE而言,它用于获取4KB对齐页的“最终”分页结构)。由于这两个虚拟地址都包含相同的PFN,这意味着当将PFN转换为物理地址(本例中为0xfc1000)时,两个虚拟地址将映射到相同的物理页面! 我们可以通过查看映射到每个虚拟地址的物理地址的内容以及虚拟地址本身来确认这一点。

1.png

我们这里有两个虚拟地址,并且具有不同的内存权限(一个是只读的,另一个是读/写的),它们由一个物理页面提供支持。换句话说,有两个虚拟地址具有相同物理内存的不同视图。这怎么可能?

内存段

围绕KUSER_SHARED_DATA实现的变动,这里的“要点”是内存段的概念。这意味着一段内存实际上可以由两个进程共享(内核也是如此,就像我们的例子一样)。其工作方式是,相同的物理内存可以映射到一系列虚拟地址。

在本例中,KUSER_SHARED_DATA与nt!MmWriteableUserSharedData(一个虚拟地址)的新随机读/写视图,由与“静态”KUSER_SHARED_DATA(另一个虚拟地址)共享同一段物理内存。这意味着,现在这个结构体具有两个“视图”,具体如下所示:

1.png

这意味着:只要更新其中一个虚拟地址(例如nt!MmWriteableSharedUserData)的内容,将同时更新另一个虚拟地址(0xfffff78000000000)的内容。这是因为对其中一个虚拟地址处内容的改变将更新物理内存的内容。由于这段物理内存的内容供两个虚拟地址共享,所以,两个虚拟地址的内容都将收到更新。这为Windows提供了一种方法:在保持旧的KUSER_SHARED_DATA地址的同时,也允许一个新的映射视图是随机的,以“缓解”传统上在KUSER_SHARED_DATA结构体中发现的静态读写代码洞。0xfffff78000000000的“旧”地址现在可以被标记为只读,因为这个内存有一个新的视图可以用来代替它,这个视图是随机的!接下来,我们开始介绍更复杂、更低层次的实现细节。

nt!MiProtectSharedUserPage

在继续分析之前,请允许我介绍两个术语。当我提到内存地址0xFFFF78000000000(KUSER_SHARED_DATA的静态映射)时,我将使用术语“static”KUSER_SHARED_DATA。当我提及新的“随机化映射”时,我将使用符号名nt!MmWriteableSharedUserData。这样的话,每次都能指出我所谈论的 "版本"。

在WinDbg中进行了一些动态分析后,我终于搞明白了KUSER_SHARED_DATA的更新到底是如何实现的。为此,我首先在正在加载的ntoskrnl.exe上设置一个断点。在现有的内核调试会话中,可以使用以下命令来实现这一点:

    sxe ld nt

    .reboot

在断点被命中后,我们实际上可以看到新发现的符号nt!MmWriteableUserSharedData指向了“静态”的KUSER_SHARED_DATA地址。

1.png

这显然表明,这个符号在加载过程中会进一步更新。

在通过IDA逆向分析的过程中,我注意到在函数nt!MiProtectSharedUserPage中,对nt!MmWriteableSharedUserData有一个交叉引用,这引起了我们的极大兴趣。

1.png

当执行仍然处于暂停状态时,由于ntoskrnl.exe触发了断点,我趁机在上述函数nt!MiProtectSharedUserPage上设置了另一个断点,并发现,在到达新的断点后,nt!MmWriteableSharedUserData符号仍然指向旧的0xfff78000000000地址。

1.png

更有趣的是,“静态的”KUSER_SHARED_DATA'在加载过程中的这一时刻仍然是静态的,可读的,可写的! 下面的PTE地址0xffffb7fbc0000000是与虚拟地址0xfff78000000000相关的PTE的虚拟地址。由于我们重新启动系统,导致ntoskrnl.exe的加载中断,PTE地址也发生了变化。如前所述,这个地址可以通过命令 !pte 0xfffff78000000000找到,并且不同的系统,这个地址可能会有所差异:

1.png

因为我们知道0xfffff78000000000,这个“静态”的KUSER_SHARED_DATA结构体的地址,在某一时刻会变成只读的,这说明这个函数可能负责改变这个地址的权限,并且动态地填充nt!MmWriteableSharedUserData,特别是基于命名约定。

深入研究nt!MiProtectSharedUserPage的反汇编代码,我们可以看到nt!MmWriteableSharedUserData这个符号在这个指令执行时被更新为RDI的值。但是这个值是从哪里来的呢?

1.png

让我们来看看这个函数的开头部分。首先引起我们注意的就是内核模式地址和对nt!MI_READ_PTE_LOCK_FREE和nt!Feature_KernelSharedUserDataAaslr__private_IsEnabled的调用(这对我们的目的来说,兴趣不大)。

1.png

上图中内核模式地址0xfffffb7000000000,在WinDbg的反汇编窗口中用红框标出,实际上是页表项的基址(例如PTE数组的地址)。第二个值,即常量0x7bc00000000,是用来索引这个PTE数组的值,以获取与“静态”的KUSER_SHARED_DATA相关的PTE。这个值(PTE数组的索引)可以通过以下公式得到:

1、将目标虚拟地址(本例中为0xfff78000000000)转换成虚拟页号(VPN),方法是用地址除以一个页面的大小(本例中为0x1000)。

2、将VPN乘以一个PTE的大小(64位系统=8字节)

我们可以用上述公式来处理虚拟地址0xfffff78000000000,得到的值就是PTE数组相应的索引,从而获得与“静态”的KUSER_SHARED_DATA结构体相关的PTE。这可以在上面的WinDbg的命令窗口中看到。

这意味着与“静态”的KUSER_SHARED_DATA结构体相关的PTE将被传入nt!MI_READ_PTE_LOCK_FREE。上述PTE的地址为0xffffb7fbc0000000。

简单来说,nt!MI_READ_PTE_LOCK_FREE将解除对PTE内容的引用,并将其返回,同时,还会对作用域内的页表项进行检查,看看它们是否位于PML4E数组的已知地址空间内,其中包含用于PML4分页结构的PML4页表条目数组。回顾一下,PML4结构是基本分页结构。所以,换句话说,这确保了所提供的页表项驻留在分页结构的某个地方。这可以在下面看到。

1.png

然而,稍有细微差别的是,该函数实际上是在检查页表项是否位于“用户模式分页结构”中,该结构又称为“影子空间”。回想一下,在KVA Shadow的实现(即Microsoft的内核页表隔离(KPTI)实现)中,现在有两套分页结构:一套用于内核模式执行,另一套用于用户模式。这种缓解措施被用于防御Meltdown漏洞。但是,这个检查很容易被“绕过”,因为PTE显然被映射到了内核模式的地址,所以,肯定不是通过“用户模式分页结构”来表示的。

如果PTE不在“影子空间”中,则nt!MI_READ_PTE_LOCK_FREE将返回PTE解引用的内容(例如,PTE的各个“比特”)。如果PTE确实位于“影子空间”中,在返回内容之前,还将对PTE进行一些检查,以确定KVAS是否被启用。从漏洞利用的角度来看,这对我们关注的整体变化不是太重要,但它仍然是整个“过程”的一部分。

此外,对我们来说nt!Feature_KernelSharedUserDataAslr__private_IsEnabled并不是很有用——利用它,我们只能通过命名规则了解是否走在正确的道路上。这个函数似乎主要是为了收集关于这个功能的指标和遥测数据。

1.png

在第一次调用nt!MI_READ_PTE_LOCK_FREE后,“静态”的KUSER_SHARED_DATA结构体的PTE的内容将被复制到一个堆栈地址:RSP,其偏移量为0x20。类似的,这个堆栈地址也用于对另一个函数(即nt!MI_READ_PTE_LOCK_FREE)的调用。再说一次,这对我们来说并不是特别重要,但它却是这个过程的一部分。

1.png

然而,更有趣的是,nt!MI_READ_PTE_LOCK_FREE解除了对PTE内容的引用,并通过RAX返回它们。由于定义内存的属性/权限的“静态”的KUSER_SHARED_DATA结构体的PTE“比特”位于RAX中,所以,需要对其进行相应的位运算,以便从“静态”的KUSER_SHARED_DATA的PTE中提取页帧号(PFN)。这个值在PTE中的偏移量是0xf52e,其值是0x800

000000000f52e863。

1.png

 1.png

这个PFN将在以后调用nt!MiMakeValidPte时用到。现在,让我们继续前进。

现在,我们可以将注意力转向对nt!MiMakeValidPte的调用。

1.png

请允许我简单介绍一下PFN记录:PFN的“值”在技术上只是一个抽象的值,当它乘以0x1000(一个页面的大小),就会得到一个物理内存地址。在内存分页过程中,它通常是下一个分页结构的地址,或者,如果被用于“最后一个”分页表,即PT(page table)时,可以用来计算最后一个4KB对齐的物理内存页。

除此之外,PFN记录还被存储在一个虚拟地址数组中。这个数组被称为PFN数据库。这样做的原因是,内存管理器可以通过线性(虚拟)地址访问页表项,这就提高了性能,因为MMU不需要不断遍历所有的分页结构来获取PFN、页表项等。实际上,这就为记录的引用提供了一种简单的方法,即通过一个索引访问数组。这适用于所有的“数组”,包括PTE数组。同时,像nt!MiGetPteAddress这样的函数,也能够通过索引访问相应的页表数组,比如PTE数组(对应于nt!MiGetPteAddress),PDE数组(PDPT条目,通过nt!MiGetPdeAddress进行访问),等等。

小结

在本文中,我们为读者详细介绍了在Windows 11内部预览版中,KUSER_SHARED_DATA结构体发生了哪些新变化。由于篇幅较长,我们分为上下两篇进行发布。更多精彩内容,敬请期待!

(未完待续!)

本文翻译自:https://connormcgarr.github.io/kuser-shared-data-changes-win-11/如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/vL40
如有侵权请联系:admin#unsafe.sh