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

[原创] AliyunCTF 2024 - BadApple

2024-4-11 23:48
9424

@

目录

前言

依稀记得那晚被阿里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(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 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(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(DataFormat preferredFormat)>& prefix)

并且要求最后执行 if 分支,即 format == DataFormatJS 得成立,而 format 被赋值的语句如下:

1
2
3
4
5
6
7
void SpeculativeJIT::compileGetByValOnFloatTypedArray(Node* node, TypedArrayType type, const ScopedLambda(DataFormat preferredFormat)>& prefix)
{
 ......
    JSValueRegs resultRegs;
    DataFormat format;
    std::tie(resultRegs, format, std::ignore) = prefix(DataFormatDouble);
......

可以看到 format 的值为 compileGetByValOnFloatTypedArray 的第三个参数 prefix(其是一个 lambda 函数) 返回值 tuple 的第二个值。所以这里目标就很清楚了:

  • 调用 compileGetByValOnFloatTypedArray 函数
  • 传入的第三个参数 prefix 执行后返回值 tuple 的第二个元素为 DataFormatJS

[注意]看雪招聘,专注安全领域的专业人才平台!

最后于 2024-4-12 09:07 被XiaozaYa编辑 ,原因:
收藏
免费 4
支持
分享
赞赏记录
参与人
雪币
留言
时间
音货得福
+1
期待更多优质内容的分享,论坛有你更精彩!
2024-12-20 22:02
PLEBFE
为你点赞~
2024-5-31 01:26
Arahat0
为你点赞~
2024-4-12 11:42
ElegyYuan0x1
为你点赞~
2024-4-12 09:06
最新回复 (2)
雪    币: 3972
活跃值: (31426)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2024-4-12 09:22
1
雪    币: 3037
活跃值: (3270)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
3
牛的
2024-4-12 11:42
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册