Scenario
When the Signet shattered, Crownspire's oaths didn't die. They got imitated. Houses started turning out claims so immaculate they felt wrong in the hand, too perfect and too eager to be believed. The Corroded Crown was the old forge where the Signet's fragments were first shaped, a sanctified mechanism of authority now rusted and corrupted. Its relics still carry the old geometry, but the tolerances are off. Perfect face, wrong spine. Rin has found her way into the forge's service throat. The locks here are still called "sanctified," but she knows the tell by now. Someone has been refitting these holy mechanisms with new tolerances, quietly changing who gets let in once panic has everyone begging at the door. She isn't here to smash the system. She's here to make it accept the wrong truth.
Prerequisite Knowledge
To thoroughly understand and exploit **Corroded Crown**, knowledge of the following concepts is required:
A. Linux Heap Allocations & Glibc Bins
- Dynamic Memory (
malloc/free): When applications request heap memory dynamically viamalloc(size), the GNU C Library (glibc) heap manager allocates memory chunks. When freed viafree(ptr), these chunks are not immediately returned to the operating system; instead, they are placed into various recycling bin queues (Tcache, Fastbins, Unsorted bin, Small/Large bins) to improve subsequent allocation performance. - Tcache (Thread Local Caching): In glibc 2.26+, small chunks (typically bytes on 64-bit systems) are stored in singly-linked LIFO (Last-In, First-Out) lists called Tcache. When freed into Tcache, the first 8 bytes of the chunk body store a
nextpointer directing to the previously freed chunk in the same size class. - Unsorted Bin: When a chunk larger than the maximum Tcache threshold (e.g., bytes in GLIBC 2.31) is freed, it bypasses both fastbins and Tcache, falling straight into the Unsorted Bin. Unsorted bins are circular doubly-linked lists whose header lives within glibc's internal
main_arenadata structure. An unsorted bin chunk has its first 16 bytes populated with forward (fd) and backward (bk) pointers pointing directly into libc!
B. Glibc 2.31 Specific Heap Mechanics
- No Safe-Linking Obliteration: Starting in GLIBC 2.32, heap pointers in Tcache and fastbins are obfuscated using an XOR encryption mechanism known as Safe Linking (
pointer ^= address >> 12). However, in GLIBC 2.31, Tcachenextpointers are stored as raw, unencrypted virtual addresses. This allows trivial corruption of linked lists without requiring heap base leaks. - Hook Pointers (
__free_hook): Before being removed in GLIBC 2.34, glibc maintained writable global pointer hooks such as__malloc_hookand__free_hook. Wheneverfree(ptr)is called, the library first checks if__free_hookis non-null; if set, it jumps directly to__free_hook(ptr). Overwriting__free_hookwith the address ofsystemturns anyfree("/bin/sh")invocation into an instant shell!
C. Use-After-Free (UAF)
- A Use-After-Free occurs when a program frees a block of memory but retains a reference (dangling pointer) to that memory block in an accessible structure. If the application subsequently reads from or writes to this dangling pointer, attackers can inspect internal libc/heap management pointers or alter linked list architecture.
Enumeration
We are given a binary corroded_crown to locally reverse engineer the binary to fully understand what's happening behind. I begin my inspection by checking the binary properties, security mitigations, and dynamic linking dependencies using command-line tools.
Tool Stack Used
file,checksec,objdump,readelf,strings,gdb, and Pythonpwntools.
Binary & Security Analysis
Observations from Triage:
- Full RELRO: The Global Offset Table (GOT) is entirely read-only. We cannot perform simple GOT overwrites on standard library calls like
putsorprintf. - NX Enabled: Stack and heap memory areas are non-executable. We must rely on existing code or Return-Oriented Programming (R/W of library functions like
systemin libc). - PIE & Canary Enabled: Code addresses are randomized at runtime.
- Custom Glibc Provided: The binary uses an explicit RUNPATH to load
./glibc/libc.so.6. Verifying the version string confirms it is GLIBC 2.31:
Reverse Engineering & Code Analysis
Because the ELF executable is not stripped, symbol names remain fully intact. Disassembling the application with objdump -d -j .text challenge/corroded_crown reveals an interactive CLI menu that manages "Relics" positioned across "Shelves".
Data Model Layout (.bss section)
At address 0x4040 in the .bss segment, there is a 1024-byte global array named relic.
Each entry represents an individual shelf slot and is structured as a 16-byte (0x10) record:
Function Analysis
1. forge_relic() (Menu Option 1: Allocate)
- Analysis: Properly verifies if a slot's
activeflag is set to0before allocating. Allows arbitrary allocation sizes (no upper restriction).
2. destroy_relic() (Menu Option 4: Free)
- Analysis: Checks if
active == 1, callsfree()on the stored pointer, and togglesactiveback to0. Crucially, it does NOT clearrelic[idx].ptrorrelic[idx].size! Notice the descriptive developer prompt: "The relic crumbles to ash. The mark lingers." — hinting directly at the remaining dangling pointer!
3. inscribe_relic() (Menu Option 2: Edit / Write)
- Analysis: Reads input via raw system
read()directly intorelic[idx].ptrfor up torelic[idx].sizebytes. It never verifies ifrelic[idx].active == 1!
4. inspect_relic() (Menu Option 3: Inspect / Read)
- Analysis: Outputs binary contents via system
write()directly fromrelic[idx].ptrup torelic[idx].sizebytes without checking ifrelic[idx].active == 1! Because it useswrite(1, ...), it prints binary byte sequences regardless of null-terminator truncation.
Vulnerability Analysis
Why is it Vulnerable? (Root Cause)
The vulnerability is a classic, unrestricted Use-After-Free (UAF) Read & Write caused by two concurrent flaws:
- Dangling Pointer Retention in
destroy_relic: When an object is freed viafree(relic[idx].ptr), the pointer is not wiped (relic[idx].ptr = NULL). The reference to memory now owned and managed by glibc's heap recycling allocator remains active in therelicarray. - State Verification Bypass in
inscribe_relicandinspect_relic: Neither editing nor inspecting validates the slot's allocation state (relic[idx].active == 1).
Exploit Capabilities:
- Arbitrary Heap Leak: Freeing a chunk writes glibc bookkeeping data (
nexttcache links orfd/bkunsorted bin addresses) into the chunk body. We can invokeinspect_relic()on freed slots to instantly read internal library pointers! - Arbitrary Tcache Poisoning: By executing
inscribe_relic()on a freed Tcache chunk, we can overwrite its forwardnextpointer to point to any writable arbitrary memory address in the process virtual address space!
Exploitation
To bypass ASLR, Full RELRO, and non-executable stack execution, our exploitation objective is to take control of glibc's execution hook __free_hook and convert a routine free operation into an execution of system("/bin/sh").
Exploit Breakdown
Step 1: Bypassing ASLR via Unsorted Bin UAF Read
In GLIBC 2.31, chunks up to size 0x408 bytes are diverted into Tcache when freed. To obtain a libc pointer, we must allocate a chunk larger than the Tcache upper limit so it routes directly into the Unsorted Bin.
- Allocate Slot 0 with size
0x420. - Allocate Slot 1 with size
0x20(Guard Chunk): If a large heap chunk borders the wilderness memory region (Top Chunk), callingfree()will cause glibc to merge and consolidate it with the Top Chunk, stripping away unsorted bin pointers. Our guard chunk isolates Slot 0 from the top chunk! - Free Slot 0:
Slot 0 enters the Unsorted Bin. Its first 8 bytes (
fd) now contain an address pointing tomain_arena + 0x60insidelibc.so.6. - Leak & Resolve Libc Base:
Step 2: Tcache Poisoning via UAF Write
With the exact ASLR loading addresses of __free_hook and system computed, we execute a linked-list poisoning attack against the Tcache. Because this is GLIBC 2.31, pointer obfuscation (Safe Linking) is absent!
- Allocate Slots 2 and 3 with size
0x40(landing in the0x50size bin). - Free both chunks in succession:
The
0x50Tcache singly-linked list becomes:[Slot 2] -> [Slot 3] -> NULL. - Overwrite Tcache Forward Link (UAF Write):
We invoke
inscribeon freed Slot 2, replacing its first 8 bytes with the target address of__free_hook: The corrupted Tcache chain now resembles:[Slot 2] -> [__free_hook] -> ...
Step 3: Overwriting __free_hook and Spawning Shell
- Pop the First Chunk:
- Allocate at Target Address:
- Inscribe
systeminto__free_hook: We write the address of libc'ssystem()directly into Slot 5: - Trigger Shell execution:
Remember Slot 1 (our guard chunk of size
0x20from Step 1)? It remains active and untouched in heap storage. We inscribe the string/bin/sh\x00into it and calldestroy(1):
Exploit Script
The complete standalone exploit script I've crafted with AI assistance works seamlessly across both local emulation and remote server endpoints.
Execution Output & Flag Verification
Running the solver against the official competition server yields an instant shell and flag retrieval:
8. Conclusion & Remediation
Remediation & Fixes
To prevent this vulnerability in secure application development, developers must enforce strict lifecycle cleanup and state checking:
- Zero Out Dangling Pointers: Upon invoking
free(ptr), immediately set the variable toNULLand zero out any corresponding metadata lengths (relic[idx].ptr = NULL; relic[idx].size = 0;). - Enforce Consistent State Validation: Ensure that all accessor routines (reading, modifying, or inspecting object resources) rigorously assert
if (relic[idx].active != 1) return ERROR;prior to memory dereference. - Upgrade Library Environments: Compile projects against modern glibc builds (), which eliminate global hook pointers (
__free_hook,__malloc_hook) and enforce XOR pointer obfuscation in Tcache allocations (Safe Linking).