-
-
[翻译]Windows Uniscribe Fuzzing 笔记
-
发表于: 2017-5-1 16:30 6277
-
原文链接:https://googleprojectzero.blogspot.jp/2017/04/notes-on-windows-uniscribe-fuzzing.html
在几周前微软三月星期二补丁日1 修复的119个CVE2 漏洞中,有29个漏洞来自我们报告的Uniscribe库的字体处理代码。诚然,字体相关的安全话题已经在这个博客上进行过广泛的讨论,包括手工分析[1][2]和Fuzzing[3][4]两个方面的内容。然而,与包含在win32k.sys和ATMFD.DLL3 这两个驱动里的内核模式的字体实现相反,这次的尝试和之前的的一点不同之处在于Uniscribe是一个鲜为人知的用户模式下的组件,此前它并未被广泛认为是一个可行的攻击媒介。在这篇文章中,我们先列出Uniscribe的简要历史和描述,解释一下我们是如何实现规模化地fuzzing这个库,然后强调一些我们目前已有的更有趣的发现。所有我们涉及到的这些bug(正如它们被提交给微软那样),都有相应的POC(概念证明)样本,它们可以在Project Zero bug追踪的官方网页[5]上找到。请享受这一切!
752378f3 668b4c5002 mov cx,word ptr [eax+edx*2+2] ds:002b:09340fff=????
我们马上认定usp10.dll是与“Uniscribe Unicode脚本处理器”(Uniscribe Unicode script processor)(微软自己是这样说的) [7]相对应的库。这是一个相对较大的模块(600-800KB,具体取决于操作系统版本与位数),用来渲染Unicode编码的文本,正如它的名字所指的那样。从安全的视角看去,它是重要的,因为代码的日期可以追溯到Windows 2000,并且包含一个用C++实现的对多种复杂TrueType/OpenType结构的解析,作为对已经实现在内核功能的补充。Uniscribe涉及到的特定的表主要是高级排版表(“GDEF”, “GSUB”, “GPOS”, “BASE”, “JSTF”),但某种程度上也有“OS/2”, “cmap” 和 “maxp”。同样重要的是这个库的代码可以通过调用DrawText[8]或另外等价的API配合Unicode编码的文本和攻击者控制的字体访问到。既然执行这个库的大部分暴露对外的代码时,除了这几个典型API外没有别的API是必须的,这就为使用GDI去渲染文本,同时使用不可信任源的字体的应用程序中制造了一个极好的攻击媒介。这一点也被原始crash的栈回溯所证明,我们可以发现这样的事实:它发生在一个(源代码)不包含任何usp10相关代码的程序中:
0026f824 ffffffff USER32!DrawTextExW+0x1e
除一个外,所有这些bug都通过一个标准的DrawText调用进行触发,并且导致了堆破坏。那额外的一个是#1030号问题,它位于一个文档化的Uniscribe特定的API函数:ScriptGetFontAlternateGlyphs中。这个例程用来获取一个特定字符的一组交叉字形,关于这个bug的有趣的一点是它并不是一个操作任何内部数据结构的问题。取而代之的,这个函数没有正确处理cMaxAlternates参数的值,因此导致可向pAlternateGlyphs缓冲区中写入比被调用者允许的更多的返回数据。这意味着这个缓冲区溢出并不与任何特殊的内存类型相关联——而是取决于客户传入了哪个指针,这个溢出可以发生在栈上,堆上或是静态内存上。这样一个bug的利用性极大依赖于用来编译程序的设计和编译选项。然而,我们必须承认,真实世界中的函数情况是怎样,它们是否满足变成一个可被用于攻击对象的要求,我们并不清楚。
更进一步,我们提取了27个不同的空指针导致的内存访问违例的崩溃,这些可能导致存储在进程地址空间内的信息泄露。鉴于这些crash的巨大数量,我们不可能一一去更详细地分析它们或进行任何高级的数据去重。取而代之的,我们通过顶层异常地址对它们进行了区分,并将这些bug统一填在#1031的入口中。
最终,这27个崩溃里面存在21个实际的bug,这21个漏洞微软在MS17-011安全公告里面以编号为CVE-2017-0083, CVE-2017-0091, CVE-2017-0092和CVE-2017-0111 至 CVE-2017-0128修复。
最后,我们也报告了7个没有修复时间限制的unicode空指针引用问题,希望微软修复这些漏洞能够使我们的fuzzer发现更多另外的bug。在3月17号,MSRC回复说他们调查了这些例子,得出的结论是它们只是低安全等级的DoS问题,并且在最近的将来不会作为安全公告的一部分修复。
突变的配置文件也与我们之前为内核准备的相当相似:我们使用相同的五个标准的位翻转,比特翻转,chunkspew,特殊整形和二进制算数算法配合预先计算好的每个表里面突变的比例范围。为Uniscribe做的唯一的特别改变是为“BASE” 和 “JSTF”表增加了突变,这是我们之前没有用到的。
最后但并不是最不重要的,我们扩展了客户机Fuzzing用具的功能性,负责调用被测试的字体相关API(大多数以不同的点大小展现字形,但也要求一些属性等)。当我们清楚其中的一些相关代码只需通过调用无需修改的user32!DrawText时就会被自动执行后,我们想要去尽可能最大化地覆盖Uniscribe库的代码。关于它的导出函数可以MSDN[9]上找到。在浏览了这些文档后,我们增加了对ScriptCacheGetHeight,ScriptGetFontProperties, ScriptGetCMap, ScriptGetFontAlternateGlyphs, ScriptSubstituteSingleGlyph和ScriptFreeCache的调用。这马上被证明是一个成功的主意,因为它允许我们在ScriptGetFontAlternateGlyphs内去发现上述通用的bug。更进一步,我们决定移除对GetKerningPairs和GetGlyphOutline系列API函数调用,因为它们对应的逻辑在内核里面,而我们当前的关注重点被严格限制在用户态模式。被移除的那些API不会导致任何Uniscribe的新bug的发现,取而代之的是降低整个fuzzing过程的速度。除了这些微小的改动外,测试用具的核心都保持不变。
通过采取上面提到的方法,我们希望它们在触发大多数低层次的bug上是高效的。有了这个假设,现在剩下的唯一问题就是确保crash会被可靠地捕获并且报告给fuzzer。这个问题在下面一节里面讨论。
感谢这一事实:usp10.dll里被用来fuzz测试的代码执行在与剩下的用具逻辑相同的上下文。我们不需要写一个成熟的Windows调试器去监视另外一个进程。取而代之的,我们只需要使用SetUnhandledExceptionFilter 函数设定一个顶层异常处理例程,这个函数在每次进程发生关键异常的时候都会被调用。这个历程的作用是将当前发生崩溃的CPU的上下文信息(通过ExceptionInfo->ContextRecord传递)通过“debug print”这一上层调用发送给虚拟机接管程序(例如Bochs指令),从而报告崩溃发生的特定地址。
在内核模式的字体fuzzing场景中,crash通过 BX_INSTR_RESET指令的回调例程被被Boshs指令检测到。这个方法是管用的,因为客户系统被配置为在蓝屏发生后自动重启,因而触发了bx_instr_reset例程。在用户模式Fuzzing下整合这个方法只需要在异常处理的结束例程中增加一个ExitWindowsEx调用。这使得任何事物都可以直接使用而无需涉及已有的Bochs指令。然而,这个方法会导致有关崩溃位置的信息丢失,这使得自动去重变为不可能。为了解决这个问题,我们引入看一种新的“崩溃发生”的Hyper调用,它从客户机的参数里面接收出错指令的地址,并将它进一步传到我们可扩展的fuzzing体系中。在开启后立即通过异常地址对崩溃点进行分组,从而帮助我们节省了大量的处理时间,并且将我们需要去看的测试案例限制在了最低。
这是为到目前为止我们已经使用快两年的Windows内核字体Fuzzing组织的不同系列的最后一篇,也是对我们几个月前刚开始,但已经被证明很高效的用户模式的fuzzing组织的一个总结。除此之外的每件事都与去年“font fuzzing techniques”系列文章中描述的一模一样。[4]
这次的努力和它的结果显示fuzzing是一门非常通用的技术,它的大部分组件可以被简单地从一个目标移到另一个目标重复使用,特别是在单个文件格式的范围内。最终,它被证明不仅可以fuzz Windows内核,也可以fuzz常规的用户态模式,如果不考虑客户机的系统环境(在我们的例子里面,Windows或是Linux)。尽管Bochs x86模拟器与原始操作系统执行速度相比增大了很多开销,它通常可以缩小以达到每秒迭代次数的净增益。作为一个有趣的事实,问题#993(Windows内核注册表加载),在上个补丁日修复的问题#1042(GDI+中的EMF+处理),问题#1052和#1054(颜色配置文件处理),我们也是通过在Boshs上fuzzing Windows发现的,只是在输入样本,测试用具和突变策略上有些轻微的不同。:)
原文链接:https://googleprojectzero.blogspot.jp/2017/04/notes-on-windows-uniscribe-fuzzing.html
在几周前微软三月星期二补丁日1 修复的119个CVE2 漏洞中,有29个漏洞来自我们报告的Uniscribe库的字体处理代码。诚然,字体相关的安全话题已经在这个博客上进行过广泛的讨论,包括手工分析[1][2]和Fuzzing[3][4]两个方面的内容。然而,与包含在win32k.sys和ATMFD.DLL3 这两个驱动里的内核模式的字体实现相反,这次的尝试和之前的的一点不同之处在于Uniscribe是一个鲜为人知的用户模式下的组件,此前它并未被广泛认为是一个可行的攻击媒介。在这篇文章中,我们先列出Uniscribe的简要历史和描述,解释一下我们是如何实现规模化地fuzzing这个库,然后强调一些我们目前已有的更有趣的发现。所有我们涉及到的这些bug(正如它们被提交给微软那样),都有相应的POC(概念证明)样本,它们可以在Project Zero bug追踪的官方网页[5]上找到。请享受这一切!
简介
752378f3 668b4c5002 mov cx,word ptr [eax+edx*2+2] ds:002b:09340fff=????
我们马上认定usp10.dll是与“Uniscribe Unicode脚本处理器”(Uniscribe Unicode script processor)(微软自己是这样说的) [7]相对应的库。这是一个相对较大的模块(600-800KB,具体取决于操作系统版本与位数),用来渲染Unicode编码的文本,正如它的名字所指的那样。从安全的视角看去,它是重要的,因为代码的日期可以追溯到Windows 2000,并且包含一个用C++实现的对多种复杂TrueType/OpenType结构的解析,作为对已经实现在内核功能的补充。Uniscribe涉及到的特定的表主要是高级排版表(“GDEF”, “GSUB”, “GPOS”, “BASE”, “JSTF”),但某种程度上也有“OS/2”, “cmap” 和 “maxp”。同样重要的是这个库的代码可以通过调用DrawText[8]或另外等价的API配合Unicode编码的文本和攻击者控制的字体访问到。既然执行这个库的大部分暴露对外的代码时,除了这几个典型API外没有别的API是必须的,这就为使用GDI去渲染文本,同时使用不可信任源的字体的应用程序中制造了一个极好的攻击媒介。这一点也被原始crash的栈回溯所证明,我们可以发现这样的事实:它发生在一个(源代码)不包含任何usp10相关代码的程序中:
0026f824 ffffffff USER32!DrawTextExW+0x1e
结果一览
Tracker ID |
崩溃点的内存访问类型 |
崩溃函数 |
CVE |
n字节的非法写内存(memcpy) |
usp10!otlList::insertAt |
CVE-2017-0108 |
|
2字节的非法读/写内存 |
usp10!AssignGlyphTypes |
CVE-2017-0084 |
|
n字节的非法写内存(memset) |
usp10!otlCacheManager::GlyphsSubstituted |
CVE-2017-0086 |
|
n字节的非法写内存(memcpy) |
usp10!MergeLigRecords |
CVE-2017-0087 |
|
2字节的非法写内存 |
usp10!ttoGetTableData |
CVE-2017-0088 |
|
2字节的非法写内存 |
usp10!UpdateGlyphFlags |
CVE-2017-0089 |
|
n字节的非法写内存 |
usp10!BuildFSM and nearby functions |
CVE-2017-0090 |
|
n字节的非法写内存 |
usp10!FillAlternatesList |
CVE-2017-0072 |
除一个外,所有这些bug都通过一个标准的DrawText调用进行触发,并且导致了堆破坏。那额外的一个是#1030号问题,它位于一个文档化的Uniscribe特定的API函数:ScriptGetFontAlternateGlyphs中。这个例程用来获取一个特定字符的一组交叉字形,关于这个bug的有趣的一点是它并不是一个操作任何内部数据结构的问题。取而代之的,这个函数没有正确处理cMaxAlternates参数的值,因此导致可向pAlternateGlyphs缓冲区中写入比被调用者允许的更多的返回数据。这意味着这个缓冲区溢出并不与任何特殊的内存类型相关联——而是取决于客户传入了哪个指针,这个溢出可以发生在栈上,堆上或是静态内存上。这样一个bug的利用性极大依赖于用来编译程序的设计和编译选项。然而,我们必须承认,真实世界中的函数情况是怎样,它们是否满足变成一个可被用于攻击对象的要求,我们并不清楚。
更进一步,我们提取了27个不同的空指针导致的内存访问违例的崩溃,这些可能导致存储在进程地址空间内的信息泄露。鉴于这些crash的巨大数量,我们不可能一一去更详细地分析它们或进行任何高级的数据去重。取而代之的,我们通过顶层异常地址对它们进行了区分,并将这些bug统一填在#1031的入口中。
最终,这27个崩溃里面存在21个实际的bug,这21个漏洞微软在MS17-011安全公告里面以编号为CVE-2017-0083, CVE-2017-0091, CVE-2017-0092和CVE-2017-0111 至 CVE-2017-0128修复。
最后,我们也报告了7个没有修复时间限制的unicode空指针引用问题,希望微软修复这些漏洞能够使我们的fuzzer发现更多另外的bug。在3月17号,MSRC回复说他们调查了这些例子,得出的结论是它们只是低安全等级的DoS问题,并且在最近的将来不会作为安全公告的一部分修复。
输入语料,突变配置文件和调整测试用具
突变的配置文件也与我们之前为内核准备的相当相似:我们使用相同的五个标准的位翻转,比特翻转,chunkspew,特殊整形和二进制算数算法配合预先计算好的每个表里面突变的比例范围。为Uniscribe做的唯一的特别改变是为“BASE” 和 “JSTF”表增加了突变,这是我们之前没有用到的。
最后但并不是最不重要的,我们扩展了客户机Fuzzing用具的功能性,负责调用被测试的字体相关API(大多数以不同的点大小展现字形,但也要求一些属性等)。当我们清楚其中的一些相关代码只需通过调用无需修改的user32!DrawText时就会被自动执行后,我们想要去尽可能最大化地覆盖Uniscribe库的代码。关于它的导出函数可以MSDN[9]上找到。在浏览了这些文档后,我们增加了对ScriptCacheGetHeight,ScriptGetFontProperties, ScriptGetCMap, ScriptGetFontAlternateGlyphs, ScriptSubstituteSingleGlyph和ScriptFreeCache的调用。这马上被证明是一个成功的主意,因为它允许我们在ScriptGetFontAlternateGlyphs内去发现上述通用的bug。更进一步,我们决定移除对GetKerningPairs和GetGlyphOutline系列API函数调用,因为它们对应的逻辑在内核里面,而我们当前的关注重点被严格限制在用户态模式。被移除的那些API不会导致任何Uniscribe的新bug的发现,取而代之的是降低整个fuzzing过程的速度。除了这些微小的改动外,测试用具的核心都保持不变。
通过采取上面提到的方法,我们希望它们在触发大多数低层次的bug上是高效的。有了这个假设,现在剩下的唯一问题就是确保crash会被可靠地捕获并且报告给fuzzer。这个问题在下面一节里面讨论。
崩溃检测
感谢这一事实:usp10.dll里被用来fuzz测试的代码执行在与剩下的用具逻辑相同的上下文。我们不需要写一个成熟的Windows调试器去监视另外一个进程。取而代之的,我们只需要使用SetUnhandledExceptionFilter 函数设定一个顶层异常处理例程,这个函数在每次进程发生关键异常的时候都会被调用。这个历程的作用是将当前发生崩溃的CPU的上下文信息(通过ExceptionInfo->ContextRecord传递)通过“debug print”这一上层调用发送给虚拟机接管程序(例如Bochs指令),从而报告崩溃发生的特定地址。
在内核模式的字体fuzzing场景中,crash通过 BX_INSTR_RESET指令的回调例程被被Boshs指令检测到。这个方法是管用的,因为客户系统被配置为在蓝屏发生后自动重启,因而触发了bx_instr_reset例程。在用户模式Fuzzing下整合这个方法只需要在异常处理的结束例程中增加一个ExitWindowsEx调用。这使得任何事物都可以直接使用而无需涉及已有的Bochs指令。然而,这个方法会导致有关崩溃位置的信息丢失,这使得自动去重变为不可能。为了解决这个问题,我们引入看一种新的“崩溃发生”的Hyper调用,它从客户机的参数里面接收出错指令的地址,并将它进一步传到我们可扩展的fuzzing体系中。在开启后立即通过异常地址对崩溃点进行分组,从而帮助我们节省了大量的处理时间,并且将我们需要去看的测试案例限制在了最低。
这是为到目前为止我们已经使用快两年的Windows内核字体Fuzzing组织的不同系列的最后一篇,也是对我们几个月前刚开始,但已经被证明很高效的用户模式的fuzzing组织的一个总结。除此之外的每件事都与去年“font fuzzing techniques”系列文章中描述的一模一样。[4]
结论
这次的努力和它的结果显示fuzzing是一门非常通用的技术,它的大部分组件可以被简单地从一个目标移到另一个目标重复使用,特别是在单个文件格式的范围内。最终,它被证明不仅可以fuzz Windows内核,也可以fuzz常规的用户态模式,如果不考虑客户机的系统环境(在我们的例子里面,Windows或是Linux)。尽管Bochs x86模拟器与原始操作系统执行速度相比增大了很多开销,它通常可以缩小以达到每秒迭代次数的净增益。作为一个有趣的事实,问题#993(Windows内核注册表加载),在上个补丁日修复的问题#1042(GDI+中的EMF+处理),问题#1052和#1054(颜色配置文件处理),我们也是通过在Boshs上fuzzing Windows发现的,只是在输入样本,测试用具和突变策略上有些轻微的不同。:)
参考文献
“一个字体漏洞适用于所有”系列文章:Tracker ID |
崩溃点的内存访问类型 |
崩溃函数 |
CVE |
n字节的非法写内存(memcpy) |
usp10!otlList::insertAt |
CVE-2017-0108 |
|
2字节的非法读/写内存 |
usp10!AssignGlyphTypes |
CVE-2017-0084 |
|
n字节的非法写内存(memset) |
usp10!otlCacheManager::GlyphsSubstituted |
CVE-2017-0086 |
|
n字节的非法写内存(memcpy) |
usp10!MergeLigRecords |
CVE-2017-0087 |
|
2字节的非法写内存 |
usp10!ttoGetTableData |
CVE-2017-0088 |
|
2字节的非法写内存 |
usp10!UpdateGlyphFlags |
CVE-2017-0089 |
|
n字节的非法写内存 |
usp10!BuildFSM and nearby functions |
CVE-2017-0090 |
|
n字节的非法写内存 |
usp10!FillAlternatesList |
CVE-2017-0072 |
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)