Scenario
The nightmares started the night she cracked the Corroded Crown open. Rin remembers the forge's relics glowing wrong in her hands, remembers making that sanctified mechanism swallow a lie and call it truth, and remembers, less clearly, something rising back through the same crack once the tolerances gave. Maelor never left the Signet's fire whole. Some scorched piece of him must have ridden the wrong truth she fed the forge straight into her, because it has been talking to her ever since, quiet at first, close enough to her own voice that it took weeks for Rin to learn where she ended and it began. Veylen calls what he's built the Emptiness Machine, an old Registry working that lets a mind step outside its own head into a space empty enough to see the echo clearly, and maybe cut it loose. He can open the working. He can't walk it for her. Inside, there's no lock, no door, only two currents of herself running side by side, and whichever one she turns against the voice first is the one that gets her out clean. Veylen's ink keeps the seal steady from the outside. Everything past that line is hers alone, and she has to decide how much of herself she's willing to rewrite to be free of a dead king's voice.
Prerequisite Knowledge
To fully grasp this challenge and its exploitation methodology, understanding of the following concepts is essential:
A. Glibc File Stream Internal Structures (_IO_FILE / _IO_FILE_plus)
Standard input, output, and error streams (stdin, stdout, stderr) in Linux userspace programs are governed by complex memory structs containing control flags, internal read/write buffers, lock pointers, wide-character data structures, and vtable function pointer arrays.
B. File Structure Oriented Programming (FSOP)
A sophisticated exploitation paradigm that achieves arbitrary information leaks and Remote Code Execution (RCE) by corrupting standard library I/O file structs instead of relying on traditional ROP chains or GOT overwrites.
C. Glibc Vtable Hardening (__io_vtables)
Since glibc 2.24, indirect function calls through standard stdio vtables (like _IO_file_jumps) are protected by runtime pointer validation checking whether the target vtable address falls inside a read-only libc memory section named __io_vtables.
D. The "House of Apple 2" Attack
A modern FSOP RCE technique targeting wide-character stream processing in glibc. While standard vtables are checked against __io_vtables, the secondary wide-character vtable pointer (_wide_data->_wide_vtable) is not subject to pointer validation checks. By redirecting execution through wide-character allocation pathways, arbitrary function execution can be achieved.
Enumeration
I begin by inspecting the given ELF binary and its provided C runtime library using standard command-line tools.
Binary & Security Analysis
- Format: ELF 64-bit LSB pie executable, x86-64, dynamically linked, interpreter
./glibc/ld-linux-x86-64.so.2.
- Security Mitigations:
- RELRO:
Full RELRO(The Global Offset Table is read-only; overriding library symbols likeputsorprintfin the GOT is completely impossible). - Stack Canary:
Canary found(Protects against simple stack buffer overflows). - NX (No-Execute):
NX enabled(Stack and heap segments are non-executable; shellcode injection will trigger SIGSEGV). - PIE:
PIE enabled(Position-Independent Executable; code and data addresses are randomized by ASLR on every invocation).
- RELRO:
Reverse Engineering & Code Analysis
Disassembling the primary logic of main (via r2, objdump, or GDB disassembly) reveals a very compact and clean loop of execution:
C Decompilation Equivalent
Vulnerability Identification
The binary intentionally introduces two devastating structural vulnerabilities:
- Direct Overwrite of
stdout: The program callsscanf("%224s", &_IO_2_1_stdout_). The global struct_IO_2_1_stdout_resides in libc's writable data section (.data/.data.rel.ro). The value224(0xe0bytes) is significant: exactly0xe0bytes spans the entire_IO_FILE_plusstructure, from_flagsat offset0x00all the way through to the primaryvtablepointer at offset0xd8. - Direct Overwrite of
stderr: The program executesscanf("%224s", &_IO_2_1_stderr_)immediately before returning frommain. When a Linux binary terminates viareturnorexit(), glibc invokes_IO_flush_all(), which traverses a linked list of all active streams starting at_IO_list_all(stderr -> stdout -> stdin) and attempts to flush buffered data by calling function pointers inside each stream's vtable.
Vulnerability Analysis
The binary intentionally introduces two devastating structural vulnerabilities:
- Direct Overwrite of
stdout: The program callsscanf("%224s", &_IO_2_1_stdout_). The global struct_IO_2_1_stdout_resides in libc's writable data section (.data/.data.rel.ro). The value224(0xe0bytes) is significant: exactly0xe0bytes spans the entire_IO_FILE_plusstructure, from_flagsat offset0x00all the way through to the primaryvtablepointer at offset0xd8. - Direct Overwrite of
stderr: The program executesscanf("%224s", &_IO_2_1_stderr_)immediately before returning frommain. When a Linux binary terminates viareturnorexit(), glibc invokes_IO_flush_all(), which traverses a linked list of all active streams starting at_IO_list_all(stderr -> stdout -> stdin) and attempts to flush buffered data by calling function pointers inside each stream's vtable.
Why It Is Exploitable?
Despite having Full RELRO, Stack Canaries, NX, and PIE active, standard I/O FILE structures must remain writable in libc memory so programs can update buffer pointers, maintain locks, and toggle reading/writing flags during execution.
By allowing arbitrary writes into these live stream structures:
- Memory Disclosure (ASLR Defeat): Manipulating buffer boundaries and flags on
stdouttricks glibc into treating uninitialized internal libc pointer regions as buffered ASCII user output, bypassing PIE and ASLR entirely without needing a format string or stack overflow. - Control Flow Hijacking (RCE): Modifying
stderrright before program termination allows us to hijack the automatic teardown process (_IO_flush_all()). By routing execution into glibc's internal wide-character handlers, we bypass modern vtable pointer restrictions and turn standard program shutdown into arbitrary command execution.
Exploitation
Stage 1: Defeating ASLR via stdout Memory Leak
To bypass ASLR and dynamically calculate the addresses of system and internal libc structures, we target the first scanf("%224s", &_IO_2_1_stdout_).
In standard FSOP leak methodologies, overwriting _flags with 0xfbad2887 is common. However, in glibc 2.39, abrupt modifications to active stream flags during puts/printf buffering can trigger heap assertions or munmap_chunk() aborts. Through diagnostic testing, we established that setting:
and clearing the next three pointer words (_IO_read_ptr, _IO_read_end, and _IO_read_base filled with 8 null bytes each) forces glibc's internal output writers (_IO_file_xsputn) to ignore read-mode invariants and directly dump internal libc pointers sitting immediately after the structure header.
When printf("\xF0\x9F\xA9\xB8...") executes, the remote server blasts back a user-space pointer corresponding to _IO_2_1_stdout_ + 131 (0x7f...644). From this single leak, we reliably compute:
Stage 2: House of Apple 2 RCE on stderr
When main() exits, glibc invokes _IO_flush_all(). For every stream fp in _IO_list_all (which begins with stderr), glibc tests if the stream contains pending write data:
We set _IO_write_base = 0, _IO_write_ptr = 1, and _mode = 0 inside our stderr payload. This guarantees that fp->_IO_write_ptr > fp->_IO_write_base evaluates to true, forcing glibc to call _IO_OVERFLOW(stderr, EOF).
Bypassing __io_vtables via Wide-Character Routing
If we directly point stderr->vtable to system, glibc's runtime checks immediately abort execution because system does not live inside __io_vtables.
To bypass this check, we point stderr->vtable (offset 0xd8) to _IO_wfile_jumps (which IS a legal, validated vtable inside __io_vtables). When _IO_OVERFLOW(stderr, EOF) is called, it executes _IO_wfile_overflow(stderr).
The Breakthrough Diagnosis: Solving the Wide-Data Alignment Trap
During early exploitation development, payloads attempting to invoke system would inexplicably terminate with SIGSEGV at _IO_flush_all+366. By performing instruction-by-instruction dynamic assembly tracing inside GDB batch mode, we discovered the precise cause of the crash:
Inside _IO_wfile_overflow(), glibc executes the following check:
In standard generic writeups, attackers point _wide_data to stderr - 0x38 or stderr - 0x20 to save space. However, in glibc 2.39, _wide_data->_IO_write_base resides at offset 0x18 of the _wide_data structure.
- If
_wide_data = &stderr - 0x38, offset0x18maps to memory at(&stderr - 0x38) + 0x18 = &stderr - 0x20. - What resides at
&stderr - 0x20in libc's.data.rel.rosegment? The global_IO_list_allpointer! - Since
_IO_list_allis never null (it points tostderr),f->_wide_data->_IO_write_base == NULLevaluated to FALSE. Deceived into believing a buffer was already allocated,_IO_wfile_overflowskipped calling_IO_wdoallocbuf(), altered stream internal pointers, and exited back to_IO_flush_all(). - On the next iteration of
_IO_flush_all(), the loop traversedstderr->_chain, treated our injectedsystem_addras a file structure, and triggered a fatal segmentation fault!
The Final Alignment Fix
To force glibc into _IO_wdoallocbuf(), we repositioned:
Let's trace how this realignment affects execution:
wide_data->_IO_write_base(Offset0x18): Maps to(stderr - 0x10) + 0x18 = stderr + 0x08. We set offset0x08ofstderrtop64(0)(8 null bytes). Check passes!wide_data->_IO_buf_base(Offset0x30): Inside_IO_wdoallocbuf(), glibc checksif (fp->_wide_data->_IO_buf_base) return;. Offset0x30maps to(stderr - 0x10) + 0x30 = stderr + 0x20. This corresponds tostderr->_IO_write_base, which we also set top64(0). Check passes!- Triggering Arbitrary Vtable Execution: Having confirmed both fields are null,
_IO_wdoallocbuf()invokes:- Notice that
_wide_vtableis at offset0xe0of_wide_data, which maps to(stderr - 0x10) + 0xe0 = stderr + 0xd0. - We store
p64(stderr_addr)atstderr + 0xd0, pointing_wide_vtabledirectly back tostderritself! - In vtable structures,
_doallocatesits at offset0x68(104). At offset0x68ofstderr, we placep64(system_addr). - When
_doallocate(fp)is invoked, glibc jumps straight intosystem(&stderr).
- Notice that
Finally, because &stderr points directly to offset 0x00 (_flags), we set the first 8 bytes of our payload to:
The ASCII string "a=0;sh" acts as a shell command (assigning 0 to shell variable a and launching /bin/sh). In memory, its hex representation is 0x3b6873303d61. Notice that the ASCII character '=' (0x3d) conveniently has bit 0x8 set, satisfying bitmask tests for _IO_CURRENTLY_PUTTING (0x800 in upper bytes) and guaranteeing flawless execution without runtime warnings!
Stage 3: Bypassing ASLR Whitespace Termination
A subtle trap in this challenge involves the input method: scanf("%224s", ...).
The %s format specifier immediately terminates reading if it encounters any ASCII whitespace character:
0x20(SPACE)0x09(TAB)0x0a(NEWLINE / LF)0x0d(CARRIAGE RETURN / CR)
Because ASLR randomizes the middle byte locations of libc_base on every run, there is approximately a 4.7% mathematical probability (1 - (252/256)^3) that one of our computed pointer targets (such as system_addr, wfile_jumps, or stderr) will contain a forbidden whitespace byte. If transmitted blindly, scanf stops reading midway through the pointer, leaving the trailing vtable pointers corrupt and causing a silent server disconnect or segmentation fault.
Our final solution solves this via defensive engineering: before transmitting the RCE payload, the Python script inspects every byte of the constructed structure against a bad-character blacklist. If an unfavorable ASLR alignment occurs, the script silently terminates the socket, opens a new connection, and retries until a clean ASLR memory layout is confirmed. In practice, this achieves interactive shell execution on the 1st or 2nd attempt with 100% reliability.
Exploit Script
Below is the complete, self-contained Python solver script. It relies entirely on Python built-ins (socket, struct, select), eliminating dependencies like pwntools so it can execute effortlessly across any system or docker environment.
Conclusion
The Emptiness Machine is a masterclass challenge demonstrating how modern glibc file structures can be leveraged for reliable execution hijacking in heavily fortified binary environments. By uniting a clean FSOP read leak on stdout with precise structure offsets for a House of Apple 2 attack on stderr, we achieved remote root shell access and secured the flag!