Skip to content

Uncontrolled format string

Attacker-controlled veri printf ailesinin fonksiyonlarına format argument'ı olarak geçtiğinde, %p ve %n gibi conversion specifier'lar fonksiyonun variadic argument slot'larını okur ve yazar — belleği leak ederek ve arbitrary adreslere yazarak.

Mechanism

CWE-134 (Use of Externally-Controlled Format String) şu durumda oluşur: "ürün, argument olarak bir format string kabul eden bir fonksiyon kullanır, ama format string harici bir kaynaktan gelir." printf(user) kanonik bug'dır: printf ilk argument'ın güvenilir bir template ve herhangi bir % directive'inin caller'ın sağladığı variadic argument'larla desteklendiğini bekler. Kullanıcı template'i kontrol ettiğinde, her % specifier'ı variadic ABI'deki sonraki slot'u — önce register'lar, sonra stack — caller'ın oraya bir şey geçirip geçirmediğine bakılmaksızın tüketir.

Note

İhlal edilen invariant, "conversion specifier'ların sayısı ve tipi, gerçekten geçirilen variadic argument'larla eşleşir"dir. Onu kır ve:

  • %p / %x ardışık argument slot'larını okur ve yazdırır — stack içeriğini, kaydedilmiş pointer'ları, canary'leri ve libc/PIE adreslerini leak eder (bir format-string-read).
  • Direct parameter access %N$, kendinden öncekileri dolaşmadan N'inci argument'ı seçer: %7$p doğrudan 7. slot'u yazdırır. Bu, bir attacker'ın doğrudan ilgilendiği bir değere ve (stack'e yerleştirilmiş bir hedef adresle birleştirildiğinde) bir write'ın kullandığı slot'a index'lemesine olanak verir.
  • %n yazdırmaz; o ana kadar çıktıya verilen karakter sayısını, ilgili argument slot'undan alınan int *'a yazar. Çıktıyı field-width (%100c) ile doldurarak ve bir argument slot'una bir hedef adres yerleştirerek attacker hem değeri hem de hedefi kontrol eder — bir write primitive'i. Length modifier'lar %hn (2 byte) ve %hhn (1 byte) dar, aşamalı write'lara izin verir, böylece milyarlarca karakter yazdırmadan tam bir pointer inşa edilebilir.

Walkthrough

Güvenlik açığı olan program (CWE-134 gösterici biçim):

#include <stdio.h>
int main(int argc, char **argv) {
    char buf[256];
    fgets(buf, sizeof buf, stdin);
    printf(buf);            // VULNERABLE: user data is the format string
    return 0;
}

Pointer conversion'ları ve direct parameter access ile stack'i leak et:

$ printf '%p %p %p %p\n' | ./fmt
0x7ffd... 0x7f... 0x55... (nil)
$ printf 'AAAA.%6$p\n' | ./fmt          # direct access to the 6th argument slot
AAAA.0x41414141                          # our "AAAA" appears -> offset 6 is our buffer

Kontrol edilen offset bilindiğinde, %n ile yaz. Hedef adresi buffer'a yerleştir, yazdırılan uzunluğu istenen değere kadar doldur, sonra %n'i ona nişanla:

# pseudo-layout: [target addr][padding...]%<width>c%<offset>$hn
# %<offset>$hn writes (chars printed so far) into *target as a 2-byte value

Warning

%n write'larını yanlış yapmak kolaydır: karakter sayısı kümülatiftir, dolayısıyla önce daha büyük 16-bit yarıları inşa et ve write'ları değere göre sırala. _FORTIFY_SOURCE'lu modern glibc, writable-format string'lerde %n'i reddeder ve birçok distro -Wformat-security / -Werror=format-security ile build eder, dolayısıyla literal bir printf(user) çoğunlukla compile olmaz ya da runtime'da abort eder.

Offset neden önemli

"offset" (örn. 6$), printf'in ilk variadic slot'undan input byte'larının stack'te oturduğu yere kadar argument slot'ları cinsinden mesafedir. AAAA gibi bir marker'ı ardından %1$p %2$p ... göndererek ve hangi index'in 0x41414141 yazdırdığını not ederek bul. Oradan %offset$p kendi kontrol ettiğin word'leri okur ve %offset$n yerleştirdiğin bir pointer üzerinden yazar.

Detection

  • -Wformat -Wformat-security ile compile et; GCC/Clang non-literal format string'leri işaretler. -Werror=format-security onları build hatalarına çevirir.
  • Format argument'ı bir string literal olmayan herhangi bir printf/fprintf/sprintf/ syslog/err call'u için static analysis ve code review.
  • Runtime: _FORTIFY_SOURCE, writable bir format string'teki %n'de abort eder.

Mitigation

  • Format string'i statik ve asla user-controlled olmayacak şekilde tut: printf(user) yerine printf("%s", user) (CWE-134'ün birincil mitigation'ı).
  • Mümkün olduğunda %n'siz API'leri tercih et; glibc'de format string'in read-only bellekteki mapping'leri %n'i devre dışı bırakır.
  • Harici kaynaklı veriyi bir formatting fonksiyonuna ulaşmadan önce validate/encode et.
  • Full RELRO + ASLR, ortaya çıkan herhangi bir write'ın etkisini sınırlar.

İlgili notlar

Bu, genel CWE-134 bug sınıfıdır (read + write birlikte). Yalnızca leak tarafının (%x/%p/%s) ayrıntısı için format-string-read sub-primitive'ine bak.

References