0x00 前言
2018年12月,Mozila通过mfsa2018-29发布了Firefox 64,修复了多个安全问题,其中就包含CVE-2018-18492,这是一个与select
元素有关的UAF(use-after-free,释放后重用)漏洞,最早由Nils发现并报告。之前我们已经讨论过UAF问题,也看到厂商采取了多种保护措施想消除这些问题。但直到目前为止,我们还是经常能在web浏览器中发现与UAF相关的漏洞,因此理解这些问题对查找及修复漏洞来说至关重要。在本文中,我们将与大家分享这个UAF漏洞以及相关补丁的具体细节。
0x01 触发漏洞
我们可以使用如下PoC触发该漏洞:
图1. PoC
在存在漏洞Firefox版本中运行该PoC,我们可以看到crash时的栈跟踪状态如下:
图2. 崩溃时的栈跟踪状态
从图中可知,当解析填充0xe5e5e5e5
的某个内存地址时,会出现read access violation(读取访问冲突)。0xe5e5e5e5
是jemalloc
用来“污染”已释放内存的一个值。这里的“污染”指的是以一种可识别模式来填充已被释放的内存,这样便于分析。正常情况下,我们选择的填充模式不要对应于可访问的地址,这样任何尝试从已填充内存中解析值的操作(比如UAF)都会导致crash。
0x02 漏洞分析
PoC包含6行代码,让我们来逐行分析:
- 创建一个
div
元素 - 创建一个
option
元素 - 将
option
元素附加到div
元素,现在这个div
元素是option
元素的父节点 - 将
DOMNodeRemoved
事件监听器添加到div
元素中。这意味着如果option
节点被移除,就会调用我们在这里添加的函数 - 创建一个
select
元素
这里再深入分析一下:
当使用JavaScript创建select
元素时,xul.dll!NS_NewHTMLSelectElement
函数就会获得控制权,该函数会为这个select
元素分配大小为0x188
字节的一个对象:
图3. xul.dll!NS_NewHTMLSelectElement
函数
如上所示,在代码底部会跳转至mozilla::dom::HTMLSelectElement::HTMLSelectElement
函数。
图4. mozilla::dom::HTMLSelectElement::HTMLSelectElement
函数
在该函数中会初始化新分配对象的各种字段。需要注意的是,这里也会分配大小为0x38
字节的另一个对象,将其初始化为HTMLOptionsCollection
对象。因此,每个Select
元素默认情况下都会对应一个options collection(选项集合)。让我们看一下PoC的最后一行。
- 将第二行创建的
option
元素移动到select
元素的options collection中
在JavaScript中执行该操作将导致mozilla::dom::HTMLOptionsCollection::IndexedSetter
函数被调用(大家可以在图2的栈跟踪状态中看到该函数被调用)。
图5. IDA中观察到的程序逻辑
这里浏览器会执行一些检查操作。比如,如果选项索引值大于当前options collection的长度值,那么就会调用mozilla::dom::HTMLSelectElement::SetLength
函数来扩充options collection容量。在PoC中,这个长度值为为0
(参考图1第6行的[0]
)。然后浏览器会进入图5的红色方框处的检测逻辑。如果待设置的索引值不等于options collection的选项数,那么就会进入右分支。在PoC中,我们使用的索引值为0
,而选项数也为0,因此会执行左分支。因此浏览器会调用nsINode::ReplaceOrInsertBefore
函数,如下红框所示:
图6. 调用nsINode::ReplaceOrInsertBefore
函数
在nsINode::ReplaceOrInsertBefore
函数中,浏览器会调用nsContentUtils::MaybeFireNodeRemoved
函数,通知父节点关于子节点移除的相关事件(如果父节点在监听这类事件)。
图7. 调用nsContentUtils::MaybeFireNodeRemoved
函数
由于我们在PoC第4行(参考图1)中,在div
元素上设置了DOMNodeRemoved
事件监听器,因此这里浏览器就会触发我们设置的函数。在这个函数中,第一个sel
变量会被设置为0
,该操作会移除对select
元素的最后一个引用。接下来,函数会创建一个巨大的数组缓冲区,这会给内存带来较大负担,触发垃圾回收机制(garbage collector)。此时会释放select
元素对象,因为已经没有关于该对象的任何引用。现在被释放的内存已被0xe5e5e5e5
填充。最后,函数会调用alert
来flush挂起的异步任务。从nsContentUtils::MaybeFireNodeRemoved
函数返回后,被释放的select
对象会被用来读取指针,触发read access violation:
图8. 触发Read Access Violation
这里需要注意的是,如果浏览器执行的是右分支,那么还是会调用同一个函数(nsINode::ReplaceOrInsertBefore
),但在调用该函数之前,会使用AddRef
函数来增加select
对象的引用计数。如果是这样操作,就不会出现UAF问题:
图9. 通过AddRef
函数避免出现UAF问题
0x03 补丁分析
Mozilla通过d4f3e119ae841008c1be59e72ee0a058e3803cf3改动修复了这个漏洞。这里最主要的改动是修改options collection中对select
元素的弱引用逻辑,替换成强引用:
图10. 补丁细节
0x04 总结
虽然UAF是大家都比较熟悉的一个问题,但在许多浏览器中依然存在。在几个月之前,有攻击者成功利用了Google Chrome浏览器中的UAF漏洞。UAF漏洞也不局限于浏览器,Linux内核之前也推出过一个补丁,用来修复由UAF导致的拒绝服务问题。澄清UAF触发原理是检测这类漏洞的关键所在。与缓冲区溢出问题一样,我们认为软件中无法完全避免UAF。然而,通过正确的编程以及安全的开发实践,在未来我们可以进一步消除、或者缓解UAF所带来的影响。