前言
通过 2019-starctf
的一道例题尝试入门 v8
。跟着前辈们学习并记录一下自己的复现路程。
环境:Ubuntu 18.04
题目:下载链接:https://github.com/0xfocu5/CTF/blob/master/Chrome/2019-starctf-oob.zip
环境搭建
翻墙 翻墙 翻墙
1 | $ cd ~ |
把题目给出的diff
文件应用到源码中
1 | $ git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598 |
基础知识
V8流程
JavaScript
是一门解释型语言,而v8
则是chrome
浏览器的JavaScript
解析引擎,大多数漏洞都是由v8
所引起的 (v8
编译过后的可执行文件是d8
).
JavaScript
的执行流程大致如下图:
- JS 源代码经过词法分析形成
Token
,解析器(Parser)解析token
形成抽象语法树(AST) - 解释器(Ignition)将 AST 生成可执行的字节码。解释器可以直接执行字节码,或者通过编译器将其编译为二进制的机器代码再执行。
- 解释器执行字节码过程中,如果发现代码被重复执行,热点代码(HotSpot)超过阈值后就会丢给优化编译器(TurboFan)编译成二进制代码,然后优化。下次再执行时则直接执行这段优化后的二进制代码。
- 如果JS对象发生变更,优化后的二进制代码变为无效代码,编译器执行反优化,下次执行就回退到解释器解释执行。
V8调试
入这个选项就可以在js
中调用一些有助于调试的本地运行时函数:
1 | %DebugPrint(obj) 输出对象地址 |
PS:v8团队的专门编写了一个gdb
的gdbinit
脚本。在~/xxx/v8/tools
下,将其更名为gdbinit_v8
1 | $ cp gdbinit_v8 ~/.gdbinit_v8 |
有两个常用命令
1 | job [address_of_obj] # gdbinit_v8中的特有命令,打印出对象内存结构,注意对象地址为其实际地址加1 |
示例:
1 | var a = [1,2,3]; |
1 | v8/out.gn/x64.debug$ gdb ./d8 |
PS:在release
使用 job
命令会报`No symbol “_v8_internal_Print_Object” in current context. 的错误
map | 表明了一个对象的类型对象b为PACKED_DOUBLE_ELEMENTS类型 |
---|---|
prototype | prototype |
elements | 对象元素 |
length | 元素个数 |
properties | 属性 |
Value B is an 8 bytes long value //in x64.
If B is a double:
B is the binary representation of a double
Else:
if B is a int32:
B = the value of B << 32 // which mean 0xdeadbeef is 0xdeadbeef00000000 in v8
else: // B is a pointer
B = B | 1
v8
在内存中只有数字和对象两种表示。为了区分两者,v8在所有对象的内存地址末尾都加了1。例:上述 elements的实际地址应为 0x2658b5f8de39-1
题目复现
漏洞分析
分析题目所给出的diff
文件。
1 | diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc |
- 自定义了一个函数kArrayOob,可以通过oob调用
- 该函数将首先检查参数的数量是否大于2(第一个参数始终是
this
参数)。如果是,则返回undefined。 - 如果只有一个参数(
this
),则会返回array[length]
。 - 如果有两个参数(
this
和value
),它将value
作为一个浮点数写入array[length]
。(以上所述的参数均为cpp中) - 上述逻辑转换为JavaScript中的对应逻辑就是,当
oob
函数的参数为空时,返回数组对象第length个元素内容;当oob
函数参数个数不为0时,就将第一个参数写入到数组中的第length个元素位置。
编写test.js
如下
1 | var a = [1, 2, 3]; |
1 | pwndbg> telescope 0x25641fd4ddd9-1 |
可以发现v8的内存对象大致如下:其中map pointer
描述数组对象的结构,element pointer
是存储数组元素的结构。
1 | -32 : some pointer // not related to the challenge. This is memory is also where the element pointer points at. |
漏洞利用
1 | var obj = {} |
1 | /v8/out.gn/x64.debug$ ./d8 --allow-natives-syntax ./test.js |
通过上述例子,我们可以看到我们所泄露出来的地址就是map pointer
,而map pointer
数组的指示其元素的类型,如果我们利用oob
的读取功能将数组对象A的对象类型Map读取出来,然后利用oob的写入功能将这个类型写入数组对象 B,就会导致数组对象B的类型变为了数组对象A的对象类型,这样就造成了类型混淆。
如果我们定义一个 FloatArray
浮点数数组A,然后定义一个对象数组B。正常情况下,访问A[0]返回的是一个浮点数,访问 B[0] 返回的是一个对象元素。如果将B的类型修改为A的类型,那么再次访问 B[0] 时,返回的就不是对象元素 B[0] ,而是B[0]对象元素转换为浮点数即B[0]对象的内存地址了;如果将A的类型修改为B的类型,那么再次访问 A[0] 时,返回的就不是浮点数 A[0],而是以 A[0] 为内存地址的一个JavaScript对象了。
其实到现在可以简化一下漏洞:
- 泄露
map pointer
- 覆写
map pointer
addressOf && fakeObject
我们得到的数据都是浮点数的形式,而我们需要的是其在内存中的16进制数据,所以需要浮点数和整数之间的转换
1 | var obj = {"a": 1}; |
任意地址写
我们可以写一个 js 数组伪造成一个 js 对象(结构如下),那么当我们访问fake_array[2]
的时候就会当成一个对象去访问,那么我们就可以修改他的值,从而实现任意地址写
1 | var fake_array = [ |
1 | var obj = {"a": 1111}; |
1 | var fake_array = [ |
泄露libc
方法A:
1 | pwndbg> telescope 0x000038ebfeb8f9a0-0x8000 0x5000 |
1 | var a = [1.1, 2.2, 3.3]; |
跑了半天,没泄露出来…
方法B:
1 | /*------------------------------leak d8------------------------------*/ |
查看Array对象结构 –> 查看对象的Map属性 –> 查看Map中指定的constructor结构 –> 查看code属性 –>在code内存地址的固定偏移处存储了v8二进制的指令地址
1 | var test_array = [1.1]; |
在release
版本下,则会泄露出 d8
的地址
本地shell
有了libc
剩下就可以和常规pwn
一样了,有任意地址写,直接写free_hook
就好。
在调试的时候发现写0x7f…这样的地址写不上去,看e3pem师傅的博客发现另一种写法
这里有另外一种方式来解决这个问题,DataView对象中的
backing_store
会指向申请的data_buf
,修改backing_store
为我们想要写的地址,并通过DataView对象的setBigUint64方法就可以往指定地址正常写入数据了。
1 | var data_buf = new ArrayBuffer(8); |
1 | var obj = {"a": 1}; |
WASM
WebAssembly或称wasm是一个实验性的低级编程语言,应用于浏览器内的客户端。WebAssembly是便携式的抽象语法树[1],被设计来提供比JavaScript更快速的编译及运行[2]。WebAssembly将让开发者能运用自己熟悉的编程语言(最初以C/C++作为实现目标)编译,再藉虚拟机引擎在浏览器内运行[3]。WebAssembly的开发团队分别来自Mozilla、Google、Microsoft、Apple,代表着四大网络浏览器Firefox、Chrome、Microsoft Edge、Safari[4]。2017年11月,以上四个浏览器都开始实验性的支持WebAssembly[5][6]。WebAssembly 于 2019 年 12 月 5 日成为万维网联盟(W3C)的推荐,与 HTML,CSS 和 JavaScript 一起,成为 Web 的第四种语言。[7]。
https://wasdk.github.io/WasmFiddle/,这个网站可以在线将C语言直接转换为wasm并生成JS配套调用代码。
1 | 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]); |
在js
代码中加入wasm
之后,程序中会存在一个rwx
的段,我们可以把shellcode
放到这个段里面,再跳过去执行。
1 | 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]); |
1 | var obj = {"a": 1}; |
结语
复现了蛮久的,细节还需要多理解。
参考文章
https://juejin.im/post/6844904096260947981#heading-2
https://changochen.github.io/2019-04-29-starctf-2019.html
https://www.freebuf.com/vuls/203721.html