Skip to content

printf-Oriented Programming

A single format-string-controlled printf call is Turing-complete: %n writes, positional (%N$) arguments, and width specifiers compose a weird machine that performs arbitrary computation and memory writes from one bug — even under a shadow stack.

Sınıflandırma notu: bu bir attack/bypass primitive'idir (ROP/COP/JOP ailesi gibi); yendiği shadow-stack / CFI savunmalarına komşuluğu nedeniyle bu KB'de mitigation dizini altında tutulur.

Mechanism

Neden çalışır

printf yalnızca bir output fonksiyonu değildir — format string üzerinde kontrol verildiğinde küçük bir interpreter'dır. Kaldıraç, yazdırmak yerine bir pointer argümanına "şu ana kadar yazdırılan byte sayısını yazan" %n'dir. Carlini, Barresi, Payer, Wagner ve Gross Control-Flow Bending'de (USENIX Security 2015) bunun tek bir printf çağrısını Turing-complete yaptığını gösterdi; tekniğe printf-oriented programming adını verdiler.

Üç format özelliği makineyi besler:

  • %n / %hhn write primitive'idir. %hhn byte sayısını mod 256 olarak bir char*'a yazar; değeri, formatın ondan önce ürettiği output miktarına göre belirlenen kontrol edilebilir tek-byte'lık bir write verir.
  • Positional argümanlar (%2$d, %3$hhn) formatın aynı argüman slot'larını sıra dışı yeniden okumasına izin verir, böylece tek bir stack/argüman layout'u register'lar gibi adreslenebilir.
  • Width specifier'ları (%8d, %255d, %*$d) yazdırılan byte sayısını seçilmiş bir sabite şişirir; bu da tam olarak %n'in ardından yazdığı değerdir.

Invariant: %n'in yazdığı değer, saldırganın kontrol ettiği format string tarafından tamamen belirlenir ve positional argümanlar o string'in seçilmiş bellek hücreleri üzerinde hesap yapmasına izin verir. Her control transfer'i meşru printf çağrısının içinde kaldığı için saldırı hiç return etmeden keyfi hesaplama kurar — bu yüzden bir shadow stack ya da kaba bir CFI onu kesintiye uğratmaz. Makale, tekniğin test edilen biri hariç tüm binary'lere karşı çalıştığını bildirir.

Walkthrough

Klasik primitive: doğrudan printf'e beslenen saldırgan-kontrollü bir format string (bir format-string-write bug'ı).

1. Kontrol ettiğin argüman offset'ini bul. Buffer'ının varargs içindeki konumunu sızdır:

$ ./vuln 'AAAA.%p.%p.%p.%p.%p.%p'
AAAA.0x7fffffffe2c0.0x1.0x7ffff7...0x41414141...
                                  ^ our 'AAAA' appears at arg position 6

2. %n ile arbitrary write. Hedef address'i buffer'a yerleştir, sonra yazdırılan byte sayısını ona yazmak için positional bir %n kullan:

# write the value `count` to address `target` (4 bytes via %n)
payload  = p64(target)                 # arg slot 6 holds the pointer
payload += b'%' + str(count - 8).encode() + b'c'  # pad output to `count`
payload += b'%6$n'                     # *target = bytes_printed

%6$n argüman slot 6'yı (bizim target'ımız) dereference eder ve o anki byte sayısını saklar — değerin field width, addressin ise yerleştirdiğimiz pointer olduğu bir arbitrary-write.

3. %hhn ile byte-granüler write'lar. Milyarlarca pad byte yazdırmadan tam kontrol edilen bir değeri yazmak için onu dört %hhn write'ına bölün; her biri 256 modülünde bir sonraki byte'a pad'lenir:

# write 0xdeadbeef one byte at a time to target..target+3
for i, b in enumerate(bytes_in_print_order(0xdeadbeef)):
    payload += f"%{pad_to(b)}c%{slot+i}$hhn".encode()

4. Weird-machine encoding (printf-oriented programming). Carlini bir bit'i output uzunluğu olarak encode eder — "sıfır bit'i 00 00 dizisiyle temsil edilir" ve "bir-bit'i xx 00 dizisiyle temsil edilir; burada xx sıfır olmayan herhangi bir byte'tır" — yani bir hücrenin strlen'i 1 ya da 0'dır. Logic gate'ler %s+%hhn'den ortaya çıkar:

// OR:  *c = strlen(a) + strlen(b)
printf("%1$s%2$s%3$hhn", a, b, c);
// NOT: *b = (strlen(a) + 255) % 256   -> inverts the bit
printf("%1$255d%1$s%2$hhn", a, b);

Bunları zincirlemek (çağrıyı format string'i yeniden besleyen bir loop'a yerleştirerek) tamamen printf içinde keyfi hesaplama verir.

FORTIFY onu daraltır

_FORTIFY_SOURCE ve -Wformat-security, writable format string'lerde %n'i reddeder ve non-literal formatları işaretler; glibc ek olarak format writable bellekte yaşadığında %n'i reddeder. Gerçek hedefler tipik olarak hâlâ constant-format logging ya da kısmi kontrol üzerinden %n'i açığa çıkarır.

Detection

Non-constant bir format argümanıyla printf/fprintf/syslog'a ulaşan ve %n/%hhn içeren bir format string belirtidir. Compile-time -Wformat -Wformat-security ve writable-format %n'de FORTIFY runtime abort'u standart tetikleyicilerdir; %n içeren input'lar üzerinde fuzzing bug'ı ortaya çıkarır.

Mitigation

(Artık risk / bypass.) Hesaplama hiçbir zaman printf frame'ini terk etmediği için return-address savunmaları — stack canary'ler, shadow stack'ler, return-edge CFI — onu durdurmaz; bu makalenin CFI'ya karşı merkezi noktasıdır. Kalıcı düzeltmeler upstream'dedir: saldırgan verisini asla format string olarak geçirmeyin, FORTIFY ile build edin ve writable formatlarda %n'i yasaklayın. Yalnızca kısıtlı bir format sızdığında, saldırganlar düz format-string-write / format-string-read primitive'lerine ve ret2dlresolve tarzı devam adımlarına geri döner.

References