从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 # 强制切到题目要求的版本 |
在内存中的分布可以发现, 由四个成员组成
1 | pwndbg> x/wx 0x367c00042c64 |
其中指针和小数都被LSB标记
1
在map中
1 | 0x367c001cafa5: [Map] in OldSpace |
对于的虚拟内存
1 | Memory Address Value (Hex) Meaning (Compressed Pointer / Data) |
2
对于properties成员
1 | pwndbg> job 0x367c00000725 |
3
对于elements成员
1 | pwndbg> x/wx 0x367c001d3695-1 |
map用标记指针,lenth和members用小数存储
可以看出来这是个存放了4个元素,每个元素都是0xa的整数数组
1 | pwndbg> job 0x64000042c49 |
3.Map相关
我们知道,obj有map成员,这个成员也是个obj,那么它自己有也map成员…那这种递归存在多少层呢?
1 | [ 普通对象 (JSArray) ] |
注意打远程时要把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 |
添加了GetAddressOf , ArbRead32 , ArbWrite32 三个函数
这是三个v8中重要的原语 , 获取obj地址/任意地址读写
GetAddressOf
1 | +void Shell::GetAddressOf(const v8::FunctionCallbackInfo& info) { |
先是检查参数是否是堆上obj
然后获取obj的地址并使用u32进行截断得到obj的压缩指针返回给
也就是获得obj的压缩指针
ArbRead32
1 | +void Shell::ArbRead32(const v8::FunctionCallbackInfo& info) { |
检查参数是否是一个数字, 然后获取cage_base(也就是GetAddressOf截断的那部分数值)并加在参数上
最后将结果转化为u32指针并解引用 , 把内容返还给用户
也就是任意地址读, 参数为一个压缩指针
ArbWrite32
1 | +void Shell::ArbWrite32(const v8::FunctionCallbackInfo& 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 [ |
增加了两个函数GetAddressOf与GetFakeObject,其中GetAddressOf的实现同 level2
1 | +void Shell::GetFakeObject(const v8::FunctionCallbackInfo& 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 |
只增加了一个函数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); |
提供了方法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); |
先更这么多 , 会尽快更新完的