Chrome-v8-入门
- 学习一下
Chrome-v8
,堆打累了,顺便给新生赛出一题简单的chrome-v8
的pwn
题 - 参考博客:Chorme-v8-入门学习 | A1ex’s Blog
- 参考博客:chrome v8 pwn 学习 (1) | CoLin’s BLOG
- 参考博客:如何构建和使用V8的调试工具d8 | zmx的前端日志
- 参考博客:从一道CTF题零基础学V8漏洞利用 - FreeBuf网络安全行业门户
浏览器相关知识
Chrome-v8
的pwn
属于浏览器的pwn
。接下来对浏览器做一个比较系统比较全面的了解Chrome
就是我们经常说的谷歌浏览器,本质上是一个网页浏览器,该浏览器是由谷歌公司开发的。而Chrome
里面的JavaScript
解释器被称为v8
,一开始主要做的pwn题就是面向v8
。- 接下来介绍一下主流的
JS
引擎。
引擎 | 开发者 | 主要应用 | 编译方式 | 备注 |
---|---|---|---|---|
V8 | Chrome、Node.js | JIT(TurboFan + lgnition) | 速度快,广泛用于服务器端 | |
SpiderMonkey | Mozilla | Firefox | JIT(IonMonkey) | 早期JS引擎,支持WebAssembly |
JavaScriptCore(JSC) | Apple | Safari、WebKit | JIT(Nitro) | 适用于macOS/iOS |
Chakra | Microsoft | 旧版Edge、IE | JIT | Edge现已经改用V8 |
Hermes | Meta | React Native | AOT | 专注移动端优化 |
QuickJS | Fabrice Bellard | 嵌入式设备 | 解释执行(无 JIT) | 轻量级,支持ES2020 |
- 而解释器这个的实现也就是使用底层语言去解释执行另一种语言,在这里是使用
C++
语言来解释JavaScript
语言
环境搭建
编译最新版本
-
(注:如果是在打比赛现学就请看编译之前版本)
-
这边需要手动编译源码,
chrome
里面的JavaScript
解释器被称为v8 -
我们先要下载一个源码,这个源码被称为
v8
,而v8
经过编译后的文件被称为d8
。根据编译的可选项,可以编译出debug
版本或者release
版本,一般两个版本都编译出来 -
还需要下载两个编译
v8
源码的工具depot_tools
、ninja
depot_tools
:是用来得到v8
源码(也就是使用这个工具去下载v8
源码,而不是直接使用git
去拉取源码)ninja
:用来编译v8
1 | git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git |
- 然后将这两个工具添加进环境变量,将
depot_tools
添加环境变量(注意添加环境变量的时候需要使用绝对路径)
1 | echo 'export PATH=$PATH:"/home/myheart/CTF/pwn/chrome_v8_pwn/depot_tools"' >> ~/.bashrc |
- 将
ninja
添加环境变量:在添加ninja为环境变量之前先要使用./configure.py
编译ninja
1 | cd ninja |
- 接下来就是使用
depot_tools
去下载源码
1 | fetch v8 |
- 然后准备依赖和编译
v8
gclient sync
:v8
项目的所有依赖项(注意旧版的源码可能会出现Python版本问题)tools/dev/v8gen.py x64.debug
:传递给v8gen.py
一个参数,表示生成x64
架构生成的调试版本
ninja -C out.gn/x64.debug
:编译项目
1 | gclient sync |
注意:在gclient sync
命令执行的时候可能会出现代理问题,执行成功后会在这个文件中生成文件。
注意:在ninja -C out.gn/x64.debug
这个命令就是开始编译了,编译时间比较久,很吃CPU
回退版本与加载补丁
- 在打比赛的时候,需要对相应的版本进行调试,这就导致了我们需要编译指定的版本。在比赛中我们得到的
v8
不一定是最新版本,我们之前编译的版本是V8 version 13.5.0
,假如我们比赛的时候v8
的版本为v8 version 13.3
版本,这时我们就要回退版本。 - 我们现在已经使用
fetch v8
,将远程的v8
源码版本为v8 version 13.5.0
给拉取到本地了。
- 如果我们修改了这个源码,我们就可以使用
git diff > my_changes.diff
,就会生成一个my_changes.diff
文件,这个文件之后有用。 - 如果我们想要回退到指定的
v8
版本,这时就需要输入如下命令,用于查看v8
的版本:
1 | git tag |
- 要切换版本就需要输入如下命令:
1 | git checkout tags/10.0.1 -b v8-10.0.1 |
动态调试
-
js
有两种动态调试的方式- 第一种使用的是
d8
内部的API
调试,但是调试并没有调试到寄存器
、内存
这么底层。 - 第二种就是使用
d8
配合gdb
进行调试,这种调试就会涉及到寄存器
和内存
- 第一种使用的是
-
对于第一种方式,具体介绍一下
V8
内部的API
:
API | 作用 |
---|---|
console.log(%HasFastProperties({})); | 检查对象是否使用 Fast Properties |
console.log(%GetOptimizationStatus(foo)); | 获取 foo 函数的优化状态 |
console.log(%HasInlinedFunctionCode(bar)); | 查看 bar 是否被内联 |
%CollectGarbage(); | 触发垃圾回收 |
console.log(%GetHiddenClass(obj)); | 获取对象的隐藏类信息 |
d8调试
- 我们先创建一个
test.js
文件
1 | function Foo(property_num,element_num) { |
- 然后使用
d8 test.js --allow-natives-syntax
即可进行调试,使用--allow-natives-syntax
就可以调用V8
的API
,这样就可以输出一些调试信息,这样就可以输出调试信息。
- 还可以这样进行调试,类似于交互式
Shell
效果调试。先输入命令../v8/out.gn/x64.debug/d8 --allow-natives-syntax
- 这样我们就可以进入
d8
的交互式界面
- 然后我们就可以进行一边编写代码一边调试
gdb调试
- 我们先创建一个
test.js
文件
1 | var num = 10; |
- 使用
gdb
调试,就要进行如下操作
1 | gdb ../v8/out.gn/x64.debug/d8 |
- 然后在
gdb
的内部输入命令,就可以调试了
1 | pwndbg> set args --allow-natives-syntax test2.js |
- 在
v8
源码中就可以v8
自带的调试JS
代码的gdb插件,我们先进入/path/to/v8/tools目录
,然后在这个目录下可以找到gdbinit
文件,这样就可以使用如下命令:
1 | cp gdbinit ~/.gdbinit_v8 |
- 之后编辑
~/.gdbinit
,添加如下文件:
1 | source ~/.gdbinit_v8 |
- 这样我们调试的时候就会出现对应的源码
- 这里也介绍一下
~/.gdbinit_v8
中的一些调试命令job
命令:用于可视化显示JavaScript
对象的内存结构。telescope
命令:查看内存数据
基础知识
JIT编译初步了解
-
这里先介绍一下一些基础知识。
Chrome V8
目前演变成如下解释过程: -
当我们编写一个
JS
代码,使用V8
去执行这个JS
代码:- 首先我们会通过
解析器
将V8
的JS
代码解析为抽象语法树。 - 然后会通过
解释器
对抽象语法树进行解释,将JS
代码转换为字节码,一边解释一边执行,并且解释器会记录特定代码片段的运行次数 - 当运行次数超过某个阈值,该段代码就会被记为热代码,并且将运行时的信息反馈给
优化编译器
优化编译器
根据反馈信息,优化并编译字节码,生成优化后的机器码,这样再次执行这个代码的时候就会执行相应的机器码。
- 首先我们会通过
-
上面的技术就被称为
JIT
(及时编译技术)- 其中
解析器
的源码在v8/src/parsing
- 解释器的源码在
v8/src/interpreter
- 优化编译器源码在
v8/src/compiler
或者v8/src/maglev
- 其中
JS常用的类
数组Array
关于Chrome-pwn的题型
-
Chrome-pwn
题型有两种第一种
:一般就是对v8
进行一些修改,人为制造出一个漏洞,然后给出.diff
文件第二种
:直接用CVE
出题
-
对于第二种就是看
CVE
漏洞在哪,或者一步一步去牢。接下来重点分析第一种题型 -
对于第一种题型,出题人先会对源码进行修改,然后编写
.diff
文件。而这个.diff
文件,是github主要用于显示代码变更,是Git版本控制系统的一部分。所以给出.diff
文件,我们就可以从.diff
文件中看出出题人所修改的地方,从而发现并利用漏洞。接下来就以2019StarCTF oob
这题给的.diff
为例子,对.diff
的一些进行分析 -
下面就是该题给的
.diff
文件,接下来逐句解释一下.diff
文件的每行代码的意思- 前四行是
Git Diff
格式的头部信息,用于描述对比的文件和修改信息diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
:这里就表明了对源码中/src/bootstrapper.cc
这个文件做了修改diff --git
是Git生成的差异比较标识a/src/bootstrapper.cc
是修改前的文件,其中a/
表示修改前的文件。b/src/bootstrapper.cc
是修改后的文件,b/
表示修改后的文件
index b027d36..ef1002f 100644
:表示哈希和文件权限模式,b027d36
修改前的哈希、ef1002f
修改后的哈希,100644
文件权限模式。这里的哈希仅仅只是被修改文件修改前后的哈希- 第三行和第四行
--- a/src/bootstrapper.cc
、+++ b/src/bootstrapper.cc
,表示修改前和修改后的文件路径
@@ -1668,6 +1668,8 @@
表示改文件改动的地方:表示改动的位置和行号,从这边我们就可以得知对源码修改了2
行- ``-1668,6
:
-表示旧代码,而
1668,6表示
1668行的位置往下
6`行; +1668,8
:-
表示新代码,而1668,8
表示1668
行的位置往下8
行;
- ``-1668,6
- 之后的
@@ -1668,6 +1668,8 @@
之后从第6
行到第14
行就是展现修改后具体的代码,而有两行开头有+
就表示是新添加的代码
- 前四行是
-
该
.diff
文件的剩余部分是对其它源码文件进行修改,就不做详细介绍了。
1 | diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc |
魔改V8
-
对于第一种类型的出题方式,就需要对
v8
的源码进行魔改,这里需要理解一下JavaScript
和C++
,这样会更容易理解,不过现在有AI
会方便许多。 -
对于
V8
的开发不熟悉的需要多花费一点时间去理解和尝试,这里就先从总体开始了解,如何修改V8
的源码编译好的d8
中新增加一个全局函数MyFunc()
。这个MyFunc()
函数的主要功能就是接收用户传入的参数(可以是字符串的数字
、浮点数
、整数
),返回的是传入的参数加上100
的结果,这个结果的数据类型默认为整型或者是浮点型
。 -
接下来就先给出我修改源码后的
.diff
文件,根据上面初步了解了.diff
文件,我们从.diff
文件这边可以了解到开发V8
流程的其中一小部分,也就是为V8
增加一个全局函数。 -
接下来是我修改源码后使用
git diff > my_changes.diff
,从这里可以了解到我们增加一个全局函数需要对源码的什么位置进行修改,接下来说明一下,具体修改了哪些文件的代码path/to/v8/BUILD.bazel
path/to/v8/BUILD.gn
path/to/v8/src/builtins/builtins-definitions.h
path/to/v8/src/init/bootstrapper.cc
path/to/v8/src/compiler/turbofan-typer.cc
- 注意:从
BUILD.bazel
和BUILD.gn
,我们可以了解到我们还新建了一个文件,该文件为path/to/v8/src/builtins/builtins-myfunc.cc
-
可以先尝试一下根据
.diff
文件不用git
命令自己尝试修改源码,然后编译,成功添加MyFunc()
这个全局函数。也可以根据后面的操作去尝试
1 | diff --git a/BUILD.bazel b/BUILD.bazel |
- 这里也给出参考博客中的
myfunc.cc
中的具体代码:
1 |
|
- 修改完编译后就可以得到我们自定义的一个全局函数
- 我们就先来介绍一下添加全局函数要修改的这些文件
相关文件
- 下面均为
V8 version 13.5.0
的源码
BUILD.bazel和BUILD.gn
- 用于定义如何构建、编译和链接 V8 项目的各个模块和文件。
- 也就是这些文件用于要构建、编译和链接的V8项目的源文件,所以我们在
src
中新建一个文件,这时我们就要将这个文件的路径写入,到这两个文件中,这样我们自定义的功能才能被编译
builtins-definitions.h
builtins-definitions.h
,文件:这个文件主要就是定义JavaScript
的内置类型、方法与函数,包括基本类型,比如:整数
、浮点数
、布尔值
、数组
、字符串
。- 例如下面的代码定义了一些数组的操作:比如
ArrayPop
,就是对数组的操作。
1 | // 源码:488行-499行 |
- 在源码中有
CPP
、TFJ
、TFS
这三个宏定义,还有ASM()
等宏定义。接下来简单介绍一下前三个宏定义。我们编写内置函数是使用CPP编写,编写其具体功能。 - 而
CPP
、TFJ
、TFS
这三个宏定义主要决定的是这个函数使用的是编译执行
还是解释执行
,他们是决定了内建函数如何在 V8 引擎内部执行(编译执行、解释执行、JIT 优化、辅助优化等) - 然后介绍一下这个文件中的相关参数:
-
CPP
宏定义中的相关参数:ArrayPop
:kDontAdaptArgumentsSentinel
:
-
TFJ
宏定义相关参数:ArrayPrototypePush
:kDontAdaptArgumentsSentinel
:
-
TFS
宏定义相关参数:CloneFastJSArray
:NeedsContext::kYes
:kSource
:
-
bootstrapper.cc
- 初始化和引导 V8 引擎的运行。它是 V8 启动过程中的核心部分之一,负责执行引擎的初始化和配置工作。
- 在这里我们就分析一个比较重要的方法:我们从源码可以看到这个方法从
2269
行到5051
行,占这个文件非常大一部分。
1 | void Genesis::InitializeGlobal(DirectHandle<JSGlobalObject> global_object, |
- 我们对数组一些操作的实现,会这这个方法中的里面进行初始化的配置,这样一些名称等都会被初始化,我们才能通过像这样
arr.pop()
关键字方法使用该功能,这边的{}
并不代表函数,只是将一些列操作或者方法给集合在一起,这样就更方便查找。
1 | { |
- 接下来介绍一下
SimpleInstallFunction
相关参数的具体含义:isolate_
:proto
:"findIndex"
:Builtin::kArrayPrototypeFindLastIndex
:kDontAdapt
:
turbofan-typer.cc
-
这里简单介绍一下
turbofan
,turbofan
是一个编译器,可以将字节码编译为CPU可以直接执行的机器码。 -
这个文件算是一个编译器优化,就添加如下形式就行:
1 | // 第1851行 |
- 接下来介绍一下具体这个函数的具体含义:
function.shared(t->broker()).builtin_id()
:Builtin::kMyFunc
:Type::Number()
:
添加MyFunc()全局函数
新建全局函数文件
- 在创建全局函数的时候我们会在这个文件目录下创建这样的文件
src/builtins/builtins-xxx.cc
,这里面编写的就是这个函数具体实现的功能 - 所以我们就先创建一个
src/builtins/builtins-myfunc.cc
文件,在文件中写入如下代码
1 |
|
添加源文件
- 然后我们在
BUILD.bazel
和BUILD.gn
这两个文件中添加我们新建的文件,如果是在开发中最好是按照顺序添加,这样会保证顺序不会乱
1 | //BUILD.bazel |
- 然后在这个文件
builtins-definitions.h
中添加
1 | CPP(MyFunc, kDontAdaptArgumentsSentinel) |
初始化函数
- 然后在这个文件中
bootstrapper.cc
,添加如下代码,注意这边需要在
1 | //注意需要再这个方法里面添加 |
设置函数优化
- 最后在这个文件中添加
turbofan-typer.cc
如下代码,需要再switch
内部中加入:
1 | switch (function.shared(t->broker()).builtin_id()) { |
- 这些都添加完之后就可以编译源码了,编译后就可以使用自定义的内置函数
MyFunc()