Introduction
Most of the malwares in the wild are adding some protections techniques to their malicious software against Virtual Machines to make their malwares more harder to run inside virtual machine or sandboxes for analysis purposes. In this post I’m going to create a KVM based Windows 11 virtual machine trying to evade some VM detection tools.
Virtual Machine Specs
Virtual Machine Hypervisor: KVM Operating System: Windows 10 / Windows 11 CPU Cores: 6 RAM Size: 6244 MB
Create the Virtual Machine
Moving to the creation of our VM with specs mentioned earlier.
After providing the disk space, ram size and cpu cores change the MAC Address of the network interface you provided with known mac address vendor, You can download the mac vendors json files from https://maclookup.app and choose whatever you want.
In this case I have choosed vendor PHYGITALL SOLUÇÕES EM INTERNET DAS COISAS and the mac address will be 8C:1F:64:B4:67:88
And leave the bios firmware and chipset as it is.
Then apply and begin installation.
By the way you should know how to install windows os inside kvm hypervisor as it’s a bit different from other hypervisors, Install the os and the QEMU tools and get back
Install windows without XApps Bloatware and Microsoft Account
Want to install windows without installing the XApps bloatware with only 2-clicks ? Here’s a hidden trick, While in Windows Setup window, Select English (World) in Time and currency format
After installation is done, You will notice that no XApps bloatwares like candy crush, tiktok, instagram, etc. didn’t installed.
While going through the installation process, You will get this page that will ask you to sign-in with your Microsoft account.
To bypass that, Hit Shift + F10 and type inside the command prompt OOBE\BYPASSNRO
then hit Enter.
It will restart the machine, After it’s up hit Shift + F10 again and type ipconfig /release
and hit Enter.
Now Select I don’t have internet and hit Next.
Now you should proceed without sign-in Microsoft account.
Create VMProtected executable binary
At first I have created a simple golang binary that will pops up a windows message box when it executes successfully.
package main
import "tawesoft.co.uk/go/dialog"
func main() {
dialog.Alert("Software executed successfully - r0ttenbeef")
}
I just like using golang, You can use anything else you want.
For the sake of this example I have used an already existing package instead of doing complicated windows API calls. Now will try to compile the golang binary for windows 64bit system.
go mod init main
go get -v tawesoft.co.uk/go/dialog
GOOS=windows GOARCH=amd64 go build -v -o vmprotected.exe -ldflags="-s -w -H windowsgui"
So it should compile successfully and see file vmprotected.exe
Now move the compiled binary vmprotected.exe
to a windows box that have VMProtect Demo Version that we will try to evade it.
After pressing on Add Function we will check the Compilation Type and give it value Ultra (Mutation + Virtualization) by doing bunch of clicks against it.
And then go to Options and give yes to all values, We are trying to make it as hard as possible to be evaded right there. Then press the compile button above behind the exe name.
It should compile and packed successfully with output name vmprotected.vmp.exe, If we tried to run the exe it will pops up an error message “Sorry, this application cannot run under a Virtual Machine.”
Bypass VMProtect detection
Now the virtual machine should be up and running, If we tried to open taskmanager
and navigating to Performance tab we should see it’s a virtual machine.
We need to get rid of that detection, We can do this by modifying the vm XML using virsh
.
- Turnoff the virtual machine
- Then edit the virtual machine xml
virsh edit <YOUR_MACHINE_NAME>
- Find the following line
<cpu mode='host-passthrough' check='none' migratable='on'>
<topology sockets='1' dies='1' cores='6' threads='1'/>
- Under the
topology
tag add the following line
<feature policy='disable' name='hypervisor'/>
- Now before the
</features>
tag add the following line
<kvm>
<hidden state='on'/>
</kvm>
So it should look like this
Now start the vm again and check the taskmanager
, it should looks different now.
If we tried to run the vmprotected binary vmprotected.vmp.exe it should run normally now.
Bypass the CPUID detections
Alright that was surprisingly easy, We need it more harder so i will try to run Pafish now which is a testing tool that uses different techniques to detect virtual machines and malware analysis environments in the same way that malware families do. When it tries to detect sandbox environment it passed a lot of entries due to the specs we have already defined.
Another detection that Pafish does is detecting the hypervisor vendor
If we checked the code of Pafish we will find out that it’s grabbing it from CPUID
We can evade this by doing the following:
- Turnoff the virtual machine
- Edit the virtual machine xml using
virsh
virsh edit <MACHINE_NAME>
- Before the tag
</hyperv>
add the following
<vendor_id state='on' value='ANYTHING YOU WANT'/>
It should looks like this
If we checked again will see it know passed successfully but “Checking hypervisor bit in cpuid feature bits” is not yet.
If we check the Pafish code will see it checks for the CPUID bits which returns a constant signatures
function: KVM_CPUID_SIGNATURE (0x40000000)
returns:
eax = 0x40000001
ebx = 0x4b4d564b
ecx = 0x564b4d56
edx = 0x4d
So to bypass this do the following:
- Turnoff the virtual machine
- Edit the virtual machine xml using
virsh
virsh edit <MACHINE_NAME>
- Add and edit the lines between
<cpu
tags to be look like the following
<cpu mode='host-model' check='partial'>
<topology sockets='1' dies='1' cores='6' threads='1'/>
<feature policy='disable' name='svm'/>
<feature policy='disable' name='vmx'/>
<feature policy='disable' name='hypervisor'/>
<feature policy='disable' name='aes'/>
<feature policy='disable' name='rdtscp'/>
</cpu>
So it should looks like this
So now it should pass the CPUID bits detection
Here if you noticed that “Checking the difference between CPU timestamp counters (rdtsc) forcing VM exit” is traced, Actually patching the RDTSC VM Exit on CPUID is pain in the butt which is requires recompiling the host kernel from source, you can try this patch called BetterTiming yourself.
QEMU detection bypass
At the QEMU it didn’t pass the first detection Scsi port->bus->target id->logical unit id-> 0 identifier
which is an exact registry key path the checks for “Identifier” value.
So if we changed “Identifier” value to something else it will bypass this detection.
Also if you saw this registry in Scsi Bus 1
like below:
Then try to remove any mounted CDROM from the Virtual Machine and after shutting down the VM and rerun it again the registry key should disappear.
Now at Bochs detection it traced the registry path HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System
and check for the “SystemBiosVersion” Value.
So if we changed “SystemBiosVersion” value to something else it will bypass this detection.
Now most of the detection has been bypassed.
Patching WMI Entries
There’s another tool al-khaser which also detects the VM/Sandbox environments that checks for more evidence about the running environment.
Here most of the WMI queries has detected that it’s running inside a VM, We will try to modify some of it as there’s multiple WMI classes that I wasn’t able to modify it.
If we checked the al-khaser source code in the WMI section, we will see that it’s doing some queries against WMI classes.
To manipulate this I have wrote an MOF Managed Object Format to modify some of this WMI variables.
#pragma namespace ("\\\\.\\root\\CIMv2")
#PRAGMA AUTORECOVER
/* PS C:> get-wmiobject -class Win32_BIOS */
class Win32_BIOS
{
[key] string SMBIOSBIOSVersion;
string Manufacturer;
string SerialNumber;
string Name;
uint16 BiosCharacteristics[];
string Version;
};
[DYNPROPS]
instance of Win32_BIOS
{
SMBIOSBIOSVersion = "6.0";
Manufacturer = "Synergies Intelligent Systems";
SerialNumber = "b3 5b 87 f1 2d 24 70 8c-b3 5b 87 f1 2d 24 70 8c";
Name = "Synergies Intelligent Systems Inc.";
BiosCharacteristics = {1,2,3};
Version = "INTEL - 6040001";
};
/* PS C:> get-wmiobject -class Win32_ComputerSystem */
class Win32_ComputerSystem
{
[key] string Name;
string Domain;
string Manufacturer;
string Model;
string OEMStringArray[];
};
[DYNPROPS]
instance of Win32_ComputerSystem
{
Name = "IE11WIN8_1";
Domain = "WORKGROUP";
Manufacturer = "Synergies";
Model = "Synergies Intelligent Systems";
OEMStringArray = {"Dave Network Corp"};
};
/* PS C:> Get-WmiObject -Query "SELECT * FROM Win32_DiskDrive" */
class Win32_DiskDrive {
[key] string Model;
string Caption;
};
[DYNPROPS]
instance of Win32_DiskDrive {
Model = "Synergies SCSI Disk Device";
Caption = "Synergies SCSI Disk Device";
};
/* PS C:> Get-WmiObject -Query "SELECT * FROM Win32_Fan" */
class Win32_Fan {
[key] string Name;
string Description;
string DeviceID;
string SystemName;
};
[DYNPROPS]
instance of Win32_Fan {
Name = "Dave Network";
Description = "Synergies Cooling Systems Status";
DeviceID = "F0-22-1D-4E";
SystemName = "Synergies Intelligent Systems";
};
/* PS C:> Get-WmiObject -Query "SELECT * FROM Win32_CacheMemory" */
class Win32_CacheMemory {
[key] string Name;
string Description;
string DeviceID;
string Status;
};
[DYNPROPS]
instance of Win32_CacheMemory {
Name = "Dave Network";
Description = "Synergies Caching";
DeviceID = "F0-22-1D-4E";
Status = "Memory Caching Enabled";
};
/* PS C:> Get-WmiObject -Query "SELECT * FROM Win32_MemoryDevice" */
class Win32_MemoryDevice {
[key] string Name;
string Description;
string DeviceID;
string Status;
};
[DYNPROPS]
instance of Win32_MemoryDevice {
Name = "Dave Network";
Description = "Synergies Memory Device";
DeviceID = "F0-22-1D-4E";
Status = "OK";
};
/* PS C:> Get-WmiObject -Query "SELECT * FROM Win32_VoltageProbe" */
class Win32_VoltageProbe {
[key] string Name;
string Description;
string DeviceID;
string Status;
};
[DYNPROPS]
instance of Win32_VoltageProbe {
Name = "Dave Network";
Description = "Synergies electronic voltmeter";
DeviceID = "F0-22-1D-4E";
Status = "OK";
};
/* PS C:> Get-WmiObject -Query "SELECT * FROM Win32_PortConnector" */
class Win32_PortConnector {
[key] string Name;
string Manufacturer;
string Model;
};
[DYNPROPS]
instance of Win32_PortConnector {
Name = "Dave Network";
Manufacturer = "Synergies physical connection ports";
Model = "Centronics";
};
Save it to a file with .mof
extension and run it with mof compiler mofcomp.exe
mofcomp.exe wmi_hide.mof
It evaded most of it but still the WMI CIM ones as I didn’t find a proper way to modify them.
Uninstall QEMU VirtIO driver (If installed)
When we running al-khaser in the QEMU Detection Section we will see its checking for some registry keys, files and processes.
So we will need to uninstall the QEMU Guest Agent and its services.
Reattach the VirtIO ISO image from Virt-Manager
Then restart the machine and open virtio-win mounted drive.
Also check mark on “Remove all customized settings” and then click “Remove” and restart the machine to take effect.
Now will stop all QEMU Agent Services, Press WinKey + R and run services.msc
and stop QEMU Guest Agent service and QEMU Guest Agent VSS Provider.
After that open windows cmd with Admin Privileges, and go to C:\Program Files\Qemu-ga
and run these two commands.
.\qemu-ga.exe -s uninstall
.\qemu-ga.exe -s vss-uninstall
So the services should be removed completely. Unmount the disk driver and remove the Qemu-ga path.
Also remove “Spice Agent” Service and then delete its path at C:\Program Files\Spice Agent
recursively.
sc delete spice-agent
Firmware Table Changes
After the previous changes the QEMU changes passed except two other checks.
Those check are actually checking some strings in the firmware tables of SMBIOS and ACPI like QEMU , BOCHS , BXPC as we see in the source code.
If we used FirmwareTablesView in the Firmware Provider column we will see it more obviously.
We can overwrite most of these strings by editing some elements to the XML of the machine by running virsh edit <MACHINE_NAME>
, under <os>
element add <smbios mode='sysinfo'/>
, Then add the following elements under <vcpu>
tag.
<sysinfo type='smbios'>
<bios>
<entry name='vendor'>Dell Inc.</entry>
<entry name='version'>2.5.2</entry>
<entry name='date'>01/28/2015</entry>
<entry name='release'>2.5</entry>
</bios>
<system>
<entry name='manufacturer'>Dell Inc.</entry>
<entry name='product'>PowerEdge R720</entry>
<entry name='version'>Not Specified</entry>
<entry name='serial'>H5DR542</entry>
<entry name='uuid'>SHOULD MATCH THE UUID OF THE DOMAIN .. CHECK THE ELEMENT uuid ABOVE</entry>
<entry name='sku'>SKU=NotProvided;ModelName=PowerEdge R720</entry>
<entry name='family'>Not Specified</entry>
</system>
<baseBoard>
<entry name='manufacturer'>Dell Inc.</entry>
<entry name='product'>12NR12</entry>
<entry name='version'>A02</entry>
<entry name='serial'>.5KT0B123.ABCDE000000001.</entry>
<entry name='asset'>Not Specified</entry>
<entry name='location'>Null Location</entry>
</baseBoard>
<chassis>
<entry name='manufacturer'>Lenovo</entry>
<entry name='version'>none</entry>
<entry name='serial'>J30038ZR</entry>
<entry name='asset'>none</entry>
<entry name='sku'>Default string</entry>
</chassis>
<oemStrings>
<entry>myappname:some arbitrary data</entry>
<entry>otherappname:more arbitrary data</entry>
</oemStrings>
</sysinfo>
Surely you can modify all these strings between the <entry>
elements, Then poweroff the machine and poweron again and check the FirmwareTablesView.
The changes has its effects but still some strings related to the QEMU, I actually tried alot of steps to change it non of them worked until now. However we will move to the next step.
Modify and rebuild SeaBIOS from source
Here we will try to modify some values and replace it with something more realistic.
- So first clone the SeaBIOS source code
git clone https://github.com/coreboot/seabios.git
- Then will modify the values in
src/config.h
and replace the QEMU and Bochs string with something else.//#define BUILD_APPNAME "QEMU" //#define BUILD_CPUNAME8 "QEMUCPU " //#define BUILD_APPNAME6 "QEMU " //#define BUILD_APPNAME4 "QEMU" #define BUILD_APPNAME "Bochs" #define BUILD_CPUNAME8 "BOCHSCPU" #define BUILD_APPNAME6 "BOCHS " #define BUILD_APPNAME4 "BXPC"
- So it will be something similer to this:
//#define BUILD_APPNAME "QEMU" //#define BUILD_CPUNAME8 "QEMUCPU " //#define BUILD_APPNAME6 "QEMU " //#define BUILD_APPNAME4 "QEMU" #define BUILD_APPNAME "GIGABYTE" #define BUILD_CPUNAME8 "GIGABYTE" #define BUILD_APPNAME6 "AORUS " #define BUILD_APPNAME4 "AORUS"
- After saving
src/config.h
file build the current modified version and replace it with the oldbios.bin
one.make sudo cp /usr/share/seabios/bios.bin /usr/share/seabios/bios.bin.bak sudo cp out/bios.bin /usr/share/seabios/bios.bin
Make sure that the machine is actually booting from bios.bin file, If not do the same instructions above and replace it with whatevery bios file that the machine is booting from.
Manipulating the registry keys
In the beginning of this post I have showed an example of modifying the registry key HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\SystemBiosVersion
so now we will proceed modifying it.
In al-khaser program uses multiple of registry keys to detect the VM environment, We will try to modify all of them to evade the VM detection.
So in this registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\SCSI
there’s a lot of keys that contains strings like Red_Hat
and QEMU
, So we should modify all of these strings.
So here al-khaser checking for multiple registry key values in registry_disk_enum
function.
So in the path HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\SCSI
we see most of registry key values contains “QEMU” string.
So every key that contains value “QEMU” and “VirtIO” should be replaced by any other strings which is easy to do.
- Summary of registry keys that needs to change
Registry Path | Name |
---|---|
HKLM\HARDWARE\ACPI\DSDT |
BOCHS_\BXPC___ |
HKLM\HARDWARE\ACPI\FADT |
BOCHS_\BXPC___ |
HKLM\HARDWARE\ACPI\RSDT |
BOCHS_\BXPC___ |
HKLM\SYSTEM\CurrentControlSet\Enum\SCSI |
Replace any QEMU related strings |
HKLM\HARDWARE\DEVICEMAP\Scsi\Scsi Port 0\Scsi Bus 0\Target Id 0\Logical Unit Id 0 |
Identifier |
HKLM\HARDWARE\DEVICEMAP\Scsi\Scsi Port 0\Scsi Bus 1\Target Id 0\Logical Unit Id 0 |
Identifier Remove mounted CDROM |
HKLM\HARDWARE\DESCRIPTION\System |
SystemBiosVersion |
The problem here is you have to change registry subkey that contains same strings but you will face a permission issue, As the program checks all keys and subkeys in the registry path looking for the strings mentioned earlier.
Keep updated with this post as I will add more techniques for VM detection bypasses