Commonly, malware will fingerprint the host it executes on, in an attempt to discover more about its environment and act accordingly.
Part of this process is quite often dedicated to analyzing specific data in order to figure out if the malware is running inside a VM, which could just be a honeypot or an analysis environment, and also for detecting the presence of other software.  For example, malware will quite often try to find out if a system monitoring tool is running (procmon, sysmon, etc.) and which AV software is installed.
In this article, we will introduce another method of fingerprinting a host that could be potentially abused by malware.

Common ways of host fingerprinting

In this section we provide a short list of well-known ways of detecting a VM environment, and the presence of other security software, that are often applied by malware. Note that the following list is not exhaustive.

  • Process Enumeration
  • Loaded modules Enumeration
  • File Enumeration
  • Data Extracted from Windows Registry (Hard disk, BIOS etc…)
  • Loaded drivers enumeration
  • Open a handle to specific named device object
  • System Resources Enumeration (CPU cores, RAM, Screen Resolution, etc…)

The PoolTag way

If you have some experience with Windows kernel drivers development & analysis, you should be familiar with the ExAllocatePoolWithTag [1] function that is used in order to allocate memory chunks at kernel level.  The key part here is the ‘Tag’ parameter that is used in order to provide some sort of identification for a specific allocation.
If something goes wrong, for example because of a memory corruption issue, we can use the specified Tag (up to four characters) in order to associate a buffer with a code path in the kernel driver that allocated that memory chunk. This method is adequate for detecting the presence of kernel drivers, and thus software that loads modules in the kernel that could potentially circumvent the fingerprinting methods mentioned above, which rely on information that a driver could potentially alter.  In other words, it is ideal for detecting the stuff that really matters from the malware author’s point of view.
For example, security/monitoring software might try to hide its processes and files by registering callback filters at kernel level.  An analyst might try to harden their VM environment by removing artefacts from the registry and other things that malware is usually searching for.
However, what a security software vendor and/or analyst probably won’t do, is modify specific kernel drivers used by their own program and/or system/VM environment to constantly alter the Tags of their kernel pool allocations.

Getting PoolTag Information

This information can be obtained by calling the NtQuerySystemInformation [2] function and selecting SystemPoolTagInformation (0x16) [3] for the SysteminformationClass parameter.
The aforementioned function and the associated SysteminformationClass possible values are partially documented on MSDN, but fortunately with some online research we can find some documentation done by researchers. In particular, Alex Ionescu has documented a lot of otherwise undocumented stuff about Windows internals in his NDK [3] project.
For this proof of concept, we wrote our own version of getting and parsing PoolTag information, but if you want to go the GUI way to experiment with the results, then PoolMonEx [4] is a really nice tool to play with.
For instance, the following is a screenshot of our tool’s output.  Source code below.

Which you can compare with regards to the Nbtk tagged allocations results from PoolMonEx as shown below.

QueryPoolTagInfo.cpp

#include "Defs.h"
#include <iostream>
using namespace std;
int main()
{
	NTSTATUS NtStatus = STATUS_SUCCESS;
	BYTE * InfoBuf = nullptr;
	ULONG ReturnLength = 0;
	_ZwQuerySystemInformation ZwQuerySystemInformation = (_ZwQuerySystemInformation)GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwQuerySystemInformation");
	do{
		NtStatus = ZwQuerySystemInformation(SystemPoolTagInformation, InfoBuf, ReturnLength, &ReturnLength);
		if (NtStatus == STATUS_INFO_LENGTH_MISMATCH)
		{
			if (InfoBuf != nullptr)
			{
				delete[] InfoBuf;
				InfoBuf = nullptr;
			}
			InfoBuf = new (nothrow) BYTE[ReturnLength];
			if (InfoBuf != nullptr)
				memset(InfoBuf, 0, ReturnLength);
			else
				goto Exit;
		}
	} while (NtStatus != STATUS_SUCCESS);
	PSYSTEM_POOLTAG_INFORMATION pSysPoolTagInfo = (PSYSTEM_POOLTAG_INFORMATION)InfoBuf;
	PSYSTEM_POOLTAG psysPoolTag = (PSYSTEM_POOLTAG)&pSysPoolTagInfo->TagInfo->Tag;
	ULONG count = pSysPoolTagInfo->Count;
	cout << "Count: " << count << endl << endl;
	for (ULONG i = 0; i < count; i++)
	{
		cout << "PoolTag: ";
		for (int k = 0; k < sizeof(ULONG); k++)
			cout << psysPoolTag->Tag[k];
		cout << endl;
		if (psysPoolTag->NonPagedAllocs != 0)
		{
			cout << "NonPaged Allocs: " << psysPoolTag->NonPagedAllocs << endl;
			cout << "NonPaged Frees: " << psysPoolTag->NonPagedFrees << endl;
			cout << "NonPaged Pool Bytes Used: " << psysPoolTag->NonPagedUsed << endl;
		}
		else
		{
			cout << "Paged Allocs: " << psysPoolTag->PagedAllocs << endl;
			cout << "Paged Frees: " << psysPoolTag->PagedFrees << endl;
			cout << "Paged Pool Bytes Used: " << psysPoolTag->PagedUsed << endl;
		}
		psysPoolTag++;
		cout << endl << "-------------------------------" << endl;
		cout << endl << "-------------------------------" << endl << endl;
	}
	if (InfoBuf != nullptr)
		delete[] InfoBuf;
Exit:
	cin.get();
	return 0;
}

Defs.h

#include <Windows.h>
#define SystemPoolTagInformation (DWORD)0x16
#define STATUS_SUCCESS 0
#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004
typedef DWORD SYSTEM_INFORMATION_CLASS;
typedef struct _SYSTEM_POOLTAG
{
	union
	{
		UCHAR Tag[4];
		ULONG TagUlong;
	};
	ULONG PagedAllocs;
	ULONG PagedFrees;
	SIZE_T PagedUsed;
	ULONG NonPagedAllocs;
	ULONG NonPagedFrees;
	SIZE_T NonPagedUsed;
}SYSTEM_POOLTAG, *PSYSTEM_POOLTAG;
typedef struct _SYSTEM_POOLTAG_INFORMATION
{
	ULONG Count;
	SYSTEM_POOLTAG TagInfo[ANYSIZE_ARRAY];
}SYSTEM_POOLTAG_INFORMATION, *PSYSTEM_POOLTAG_INFORMATION;
typedef NTSTATUS(WINAPI *_ZwQuerySystemInformation)(
	_In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,
	_Inout_   PVOID                    SystemInformation,
	_In_      ULONG                    SystemInformationLength,
	_Out_opt_ PULONG                   ReturnLength
	);

Targetting PoolTag Information

In order to give some sense to the acquired PoolTag information, it is necessary to analyse those drivers that we are interested in. By searching for calls to ExAllocatePoolWithTag we can log specific tags used by those drivers and keep them in our list.
At this point, you should be aware that any driver can use any tag at will, and for that reason it makes sense to try to find some tags that appear to be less common and not otherwise used by standard Windows kernel drivers and/or objects.
With that being said, this method of detecting specific drivers might produce false positives if not used with extra care.

A PoolTag Example List

For the sake of demonstrating a proof of concept we have collected some PoolTag information from specific drivers.

  • VMWare (Guest OS)
    • vm3dmp.sys (Tag: VM3D)
    • vmci.sys (Tags: CTGC, CTMM, QPMM, etc…)
    • vmhgfs.sys (Tags: HGCC, HGAC, HGVS, HGCD etc…)
    • vmmemctl.sys (Tag: VMBL)
    • vsock.sys (Tags: vskg, vskd, vsks, etc…)
  • Process Explorer
    • procexp152.sys (Tags: PEOT, PrcX, etc…)
  • Process Monitor
    • procmon23.sys (Tag: Pmn)
  • Sysmon
    • sysmondrv.sys (Tags: Sys1, Sys2, Sys3, SysA, SysD, SysE, etc…)
  • Avast Internet Security
    • aswsnx.sys (Tags: ‘Snx ‘, Aw++) (We used single quotes in the first because it ends with a space character)
    • aswsp.sys (Tags: pSsA, AsDr)

Conclusion

Just like every other method, this one has its strengths and weaknesses.
This method cannot be easily circumvented, especially in 64-bit Windows where the Kernel Patch Protection (Patch Guard) does not allow us to modify kernel functions among other things, and thus directly hooking those such as NtQuerySystemInformation is not a solution anymore for security and monitoring tools.
Also, this method is not affected by drivers that attempt to hide/block access to specific processes, files, and registry keys, from userland processes.
In addition, this method could be potentially used to fingerprint the host further.
By searching for specific tags of Windows objects that are being introduced in the OS, we can determine its major version.
For example, by comparing the poolTag information (pooltag.txt) that comes with different versions of Windbg, in this case Windows 8.1 x64 and Windows 10 x64 (Build 10.0.15063), we were able to find PoolTags that are used in Windows 10 by the netio.sys kernel driver such as Nrsd, Nrtr, Nrtw, but not in Windows 8.1
We later did a quick verification by using two VMs, and we could indeed find pool allocations with at least two of the aforementioned tags in Windows 10, while there were none of those in our Windows 8.1 VM.
That being said, it is a common and good practice for kernel driver development to use tags that make some sense based on the module that allocates them and their purpose.
On the other hand, as mentioned already, PoolTags can be used at will and for that reason we have to be careful about which ones we are targeting.
One last thing to mention is that PoolTag information changes all the time, in other words chunks of memory are constantly allocated and de-allocated, and for this reason we should keep this in mind when choosing the PoolTag to search for.
Even though this method might look more experimental than practical, in reality when malware is searching for specific monitoring and security software, `PoolTag` information can be very reliable.
References

  1. https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/wdm/nf-wdm-exallocatepoolwithtag
  2. https://msdn.microsoft.com/en-us/library/windows/desktop/ms724509(v=vs.85).aspx
  3. https://github.com/arizvisa/ndk
  4. http://blogs.microsoft.co.il/pavely/2016/09/14/kernel-pool-monitor-the-gui-version/