Skip to content

ShadowCallStack

Yalnızca return address'leri saklayan, compiler tarafından enstrümante edilmiş, ayrı allocate edilen bir stack; buffer overflow'lardan gelen return-address overwrite'larını yener.

Mechanism

Note

ShadowCallStack (SCS), LLVM/Clang'de bir compiler hardening mode'udur (-fsanitize=shadow-call-stack). Uyguladığı invariant şudur: bir fonksiyonun return address'i, overflow'ların tipik olarak bozduğu data buffer'larından farklı ve onlara bitişik olmayan bir memory bölgesinden geri okunur.

Normal bir frame, kaydedilmiş return address'i local array'lerle aynı bitişik stack üzerinde saklar. Bu yüzden bir local buffer'ın linear overflow'u return slot'una ulaşır — kanonik stack-smash. SCS, her call frame başına yalnızca bir word — return address — tutan ikinci bir stack bulundurarak bu bitişikliği kırar. Fonksiyon prolog'unda return address shadow stack'e push edilir; epilog'da ret'ten önce (regular stack'ten değil) shadow stack'ten geri okunur.

Shadow stack'e, ona işaret eden pointer asla attacker'ın erişebileceği stack memory'sinde yaşamasın diye özel, ABI-reserved bir register (SCSReg) üzerinden referans verilir:

  • AArch64: x18
  • RISC-V (software): x3
  • RISC-V (hardware, Zicfiss): -fcf-protection=return ile ssp

Shadow stack kendi randomize edilmiş base address'i ile bağımsız olarak allocate edildiğinden ve ona işaret eden pointer'lar nadir olduğundan, bitişik bir buffer overrun veya tipik bir use-after-free'nin return address'in shadow kopyasını da üzerine yazması çok olası değildir. Değer regular stack üzerinde de kalır, ama yalnızca unwinder'lar için — bunun dışında control flow için kullanılmaz. Leaf fonksiyonlar doğası gereği güvenlidir çünkü bu target'larda call/ret stack üzerinde değil bir register (link register) üzerinde çalışır.

Walkthrough

AArch64'te enstrümante edilmiş prolog/epilog şöyle görünür. x18 shadow-stack pointer'ıdır; return address x30 (LR) ona spill edilir:

str     x30, [x18], #8          ; push LR onto shadow stack, post-increment
stp     x29, x30, [sp, #-16]!   ; normal prologue (saves x29/x30 to real stack)
mov     x29, sp
bl      bar
add     w0, w0, #1
ldp     x29, x30, [sp], #16     ; normal epilogue restore (ignored for control)
ldr     x30, [x18, #-8]!        ; pop LR from shadow stack, pre-decrement
ret                             ; returns to the *shadow* copy

SCS ile bir program build etmek, hem compile hem link zamanında flag'i, ayrıca AArch64'te register'ı reserve etmeyi gerektirir:

clang -target aarch64-linux-android \
  -fsanitize=shadow-call-stack -ffixed-x18 \
  -O2 prog.c -o prog

AArch64'te, target ABI zaten x18'i reserve etmiyorsa -ffixed-x18 geçmeniz gerekir (Android, Darwin, Fuchsia ve Windows onu reserve eder).

Mode'u compile zamanında tespit edip tek tek fonksiyonları dışarıda bırakabilirsiniz:

#if defined(__has_feature)
# if __has_feature(shadow_call_stack)
#  define HAS_SCS 1
# endif
#endif

/* exclude a function (e.g. one doing custom stack switching) */
__attribute__((no_sanitize("shadow-call-stack")))
void context_switch(void) { /* ... */ }

Warning

SCS, compiler-rt'de kendi runtime'ını sağlamaz: "A runtime is not provided in compiler-rt so one must be provided by the compiled application or the operating system." Android'de runtime, shadow stack'i allocate eder ve thread teardown için guard-region adresini TLS'te saklar. -fsanitize=shadow-call-stack'i SCSReg'in allocation/kurulumunu sağlamadan çıplak bir target'ta enable ederseniz, enstrümante edilmiş prolog initialize edilmemiş bir register'ı dereference eder.

Detection

SCS ile build edilmiş bir binary, non-leaf prolog'larındaki/epilog'larındaki str x30, [x18], #8 / ldr x30, [x18, #-8]! deyiminden ve x18'in reserve olmasından (asla scratch register olarak kullanılmamasından) tanınabilir. Runtime'ı olan sistemlerde, içine seyrek pointer'lar olan, ayrı map'lenmiş, randomize edilmiş bir bölge shadow stack'in kendisidir.

Mitigation

Dokümantasyonda belirtilen bilinen sınırlamalar ve zayıflıklar:

  • Side channel'lar / address guessing. Koruma, shadow stack'in adresinin gizli kalmasına bağlıdır. "OS-level and processor side channels"'a ve bölgeyi bulan address-guessing saldırılarına karşı savunmasızdır.
  • jmp_buf sızıntısı. Shadow stack pointer'ı, setjmp/longjmp tarafından kullanılan jmp_buf gibi yapılar üzerinden sızabilir.
  • Data-only saldırılar. SCS yalnızca return address'i korur. Indirect call'ları veya bozulmuş data/function pointer'larını kısıtlamaz, bu yüzden JOP'u, COOP'u veya data-only chaining'i durdurmaz — bkz. block-oriented-programming ve shadow-stack-aware-rop.
  • Register-pressure / ABI kısıtları. SCS kodu ile bir thread'i paylaşan her kod SCSReg'i reserve etmelidir, bu da mixed-toolchain deployment'ı sınırlar.

Forward-edge CFI (bkz. clang-cfi) ve hardware shadow stack'ler (intel-cet-shadow-stack, arm-pacbti) ile birbirini tamamlar.

References