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

您所在的位置:网站首页 苹果webkit漏洞 这个 WebKit 漏洞助力 Pwn2Own 冠军斩获5.5万美元赏金(详细分析)

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

2023-03-05 15:07| 来源: 网络整理| 查看: 265

 聚焦源代码安全,网罗国内外最新资讯!

编译:奇安信代码卫士团队

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 就可以以 DFG 和 FTL 格式转储其编译代码。

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 生成到一个 MacroAssembler(JITCompilier 通过集成关系拥有)。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_arr 和 spray_arr2 的类型(分别是 ArrayWithDouble 和 ArrayWithContiguous),因为这是构建利用原语的必要条件。这意味着类型为 ArrayWithDouble 的数组包含未 box 的浮点值,也就是说元素被当作原生浮点数读写。而 ArrayWithContiguous的不同之处在于,它的元素被当做 box 的 JSValues,因此它读写 JS 对象。

基本的想法是找到将对象写入 ArrayWithContiguous 数组 (spray_arr2) 的方法,之后从 ArrayWithDouble 数组 (spray_arr) 中读取其内存地址。我们将内存地址写入 spray_arr 中并将其当做使用 spray_arr2 的对象读取也是一样的。

为此,我们需要使用两个数组 spray_arr和spray_arr2 来持有重叠的空间。

如下:

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

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

如下内容展示的是addrOf 和 fake原语。原语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之间。

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

我们将使用outer 对象创建虚假对象 hax。第一个属性 a 将是该虚假对象的JSCell。它以 0x0108200700000200结尾,也就是说我们预测的结构 ID 是 0x200。- (1



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3