Safari, Hold Still for NaN Minutes!

Introduction 介绍

In October 2023 Vignesh and Javier presented the discovery of a few bugs affecting JavaScriptCore, the JavaScript engine of Safari. The presentation revolved around the idea that browser research is a dynamic area; we presented a story of finding and exploiting three vulnerabilities that led to gaining code execution within Safari’s renderer. This blog post extends into the second vulnerability in more detail: the NaN bug.
2023 年 10 月,Vignesh 和 Javier 展示了一些影响 Safari 的 JavaScript 引擎 JavaScriptCore 的错误。演讲围绕着浏览器研究是一个动态领域的想法展开;我们介绍了一个发现并利用三个漏洞的故事,这些漏洞导致在 Safari 的渲染器中获得代码执行。这篇博文更详细地介绍了第二个漏洞:NaN 错误。

Safari, Hold Still for NaN Minutes!
At the conference 在会议上

NaN-boxing NaN-拳击

To understand why we gave the name “NaN bug” to this bug, we first need to understand the IEEE754 standard.  We shall also dive into how JSValues are represented in memory by means of a technique called “NaN-boxing”.
要理解为什么我们给这个 bug 起了个名字“NaN bug”,我们首先需要了解IEEE754标准。我们还将深入研究 s 是如何 JSValue 通过一种称为“NaN-boxing”的技术在记忆中表示的。

IEEE754

JavaScriptCore uses the IEEE Standard for Floating-Point Arithmetic (IEEE754). This standard serves the purpose of representing floating point values in memory. It does so by encoding, for example on a 64-bit value (double-precision floating-point format), data such as the sign, the exponent, and the significand. There are also 16-bit (half-precision) and 32-bit (single-precision) representations that are outside of the scope of this blog post.
JavaScriptCore 使用 IEEE 浮点运算 (IEEE754) 标准。该标准用于表示内存中的浮点值。它通过对 64 位值(双精度浮点格式)等数据进行编码来实现,例如在 64 位值(双精度浮点格式)上对符号、指数和有效数等数据进行编码。还有 16 位(半精度)和 32 位(单精度)表示形式,超出了本博客文章的范围。

Sign 标志 Exponent 指数 Significand 意义和
Bit 63 位 63 Bits 62-52 位 62-52 Bits 51-0 位 51-0

Depending on these bits, the calculation for the representation would be as follows.
根据这些位,表示的计算如下。

  • With exponent 0: (-1)**(sign bit) * 2**(1-1023) * 1.significand
    指数为 0: (-1)**(sign bit) * 2**(1-1023) * 1.significand

  • With exponent other than 0: (-1)**(sign bit) * 2**(exponent-1023) * 0.significand
    指数不是 0: (-1)**(sign bit) * 2**(exponent-1023) * 0.significand

  • With all bits of exponent set and significand is 0: (-1)**(sign bit)*Infinity
    当指数集的所有位且有效位为 0 时: (-1)**(sign bit)*Infinity

  • With all bits of exponent set and significand not 0: Not a number (NaN)
    当指数集的所有位且有效位不为 0: Not a number ( NaN )

The reason why 1023 is used on the exponent is because it is encoded using an offset-binary representation which aides in implementing negative numbers with 1023 as the zero offset. In order to understand offset-binary representation, we can picture an example with a 3 digit binary exponent. In this representation it would be possible to encode up to number 7 and the offset would be 4
1023 之所以在指数上使用,是因为它使用偏移二进制表示进行编码,这有助于实现以 1023 为零偏移的负数。为了理解偏移二进制表示,我们可以用 3 位二进制指数来描绘一个示例。在这种表示中,最多可以编码到数字 7 ,偏移量为 4

(2**2). This way we would encode the number 0 as (2**1) in this offset-binary representation and therefore the encoded range would be (-4, 3) corresponding to the binary range of (000111).
( 2**2 )。这样,我们就可以将这个偏移二进制表示中的数字 0 编码为 ( 2**1 ),因此编码范围 (-4, 3) 将对应于 ( 000 , 111 ) 的二进制范围。

NaN

If all the bits of the exponent on the IEE754 standard representation are set, it describes a value that is not a number (NaN). These values are described in the standard as a way to establish values that are either undefined or unrepresentable. In addition, there exist Quiet and Signaling NaN values (QNaNsNaN) which serve the purpose of either notifying of a normal undefined or unrepresentable value or, in the case of a signaling NaN, a representation to add diagnostics info (other data encoded in the payload of the value).
如果设置了 IEE754 标准表示上指数的所有位,则它描述的值不是数字 ( NaN )。这些值在标准中被描述为建立未定义或无法表示的值的一种方式。此外,还存在 Quiet 和 Signaling NaN 值 ( , ),它们用于通知正常的未定义或无法表示的值,或者在信令 NaN 的情况下 QNaN , sNaN 用于添加诊断信息(在值的有效负载中编码的其他数据)的表示。

There are 2**51 possible values we can encode in the payload of the NaN number in the double-precision floating-point format. This allows a huge value space for implementers to encode all sorts of information. In hexadecimal, this range would be any values between 0xFFF0000000000000 and 0xFFFFFFFFFFFFFFFF.
我们可以在 NaN 双精度浮点格式的数字有效载荷中对一些 2**51 可能的值进行编码。这为实现者提供了巨大的价值空间来对各种信息进行编码。在十六进制中,此范围是介于 和 0xFFFFFFFFFFFFFFFF 之间的 0xFFF0000000000000 任何值。

Specifically, JavaScriptCore uses NaN values to encode different types of information.
具体来说,JavaScriptCore 使用 NaN 值对不同类型的信息进行编码。

JSValue

Most JavaScript engines choose to represent JavaScript objects in memory in a way that enables efficient handling of the values. JavaScriptCore is no exception, and to do so, it backs up JavaScript objects with the C class JSValue. It is possible to find a detailed explanation on how values in the JavaScript engine are encoded in JavaScriptCore within the file Source/JavaScriptCore/runtime/JSCJSValue.h:
大多数 JavaScript 引擎选择在内存中表示 JavaScript 对象,以便能够有效处理值。JavaScriptCore也不例外,为此,它使用 C 类 JSValue 备份 JavaScript 对象。可以在文件 Source/JavaScriptCore/runtime/JSCJSValue.h 中找到有关如何在 JavaScript 引擎中以 JavaScriptCore 编码的值的详细说明:

*     Pointer {  0000:PPPP:PPPP:PPPP
*              / 0002:****:****:****
*     Double  {         ...
*              \ FFFC:****:****:****
*     Integer {  FFFE:0000:IIII:IIII

Raw pointers keep their upper bits (16 most-significant bits) at 0. Other specific values such as Booleannull and undefined values share the same 0x0000 tag:
原始指针将其上限位(16 个最高有效位)保持在 0。其他特定值(如 Boolean 和 null undefined values)共享相同的 0x0000 标记:

*     False:     0x06
*     True:      0x07
*     Undefined: 0x0a
*     Null:      0x02

Doubles start with the upper 16-bit at 0x0002... and end with the upper 16-bit at 0xFFFC.... This is encoded by adding the constant 2**49 (0x0002000000000000) to all double values. After this addition, no double-precision value begins with 0x0000 or 0xFFFE tags. If further manipulation is required, this constant (2**49) should be subtracted before performing operations on double-precision numbers.
双精度以 的上 16 位开始,以 的上 16 位 结束 0x0002... 0xFFFC... 。这是通过将常量 2**49 ( 0x0002000000000000 ) 添加到所有双精度值来编码的。添加此值后,不会有以 0x0000 或 0xFFFE 标记开头的双精度值。如果需要进一步操作,则在对双精度数字执行操作之前,应减去此常量 ( 2**49 )。

Integers have the upper 16-bit set to 0xFFFE..., only using the 32 least-significant bits for the actual integer values.
整数的上限 16 位设置为 0xFFFE... ,仅使用 32 个最低有效位作为实际整数值。

gef>  r
Starting program: ./jsc
>>> let obj = {f: 1.1}
undefined

[1]

>>> describe(obj);
"Object: 0x7fb9d34e0000 with butterfly (nil)(base=0xfffffffffffffff8) (Structure 0x7fb9d34d49a0:[0xe8b/3723, Object, (1/2, 0/0){f:0}, NonArray, Proto:0x7fba1501d8e8, Leaf]), StructureID: 3723"

[2]

gef>  x/32gx 0x7fb9d34e0000
0x7fb9d34e0000:	0x0100180000000e8b	0x0000000000000000
0x7fb9d34e0010:	0x3ff399999999999a	0x0000000000000000

[3]

gef>  p/x 0x3ff399999999999a - 0x0002000000000000 # 2**49
$1 = 0x3ff199999999999a
gef➤  p/f 0x3ff199999999999a
$2 = 1.1000000000000001

After defining an object with a float property f of value 1.1 we use the runtime debugging function describe to obtain the address in memory of the declared object [1]. Note that the object’s butterfly is nil. For other cases, for example arrays, this butterfly pointer would be the elements pointer – for (a lot) more information on these terms refer to this WebKit Blog. By inspecting the aforementioned object address in the debugger, at offset 0x10 the encoded double-precision value is retrieved [2]. By following the previous encoding of subtracting 2**49 from the value [3], the original double-precision value 1.1 is retrieved.
在定义一个具有 float 1.1 属性 f 的对象后,我们使用运行时调试函数 describe 来获取声明对象 [1] 的内存地址。请注意,对象的蝴蝶是 nil 。对于其他情况,例如数组,这个蝴蝶指针将是元素指针——有关这些术语的(很多)更多信息,请参阅此 WebKit 博客。通过在调试器中检查上述对象地址,在偏移0x10处检索编码的双精度值 [2]。通过遵循前面从值 [3] 2**49 中减去的编码,可以检索原始的双精度值 1.1 。

In the source code, there are helper constants to perform such manipulation of integer and double-precision values.
在源代码中,有帮助程序常量来执行整数和双精度值的此类操作。

// This value is 2^49, used to encode doubles such that the encoded value will begin
// with a 15-bit pattern within the range 0x0002..0xFFFC.
static constexpr size_t DoubleEncodeOffsetBit = 49;
static constexpr int64_t DoubleEncodeOffset = 1ll << DoubleEncodeOffsetBit;
// 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;

The “NaN-boxing” techniques effectively use the payload in a NaN value to box information within the value itself, hence the name “NaN-boxing”. One of the key points of the vulnerability described within this blog post relies on abusing such encoding techniques. If an attacker were to provide unsanitized double-precision values starting at 0xFFFE..., once the engine tried to encode and store such a value by adding the 2**49 constant, the value would end up as 0xFFFE000000001234 + 2**49 = 0x0000000000001234 as it overflows, resulting in the 0x0000 tag, which corresponds to a raw pointer to 0x1234.
“NaN-boxing”技术有效地使用 NaN 值中的有效载荷来装箱值本身内的信息,因此得名“NaN-boxing”。这篇博文中描述的漏洞的关键点之一依赖于滥用此类编码技术。如果攻击者提供从 开始 0xFFFE... 的未经审查的双精度值,一旦引擎尝试通过添加 2**49 常量来编码和存储此类值,该值最终 0xFFFE000000001234 + 2**49 = 0x0000000000001234 将溢出,从而导致标记,该 0x0000 标记对应于 0x1234 指向 的原始指针。

Vulnerability 脆弱性

Optimizing Compilers: DFG & FTL
优化编译器:DFG 和 FTL

DFG (Data Flow Graph) and FTL (Faster Than Light) are two of JavaScriptCore’s Just-in-Time (JIT) Optimizing Compilers. In case these concepts are new, reading about them beforehand would make understanding the following vulnerability details easier. JIT compilers have been extensively written about, including on Vignesh’s post on another Safari vulnerability.
DFG(数据流图)和 FTL(比光速更快)是 JavaScriptCore 的两个即时 (JIT) 优化编译器。如果这些概念是新的,请事先阅读它们,以便更轻松地理解以下漏洞详细信息。JIT编译器已经被广泛地写过,包括Vignesh关于另一个Safari漏洞的帖子。

Vulnerability Details 漏洞详情

The vulnerability that we are going to discuss arises from the manner in which JavaScriptCore’s DFG JIT and FTL JIT optimize and compile fetching an element from a Floating point typed array. For the purpose of this blog post, we will be primarily looking at the DFG JIT code, however this same issue also existed in FTL.
我们将要讨论的漏洞源于 JavaScript Core 的 DFG JIT 和 FTL JIT 优化和编译从浮点类型数组中获取元素的方式。出于这篇博文的目的,我们将主要研究DFG JIT代码,但是FTL中也存在同样的问题。

Consider the following JavaScript code.
请考虑以下 JavaScript 代码。

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

In the second line the float_array[0] is fetching an element from the floating point typed array. If such a statement were to be compiled by the DFG compiler, the function in the compiler responsible for converting the DFG IR into native assembly would be SpeculativeJIT::compileGetByValOnFloatTypedArray from the file Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp. Let’s take a look at the this function.
在第二行中,正在 float_array[0] 从浮点类型数组中获取元素。如果这样的语句是由 DFG 编译器编译的,则编译器中负责将 DFG IR 转换为本机程序集的函数将来自 SpeculativeJIT::compileGetByValOnFloatTypedArray 文件 Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp 。我们来看看这个函数。

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();

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

    emitTypedArrayBoundsCheck(node, baseReg, propertyReg, scratchGPR);
    switch (elementSize(type)) {
    case 4:
        m_jit.loadFloat(MacroAssembler::BaseIndex(storageReg, propertyReg, MacroAssembler::TimesFour), resultReg);
        m_jit.convertFloatToDouble(resultReg, resultReg);
        break;
    case 8: {

        // [1]

        m_jit.loadDouble(MacroAssembler::BaseIndex(storageReg, propertyReg, MacroAssembler::TimesEight), resultReg);
        break;
    }
    default:
        RELEASE_ASSERT_NOT_REACHED();
    }

    // [2]

    if (format == DataFormatJS) {

        // [3]

        m_jit.boxDouble(resultReg, resultRegs);
        jsValueResult(resultRegs, node);
    } else {
        ASSERT(format == DataFormatDouble);
        doubleResult(resultReg, node);
    }
}

From the code snippet above, we can see that if the element size is 8 bytes, which means that the array we are accessing is a Float64Array and not a Float32Array, then at [1], the element is loaded from an index in the array into a temporary register (resultReg in the above snippet). At [2], the format parameter is checked. This parameter is telling the compiler about the type in which the loaded float is going to be used. If the compiler thinks that the loaded value is going to be used as a float in the future, then there is no need to convert it into a JSValue. In this case, the value of the format variable will be DataFormatDouble. However, if the compiler thinks that the float value that is loaded from the array is going to be used as a JSValue, then it has to convert this float into a JSValue.
从上面的代码片段中,我们可以看到,如果元素大小为 8 个字节,这意味着我们正在访问的数组是 a Float64Array 而不是 a Float32Array ,那么在 [1] 处,元素从数组中的索引加载到临时寄存器中( resultReg 在上面的代码段中)。在 [2] 处,检查 format 参数。此参数告诉编译器将使用加载的浮点数的类型。如果编译器认为加载的值将来将用作浮点数,则无需将其转换为 JSValue。在本例中, format 变量的值将为 DataFormatDouble 。但是,如果编译器认为从数组加载的浮点值将用作 JSValue,则必须将此浮点数转换为 JSValue。

As we saw in previous sections, to convert a raw double into a JSValue double, the engine adds 2**49 to the raw double. The code to do this is provided by the boxDouble() function. Therefore, if the value of the format variable is DataFormatJS, then the control reaches [3], where the boxDouble function is called with resultReg as the first argument, which contains the double element that was loaded from the array at [1]. The following listing shows the boxDouble() function.
正如我们在前面的部分中看到的,为了将原始双精度转换为 JSValue 双精度,引擎将 2**49 添加到原始双精度。执行此操作的代码由 boxDouble() 函数提供。因此,如果 format 变量的值为 DataFormatJS ,则控件到达 [3],其中调用函数 boxDouble resultReg 作为第一个参数,其中包含从 [1] 处的数组加载的 double 元素。下面的清单显示了该 boxDouble() 函数。

// File - Source/JavaScriptCore/jit/AssemblyHelpers.h
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);

    // [2]

    if (mode == DoNotHaveTagRegisters)
        sub64(TrustedImm64(JSValue::NumberTag), gpr);
    else {
        sub64(GPRInfo::numberTagRegister, gpr);
        jitAssertIsJSDouble(gpr);
    }
    return gpr;
}

The double value is moved into a General Purpose Register (gpr) at [1] and then converted into a JSValue at [2]. In order to convert the double to a JSValue, the value JSValue::NumberTag is subtracted from the double value. The JSValue::NumberTag is the constant value 0xfffe000000000000 as can be seen in the Source/JavaScriptCore/runtime/JSCJSValue.h file.
double 值在 [1] 处移动到通用寄存器 (gpr) 中,然后在 [2] 处转换为 JSValue。为了将 double 转换为 JSValue,需要从 double 值中减去该值 JSValue::NumberTag 。这是 JSValue::NumberTag 常量值 0xfffe000000000000 , Source/JavaScriptCore/runtime/JSCJSValue.h 如文件中所示。

The interesting part to note here is that the result of the subtraction is never checked for an integer overflow. In an ideal case, it should never overflow because in order for it to overflow the 49th bit of the double value should be set which will make it an invalid double or in other words, a NaN value. There can be multiple values for NaN, but JavaScriptCore has one representation for it and uses the value 0x7ff8000000000000, which it calls pureNaN, to represent NaN. Hence, if the argument for the boxDouble function is coming from a previous JSValue then this subtraction can never overflow.
这里需要注意的有趣部分是,从不检查减法结果是否存在整数溢出。在理想情况下,它不应该溢出,因为为了使它溢出,应该设置双精度值的第 49 位,这将使它成为无效的双精度值,或者换句话说,一个 NaN 值。NaN 可以有多个值,但 JavaScriptCore 有一个表示形式,并使用它调用 pureNaN 的值 0x7ff8000000000000 来表示 NaN 。因此,如果 boxDouble 函数的参数来自以前的 JSValue,那么这个减法永远不会溢出。

However, if the argument to this function is a raw, user-controlled value, then the subtraction can overflow. For example, if our input to this function (fpr in the above snippet) has the value 0xfffe000012345678, then the subtraction will follow the following course:
但是,如果此函数的参数是原始的、用户控制的值,则减法可能会溢出。例如,如果我们对此函数的输入( fpr 在上面的代码片段中)的值为 0xfffe000012345678 ,则减法将遵循以下过程:

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

As we can see, subtraction with  0xfffe000000000000 is same as addition with 2**49. In the end, the gpr ends up as a fully controlled value with all the top bits unset. However, as we discussed in the NaN-Boxing section, a JSValue with all the top bits unset represents a JSObject pointer. Therefore if we manage to control the first argument, fpr, then we can craft a pointer and get JSC into believing that this is a valid pointer to a JSObject. This works because when DFG emits the code to load a value from a Float64Array, which holds raw doubles, it never checks if the double is an “impure NaN” or in other words, if the double is a NaN value but not the pure NaN value of 0x7ff8000000000000. Due to this we can point to anywhere in memory and the engine will read such a pointer as a JS object. Effectively resulting in a straight fakeobj primitive from this bug.
正如我们所看到的,用 减法 与用 0xfffe000000000000 2**49 加法相同。最后,最终 gpr 成为一个完全受控的值,所有顶部位均未设置。但是,正如我们在 NaN-Boxing 部分中讨论的那样,所有顶部位均未设置的 a JSValue 表示指 JSObject 针。因此,如果我们设法控制第一个参数,那么我们可以制作一个指针, fpr 让 JSC 相信这是一个指向 . JSObject 这之所以有效,是因为当 DFG 发出代码以从 Float64Array 保存原始双精度值的 加载值时,它从不检查双精度值是否为“不纯 NaN”,或者换句话说,双精度值是否为 NaN 值,而不是 0x7ff8000000000000 的纯 NaN 值。因此,我们可以指向内存中的任意位置,引擎将读取这样的指针作为 JS 对象。有效地从这个错误中产生了一个直接 fakeobj 的原语。

Path to trigger the bug
触发 bug 的路径

Now that we see what the bug is, let’s take a look at how it can be reached from JavaScript. In order to hit the bug, we will need to make use of the for…in enumeration in JavaScript.
现在我们了解了错误是什么,让我们来看看如何从 JavaScript 访问它。为了击中错误,我们需要利用…在 JavaScript 中的枚举中。

Take a look at the following code that shows a JS for-in loop, which will enumerate all the property names of the obj object.
请看以下代码,该代码显示了一个 JS for-in 循环,该循环将枚举对象 obj 的所有属性名称。

obj = {x:1, y:1}
function forin(arg) {
    for (let i in obj) {

        // [1]
        let out = arg[i];
    }
}

At [1], the value of the currently enumerated property name (i variable in the snippet) is fetched from the arg object. When the code is being JIT compiled, [1] will be represented by the DFG IR opcode EnumeratorGetByVal. When this opcode is compiled into assembly code in the DFG JIT compiler, it reaches the following piece of code.
在 [1] 处,从 arg 对象中获取当前枚举的属性名称(代码段中的 i 变量)的值。当代码被JIT编译时,[1]将由DFG IR操作码 EnumeratorGetByVal 表示。当此操作码在 DFG JIT 编译器中编译为汇编代码时,它会到达以下一段代码。

// File - Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp
case EnumeratorGetByVal: {
    compileEnumeratorGetByVal(node);
    break;
}

As we can see, this is just calling the compileEnumeratorGetByVal() function which contains the logic to convert this opcode into native assembly. Let’s look at the definition of this function.
正如我们所看到的,这只是调用包含将此操作码转换为本机程序集的逻辑 compileEnumeratorGetByVal() 的函数。让我们看一下这个函数的定义。

// File - Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
void SpeculativeJIT::compileEnumeratorGetByVal(Node* node)
{
    Edge baseEdge = m_graph.varArgChild(node, 0);
    auto generate = [&] (JSValueRegs baseRegs) {

[TRUNCATED]

[1]

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

[TRUNCATED]

            notFastNamedCases.link(&m_jit);

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

[TRUNCATED]
    };

    if (isCell(baseEdge.useKind())) {
        // Use manual operand speculation since Fixup may have picked a UseKind more restrictive than CellUse.
        SpeculateCellOperand base(this, baseEdge, ManualOperandSpeculation);
        speculate(node, baseEdge);
        generate(JSValueRegs::payloadOnly(base.gpr()));
    } else {
        JSValueOperand base(this, baseEdge);
        generate(base.regs());
    }

The compileEnumeratorGetByVal() calls the generate() closure. This closure calls the compileGetByVal() function at [1]. This function
称为 compileEnumeratorGetByVal() generate() 闭包。此闭包调用位于 [1] 的 compileGetByVal() 函数。此功能

is responsible for handling the compilation of all indexed accesses from all types of arrays. The compileEnumeratorGetByVal() calls this function informing it to handle all the indexed accesses in the enumerator loop. This is done using the lambda function that is passed as an argument to compileGetByVal(). At [2], the lambda returns a tuple, the first value being the register where the current value of the indexed load is to be stored and the second value being the format in which it should be stored. As we can see, the second value is always a constant – DataFormatJS – informing that the loaded value is always to be stored in the JSValue format.
负责处理来自所有类型数组的所有索引访问的编译。 compileEnumeratorGetByVal() 调用此函数,通知它处理枚举器循环中的所有索引访问。这是使用 lambda 函数完成的,该函数作为参数传递给 compileGetByVal() 。在 [2] 处,lambda 返回一个元组,第一个值是要存储索引负载的当前值的寄存器,第二个值是存储该值的格式。正如我们所看到的,第二个值始终是一个常量 – DataFormatJS – 通知加载的值始终以 JSValue 格式存储。

In case arg in the JS snippet above is the floating point typed array Float64Array, then the following parts of the compileGetByVal() function will be executed:
如果 arg 上面的 JS 代码段是浮点类型数组 Float64Array ,则将执行 compileGetByVal() 函数的以下部分:

// File - Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp
void SpeculativeJIT::compileGetByVal(Node* node, const ScopedLambda<std::tuple<JSValueRegs, DataFormat, CanUseFlush>(DataFormat preferredFormat)& prefix)
{
    switch (node-arrayMode().type()) {

// [TRUNCATED]

// [1]

    case Array::Float32Array:
    case Array::Float64Array: {
        TypedArrayType type = node-arrayMode().typedArrayType();
        if (isInt(type))
            compileGetByValOnIntTypedArray(node, type, prefix);
        else

// [2]

            compileGetByValOnFloatTypedArray(node, type, prefix);
    } }
}

If the array that is being accessed is a Float64Array array, then the function that is called is compileGetByValOnFloatTypedArray(), which is the vulnerable function. The next important point is that the compileEnumeratorGetByVal() function is saying that the result of the element fetch is to be stored in the JSValue format using the return value of the lambda that we saw above. In this manner, our vulnerable function is called with a Floating Point Typed Array that we control, and with the compiler being told that the value being fetched is to be converted from a raw double to a JSValue double. Keep in mind that the values in Floating Point Typed arrays can be made into “impure NaN” values by changing the underlying array buffer contents using a typed array of another type as shown below:
如果正在访问的数组是 Float64Array 数组,则调用的函数是 compileGetByValOnFloatTypedArray(),  易受攻击的函数。下一个重要的一点是,该 compileEnumeratorGetByVal() 函数表示元素获取的结果将使用我们上面看到的 lambda 的返回值以 JSValue 格式存储。以这种方式,我们易受攻击的函数使用我们控制的浮点类型数组进行调用,并告知编译器正在获取的值将从原始双精度转换为 JSValue 双精度。请记住,通过使用另一种类型的类型化数组更改基础数组缓冲区内容,可以将浮点类型化数组中的值转换为“不纯 NaN”值,如下所示:

let abuf       = new ArrayBuffer(0x10);
let bigint_buf = new BigUint64Array(abuf);
let float_buf  = new Float64Array(abuf);

bigint_buf[0] = 0xfffe_0000_0000_0000;

After the above snippet is run, the raw float in float_buf[0] will be 0xfffe_0000_0000_0000. Using this value we can trigger the bug to trick the JS engine to think that an arbitrary number is a pointer to a JSObject.
运行上述代码段后,原始浮点数 float_buf[0] 将为 0xfffe_0000_0000_0000 。使用此值,我们可以触发错误来欺骗 JS 引擎,使其认为任意数字是指向 JSObject .

In summary, the boxDouble() function assumes that the double value that is passed to it as an argument is a valid double value or a “pure NaN” (0x7ff8000000000000) and has no checks to verify that the result did not overflow. Hence, it is the job of the caller to ensure this condition is satisfied before calling this function. If there is a call site that does not respect this and directly calls this function with a raw user controlled double value, then the attacker can gain full control of a JSValue and fake a pointer to a JSObject by using the overflow to build a very powerful fakeobj primitive.
总之,该 boxDouble() 函数假定作为参数传递给它的双精度值是有效的双精度值或“纯 NaN”( 0x7ff8000000000000 ),并且没有检查来验证结果是否未溢出。因此,调用方的工作是在调用此函数之前确保满足此条件。如果有一个调用站点不遵守这一点,而是直接使用原始用户控制的双精度值调用此函数,那么攻击者可以通过使用溢出来构建一个非常强大的 fakeobj 原语,从而获得对 a 的完全控制并伪造指向 a JSValue JSObject 的指针。

The compileGetByValOnFloatTypedArray() function does not check that the raw double fetched from a Float64Array is indeed a valid float or not. It just blindly passes it to the boxDouble() function at [3] which makes this vulnerable to the technique described above. If an attacker can trigger this code path, it is possible to achieve the fake object primitive as shown above.
该 compileGetByValOnFloatTypedArray() 函数不会检查从 a Float64Array 获取的原始双精度是否确实是有效的浮点数。它只是盲目地将它传递给 [3] 处的 boxDouble() 函数,这使得它容易受到上述技术的影响。如果攻击者可以触发此代码路径,则有可能实现如上所示的假对象原语。

Triggering the Bug 触发 Bug

Finally let’s look at the full JavaScript trigger for this bug:
最后,让我们看一下这个错误的完整 JavaScript 触发器:

let abuf = new ArrayBuffer(0x10);
let bbuf = new BigUint64Array(abuf);
let fbuf = new Float64Array(abuf);

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

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

function main() {

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

// [1]
    for (let i = 0 ; i < 0x1000; i++) {
      trigger(fbuf,t);
    }

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

// [3]
    t.x;
}

main()

In the above PoC, the trigger() function is the one that will trigger the vulnerability. At [1] we call the trigger() function in a loop with a  Float64Array that contains a normal benign float – that is no impure NaNs. This is done to train the compiler into emitting the code we want. After this, at [2], we use a BigUint64Array to change the first element of the Float64Array to an impure NaN. Then we call the trigger() function again. This time the bug will trigger and the engine will think that 0x12345678 is a pointer to a valid JSObject. This JSValue is stored in t.x and when we return from the function, we access t.x at [3]. This causes the engine to dereference the pointer which obviously points to an invalid address and crashes the engine while accessing 0x12345678.
在上面的PoC中,函数 trigger() 是会触发漏洞的函数。在 [1] 中,我们在一个循环中调用该 trigger() 函数, Float64Array  其中包含一个正常的良性浮点数——这不是不纯的 NaNs。这样做是为了训练编译器发出我们想要的代码。在此之后,在 [2] 处,我们使用 a BigUint64Array 将 的第一个 Float64Array 元素更改为不纯的 NaN。然后我们再次调用该 trigger() 函数。这一次,错误将被触发,引擎会认为这是 0x12345678 指向有效 JSObject .它 JSValue 存储在 [3] 中 t.x ,当我们从函数返回时,我们访问 t.x [3]。这会导致引擎取消引用指针,该指针显然指向无效地址,并在访问 0x12345678 时使引擎崩溃。

Bypassing ASLR 绕过 ASLR

While we have a fakeobj primitive from the bug, we still are constrained by the fact that we don’t have an ASLR bypass and hence can’t fake anything without crashing the engine. However, when we were researching a different case on JSC, we saw some interesting DFG IR.
虽然我们有一个来自错误的 fakeobj 原语,但我们仍然受到这样一个事实的限制,即我们没有 ASLR 旁路,因此无法在不使引擎崩溃的情况下伪造任何东西。然而,当我们在JSC上研究一个不同的案例时,我们看到了一些有趣的DFG IR。

Safari, Hold Still for NaN Minutes!
CompareStrictEq opcode and assembly – type checking
CompareStrictEq 操作码和汇编 – 类型检查

The image shows the assembly that is emitted by the CompareStrictEq IR opcode, which is used to denote JavaScript’s Strict Equality operation in DFG IR. In this case, the LHS (Left Hand Side) D@27 is being compared against the RHS (Right Hand Side) D@34. From the above image, we see that the LHS is not typed – which means that the DFG JIT compiler did not make any assumptions on its type. We can also see that the RHS is typed to Object. This means that the compiler assumes that in this case, the RHS of the === operation is assumed to be a Javascript object by the compiler and it has to verify that this assumption holds. Again, from the image we can see that the compiler has indeed emitted checks to make sure that the type of RHS is checked.
该图显示了 CompareStrictEq IR 操作码发出的程序集,该操作码用于表示 DFG IR 中 JavaScript 的严格相等操作。在这种情况下,LHS(左侧)与 RHS(右侧) D@27 D@34 进行比较。从上图中,我们看到 LHS 没有被类型化——这意味着 DFG JIT 编译器没有对其类型做出任何假设。我们还可以看到 RHS 的类型为 Object .这意味着编译器假定在这种情况下,编译器假定 === 操作的 RHS 是 Javascript 对象,并且必须验证此假设是否成立。同样,从图像中我们可以看到编译器确实发出了检查,以确保检查了 RHS 的类型。

After the type of the RHS is checked, we can see the actual logic for comparing LHS and RHS as in the image below. The code simply compares LHS and RHS with the x86 cmp instruction (this code was generated on an x86-64 Linux machine). This means that in case LHS is not a valid pointer it can still get checked against the pointer to a valid object. Also the return value of this can be read in JavaScript. Therefore we can compare an invalid pointer with a valid pointer and check to see if they are equal without triggering any crash or abnormal behaviour. These are the perfect ingredients for brute-forcing an address! We can use our fakeobj primitive from the NaN bug to get the engine into believing that arbitrary numbers that we control are actually pointers. Then we can compare this fake invalid pointer against a valid one. If the result is true, then we just correctly guessed the address of the valid pointer. Else we update the invalid pointer to a new value and then rinse and repeat the procedure.
检查 RHS 类型后,我们可以看到比较 LHS 和 RHS 的实际逻辑,如下图所示。该代码只是将 LHS 和 RHS 与 x86 cmp 指令进行比较(此代码是在 x86-64 Linux 计算机上生成的)。这意味着,如果 LHS 不是有效指针,它仍然可以根据指向有效对象的指针进行检查。此外,可以在 JavaScript 中读取 this 的返回值。因此,我们可以将无效指针与有效指针进行比较,并检查它们是否相等,而不会触发任何崩溃或异常行为。这些是暴力破解地址的完美成分!我们可以使用来自 NaN bug 的 fakeobj 原语来让引擎相信我们控制的任意数字实际上是指针。然后我们可以将这个假的无效指针与有效的指针进行比较。如果结果为真,那么我们只是正确地猜到了有效指针的地址。否则,我们将无效指针更新为新值,然后冲洗并重复该过程。

Safari, Hold Still for NaN Minutes!
CompareStrictEq – pointer comparison
CompareStrictEq – 指针比较

In this way we have a mechanism to use the bug to brute force and find the address of an object pointer in memory. While this technique works, it is also extremely slow taking more than an hour to brute force 32-bits on an M1 mac. Hence its necessary to
这样,我们就有了一种机制,可以使用错误来暴力破解并在内存中查找对象指针的地址。虽然这种技术有效,但在 M1 Mac 上暴力破解 32 位也非常慢,需要一个多小时。因此,有必要

improve it to get it to run faster.
改进它以使其运行得更快。

Optimizing the Brute Force
优化暴力破解

Initially we were brute forcing the pointer with something like this:
最初,我们用这样的内容暴力破解指针:

let object_to_leak = {p1: 0x1337, p2: 0x1337};

for (let i=0n; i<0xffff_ffffn; i+=1n) {
    let fake_pointer = fakeobj(i);
    let result = brute_force(fake_pointer, object_to_leak);

    if (result) {
        print('Found the address at: '+ hex(i));
        break;
    }
}

The first issue with the above is that the address is incremented by one on each loop iteration in the brute force loop. It’s given that the address of any object will be aligned to a multiple of 8. Hence, instead of single stepping in the for loop, an addition of 8 can be done to the loop variable after each iteration. This will give a significant 8x speed up over the original PoC without making any additions assumptions. However, this is still too slow for a browser exploit especially seeing that the iOS and MacOS architectures have 64-bit pointers and not 32-bit.
上述的第一个问题是,在暴力循环中,地址在每次循环迭代时都会递增 1。给定任何对象的地址都将与 8 的倍数对齐。因此,可以在每次迭代后对循环变量添加 8,而不是在 for 循环中进行单步执行。这将比原始 PoC 显着提高 8 倍的速度,而无需进行任何附加假设。但是,对于浏览器漏洞来说,这仍然太慢了,尤其是看到 iOS 和 MacOS 架构具有 64 位指针而不是 32 位指针。

We observed that on MacOS and iOS the JavaScriptCore heap addresses were always 5 bytes (40 bits) long. Another observation was that, if the exploit is run on a JS worker, and an object is created at the very beginning of the exploit, then the address of the object was always page aligned which means that the last 12 bits of the address of the object were always zero. Using these observations can greatly speed up the brute force as now the object whose address is to be leaked, can be created at the beginning of the exploit before any other object has been initialized, and then, in the brute force loop, the loop variable can be stepped over by 0x1000 instead of 1 or 8 giving a 4096x speed up over the original PoC. This is a huge speed up and now a 5-byte address can be brute forced in seconds.
我们观察到,在 MacOS 和 iOS 上,JavaScriptCore 堆地址的长度始终为 5 个字节(40 位)。另一个观察结果是,如果漏洞利用在 JS worker 上运行,并且在漏洞利用的一开始就创建了一个对象,那么该对象的地址始终是页面对齐的,这意味着该对象地址的最后 12 位始终为零。使用这些观察可以大大加快暴力破解速度,因为现在可以在初始化任何其他对象之前在漏洞利用开始时创建地址要泄露的对象,然后,在暴力循环中,循环变量可以单步执行, 0x1000 而不是 1 或 8,比原始 PoC 加速 4096 倍。这是一个巨大的速度提升,现在一个 5 字节的地址可以在几秒钟内被暴力破解。

Summary 总结

The bug we discussed arose from the fact that DFG and FTL loaded a raw double from a typed array and proceeded to convert it into a JSValue double without verifying that the raw double was indeed a valid double or a pure NaN. This led us to achieve a fakeobj primitive whereby we could get the engine to think that any address we wanted is a pointer to a JSObject. After that we used JIT compiled code to brute force ASLR, using the fakeobj primitive, to leak the address of an object. This could be turned into a full addrof primitive, which can leak the address of any JSObject. Using a fakeobj and an addrof primitive, it is possible to achieve arbitrary read/write in the Safari renderer process.
我们讨论的 bug 源于以下事实:DFG 和 FTL 从类型化数组中加载了一个原始双精度值,并在未验证原始双精度值确实是有效的双精度值或纯 NaN 的情况下将其转换为 JSValue 双精度值。这导致我们实现了一个 fakeobj 原始的,我们可以让引擎认为我们想要的任何地址都是指向 JSObject .之后,我们使用 JIT 编译代码来暴力破解 ASLR,使用 fakeobj 原语来泄露对象的地址。这可以变成一个完整的 addrof 原语,它可以泄漏任何 JSObject 的地址。使用 a fakeobj 和 addrof an 基元,可以在 Safari 渲染器进程中实现任意读/写。

Conclusion 结论

The vulnerabilities discussed in this blog post and the referenced conference talk were introduced due to Apple performing large code commmits in the JavaScriptCore repository, specifically to optimize the for-in functionality of JavaScript. Browsers are ever-evolving large pieces of software, with many modules being added and stripped continually. Smart fuzzing and source-code audits are gradually being adoped into the software development lifecycle at large vendors, but they haven’t yet caught up to the offensive research industry.
这篇博文和引用的会议演讲中讨论的漏洞是由于 Apple 在 JavaScriptCore 存储库中执行大型代码指令而引入的,特别是为了优化 JavaScript for-in 的功能。浏览器是不断发展的大型软件,许多模块不断添加和剥离。智能模糊测试和源代码审计正逐渐被大型供应商纳入软件开发生命周期,但它们还没有赶上攻击性研究行业。

 

原文始发于Vignesh Rao and Javier Jimenez:Safari, Hold Still for NaN Minutes!

版权声明:admin 发表于 2023年12月12日 下午8:35。
转载请注明:Safari, Hold Still for NaN Minutes! | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...