Skip to content

unsafe unlink

fd/bk'si bilinen bir global pointer'ın yakınını gösteren sahte bir free chunk forge et, böylece free() consolidate edip unlink() macro'sunu çalıştırdığında safe-unlinking check'i geçer ve macro o pointer'ı near-arbitrary bir write ile overwrite eder.

Mechanism

glibc'nin free()'si bir chunk'ı free bir komşuyla birleştirdiğinde, komşuyu doubly linked bin'inden çıkarmak için unlink() macro'sunu çağırır. Klasik unlink (ve modern olanın naive bir anlayışı), bir chunk P için şunu çalıştırır:

FD = P->fd;
BK = P->bk;
FD->bk = BK;     // *(FD + offsetof(bk)) = BK
BK->fd = FD;     // *(BK + offsetof(fd)) = FD

Bu iki store, attacker'ın etkilediği fd/bk değerleri üzerinden yapılan write'lardır. Modern glibc bunları safe-unlinking check'i ile korur:

if (FD->bk != P || BK->fd != P)   malloc_printerr("corrupted double-linked list");

Note

Invariant şudur: "gerçek bir doubly linked list'te P->fd->bk == P ve P->bk->fd == P." Saldırı bu check'i kırmak yerine karşılar. Saldırıya uğrayan chunk'ın adresini tutan bir global pointer chunk0_ptr varsa, sahte chunk'ın fd = &chunk0_ptr - 3*8 ve bk = &chunk0_ptr - 2*8 (64-bit'te 8 = sizeof(uint64_t)) olarak ayarla. O zaman FD->bk ve BK->fd'nin ikisi de chunk0_ptr'ın kendisini alias'lar, ki o da zaten P'ye eşittir — böylece check geçer. İki macro store'u sonra chunk0_ptr'a &chunk0_ptr - 0x18 yazar (ve tersi), chunk0_ptrkendinden 0x18 byte önceyi gösterir bırakır. chunk0_ptr[...] üzerinden sonraki bir write artık pointer'ın kendi storage'ına (ve komşu global'lere/GOT'a) ulaşır ve bozulmayı near-arbitrary bir write'a yükseltir.

Walkthrough

Kanonik referans shellphish how2heap unsafe_unlink.c'dir. Sahte fd/bk, safe-unlinking check'i her iki tarafta da chunk0_ptr'ı görecek şekilde hazırlanır:

// craft a fake free chunk in-place at chunk0_ptr so FD->bk == P and BK->fd == P
chunk0_ptr[2] = (uint64_t) &chunk0_ptr - (sizeof(uint64_t)*3);  // fake fd
chunk0_ptr[3] = (uint64_t) &chunk0_ptr - (sizeof(uint64_t)*2);  // fake bk

// make the next chunk's header point back to our fake chunk and clear PREV_INUSE
chunk1_hdr[0] = malloc_size;   // prev_size: distance to the fake chunk
chunk1_hdr[1] &= ~1;           // clear prev_inuse -> "previous chunk is free"

free(chunk1_ptr);              // backward consolidation runs unlink() on the fake chunk

free() sonrası chunk0_ptr artık user data'yı göstermez — &chunk0_ptr - 0x18'i gösterir. Exploit sonra arbitrary belleğe vurmak için onun üzerinden yazar:

chunk0_ptr[3] = (uint64_t) victim_string;   // overwrite a pointer near chunk0_ptr
chunk0_ptr[0] = 0x4141414142424242LL;        // write through it -> arbitrary write
$ git clone https://github.com/shellphish/how2heap
$ cd how2heap/glibc_2.23 && make unsafe_unlink && ./unsafe_unlink
...
The new value of the global chunk0_ptr is &chunk0_ptr-0x18
We can now overwrite that pointer and write where we want
Now the next write to chunk0_ptr[...] modifies victim_string

Warning

Teknik, self-referential fd/bk'nin hesaplanabilmesi için chunk'a bilinen bir adreste (chunk0_ptr global'i) yazılabilir bir pointer gerektirir — onsuz safe-unlinking check'i karşılanamaz. Ayrıca PREV_INUSE'u temizlemek için sonraki chunk'ın prev_size/size header'ına bir heap overflow gerektirir. Modern glibc'de ek corrupted size vs. prev_size check'ine de uyulmalıdır; how2heap'in eşleşmesi için prev_size = malloc_size ayarlamasının nedeni budur. Kullanışlı bir pointer olmadığında, ilgili off-by-one / consolidation saldırıları (off-by-null-poison-null-byte, house-of-einherjar) yerine kullanılır.

Detection

  • Forge edilmiş metadata tutarsız olduğunda glibc malloc(): corrupted double-linked list (safe-unlinking check'i) veya corrupted size vs. prev_size ile abort eder.
  • ASan, sonraki chunk header'ına yapılan öncü heap-buffer-overflow'u işaretler.
  • Bir overflow'un ulaşabileceği bir chunk'a global/heap pointer'ı, komşu chunk'ın bir free()'siyle birlikte denetle.

Mitigation

  • Safe-unlinking check'in kendisi (FD->bk == P && BK->fd == P, artı corrupted size vs. prev_size) allocator içi mitigation'dır; glibc'yi güncel tut.
  • Chunk metadata'sına yapılan kök heap overflow'u ortadan kaldır (bounds-checked kopyalar, _FORTIFY_SOURCE, testte ASan).
  • Bir overflow'un etraflarında fd/bk forge edebileceği heap chunk'larının adresini saklayan uzun ömürlü global'ler tutmaktan kaçın.

References