聚焦源代码安全,网罗国内外最新资讯!
编译:奇安信代码卫士团队
本文是趋势科技 ZDI 项目推出的第二届年度最有意思的五大案例系列文章之一。他们从1000多份安全公告中遴选出这些案例,奇安信代码卫士团队将一一翻译,供大家参考。本文说明的是由 Marcin Wiazowski 提交的存在于 Windows 内核模式驱动器中的一个本地提取漏洞。
随着安全沙箱项目变得越发常见,沙箱逃逸类漏洞也愈发重要。因此,我们一直都在寻找这类漏洞,而当 Marcin Wiazowski 提交了这个具有创新性的 Windows 7 本地提权漏洞以及完整的 exploit 之后我们感到尤其兴奋。这个 bug 纵然优质,不过他提交的漏洞 write-up 和 exploit 更加妙不可言。本博客具体说明了他对最终获得 CVE 编号 CVE-2019-1362 的分析过程。
该漏洞可被低权限用户用于在内核上下文中执行代码并获得系统权限。
漏洞概述
该漏洞更是因为对内核函数win32k.sys!CreateSurfacePal的参数缺乏验证造成的。该函数与处理调色板对象有关。调色板对象即内核模式对象,是一个可用颜色组成的数组,与某些用于操作一组预定义颜色的图形输出设备(显示器和打印机)结合使用。
在 Windows NT 内核的原始设计中,打印机驱动是加载到内核中的模块。从 Windows Vista 开始,微软做出了一项重大的架构调整:打印器驱动将以用户模式运行而不是作为内核的一部分运行。这种调整被视作根本的安全增强,而它所带来的安全一处也是显而易见的:一旦迁移到用户模式,打印机驱动中的 bug 所带来的安全影响就大大降低。
然而,颇具讽刺意味的是,事实证明这种架构调整对内核中余下图形代码的安全性产生了相反的影响。重构打印机驱动并将其迁移到用户空间创造了一种全新的攻击面,它由内核模式图形代码和现已被迁移到用户空间的驱动代码之间创建的新接口组成。曾经安全的内核模式代码现在注定会以各种方式失败,而这种失败是由以用户模式运行的打印机驱动程序代码产生的影响造成的,它更容易遭篡改。
具体而言,这个 bug 的主体是内核函数 win32k.sys!CreateSurfacePal,这种用户模式下打印机驱动架构的情况使得在用户模式下运行的恶意代码能够传递不合法参数。一般而言,攻击将始于干扰合法打印机驱动的功能。
攻击计划
我们首先 hook 用户模式下的打印机驱动。
用户模式下的打印机驱动是在系统注册表中注册的 DLL,它导出某些标准化的函数。
例如,默认的“Microsoft XPS Document Writer”驱动存在于:
C:\Windows\system32\spool\DRIVERS\W32X86\3\mxdwdrv.dll
它会导出如下函数:
当用户调用参数为“Microsoft XPS Document Writer” 的API gdi32.dll!CreateDCA/W 时,则加载驱动mxdwdrv.dll。之后进行从内核到用户模式的回调,接着调用函数mxdwdrv.dll!DrvEnableDriver:
调用之后,参数pded 返回 DRVENABLEDATA结构的一个指针:
当 c 给出 pdrvfn 表格中的多个项目时,pdrvfn 表格由 DRVFN 记录组成:
Pdrvfn 表格位于mxdwdrv.dll 驱动内存的某处,每个_DRVFN项目描述的是其中一个已实现驱动的函数——iFunc 是winddi.h标头文件中所列的其中一个值:
Pfn 是用户模式代码的一个指针,位于mxdwdrv.dll驱动中的某处。
通过覆写 pdrfn 表格,我们能够将该驱动实现的任意函数重定向到我们自己的代码中。
深入分析
尽管我们以 Microsoft XPS Document Writer 驱动作为案例,但任何安装在该操作系统上的打印机驱动均适用。该驱动 hooking 算法是“算法1”:
1、调用含有参数 PRINTER_ENUM_ LOCAL的用户模式下的API winspool.drv!EnumPrintersA/W 找出任何可用的打印机。对于每台打印机,返回其名称。在我们的实例中,返回的是 Microsoft XPS Document Writer。
2、使用两个 API winspool.drv!OpenPrinterA/W和winspool.drv!GetPrinterDriverA/W检索驱动 DLL 的路径(即mxdwdrv.dll 的路径)。
3、调用标记为 LOAD_WITH_ALTERED_SEARCH_PATH的kernel32.dll!LoadLibraryExA/W 来加载驱动 DLL。
4、调用驱动的导出函数DrvEnableDriver来获取pdrvfn表格的地址。
5、使用 API kernel32!VirtualProtect 使pdrvfn表格在内存中是可写的。
6、按需修改pdrvfn表格。应该将表格的原始内容保存,以便调用原始的一个或多个函数。
7、调用驱动的导出函数DrvDisableDriver。该打印机驱动 DLL 仍然被加载到已修复状态的内存中。
8、调用含从第一步获取的打印机名称的 API gdi32.dll!CreateDCA/W。该调用从内部加载打印驱动 DLL。然而,它已被加载(且已修复),因此只有 DLL 的引用计数器才会增加。这样,我们就强制内核使用内存中修复的打印机驱动,将所需打印机函数重定向至我们自己的代码中。
为了实施进一步利用,我们 hook 打印机驱动的DrvEnablePDEV函数。为此,我们将修改_DRVFN 记录,令iFunc == INDEX_DrvEnablePDEV:
我们的计划是从已 hook 的代码中调用原始的DrvEnablePDEV 函数,之后修改返回 pdevcaps和 pdi记录中的某些字段:
字段hpaDefault是模板调色板的句柄。我们必须通过调用用户模式下的 API gdi32.dll!EngCreatePalette才能创建该调色板。在默认情况下,该调色板用于256个条目,而每个条目都是4个字节的结构:
在字段flGraphicsCaps 中,我们把GCAPS_PALMANAGED设置为 flag,因此上述调色板将被用于“管理 (managed)”模式下。该 flag 并非默认设置。
值ulNumColors 给出上述管理模式下的调色板很多保留条目(保留条目的默认值是256)。在默认情况下,这个值等于20,因此前10个和最后10个调色板条目是保留的,即无法通过该应用程序进行修改;其余条目(中间)可以自由修改。
win32k.sys!CreateSurfacePal 中的漏洞
调色板对象在内核模式下以一个_PALOBJ 结构表示,它并非公开定义(winddi.h包含一个空声明)。然而,我们可以从 ReactOS 文档中找到一些信息(尽管它使用的是另外一个结构名称):
_PALOBJ 结构中存在一个指针ppalThis,它默认指向结构本身。调色板条目立即遵从内存中的_PALOBJ结构。注意,第一个条目仍然属于 _PALOBJ 结构本身且被声明为apalColors。pFirstColor字段指向第一个调色板条目。在默认情况下它仅指向字段apalColors。
调用用户模式下的 API gdi32.dll!CreateDCA/W后,内核中的回调用于调用我们的 hook,即用户模式下的DrvEnablePDEV函数。之后内核针对DrvEnablePDEV调用返回的数据执行多种操作。具体而言,如果字段flGrphicsCaps设置了GCAPS_PALMANAGED flag,则调色板在字段中返回的调色板被用作模板。它用于创建内部副本,而该副本变为设备调色板。这种操作是在函数win32k.sys!CreateSurfacePal 中执行的,而该函数也使用了我们的ulNumColors和ulNumPalReg值。在新创建的设备调色板中,调色板条目(将成为保留条目)通过将peFlags值设为0x30的方式被初始化。
函数win32k.sys!CreateSurfacePal的算法(算法2)是:
1、CreateSurfacePal接受如下参数:
——我们hpalDefault调色板的内核内存的一个指针,即内核内存中调色板的_PALOBJ 结构的指针。我们将该指针命名为 paSrc。
——ulNumColors(保留条目的数量)
——ulNumPalReg
2、调用win32k.sys!PALMEMOBJ::bCreatePalette,通过使用作为模板的palSrc 调色板创建新的调色板。这个新的调色板将变成设备调色板,我们将其命名为palDst。
3、初始化值为 0x30 peFlags的palDst中的保留条目。伪代码如下:
4、调用win32k.sys!XEPALOBJ::vCopyEntriesFrom,将palDst中的所有调色板条目复制回到
palSrc
中。
在
x64 系统中,该调用是内联的,因此只调用 win32k.sys!memmove 。5、调用在 x64 内联的win32k.sys!XEPALOBJ::ulTime,将值palSrc->ulTime复制到:
——字段palDst->ulTime
——字段palDst->ppalThis->ulTime(只在palDst->ppalThis != palDst的情况下)
在正常情况下,CreateSurfacePal 将被调用,其参数如下:
hpalDefault 是调色板的句柄,条目为256个
ulNumColors(保留条目的数量)为20
ulNumPalReg = 256
因此第三步中描述的代码在palDst 调色板的头10个和最后10个条目中将peFlags的值设置为 0x30。
第三步中所述代码的问题在于,该算法并不会对照调色板条目真实数量(palSrc -> cEntries) 来验证参数ulNumCoors(说明保留条目的数量)。攻击者可以通过劫持用户模式下的打印机驱动的方式传递范围之外的ulNumColors值,从而导致界外内存写入。
假设默认的调色板大小为256 个条目,攻击者应该传递的ulNumColors的值是514。
如上可见,该范围之下的条目和该范围之上的条目都会被修改,方法是将PALETTEENTRY记录的最高字节数 (DWORD) 设置为0x30。
该范围之下的一个条目包含字段palDst->ppalThis,它可使我们通过将其最高字节设置为0x30 的方法修改指针ppalThis。
该范围之上的条目幸好并不使用。为调色板对象分配内存时,计算如下:
sizeof(_PALOBJ)+ sizeof(PALETTEENTRY) * NumberOfEntries
然而,_PALOBJ 结构包含一个条目(字段apalColors),因此总会分配比所需条目多一个的条目。
在 x86 上实施利用
在 x86 系统上,palDst->ppalThis 是一个4字节值。通过 0x30 覆写其最高字节,我们将其设置成格式为 0x30XXXXXX 的值。它表示的是用户模式下的地址。首次使用被覆写的指针palDst->ppalThis发生在函数CreateSurfacePal本身。它用于写入字段ulTime。这就是上述算法2中的第5步。
如果我们想让操作系统崩溃,那么确保整个内存范围(从0x30000000 到 0x30ffffff)是不可访问的就足够了。
现在我们将考虑的是,如何利用该漏洞实现权限提升。
当无需任何操纵的情况下使用指针驱动时,操作的顺序如下:
1、应用程序代码调用 gdi32.dll!CreateDCA/W来查找并加载打印机驱动 DLL。
2、指针驱动 DLL 通过调用gdi32.dll!EngCreatePalette 创建模板调色板。
3、内核回调用户模式并调用打印机驱动的函数DrvEnablePDEV。这时,DrvEnablePDEV 可以指定模板调色板的句柄被使用。内核将使用该句柄获取调色板内核内存的指针。在CreateSurfacePal 中,它就是palSrc。
4、如果DrvEnablePDEV 调用返回标记GCAPS_PALMANAGED,则内核基于模板调色板创建一个驱动调色板。在CreateSurfacePal 中,它就是palDst。在模板调色板 palSrc 中,内核将字段hSelected 设置为指向新建的设备模板:
palSrc->hSelected = palDst
5、现在执行的是常见的打印动作。
6、用户调用gdi32.dll!DeleteDC。
7、内核回调用户模式并调用打印机驱动的函数DrvDisablePDEV。打印机驱动 DLL 通过调用gdi32.dll!EngDeletePalette 删除模板调色板。由于模板调色板在其字段hSelected中引用设备调色板,因此设备调色板(即我们所知的palDst)被自动删除。
8、打印驱动 DLL 被卸载。
我们现在重点关注“设备调色板被自动删除”部分。在实践中,该设备调色板的字段palDst -> ppalThis 指向一个_PALOBJ 记录,且它的用途如下:
(1) 访问字段BaseObject,它包含BaseObject结构开头的调色板palDst的句柄。该调色板的句柄被传递到win32k.sys!HmgRemoveObject,而后者要求调色板的引用计数器为1。引用计数器由操作系统内部管理。
(2) 成功调用HmgRemoveObject 后, ppalThis 指针被传递到一个调用win32k.sys!FreeObject 中,结果将该指针指向win32k.sys!ExFreePoolWithTag来释放该对象。
通过触发该漏洞,我们就能将ppalThis指针重定向至用户模式的内存。如果我们能够在该地址欺骗“足够合法的”_PALOBJ 结构,我们就能导致将该用户模式下地址被传递到ExFreePoolWithTag,而这正是我们进一步利用的目标。
要“足够合法”,我们必须用多个0来填充虚假的用户模式下的 _PALOBJ 结构,但例外如下:
(1) BaseObject 字段必须包含引用计数器为1的调色板的合法句柄。
(2) 字段ppalThis必须指向我们虚假结构的开头以避免在 objection 破坏算法中的进一步递归。
现在有两个问题需要克服。第一个问题是覆写的ppalThis指针指向某些 0x30XXXXXX 位置,但我们不知道它具体在何处。幸好我们可以在初期准备阶段可以用多个0填充从 0x30000000 到0x30ffffff 的整个内存范围。之后如算法2中的第5步,函数CreateSurfacePal 将通过引用已经覆写的ppalThis指针把非零时间戳写入字段ulTime中。之后我们可以扫描非零的 DWORD 值的内存范围。这就披露了我们被欺骗的用户模式下_PALOBJ 结构的确切位置。
第二个问题是我们假冒的_PALOBJ结构的BaseObject 字段必须包含调色板将引用计数器设置为1的一个句柄。在正常情况下,它应该只是由内核创建的设备调色板palDst 的一个句柄,但我们不知道该调色板的句柄值。幸好我们可以使用其它任何调色板。于是,问题就变成了从哪里获得引用计数器等于1的调色板。有意思的是,这样的调色板可通过再次使用我们被 hook 的打印机驱动 DLL 实现。我们可以再次调用gdi32.dll!CreateDCA/W 并将新建的模板调色板传递给内核,不过这次不会执行任何其它操纵。从 CreateDCA/W调用中返回后,只要我们不调用gdi32.dll!DeleteDC,那么我们的模板调色板的引用计数器就等于1。现在我们可以将该调色板的句柄放到虚假_PALOBJ 结构中的BaseObject 字段中。
现在我们可以将用户模式下的 _PALOBJ结构的地址传给内核调用ExFreePoolWithTag,而该调用确实是一个不寻常的情况。在正常情况下,ExFreePoolWithTag仅处理内核内存块。通过两个虚假的池标头环绕我们虚假的用户模式下的_PALOBJ 结构,我们可诱骗ExFreePoolWithTag内部结构将半控制值写入一个完全受控制的内核内存地址中。当然,这正是我们想要做的。
函数 ExFreePoolWithTag在所谓的池内存块上起作用。本文将不再赘述不良内存利用的详情,可参考 Tarjei Mandt 所著的经典作品《Windows 7 上的内核池利用》获取更多信息。所以这里我们将重点说明几个要点:
——每个池内存块(除了大块之后,不过它们不相关)前面都有一个POOL_HEADER记录。我们需要在虚假的_PALOBJ 结构前欺骗一个池标头。
——当释放内存块时,它的池标头内容通过下一个池标头进行验证。因此我们还需要在虚假的_PALOBJ结构之上创建另外一个虚假的池标头。
——池标头(PoolIndex字段)的字段中包含该池的指数是内存块的所属位置。
——在正常情况下,操作系统中可用4个池,尽管从理论上来讲可以分配16个池。每个池都通过使用其描述符POOL_DESCRIPTOR记录进行管理。
——我们将使用名为“PoolIndex 覆写”的池利用技术,尽管在我们的案例中,由于我们虚假的池标头位于用户内存中,因此“覆写”部分并非技术,所以我们可以随意写入。我们将虚假池标头中的PoolIndex 字段设置为15。该内核使用表将 PoolIndex值转换为相应的POOL_DESCRIPTOR记录的地址。由于只分配了4个池,因此PoolIndex 为15将被转换为一个空指针。
在 x86 的 Windows 7默认版本中,我们可以通过调用原生 API ntdll.dll!NtAllocateVirtualMemory 的方式分配一个空页面(内存始于地址0)。(注意,可使用注册表键值 HKLM\SYSTEM\CurrentControlSet\Control\SessionManager\Memory的 EnableLowVaAccess 启用或禁用空页面分配。这就使得我们能够分配一个虚假的 POOL_DESCRIPTOR 结构。)这样我们足以初始化在之前“在 Windows 7 上的内核池利用”中说明的结构的PendingFrees、PendingFreeDepth和ListHeads。这将导致我们获得通过半控制值覆写内核内存的能力。
根据物理安装的 RAM 内存大小,操作系统或者返回被立即释放到池中的内存块或者更大的突发次数(“延迟释放”)。根据该变量,通过使用构造的POOL_DESCRIPTOR 结构,我们或者通过被释放的内存块地址(在我们的案例中,它是格式为 0x30XXXXXX 的DWORD)覆写内核内存,或者通过某些在准备构造的 POOL_DESCRIPTOR 结构时分配的用户模式下内存块的地址(从 0x00010000 到 0x7fff0000 的范围的某值)。我们的最后一个任务是从内核中找到通过基本未知但可以确保至少是 0x10000 的某些值进行覆写的目标。
在这些限制条件下,一个不错的选择是调色板对象。可通过使用 API gdi32.dll!SetPaletteEntries来写入调色板条目。在内核实现中,这个 API 执行了被要求写入的索引的 0xffff 限制。这是因为无法在用户模式下创建更大的调色板。我们可以创建某些调色板(我们将其命名为 PaletteLO)具备比如说256个条目。调用 PaletteLO 上的 SetPaletteEntries 只有在被要求的写入的调色板条目的范围为0到255时才会成功。我们之后可以使用上述提到的利用方法来覆写 PaletteLO 的 _PALOBJ 内核结构的 cEntries 字段,该值的大小最少为 0x10000。完成这一步之后,SetPaletteEntries 将在最大可能的范围(0到0xffff)适用于 PaletteLO。这就使得我们能够在 0xffff 限制中覆写所选的内存位置,而且能够完全控制在调用 SetPaletteEntries 时通过颜色参数传递的内容。
在攻击的下一步,我们想要创建一些稍高于 PaletteLO 的其它调色板。我们将其命名为 PaletteHI。我们之后将调用 PaletteLO 上的SetPaletteEntries,通过所选的指针调用 PaletteHI 的_PALOBJ 内核结构的字段pFirstColor字段。由于pFirstColor表明的是调色板颜色数组的基地址,因此之后对 PaletteHI 上的SetPaetteEntries调用将会通过具有完全受控内容的pFirstColor值覆写所指向的内存。这时,一切都结束了。我们已经获得通过调用 PaletteHI 上的 SetPaletteEntries 的方法以任意所选内容覆写任意所选内存地址的能力。
操纵之前,我们的调色板如下:
覆写 PalleteLO 的 cEntries 字段后:
覆写 PaletteHI 中的 pFirstColor 字段后:
为了找到调色板的内核地址,我们可以使用全局内核 GDI 表格,它具有关于每个 GDI 对象的信息并且将只读模式映射到每个正在运行的进程的用户地址空间中。该表格包含65536个条目。该条目结构并未公开记录,但通常被称为 GDICELL:
GDICELL 多个字段的意义如下:
——KernelAddress:指向为该对象分配的内核内存的指针
——ObjectOwnerPid:该对象所拥有进程的 PID
——Upper:该对象32位句柄值的高16位
——ObjectType:代表对象类型的枚举
——Flags:显然是包含某些标志的一个字段
——UserAddress:指向为该对象分配的用户内存的指针
要找到调色板的内核地址,使用调色板句柄值的16个最低位作为 GDI 表格的索引,之后读取字段GDICELL.KernelAddress 就足够了。
要创建在内核内存中足够相近的PaletteLO和PaletteHI 调色板,只要在一个循环中创建调色板对象直到任意两个所创建调色板的位置满足条件就足够了。在解决 0xffff 心智中的条目,PaletteLO 必须能够覆写PaletteHI内核内存中的pFirstColor指针。这种调色板对在实践中可被轻易创建。
未完待续......
推荐阅读
史无前例:微软 SQL Server 被黑客组织安上了后门 skip-2.0(来看技术详情)
原文链接
https://www.zerodayinitiative.com/blog/2019/12/16/local-privilege-escalation-in-win32ksys-through-indexed-color-palettes
题图:Pixabay License
本文由奇安信代码卫士编译,不代表奇安信观点,转载请注明“转自奇安信代码卫士 www.codesafe.cn”
奇安信代码卫士 (codesafe)
国内首个专注于软件开发安全的产品线。