前言
学习浏览器,从v8入手,这道题有比较详细的资料,作为入门题非常有优势。
环境搭建
基础v8的环境搭建
使用的环境:ubuntu 18.04
v8环境搭建:https://warm-winter.github.io/2020/10/11/v8%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/
解题的搭建
一般浏览器的出题有两种
- 一种是diff修改v8引擎源代码,人为制造出一个漏洞,
- 另一种是直接采用某个cve漏洞。一般在大型比赛中会直接采用第二种方式,更考验选手的实战能力。
出题者通常会提供一个diff文件,或直接给出一个编译过diff补丁后的浏览器程序。如果只给了一个diff文件,就需要我们自己去下载相关的commit源码,然后本地打上diff补丁,编译出浏览器程序,再进行本地调试。
比如starctf中的oob题目给出了一个diff文件:
1 | diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc |
以上截取了第一部分,对/path/v8/src/bootstrapper.cc做了修改。
下载v8然后利用下面的命令,将diff文件加入到v8中源代码分支中:
1 | git apply /path/oob.diff |
我们找到bootstrapper.cc
文件,搜索SimpleInstallFunction(isolate_, proto, "fill",
,发现下面已经将oob函数加入进去,patch成功。
最后编译出增加了diff补丁的v8程序调试即可。
环境问题
正常来说,debug版本和release版本都能使用,但是调试这道题的时候,碰到了如下的问题:
release版本正常运行
debug版本报错
e3pem师傅的博客是这样解释的:
了解到是DCHECK宏的问题,然而对宏修改或是注释之后发现编译出来的d8执行还是会出现问题(这个时候已经开始怀疑人生了)。后来仔细的观察了一下师傅们写的文章,发现里面调试oob的时候都是用的release版本,之前也试过release版本的d8确实不会出现问题,所以很可能debug版本的d8就是不行,而别人文章里面出现的debug版本的d8的目的就是为了了解v8的数据是怎么存储的。所以这里正确的用法应该是用release版本进行调试,用debug版本来辅助分析。
v8的基础知识
v8编译后二进制名称叫d8而不是v8。
调试
1.allow-natives-syntax选项
功能:定义了一些v8运行时支持函数,主要有以下两个:
1 | %DebugPrint(obj) 输出对象地址 |
使用:
1 | //方法一 |
2.job命令
功能:可视化显示JavaScript对象的内存结构.
gdb下使用:job 对象地址
显示如下,具体v8的内存结构,稍后“v8对象结构”里进一步解释。
1 | pwndbg> job 0x4f9d210dd59 |
3.telescope
功能:查看一下内存数据
使用:telescope 查看地址 (长度)
()表示里面的可以没有
v8知识点
指针标记
v8使用指针标记机制来区分指针,双精度数和Smis(代表)immediate small integer
。
1 | Double: Shown as the 64-bit binary representation without any changes |
所以,v8中,如果一个值表示的是指针,那么会将该值的最低bit设置为1,但其实真实的值需要减去1。
job直接给对象地址就行,telescope的时候,需要给真实值,需要-1。
v8对象结构
在/path/v8/out.gn/x64.debug下创建一个test.js
1 | var a = [1,2,3]; |
1 | winter@ubuntu:~/v8/v8/out.gn/x64.debug$ gdb ./d8 |
所以,一个对象有如下属性:
- map:定义了如何访问对象
- prototype:对象的原型(如果有)
- elements:对象的地址
- length:长度
- properties:属性,存有map和length
分析:
对象里存储的数据是在elements指向的内存区域的,而且是在对象的上面。也就是说,在内存申请上,v8先申请了一块内存存储元素内容,然后申请了一块内存存储这个数组的对象结构,对象中的elements指向了存储元素内容的内存地址。
map属性详解
因为稍后需要用到,,所以放在这里讲一下
1 | pwndbg> job 0x176329f00801 |
对象的map(数组是对象)是一种数据结构,其中包含以下信息:
- 对象的动态类型,即String,Uint8Array,HeapNumber等。
- 对象的大小(以字节为单位)
- 对象的属性及其存储位置
- 数组元素的类型,例如,unboxed的双精度数或带标记的指针
- 对象的原型(如果有)
属性名称通常存储在Map中,而属性值则存储在对象本身中几个可能区域之一中。然后,map将提供属性值在相应区域中的确切位置。
本质上,映射定义了应如何访问对象。
重点
对于对象数组:存储的是每个对象的地址
对于浮点数组:以浮点数形式存储数值
所以,如果将对象数组的map换成浮点数组 => 就变成了浮点数组,会以浮点数的形式存储对象的地址;如果将对浮点组的map换成对象数组 => 就变成了对象数组,打印浮点数存储的地址。这实际上就是类型混淆的内容。
对象和对象数组
有时候想着想着有点乱,调试一下。
一个浮点数组、整数数组和一个对象数组。
1 | var a = [1.1,2.2,3.3]; |
1 | winter@ubuntu:~/v8/v8/out.gn/x64.debug$ gdb ./d8 |
也就是说,对象数组里面,存储的是别的对象的地址,这里存储的是浮点数组和整型数组的地址
漏洞分析
分析给定文件中的oob.diff,左边行开头的地方,表示diff文件增加的内容
该diff文件实际就是增加了一个oob函数。主要分为三部分:定义、实现和关联。
定义
为数组添加名为oob的内置函数(就是别人调用的话),内部调用的函数名是kArrayOob(实现oob的函数)
1 | src/bootstrapper.cc |
实现
- 函数将首先检查参数的数量是否大于2(第一个参数始终是
this
参数)。如果是,则返回undefined。- 如果只有一个参数(
this
),它将数组转换成FixedDoubleArray
,然后返回array[length](也就是以浮点数形式返回array[length])- 如果有两个参数(
this
和value
),它以float形式将value
写入array[length]
。
1 | src/builtins/builtins-array.cc |
重点
漏洞就出在这个函数里面
如果给一个参数,返回了array[length]
如果给两个参数,将给定的参数写入array[length]
很显然array[length]这里冒了,访问到了数组后面的内存区域。调试看一下后面这个内存存储什么信息。
使用debug版本
1 | //test.js |
1 | winter@ubuntu:~/v8/v8/out.gn/x64.debug$ gdb ./d8 |
综上,我们得到的是读写map和修改map的功能
我们在release版本下实际调试
1 | var a = [1.1,2.2,3.3]; |
1 | winter@ubuntu:~/v8/v8/out.gn/x64.release$ gdb ./d8 |
关联
为kArrayOob类型做了与实现函数的关联:
1 | src/builtins/builtins-definitions.h |
漏洞利用
类型混淆
由于v8完全依赖Map类型对js对象进行解析。
所以,我们通过修改对象的map,将对象数组的map设置为浮点数组的map,就能让v8解析原来的对象数组的时候,解析成为浮点数组,反之同理。由于两种数组内部存储的不同,可以实现一些小功能。
对象数组存储的是每个对象的地址,也就是对象数组存的是地址。
浮点数组存储的是浮点型是的数值。
addressOf
泄露某个对象的内存地址,日后可以实现任意地址读的功能。
因为对象数组存储的是地址,但是如果v8解析是对象数组的话,肯定就不会输出这个地址,而是找到这个对象再操作。但是,如果,让v8误以为这是一个浮点数组,那么,v8就把把这个地址当作是浮点数,以浮点数的形式将对象数组里面存储的对象地址输出了。
所以,步骤如下:
1.拿到要泄漏的地址
2.把这个地址,覆盖已经创建好的对象数组第一个元素obj_array[0](让地址成为对象数组的一员)
3.将对象数组的map替换为浮点数组的map
4.输出数组的第一个元素,此时,就会按照浮点形式,将地址里的内容输出出来。
1 | var obj = {"a": 1}; |
fakeObject
将指定内存地址强制转换为一个js对象,日后可以实现任意地址写的功能。
现在,有了地址,地址是一个整数,整数可以直接变成以浮点数表示,但是不能变成对象,所以还是需要混淆。
步骤:
1.拿到地址,转换为浮点数表示。
2.放入浮点数组第一个位置中。
3.将浮点数组的map替换为对象数组的map
4.数组的第一个位置上,内存地址就已经变成一个js对象了。
1 | function fakeObject(addr_to_fake) |
辅助的工具函数
浮点数转整数、整数转浮点数、字节串表示整数
实现方法:开辟一块空间,创建两个数组,分别是浮点数组float64和整数数组bigUint64,他们公用创造的那块空间。
这样,根据原来的形式放入对应的数组,用转换的数组输出即可。
例如:f2i(),要将浮点数转换为整数,只要将浮点数放入浮点数组,然后用整数数组输出,因为空间是一个,所以,输入输出的是同一个值,但由于数组的属性不同,会按数组的属性进行解释,进来的时候是浮点数,比如存入了0001H单元,然后输出的时候,还会读这个0001H单元,但是这个时候,用的是整数数组,所以会把它以整数的格式输出。
1 | var buf =new ArrayBuffer(16); |
整合在一起调试:
1 | // ××××××××1. 无符号64位整数和64位浮点数的转换代码×××××××× |
1 | pwndbg> r |
成功泄漏对象的地址。同样,利用fakeObject可以将某个内存地址转换为一个object对象。
任意地址读写
我们首先构造一个假的数组对象,我们可以用fakeObject将其转换为一个object对象。因为自己构造的elements指针是可控的,而这个指针是指向存储数组元素内容的内存地址。所以,只要在elements上放入我们想要读写的地址,就可以用对象进行读写操作了。
步骤:
1.利用可控内存,伪造自己的对象结构。
2.将自己伪造的对象结构转换为真的对象。
我们伪造的是一个对象在内存中的表示,只有这样,elements才是我们自己可以填的。通过addressOf找到是,伪造的对象数组在内存中的地址,也就是他的对象结构开头,真实存储的内容在泄漏的地址-伪造的长度(6×0x8),然后我们要让v8认为真实存储的内容是一个对象,所以对泄漏的地址-伪造的长度(6×0x8)做fakeObject,那么,我们构造的这个数组,就真的成为了一个对象在内存的表示。
3.任意地址读。给定的地址是要读的地址,elements在读写的数据-0x10。把这个伪造的elements给伪造的内存,然后利用上述第二步,变成一个对象(fake_object是用fake_array出来的),读取对象的元素,就是地址的内容了。
4.任意地址写也是一样。把地址变成一个对象,那么要写入的地址就是我们对象的数据了。
1 | // read & write anywhere |
整合测试一下:
1 | // ××××××××1. 无符号64位整数和64位浮点数的转换代码×××××××× |
创建一个对象,找到他的地址。
读取对象地址存储的内容,然后改写对象地址存储的内容。
1 | pwndbg> r |
成功!!
任意写改进
问题:通过上面的方式任意地址写,在写0x7fxxxx这样的高地址的时候会出现问题,地址的低位会被修改,导致出现访问异常。
解决:DataView对象中的backing_store
会指向申请的data_buf
(backing_store
相当于我们的elements),修改backing_store
为我们想要写的地址,并通过DataView对象的setBigUint64方法就可以往指定地址正常写入数据了。
1 | var data_buf = new ArrayBuffer(8); |
浏览器运行shellcode:wasm
wasm是让JavaScript直接执行高级语言生成的机器码的一种技术。
使用:网站https://wasdk.github.io/WasmFiddle/:在线将C语言直接转换为wasm并生成JS配套调用代码。(左下角选择Code Buffer,然后点击最上方的Build按钮,左下角生成了我们需要的wasm代码。)
问题:wasm中只能运行数学计算、图像处理等系统无关的高级语言代码。所以不能直接在wasm中写入我们的shellcode,然后浏览器调用执行。
方案:结合漏洞将原本内存中的的wasm代码替换为shellcode,当后续调用wasm的接口时,实际上调用的就是我们的shellcode了。
步骤:
1.首先加载一段wasm代码到内存中
2.然后通过addressOf找到存放wasm的内存地址
3.接着通过任意地址写原语用shellcode替换原本wasm的代码内容
4.最后调用wasm的函数接口即可触发调用shellcode
寻找存放wasm代码的内存页地址
通过Function—>shared_info—>WasmExportedFunctionData—>instance,在instance+0x88的固定偏移处,就能读取到存储wasm代码的内存页起始地址。
1 | //test.js,用debug版本调试 |
1 | winter@ubuntu:~/v8/v8/out.gn/x64.debug$ gdb ./d8 |
所以,根据以上,可以编写代码自动查找该地址。
1 | var shared_info_addr = read64(f_addr + 0x18n) - 0x1n; |
整合的调试代码如下:
1 | // ××××××××1. 无符号64位整数和64位浮点数的转换代码×××××××× |
1 | pwndbg> r |
成功!
getshell
编写getshell的部分
shellcode这里找的:https://www.it610.com/article/1295723160905261056.htm
1 | var shellcode=[ |
完整的exp
1 | // ××××××××1. 无符号64位整数和64位浮点数的转换代码×××××××× |
注:非root用户可以开shell,/bin/sh这个文件不是只有root才能执行,进root是提权洞存在的意义。
参考资料
- 从一道CTF题零基础学V8漏洞利用:https://www.freebuf.com/vuls/203721.html(这篇知识+调试,全但是有点杂,建议进一步理解时候看)
- 浏览器入门之starctf-OOB:https://e3pem.github.io/2019/07/31/browser/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%85%A5%E9%97%A8%E4%B9%8Bstarctf-OOB/(这篇方便调试,建议先看,调一遍)
- Exploiting v8: *CTF 2019 oob-v8:https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/
- 2019-StarCtf-oob:https://0xfocu5.github.io/posts/7eb4a1e6/
总结
花了好久,终于弄完了,真的是,做题5分钟,环境3小时的真实写照,环境强推国外云服务器,大概需要1天时间。
v8这块做下来,还是比较好理解的,可能刚开始看有点晕,但是静下心来好好想想还是能想得通。
本文由winter原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/219815
安全客 - 有思想的安全新媒体