查看原文
其他

这个 WebKit 漏洞助力 Pwn2Own 冠军斩获5.5万美元赏金(详细分析)

ZDI 代码卫士 2023-03-30
 聚焦源代码安全,网罗国内外最新资讯!
编译:奇安信代码卫士团队
Pwn2Own 东京大赛已落下帷幕,我不由想起了三次蝉联大赛冠军的 Fluoroacetate 团队在Pwn2Own 温哥华大赛上使用的一个 WebKit 漏洞。它作为利用链的一部分,让冠军团队打了一次漂亮仗:赢得5.5万美元的奖励金。奇安信代码卫士团队现将该漏洞的详细分析翻译如下,希望给读者带来一些启发。
我们先从 PoC 开始:
 


首先,我们需要编译一下受影响的 WebKit 版本。在2019年 Pwn2Own 春季大赛时,它的 Safari版本是12.0.3。根据苹果公司的描述,它就是版本 240322。
svn checkout -r 240322 https://svn.webkit.org/repository/webkit/trunk webkit_ga_asan

我们用AddressSanitizer (ASAN) 编译后就会检测到内存损坏情况。
ZDIs-Mac:webkit_ga_asan zdi$ Tools/Scripts/set-webkit-configuration --asan ZDIs-Mac:webkit_ga_asan zdi$ Tools/Scripts/build-webkit # --jsc-only can be used here which should be enough

由于 iidb已经包含在macOS 中,因此我们将使用iidb  进行调试。由于上述 POC 并不包含任何渲染代码,因此我们只能在 iidb 中通过 JavaScriptCore (JSC) 执行。在 iidb 中执行 jsc需要调用其二进制文件而非脚本 run-jsc。我们可从 WebKitBuild/Release/jsc 中获取该文件,而要让其正确运行则必须获取一个环境变量。
这里需要指出的是:
env DYLD_FRAMEWORK_PATH=/Users/zdi/webkit_ga_asan/WebKitBuild/Release

可在 iidb中运行,但我更偏向于将其放在一个文本文件并传递到 iidb –s 中。
ZDIs-Mac:webkit_ga_asan zdi$ cat lldb_cmds.txt env DYLD_FRAMEWORK_PATH=/Users/zdi/webkit_ga_asan/WebKitBuild/Release r
现在我们开始调试:


它在 0x6400042d1d29:mov qword ptr [rcx + 8*rsi], r8 处崩溃,看似是界外写入问题。该堆栈追踪表明这种现象发生在虚拟机中,也就是存在于编译代码中或即时编译 (JIT) 后的代码中。我们还注意到被用作索引的 rsi 中包含 0x20000040之前在 POC 中已经见过该数字。


它是bigarr 的大小(减1),实际上就是 NUM_SPREAD_ARGS* sizeof(a)
为了看到被即时编译(JIT) 过的代码,我们可以设置环境变量 JSC_dumpDFGDisassembly,这样 jsc 就可以以 DFGFTL 格式转储其编译代码。
ZDIs-Mac:webkit_ga_asan zdi$ JSC_dumpDFGDisassembly=true lldb -s lldb_cmds.txt WebKitBuild/Release/jsc ~/poc3.js


这样做会转储很多外部汇编。那么我们如何定位相关代码呢?
既然我们知道崩溃是在 0x6400042d1d29: mov qword ptr [rcx + 8*rsi], r8 处发生的,何不搜查下这个地址?或许会发现一些相关的东西。
果不其然!就在 DFG 格式中!
 
 
当使用 DFG JIT 层中的展开运算符 () 创建一个新数组时会调用NewArrayWithSpread。它会在由 gen_func 生成且在某循环中调用的函数 f 中发生。迭代 f ITERS 次数的主要原因是让该代码部分变热 (hot) ,从而得到 DFG JIT 层的优化。
深挖源代码后,我们在 Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp 中发现了函数 SpeculativeJIT::compileNewArrayWithSpread而这正是 DFG 发出 (emit)代码的地方。发出代码就是将 JIT 生成的机器代码写入内存中供后续执行。
看下compileNewArrayWithSpread 我们就能理解该机器代码了。可以看到 compileAllocateNewArrayWithSize() 负责分配某大小的新数组。它的第三个参数 sizeGPR 作为第二个参数被传递到 emitAllocateButterfly() 中,也就是说它将为数组分配一个新的 butterfly,即包含 JS 对象多个值的内存空间。
参照如下网站了解 JSObject butterfly 情况:
https://liveoverflow.com/the-butterfly-of-jsobject-browser-0x02/


再跳到emitAllocateButterfly(),我们看到大小 (size) 参数 sizeGPR 向左移动了3个位(乘以8)之后被添加到常数sizeof (IndexingHeader) 中。
简言之,我们需要匹配函数中真正的机器代码和 C++ 代码。字段 m_jit 的类型是 JITCompiler。

DFG::JITCompiler) 负责从数据流图中生成 JIT 代码。它通过委派投机性和非投机性 JIT 来实现。这些JIT 生成到一个 MacroAssemblerJITCompilier 通过集成关系拥有)。JITCompiler 保留对编译期间所需信息的应用,同时记录链接中使用的信息(如所有要链接的调用列表)。
也就是说我们看到的调用如 m_jit.move()m_jit.add32() 等都是emit汇编的函数。通过追踪我们就能够匹配相应的 C++ 代码。除了配置用于追踪内存分配的 malloc 调试功能外,我们在iidb 上配置了所选的 Intel 汇编。
ZDIs-Mac:~ zdi$ cat ~/.lldbinit settings set target.x86-disassembly-flavor intel type format add --format hex long type format add --format hex "unsigned long" command script import lldb.macosx.heap settings set target.env-vars DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib settings set target.env-vars MallocStackLogging=1 settings set target.env-vars MallocScribble=1
由于启用Guard Malloc 后分配了一个大的size,因此需要设置一个允许如此分配的另外一个环境变量。
ZDIs-Mac:webkit_ga_asan zdi$ cat lldb_cmds.txt env DYLD_FRAMEWORK_PATH=/Users/zdi/webkit_ga_asan/WebKitBuild/Release env MALLOC_PERMIT_INSANE_REQUESTS=1
JSC_dumpDFGDisassembly 将会以 AT&T 格式转储汇编,因此我们运行  disassemble -s 0x6400042d1c22-c 70 以符合Intel 要求,结果如下:
 


我们来试着匹配一下  emitAllocateButterfly() 中的某些代码。查看汇编列表后,我们可以匹配如下情况: 


是时候观察下该机器代码的动机了。首先设置一个断点,然后观察。为此,在编译前为 jsc.cpp 添加一个 dbg() 函数,这样就能随时分解为 JS 代码。编译器提示未使用 EncodedJSValue JSC_HOST_CALL functionDbg(ExecState* exec) 函数中的 exec,因此失败了。为此,我们只添加 exec->argumentCount(); 它应该不会影响执行。
在这里加一个dbg(),因为在创建bigarr 过程中我们会执行真正的 NewArrayWithSpread 函数。


再次运行 JSC_dumpDFGDisassembly=true lldb -s lldb_cmds.txtWebKitBuild/Release/jsc ~/poc3.js 将转储该汇编并在如下地方停止:
它恰恰在创建bigarr 之前断开,我们就看到了 NewArrayWithSpread 的机器代码。我们在该函数开头设置一个断点,然后继续执行。


达到断点了!


在继续分析之前,先来看看内存中的 JS 对象是什么情况。Describe() 函数仅在 jsc 中运行,我们能够借此了解到 JS 对象在内存中的位置、类型等等,如下所示:


注意一下在增加一个对象后,上述 arr_dbl对象的类型如何从 ArrayWithDouble 转变为 ArrayWithContiguous这是因为结构已经改变,因此不再只是存储两个值,而是多个类型。
内存中的 JS 对象如下所示:


我们从上述例子中的 arr 数组开始。通过转储对象地址 0x1034b4320,我们可以看到两个“四字”,一个是 JSCell,一个是 butterfly 指针。
JSCell 由如下部分组成:
--StructureID m_structureID;#例如对象 arr 中第一个四字中的 0x5f (95) (4个字节)
--IndexingType m_indexingTypeAndMisc; # 0x05 (1个字节)
--JSType m_type; # 0x21(1个字节)
--TypeInfo::InlineTypeFlags m_flags; # 0x8 (1个字节)
-- CellState m_cellState; # 0x1 (1个字节)
该butterfly 指针指向的是数组中真正的元素。

 
这里显示的值1、2、3、4、6 以0xffff 开头,因为它就是整数在内存中表示的 JSValue 格式。如果以 0x10 字节的方式表示,则知道该数组的长度是5。


某些对象并不存在butterfly,因此它们的指针是空或者0。它们的属性将被存储为如下所示的内联存储形式。


该脚本将有助于双重内存地址的转换,反之亦然。


上面我们简单地介绍了 WebKit 的相关信息。更多详情可参见:https://liveoverflow.com/getting-into-browser-exploitation-new-series-introduction-browser-0x00/
接下来我们深入分析一下这个断点。
 


这里究竟发生了什么?
注意一下 PoC 中的这个部分:


函数mk_arr 创建了一个数组,其第一个参数是大小,第二个是元素。其中大小是 (0x20000000 + 0x40) / 8 = 0x4000008,从而创建了一个大小为 0x4000008、元素值为 0x4141414141410000 的数组。函数 i2f 是为了将整数转换为浮点数,因此它在内存中以期望的值结束。
于是,我们了解到rcx 指向对象 a 的butterfly -0x10,由于它的大小是 rcx + 8,因此butterfly 就是 rcx + 0x10。查看余下代码,我们可以看到 r8、r10、rdi、r9、rbx、r12 和 r13 均指向对象a 的一个副本,确切地说是8个副本,而 edx 还在继续加和每个副本的大小。


看下 edx,它的值变成了 0x20000040


那么,这8个 a 副本是什么?0x20000040的值是多少?
我们再来看下这个 PoC。


f 变为:

通过展开第一个参数的 NUM_SPREAD_ARGS (8) 副本以及第二个参数的单个副本,f 创建了一个数组。F 被对象a (8*0x04000008)c(长度为1)调用。当调用NewArraryWithSpread 时,它为8个a 和1个c 留下空间。
最后一步分析发现了对象 c 的长度,使得edx 的最终值变为 0x20000041


下一步应该是对该长度的分配,它出现在 emitAllocateButterfly() 中。


我们注意到发生在 shl r8d, 0x3 处发生的溢出情况,0x20000041被回绕为0x208当它被传递给 emitAllocateVariableSized() 时,它的分配大小变为 0x210

我们看到的这种违反界外读取权限的行为发生在如下关于 mov qword ptr [rcx + 8*rsi] 的如下片段中。此代码片段以不正确的大小 0x20000041 向后迭代新建的 butterfly,而实际上溢出之后的真实大小0x210。之后它将每个元素清零但由于内存中的实际大小远小于 0x20000041,因此它触发了 ASAN 构建中的越界访问冲突。



原语


它看似是一个整数溢出问题,但实际上后果要严重得多。当分配大小回绕后,它要比初始值小,从而导致创建了过小的 butterfly。结果就是当写入数据时会触发堆溢出问题,因此其附近的其它数组也将被破坏。我们计划执行的操作如下:
  • spray 一堆数组

  • 写入bigarr 以导致堆溢出,从而破坏被 spray 的数组

  • 使用被损坏的数组使用虚假的 JS 对象实现堆读(addrOf)/写(fake)

如下代码片段展示的是 spray。当调用 f() 创建大小为 0x20000041 的 butterfly 时,会触发整数溢出问题,从而因为回绕造成过小的 butterfly。然而,仍然会写入 0x20000041 个元素,从而导致堆溢出问题。当访问 c 时,第一个元素已定义的 getter 将启动并使用slice() 调用中新建的数组的 0x4000 个元素填充spray 数组。


Spray 中创建的大量 butterfly 以及 bigarr 的 butterfly 的巨大长度注定会在某个点重叠,这是由堆溢出问题以及butterfly 在同样的内存空间中创建的情况导致的。在非 ASAN 发布 build 中执行 POC 后,我们会得到如下结果:


我们注意到其中一个spray 对象的butterfly (或者是 spray_arr 或者是 spray_arr2)和 bigarr 是如何重叠的。
我们可视化一下整个情况:
 


在这里需要注意的是spray_arrspray_arr2 的类型(分别是 ArrayWithDoubleArrayWithContiguous),因为这是构建利用原语的必要条件。这意味着类型为 ArrayWithDouble 的数组包含未 box 的浮点值,也就是说元素被当作原生浮点数读写。ArrayWithContiguous的不同之处在于,它的元素被当做 box 的 JSValues,因此它读写 JS 对象。
基本的想法是找到将对象写入 ArrayWithContiguous 数组 (spray_arr2) 的方法,之后从 ArrayWithDouble 数组 (spray_arr) 中读取其内存地址。我们将内存地址写入 spray_arr 中并将其当做使用 spray_arr2 的对象读取也是一样的。
为此,我们需要使用两个数组 spray_arrspray_arr2 来持有重叠的空间。
如下:


这个代码片段是spray循环,具体来讲是ArrayWithDouble 实例 (spray_arr) 循环,当它找到第一个重叠空间为bigarr 时退出循环,并返回在 spray 中的索引值oobarr_idx以及指向该空间的新对象 oobarr。退出循环的主要条件是spray[i].length > 0x40,因为当 spray[i] 指向 bigarr 数据(0x4142414141410000)时,它的长度将会向后定位8个字节,也就是  0x4142414141410000。这就使得长度为 0x41410000,也就是 >0x40。什么是oobarr?它是指向spraybigarr之间重叠空间开头的类型为 ArrayWithDouble 的数组。函数 oobarr[0] 应当返回 0x4142414141410000Oobarr数组是我们能够使用的读写对象地址的第一个数组。


contarr 是类型为 ArrayWithContiguous 的数字,它指向和 oobarr 共享的一个空间。如下是执行的代码片段:

如下内容展示的是addrOffake原语。原语addrOf 通过写入ArrayWithContiguous 数组并从 ArrayWithDouble 数组读取为一个浮点数的方式返回任意 JS 对象的地址。原语 fake 正好相反,它用于通过将地址写入 ArrayWithDouble 并从 ArrayWithContiguous 的方式从内存地址中创建一个JS对象。


调试输出中很明确地表明这两种原语按预期工作。


下一步是通过创建虚假对象并控制其 butterfly 的方式实现任意读取/写入。现在我们已经知道,如果数据不是内联的,则对象将数据存储在 butterfly 中。如下:


检查如下内容:


我们创建了一个只有一个包含一个字符串的属性 p0的空数组(长度为0)。它的内存布局如下。当我们看到 butterfly 0x10 时,我们看到了长度的四字以及第一个属性。它的向量长度是0,而属性指向0x1034740a0目前我们可以明确的是要访问对象中的属性,我们得到 butterfly 之后减去 0x10如果我们控制该 butterfly 的话会发生什么情况?答案是任意读取和写入。

对于内存中的任意合法 JS 对象,其 JSCell 必须也是合法的,其中包括其结构 ID。结构ID无法手动生成,但是可预测的,至少在我们准备的 build 上是可预测的。由于我们计划创建一个虚假对象,因此需要确保它的 JSCell 是合法的。
如下代码片段spray 0x400大小的多个 a 对象,因此我们可以预测它的结构 ID 介于1和0x400之间。


我们需要创建一个受控制的受害者对象。如下:mngrstruct_spray 中的中间对象,我们创建 victim 确保它位于 mngr 地址之后的地址范围。


我们将使用outer 对象创建虚假对象 hax第一个属性 a 将是该虚假对象的JSCell它以 0x0108200700000200结尾,也就是说我们预测的结构 ID0x200- (1<<16) data-preserve-html-node="true" 部分用于将值存储在对象中时负责boxing 影响(增加 2^48)。属性 b 将成为虚假对象的 butterfly。为创建hax,我们获得 outer 地址,之后增加 0x10之后我们将结果给到此前创建的 fake该对象的布局在 iidb 输出中如下:


当访问 hax 的索引时,意味着我们在访问从如下 mngr 地址开始的内存空间。由于对象位于相同的空间中且 victim 是最后创建的,因此它位于 mngr 之后。victim_addr 中减去 mngr_addr,我们就能在索引hax 中的结果时知悉 victimJSCell 和 butterfly (+8)。


我们实现一下任意读/写:

 
如此前所述,当访问victim.p0 时,它的 butterfly 被获取,之后向后退 0x10 以获取第一个属性。set_victim_addr 为我们此前增加 0x10 的值设置 victim 的 butterfly。下面的调试器更好地表述了这一点:


从上述转储中我们可以看到,最开始 victim 的butterfly 是 0x18014e8028,之后变为 0x18003e4030而后者实际上是测试的地址增加 0x18当调用 read64 时,它传递的是测试的地址加8。set_victim_addr 中,地址再次增加 0x10。当读取 victim.p0 时,其 butterfy 0x2042fc058 被获取,之后减去 0x10,结果就是实际上指向测试 butterfly 的 0x2042fc048victim.p0 实际上获取的值是属性地址指向的值(本案例中是0x18003e4030)。在其基础上增加 addrOf() 将使我们获取到真正的 0x18003e4030 值。现在我们实现了任意读取结果。我们使用 fake() 来写入 victim.p0 如下:



整个过程一气呵成,是吧?

结论


希望你喜欢这篇深入分析文章。Pwn2Own 大赛展现的部分漏洞是我们见到的最佳漏洞,本文分析的漏洞也不例外。同时希望你也了解了一些关于 iidb 的知识并借此找到 WebKit 中的漏洞。如果发现了类似漏洞,记得提交给ZDI漏洞奖励计划哟~
 




推荐阅读

2019 Pwn2Own 东京大赛落下帷幕:Fluoroacetate 二人组蝉联三连冠

Pwn2Own 2020 黑客大赛将涵盖工控类别



原文链接

https://www.thezdi.com/blog/2019/11/25/diving-deep-into-a-pwn2own-winning-webkit-bug






题图:Pixabay License



本文由奇安信代码卫士编译,不代表奇安信观点,转载请注明“转自奇安信代码卫士 www.codesafe.cn”



奇安信代码卫士 (codesafe)

国内首个专注于软件开发安全的产品线。



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存