首页
社区
课程
招聘
[翻译]使用二进制比较发现Windows内核内存泄露bugs
2018-5-17 09:42 4122

[翻译]使用二进制比较发现Windows内核内存泄露bugs

2018-5-17 09:42
4122

翻译:https://googleprojectzero.blogspot.com/2017/10/using-binary-diffing-to-discover.html


补丁比较是比较相同代码的两次构建(一个已知有脆弱性,一个包含修补)的常用技术。通常通过该技术来发现措辞含糊的漏洞公告背后的技术细节,弄清漏洞根本原因、攻击代码和可能的攻击变种。近些年,该方法吸引了大量研究【1】【2】【3】,并且开发出了许多工具【4】【5】【6】,该方法在利用1-day漏洞攻击未及时打补丁用户上体现了价值。对于可以很容易地逆向的软件,已公布补丁的脆弱性被攻击者利用是不可避免的。

同样,若一个产品同时有多个版本在市面上,二进制比较也可以被用来发现它们之间的差异。比如Windows操作系统,当前微软还支持的版本有Windows 7、8和10。并且,当前Windows7在桌面市场仍然占有近50%的份额。微软对较新的版本引入和许多架构上的安全加固和补丁。这让旧版本系统的用户很不安全,使得他们可以被软件缺陷的脆弱性攻击,这些脆弱性可以简单地通过定位不同版本系统对应代码的微妙变化发现。

在后面的文章里,我们会展示如果通过二进制比较发现一个0-day例子,这是一个用户模式程序的未初始化内核内存泄露bug。这类bug在本地提权中很有用,也可以获取内核地址空间中存储的敏感数据。如果对这类bug不熟悉,我们建议你先看一下今年REcon和BlackHat上的演讲Bochspwn Reloaded【9】。

捕捉memset调用

大多数内核内存泄露发送的原因是将一大块内存区域复制到用户模式时内存未初始化,这块内存可能是结构、联合体、数组或它们的结合。发生这种情况意味着,内核向ring-3程序提供了比有效数据更多的内容,可能的原因是:被编译器插入的填充部分,未使用的结构/联合体,为可变长度内容提供的固定长度大数组。这类脆弱性的修复工作通常只是换一个小的内存空间,原来的代码行为被完全保留,并且还要加一个memset调用来初始化输出内存以保证它不包含无关的数据。这使得通过逆向工程识别这类补丁非常容易。

当对https://bugs.chromium.org/p/project-zero/issues/detail?id=1267&desc=2(Bochspwn发现的位于win32k!NtGdiGetGlyphOutline的Windows内核池内存泄露)进行粗略分析时,我意识到这个bug只在Windows7和8中存在,微软已经在Windows10中已经将其在内部修复。下图显示修复前后代码的不同,由Hex-Rays反编译,由Diaphora进行二进制比较:

Windows10中补丁的特征非常明显(在syscall的顶层处理过程中一个新的memset调用),我猜测在老版本内核中会有其它相似的问题在新版本中被微软默默修复。为了验证这个猜测,我决定在Windows7和Windows10中比较所有顶层syscall处理函数(如以Nt作为前缀的函数,位于内核和图形子系统中)中memset的数量,然后在Windows8.1和Windows10间进行一样的比较。原理上这是个非常简单地分析,一个很简单地分析方法就可以得到期望的结果,我决定在IDA反汇编产生的代码列表上执行比较。

执行比较时,我很快发现内核中的内存清0操作都被编译为三种模式的一种:

  • 直接调用memset

  • 使用rep stosd指向以内联的形式实现

  • 一系列展开的mov指令

最常见的两种情况(memset和rep stosd)都被Hex-Rays反编译为对memset的调用。

不幸的是,使用一个被清0的寄存器执行一系列mov指令没有被Hex-Rays识别为调用memset,所幸这种情况很少,所以在后续人工处理误报前可以忽略。最终,为了容易些,我决定基于反编译后的.c文件进行代码比对而不是反汇编文件。

获得最后结果的完整操作步骤如下,我们分别对Windws7/10和Windoes8.1/10各执行一次:

  1. 使用Hex-Rays同时反编译Windows7和Windows8.1的ntkrnlpa.exe和win32k.sys为.c文件,对Windows10下的ntoskrnl.exe、tm.sys、win32kbase.sys、win32kfull.sys执行相同操作。
  2. 提取调用memset的内核函数列表(调用次数也考虑),按字母排序
  3. 对两个列表进行普通的字符比较,选出在Windows中调用memset次数更多的函数
  4. 过滤上一步的输出,只保留在旧版本(Windows7或Windows8.1)存在的函数。

最后得到的结果统计如下:

   ntoskrnl functio ntoskrnl syscall handlers
  win32k function win32k syscall handlers  
  Windows 7 vs. 10  153 8 89 16
  Windows 8.1 vs. 10  127 5 67 11







直观看,Windows7/10之间的不同比Windows8.1/10之间大。另一个有趣的结果是图形子系统的变换相对小,但比核心内核的syscal处理过程多。在此结果之上,我们人工详细分析每一个有变换的函数,在 win32k!NtGdiGetFontResourceInfoInternalW和 win32k!NtGdiEngCreatePalette系统服务中发现了两个新的脆弱性。它们都在17年9月的补丁中被修复了,它们有一些相同的特征,接下来分别讨论:

win32k!NtGdiGetFontResourceInfoInternalW (CVE-2017-8684)

表明存在bug的不一致memset如下:

这是一个0x5c字节的栈上的内核内存泄露。函数代码的结构遵循一个通常的Windows优化设计,使用一个栈上的本地缓存进行较短的syscall输出,并且池分配器直接使用一个比输出大的空间。相关的伪代码片段如下:

注意到,就算在存在脆弱性的过程中,内存泄露只在第一个分支中存在,所以需要的缓存尺寸(a4)最大是0x5c字节。因为动态的PALLOCMEM池分配内存时会执行清0操作:

幸运的是,介绍这个例子也顺便介绍了另一个安全漏洞(http://j00ru.vexillium.org/slides/2017/recon.pdf的32-33页)中与用户模式交互的方式。利用漏洞的代码模式如下:

  1. 根据用户指定的尺寸分配一个输出缓存,也是被泄露的地方
  2. 调用win32k!GetFontResourceInfoInternalW函数使请求的数据写到内核缓存
  3. 不考虑win32k!GetFontResourceInfoInternalW实际填入的数据量,将整个内核临时缓存的内容写回ring-3

存在脆弱性的win32k!NtGdiGetFontResourceInfoInternalW处理过程实际上知道有意义的数据有多少(通过第五个syscall参数传回用户模式的调用者),但是在对功能正常运行无帮助的情况下仍然按照客户请求的内存数量进行复制。

结合缓存未进行预初始化和准许复制多余字节两个情况,这就成了一个可以被利用的安全bug。在POC(https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=287391)中,我们使用一个未文档化信息类,它只写了缓存的前4字节,留下剩下的88字节准备泄露给攻击者。

win32k!NtGdiEngCreatePalette (CVE-2017-8685)

这个例子中,脆弱性在Windows8中通过引入下图的memset被修复,但在Windows7中仍然存在:

存在问题的系统调用负责建立由N个4字节颜色项组成的GDI调色板对象。同样的,实现中包含一个内存使用优化:如果N小于等于256(总大小1024字节),则通过win32k!bSafeReadBits将数据从用户模式读到内核栈缓存中;否则,直接调用win32k!bSecureBits锁定ring-3中的内存。和你猜的一样,添加额外memset的内存区域是用于临时存储用户定义的RGB颜色列表的本地缓存,它随后会被传给win32k!EngCreatePalette来建立调色板对象。现在的问题是,我们如何让缓存不初始化确仍然传输去创建一个非空的调色板?答案在win32k!bSafeReadBits的实现中:

从上面的反编译结果可以看到,如果源或目的指针为空,则函数未进行任何实际工作就结束。这里的源地址直接来自syscall的第三个参数,在传到该函数前未进行任何处理。这意味着,我们可以使得syscall认为它成功从用户模式获得了一个256个元素的列表,实际上栈缓存没有被写入东西。在我们的POC(https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=287397)中,这通过如下的系统调用实现:

HPALETTE hpal = (HPALETTE)SystemCall32(__NR_NtGdiEngCreatePalette, PAL_INDEXED, 256, NULL, 0.0f, 0.0f, 0.0f);

syscall返回后,我们收到一个调色板句柄,调色板内存着被泄露的栈内存。为了从句柄读出内存,需要调用GetPaletteEntries来实现。再次强调这个bug的重要性,它准许攻击者泄露整整1kB未初始化的内核栈内存,这将在攻击者发挥很大的作用。

在内存泄露本身之外,在附加代码区域还会发现其它有趣的东西。如果你看Windows8.1和Windows10中win32k!NtGdiEngCreatePalette有关的代码,你将发现它们之间的一个有趣的不同:在两个系统中栈数组都被完全清0了,但是却使用不同的方法实现。在Windows8.1中,函数先设置第一个DWORD为0,然后调用memset设置剩下0x3fc字节为0,Windows10直接使用memset设置全部0x400直接为0。存在这个不同的原因不清楚,尽管最后的结果一样,但这个不同又引起了一个想法,不仅比较是否存在不同数量的memset,还可以比较memset操作的内存尺寸。

在一个最近的报告中,win32k!NtGdiEngCreatePalette在利用内核漏洞时进行栈spraying也非常有用,它准许用户方便地写1024字节的可控数据到一个栈上的连续区域。虽然控制的缓存尺寸小于nt!NtMapUserPhysicalPages能提供的,但是控制的缓存位于顶层系统调用处理函数栈帧的高偏移处,这才某些情况下非常有用。

结论

本文目的是举例说明一个产品同时支持的不同分支版本的安全相关区别会被攻击者用于定位重要的漏洞。它不仅将某些客户暴露给攻击者,同时还清楚地说明了攻击向量是什么。这对于修复特征明显的bug尤其严重,如内核内存泄露和增加的memset。本文采用的二进制比较过程实际上是伪代码层的比较,不需要跟到系统底层的知识。非高级的攻击者就可以无需很多工作就可以发现这三个脆弱性(CVE-2017-8680,CVE-2017-8684,CVE-20178685)。我们期望这类很容易被发现与利用的问题是少数的,并且厂商要对他们的各个版本产品进行一致地修复。

引用

1. http://www.blackhat.com/presentations/bh-usa-09/OH/BHUSA09-Oh-DiffingBinaries-SLIDES.pdf

2. https://beistlab.files.wordpress.com/2012/10/isec_2012_beist_slides.pdf

3. https://www.rsaconference.com/writable/presentations/file_upload/ht-t10-bruh_-do-you-even-diff-diffing-microsoft-patches-to-find-vulnerabilities.pdf

4. https://www.zynamics.com/bindiff.html

5. http://www.darungrim.org/

6. https://github.com/joxeankoret/diaphora

7. https://support.microsoft.com/en-us/help/13853/windows-lifecycle-fact-sheet

8. http://gs.statcounter.com/os-version-market-share/windows/desktop/worldwide

9. http://j00ru.vexillium.org/slides/2017/recon.pdf











[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
点赞1
打赏
分享
最新回复 (1)
雪    币: 2694
活跃值: (80)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
BlackJZero 2018-5-17 12:32
2
0
游客
登录 | 注册 方可回帖
返回