v8的知识结构
环境搭建
预先准备
- 各种依赖
1
apt-get install binutils python2.7 perl socat git build-essential gdb gdbserver
- gdb-peda
1
2
3git clone https://github.com/scwuaptx/peda.git ~/peda
git clone https://github.com/scwuaptx/Pwngdb.git ~/Pwngdb
cp ~/Pwngdb/.gdbinit ~/ - 环境设置
ubuntu16.04 x64
使用make的方式去编译v8(2016年当时)
- depot_tools准备
1
2$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
$ echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc - v8编译
1
2
3
4
5
6
7
8
9
10
11$ fetch v8 && cd v8$ git reset --hard 6ff5881b1def45b35384572f61327e42563a89c3
$ gclient sync
$ make x64.debug -j 8 # 如果这一步出现问题,就按照下面的方式重新编译
...
...
$ mv ./third_party/binutils/Linux_x64/Release/bin/ld.gold{,.old}
$ ln -s {/usr,./third_party/binutils/Linux_x64/Release}/bin/ld.gold
$ make x64.debug -j 8
# instead of using symbloic link, you can use the following line (thank ishita for helping
$ GYP_DEFINES="werror= linux_use_bundled_binutils=0 linux_use_bundled_gold=0" make x64.debug -j8
/path/to/根据你自己的环境替换。
- 启动
1
2$ ./out/x64.debug/d8
$ ./out/x64.debug/shell
使用ninja的方式去编译v8(2018现在)
- depot_tools准备
1
2$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
$ echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc
/path/to/根据你自己的环境替换。
- ninja准备
1
2
3$ git clone https://github.com/ninja-build/ninja.git
$ cd ninja && ./configure.py --bootstrap && cd ..
$ echo 'export PATH=$PATH:"/path/to/ninja"' >> ~/.bashrc - v8编译
1
2
3$ fetch v8 && cd v8&& gclient sync
$ tools/dev/v8gen.py x64.debug
$ ninja -C out.gn/x64.debug - 启动
1
2$ ./out/x64.debug/d8
$ ./out/x64.debug/shell
关于js的问题
js引擎
世界上有各类的js引擎,比较有名的有下面这几种。
浏览器 渲染引擎 js引擎
其他的js引擎介绍可以在这里找到:
https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e
js的pattern分为两类
- 由patch引入的更多pattern
了解如何利用由patch所引发的新漏洞,用js来编写exp - 过去的CVE
编译有漏洞的源码
在上一次和下一次commit之间找到正确的patch
有些情况下,PoC(attack code)会随着commit一起发布。
从修补程序中找出漏洞,并参考公开可用的PoC进行编写exp
今天我们将解决pattern 1,但是它和解决pattern 2要做的是相似的。
攻略方法
- 创建一个用于调试的js环境版本
如果有一个包含漏洞的patch,hit和build它。 - 分析patch以确定哪个patch适用于哪个进程
Full-Codegen, Crankshaft, TurboFan, Ignition, AST, IC, …
Full-Codegen and Crankshaft do not exist in V8 as of 2018 (see below) - 编写利用漏洞的js代码段
Think JavaScript code that causes patched parts to pass and causes bugs - 创建一个任意地址读/写的原语
主要使用ArrayBuffer和TypedArray - getshell
由于这是Pwn类别的问题,getshell是第一目标。
在JIT区域嵌入shell代码经常被使用
今天的主题1是如何阅读v8和给出一些编写js exp的建议。
今天的主题2是我将介绍getshell的通用技术。
目标
问题出现在当时的v8,已经和现在大不相同。
- 当时: Full-Codegen(JIT生成) + Crankshaft(优化1) + TurboFan(优化2)
- 现在: Ignition(JIT生成) + TurboFan(优化)
然而对这个bug的学习依然有用。
要完成这个目标,我们需要掌握以下知识:
- 编译器优化
触发optimize的条件 - GC(垃圾回收)
GC的实现和触发条件 - 了解V8的内存结构和类型表示
Integer value, double value, pointer, character string, special value, array, ArrayBuffer, etc.
供参考的exp:
https://gist.github.com/sroettger/d077d3907999aaa0f89d11d956b438ea
https://rzhou.org/~ricky/pctf2016/js_sandbox.js
什么是v8?
解释和执行js的引擎
由c++实现,parse js代码,构造AST
基于AST,JIT将其编译成汇编执行。
AST:a+b
在做v8 exp之前,首先我们需要知道v8的结构(你不需要知道所有的结构,因为它更新很快……)
但是你必须了解基本的概念。
在了解了这些之后,你就可以从Exploit的观点去深入
- 如何实现任意地址读写?
- 如何稳定的利用?
注意,我们主要讲解2016年4月的v8的结构,如果你想了解现在的v8,下面这些资料是十分有用的。
https://www.slideshare.net/ssuser6f246f/v8-javascript-engine-for
https://speakerdeck.com/brn/source-to-binary-journey-of-v8-javascript-engine
v8的编译器和优化
编译器的种类
要理解v8,其中最重要的组件就是编译器。
内部大概分成四个编译器
旧的baseline编译器:Full-Codegen
旧的优化编译器:Crankshaft
新的优化编译器:TurboFan
新的baseline编译器:Ignition
下面这些资料可以用于参考:
An overview of the TurboFan compiler
TurboFan: A new code generation architecture for V8
编译器的历史
最初,Full-Codegen直接生成和执行汇编语言
从AST直接生成汇编语言代码(JIT)相对较快,但是生成的汇编语言代码有很多冗杂部分,还有优化空间。
2010年,用于优化hot-code的Crankshaft被引入。
2015年,又引入了TurboFan,为了更好的适应新的javascript规范。
2017年,引入了生成中间语言(bytecode)的Ignition
2018年至今,Full-Codegen和Crankshaft已经被从v8中移除。
今天的问题~
2016年当时的latest
Hidden Class和Inline Caching也用作优化。
编译器和优化
- baseline编译器
1.Full-Codegen 重要度低—>对于理解这个exp的重要性
- 优化机制
2.Hidden Class 重要度中—>更准确的说,Hidden Class是一种有助于自身加速的机制,而Inline Caching是一种基于Hidden Class信息进行优化的机制。
3.Inline Caching 重要度中
- 优化编译器
4.Crankshaft 重要度低
5.TurboFan 重要度高
(顺便说一下)你应该知道的其他部分
api.cc、api.h:如果你想集成v8到你自己的程序,可以使用这里的API。
compiler/、compiler.cc、compiler.hh:编译起点是src/compiler.cc(从src/api.cc调用)
globals.h:常量和其他的定义
heap:GC
ic:Inline Caching
objects-ini.h、objects.cc、objects.h、type.cc、type.h: V8中使用的对象和类型的定义
Full-Codegen
Full-Codegen中存在的机制:
- 将AST转换为汇编语言
- 它是一个JIT编译器
- JIT编译器:一种在软件执行时进行编译并提高执行速度的机制
- 通过它,v8把要执行的JavaScript代码转换为机器语言
- 机器语言输出位于JIT区域(= RWX区域)
- 将EIP寄存器移到这个JIT区域并按原样继续执行
- 它尚未优化
- 它是一个JIT编译器
它是一种与当前问题没有太大关系,并且不存在于最新代码中的机制,因此省略了细节
优化
Full-Codegen生成的机器语言特性(半优化代码)
- 生成速度快,但执行速度慢(造成浪费)
因此,使用了根据需要来进行优化的机制
优化1:缓存使用情况
- 使用Hidden Class和Inline Caching
- 缓存要调用的地址和要引用的偏移量
- 使用Hidden Class和Inline Caching
优化2:重新编译为更高效的JIT代码
- 优化目标是在运行时确定的
- 在主线程中,正常执行机器语言
- 在另一个线程中,Runtime-Profiler测量使用状态
- Runtime-Profiler:在程序执行时测量和统计执行状态的机制
- 根据测量结果判断是否优化
- 使用Crankshaft进行优化编译
- 再次将源编译为机器语言,并将正在运行的机器语言替换掉
- 使用TurboFan优化编译
- 再次将源编译为机器语言,并将正在运行的机器语言替换掉
- 优化目标是在运行时确定的
Hiddern Class
- 每个property的值都以数组的形式进行管理。
- 通过偏移值访问数组里的property值
偏移值被分开管理
将属性名称和偏移量的依赖关系保留给另一个类(Map类)
这个Map类被称作Hidden Class
Map生成
①创建object的时刻(还没有property时),obj1内部指向C0
②创建一个没有property的map,type+offset管理,通常被称作C0
①当你添加obj1.x的时候,改变obj1内部指向C1
②通过向C0添加x的offset信息来创建新的map C1(map也有类型信息)
③在C0中添加转换条件
Map:C0
条件:当x加入时转移到C1
当访问obj1.x的值时,跟踪obj1所持有的指针,并引用C1以获取“x的偏移量为0”的信息。之后,通过访问obj 1的偏移量0处的值,可以高速的获得x的值。在C1内部,有必要寻找“x”,尽管我个人觉得它与哈希表似乎没有多大区别,但是这会让它更快。
①当你添加obj1.y的时候,obj1在内部更改为指向C2
②通过向C1添加y的offset信息来创建新的map C2
③在C1中添加转换条件
条件:添加y时,转换到C2
此时C0和C1不再使用,但它们不会被移除,因为它们可能在将来被重新使用。
对于具有完全相同属性的对象,如果你创建类似的对象,自然存在“x”和“y”,你可以使用创建的Map。
创建obj 2的时刻指向C0,但通过以与obj 1相同的方式按x和y的顺序添加属性,它遵循转换条件以完成C0->C1->C2
注意:如果属性添加顺序不同,即使具有相同名称的属性的对象也将具有不同的转换条件。 因此,最终创建的map也会变成不同的map,并且无法获得加速的好处。
有关详细信息,请参阅
http://richardartoul.github.io/jekyll/update/2015/04/26/hidden-classes.html
另外,如果property添加/删除次数增加太多,Hidden Class的管理会减慢。
因此,此时它不使用Hidden Class,而使用字典类型来管理
https://v8project.blogspot.jp/2017/08/fast-properties.html
property的管理方法
1.默认情况下,object的内部管理是通过array实现的
- In-Object property(这次的解说就是用这种方法)
2.当property增加到11个以上,使用外部的array来管理。
- Fast property
3.如果再进一步增加property,那么就用object外的dictory来管理
- slow/dict properties
- 它也被称为self-contained,因为没有使用map且使用外部的dictory保存所有的信息
- 尽管实体是一个FixedArray的数组,但它被用作如下所示的字典
参考资料:https://v8project.blogspot.jp/2017/08/fast-properties.html
我想在这里说的
- 一个object(javascript中)有一个指向Map的指针
- 正如我们稍后会看到的,object的前8个字节是一个指向Map的指针
- (JavaScript)object指向的map将根据状况改变
- 在漏洞利用中,这不是一个可靠的指针
- 相同的类型=Map的地址是相同的
- 比较map的地址即可确定类型是否一致
Inline Caching
参考资料:
- https://blog.ghaiklor.com/optimizations-tricks-in-v8-d284b6c8b183
- https://speakerdeck.com/brn/source-to-binary-journey-of-v8-javascript-engine
- https://www.slideshare.net/ssuser6f246f/v8-javascript-engine-for
- http://cs.au.dk/~jmi/VM/IC-V8.pdf
附注:其中部分我做了翻译,可以在这里找到
对于各个action,对类型进行缓存和优化的机制
这里所说的action可以表示下列任意一种
- 参照,代入(LoadIC, StoreIC) - 数组访问(KeyedLoadIC,KeyedStoreIC) - 二项演算 (BinaryOpIC)**最近的V8中被去掉了?** - 函数调用(CallIC) - 比较(CompareIC) - 布尔化(ToBooleanIC) **最近的V8中被去掉了?**
某些action的jit code被多次调用时需要考虑的
- 循环和函数多次传递相同的JIT code
在执行JIT代码时着眼于操作目标的类型(≒参数)
- JIT code很可能与上次通过时的操作类型相同
- 例如,以下JavaScript代码显示重复相同类型的操作
- 即使对应每个JIT代码,这个推断也应该保持不变
1
2
3
4
5
6
7for (var i=0;
i<10000; // 也许一个integer被从i载入,它是很可能去进行integer和integer的比较
i++) // 进行整数相加的可能性很大,i可能用一个整数去代替
{
var j = // j可能是被赋值为整数
100*i; // 也许一个整数会从i加载,它是很可能去进行整数相乘
}
- 即使对应每个JIT代码,这个推断也应该保持不变
JavaScript类型=map地址
- 从Hidden Class实现中可以看到,如果是相同类型,那么Map地址是相同的。
- 缓存类型意味着将map地址嵌入到JIT code中
- 例如,加载obj.x时的IC具有以下image
- 将x的偏移值一起缓存
- 当map匹配时,直接由缓存的x的偏移值得到property x的值,并返回。
1
2
3
4
5
6
7
8
9
10mov ecx, "x"
mov eax, obj
cmp [eax + kMapOffset], <被缓存的map地址>
jne MISS
mov eax, [eax + kPropertiesOffset]
mov eax, [eax + 被缓存的"x"(通过hidden class获得)的offset]
jmp DONE
MISS:
call IC_Miss// 抄近路失败,根据本来的code来获得"x"的offset
DONE:
实际上,由于有多个Map被注册的情况,所以需要进行函数化
箭头1:当第二个map被注册时
箭头2:当第三个map被注册时IC[存储了/持有]State
- UNINITIALIZED(0): 未初始化
- PREMONOMORPHIC(.): 只被执行一次的情况,还没进行IC
- MONOMORPHIC(1): IC注册一个的状态(快速)
- POLYMORPHIC(P): IC注册两个以上的状态(一般的快)
- MEGAMORPHIC(N): IC注册多个的状态
- GENERIC(G):IC已停止的状态
括号里是对于之后所说的debug输出(–trace-ic)的省略的标注
- 基本上是从上到下进行迁移的(0->.->1->P->N->G)
- 有些直接从0→1,如CallIC等
7. Inline Caching可通过-trace-ic进行确认
8. 使用–use-ic启用IC(默认),使用–no-use-ic禁用
关于inline caching
到目前为止说过的东西:
- 它与Hidden Class配对,对hign speed有很大的贡献
- 但是在exploit观点,只需缓存在JIT中的地址和偏移量即可
- 由于很难创建任意地址读/写的原语,因此与exploit的兼容性不是很好
- 但是,有些情况下应该部分简化IC检查(例如边界检查)
- 因此,在非IC下不会引发的漏洞可能会在IC下触发
优化
Crankshaft和TurboFan
两种编译器都可以用于优化
- 如何调用优化
- hot-code,也就是说,它是一个多次调用的函数或循环
- 优化由函数单元或循环单元执行
- 与主线程中并行执行,runtime-profiler在另一个线程中计数并作出判断
- 它也取决于函数和循环的代码段大小,但如果调用大约1000次或10000次左右,它将成为优化目标
1
2
3
4
5
6function f() {
return 1;// hot-code(都有成为hot-code的可能性)
}
for (var i=0; i<10000; i++) {
func(); // hot-code(都有成为hot-code的可能性)
}
- hot-code,也就是说,它是一个多次调用的函数或循环
- 被判断为hot-code的话
- turbofan/crankshart会在其他线程里再次编译(hot-code的)所属区域(的代码)
- 但是,hot-code不被最优化的情况也是存在的
- 通过替换机器语言的jmp目标地址(在主线程中执行)来切换以执行优化的机器语言,
- turbofan/crankshart会在其他线程里再次编译(hot-code的)所属区域(的代码)
1 | 将函数切换为优化代码时,可以将指针更新为函数对象的JIT区域 |
- 最优化编译器的使用条件(主要的)
- 未优化的语法不在函数/循环中使用
- debugger语句,eval语句,等等
- 如果有“use asm”语句,则使用TurboFan
- 只有TurboFan可以优化asm.js
- 如果有Crankshaft不支持的语法,则将使用TurboFan
- try catch,with等
- Crankshaft被默认使用
- 这是2016年的情况,现在Crankshaft被移除。
- 未优化的语法不在函数/循环中使用
Crankshaft
Crankshaft的特点
- Type-feeback
- 通过使用runtime-profiler收集的信息,确定类型来加快速度
- 最终生成的优化代码包含一个类型检查
- 当它不能确定类型时,它将返回到优化前的代码。
- Hydrogen (optimization by high-level intermediate representation (HIR))
- AST以SSA格式表示
- 各种优化,比如将loop内部不变的变量移到loop外。
- lithium(Optimization by Low-Level Intermediate Representation (LIR))
- 快速的寄存器分配算法
- 依赖CPU的优化,code生成
因为,它是一种与当前问题没有太大关系并且不存在于最新代码中的机制,因此省略了细节。
如果需要了解细节,可以参考这篇文章:http://nothingcosmos.github.io/V8Crankshaft/src/blog.html
TurboFan
参考资料:
https://github.com/v8/v8/wiki/TurboFan
https://speakerdeck.com/brn/source-to-binary-journey-of-v8-javascript-engine
https://docs.google.com/presentation/d/1H1lLsbclvzyOF3IUR05ZUaZcqDxo7_-8f4yJoxdMooU/edit#slide=id.p
TurboFan全览
下图显示了截至2018年TurboFan的整体情况
TurboFan的特征
- Graph Building
- 从AST创建一个JavaScript节点的graph
- JSAdd,JSCallFunction,JSLoadProperty,IfTrue,IfFalse等等
- 在making graphs优化
- 从AST创建一个JavaScript节点的graph
- Optimization
- graph的各种优化
- Code Generation
- 机器码生成
TurboFan优化
- src/compiler/pipeline.cc参考
- inline
内联函数调用 - trimming
未到达节点删除 - type
类型推断 - typed-lowering
根据类型将表达式和指令替换为更简单的处理 - loop-peeling
取出循环内的处理。 - loop-exit-elimination
删除Loop Exit - load-elimination
删除不必要的读取和检查 - simplified-lowering
用更具体的值来进行指令的简单转换 - generic-lowering
将JS前缀指令转换为更简单的调用和stub调用 - dead-code-elimination
删除无法访问的代码
- inline
文本框中的文字如下:
细节
出于某种原因,在名为GenerateCode()的函数中执行了对类型和graph的各种优化。
此外,尽管从CreateGraph()调用GenerateCode(),但这些函数原本应该是独立的。(在代码中还有三个独立的部分,job-> CreateGraph(),job-> OptimizeGraph(),job-> GenerateCode())
实际上,在V8的这个时间段中,每个phase都没有完全分离,因为优化和代码生成都是在CreateGraph()函数内部实现的。
Crankshaft/TurboFan检查
- Crankshaft/TurboFan能够被确认使用,通过–trace-opt
上面框:大约调用函数10000次
下面框:Crankshaft被使用
Crankshaft不能对包含with语句的函数进行优化,所以如果你在函数后添加add语句,TurboFan将会被调用。
TurboFan还可以通过 –turbo-stats查看优化列表和统计数据
Confirm results with d8 –print_code等
Crankshaft, TurboFan, Inline Caching related, etc. can be confirmed considerably
编译器调用的流程
被调用
- 参考samples/hello-world.cc
- 它只涵盖main()
有各种各样的东西,但最重要的是Compile()和Run()
- 它只涵盖main()
调用堆栈查看
如何调用Full-Codegen
- 描述了调用Compile函数时的转换
- 如果它是最新的源代码,它会跳转到ParseProgram()而不是ParseStatic(),但它不会有太大的改变,因为它最终会达到AST方向。
- 如果它是最新的源代码,它将跳转到GenerateUnoptimizedCode()而不是CompileBaselineCode(),并使用Ignition注册编译作业。
- 描述了调用Compile函数时的转换
调用Run()函数时的转换如下
CALL_GENERATED_CODE是一个宏,通过这个宏,在跳转到由Full-Codegen生成的机器语言(JIT)的阶段,优化编译器不会被调用如何调用Crankshaft / TurboFan
- Called after runtime-profiler decides whether optimization is possible
- Optimization availability determination is done automatically in another thread during Run ()
- 因此,使用V8作为库的程序员基本上不会主动调用执行优化的函数
- 当然,开发V8的程序员有可能自己故意调用一个根据选项执行优化的函数,但你在exploit的角度不用去考虑它。
- Optimization availability determination is done automatically in another thread during Run ()
- Called after runtime-profiler decides whether optimization is possible
在确定使用UseTurboFan()的优化编译器后,将创建Crankshaft / TurboFan作业。
之后,job->CreateGraph()实际触发优化编译
阅读V8的源码
在exploit中,您还需要阅读源代码.
源代码(samples/hello-world.cc),我们还介绍了用于阅读和调试源代码的重要概念
顺便说一下,ToLocalChecked()是一个no-NULL检查函数。
本节介绍以下内容
我只是总结了我不知道的概念
从exploit的角度来看,它们都不是那么重要,但是最好从源码上了解
- Handle/HandleScope
- Context
- Isolate
- Platform
- Interpreter
- blob
- ICU
- third_party
- tools
参考资料:https://github.com/v8/v8/wiki/Embedder's-Guide
Handle/HandleScope
- Handle
- 要启用GC跟踪,指针包装类型
- 为了对应任何类型的指针,请使用C++模板
- 在源代码中,所有Object都使用此Handle
类型进行管理 - GC有可能移动Object的位置
即使GC移动该Object,由于handle不移动,所以没有不一致
- 常用Handle
- Handle
- Abstract class
- Local
- Temporary Handle, 保留在stack上
- 使用后面将介绍的HandleScope进行生命周期管理
- MaybeLocal
- 它与Local
相同,但在使用前检查它是否为空
- 它与Local
- Persistent
- 一个persistent Handle,保留在heap上
- 代码编写器使用Persistent::Reset()管理生命周期
- Handle
- 要启用GC跟踪,指针包装类型
- HandleScope
- handle总结
- Temporary Handle such as Local
, MaybeLocal - 在声明HandleScope时,块中的每个handle都会自动关联
- Temporary Handle such as Local
- 当HandleScope超出范围时,它会处理释放handle
- 返回函数时,结束{}时,等
- 用所有使用的handle来描述释放处理是低效的
- 使用HandleScope的析构函数,GC负责实际的释放处理
- 参考以下的文件
- include/v8.h,src/handles.h
- handle总结
Context
- 在一个V8实例中创建多个执行环境的机制
- 您可以在一个线程中同时运行彼此独立的JavaScript代码
- 每个Context对象都有一个全局的Root-Object
左边:每个context都有一个Root-Object,并且彼此独立(在本例中,context是嵌套的,但Root-Object正确切换)
右边:总之,它实现了环境的切换。 我们希望分别通过window,iframe和extended script来独立保护环境。所谓的origin也是在Context中定义的,并且从一个Context到另一个Context的访问不能被默认完成。
Isolate
- Instance of V8 itself
- context是在同一个instance中实现不同的执行环境
- 当你想运行自己的多个实例时使用Isolate
- 为了适应多线程
Platform
- It seems to define the operating environment (it seems)
- 线程相关
- 决定后台线程和前台线程
- 管理线程池
- 任务队列管理
- 事件追踪
- 线程相关
我没有很好理解,因为没有真实的信息
参考
- include/v8-platform.h
- src/libplatform/default-platform.cc
Interpreter
- In V8, two Interpreters are prepared
- d8
- 构建src/d8.cc,可以用参数指定各种选项
- debug-shell d8 in the sense of V8
- 如果你不用一个文件作为参数去运行它,它将作为interactive interpreter运行。
- 当文件被指定为参数时,它将被解析为JavaScript并执行
- shell
- 构建samples/shell.cc
- 主要操作与d8相同,但功能减少且轻量级
- 它可以用于对CTF中的V8的jsp问题进行调试
- d8
blob
- 关于snapshot文件
- V8在初始化时在内部生成内置JavaScript代码
- 尽管这些代码可以每次使用时进行编译,但是效率不高。
- 所以在编译阶段预先准备好他们。
- 它只需要在启动时读取,因此初始化变得更快
- 在构建V8时,他们一起生成
- snapshot可以在程序内部/外部进行
- 当snapshot文件被放置在外部的时候,就是blob
- There are two of natives_blob.bin and snapshot_blob.bin
- V8在初始化时在内部生成内置JavaScript代码
ICU
- International Components for Unicode
- 也就是说,与Unicode有关的外部库
- 参考下面的链接:
- https://github.com/v8/v8/wiki/i18n-support
- src/icu_util.cc
third_party
- 和v8捆绑的工具(=被用来构建等)
- icu, binutils, llvm, etc.
- icu, binutils, llvm, etc.
- 它可以根据需要进行更换
- 在使用Ubuntu 16.04进行构建时,由于其下属的ld.gold报错,因此通过系统链接程序的符号链接(/usr/bin/ld.gold)
- ld.gold是Google在2012年左右制作的ld的高速版本
- 使用GYP_DEFINES,您也可以替换环境变量
- 在使用Ubuntu 16.04进行构建时,由于其下属的ld.gold报错,因此通过系统链接程序的符号链接(/usr/bin/ld.gold)
tools
- GDB扩展命令已准备好用于调试目的
- gdb-peda$ source tools/gdbinit
- gdb-peda$ source tools/gdb-v8-support.py
- 如果你这样做,你可以增加gdb命令
- 也许你可以使用Ok
- tools/ Because there are various other things under his eyes
- 但是,由于我们的案例是在2016年,因此需要进行一些修改
- gdbinit
- 由于出现与命名空间相关的错误,因此使用“’修补它即可(???)
- gdb-v8-support.py
- 由于python 3语法错误出现,所以可以在打印语句OK中放入括号
- 由于python 3语法错误出现,所以可以在打印语句OK中放入括号
- 此外,该文件的内容在最新的V8中已更改
- 我还没有确认这一点。
- gdbinit
- For example, v8print and job commands display HeapObject cleanly
- 对象的结构将在后面描述
- 由于map有各种标志,最好在这里查看
- 例如,[0xdeadbee,0xdeadbeef,“hoge”]
显示FixedArray(减去0x14,因为它指向FixedArray的第一个偏移量) - 我试图显示FixedArray的Map的内容
- 实际上,我不必费心使用v8print命令,但我觉得直接调用内部函数可以调用__gdb_print_v8_object(address),如果我传入一个奇怪的地址,我无法通过SEGV恢复它,所以我直接看内存,它会更安全。
其他
- 其他的细节
- Macro-intensive use
- 如果对源代码进行grep找不到定义,则可能有宏
- 寻找宏的定义是很好的(在源码中的某处#define 〜)
- In particular, many macros based on the token concatenation operator (##) are used
#define HOGE(name,type) hoge_name_##type();
- 当我找不到它时,一般会有很多模式
__asm __(“int 3”)
不能嵌入到IC或机器语言生成系统的功能中- 做一个blob作为构建过程的一部分
- 目前这些代码似乎被使用,并且在很多情况下构建失败
- namespace
- i是v8::internal的别名
- namespace i = v8::internal;(src/globals.h)
- i是v8::internal的别名
- Changes will be made immediately
- 你现在看的源码和最新的可能大有不同。
- 如果您认为将其与最新版本进行比较很有趣
- https://cs.chromium.org/chromium/src/v8/
- 你现在看的源码和最新的可能大有不同。
- 以下是非常有用的
https://github.com/danbev/learning-v8/blob/master/README.md
- Macro-intensive use
关于V8的GC
参考资料
https://github.com/thlorenz/v8-perf/blob/master/gc.md
https://github.com/joyeecheung/v8-gc-talk
https://www.youtube.com/watch?v=DSBLAG2IvsY
垃圾收集器(GC)
另一个重要组件是GC
- 一种在V8中单独管理JavaScript对象(称为HeapObject)的机制
- 如何检测废弃的对象并自动释放它们
- 使用与Linux heap不同的区域
- 一种在V8中单独管理JavaScript对象(称为HeapObject)的机制
GC区域
- 除heap以外,还有多个由mmap存放的区域
- V8内部使用的各种HeapObject被保留在这个区域
heap区域
- 如果不是js object(=不应该由GC管理),用c++语言管理普通object
- 虽然是一个JavaScript object,但有些例外,存放在heap而不是HeapObject(例如JSArrayBuffer的BackingStore)
GC中有2种generation(= regions with different management methods)
- Young Generation
- Old Generation
根据GC中object的生存时间,它被分为两类generation ,Young/Old (and New/Old described later)
这不是关于V8版本之间的区别
除此之外,还有一些区域不属于任何一个generation
为了方便起见,它被写为Other,但是其实是关于Large Object Space
在源代码中,有些地方包含Old generation的large object space的描述,但基本上认为它们是不同的东西
Young Generation
New Space
- 新创建的object被保留在这里,并且受到GC管理
- Almost all objects
- code object,map object和large object被排除在外
- 除了看源码之外还有其他的东西,但是从exploit的视角看不重要
The GC algorithm is Cheney’s algorithm
为了使用这种算法,它进一步分为两个区域
- ToSpace
- FromSpace
这个GC在源码里被称为Scavenge
- 我会解读它,因为它很重要。
- 这一次我会解读2016年的GC,但最近这个算法发生了变化,与并行化兼容
参考资料:https://v8project.blogspot.jp/2017/11/
Cheney’s algorithm
- Each object is reserved from the beginning of ToSpace
- 当memory exhaustion(空间用罄)时候,GC被调用
- 主线程的操作(Javascript执行的线程)被暂停
- Switch To Space and From Space
- Actually dealing with pointer swap (flip)
- 仅将living object复制到To Space
- 首先,确保live (= alive), copy one starting object。
- 有各种各样的root objects (such as global objects, built-in objects, local objects within the scope of living, etc. )
有各种各样的说法,详情请参阅heap/heap.cc的IterateRoots()。 - 从Old side可以访问的object (由后面讲解的Write Barrier mechanism管理), etc.
- 有各种各样的root objects (such as global objects, built-in objects, local objects within the scope of living, etc. )
- 顺次复制living的object。
- 还有一些要复制到old generation的object
正如我们后面将会看到的那样,两次在young generation的GC中幸存下来的对象,被复制到old generation的空间而不是复制到ToSpace
- 还有一些要复制到old generation的object
- 完成后,重新选择root并重复复制。
- 首先,确保live (= alive), copy one starting object。
- 再次分配之前未分配完成的obj-e
- From Space中还有garbage存在,但是因为我们不会再次使用它们,所以无所谓。
- 之后,每次GC发生时,都会重复上面这一系列的流程
- Each object is reserved from the beginning of ToSpace
Old Generation
old space
- long-lived objects存放的区域
- New Space中, 在两次GC之后存活下来的object
- 更多细节参考Heap::ShouldBePromoted()
- old space发生GC的频率比new space少(取决于使用过程)
- 如果一个object被移动到old space,该object不会受到GC更改layout的影响
- 如果一个object被移动到old space,该object不会受到GC更改layout的影响
- New Space中, 在两次GC之后存活下来的object
- long-lived objects存放的区域
code space
- 仅适用于JIT的code object
- 由于code object是RWX,因此它从一开始就保留在此区域中
- 由于它是JIT代码,因此不仅要读取(R)写入(W),还要执行(X),因此memory permissions与其他的地方不同。
- 由于code object是RWX,因此它从一开始就保留在此区域中
- 仅适用于JIT的code object
Map Space
- 仅Map object
- 出于GC效率的考虑,Map object从一开始就位于此区域
- 出于GC效率的考虑,Map object从一开始就位于此区域
- 仅Map object
old generation的GC算法是Mark-Sweep-Compact
- 除New Space区域以外的所有算法
- 由于它与Exploit无关,因此省略了详细信息
Other
- Large Object Space
- 保留600KB或更大的object的区域
- 它由mmap直接分配
- 如果有多个存放区域,请使用链表进行管理
- 它不在GC中移动
- 保留600KB或更大的object的区域
Write Barrier
https://v8project.blogspot.jp/2016/04/jank-busters-part-two-orinoco.html
对应于从old/large一侧,指向young一侧的object的场合
- 当你在Young进行GC时,Young side的object会移动
- 因此old/large一侧所持有的指针就变成无效的了
- 虽然有必要对old/large一侧中的指针进行修正,但是希望在young的gc过程中避免对old/large进行扫描
- 对这样的指针事先使用store buffer + remembered set进行管理
- 在young进行gc时,利用这些的信息对old/large一侧的指针进行处理
- 在young进行gc时,利用这些的信息对old/large一侧的指针进行处理
- 当你在Young进行GC时,Young side的object会移动
从Exploit的角度来看
- In case of exploit repeating memory allocation/release
- When GC runs at Young Generation, memory layout collapses
- It is more stable to intentionally activate the GC in advance and move the object as much as possible to Old Generation
- In order to cause GC, it is sufficient to secure a lot of memory (= non-large) in detail
- When GC runs at Young Generation, memory layout collapses
- In case of heap BOF type exploit
- Each object does not have metadata for GC (concrete example will be described later)
- Meta data such as size and prev_size in the malloc chunk are not particularly used for GC applications
- Since the reserved JS object is used like a structure, there are various information inside the JS object, but there are no headers in the JS object itself, and each JS object is secured consecutively
- In other words, the technique of the metadata destroying system which is well-known in the Linux heap basically does not exist on the GC
- Each object does not have metadata for GC (concrete example will be described later)
v8对象模型
参考资料
http://steps.dodgson.org/bn/2008/09/07/
V8的object
- Object
- v8自己创建的各种各样的类
- 和GC合作
- 针对C++类结构制作一个触发器
- 例如,V8的object没有成员变量
- 它既没有虚函数,也没有构造函数/析构函数
- 细节参考src/objects.h,src/objects-inl.h等
- v8自己创建的各种各样的类
- Exploit
Object和Tagged Value
Object
- Object
- 它由以下两种类型组成
- Smi(Small Integer)
- 整数值
- 整数由带符号的31位范围表示(在32位环境的情况下)
- 整数由带符号的32位范围表示(在64位环境的情况下)
- 整数值
- HeapObject
- 除整数值之外的其他类
- 也适用于不能在Smi范围内表达的整数
- Double value and hold at the end of the pointer (= HeapNumber object)
- 始终有一个指向Map的指针
- 也适用于不能在Smi范围内表达的整数
- 由于HeapObject基本上由GC管理,因此它位于GC区域(它不存放在堆区域)
- 除整数值之外的其他类
Smi
如果一个成员的值是一个整数,那么存储它的速度会更快
这就是使用Smi的原因(我认为这是原因)
对于使用指针来创建一个整数对象(B)的实现,如图
指针必须被追踪一次,内存访问两次(慢)对于省略了指向整数对象(B)的指针并且整数直接存放在内部的实现,如图
指针应该指向的整数值在V8中被称为Smi
没有必要跟随指针,内存访问执行一次(快速)
Tagged Values
Tagged Values
- 同时表示指向Smi和HeapObject的指针的机制
- 但是,不可能区分它们是整数值还是指针
- 低1位(LSB)是一个标志
- 但是,不可能区分它们是整数值还是指针
- 同时表示指向Smi和HeapObject的指针的机制
Smi(=Object)
- 如果LSB为0,则可以通过右移1位获得原始值
- 如果LSB为0,则可以通过右移32位获得原始值
- 如果LSB为0,则可以通过右移1位获得原始值
指向HeapObject的指针
- 32位
- 64位
由于GC上的chunk在32位环境中的4字节对齐,64位环境中是8字节对齐的,因此LSB始终为0。也就是说,当它存储在内存中时,将其LSB设置为1即代表指针。
- 32位
HeapNumber
- 对象的值为double
- 数字表达式不能在Smi范围内表达
- 继承Object, HeapObject
- 内存结构如下所示(64位环境下)
V8的HeapObject完全没有任何成员变量,它完全由偏移量独立表示。
为了画起来方便,我们将其视为变量名称,并如右图所示表示(对于后续幻灯片也是如此)
- 内存结构如下所示(64位环境下)
- 实际演示
- Smi值(0xdeadbee)和double值(0xdeadbeef,由于它大于0x7fffffff,非Smi)存放在数组中
搜索
- Smi值(0xdeadbee)和double值(0xdeadbeef,由于它大于0x7fffffff,非Smi)存放在数组中
- 由于0xdeadbee是Smi,因此可以通过在内存中搜索,来查找存储在数组中的值。(换句话说,直接看数组里对应的值就行了)
- 0xdeadbee(Smi)之后的元素应该是0xdeadbeef(HeapNumber)
0x41ebd5b7dde00000是0xdeadbeef的double值表示 - HeapNumber对象和其他对象连续,保证没有任何间隙
PropertyCell
- Object meaning variable
- 继承Object,HeapObject
- 内存结构如下(在64位环境的情况下)
- 内存结构如下(在64位环境的情况下)
- 继承Object,HeapObject
- 实际演示
- 存放Smi值(0xdeadbee)
- 使用0xdeadbee搜索此PropertyCell的在内存中的位置
- 尝试覆盖变量的值
- 由于0xdeadbee是Smi,它的值可以通过在内存中搜索找到。
- 更改为字符串
- 如果你像以前一样检查相同的地址,则kValueOffset所保持的值会更改
- 指向的地址是一个表示“hoge”的String对象(String对象的细节将在后面描述)
String
- 保存字符串的对象
- 继承Object, HeapObject, Name
- 内存结构如下(在64位环境的情况下)
- 内存结构如下(在64位环境的情况下)
- 继承Object, HeapObject, Name
- 实际演示
- 存放在数组中的字符串“hoge”,“fuga”
- 用0xdeadbee查找这个数组的内存位置
- 由于0xdeadbee是Smi,因此你可以发现这个在数组中的值,通过在内存中搜索。基于此,确定数组的内存位置
- 跟在0xdeadbee(Smi)之后的元素应该是一个String对象,如“hoge”或“fuga”
- “hoge”和“fuga”连续存放,没有缺口。
Oddball
表示特殊值的对象,例如true,false,undefined
- 继承object,HeapObject
- 内存结构如下(在64位环境的情况下)
- 继承object,HeapObject
实际演示
- 确保true,false等在数组中
- 用0xdeadbee查找这个数组的内存位置
- 0xdeadbee(Smi)的下一个元素应该是一个Oddball对象,如true或false
- 确保true,false等在数组中
顺便说一下,种类的定义就是这样
JSObject
参考链接
https://v8project.blogspot.jp/2017/08/fast-properties.html
JSObject
- 表示JavaScript对象的对象
- 继承自Object,HeapObject,JSReceiver
- 对于想要了解element的properties和description的人,请参阅上面给出的参考链接
- properties
- It is called NamedProperties and manages elements accessed by name. The entity is FixedArray
- Management when an object has a property (like a.x)
- element
- It is called IndexedProperties and manages elements accessed by index. The entity is FixedArray
- Management when an object has an index (like a[0])
JSFunction
表示JavaScript function的对象
- 继承Object, HeapObject, JSReceiver, JSObject
- 内存结构如下(在64位环境的情况下)
- 内存结构如下(在64位环境的情况下)
- 继承Object, HeapObject, JSReceiver, JSObject
实际演示
存放function f()在数组中
用0xdeadbee查找这个数组的内存位置
kCodeEntryOffset is a pointer to the JIT code (RWX area), many strategies to realize arbitrary code execution by writing shellcode before this
JSArray
- Object holding a JavaScript array
- 继承Object, HeapObject, JSReceiver, JSObject
- 内存结构如下(在64位环境的情况下)
- 内存结构如下(在64位环境的情况下)
- 继承Object, HeapObject, JSReceiver, JSObject
- 实际演练
- 由于0xdeadbee是Smi,因此可以通过在内存中搜索来查找存储在数组中的值。 基于此,查找数组的内存位置(因为有一些候选项,请小心)
- 如果增加数组的元素,它将自动扩大
- 第三个和第四个元素被添加到只有两个元素的数组中
- 第三个和第四个元素被添加到只有两个元素的数组中
- 当元素的数量增加时,它会扩展长度(FixedArray存放到另一个位置,并且kElementsOffset所保存的指针改变)
- 顺便说一下,有很多0x186e00404369代表TheHoleObject的地址(Oddball的kind = 2意思是void)
- 由于0xdeadbee是Smi,因此可以通过在内存中搜索来查找存储在数组中的值。 基于此,查找数组的内存位置(因为有一些候选项,请小心)
- 注意
- 在一个数组中,有时会存储一个double值的情况
- 它是一个非Smi范围,但它被存储为一个double值而不是HeapNumber地址
- Smi范围,但存储为double值而不是Smi表示
- Perhaps, it seems to be to decide the type of the entire array and speed up it
- 在一个数组中,有时会存储一个double值的情况
JSArrayBuffer
ArrayBuffer and TypedArray
- Originally ArrayBuffer
- 一个可以直接从JavaScript访问内存的特殊数组
- 但是,ArrayBuffer仅准备一个内存缓冲区
- BackingStore——可以使用TypedArray指定的类型读取和写入该区域,例如作为原始数据数组访问的8位或32位内存
- 为了实际访问,有必要一起使用TypedArray或DataView
- 使用例子 (TypedArray版本)
- 创建方法1,仅指定长度,初始化为零
t_arr = new Uint8Array(128) //ArrayBuffer被创建在内部 - 创建方法2,使用特定值初始化
t_arr = new Uint8Array([4,3,2,1,0]) //ArrayBuffer被创建在内部 - 创建方法3,事先构建缓冲区并使用它
arr_buf = new ArrayBuffer(8);
t_arr1 = new Uint16Array(arr_buf); //创建一个Uint16数组
t_arr2 = new Uint16Array(arr_buf, 0, 4); //或者,您也可以指定数组的开始和结束位置
- 创建方法1,仅指定长度,初始化为零
- ArrayBuffer可以在不同的TypedArray之间共享
- 它也可以用于double和int的类型转换
- 类型转换的意义在于改变字节序列的解释,而不是转换
- 就像C语言的Union
- BackingStore——可以使用TypedArray指定的类型读取和写入该区域,例如作为原始数据数组访问的8位或32位内存
- ①预先准备ArrayBuffer
var ab = new ArrayBuffer(0x100); - ②向ArrayBuffer中写入一个Float64的值
var t64 = new Float64Array(ab);
t64[0] = 6.953328187651540e-310;//字节序列是0x00007fffdeadbeef
- ③从ArrayBuffer读取两个Uint32
var t32 = new Uint32Array(ab);
k = [t32[1],t32[0]]
k=[0x00007fff,0xdeadbeef] - 它也可以用于double和int的类型转换
- 一个可以直接从JavaScript访问内存的特殊数组
JSArrayBuffer
- 持有ArrayBuffer的对象
- 继承Object,HeapObject,JSReceiver,JSObject
- 内存结构如下(在64位环境的情况下)
- 内存结构如下(在64位环境的情况下)
- 继承Object,HeapObject,JSReceiver,JSObject
- 实际演示
- 存放TypedArray
- 使用长度0x13370搜索ArrayBuffer的内存位置
- 在V8中,对象通常被存放在由GC管理的mapped区域,然而BackingStore是一个不被GC管理的区域,并且被存放在heap中(在图中,可以看到malloc块有prev_size和size成员)
此外,由于它不是由GC管理的HeapObject,因此指向BackingStore的指针不是Tagged Value(末尾不能为1) - 虽然在ArrayBuffer中描述了大小,但如果将此值重写为较大的值,则可以允许读取和写入的长度,超出BackingStore数组的范围。
- 同样,如果您可以重写BackingStore指针,则可以读取和写入任意内存地址,这些是在exploit中常用的方法。
Numerical conversion tool (It is original work)
- 在开始JavaScript利用之前
- 频繁转换unsigned long long <-> double
- 预先制作转换工具很好
- 我制作了以下工具(我不会使用float,但只是为了确保)
- 源代码看起来像这样
实践
- Plaid CTF 2016 -Pwnables 666pts –js_sandbox
- 下载链接
https://goo.gl/Se5HTy - 问题
Can you get a flag off of this site(http://js.pwning.xxx:27251/)?
If so, I’ve got 666 points with your name on it!
- 下载链接
攻略方法
- 模式1和2,策略如下
- 创建一个用于调试的js环境版本
如果有一个包含漏洞的patch,hit和build它。 - 分析patch以确定哪个patch适用于哪个进程
Full-Codegen, Crankshaft, TurboFan, Ignition, AST, IC, …
Full-Codegen and Crankshaft do not exist in V8 as of 2018 (see below) - 编写利用漏洞的js代码段
Think JavaScript code that causes patched parts to pass and causes bugs - 创建一个任意地址读/写的原语
主要使用ArrayBuffer和TypedArray - getshell
由于这是Pwn类别的问题,getshell是第一目标。
在JIT区域嵌入shell代码经常被使用
初步调查
- Plaid CTF 2016-Pwnable 666pts -js_sandbox
- 访问
- 访问
http://js.pwning.xxx:27251/files/problem.patch
https://developers.google.com/v8/
http://js.pwning.xxx:27251/file
提供了5个文件作为capture的必要文件。
- libc.so.6
当前的libc(ubuntu14.04) - natives_blob.bin
shell操作所需的文件 - problem.patch
有漏洞的v8 patch - shell
构建samples/shell.cc用于本地测试 - snapshot_blob.bin
shell操作所需的文件
shell文件
shell (and *.blob)用作debug的目的
distribution file应该包含与problem服务器相同的源文件
- V8解释和执行的是JavaScript
- 因此,exploit也只需要用JavaScript编写
- 对于相同的输入,V8应该表现相同
- 如果你可以通过shell来exp v8,你也可以通过Web服务器exp v8
- 如果你可以通过shell来exp v8,你也可以通过Web服务器exp v8
patch分析
- 启用PIE,FORTIFY_SOURCE,stack canary
- PIE,FULL-RELRO启用
- 使read和load函数无效
print,read,load,quit和version等功能都是在sample/shell.cc中专门定义的,但是经过patch后,read和load时会失效 - 从源代码路径,TurboFan相关补丁和猜测
lhs:左手侧(左侧)rhs:右手侧(右侧)?
此外,因为函数名称是JSAddRanger,所以可以预料可能是与加法有关的漏洞(?)
Identification of calling conditions
- 假设被patch的函数是有漏洞的
- 我想调用这个函数
- 如果我们不能调用这个函数,我们不能触发漏洞
- 怎么调用它?
- 我没有任何提示,我只有一个函数名称
- 猜测这个函数所在的文件或目录有什么功能
- 从该函数的调用回溯,找到调用路径。
- 最终目标是找到读取和调用每个函数的条件。
- 我想调用这个函数
尝试google搜索函数名称
触及此功能的评论页尤为重要
确认内置漏洞的文件名
- src/compiler/typer.cc
- 由于它是src/compiler/下的一个文件,因此它被认为与TurboFan有关
- 由于它是src/compiler/下的一个文件,因此它被认为与TurboFan有关
确定来源
- 检查修补程序周围的代码
- 看看JSAddRanger()
- 看看JSAddRanger()
确定调用路径
到目前为止,做一个总结
寻找调用路径
- 使用gdb回溯调用栈是最快的方法。
编写测试代码
- 准备Javascript code去调用TurboFan
- 通过代码调用函数10000次,由于我想调用TurboFan,所以不要忘记使用with语句
- 通过代码调用函数10000次,由于我想调用TurboFan,所以不要忘记使用with语句
- 准备Javascript code去调用TurboFan
使用gdb运行
- 如果某些条件不满足,JSAddRanger()将不会调用…
- In C ++ that does not reach JSAddRanger () in test code, that is, it does not reach the JSAddRanger () in the test code, you can not set a breakpoint unless you specify not only the function name but also the namespace and type to which it belongs. So using nm, it searches for mangled function names and specifies a breakpoint (if PIE is invalid, it is OK even if you set a breakpoint at the found address)
- 实际上,您无法在测试代码中访问JSAddRanger()
调用JSAddRanger()
图示第一行是函数原型,第二行是函数定义,看看第三行。- 从JSAddTyper()调用。
- 除非满足某些条件,否则不会调用JSAddRanger()
- 除非满足某些条件,否则不会调用JSAddRanger()
- 从JSAddTyper()调用。
同时检查JSAddTyper()的调用者
- 在函数定义中只找到一个地方
- 由于无法用简单的grep找到它,因此很有可能涉及宏。
- 追溯更多来源是很麻烦的
- 由于无法用简单的grep找到它,因此很有可能涉及宏。
- 在JSAddTyper()上放置一个断点并查看是否能在测试代码中断下来
- 如果没有断下,努力尝试,阅读源码,进一步回溯
- 使用gdb运行
- 在JSAddTyper()上放置一个断点,发现停在断点处
- 在JSAddTyper()上放置一个断点,发现停在断点处
- 在函数定义中只找到一个地方
在停止后进行回溯(查看调用栈)
- 从这个函数名,可以确认JSAddRanger()和JSAddTyper()与优化编译器(TurboFan)相关
- 由于它是TyperPhase::Run,证明它是一个关于TurboFan优化中“Typer”阶段的函数。
另外,如果TurboFan进行了优化编译运行,就会发现JSAddTyper()在测试代码中被调用。
- 从这个函数名,可以确认JSAddRanger()和JSAddTyper()与优化编译器(TurboFan)相关
您可以在TyperPhase中看到TurboFan的功能
到目前为止做一个总结
JSAddRanger()调用条件探索
- lhs-> IsRange()&& rhs-> IsRange()
- 首先,代码中出现的Range是什么?
- 为了知道这一点,我们需要更多地了解TurboFan的优化机制
Typer和Range调查
- 了解V8的优化(特别是Typer和Range相关)
- GitHub中有很多链接,从上往下依次阅读
- 和Typer有关的材料
- TurboFan概述
- Typer收集类型信息并在其他地方完成优化
- TurboFan’s JIT Design
- https://docs.google.com/presentation/d/1sOEF4MlF7LeO7uq-uThJSulJlTh--wgLeaVibsbb3tc/edit#slide=id.p
- 当JavaScript代码中的变量或值不确定时,表示可以读取的范围的变量或值可以作为范围读取
- 据此,JSAddRanger()在添加带有范围信息的不定变量时,会派生出一个新的范围
- https://docs.google.com/presentation/d/1sOEF4MlF7LeO7uq-uThJSulJlTh--wgLeaVibsbb3tc/edit#slide=id.p
- Fast arithmetic for dynamic languages
https://docs.google.com/presentation/d/1wZVIqJMODGFYggueQySdiA3tUYuHNMcyp_PndgXsO1Y/edit#slide=id.p
虽然它没有直接解释范围,但根据该图,似乎在使用AND操作时变成范围(可能是“%”,这也在MOD操作中)
确定调用条件
- 编写测试代码
- 添加AND操作并在gdb下执行
- 在此之后,在断点处停止
- 添加AND操作并在gdb下执行
- 调用路径总结
漏洞理解
补丁周围的代码
从操作实例理解
用make x64.debug -j 8重新编译
- 在“环境设置”描述的步骤中,我已经预先构建了它。
尝试有趣的测试case
- 下面的JavaScript代码展示了有趣的结果
- 如果打了有漏洞的补丁,你会在AND操作之后得到新的范围信息,而这个范围信息是错误的
- 但是,由于该信息仅用作类型提示,因此它不会对选择Word32的类型造成影响,因为它可以用32位表示[0,24]和[0,16]。在这个例子里,f函数正常应该返回24作为结果
- 下面的JavaScript代码展示了有趣的结果
解法
https://gist.github.com/sroettger/d077d3907999aaa0f89d11d956b438ea
漏洞利用
- 通过假的range信息,你能做什么?
- 这里的目标是创建任意地址读/写的原语
- 具体来说,就是伪造一个很大的array的length
- 许多JavaScript引擎的漏洞利用都是使用这种方法。
- OOB-RW (Out-Of-Bounds-Read/Write access)
- 如果在array之外读写数据,并劫持JSArrayBuffer的BackingStore的指针,就获胜。
- 如何用OOB-RW创建任意地址读/写的原语
- 内存排布如下
- 如果可以模拟长度,就可以通过越界访问来更改JSArrayBuffer的BackingStore的指针
- 内存排布如下
- 通过range来实现我们的目的(改大array的length)
- 正确的尝试
- 首先用范围信息创建一个值
- 使用这个值来创建一个array
- 测试代码看起来像这样
- 变量sz具有范围信息
- 变量sz具有范围信息
- 修改源代码为有漏洞的版本
- make x64.debug -j 8再次编译
- 正确的尝试
- 在gdb下运行,检查内存
- 搜索0xdeadbee将命中3次,其中这个是一个数组(= FixedArray)
- 通过将标记值1加到FixedArray的地址上,然后进行搜索
- 找到的地址应该是JSArray
通过check我知道我能伪造FixedArray的长度,因为如果不打patch,上下的值应该都是0x18 - 这意味着什么?
- 我们打算创建一个长度0x18元素的数组,但实际上只准备了长度0x10元素的数组
- 我们打算创建一个长度0x18元素的数组,但实际上只准备了长度0x10元素的数组
虽然它与基本形式稍有不同,但如果您仅信任此长度(0x18),并将这个长度的值写入FixedArray,则您能够读取和写入的数据,将会超出原本仅有0x10的FixedArray
实际上想用只有0x8个字节的数据来覆盖backing store ptr是十分困难的,我们将在后面介绍更好的方法。
参考解说:typed-lowering
为什么会出现这种情况?
- 使用range信息的部分在哪里?
- 在typer phase旁边的typed-lowering phase
- 在typer phase旁边的typed-lowering phase
- 使用range信息的部分在哪里?
typed-lowering
- 它似乎使用type information来optimize graph
- Follow until array association comes out
用printf调试确认
编写exp
- 你现在在做什么?
- Array length伪造
- 我们将会考虑编写exp
- 流程如下
- 伪造Array length
- 伪造ArrayBuffer
- Identify function object
- 在JIT区域嵌入shellcode
- 流程如下
伪造Array length
- 顺次排列两个FixedArray(其长度通过range漏洞伪造)
由于这些FixedArrays的范围是伪造的,因此它们可以在数组之外进行读写操作. - 每个double值存储在Element [0]中,并且数组类型被识别为double类型(即FixedArray被转换为FixedDoubleArray)
- 在FixedArray变成FixedDoubleArrayed之后,内存的布局会发生变化并且变的很麻烦,所以让我们先提前认识它是一个只有double类型元素的FixedDoubleArray。
这也确保了FixedDoubleArray 1和2连接
- 在FixedArray变成FixedDoubleArrayed之后,内存的布局会发生变化并且变的很麻烦,所以让我们先提前认识它是一个只有double类型元素的FixedDoubleArray。
- 使用OOB-W漏洞更新FixedDoubleArray1中的FixedDoubleArray 2的kLengthOffset
如果连续排列,FixedDoubleArray 1的元素[17]应该是kLengthOffset。
通过此更新,FixedDoubleArray 2可以越界读取和写入了。
其实我们不能直接访问Element [17]。
- 状态
在JSArray中,长度信息首先存储在两个地方。JSArray::kLengthOffset(= 24)和FixedArray::kLengthOffset(=16) - 原因
当存储到一个数组时,它使用JSArray::kLengthOffset和FixedArray::kLengthOffset来进行索引的范围判断(可以确认的是,如果你通过gdb上的rwatch设置了一个内存访问断点,它将以任一方式停止)
17超出了FixedArray::kLengthOffset的范围,所以我们扩展了数组的长度并在其他地方预留了一个新数组。这不会成为OOB-RW。 - 解决方法
According to Write-up, it seems to be enough to substitute twice in the for statement as follows称为KeyedStore IC的IC对应于数组元素中的存储,KeyedStoreIC是状态直接转换为(0)->(1)的IC,IC在第二次访问时已处于活动状态.1
2for(var i=0;i<24;i+=17)
Element[i]=value;
当通过触发IC来处理时,它不会与JSArray::kLengthOffset或FixedArray::kLengthOffset进行比较(However, this is not exactly confirmed here, and only JSArray :: kLengthOffset comparison may be done on the code embedded in JIT)
伪造ArrayBuffer
将ArrayBuffer放置在FixedDoubleArray 1,2之后
由FixedDoubleArray 2的漏洞搜索kByteLengthOffset和kBackingStoreOffset的偏移量
- 它每次从FixedArray2的元素[16]中读取一个元素,并根据是否包含假定值进行搜索。 您可以在kByteLengthOffset(= ArrayBuffer拥有的BackingStore的长度)中放入一个特征值并搜索它(如果向此添加1,kBackingStoreOffset的偏移量也是已知的)
- ab_off表示从FixedArray 2(代码中的arr [1])中看到的作为kByteLengthOffset位置的元素索引,
- 它每次从FixedArray2的元素[16]中读取一个元素,并根据是否包含假定值进行搜索。 您可以在kByteLengthOffset(= ArrayBuffer拥有的BackingStore的长度)中放入一个特征值并搜索它(如果向此添加1,kBackingStoreOffset的偏移量也是已知的)
更新来kByteLengthOffset和kBackingStoreOffset的偏移量
- 如果设置元素[kByteLengthOffset_offset] = 0xfffff0,
元素[kByteLengthOffset_offset + 1] = rw_addr。
请记住在步骤2-2中获得的偏移量,并且在ArrayBuffer读取和写入rw_addr,也就是说,它几乎变成了任何内存读/写的原语。 - 通过重写kBackingStoreOffset的值,您可以读取和写入任意地址,因此在利用时可以将其作为RW原语的函数来实现。
另外,关于i2_to_d()的描述被省略了,它是一个函数,可以使用ArrayBuffer将Uint32转换为double。
- 如果设置元素[kByteLengthOffset_offset] = 0xfffff0,
Identify function object
ArrayBuffer后面跟着FixedArray 3,它有一个适当的函数对象作为元素
选择7作为特征值没有特别的意义。 只要选择你喜欢的长度,然后尝试。从FixedDoubleArray 2中,使用漏洞探索FixedArray 3的Element的偏移量,获取函数f的对象地址。
- 如果一个值连续出现kLengthOffset次(如图中是7),则可以推断出它可能是包含函数f的对象地址的元素的偏移量。
- 如果一个值连续出现kLengthOffset次(如图中是7),则可以推断出它可能是包含函数f的对象地址的元素的偏移量。
在JIT区域嵌入shellcode
- 通过将函数f的对象地址放入kBackingStoreOffset并读取第七个元素,获取kCodeEntryOffset中的JIT地址
- 将该地址放在kBackingStoreOffset中,用shellcode覆盖JIT区域,然后调用函数f()
exploit
- I am rewriting a part of the author’s write-up
- Execute exploit code with V8 sample-shell
- 你可以确认/bin/sh启动
- 虽然以上是针对自建shell执行的,即使对distributed shell执行了exploit代码,也应该启动/bin/sh
- Then replace it with the shellcode to be backconnected, then try exploit to the production is OK
- 你可以确认/bin/sh启动
- 除了本文中讨论的解决方案之外,PPP还使用GC实现了另外一种writeup
- https://rzhou.org/~ricky/pctf2016/js_sandbox.js
- 但是,从range伪造,到进行数组的OOB-RW的思路是相同的
Bonus Other JavaScript issues and links
V8
- [SECUINSIDE 2014] Yet another Javascript jail - 300
https://gist.github.com/anonymous/b2340ac8429f4c5d186c
http://ddaa.logdown.com/posts/203080-secuinside-2014-pwn-300-yet-another-javascript-jail
https://gist.github.com/potetisensei/9a41a1848da4021c29e5 - [34c3 CTF 2017] v9
https://github.com/saelo/v9
https://gist.github.com/itsZN/9ae6417129c6658130a898cdaba8d76c
SpiderMonkey
- [33c3 CTF 2016] pwn600 -feuerfuchs
https://github.com/saelo/feuerfuchs
https://bruce30262.github.io/2017/12/15/Learning-browser-exploitation-via-33C3-CTF-feuerfuchs-challenge/ - [CodeGate 2017] JS World -pwnable500
https://gist.github.com/akiym/966b3d24146edb685b8a08edee551de2
Chakra
- [PlaidCTF 2017] Chakrazy -Pwnable600
https://lokalhost.pl/ctf/pcft2017_chakra.js
https://bruce30262.github.io/2017/12/26/Chakrazy-exploiting-type-confusion-bug-in-ChakraCore/
https://gist.github.com/eboda/18a3d26cb18f8ded28c899cbd61aeaba
WebKit-JSC
- [BkP CTF 2016] qwn2own -pwn10
https://github.com/kitctf/writeups/blob/master/bkp2016/qwn2own/index.html
https://rzhou.org/~ricky/bkpctf2016/qwn2own/test.js
http://vulph.com/2016/03/05/Boston-Key-Party-writeups.html
https://github.com/acama/ctf/tree/master/bkpctf2016/qwn2own
http://lokalhost.pl/ctf/bkp2016/
http://blog.frizn.fr/bkpctf-2016/qwn2own-bkpctf16
v8 exploit study
https://cansecwest.com/slides/2017/CSW2017_QidanHe-GengmingLiu_Pwning_Nexus_of_Every_Pixel.pdf
https://docs.google.com/document/d/1tHElG04AJR5OR2Ex-m_Jsmc8S5fAbRB3s4RmTG_PFnw/edit
http://researchcenter.paloaltonetworks.com/2014/12/google-chrome-exploitation-case-study/
https://bugs.chromium.org/p/chromium/issues/detail?id=386988
https://bugs.chromium.org/p/chromium/issues/detail?id=416449
https://bugs.chromium.org/p/chromium/issues/detail?id=468933 (pwn2own 2015)
https://bugs.chromium.org/p/chromium/issues/detail?id=595834 (pwn2own 2016)
https://bugs.chromium.org/p/chromium/issues/detail?id=659474
https://bugs.chromium.org/p/chromium/issues/detail?id=776677
https://arthurgerkis.gitbooks.io/it-sec-catalog/content/browser_exploitation.html
SpiderMonkey exploit study
- http://www.geeknik.net/6zawqzuuu
- https://phoenhex.re/2017-06-21/firefox-structuredclone-refleak
- https://grehack.fr/data/2017/slides/GreHack17_Get_the_Spidermonkey_off_your_back.pdf
- https://github.com/rh0dev/slides/blob/master/OffensiveCon2018_From_Assembly_to_JavaScript_and_back.pdf
WebKit-JSC exploit study
- https://cansecwest.com/slides/2015/Liang_CanSecWest2015.pdf
- http://www.powerofcommunity.net/poc2016/keen.pdf
- http://www.phrack.org/papers/attacking_javascript_engines.html
- https://scarybeastsecurity.blogspot.jp/2017/05/ode-to-use-after-free-one-vulnerable.html
- https://phoenhex.re/2017-06-09/pwn2own-diskarbitrationd-privesc
- https://phoenhex.re/2017-07-06/pwn2own-sandbox-escape