printf-Oriented Programming¶
A single format-string-controlled
printfcall is Turing-complete:%nwrites, 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/%hhnwrite primitive'idir.%hhnbyte sayısını mod 256 olarak birchar*'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¶
- N. Carlini, A. Barresi, M. Payer, D. Wagner, T. R. Gross. Control-Flow Bending: On the Effectiveness of Control-Flow Integrity. USENIX Security 2015. — https://www.usenix.org/conference/usenixsecurity15/technical-sessions/presentation/carlini
- N. Carlini. printf-tac-toe — tic-tac-toe in a single call to printf (README: printf Turing-completeness writeup). — https://github.com/carlini/printf-tac-toe/blob/master/README.md