从pwn.college入门浏览器安全
| 0 | 前言
总之 , 换个东西学一下了 :_(
这段时间想平复下心情 , 所以找了个难的方向了解
希望我不要什么都不擅长吧
| 1 | 简介
v8
V8是谷歌公司用C++编写的开源高性能JavaScript和WebAssembly引擎。 它是Google Chrome浏览器和Node.js运行时的核心组成部分。 V8引擎的主要作用是将开发者编写的JavaScript代码编译成本地机器码,从而让计算机能够直接执行,极大地提升了代码的运行速度。
- 即时编译(JIT): V8引擎在执行JavaScript代码时,会将其编译成机器码,而不是逐行解释。这使得后续的执行速度更快。
- 垃圾回收机制: V8能够自动管理内存,识别并回收不再使用的对象,帮助开发者避免内存泄漏问题。
- 跨平台能力: V8可以在多种操作系统和处理器架构上运行,包括Windows、macOS、Linux以及x64、IA-32和ARM处理器。[1]
- 可嵌入性: V8引擎可以轻松地嵌入到任何C++应用程序中,为其提供JavaScript脚本执行能力。[1]
d8
简单来说,d8 是V8引擎自带的一个极简的命令行工具和调试外壳。当开发者编译V8源代码后,就会生成这个 d8 可执行文件。 它并不是一个独立的JavaScript引擎,而是一个轻量级的封装,让开发者可以不依赖于Chrome浏览器或Node.js等宿主环境,直接与V8引擎进行交互。
- 本地运行JavaScript文件:可以直接使用 d8 来执行一个 .js 文件。
- 调试V8引擎:开发者可以通过 d8 来测试对V8源码所做的修改。
- 性能分析与探索:配合不同的命令行标志,d8 可以输出V8在执行过程中的各种内部信息,如字节码、优化后的代码等。
- 漏洞利用研究:在V8漏洞分析和利用代码的编写和测试中,d8 是一个核心工具。
| 2 | 环境搭建
下载depot_tools
1 | git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git |
🧰 depot_tools 里有什么?
- gclient:用来同步、管理 Chromium/V8 那种多仓库巨型代码树
- fetch:一条命令把整套工程源码拉下来
- gn:Chromium/V8 的构建配置工具
- ninja:高效构建系统
- 一些自动化脚本(如 hooks、git 补丁命令等)
拉取源码
1 | mkdir ~/v8-dev |
创建工作目录 , 并拉取v8源码(相当耗时)
以上两个步骤只需要执行一次即可
题目环境搭建
每次题目会下发的重要附件为
- v8源码的git版本
- patch文件
在v8的工作目录中执行以下指令
1 | git reset --hard <hash> # 强制切到题目要求的版本 |
然后修改./out/release/args.gn中的配置文件
1 | # Set build arguments here. See `gn help buildargs`. |
编译d8
1 | autoninja -C out/release d8 |
这个过程同样耗时,会生成一个可以用于执行js脚本的v8引擎,同时加入了我们打入的补丁
此时我们便可以进行exp脚本的执行与调试 :
gdb调试
1 | gdb --args ./out/release/d8 --allow-natives-syntax ./exp.js |
直接运行
1 | ./out/release/d8 --allow-natives-syntax ./exp.js |
🌙 --allow-natives-syntax 是什么?为什么在 V8 利用里必开?
这个参数是 让你能在 JS 里调用 V8 的内部原生函数(natives)
也就是那些以 % 开头的内部调试 / 优化 / JIT 控制函数。
比如:
1 | %DebugPrint(obj) #打印出JavaScript对象在V8内存中的底层表示 |
这些函数在正常 JavaScript 环境中是 完全禁止的,
因为它们能:
- 打印堆对象内部结构
- 强行触发优化或反优化
- 访问内部 Map、ElementsKind
- 调整 inline cache 行为
- 触发断点或 crash
- 绕过 JS 层的一些安全检查
| 3 | 基础知识
标记指针 : 在js中 , 只存在两种类型 : 数字与对象
- 对于数字 , 将其左移1位 , 使其LSB标记为0
- 对于对象 , 由于堆块向0x10对其 , 直接将其LSB标记为1
压缩指针 : 由于所有对象都被v8存放在一个cage中,高32位相同,所以表示不用的指针只需要用低32位区分就可以了
对象内存分布
数组
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
30DebugPrint: 0x367c00042c65: [JSArray]
- map: 0x367c001cafa5 <Map[16](PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x367c001cb219 <JSArray[0]>
- elements: 0x367c001d3695 <FixedArray[4]> [PACKED_SMI_ELEMENTS (COW)]
- length: 4
- properties: 0x367c00000725 <FixedArray[0]>
- All own properties (excluding elements): {
0x367c00000d99: [String] in ReadOnlySpace: #length: 0x367c00025fd9 <AccessorInfo name= 0x367c00000d99 <String[6]: #length>, data= 0x367c00000069 <undefined>> (const accessor descriptor, attrs: [W__]), location: descriptor
}
- elements: 0x367c001d3695 <FixedArray[4]> {
0-3: 10
}
0x367c001cafa5: [Map] in OldSpace
- map: 0x367c001c0261 <MetaMap (0x367c001c02b1 <NativeContext[295]>)>
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- unused property fields: 0
- elements kind: PACKED_SMI_ELEMENTS
- enum length: invalid
- back pointer: 0x367c00000069 <undefined>
- prototype_validity cell: 0x367c00000a89 <Cell value= 1>
- instance descriptors #1: 0x367c001cb831 <DescriptorArray[1]>
- transitions #1: 0x367c001cb84d <TransitionArray[4]>
Transition array #1:
0x367c00000e5d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x367c001cb865 <Map[16](HOLEY_SMI_ELEMENTS)>
- prototype: 0x367c001cb219 <JSArray[0]>
- constructor: 0x367c001caf11 <JSFunction Array (sfi = 0x367c0002b3a9)>
- dependent code: 0x367c00000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0在内存中的分布可以发现, 由四个成员组成
1
2
3
4
5
6
7
8pwndbg> x/wx 0x367c00042c64
0x367c00042c64: 0x001cafa5(map)
pwndbg>
0x367c00042c68: 0x00000725(properties)
pwndbg>
0x367c00042c6c: 0x001d3695(elements)
pwndbg>
0x367c00042c70: 0x00000008(lenth)其中指针和小数都被
LSB标记1
在map中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
180x367c001cafa5: [Map] in OldSpace
- map: 0x367c001c0261 <MetaMap (0x367c001c02b1 <NativeContext[295]>)>
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- unused property fields: 0
- elements kind: PACKED_SMI_ELEMENTS
- enum length: invalid
- back pointer: 0x367c00000069 <undefined>
- prototype_validity cell: 0x367c00000a89 <Cell value= 1>
- instance descriptors #1: 0x367c001cb831 <DescriptorArray[1]>
- transitions #1: 0x367c001cb84d <TransitionArray[4]>
Transition array #1:
0x367c00000e5d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x367c001cb865 <Map[16](HOLEY_SMI_ELEMENTS)>
- prototype: 0x367c001cb219 <JSArray[0]>
- constructor: 0x367c001caf11 <JSFunction Array (sfi = 0x367c0002b3a9)>
- dependent code: 0x367c00000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0对于的虚拟内存
1
2
3
4
5
6
7
8
9
10
11
12Memory Address Value (Hex) Meaning (Compressed Pointer / Data)
-------------- ----------- -----------------------------------
0x367c001cafa4: 0x001c0261 --> Map (MetaMap)
0x367c001cafa8: 0x31040404 --> InstanceSize(16), Type(JS_ARRAY), ElementsKind
0x367c001cafac: 0x01000844 --> BitFields (Flags)
0x367c001cafb0: 0x0a0007ff --> BitFields (EnumLength = Invalid)
0x367c001cafb4: 0x001cb219 --> Prototype
0x367c001cafb8: 0x001caf11 --> Constructor
0x367c001cafbc: 0x001cb831 --> Instance Descriptors
0x367c001cafc0: 0x00000735 --> Dependent Code
0x367c001cafc4: 0x00000a89 --> Prototype Validity Cell
0x367c001cafc8: 0x001cb84d --> Transitions (or BackPointer)2
对于properties成员
1
2
3
4pwndbg> job 0x367c00000725
0x367c00000725: [FixedArray] in ReadOnlySpace
- map: 0x367c0000056d <Map(FIXED_ARRAY_TYPE)>
- length: 03
对于elements成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21pwndbg> x/wx 0x367c001d3695-1
0x367c001d3694: 0x0000065d
pwndbg>
0x367c001d3698: 0x00000008
pwndbg>
0x367c001d369c: 0x00000014
pwndbg>
0x367c001d36a0: 0x00000014
pwndbg>
0x367c001d36a4: 0x00000014
pwndbg>
0x367c001d36a8: 0x00000014
pwndbg>
0x367c001d36ac: 0x000010cd
pwndbg>
0x367c001d36b0: 0x00000000
pwndbg> job 0x367c001d3695
0x367c001d3695: [FixedArray] in OldSpace
- map: 0x367c0000065d <Map(FIXED_ARRAY_TYPE)>
- length: 4
0-3: 10map用标记指针,lenth和members用小数存储
可以看出来这是个存放了4个元素,每个元素都是0xa的整数数组
函数对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20pwndbg> job 0x64000042c49
0x64000042c49: [Function]
- map: 0x0640001c0a9d <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0640001c0951 <JSFunction (sfi = 0x64000141889)>
- elements: 0x064000000725 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x0640001d3691 <SharedFunctionInfo a>
- name: 0x064000002b09 <String[1]: #a>
- builtin: CompileLazy
- formal_parameter_count: 0
- kind: ArrowFunction
- context: 0x0640001d3721 <ScriptContext[4]>
- code: 0x064000033331 <Code BUILTIN CompileLazy>
- source code: () =>{return 12}
- properties: 0x064000000725 <FixedArray[0]>
- All own properties (excluding elements): {
0x64000000d99: [String] in ReadOnlySpace: #length: 0x064000026099 <AccessorInfo name= 0x064000000d99 <String[6]: #length>, data= 0x064000000069 <undefined>> (const accessor descriptor, attrs: [__C]), location: descriptor
0x64000000dc5: [String] in ReadOnlySpace: #name: 0x064000026079 <AccessorInfo name= 0x064000000dc5 <String[4]: #name>, data= 0x064000000069 <undefined>> (const accessor descriptor, attrs: [__C]), location: descriptor
}
- feedback vector: feedback metadata is not available in SFI3.Map相关
我们知道,obj有map成员,这个成员也是个obj,那么它自己有也map成员…那这种递归存在多少层呢?
1
2
3
4
5
6
7
8
9[ 普通对象 (JSArray) ]
| map 指针
v
[ 普通 Map (Map A) ]
| map 指针
v
[ MetaMap (Map of Maps) ] <--+
| map |
+-----------------------+ (指向自己)这个MetaMap存在所有map_obj中 , 而且它的压缩指针是写死的 , 我们不需要泄露任何地址便可以使用它
| 4 | 常用函数
由于双浮点数和v8优秀的适配性,我们不得不引入这些函数
1 | var f64 = new Float64Array(1); |
注意打远程时要把bug和peek注释掉
| 5 | pwn.college
入门的话还是从 pwn.college 开始比较好 , flyyy 师傅强力推荐 , Loora1N 师傅写的也很不错
Level 1
分析
完整的patch内容 , 只需要看有+的地方就可以了
1 | diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc |
主要为double_array增加了一个run方法
1 | +BUILTIN(ArrayRun) { |
可以使用c++的阅读方法大致理解这个方法的作用
- 检查数组是不是浮点数数组
- 检查数组的长度是小于0x1000的
- mmap出一片rwx区域 , 将数组复制到rwx区域
- 执行这片rwx区域
总的来说就是一个v8版本的ret2shellcode
因为d8本质上也是个elf文件, 如果我们在d8的虚拟空间中执行shellcode, 使用x86的shellcode就可以了
1 | zer00ne@zer00ne-VMware-Virtual-Platform:~/desktop/V8/src/v8/out/release$ file d8 |
shellcode是由字节形成的 , 所以我们需要将shellcode的字节码转化为浮点数
可以使用一个py脚本
1 | #!/usr/bin/env python3 |
直接在浮点数数组后对其使用run方法就可以rce了
EXP
1 | var shellcode = [ |
补 : pwncollege的rce目标是执行execve(“catflag”,0,0)
Level 2
分析
完整patch如下
1 | diff --git a/src/d8/d8.cc b/src/d8/d8.cc |
添加了GetAddressOf , ArbRead32 , ArbWrite32 三个函数
这是三个v8中重要的原语 , 获取obj地址/任意地址读写
GetAddressOf
1 | +void Shell::GetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& info) { |
先是检查参数是否是堆上obj
然后获取obj的地址并使用u32进行截断得到obj的压缩指针返回给
也就是获得obj的压缩指针
ArbRead32
1 | +void Shell::ArbRead32(const v8::FunctionCallbackInfo<v8::Value>& info) { |
检查参数是否是一个数字, 然后获取cage_base(也就是GetAddressOf截断的那部分数值)并加在参数上
最后将结果转化为u32指针并解引用 , 把内容返还给用户
也就是任意地址读, 参数为一个压缩指针
ArbWrite32
1 | +void Shell::ArbWrite32(const v8::FunctionCallbackInfo<v8::Value>& info) { |
需要两个参数, 都是数字
将一参加上cage_base后解引用 , 然后将二参赋值给刚刚的结果
也就是向任意压缩指针内写入任意u32数值
jit喷洒
从这里开始我们就没有现成的rwx区域了
我们需要欺骗d8让它将我们自己写的函数转化为rwx区域内的机器码
然后使用jop将简短的shellcode连接最终rce
jit喷洒 : https://www.matteomalvica.com/blog/2024/06/05/intro-v8-exploitation-maglev/#jit-spraying-shellcode

可以看到我们的第一个gadget位于rwx+0x6b的位置
1 | pwndbg> job 0x366b002c00a5 |
我们要用到的结构是
1 | shellcode ---+ |
那么我们的策略就是
- 创建jop-shellcode , 并多次循环达到加热效果
- GetAddressOf得到shellcode的压缩指针
- 读取shellocde的code成员
- 读取code中的
instruction_start(rwx)成员 , 并将其+0x6b后写回 - 执行shellcode即可rce
EXP
1 | const shellcode = () => {return [ |
Level 3
分析
完整的patch如下
1 | diff --git a/src/d8/d8.cc b/src/d8/d8.cc |
增加了两个函数GetAddressOf与GetFakeObject,其中GetAddressOf的实现同 level2
1 | +void Shell::GetFakeObject(const v8::FunctionCallbackInfo<v8::Value>& info) { |
参数为一个数字
将这个数字加上cage_base后无条件当作double_arrray_obj返回
obj结构
回顾一下
1 | pwndbg> x/wx 0x367c00042c64 |
map用来标记obj的形状 : 是什么类型的obj, 有什么内联/外联属性…
properties是存放外联属性, 比如obj.first , obj.second…
elements是元素指针, 指向存放obj[0],obj[1]…的真实地址
lenth是obj元素的个数
fakeobj
我们要伪造一个obj , 最低限度是需要伪造这四个成员
map很好得到, 我们可以随便读取一个double_array.map的压缩指针就可以直接拿来用
properties在这里没有必要用, 直接填0x725就可以了
element是重点 , 如果我们可以控制element为address-8(减去了element的头部数据: element的map和lenth成员) , 就可以使用obj[0]这样的形式向address写入任意内容(这里的address也是压缩指针, 但是写入的内容是八字节)
lenth随便填一个数就可以了, 一边任意地址写也不需要很长
堆风水
这四个成员可以写在一个double_array中 , 使用 GetAddressOf(array)+offset获得element(也就是fake_obj的地址)
风水可以这样堆

然后就可以写出aar/aaw原语, 后续的做法就和 Level 2 一样了
EXP
1 | //prepare |
这里fake_map直接从一个double_array上复制了, 因为压缩指针的关系这个偏移还是很稳定的
Level 4
分析
1 | diff --git a/BUILD.gn b/BUILD.gn |
只增加了一个函数setLength
1 | +namespace array { |
将二参写入this指针指向的obj的lenth成员
也就是无限长度的堆溢出
obj成员
我们能无限长度堆溢出 , 也就是可以篡改相邻obj的所有成员
1 | pwndbg> x/wx 0x367c00042c64 |
直接篡改element就可以达成如Level 3一样的aar/aaw
而GetAddressOf则可以靠堆溢出到一个obj_array的element的成员使用越界读直接获取
问题就被转化为Level 2了
堆风水

我们的理想情况是这些obj和它们的element成员都是相邻的
不过就算中间隔了几个幽灵obj也没关系, 反正堆溢出长度无限 ,gdb调试一下计算对偏移就好
EXP
1 | var f64 = new Float64Array(1); |
Level 5
分析
完整patch内容如下
1 | diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc |
提供了方法offByOne
当传入一个参数时 , 会越界8字节读取
当传入两个参数时, 会越界写入8字节
也就是方法的名字OffByOne , 在x64pwn中的offbyone可以用来修改chunk_size
在v8中 , OffByOne 可以覆盖的成员为map和properties
Map
我们说过, map是用来记录一个obj的类型的(是浮点数组还是对象数组?有什么外联属性?)
如果我们obj_array的map修改为double_array , 就可以以浮点数形式读取obj的压缩指针
这样就可以实现
GetAddressOf如果我们将double_array的map修改为obj_array的map , 就可以将浮点数的高/低32位作为压缩指针取出
也就是实现了
GetFakeObj
有了这两个原语, 就变回了Level 3
但是这样利用的前提就是array1.element和array2的紧邻的
1 | //jit |
对于这样构造的两个数组
1 | pwndbg> job 0xe0a00119bf9 |
可以发现和a.element紧邻的对象居然是一个幽灵obj (如果有知道为什么的师傅可以麻烦告诉我一下吗, 谢谢了otz)
我找到的方法是
1 | //jit |
这样a.element就紧贴着b, 可以修改b的map
1 | pwndbg> job 0x375a00119bf9 |
double_array_map和obj_array_map可以直接拿从gdb里来用(不太稳定就是了, 不过跑个七八次也能rce)
properties
本题的正解是修改外联属性指针
基础原理阅读这里 : https://v8.dev/blog/elements-kinds
当obj没有外联属性成员时 , properties为一个固定的压缩指针0x725
当拥有了成员后 , properties会来到obj的附近
也就是我们可以通过越界读properties , 加减偏移得到obj的地址
这之后修改properties就可以实现小范围内的任意地址写 , 如果修改a的lenth成员就变回了Level 4了
但是这个除了要求a.element和obj相邻, 而且要求obj拥有一个smi的外联属性
堆风水
这里的堆风水就比较简单了, 直接如下创建即可满足相邻的条件
1 | var a = [1.1]; |
EXP1(map)
1 | var f64 = new Float64Array(1); |
EXP2(properties)
1 | var f64 = new Float64Array(1); |
先更这么多 , 会尽快更新完的