This means that if our declared type index aliases the constant HeapType::kNone
, the type check will always be elided if we cast to any non-function, non-external reference. In combination, we can use this to turn any reference type into any other by the following steps:
这意味着,如果我们声明的类型索引为常量 HeapType::kNone
别名,如果我们转换为任何非函数、非外部引用,则类型检查将始终被省略。结合使用,我们可以通过以下步骤将任何引用类型转换为任何其他引用类型:
-
In the type section, define a structure type with a single field of type anyref
, and make this struct have a type index equal to HeapType::kNone
using the bug described above.
在 type 部分中,定义一个具有 type anyref
字段的结构类型,并使此结构具有与 HeapType::kNone
使用上述 bug 相同的类型索引。
-
Place a non-null reference value of any type on the top of the stack and call struct.new
with the type index set to HeapType::kNone
. This will succeed, as has_struct()
validates the index against the index established via the previous step.
将任何类型的非 null 引用值放在堆栈的顶部,并在类型索引设置为 的情况下 HeapType::kNone
进行调用 struct.new
。这将成功,因为 has_struct()
根据通过上一步建立的索引验证索引。
-
Also, declare a struct with a normal type index lower than kV8MaxWasmTypes
with a single field of the target reference type. Call ref.cast
with this this struct’s type index. The engine will not perform any type check, as the input value is at this point understood to be reference type HeapType::kNone
.
此外,声明一个结构体的正常类型索引低于 kV8MaxWasmTypes
目标引用类型的单个字段。用这个调用 ref.cast
这个结构体的类型索引。引擎不会执行任何类型检查,因为此时输入值被理解为引用类型 HeapType::kNone
。
-
Finally, read back the reference stored in the struct by executing struct.get
.
最后,通过执行 struct.get
读回 struct 中存储的引用。
This arbitrary casting of reference types allows transmuting any value type into any other by referencing it, changing the reference type, and then dereferencing it – a universal type confusion.
这种对引用类型的任意转换允许通过引用任何值类型、更改引用类型,然后取消引用它来将任何值类型转换为任何其他值类型,这是一种通用类型混淆。
In particular, this directly contains nearly all usual JavaScript engine exploitation primitives as special cases:
特别是,这直接包含几乎所有常见的 JavaScript 引擎开发原语作为特殊情况:
• Transmuting int
to int*
and then dereferencing results in an arbitrary read.
int
• 转换为 int*
然后取消引用会导致任意读取。
• Transmuting int
to int*
and then writing to that reference results in an arbitrary write.
• int
转换为 int*
该引用,然后写入该引用会导致任意写入。
• Transmuting externref
to int
is the addrOf()
primitive, obtaining the address of a JavaScript object.
• 转换为 externref
int
是 addrOf()
原始的,获取 JavaScript 对象的地址。
• Transmuting int
to externref
is the fakeObj()
primitive, forcing the engine to treat an arbitrary value as a pointer to a JavaScript object.
• 转换为 int
externref
是 fakeObj()
基元,强制引擎将任意值视为指向 JavaScript 对象的指针。
While casting from HeapType::kNone
to an externref
is not allowed, remember that we are actually operating on one more level of indirection – transmuting to externref
involves casting to a reference to a struct containing one externref
member.
虽然不允许从 HeapType::kNone
an 转换到 an externref
,但请记住,我们实际上是在另一个间接级别上操作 – 转换为 externref
涉及转换为对包含一个 externref
成员的结构的引用。
Note however that these “arbitrary” reads and writes are still contained in the V8 memory sandbox, as all involved pointers to heap-allocated structures are tagged, compressed pointers inside the heap cage, not full 64-bit raw pointers.
但请注意,这些“任意”读取和写入仍包含在 V8 内存沙箱中,因为所有涉及的指向堆分配结构的指针都是标记的、堆笼内的压缩指针,而不是完整的 64 位原始指针。
Integer Underflow Leading to V8 Sandbox Escape
导致 V8 沙盒逃逸的整数下溢
The primitives described above allow for freely manipulating and faking most JavaScript objects. However, all of this happens inside the limited memory space of the V8 sandbox. “Trusted” objects such as WebAssembly instance data cannot yet be manipulated. We will now turn our attention to a bug that can be used to escape the memory sandbox.
上面描述的原语允许自由操作和伪造大多数 JavaScript 对象。然而,所有这些都发生在 V8 沙盒的有限内存空间内。“可信”对象(如 WebAssembly 实例数据)尚无法操作。现在,我们将把注意力转向一个可以用来逃避内存沙盒的错误。
An often-used object for JavaScript engine exploits is ArrayBuffer
and its corresponding views, (i.e. typed arrays), as it allows for direct, untagged access to some region of memory.
JavaScript 引擎漏洞利用的一个常用对象是 ArrayBuffer
及其相应的视图(即类型化数组),因为它允许直接、无标记地访问内存的某个区域。
To prevent access to pointers outside the V8 sandbox, sandboxed pointers are used to designate a typed array’s corresponding backing store. Similarly, an ArrayBuffer’s length field is always loaded as a “bounded size access”, inherently limiting its value to a maximum of 235 − 1.
为了防止访问 V8 沙箱外部的指针,沙盒指针用于指定类型化数组的相应后备存储。同样,ArrayBuffer 的长度字段始终作为“有界大小访问”加载,本质上将其值限制为最大 235 − 1。
However, in modern JavaScript, the handling of typed arrays has become quite complex due to the introduction of resizable ArrayBuffers (RABs) and their sharable variant, growable SharedArrayBuffers (GSABs). Both variants feature the ability to change their length after the object has been created with the shared variant being restricted to never shrink. In particular, for typed arrays with these kinds of buffers, the array length can never be cached and must be recomputed on each access.
然而,在现代 JavaScript 中,由于引入了可调整大小的 ArrayBuffers (RAB) 及其可共享变体 SharedArrayBuffers (GSAB),类型化数组的处理变得相当复杂。这两种变体都能够在创建对象后更改其长度,共享变体被限制为永不缩小。特别是,对于具有此类缓冲区的类型化数组,永远无法缓存数组长度,并且必须在每次访问时重新计算。
Additionally, ArrayBuffers also feature an offset field, describing the start of the data in the actual underlying backing store. This offset must be taken into account when computing the length.
此外,ArrayBuffers 还具有一个偏移字段,用于描述实际底层后备存储中数据的开始。在计算长度时,必须考虑此偏移量。
Let’s now look at the code responsible for building a TypedArray’s length access in the optimizing Turbofan compiler. It can be found in v8/src/compiler/graph-assembler.cc
. Note that most non-RAB/GSAB cases and the code responsible for dispatching are omitted for simplicity:
现在让我们看一下负责在优化的 Turbofan 编译器中构建 TypedArray 长度访问的代码。它可以在 v8/src/compiler/graph-assembler.cc
中找到。请注意,为简单起见,省略了大多数非 RAB/GSAB 案例和负责调度的代码: