Skip to content

Byte-by-Byte Canary Brute Force (forking servers)

Forking bir server'a karşı bir stack canary'yi byte byte recover et: child'lar parent'ın canary'sini paylaşır, dolayısıyla tek bir byte'ı overwrite ettikten sonra crash-vs-survive, memory leak etmeden onu açığa çıkarır.

Mechanism

Neden çalışır

Stack canary, process startup'ta bir kez seçilir ve korunan her ret'ten önce check edilir. Yanlış bir canary process'i abort eder; doğru olan sessizce geçer. Normalde 8 byte'ın tamamını tahmin edemezsin (≈2^56 kullanışlı entropy).

Ama "fork() çağrıları etkin olarak parent process'in bir kopyasını oluşturur," dolayısıyla forking bir server'ın her child'ı aynı canary değerini tutar. Bir crash'te yalnızca child ölür — parent, özdeş canary ile servis etmeye devam eder. Bu, her child'ı bir single-byte oracle yapar:

  • Tam olarak bir canary byte'ına kadar ve dahil olmak üzere, bir tahmin edilen değerle overflow yap.
  • Tahmin yanlışsa, canary check başarısız olur ve child crash eder.
  • Tahmin doğruysa, check (şimdilik) geçer ve request normal şekilde devam eder — ayırt edilebilir bir "survive" sinyali.

O byte için 0x00–0xFF'i iterate et; crash etmeyen değer doğrudur, sonra sonraki byte'a geç. Canary'nin dayandığı invariant — secret'ın hepsi-bir-anda tahmin edilmesi gerekir — partial correctness gözlemlenebilir olduğundan lineer bir aramaya çöker.

Walkthrough

64-bit bir canary için maliyet: least-significant byte Linux'ta sabit 0x00'dır, geriye ~7 bilinmeyen byte bırakır → en fazla 256 × 7 ≈ 1.792 deneme (2^56 blind'e karşı).

canary = b"\x00"                  # LSB is the NUL terminator byte on Linux
for pos in range(1, 8):           # recover bytes 1..7
    for guess in range(0x100):
        payload  = b"A"*OFFSET            # fill up to the canary
        payload += canary + bytes([guess]) # known bytes + this guess
        child = connect(target)
        send(child, payload)
        if survived(child):       # no crash -> this byte is correct
            canary += bytes([guess])
            break
print("canary =", canary.hex())   # ~1792 connections worst case

Beklenen davranış: her byte pozisyonu için, 256 denemenin 255'i forked child'ı crash eder (connection reset / normal yanıt yok) ve tam olarak biri hayatta kalır — o byte recover edilir. 7 round sonra tüm canary bilinir ve gerçek overflow'da replay edilebilir.

Koşullar: fork, NUL-append yok, gözlemlenebilir survival

Yalnızca şu durumlarda çalışır: (a) server fork edip aynı canary'yi tutar, (b) input path binary-safe'tir (örn. read/recv, 0x00 byte'ında truncate edecek NUL-terminating bir string copy değil) ve (c) crash vs. başarı ayırt edilebilirdir. Request başına execve, canary'yi re-randomize eder ve bunu çürütür.

Detection

ASLR brute force gibi, her byte bulunmadan önce bir child crash seli üretir — __stack_chk_fail abort'ları, *** stack smashing detected *** log satırları ve core dump'lar — ki bunları crash-rate monitoring işaretleyebilir.

Mitigation

(Neyin durdurduğu.) Canary'yi her connection'da re-randomize et (yalnızca fork değil, re-exec), client başına anormal crash rate'lerini detect/throttle et ve canary'leri tek bir secret'a bağlı olmayan mitigation'larla (shadow stack'ler) eşleştir. Aynı fork-inheritance zayıflığı stack reading'in de temelindedir.

References