v8 exploit

v8的知识结构

环境搭建

预先准备

  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年当时)

  1. depot_tools准备
    1
    2
    $ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
    $ echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc
  2. 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. 启动
    1
    2
    $ ./out/x64.debug/d8
    $ ./out/x64.debug/shell

使用ninja的方式去编译v8(2018现在)

  1. 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/根据你自己的环境替换。

  1. 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
  2. v8编译
    1
    2
    3
    $ fetch v8 && cd v8&& gclient sync
    $ tools/dev/v8gen.py x64.debug
    $ ninja -C out.gn/x64.debug
  3. 启动
    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要做的是相似的。

攻略方法

  1. 创建一个用于调试的js环境版本
    如果有一个包含漏洞的patch,hit和build它。
  2. 分析patch以确定哪个patch适用于哪个进程
    Full-Codegen, Crankshaft, TurboFan, Ignition, AST, IC, …
    Full-Codegen and Crankshaft do not exist in V8 as of 2018 (see below)
  3. 编写利用漏洞的js代码段
    Think JavaScript code that causes patched parts to pass and causes bugs
  4. 创建一个任意地址读/写的原语
    主要使用ArrayBuffer和TypedArray
  5. getshell
    由于这是Pwn类别的问题,getshell是第一目标。
    在JIT区域嵌入shell代码经常被使用

今天的主题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被引入。

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(这次的解说就是用这种方法)

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

我想在这里说的

  1. 一个object(javascript中)有一个指向Map的指针
    • 正如我们稍后会看到的,object的前8个字节是一个指向Map的指针
  2. (JavaScript)object指向的map将根据状况改变
    • 在漏洞利用中,这不是一个可靠的指针
  3. 相同的类型=Map的地址是相同的
    • 比较map的地址即可确定类型是否一致

Inline Caching

参考资料:

附注:其中部分我做了翻译,可以在这里找到

对于各个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加载,它是很可能去进行整数相乘
        }
  4. 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:
  5. 实际上,由于有多个Map被注册的情况,所以需要进行函数化

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

  6. 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
        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的整体情况

TurboFan的特征

  • Graph Building
    • 从AST创建一个JavaScript节点的graph
      • JSAdd,JSCallFunction,JSLoadProperty,IfTrue,IfFalse等等
    • 在making graphs优化
  • 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
      删除无法访问的代码


文本框中的文字如下:
细节
出于某种原因,在名为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中,您还需要阅读源代码.
源代码(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相同,但在使用前检查它是否为空
      • 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) hoge_name_##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
https://www.youtube.com/watch?v=DSBLAG2IvsY

垃圾收集器(GC)

  • 另一个重要组件是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

  • 表示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版本)
      • 创建方法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,但只是为了确保)
  • 源代码看起来像这样

实践

攻略方法

  • 模式1和2,策略如下
  1. 创建一个用于调试的js环境版本
    如果有一个包含漏洞的patch,hit和build它。
  2. 分析patch以确定哪个patch适用于哪个进程
    Full-Codegen, Crankshaft, TurboFan, Ignition, AST, IC, …
    Full-Codegen and Crankshaft do not exist in V8 as of 2018 (see below)
  3. 编写利用漏洞的js代码段
    Think JavaScript code that causes patched parts to pass and causes bugs
  4. 创建一个任意地址读/写的原语
    主要使用ArrayBuffer和TypedArray
  5. 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的必要文件。

  1. libc.so.6
    当前的libc(ubuntu14.04)
  2. natives_blob.bin
    shell操作所需的文件
  3. problem.patch
    有漏洞的v8 patch
  4. shell
    构建samples/shell.cc用于本地测试
  5. snapshot_blob.bin
    shell操作所需的文件

shell文件

shell (and *.blob)用作debug的目的
distribution file应该包含与problem服务器相同的源文件

  • V8解释和执行的是JavaScript
    • 因此,exploit也只需要用JavaScript编写
  • 对于相同的输入,V8应该表现相同
    • 如果你可以通过shell来exp v8,你也可以通过Web服务器exp v8

patch分析

  1. 启用PIE,FORTIFY_SOURCE,stack canary
  2. PIE,FULL-RELRO启用
  3. 使read和load函数无效
    print,read,load,quit和version等功能都是在sample/shell.cc中专门定义的,但是经过patch后,read和load时会失效
  4. 从源代码路径,TurboFan相关补丁和猜测

    lhs:左手侧(左侧)rhs:右手侧(右侧)?
    此外,因为函数名称是JSAddRanger,所以可以预料可能是与加法有关的漏洞(?)

Identification of calling conditions

  • 假设被patch的函数是有漏洞的
    • 我想调用这个函数
      • 如果我们不能调用这个函数,我们不能触发漏洞
    • 怎么调用它?
      • 我没有任何提示,我只有一个函数名称
      • 猜测这个函数所在的文件或目录有什么功能
      • 从该函数的调用回溯,找到调用路径。
      • 最终目标是找到读取和调用每个函数的条件。

Google

尝试google搜索函数名称
触及此功能的评论页尤为重要

确认内置漏洞的文件名

  • src/compiler/typer.cc
    • 由于它是src/compiler/下的一个文件,因此它被认为与TurboFan有关

确定来源

  • 检查修补程序周围的代码
    • 看看JSAddRanger()

确定调用路径

  • 到目前为止,做一个总结

  • 寻找调用路径

    • 使用gdb回溯调用栈是最快的方法。
  • 编写测试代码

    • 准备Javascript code去调用TurboFan
      • 通过代码调用函数10000次,由于我想调用TurboFan,所以不要忘记使用with语句
  • 使用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()

  • 同时检查JSAddTyper()的调用者

    • 在函数定义中只找到一个地方
      • 由于无法用简单的grep找到它,因此很有可能涉及宏。
        • 追溯更多来源是很麻烦的
    • 在JSAddTyper()上放置一个断点并查看是否能在测试代码中断下来
      • 如果没有断下,努力尝试,阅读源码,进一步回溯
    • 使用gdb运行
      • 在JSAddTyper()上放置一个断点,发现停在断点处
  • 在停止后进行回溯(查看调用栈)

    • 从这个函数名,可以确认JSAddRanger()和JSAddTyper()与优化编译器(TurboFan)相关
    • 由于它是TyperPhase::Run,证明它是一个关于TurboFan优化中“Typer”阶段的函数。
      另外,如果TurboFan进行了优化编译运行,就会发现JSAddTyper()在测试代码中被调用。
  • 您可以在TyperPhase中看到TurboFan的功能

  • 到目前为止做一个总结

  • JSAddRanger()调用条件探索

    • lhs-> IsRange()&& rhs-> IsRange()
    • 首先,代码中出现的Range是什么?
      • 为了知道这一点,我们需要更多地了解TurboFan的优化机制

Typer和Range调查

确定调用条件

  • 编写测试代码
    • 添加AND操作并在gdb下执行
    • 在此之后,在断点处停止
  • 调用路径总结

漏洞理解

  • 补丁周围的代码

  • 从操作实例理解

  • 用make x64.debug -j 8重新编译

    • 在“环境设置”描述的步骤中,我已经预先构建了它。
  • 尝试有趣的测试case

    • 下面的JavaScript代码展示了有趣的结果
    • 如果打了有漏洞的补丁,你会在AND操作之后得到新的范围信息,而这个范围信息是错误的
    • 但是,由于该信息仅用作类型提示,因此它不会对选择Word32的类型造成影响,因为它可以用32位表示[0,24]和[0,16]。在这个例子里,f函数正常应该返回24作为结果

解法

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具有范围信息
    • 修改源代码为有漏洞的版本
    • make x64.debug -j 8再次编译
  • 在gdb下运行,检查内存
    • 搜索0xdeadbee将命中3次,其中这个是一个数组(= FixedArray)
    • 通过将标记值1加到FixedArray的地址上,然后进行搜索
    • 找到的地址应该是JSArray
    一个很重要的图


    通过check我知道我能伪造FixedArray的长度,因为如果不打patch,上下的值应该都是0x18
  • 这意味着什么?
    • 我们打算创建一个长度0x18元素的数组,但实际上只准备了长度0x10元素的数组

虽然它与基本形式稍有不同,但如果您仅信任此长度(0x18),并将这个长度的值写入FixedArray,则您能够读取和写入的数据,将会超出原本仅有0x10的FixedArray
实际上想用只有0x8个字节的数据来覆盖backing store ptr是十分困难的,我们将在后面介绍更好的方法。

参考解说:typed-lowering

  • 为什么会出现这种情况?

    • 使用range信息的部分在哪里?
      • 在typer phase旁边的typed-lowering phase
  • typed-lowering

    • 它似乎使用type information来optimize graph
    • Follow until array association comes out






  • 用printf调试确认

编写exp

  • 你现在在做什么?
    • Array length伪造
  • 我们将会考虑编写exp
    • 流程如下
      1. 伪造Array length
      2. 伪造ArrayBuffer
      3. Identify function object
      4. 在JIT区域嵌入shellcode

伪造Array length

  1. 顺次排列两个FixedArray(其长度通过range漏洞伪造)
    由于这些FixedArrays的范围是伪造的,因此它们可以在数组之外进行读写操作.

  2. 每个double值存储在Element [0]中,并且数组类型被识别为double类型(即FixedArray被转换为FixedDoubleArray)
    • 在FixedArray变成FixedDoubleArrayed之后,内存的布局会发生变化并且变的很麻烦,所以让我们先提前认识它是一个只有double类型元素的FixedDoubleArray。
      这也确保了FixedDoubleArray 1和2连接

  3. 使用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
    1
    2
    for(var i=0;i<24;i+=17)
    Element[i]=value;
    称为KeyedStore IC的IC对应于数组元素中的存储,KeyedStoreIC是状态直接转换为(0)->(1)的IC,IC在第二次访问时已处于活动状态.
    当通过触发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

  1. 将ArrayBuffer放置在FixedDoubleArray 1,2之后

  2. 由FixedDoubleArray 2的漏洞搜索kByteLengthOffset和kBackingStoreOffset的偏移量

    • 它每次从FixedArray2的元素[16]中读取一个元素,并根据是否包含假定值进行搜索。 您可以在kByteLengthOffset(= ArrayBuffer拥有的BackingStore的长度)中放入一个特征值并搜索它(如果向此添加1,kBackingStoreOffset的偏移量也是已知的)
    • ab_off表示从FixedArray 2(代码中的arr [1])中看到的作为kByteLengthOffset位置的元素索引,
  3. 更新来kByteLengthOffset和kBackingStoreOffset的偏移量

    • 如果设置元素[kByteLengthOffset_offset] = 0xfffff0,
      元素[kByteLengthOffset_offset + 1] = rw_addr。
      请记住在步骤2-2中获得的偏移量,并且在ArrayBuffer读取和写入rw_addr,也就是说,它几乎变成了任何内存读/写的原语。
    • 通过重写kBackingStoreOffset的值,您可以读取和写入任意地址,因此在利用时可以将其作为RW原语的函数来实现。
      另外,关于i2_to_d()的描述被省略了,它是一个函数,可以使用ArrayBuffer将Uint32转换为double。

Identify function object

  1. ArrayBuffer后面跟着FixedArray 3,它有一个适当的函数对象作为元素

    选择7作为特征值没有特别的意义。 只要选择你喜欢的长度,然后尝试。

  2. 从FixedDoubleArray 2中,使用漏洞探索FixedArray 3的Element的偏移量,获取函数f的对象地址。

    • 如果一个值连续出现kLengthOffset次(如图中是7),则可以推断出它可能是包含函数f的对象地址的元素的偏移量。

在JIT区域嵌入shellcode

  1. 通过将函数f的对象地址放入kBackingStoreOffset并读取第七个元素,获取kCodeEntryOffset中的JIT地址
  2. 将该地址放在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
  • 除了本文中讨论的解决方案之外,PPP还使用GC实现了另外一种writeup

Bonus Other JavaScript issues and links

V8

SpiderMonkey

Chakra

WebKit-JSC

v8 exploit study

SpiderMonkey exploit study

WebKit-JSC exploit study