专栏名称: 愿做一名渗透小学徒
分享渗透,安服方面的知识,从浅到深,循序渐进。在渗透的路上,让我们从学徒出发。 此公众号提供的任何工具仅供实验使用,如用于其它用途使用,本公众号概不承担任何责任。
目录
相关文章推荐
知产库  ·  DeepSeek商标提前1天被抢先申请,抢注 ... ·  3 天前  
知产宝  ·  著作权 | ... ·  4 天前  
51好读  ›  专栏  ›  愿做一名渗透小学徒

克隆攻击:利用重复的对象属性在 Chrome 渲染器中获取 RCE

愿做一名渗透小学徒  · 公众号  ·  · 2024-07-12 00:20

正文

在这篇文章中,我将利用 Chrome 的 Javascript 引擎 v8 中的一个对象损坏漏洞 CVE-2024-3833,该漏洞允许通过一次访问恶意网站在 Chrome 的渲染器沙箱中执行远程代码执行 (RCE)。

在这篇文章中,我将利用 CVE-2024-3833,这是 Chrome 的 JavaScript 引擎 v8 中的一个对象损坏漏洞,我于 2024 年 3 月将其报告为 漏洞 331383939。 还报告了一个类似的漏洞 331358160 ,其编号为 CVE-2024-3832。这两个漏洞均已在版本 124.0.6367.60/.61 中修复。CVE-2024-3833 允许通过一次访问恶意网站在 Chrome 的渲染器沙箱中进行 RCE。

Chrome 中的 Origin 试用

Chrome 中的新功能有时会在正式发布之前以来源试用版的 形式推出 。当某项功能以来源试用版的形式提供时,网络开发者可以向 Chrome 注册他们的来源,这样他们就可以在注册的来源上使用该功能。这样,网络开发者就可以在他们的网站上测试新功能并向 Chrome 提供反馈,同时保持该功能在未请求使用它们的网站上处于禁用状态。来源试用版的有效期有限,任何人都可以注册他们的来源以使用 有效试用列表 中的功能。通过注册来源,开发者将获得一个来源试用令牌,他们可以通过添加元标记将其包含在他们的网站中

老漏洞

通常,原始试用功能在运行任何用户 Javascript 之前启用。然而,情况并非总是如此。网页可以随时以编程方式创建包含试用令牌的元标记,并且 Javascript 可以在创建标记之前执行。在某些情况下,负责启用特定原始试用功能的代码会错误地认为在此之前没有运行任何用户 Javascript,这可能会导致安全问题。

其中一个例子是 CVE-2021-30561 ,由 Google Project Zero 的 Sergei Glazunov 报告。在这种情况下, 当检测到原始试用令牌时, WebAssembly 异常处理 功能会 Exception 在 Javascript 对象中创建一个属性。 WebAssembly

具体来说,创建属性的代码 Exception 使用内部函数来创建属性,该函数假定该 Exception 属性不存在于 WebAssembly 对象中。如果用户 Exception 在激活试用之前创建了该属性,则 Chrome 会尝试 Exception 在 中创建另一个属性 WebAssembly 。这可能会在 中产生两个具有不同值的重复 Exception 属性 WebAssembly 。然后可以利用这一点在 Exception 属性中引起类型混淆,然后可以利用它来获得 RCE。

CVE-2021-30561 的实际情况更为复杂,因为启用 WebAssembly 异常处理功能的代码会进行检查以确保对象 WebAssembly 不包含名为 的属性 Exception 。然而,那里使用的检查是不够的,CVE-2021-30561 通过使用 Javascript Proxy 对象绕过了该检查。有关此绕过和利用如何工作的详细信息,我将建议读者查看 原始错误单 ,其中包含所有详细信息。

另一天,另一次绕道

Javascript Promise Integration 是一项 WebAssembly 功能,目前处于原始试用阶段(直到 2024 年 10 月 29 日)。与 WebAssembly 异常处理功能类似,它 WebAssembly 通过调用以下方法在检测到原始试用令牌时定义对象的属性 InstallConditionalFeatures

当添加 Javascript Promise Integration (JSPI) 时,上面的代码会检查是否 webassembly 已经具有属性 Suspender Function (上面的 1. 和 2.),如果没有,它将分别使用 InstallSuspenderConstructor 和创建这些属性 InstallTypeReflection 。该函数 InstallSuspenderConstructor 用于 在对象上 InstallConstructorFunc 创建属性 Suspender WebAssembly

问题在于,在 中 InstallSuspenderConstructor WebAssembly 对象来自 (上面的 3.) wasm_webassembly_object 的属性,而 签入的对象 来自 全局对象的属性(与全局变量相同 ): context WebAssembly InstallConditionalFeatures WebAssembly WebAssembly

可以使用 Javascript 将全局 WebAssembly 变量更改为任何用户定义的对象:

虽然这会改变 的值 WebAssembly ,但 wasm_webassembly_object 中的缓存 context 不会受到影响。因此,可以先 Suspender 在对象上定义一个属性 WebAssembly ,然后将 WebAssembly 变量设置为不同的对象,然后激活原始试验以 在原始对象中 Javascript Promise Integration 创建副本 Suspender WebAssembly

触发原始试验后, InstallConditionalFeatures 首先检查全局变量 Suspender 中是否存在该属性 WebAssembly (见 newWebAssembly 上文)。然后继续 Suspender 在中创建该属性 context->wasm_webassembly_object (见 oldWebAssembly 上文)。这样做会 Suspender 在中创建一个重复的属性 oldWebAssembly ,就像 CVE-2021-30561 中发生的情况一样。

这会导致 oldWebAssembly 2 个 Suspender 属性存储在不同的偏移量。我将此问题报告为 331358160 ,并被分配了 CVE-2024-3832。

该函数 InstallTypeReflection 存在类似的问题,但还有一些额外的问题:

该函数 InstallTypeReflection 还在其他各种对象中定义 type 属性。例如,在 1. 中,在 的对象 type 中创建属性 ,而不检查该属性是否已存在: prototype wasm_tag_constructor

这样就可以 type 在 上创建重复的属性 WebAssembly.Tag.prototype 。此问题报告为 331383939 ,并被分配了 CVE-2024-3833。

新的漏洞利用

CVE-2021-30561 的漏洞利用依赖于创建“快速对象”的重复属性。在 v8 中,快速对象将其属性存储在数组中(某些属性也存储在对象本身内)。但是,此后发布了一个 强化补丁 ,该补丁在向快速对象添加属性时会检查重复项。因此,不再可能创建具有重复属性的快速对象。

但是,仍然可以利用该漏洞在“字典对象”中创建重复属性。在 v8 中,属性字典实现为 NameDictionary 。 的底层存储 NameDictionary 实现为一个数组,每个元素都是形式为 的元组 (Key, Value, Attribute) ,其中 Key 是属性的名称。向 添加属性时 NameDictionary ,数组中的下一个空闲条目用于存储这个新元组。利用此漏洞,可以使用重复的 来在属性字典中创建不同的条目。在 CVE-2023-2935 Key 的报告中 ,Sergei Glazunov 展示了如何使用字典对象利用重复属性原语。但是,这依赖于能够将重复属性创建为属性 ,这是 v8 中的一种特殊属性,通常为内置对象保留。同样,在当前情况下这是不可能的。所以,我需要找到一种新的方法来利用这个问题。 AccessorInfo

这个想法是寻找一些内部函数或优化,这些函数或优化将遍历对象的所有属性,但不希望属性重复。我想到的一个这样的优化是对象克隆。

克隆人的进攻

当使用扩展语法 复制对象时 ,将创建原始对象的浅表副本:

在 v8 中,这被实现为 CloneObject 字节码:

首次运行包含字节码的函数时,会生成 内联缓存 代码,并用于在后续调用中处理字节码。在处理字节码时,内联缓存代码还将收集有关输入对象的信息( obj1 ),并为相同类型的输入生成优化的内联缓存处理程序。首次运行内联缓存代码时,没有有关先前输入对象的信息,也没有可用的缓存处理程序。因此,会检测到内联缓存未命中,并 CloneObjectIC_Miss 用于处理字节码。为了了解 CloneObject 内联缓存的工作原理及其与漏洞利用的关系,我将回顾 v8 中对象类型和属性的一些基础知识。v8 中的 Javascript 对象存储一个 map 指定对象类型的字段,特别是它指定属性在对象中的存储方式:

的输出 %DebugPrint 如下:

我们看到 x 有两个属性——一个存储在对象中( a ),另一个存储在 a 中 PropertyArray 。请注意 的长度为 PropertyArray 3 PropertyArray[3] ,而 中只存储一个属性 PropertyArray length a 的 PropertyArray 类似于 C++ 中 a 的容量。容量稍大一些可以避免 每次向对象添加新属性时都 std::vector 必须扩展和重新分配。 PropertyArray

对象的 map 使用字段 inobject_properties unused_property_fields 来指示对象中存储了多少个属性以及 中剩余多少空间 PropertyArray 。在本例中,我们有 2 可用空间( 3 (PropertyArray length) - 1 (property in the array) = 2 )。

当发生缓存未命中时,首先尝试 通过检查 对象 CloneObjectIC_Miss 的来确定克隆的结果( target )是否可以使用与原始对象()相同的映射 如下文中的 1.): source map source GetCloneModeForMap

与我们相关的案例是 FastCloneObjectMode::kDifferentMap 模式。

在此模式下, source 首先通过慢速路径(上文中的 1.)对对象进行浅拷贝。然后将内联缓存的处理程序编码为一对映射,分别由 source target 对象的映射组成(上文中的 2.)。

从现在开始,如果要 source_map 克隆另一个具有的对象,则 使用 内联缓存处理程序 source 来克隆该对象。本质上,对象的复制方式如下:

  1. 复制 source 对象的 PropertyArray:

  2. 分配目标对象 并用作 result_map 其映射。

  3. 将对象内的属性 从复制 source target

如果我尝试克隆具有重复属性的对象会发生什么?首次运行代码时, CloneObjectSlowPath 会调用 来分配 target 对象,然后将每个属性从 复制 source target 。但是, 中的代码 CloneObjectSlowPath 可以正确处理重复属性,因此当遇到 中的重复属性时 source ,不会在 中创建重复属性 target ,而是覆盖现有属性。例如,如果我的 source 对象具有以下布局:

PropertyArray 长度为 , 其中 的最后一个属性 4 克隆此对象的结果将覆盖第一个 属性: type PropertyArray target type

请注意, target 具有和 PropertyArray length 3 三个属性 PropertyArray (属性 #a4..#a6 location 在中有)。特别是,对象 properties 没有 unused_property_fields target

虽然这看起来像是一个挫折,因为重复的属性不会传播到对象 target ,但真正的魔力发生在内联缓存处理程序接管时。请记住,当使用内联缓存处理程序克隆时,生成的对象 map target 中的对象相同 CloneObjectSlowPath ,而 是 对象的 PropertyArray 的副本 。这意味着来自内联缓存处理程序的克隆 具有以下属性布局: PropertyArray source target

请注意,它有一个 PropertyArray length 4 但数组中只有三个属性,剩下一个未使用的属性。但是,它与 ( ) map 使用的相同 ,后者没有 CloneObjectSlowPath 0x38ea003978b9 unused_property_fields

unused_property_fields 因此,我得到的不是具有重复属性的对象,而是具有不一致的和 的 对象 PropertyArray 。现在,如果我向此对象添加新属性, map 则会创建一个新的 来反映对象的新属性布局。这个新的 map 具有 unused_property_fields 基于旧的 的 map ,它是在 中计算的 AccountAddedPropertyField 。本质上,如果旧的 unused_property_fields 为正,则这会将 减 unused_property_fields 一以解释正在添加的新属性。如果旧的 unused_property_fields 为零,则将新的 unused_property_fields 设置为二,以解释 PropertyArray 已满且必须 扩展 的 事实。

另一方面,延长的决定 PropertyArray 是基于其 length 而不是 unused_property_fields map

因此,如果我有一个对象, unused_property_fields 其 中有一个空格,但 中有一个空格 PropertyArray (即 length = existing_property_number + 1 ),那么 PropertyArray 当我添加新属性时 不会扩展。因此,添加新属性后, 将 PropertyArray 是满的。但是,如前所述, unused_property_fields 会独立更新,并且会设置为 2,就像 扩展 PropertyArray 了 一样:

这很重要,因为 v8 的 JIT 编译器 TurboFan 使用来 unused_property_fields 决定是否 PropertyArray 需要 扩展

unused_property_fields 因此,通过向具有两个和一个完整的JIT 的对象添加新属性 PropertyArray ,我将能够写入 PropertyArray 越界 (OOB) 并覆盖其后分配的所有内容。

创建具有重复属性的快速对象

为了触发 OOB 写入 PropertyArray ,我首先需要创建一个具有重复属性的快速对象。如前所述, 强化补丁 在向快速对象添加属性时引入了对重复项的检查,因此我无法直接创建具有重复属性的快速对象。解决方案是首先利用该漏洞创建一个具有重复属性的字典对象,然后将该对象更改为快速对象。为此,我将使用 来 WebAssembly.Tag.prototype 触发该漏洞:

一旦我得到了一个具有重复属性的字典对象,我就可以使用 将其更改为快速对象 MakePrototypesFast ,这可以通过 属性访问 触发:

通过使 x 成为对象的原型 y ,然后访问 的属性 y MakePrototypesFast 被称为 变为 x 具有重复属性的快速对象。此后,我可以克隆 x 以触发 中的 OOB 写入 PropertyArray

利用 PropertyArray 中的 OOB 写入

要利用 中的 OOB 写入 PropertyArray ,我们首先检查并查看 之后分配了什么 PropertyArray 。回想一下 是 PropertyArray 在内联缓存处理程序中分配的。从 处理程序代码 中,我可以看到 是在 对象分配 PropertyArray 之前分配的: target

由于 v8 线性分配对象,因此 OOB 写入允许我更改对象的内部字段。为了利用此漏洞,我将覆盖 对象 target 的第二个字段,即存储 对象 地址的字段 。这涉及创建 JIT 函数以向对象添加两个属性 target properties PropertyArray target target

当将属性存储到 具有不一致 和的 a8 损坏对象时 ,对的 OOB 写入 用 Javascript 对象 覆盖 。然后可以通过仔细排列 v8 堆中的对象来利用这一点。由于对象在 v8 堆中线性分配,因此可以通过按顺序分配对象来轻松排列堆。例如,在以下代码中: obj PropertyArray unused_property_fields PropertyArray PropertyArray obj a8

对象周围的 v8 堆 a8 如下所示:

左侧显示对象 a8 a7 。字段 map properties, elements 是与 Javascript 对象相对应的 C++ 对象中的内部字段。右侧表示将内存视为的视图 PropertyArray obj 当的设置为的地址时 PropertyArray obj 。A a8 PropertyArray 两个内部字段 map length 。当对象 a8 与的类型混淆时, PropertyArray properties 字段(即其的地址 PropertyArray )被解释为 length 的的 。由于地址通常是一个大数字,因此这允许进一步对的进行 OOB 读取和 写入 PropertyArray obj PropertyArray obj

ai+3 的属性 PropertyArray 将与 length 的字段对齐 Array a7 。通过写入此属性,可以覆盖 length Array a7 。这使我能够在 Javascript 数组中实现 OOB 写入,这可以以 标准方式 利用。但是,为了覆盖 字段 length ,我必须不断添加属性, obj 直到到达 length 字段。不幸的是,这意味着我还将覆盖 map properties elements 字段,这将破坏 Array a7

为了避免覆盖 的内部字段 a7 ,我将改为创建 , a7 以便其 PropertyArray 在它之前分配。这可以通过 a7 使用克隆创建来实现:







请到「今天看啥」查看全文