0x00 Official Bug Reports
Bug report: https://bugs.webkit.org/show_bug.cgi?id=191731
关键的Patch:
1 | // Source/JavaScriptCore/builtins/RegExpPrototype.js |
[5] 官方介绍.
0x01 Simple POC
来自[1].
1 | var victim_array = [1.1]; |
它的运行结果:
- 在第20行之后,
victim_array[0]
类型为Pointer
(预期类型为Double
), 而值却为5.2900040263529e-310 == 0x0000616161616161
. - 在第21行尝试调用
victim_array[0].toString()
, 但是它是一个invalid pointer, 因此产生了segmentfault.
造成原因:
第10-12行触发JIT之后, 使得第7行这里有一个
PutByVal
without type check, 会直接将val
写入对应的内存, 不考虑它的类型. 这是因为JIT compiler在优化时, 认为val
值没有发生变化. 更具体地说, 在第6行这里读取lastIndex
时产生的副作用(调用.toString()
)没有被考虑到.第19行将
reg.lastIndex
设置为了一个object, 这个object的toString
方法里面会将victim_array[0]
置为一个empty object.第20行触发了之前生成的JIT代码:
- 调用
match
时, 会读取lastIndex
, 发现它不是一个整数, 那么需要将其转换成整数, 所以触发了toString
, 使得victim_array[0]
被置为了一个object. - 随后在写入
val
时并没有进行type checking.
- 调用
0x02 详细的漏洞原因
前面提到了大致原因, 就是DFGJIT compiler没有意识到在访问lastIndex
会产生side-effects. 运行jsc时附带上环境变量JSC_dumpSourceAtDFGTime=true
和JSC_reportDFGCompileTimes=true
, 可以显示DFGJIT优化作用在哪些source code上. 值得注意是match
方法是由Javascript代码写的, 这意味jsc中的内置函数可能有多种implementation方式, 以后在查阅指定函数时, 可以关注一下.
1 | // builtins/StringPrototype.js |
跟进第13行,
1 | // builtins/RegExpPrototype.js |
其中hasObservableSideEffectsForRegExpMatch
函数会判定此次match
方法调用过程中是否会产生side-effects. 如果没有产生side-effects, 则进行regExpMatchFast
调用. 参考前面的patch, hasObservableSideEffectsForRegExpMatch
并没有考虑到lastIndex
的存在. 所以这里会顺利的进行regExpMatchFast
调用, 而在DFGJIT认为它并不会clobber the World (side-effects alias?) .
1 | // Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h |
值得注意是RegExpTest
中的comment已经提及了lastIndex
可以会被改变, 有趣是RegExpMatchFast
中为何不考虑呢?
0x03 大致的利用路线
大致路线为:
- 构造
addressOf
原语 - 构造
fakeObj
原语 - 构造更加稳定的
addressOf
和fakeObj
原语 - 构造任意地址
read64
和write64
原语
0x04 addressOf 原语
原语如下, 传入的obj
为一个需要泄露地址的目标object. 来自[1]
1 | function addrOf(obj) { |
注意此时函数funcToJit
中的不再是PutByVal
, 而是GetByVal
. 当JIT compiler认为victim_array
在运行过程中没有改变(总是ArrayWithDouble
)之后, 它会直接读取它的第1个元素, 把它作为一个Double
返回而不进行type checking.
这里有一个小问题, 一个Pointer
对应value显然不是一个合法的Double
, 那么函数addrof
是如何将Pointer
作为Double
返回的 ? 这个问题非常有趣, 经过一段时间debug, 我发现了Double
在内存中的表示是不一样的.
- 当一个object被标记为
ArrayWithDouble
, 意味着它的所有元素都是Double
. 这个时候所有的Double
会以原double-floating point存储, 而不会加上2^48
的offset. - 当一个object被标记为
ArrayWithContiguous
, 意味着它包含的元素类型是多样的. 这个时候所有的元素都会严格按照JSValue
的规定来存储.
我是如何发现这个问题呢? 我用--dumpDFGDisassembly
打印了经过DFGJIT优化过的代码:
1 | 438:<!3:loc14> GetByVal(KnownCell:@629, Int32:@437, Check:Untyped:@621, Double|MustGen|VarArgs|UseAsOther, AnyIntAsDouble|NonIntAsdouble, Double+OriginalCopyOnWriteArray+InBounds+AsIs+Read, R:Butterfly_publicLength,IndexedDoubleProperties, Exits, bc#39, ExitValid) predicting NonIntAsdouble |
可以看到GetByVal
只进行了array index的检查 ($rax
为butterfly的地址), 然后就把值取出来了 (放在$rax
上). 而后有一个ValueRep
操作, 看一下对应的codegen代码(Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
), 它配合DoubleRep
会将指定的值boxxing成一个合法的Double
. 看到这里我们才意识到double-floating point可能就是以原值存储的. 对此, JSC (Source/JavaScriptCore/bytecode/DataFormat.h
)解释为
Values may be unboxed primitives (int32, double, or cell), or boxed as a JSValue. For boxed values, we may know the type of boxing that has taken place. (May also need bool, array, object, string types!)
总之就是一句话Storing our value as a JSValue is necessary. 可以尽可能地优化掉encode/decode带来的performance. 在这里作为函数返回值, Caller可能不能准确地预知它的类型, 那么统一将其视为JSValue
, 所以这里需要一个boxDouble过程. 使得即使这里是一个Pointer
, JSC依然认为其就是一个double-floating point.
0x05 fakeObj原语
原语如下, 来自[1] . 传入的addr
为一个double, 作为fake object的地址.
1 | function fakeObj(addr){ |
与Simple POC比较相似. 想要fakeObj
make sense, 我们需要传入一个正确的addr. 换句话, 这个addr
对应的memory layout必须长的像一个JSObject
才行. 比如v = {a : 1, b : 2}
,
1 | >>> describe(v) |
这里提出两个小问题:
- 在哪里伪造fake object ?
- 伪造fake object时需要注意什么 ?
首先我们来回答第一个问题, 最简单的方法就是利用inline property storage. 比如我可以通过v.a = int2f(0x0100160000000127 - 2^48)
(注意Double
编码) 来构造如下memory layout:
1 | pwndbg> x/4gx 0x7f5998db0080 |
按道理, 你在butterfly上构造也是可以的. 简而言之你可以通过设置property或者elements来布置memory layout.
再来回答第二个问题. 伪造一个"能用的" object需要具备以下几个条件:
JSCell
header需要设置正确, 其中比较关键是structureId
, 其余的修饰字段, 可以申请一个合适的object然后复制粘贴. 一个object的structure决定我们可以如何操作这个object.- 如果需要设置butterfly pointer, 首先它是一个
Pointer
; 如果不需要设置butterfly pointer, 那么需要将其置为NULL.
这里面问题就是, 如果需要布置的data不是一个valid double, 我们应当如何操作呢? 比如写bufferfly pointer这里, 或者JSCell
对应不是像上面一样是一个valid double. 答案是我们可以通过某种indirect write的方式实现. 关于写NULL是有一个小trick的, 我们可以delete(v.b)
来实现.
0x06 Arbitrary Read and Write 原语
0x06.1 基本构造思路
思想来自于[2], 其包含的大致路线就是:
- 构造一个fake_object
- 通过fake_object控制victim_object的butterfly.
- 读写victim_object的property实现任意地址的读写.
其实这个构造过程有点绕, 需要花一点点图来帮助理解.
前面提到, 我们如果需要设置butterfly, 就需要写入一个合法的 Pointer
. 考虑我们将fake_object的butterfly设置成一个object的地址会发生什么? 参考下述图1:
这里我们做了这样几件事:
- 申请一个正常的victim_jsobject.
- 构造了一个user_jsobject (无butterfly), 在它的inline property storage上构造了一个fake_jsobject.
- 使得fake_jsobject的butterfly pointer为victim_jsobject.
当有这样一个结构之后, 我们可以尝试进行以下操作:
- 操作fake_jsobject的属性或者元素, 来修改victim_jsobject的victim_butterfly_pointer.
- 将victim_butterfly_pointer置为我们想要读写的目标内存地址.
- 操作victim_jsobject的属性或者元素, 来修改目标内存上的内容.
这就是我们的思路, 接下来就是填补里面的细节.
0x06.2 确定fake_jsobject和victim_jsobject
影响我们构造fake_jsobject的两个因素:
- victim_jsobject被视为一个butterfly.
- vitcim_butter_pointer位于
addr_of_victim_jsobject + 0x8
的位置.
那么我们最好的做法是通过fake_obj[1] = target_victim_butterfly_pointer
. 那么需要满足这样几个条件:
- fake_jsobject包含一个array, i.e.,
fake_obj = [1.1,2.2]
. - fake_structure_id要对应包含上述array的structure.
- fake_butterfly_pointer需要正好指向victim_jsobject.
满足这些条件的方法就是heap spraying. 参考来自[2]的heap spraying代码片段:
1 | var obj_arr = []; |
每一个obj
:
- 包含array, 用来构造fake_jsobject需要的structure.
- 一个固定属性, 用于操作victim_jsobject, 后面细说.
- 一个随机属性, 用于产生大量不同的structure, 进而占据大量structure ids.
我们将其中一个obj
也作为victim_jsobject.
假设上述任意一个obj
对应的JSObject
如下:
1 | >>> describe(obj_arr[0]) |
构造fake_jsobject过程如下:
1 | var convert = new ArrayBuffer(0x10); |
这里有几个问题:
这个
f2i
准不准? 因为javascript里面是没有int64的, 而double的有效位是53. 但是对于这里我们指针操作而言, 已经足够了, 因为指针的有效位是48.fake_obj
和obj
的description不是一致的(ArrayWithDouble
), 而是ArrayWithContiguous
. 因为后面在我们会在fake_obj
写入其他类型的值, 如果是ArrayWithDouble
, 会导致一个conversion, 这个conversion会出问题. 因为fake_obj
的butterfly (即victim_obj
) 并不是一个ArrayWithDouble
. 在[1]中构造中,fake_obj
的description使用了ArrayWithDouble
, 其实是有问题的.static const IndexingType ContiguousShape = 0x08;
static const IndexingType DoubleShape = 0x06;
fake_obj
有没有可能构造失败? 显然是有的, 因为fake_structure_id有可能选择失败. 那么有没有更safe的方法呢? 可以去看[3], Linus采用的方法是- 找一个native structure来利用, 比如
WebAssembly.Memory
, 使得我们这里的obj = new WebAssembly.Memory({inital: 0})
; - 设定一个起始fake_structure_id给fake_obj, 然后通过
fake_obj instanceof WebAssembly.Memory
来确定fake_obj
是否构造成功. - 如果不是
WebAssembly.Memory
, 则让fake_structure_id加1, 继续检查.
这个过程中Linus还有一些优化操作, 具体的可以查看[4].
- 找一个native structure来利用, 比如
我们可以来验证一下构造的fake_obj.
1 | >>> describe(fake_obj) |
可以看见fake_obj
和victim_obj
都具有我们预期的structure, 并且fake_obj
的butterfly pointer指向victim_obj
.
0x06.3 构造read64和write64
接下来我们构造任意地址上的read64和write64. 首先我们看一下victim_object
的butterfly布局
1 | pwndbg> x/4gx 0x7fa8000c5f30 |
当我们访问victim_object.a
时, 实际访问是地址victim_butterfly_pointer - 0x10处的值. 这意味着我们如果想要访问target_mem, 那么我们应该将victim_butterfly_pointer设置为target_mem + 0x10.
现在我们假设victim_butterfly_pointer根据target_mem设置正确了, 这里还有几个值得商榷的细节,
- 如果
victim_object.a
的值不是Double
, 比如是一个Pointer
, 我们如何读呢? 即我们如何将任意的值都转换成Double
读出来. - 反过来, 如果要写入值是一个地址, 如何让
victim_object.a
拥有一个Pointer
呢? 即我们如何将任意的值都转换成Double
写入.
我们依然参考[2]中的利用, 接下来过程可以称的上非常精彩.
1 | var unboxed = [13.37]; |
来一步步的解释下这里在干什么:
首先我们构造了
unboxed
和boxed
, 它分别是一个ArrayWithDouble
和ArrayWithContiguous
. 前面我提到过ArrayWithDouble
里面存储的都是unboxed value, 而ArrayWithContiguous
里面存储的是boxed value, 即正常的JSValue
.执行
fake_obj[1] = unboxed
使得victim_obj
的butterfly pointer指向了unboxed
.此时
victim_obj[1]
实际就是unboxed
的butterfly pointer, 而victim_obj
是一个ArrayWithDouble
, 所以shared_butterfly
就是unboxed
的butterfly地址.1
2
3
4
5
6>>> print(shared_butterfly)
140050293817352
pwndbg> p/x 140050293817352
$7 = 0x7f6000038008
>>> describe(unboxed)
Object: 0x7f7be0bcc060 with butterfly 0x7f6000038008同理, 再执行
fake_obj[1] = boxed
使得victim_obj
的butterfly pointer指向了boxed
.此时
victim_obj[1]
实际就是boxed
的butterfly pointer, 之后使得boxed
的butterfly pointer指向了unboxed
的butterfly.1
2
3
4>>> describe(unboxed)
Object: 0x7f7be0bcc060 with butterfly 0x7f6000038008 (Structure 0x7f7be1bf2a70:[Array, {}, ArrayWithDouble, Proto:0x7f7be1bc80a0]), StructureID: 98
>>> describe(boxed)
Object: 0x7f7be0bcc070 with butterfly 0x7f6000038008 (Structure 0x7f7be1bf2ae0:[Array, {}, ArrayWithContiguous, Proto:0x7f7be1bc80a0]), StructureID: 99最后我们将
fake_obj
的description恢复成ArrayWithDouble
, 使得我们将地址当做double的时候, 以原值存储.
用图表示就是
这样使得unboxed
和boxed
拥有了相同的butterfly, 也就意味着, 一个数据可以按照unboxed value或者boxed value来进行处理. 这样使得我们有了更加稳定的addrOf
和fakeObj
原语, 这一步构造我觉得非常精妙.
1 | var stage2 = { |
另外addrOf
原语不仅可以读Object的地址 (或者说Pointer
), 其他类型的值都是可以读的, 其值都是double形式返回. 那么我们的read64
即为
1 | stage2.read64 = function(where) { |
对于write64
如下
1 | stage2.write = function(where, what) { |
注意现在fakeObj
实际是可以封装任意的值, 将其封装为一个JSValue, 最后由以原值写入目标内存.
0x07 代码执行
常规手段, 找到RWX段, 写shellcode.
0x08 小问题
ubuntu20.04编译jsc出现的依赖问题
编译过程如下:
1 | Tools/gtk/install-dependencies |
遇到了libsrtp0-dev is not available
, 可以替换成libsrtp2-dev
.
引用
- JavaScript engine exploit(二),https://www.anquanke.com/post/id/183805
- NiklasB Exploit, https://github.com/niklasb/sploits/blob/master/safari/regexp-uxss.html
- Linus Exploit, https://github.com/LinusHenze/WebKit-RegEx-Exploit/blob/master/pwn.js
- Preparing for Stage 2 of a WebKit Exploit, https://liveoverflow.com/preparing-for-stage-2-of-a-webkit-exploit/
- The Apple Bug That Fell Near The WebKit Tree, https://www.zerodayinitiative.com/blog/2019/3/14/the-apple-bug-that-fell-near-the-webkit-tree