House of Roman

House of Roman - article cover image RE & Pwn
Time it takes to read this article 8 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

House of Roman is the *leakless* heap technique: it aims to gain code execution without ever disclosing a libc address, trading a memory leak for a small brute force over ASLR’s low bits. The insight is that ASLR randomises addresses only above the low 12 bits of a page offset, and libc is page-aligned, so the *bottom* bits of a libc pointer are fixed while only a handful of bits above them vary. By using partial pointer overwrites — rewriting just the low one or two bytes of a pointer already present in memory — House of Roman edits existing libc/heap pointers into the targets it wants while guessing only the few uncertain bits, then overwrites __malloc_hook with a one_gadget so the next allocation spawns a shell.

The technique (published by a researcher under the handle *romanking*) is a masterclass in composing small primitives: a fastbin dup / poisoning step to aim an allocation near __malloc_hook, an unsorted-bin partial overwrite to plant a libc pointer without leaking it, and a final partial overwrite to bend that pointer onto a one_gadget. Because it depends on __malloc_hook, House of Roman applies to glibc versions where the malloc/free hooks still exist — i.e. before glibc 2.34, which removed them entirely — and it is further constrained on glibc >= 2.32 by safe-linking, which mangles fastbin/tcache fd pointers and complicates the partial-overwrite arithmetic the technique relies on.

Attack Prerequisites

House of Roman needs fine-grained write control (partial overwrites), a hook target, and tolerance for a small brute force. Typical requirements:

  • A partial-overwrite primitive — the ability to rewrite just the low 1–2 bytes of a pointer in memory (via a UAF edit or overflow), not only whole 8-byte writes.
  • A fastbin (or tcache) corruption primitive — a double-free or edit-after-free to point a bin’s fd near __malloc_hook.
  • glibc with __malloc_hook/__free_hook — i.e. glibc < 2.34; on 2.34+ the hooks were removed and the classic finale has no target.
  • Tolerance for a low-bits brute force — the uncertain nibble(s) of the randomised address are guessed, so the exploit succeeds probabilistically (often ~1-in-16 per uncertain nibble) and is simply re-run.
  • A restartable target (a forking service, or a local re-runnable binary) so the brute force is practical.

How It Works

The technique rests on how ASLR interacts with page alignment. Randomisation shifts a mapping by a page-granular amount, so the low 12 bits of any address within a page are invariant; for a specific libc symbol, the low 1.5 bytes are effectively fixed and only the next nibble is uncertain. A partial overwrite rewrites just those low bytes of a pointer that is already the correct libc/heap pointer, redirecting it to a nearby target while leaving the unknown high bytes untouched — no leak needed, because you never had to know the high bytes in the first place. The cost is the one uncertain nibble, which is brute-forced.

House of Roman composes three moves. First, a fastbin poisoning aims an allocation at the region around __malloc_hook. glibc’s fastbin free path checks that a chunk’s size matches the bin, so exploiters exploit a happy coincidence: a few bytes before __malloc_hook there is memory that, read as a size field, forms a valid fastbin size (the classic 0x7f value, which the allocator treats as a 0x70 chunk). By partially overwriting a fastbin fd to point at that fake-chunk address, a subsequent allocation is served on top of __malloc_hook. Second, an unsorted-bin partial overwrite is used to plant a genuine libc pointer into the target slot without leaking it: a chunk in the unsorted bin already contains a main_arena (libc) pointer in its fd/bk, and a partial overwrite of its low bytes shifts that pointer to reference the neighbourhood of a one_gadget. Third, a final partial overwrite trims that planted pointer precisely onto the one_gadget address, so __malloc_hook now holds a pointer to a single glibc gadget that execs a shell when its constraints hold.

With __malloc_hook overwritten by a one_gadget, the finish is trivial: any subsequent malloc (or a malloc-invoking abort) calls the hook, and the one_gadget’s execve("/bin/sh", ...) runs — all without a single leaked address, only a brute force over the handful of uncertain bits. On glibc >= 2.32, safe-linking XORs stored fastbin/tcache fd pointers with a page-shifted secret, so the partial-overwrite arithmetic must account for the mangling (and typically needs a heap-bits leak); and on glibc >= 2.34 the hooks are gone, so the finale must be re-pointed at a different target such as a FILE vtable — meaning House of Roman in its named form is fundamentally a pre-2.34 technique.

Vulnerable Code / Setup

The enabling bug is an allocator lifetime bug (UAF/double-free) that also grants *partial* write control over freed-chunk pointers — the granularity is what makes the leakless overwrites possible:

#include <stdlib.h>
#include <string.h>

char *slot[16];

/* edit() writes an attacker-chosen number of bytes into a freed chunk,
   so the attacker can overwrite just the LOW byte(s) of a pointer -- the
   partial-overwrite primitive House of Roman needs. */
void edit(int i, const char *src, size_t n) {
    memcpy(slot[i], src, n);      /* BUG: no free-state check; n attacker-set */
}

void demo(void) {
    slot[0] = malloc(0x68);
    slot[1] = malloc(0x68);
    free(slot[0]);
    free(slot[1]);
    free(slot[0]);                /* double free -> fastbin dup */
    /* Partial overwrite of the fastbin fd (low 2 bytes) aims the next
       allocation at the fake 0x7f chunk just before __malloc_hook. */
    edit(0, "\x00\x00", 2);       /* low-byte poke, high bytes untouched */
}
C

The 0x7f size trick is central: the bytes preceding __malloc_hook read as a valid fastbin size, so an allocation of the matching class can be placed over the hook. Triage the target — the hook must exist (pre-2.34) and the binary must be restartable for the brute force:

$ ldd ./roman | grep libc     # MUST be < 2.34 (hooks removed in 2.34);
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (2.27)   # and ideally < 2.32
                                                          # (pre safe-linking)
$ checksec --file=./roman
RELRO      STACK CANARY   NX        PIE
Partial    Canary found   NX enab.  PIE
Bash

Walkthrough / Exploitation

Confirm the fake 0x7f chunk near __malloc_hook and the fastbin fd you will partially overwrite before committing the brute force. In gdb with pwndbg:

gdb -q ./roman
pwndbg> run
pwndbg> p (void*)&__malloc_hook
pwndbg> x/4gx (long)&__malloc_hook - 0x23   # the 0x7f 'size' used as a chunk
pwndbg> fastbins            # inspect the dup'd bin and the fd to overwrite
pwndbg> one_gadget ./libc.so.6   # (shell) enumerate candidate one_gadgets
Bash

The exploit stages the three partial overwrites and then triggers the hook. Because a nibble is unknown, the whole thing is wrapped in a retry loop. A pwntools skeleton:

from pwn import *

def attempt():
    io = process('./roman')          # or remote(); restartable for brute force

    def alloc(i, sz):  ...           # menu wrappers
    def free(i):       ...
    def edit(i, data): ...           # partial-overwrite capable

    # 1) fastbin dup, then partial-overwrite fd -> fake 0x7f chunk @ hook-0x23
    alloc(0, 0x68); alloc(1, 0x68)
    free(0); free(1); free(0)
    edit(0, p16(0xXXXX))             # low 2 bytes; one nibble is the GUESS

    # 2) allocate onto __malloc_hook; plant a libc ptr via unsorted-bin
    #    partial overwrite, then trim it onto a one_gadget with a final poke
    plant_libc_pointer_partial()
    edit_hook_low_bytes(one_gadget_low)   # bend planted ptr -> one_gadget

    # 3) trigger: any malloc now calls the hook == one_gadget -> shell
    alloc(2, 0x18)
    io.sendline(b'id')
    return io

# brute force the uncertain ASLR nibble(s): re-run until the guess lands
for _ in range(64):
    io = attempt()
    if b'uid=' in io.recvline(timeout=1): io.interactive(); break
    io.close()
Python

The payoff is the hook call: once __malloc_hook holds a one_gadget, the next malloc executes execve("/bin/sh", ...) when the gadget’s register constraints are satisfied — no leak was ever required, only the low-bits guess. Success is probabilistic per run (roughly 1/16 per uncertain nibble), so the loop simply retries until a run lands, which for one uncertain nibble converges in a handful of attempts.

Note: House of Roman leans on the 0x7f/0x70 fastbin-size coincidence in the bytes just before __malloc_hook: read as a chunk size, 0x7f is accepted as a 0x70 fastbin chunk, letting an allocation of that class land on the hook. This exact offset is libc-version-specific — verify it in gdb with x/gx &__malloc_hook - 0x23 rather than hardcoding it, since the surrounding bytes differ across releases.

Opsec: Two hard version boundaries define this technique. safe-linking (glibc >= 2.32) mangles the fastbin/tcache fd you partially overwrite, so the poke must be computed against the XOR mask (usually needing a heap-bits leak) — a straight low-byte write no longer aims where you expect. And glibc 2.34 removed __malloc_hook/__free_hook outright, so the finale has no hook to hit; on 2.34+ the same leakless philosophy must retarget a FILE vtable or exit-handler pointer instead. Fingerprint the libc before attempting House of Roman.

Detection and Defense

House of Roman is defeated primarily by the allocator/libc changes that removed its targets, plus the usual anti-leak and anti-brute-force controls:

  • Run glibc >= 2.34 — with __malloc_hook/__free_hook removed, the named technique’s finale has no target; safe-linking (>= 2.32) already blunts the partial-overwrite step before that.
  • Fix the lifetime bug — the double-free/UAF that grants the partial-overwrite primitive is the root cause; null pointers after free and validate free-state.
  • Defeat the brute force — for network services, restart-with-re-randomise and crash-rate alerting turn the required re-runs into a noisy, detectable signal; per-connection ASLR raises the guess cost.
  • Full RELRO + PIE + ASLR — while House of Roman is leakless, keeping strong randomisation maximises the number of uncertain bits an attacker must guess.
  • Sanitizers in CI — AddressSanitizer catches the double-free/UAF deterministically before it ships.

Real-World Impact

House of Roman is celebrated less for real-world CVEs than for what it demonstrated: that a full shell is achievable without an information leak when partial overwrites and a small ASLR brute force are available — a result that reshaped how researchers think about ‘leakless’ exploitation on glibc. It is a staple of advanced CTF heap challenges and allocator-research write-ups, and its dependence on __malloc_hook makes it a clean historical marker for why glibc removed the hooks in 2.34: the hooks were simply too convenient a ‘call-this-pointer-on-every-malloc’ target. The bug classes it consumes — double-free (CWE-415) and use-after-free (CWE-416) — remain among the most prevalent memory-safety weaknesses, and the leakless partial-overwrite philosophy it popularised lives on in modern hook-free techniques that retarget FILE structures and exit handlers instead.

Conclusion

House of Roman turns partial pointer overwrites and a few bits of ASLR guessing into a leakless shell: fastbin poisoning aims an allocation at the 0x7f fake chunk before __malloc_hook, an unsorted-bin partial overwrite plants a libc pointer without disclosing it, and a final poke trims that pointer onto a one_gadget so the next malloc execs /bin/sh. It is a pre-2.34 technique by construction — safe-linking constrains it and the removal of malloc/free hooks retires its finale — but its core idea, that you can edit existing pointers into targets rather than needing to know their values, remains one of the most important concepts in modern heap exploitation.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL