v8 exploit

v8的知识结构

plaid ctf 2016 - js_sandbox

环境搭建

  1. 各种依赖

    1
    apt-get install binutils python2.7 perl socat git build-essential gdb gdbserver
  2. gdb-peda

    1
    2
    3
    git clone https://github.com/scwuaptx/peda.git ~/peda
    git clone https://github.com/scwuaptx/Pwngdb.git ~/Pwngdb
    cp ~/Pwngdb/.gdbinit ~/
  3. 环境设置
    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分为两类

  1. 由patch引入的更多pattern
    了解如何利用由patch所引发的新漏洞,用js来编写exp
  2. 过去的CVE
    编译有漏洞的源码
    在上一次和下一次commit之间找到正确的patch
    有些情况下,PoC(attack code)会随着commit一起发布。
    从修补程序中找出漏洞,并参考公开可用的PoC进行编写exp

今天我们将解决pattern 1,但是它和解决pattern 2要做的是相似的。

攻略方法

无论是pattern 1还是pattern 2,攻略如下:

  1. 创建一个用于调试的js环境版本
    找到patch,打patch
  2. 分析patch,确定补丁用于v8的哪个部分:
    Full-Codegen,Crankshaft,TurboFan,Ignition,AST,IC,…
    Full-Codegen,Crankshaft在2018的v8里已经弃用
  3. 编写一个能够绕过patch触发漏洞的poc
  4. 实现任意地址读写等……
    大部分时候用到ArrayBuffer和TypedArray
  5. getshell
    对于pwn类别的问题,通常在JIT区域构建shellcode代码,用于getshell

今天的主题1是如何阅读v8和给出一些编写js exp的建议。
今天的主题2是我将介绍getshell的通用技术。

目标


问题出现在当时的v8,已经和现在大不相同。

  • 当时: Full-Codegen(JIT生成) + Crankshaft(优化1) + TurboFan(优化2)
  • 现在: Ignition(JIT生成) + TurboFan(优化)

然而对这个bug的学习依然有用。

要完成这个目标,我们需要掌握以下知识:

  1. 编译器优化
    触发optimize的条件
  2. GC(垃圾回收)
    GC的实现和触发条件
  3. 了解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被引入。

Full-codegen->Suboptimization code
Full-codegen—>optimize->Crankshaft->optimisation code—>Deoptimize(If the optimized code is not appropriate, restore it)->Suboptimization code

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区域并按原样继续执行
    • 它尚未优化

它是一种与当前问题没有太大关系,并且不存在于最新代码中的机制,因此省略了细节

优化

  • Full-Codegen生成的机器语言特性(半优化代码)
    • 生成速度快,但执行速度慢(造成浪费)
  • 因此,使用了根据需要来进行优化的机制

    • 优化1:缓存使用情况

      • 使用Hidden Class和Inline Caching
        • 缓存要调用的地址和要引用的偏移量
    • 优化2:重新编译为更高效的JIT代码

      • 优化目标是在运行时确定的
        • 在主线程中,正常执行机器语言
        • 在另一个线程中,Runtime-Profiler测量使用状态
          • Runtime-Profiler:在程序执行时测量和统计执行状态的机制
          • 根据测量结果判断是否优化
      • 使用Crankshaft进行优化编译
        • 再次将源编译为机器语言,并将正在运行的机器语言替换掉
      • 使用TurboFan优化编译
        • 再次将源编译为机器语言,并将正在运行的机器语言替换掉


Hiddern Class

  1. 每个property的值都以数组的形式进行管理。
  2. 通过偏移值访问数组里的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(这次的解说就是用这种方法)
  1. 当property增加到11个以上,使用外部的array来管理。
  • Fast property
  1. 如果再进一步增加property,那么就用object外的dictory来管理
  • Slow / dict properties
    • 它也被称为self-contained,因为没有使用map且使用外部的dictory保存所有的信息
    • 尽管实体是一个FixedArray的数组,但它被用作如下所示的字典

参考资料:https://v8project.blogspot.jp/2017/08/fast-properties.html

我想在这里说的

  1. 一个object(javascript中)有一个指向Map的指针
    • 正如我们稍后会看到的,object的前8个字节是一个指向Map的指针
  2. (JavaScript)object指向的map将根据状况改变
    • 在漏洞利用中,这不是一个可靠的指针
  3. 相同的类型=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,对类型进行缓存和优化的机制

  1. 这里所说的action可以表示下列任意一种
    - 参照,代入(LoadIC, StoreIC)
    - 数组访问(KeyedLoadIC,KeyedStoreIC)
    - 二项演算 (BinaryOpIC)**最近的V8中被去掉了?**
    - 函数调用(CallIC)
    - 比较(CompareIC)
    - 布尔化(ToBooleanIC) **最近的V8中被去掉了?**
    
  2. 某些action的jit code被多次调用时需要考虑的
    • 循环和函数多次传递相同的JIT code
  3. 在执行JIT代码时着眼于操作目标的类型(≒参数)
    • JIT code很可能与上次通过时的操作类型相同
    • 例如,以下JavaScript代码显示重复相同类型的操作
      • 即使对应每个JIT代码,这个推断也应该保持不变
        1
        2
        3
        4
        5
        6
        7
        for (var i=0;
        i<10000; // 也许一个integer被从i载入,它是很可能去进行integer和integer的比较
        i++) // 进行整数相加的可能性很大,i可能用一个整数去代替
        {
        var j = // j可能是被赋值为整数
        100*i; // 也许一个整数会从i加载,它是很可能去进行整数相乘
        }

  1. 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
        10
            mov  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:
  2. 实际上,由于有多个Map被注册的情况,所以需要进行函数化

    箭头1:当第二个map被注册时
    箭头2:当第三个map被注册时

  3. 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等
  4. Inline Caching可通过-trace-ic进行确认
  5. 使用–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
        6
        function f() {
        return 1;// hot-code(都有成为hot-code的可能性)
        }
        for (var i=0; i<10000; i++) {
        func(); // hot-code(都有成为hot-code的可能性)
        }
  • 被判断为hot-code的话

    • turbofan/crankshart会在其他线程里再次编译(hot-code的)所属区域(的代码)
      • 但是,hot-code不被最优化的情况也是存在的
    • 通过替换机器语言的jmp目标地址(在主线程中执行)来切换以执行优化的机器语言,
1
2
3
4
5
6
7
8
9
将函数切换为优化代码时,可以将指针更新为函数对象的JIT区域
function f() {
return 1;// hot-code(都有成为hot-code的可能性)
}
for (var i=0; i<10000; i++) {
func(); // hot-code(都有成为hot-code的可能性)
}
在循环中,当从中间切换到优化代码时,可以将jmp目标切换到循环的顶部,但仍然存在名为OSR(On-Stack-Replacement)的切换方法。
但这里省略,参考这篇文章:https://wingolog.org/archives/2011/06/20/on-stack-replacement-in-v8)
  • 最优化编译器的使用条件(主要的)
    1. 未优化的语法不在函数/循环中使用
      • debugger语句,eval语句,等等
    2. 如果有“use asm”语句,则使用TurboFan
      • 只有TurboFan可以优化asm.js
    3. 如果有Crankshaft不支持的语法,则将使用TurboFan
      • try catch,with等
    4. 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的整体情况

在图中,bytecode在输入中,但在2016年应该不是这样。

TurboFan的特征

  • Graph Building
    • 从AST创建一个JavaScript节点的graph
      • SAdd,JSCallFunction,JSLoadProperty,IfTrue,IfFalse等等
    • 在making graphs优化
  • Optimization
    • graph的各种优化
  • Code Generation
    • 机器码生成

TurboFan优化

  • src/compiler/pipeline.cc参考
    • inline
      • inline function call
    • trimming
      • 删除无法访问的节点
    • typer
      • Type information gathering and type optimization
    • typed-lowering
      • Convert expressions and instructions based on type
    • loop-peeling
      • 将循环内的处理放在循环之外
    • simplified-lowering
      • Simplify with more specific instructions
    • branch-elimination
      • Delete unnecessary branches
    • generic-lowering
      • Convert JS prefixed instructions to simple calls and stub calls


文本框中的文字如下:
细节
出于某种原因,在名为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()

      调用堆栈查看

  • 如何调用Full-Codegen

    • 描述了调用Compile函数时的转换
    1. 如果它是最新的源代码,它会跳转到ParseProgram()而不是ParseStatic(),但它不会有太大的改变,因为它最终会达到AST方向。
    2. 如果它是最新的源代码,它将跳转到GenerateUnoptimizedCode()而不是CompileBaselineCode(),并使用Ignition注册编译作业。
  • 调用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的角度不用去考虑它。


在确定使用UseTurboFan()的优化编译器后,将创建Crankshaft / TurboFan作业。
之后,job->CreateGraph()实际触发优化编译

阅读V8的源码

在exploit中,您还需要阅读源代码.
源代码(amples/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相同,但在使用前检查它是否为空
      • Persistent
        • 一个persistent Handle,保留在heap上
        • 代码编写器使用Persistent::Reset()管理生命周期
  • HandleScope
    • handle总结
      • Temporary Handle such as Local , MaybeLocal
      • 在声明HandleScope时,块中的每个handle都会自动关联
    • 当HandleScope超出范围时,它会处理释放handle
      • 返回函数时,结束{}时,等
      • 用所有使用的handle来描述释放处理是低效的
      • 使用HandleScope的析构函数,GC负责实际的释放处理
    • 参考以下的文件
      • include/v8.h,src/handles.h

        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问题进行调试

blob

  • 关于snapshot文件
    • V8在初始化时在内部生成内置JavaScript代码
      • 尽管这些代码可以每次使用时进行编译,但是效率不高。
    • 所以在编译阶段预先准备好他们。
      • 它只需要在启动时读取,因此初始化变得更快
      • 在构建V8时,他们一起生成
    • snapshot可以在程序内部/外部进行
      • 当snapshot文件被放置在外部的时候,就是blob
      • There are two of natives_blob.bin and snapshot_blob.bin

ICU

third_party

  • 和v8捆绑的工具(=被用来构建等)
    • icu, binutils, llvm, etc.
  • 它可以根据需要进行更换
    • 在使用Ubuntu 16.04进行构建时,由于其下属的ld.gold报错,因此通过系统链接程序的符号链接(/usr/bin/ld.gold)
      • ld.gold是Google在2012年左右制作的ld的高速版本
    • 使用GYP_DEFINES,您也可以替换环境变量

      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中放入括号
    • 此外,该文件的内容在最新的V8中已更改
      • 我还没有确认这一点。
  • For example, v8print and job commands display HeapObject cleanly
    • 对象的结构将在后面描述
    • 由于map有各种标志,最好在这里查看
  1. 例如,[0xdeadbee,0xdeadbeef,“hoge”]
    显示FixedArray(减去0x14,因为它指向FixedArray的第一个偏移量)
  2. 我试图显示FixedArray的Map的内容
  3. 实际上,我不必费心使用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) hogename##type();
        • 当我找不到它时,一般会有很多模式
    • asm (“int 3”)不能嵌入到IC或机器语言生成系统的功能中
      • 做一个blob作为构建过程的一部分
      • 目前这些代码似乎被使用,并且在很多情况下构建失败
    • namespace
      • i是v8::internal的别名
        • namespace i = v8::internal;(src/globals.h)
    • Changes will be made immediately
    • 以下是非常有用的
      https://github.com/danbev/learning-v8/blob/master/README.md

      关于V8的GC

      参考资料

      https://github.com/thlorenz/v8-perf/blob/master/gc.md
      https://github.com/joyeecheung/v8-gc-talk

垃圾收集器(GC)

  • 另一个重要组件是垃圾收集器

    • 一种在V8中单独管理JavaScript对象(称为HeapObject)的机制
      • 如何检测废弃的对象并自动释放它们
    • 使用与Linux heap不同的区域
  • 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)

  1. Young Generation
  2. 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

  • 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.
      • 顺次复制living的object。
        • 还有一些要复制到old generation的object
          正如我们后面将会看到的那样,两次在young generation的GC中幸存下来的对象,被复制到old generation的空间而不是复制到ToSpace
      • 完成后,重新选择root并重复复制。
    • 再次分配之前未分配完成的obj-e
      • From Space中还有garbage存在,但是因为我们不会再次使用它们,所以无所谓。
      • 之后,每次GC发生时,都会重复上面这一系列的流程

Old Generation

  • old space
    • long-lived objects存放的区域
      • New Space中, 在两次GC之后存活下来的object
        • 更多细节参考Heap::ShouldBePromoted()
      • old space发生GC的频率比new space少(取决于使用过程)
        • 如果一个object被移动到old space,该object不会受到GC更改layout的影响
  • code space
    • 仅适用于JIT的code object
      • 由于code object是RWX,因此它从一开始就保留在此区域中
        • 由于它是JIT代码,因此不仅要读取(R)写入(W),还要执行(X),因此memory permissions与其他的地方不同。
  • Map Space

    • 仅Map object
      • 出于GC效率的考虑,Map object从一开始就位于此区域
  • old generation的GC算法是Mark-Sweep-Compact

    • 除New Space区域以外的所有算法
    • 由于它与Exploit无关,因此省略了详细信息

      Other

  • Large Object Space
    • 保留600KB或更大的object的区域
      • 它由mmap直接分配
      • 如果有多个安全区域,请使用链表进行管理
      • 它不在GC中移动

Write Barrier

从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
  • 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

v8对象模型

参考资料

http://steps.dodgson.org/bn/2008/09/07/

V8的object

  • Object
    • v8自己创建的各种各样的类
      • 和GC合作
    • 针对C++类结构制作一个触发器
      • 例如,V8的object没有成员变量
      • 它既没有虚函数,也没有构造函数/析构函数
    • 细节参考src/objects.h,src/objects-inl.h等
  • Exploit
    • 了解每个object的内存结构非常重要
    • 在本文件中,我们将主要讲述下面的内容

      继承关系

      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的指针
      • 由于HeapObject基本上由GC管理,因此它位于GC区域(它不存放在堆区域)

        Smi

        如果一个成员的值是一个整数,那么存储它的速度会更快
        这就是使用Smi的原因(我认为这是原因)
  • 对于使用指针来创建一个整数对象(B)的实现,如图
    指针必须被追踪一次,内存访问两次(慢)

  • 对于省略了指向整数对象(B)的指针并且整数直接存放在内部的实现,如图
    指针应该指向的整数值在V8中被称为Smi
    没有必要跟随指针,内存访问执行一次(快速)

Tagged Values

  • Tagged Values

    • 同时表示指向Smi和HeapObject的指针的机制
      • 但是,不可能区分它们是整数值还是指针
        • 低1位(LSB)是一个标志
  • Smi(=Object)

    • 如果LSB为0,则可以通过右移1位获得原始值
    • 如果LSB为0,则可以通过右移32位获得原始值
  • 指向HeapObject的指针

    • 32位
    • 64位

      由于GC上的chunk在32位环境中的4字节对齐,64位环境中是8字节对齐的,因此LSB始终为0。也就是说,当它存储在内存中时,将其LSB设置为1即代表指针。

HeapNumber

  • 对象的值为double
    • 数字表达式不能在Smi范围内表达
    • 继承Object, HeapObject
      • 内存结构如下所示(64位环境下)

        V8的HeapObject完全没有任何成员变量,它完全由偏移量独立表示。
        为了画起来方便,我们将其视为变量名称,并如右图所示表示(对于后续幻灯片也是如此)
  • 实际演示
    • Smi值(0xdeadbee)和double值(0xdeadbeef,由于它大于0x7fffffff,非Smi)存放在数组中

      搜索
  • 由于0xdeadbee是Smi,因此可以通过在内存中搜索,来查找存储在数组中的值。(换句话说,直接看数组里对应的值就行了)
  • 0xdeadbee(Smi)之后的元素应该是0xdeadbeef(HeapNumber)
    0x41ebd5b7dde00000是0xdeadbeef的double值表示
  • HeapNumber对象和其他对象连续,保证没有任何间隙

    PropertyCell

  • Object meaning variable
    • 继承Object,HeapObject
      • 内存结构如下(在64位环境的情况下)
  • 实际演示
    • 存放Smi值(0xdeadbee)
    • 使用0xdeadbee搜索此PropertyCell的在内存中的位置
    • 尝试覆盖变量的值
    • 由于0xdeadbee是Smi,它的值可以通过在内存中搜索找到。

    • 更改为字符串
      • 如果你像以前一样检查相同的地址,则kValueOffset所保持的值会更改
      • 指向的地址是一个表示“hoge”的String对象(String对象的细节将在后面描述)

String

  • 保存字符串的对象
    • 继承Object, HeapObject, Name
      • 内存结构如下(在64位环境的情况下)
  • 实际演示
    • 存放在数组中的字符串“hoge”,“fuga”
    • 用0xdeadbee查找这个数组的内存位置
    • 由于0xdeadbee是Smi,因此你可以发现这个在数组中的值,通过在内存中搜索。基于此,确定数组的内存位置
    • 跟在0xdeadbee(Smi)之后的元素应该是一个String对象,如“hoge”或“fuga”
    • “hoge”和“fuga”连续存放,没有缺口。

Oddball

  • 表示特殊值的对象,例如true,false,undefined

    • 继承object,HeapObject
      • 内存结构如下(在64位环境的情况下)
  • 实际演示

    • 确保true,false等在数组中
    • 用0xdeadbee查找这个数组的内存位置
    • 0xdeadbee(Smi)的下一个元素应该是一个Oddball对象,如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

  • Object holding JavaScript function

    • 继承Object, HeapObject, JSReceiver, JSObject
      • 内存结构如下(在64位环境的情况下)
  • 实际演示

    • 存放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位环境的情况下)
  • 实际演练
    • 由于0xdeadbee是Smi,因此可以通过在内存中搜索来查找存储在数组中的值。 基于此,查找数组的内存位置(因为有一些候选项,请小心)

    • 如果增加数组的元素,它将自动扩大
      • 第三个和第四个元素被添加到只有两个元素的数组中
    • 当元素的数量增加时,它会扩展长度(FixedArray存放到另一个位置,并且kElementsOffset所保存的指针改变)
    • 顺便说一下,有很多0x186e00404369代表TheHoleObject的地址(Oddball的kind = 2意思是void)
  • 注意
    • 在一个数组中,有时会存储一个double值的情况
      • 它是一个非Smi范围,但它被存储为一个double值而不是HeapNumber地址
      • Smi范围,但存储为double值而不是Smi表示
    • Perhaps, it seems to be to decide the type of the entire array and speed up it

JSArrayBuffer

ArrayBuffer and TypedArray

  • Originally ArrayBuffer
    • 一个可以直接从JavaScript访问内存的特殊数组
      • 但是,ArrayBuffer仅准备一个内存缓冲区
      • BackingStore——可以使用TypedArray指定的类型读取和写入该区域,例如作为原始数据数组访问的8位或32位内存
      • 为了实际访问,有必要一起使用TypedArray或DataView
        • TypedArray是low function,但因为它不会执行额外的操作,所以访问速度很快。
        • 对于Exploit,最好不要做额外的事情(当发生意想不到的事情时很麻烦),因此比起DataView,我们更多的使用TypedArray。
    • 使用例子 (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); //或者,您也可以指定数组的开始和结束位置
    • 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
        –>当某些地址在V8上泄露时,通常在大多数情况下被迫将其解释为双精度值,为了正确计算偏移量等,需要将其转换为整数值。 对于完成该转换,ArrayBuffer是最佳的
      • ③从ArrayBuffer读取两个Uint32
        var t32 = new Uint32Array(ab);
        k = [t32[1],t32[0]]
        –>k是6.953328187651540e-310,将字节序列按照4个字节去分开,然后解释为Uint32,于是得到:
        k=[0x00007fff,0xdeadbeef]

        JSArrayBuffer

  • 持有ArrayBuffer的对象
    • 继承Object,HeapObject,JSReceiver,JSObject
      • 内存结构如下(在64位环境的情况下)
  • 实际演示
    • 存放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,但只是为了确保)
  • 源代码看起来像这样

实践