Insights
Blackbird: Defeating Anti-Virtualization Technologies

How does Blackbird make Windows lie to malware's faces? Most anti-analysis checks trust the kernel because they have no choice. Blackbird weaponizes this by modifying syscall, timing & registry return data, erasing VM-identifiers and much, much more.

Blackbird: Defeating Anti-Virtualization Technologies

Virtualization Detection & Anti-Debug Systems

What's the actual point of modifying NtQuerySystemInformation, or any syscall for that matter? They are one of the primary sources of truth for a process trying to detect virtualization or debugging.

Processes inspect things like running processes, debugger state, hypervisor indicators, firmware identifiers, timing behaviour, and other system configuration data. A surprising amount of that visibility ultimately flows through a single syscall: NtQuerySystemInformation.

If you can modify the result of that call, you can fabricate what usermode sees while still returning valid structures, correct buffer sizes, and a legitimate NTSTATUS. From usermode the kernel is assumed to be the ground truth, so detecting the deception becomes extremely difficult.

That was the original trick. Current Blackbird still does that, but it no longer treats one syscall as the entire world. It now uses NtQuerySystemInformation as one part of a wider NTAPI, registry, timing, telemetry, and runtime-policy layer. Same idea, much wider surface: let Windows build the truth, then decide which parts of that truth usermode is allowed to see.

WinApi

Ever wondered what EnumProcesses does under the hood?

Most Win32 APIs are wrappers around deeper Windows internals. To see what's really happening, we can inspect the call path using RESXCLI Utility for quick reverse-engineering, disassembly, function-locating, peinfo and symbol resolution:

resx dump KernelBase.dll EnumProcesses --funcs --depth 5

With the --funcs flag we can see the function calls of the target API.

API Call Map for EnumProcesses  [21 call site(s)]:
  ├── 0x1405CC  CALL  sub_001295D0 [internal]
  ├── 0x1405DC  CALL  ntdll.dll!RtlAllocateHeap [import]
  ├── 0x140613  CALL  ntdll.dll!NtQuerySystemInformation [syscall]
  ├── 0x140630  CALL  ntdll.dll!RtlFreeHeap [import]
 ...

EnumProcesses doesn’t enumerate anything itself. It simply calls NtQuerySystemInformation, asks for SystemProcessInformation, and returns whatever the kernel provides.

That syscall obtains a lot more information than just processes:

  • Running processes
  • Loaded drivers
  • Handle tables
  • Kernel debugger state
  • CI flags
  • Firmware information
  • Hypervisor presence
  • Blackbird/controller artefacts themselves

NtQuerySystemInformation is relied on by a LOT of security tooling, malware, debuggers and pretty much any other tool that interacts with the system substantially.

And when the check moves outside that one syscall, Blackbird follows it there too. Timing probes go through NtQueryPerformanceCounter, memory and injection behaviour goes through the virtual memory and section syscalls, and registry probes hit the callback layer.

Now what if it were lying to you?

From Win32 to Syscalls

EnumProcesses runs in user mode, so eventually it has to cross into the kernel. The question is where that boundary actually is.

The answer is a System Call, aka syscall.

Modern Windows user-mode APIs are mostly thin wrappers around Native API functions exported from ntdll.dll, these functions begin with Nt* or Zw* and represent the lowest interface between userland and the Windows kernel.

Reversing a WinApi Function

We'll use OpenProcess as an example, we'll be working with RESXCLI Utility for quick reverse-engineering, disassembly, function-locating, peinfo and symbol resolution to do this;

resx dump KernelBase.dll OpenProcess --funcs --depth 5

API Call Map for OpenProcess  [3 call site(s)]:
  ├── 0x42E6F  CALL  ntdll!NtOpenProcess [syscall]
  │   kernel: ntoskrnl.exe!NtOpenProcess
  ├── 0x42E8F  CALL  InitOnceBeginInitialize [internal]
  └── 0x42E96  JMP  OpenProcess [tail call]

The call map shows that OpenProcess ultimately resolves to ntdll.dll!NtOpenProcess, which is the syscall boundary we care about. Since there is not much else happening here, we can move directly to NtOpenProcess.

Reversing Syscall Stubs

We can again use RESXCLI Utility for quick reverse-engineering, disassembly, function-locating, peinfo and symbol resolution to dump the function to get a quick output;

resx dump ntdll.dll NtOpenProcess --depth 5

ntdll.dll!NtOpenProcess  RVA 0x00161F40, VA 0x180161F40
  00161F40  4C 8B D1                       MOV        r10,rcx
  00161F43  B8 26 00 00 00                 MOV        eax,26h ; System Service Number

; Check KUSER_SHARED_DATA for compatibility flag
  00161F48  F6 04 25 08 03 FE 7F 01        TEST       byte ptr [7FFE0308h],1 ;
  00161F50  75 03                          JNE        short 0000000180161F55h

  00161F52  0F 05                          SYSCALL    ; System Call
  00161F54  C3                             RET

; Legacy Syscall EP
  00161F55  CD 2E                          INT        2Eh
  00161F57  C3                             RET

To make this easier, we can run cfg to see the actual control flow of the function;

resx cfg ntdll.dll NtOpenProcess

CFG: ntdll.dll!NtOpenProcess
  RVA: 0x00161F40  |  VA: 0x180161F40  |  arch: x64
  blocks:  3
  entry :  block_00161F40

block_00161F40  [entry]:  [4 insn]  range 0x00161F40..0x00161F50
    0x00161F40  4C 8B D1                    mov r10,rcx
    0x00161F43  B8 26 00 00 00              mov eax,26h
    0x00161F48  F6 04 25 08 03 FE 7F 01     test byte ptr [7FFE0308h],1
    0x00161F50  75 03                       jne short 0000000180161F55h  ; NtOpenProcess+0x15
    edges:
      [taken] JNE -> block_00161F55 (NtOpenProcess+0x15)
      [fallthrough] fallthrough -> block_00161F52

block_00161F52  [exit]:  [2 insn]  range 0x00161F52..0x00161F54
    0x00161F52  0F 05                       syscall
    0x00161F54  C3                          ret
    edges:
      [exit] return

block_00161F55  [exit]:  [2 insn]  range 0x00161F55..0x00161F57
    0x00161F55  CD 2E                       int 2Eh
    0x00161F57  C3                          ret
    edges:
      [exit] return

All syscall stubs follow the same pattern. The syscall instruction flips execution from ring 3 (user mode) into ring 0 (kernel mode).

Windows configures this transition through the IA32_LSTARIA32e Mode System Call Target Address (R/W). Target RIP for the procedure when SYSCALL is executed in 64-bit mode. MSR64-bit control registers in x86 architecture used for debugging, performance monitoring, tracing program execution, and toggling CPU features.

IA32_LSTAR points to the kernel's syscall entry inside of ntoskrnl.exe, which then validates arguments, resolves the syscall number and dispatches the request via the System Service Dispatch Table (SSDT)The System Service Dispatch Table (SSDT) is the kernel’s table of system call handlers, mapping each System Service Number (SSN) to the address of the corresponding ntoskrnl!Nt* routine. When a user-mode `syscall` executes, the kernel uses the SSN to index the SSDT and dispatch execution to the correct kernel implementation.

The SSDT itself does not implement logic. It simply maps the System Service Number (SSN) provided by the user-mode stub to the corresponding ntoskrnl!Nt* routine.

NtQuerySystemInformation

For NtQuerySystemInformation, that SSDT dispatch lands in ntoskrnl.exe!NtQuerySystemInformation.

At first glance this routine looks surprisingly small, because the entry point is mostly a dispatcher rather than the full implementation. It normalizes the requested SystemInformationClass, performs a bounds check, uses a compact remap table to compress sparse case values, and then jumps through a second table to the corresponding kernel-side dispatch target.

NtQuerySystemInformation is mostly a dispatcher. The caller supplies a SYSTEM_INFORMATION_CLASS, the kernel maps it through a remap table, then jumps to the provider responsible for that class.

Using RESX to inspect the kernel implementation:

resx dump ntoskrnl.exe NtQuerySystemInformation

ntoskrnl.exe!NtQuerySystemInformation  [RVA 0x00AE07F0, VA 0x140AE07F0]
  Base0/RVA: 0x00AE07F0  |  VA: 0x140AE07F0
  00AE07F0  48 89 5C 24 10                 MOV        [rsp+10h],rbx
  00AE07F5  57                             PUSH       rdi
  00AE07F6  48 83 EC 30                    SUB        rsp,30h
  00AE07FA  48 8B FA                       MOV        rdi,rdx
  00AE07FD  8D 41 F8                       LEA        eax,[rcx-8]
  00AE0800  33 D2                          XOR        edx,edx
  00AE0802  4D 8B D9                       MOV        r11,r9
  00AE0805  66 89 54 24 40                 MOV        [rsp+40h],dx
  00AE080A  41 8B D8                       MOV        ebx,r8d
  00AE080D  44 8B D1                       MOV        r10d,ecx
  00AE0810  3D F6 00 00 00                 CMP        eax,0F6h
  00AE0815  77 53                          JA         short 0000000140AE086Ah  ; NtQuerySystemInformation+0x7A
  00AE0817  48 8D 0D E2 F7 51 FF           LEA        rcx,[140000000h]
  00AE081E  48 98                          CDQE
  00AE0820  0F B6 84 01 A0 08 AE 00        MOVZX      eax,byte ptr [rcx+rax+0AE08A0h]
  00AE0828  44 8B 8C 81 90 08 AE 00        MOV        r9d,[rcx+rax*4+0AE0890h]
  00AE0830  4C 03 C9                       ADD        r9,rcx
  00AE0833  41 FF E1                       JMP        r9
  00AE0836  CC                             INT3
[*] ~19 instructions, ~71 bytes

Switch Map
----------

Selector  : SystemInformationClass (SYSTEM_INFORMATION_CLASS)
Params    : 4
Prototype :
    SYSTEM_INFORMATION_CLASS SystemInformationClass,
    PVOID  SystemInformation,
    ULONG  SystemInformationLength,
    PULONG ReturnLength

Bias      : 0x8
Max       : 0xF6
Targets   : 4

Remap     : RVA 0x00AE08A0
Table     : RVA 0x00AE0890


NtQuerySystemInformation +0x49  [RVA 0x00AE0839]
When :
    SystemProcessorPerformanceInformation (0x8)
    SystemInterruptInformation (0x17)
    0x2A
    0x3D
    0x53
    0x64
    0x6C
    0x8D


NtQuerySystemInformation +0x5C  [RVA 0x00AE084C]
When :
    0x49


NtQuerySystemInformation +0x69  [RVA 0x00AE0859]
When :
    0x6B
    0x79
    0xB4
    0xD2..0xD3
    0xDE
    0xE7
    0xEE..0xF0
    0xFE


NtQuerySystemInformation +0x7A  [RVA 0x00AE086A]
When :
    0x09..0x16
    0x18..0x20
    SystemExceptionInformation (0x21)
    0x22..0x24
    SystemRegistryQuotaInformation (0x25)
    0x26..0x29
    0x2B..0x2C
    SystemLookasideInformation (0x2D)
    0x2E..0x3C
    0x3E..0x48
    0x4A..0x52
    0x54..0x63
    0x65..0x66
    SystemCodeIntegrityInformation (0x67)
    0x68..0x6A
    0x6D..0x78
    0x7A..0x85
    SystemPolicyInformation (0x86)
    0x87..0x8C
    0x8E..0xB3
    0xB5..0xD1
    0xD4..0xDD
    0xDF..0xE6
    0xE8..0xED
    0xF1..0xFB
    SystemBasicProcessInformation (0xFC)
    SystemHandleCountInformation (0xFD)


API Call Map for NtQuerySystemInformation  [4 call site(s)]:
  ├── 0xAE0833  JMP  NtQuerySystemInformation+0x49 [↳ switch · 8 case(s): 0x8, 0x17, 0x2A, 0x3D, +4 more]
  │   ├── 0xAE0842  CALL  KeQueryPrimaryGroupThread [internal]
  │   ├── 0xAE0857  JMP  NtQuerySystemInformation [tail call]
  │   └── 0xAE087C  CALL  ExpQuerySystemInformation [internal]
  ├── 0xAE0833  JMP  NtQuerySystemInformation+0x5C [↳ switch · 1 case(s): 0x49]
  │   ├── 0xAE0857  JMP  NtQuerySystemInformation [tail call]
  │   └── 0xAE087C  CALL  ExpQuerySystemInformation [internal]
  ├── 0xAE0833  JMP  NtQuerySystemInformation+0x69 [↳ switch · 11 case(s): 0x6B, 0x79, 0xB4, 0xD2..0xD3, +4 more]
  │   └── 0xAE087C  CALL  ExpQuerySystemInformation [internal]
  └── 0xAE0833  JMP  NtQuerySystemInformation+0x7A [↳ switch · 227 case(s): 0x9..0x16, 0x18..0x29, 0x2B..0x3C, 0x3E..0x48, +11 more]
      └── 0xAE087C  CALL  ExpQuerySystemInformation [internal]

After the bounds check and remap, execution is transferred through an indirect jump to one of several internal dispatch stubs. Those stubs typically route the request into ExpQuerySystemInformation, which performs the class-specific validation, buffer handling, and data collection before returning to the caller.

This boundary is exactly why NtQuerySystemInformation is such a powerful interception point.

A large portion of usermode visibility flows through this single syscall. The kernel still performs the validation, layout, and buffer construction. By modifying the result after the kernel finishes, Blackbird inherits the legitimate structure building for free and only edits the output.

That part is still true, but the current codebase is a lot more aggressive than this first case study makes it sound. NtQuerySystemInformation is the clean example because it shows the whole idea in one place. In the current tree it sits beside NtQuerySystemInformationEx, NtQueryInformationProcess, NtQueryVirtualMemory, NtQueryPerformanceCounter, object, memory, section, thread, APC, filesystem, ALPC, and registry-facing hooks.

The design is not "rebuild Windows from scratch". It is much nastier than that. Let Windows do the ugly validation and structure construction, then enforce Blackbird's view at the final boundary before the caller gets the answer.

Hooking NtQuerySystemInformation

This is where Blackbird actually steps in.

Instead of trying to reimplement NtQuerySystemInformation from scratch or patching every higher-level API that eventually calls it, Blackbird takes a much cleaner route. When the runtime profile arms NTAPI monitoring, it installs hooks on the kernel routines, lets the original implementations run to completion, and then sanitizes the output buffer right before control returns to usermode.

The hook is straightforward.

  1. Call the original routine through a trampoline
  2. Let the kernel populate the buffer normally
  3. Patch specific fields before returning to usermode
  4. Emit telemetry about what was queried

Installing the Inline Hook

Blackbird uses a classic inline hook: it overwrites the beginning of ntoskrnl.exe!NtQuerySystemInformation with an unconditional jump to its own handler.

The jump is built with a short helper that emits the common x64 pattern:

VOID BkntkhBuildJump(_Out_writes_(BK_NTAPI_PATCH_SIZE) UCHAR *Patch, _In_ PVOID Destination)
{
    ULONGLONG destination64;
    ULONG displacement = 0;

    destination64 = (ULONGLONG)(ULONG_PTR)Destination;
    Patch[0] = 0xFF; // jmp qword ptr [rip+0]
    Patch[1] = 0x25;
    RtlCopyMemory(&Patch[2], &displacement, sizeof(displacement));
    RtlCopyMemory(&Patch[6], &destination64, sizeof(destination64));
}

That jump is only the small, visible part. The installer is a lot less naive than "copy 14 bytes and hope". It reads the prologue, refines the overwrite length at runtime with an instruction-length decoder so it does not split a multi-byte instruction, allocates an executable nonpaged trampoline, copies the original bytes, appends a jump back into the original routine, writes the patch through a protected-memory path, and then reads the bytes back to verify the hook actually landed.

If the ordinary export path is not enough, Blackbird can resolve through SSDT signatures and scan from IA32_LSTAR for the service descriptor path. That matters because the target is not a fixed tutorial binary; it is ntoskrnl.exe, across updates, with Microsoft free to change prologues and layouts.

A much more in-depth explanation of Blackbird's hooking is in another article; https://titansoftwork.com/capability/blackbird/

The Current NTAPI Surface

NtQuerySystemInformation is still the cleanest way to explain the trick, but it is not the only blade anymore.

Current Blackbird hooks the query surfaces used for anti-analysis, the memory surfaces used for injection, and the timing surfaces used for debugger/VM detection:

  • Query / anti-analysis: NtQuerySystemInformation, NtQuerySystemInformationEx, NtQueryInformationProcess, NtQueryObject, NtQueryVirtualMemory
  • Memory / injection: NtWriteVirtualMemory, NtReadVirtualMemory, NtProtectVirtualMemory, NtAllocateVirtualMemory, NtCreateSection, NtMapViewOfSection, NtUnmapViewOfSection
  • Thread / execution: NtCreateThread, NtCreateThreadEx, NtQueueApcThread, NtGetContextThread, NtSetContextThread
  • I/O / discovery: file, directory, object, ALPC, registry and process/thread open paths
  • Timing: NtQueryPerformanceCounter

Some hooks are required for the core capture path. Others are optional: if a build or machine layout makes one unsafe, Blackbird can still load and use the rest of the surface. The hooks are also runtime-policy controlled, so the more accurate wording is not "Blackbird is always patched into every syscall forever". It is "Blackbird can arm this NTAPI surface when the analysis profile asks for it."

The Sanitization Step

The hook calls the trampoline, the kernel fills the caller's buffer with real data, and then Blackbird inspects the SystemInformationClass. For the classes it cares about, it walks the returned structures and patches specific fields in place. Everything else flows through untouched.

Only the fields that matter get modified, structure sizes remain correct, buffer lengths still match, and the layout is identical to what the kernel produced. From usermode the result looks legitimate.

The current sanitizer does more than the old firmware-only example. It can post-process process lists, module information, handle information, kernel-debugger state, code-integrity state, firmware data, and virtual-memory query output. That gives Blackbird two advantages at the same time: the sample gets a believable answer, and Blackbird gets telemetry that the sample asked the question.

Sanitizing Firmware Tables (SMBIOS)

One target Blackbird handles is SystemFirmwareTableInformation, specifically the raw (SMBIOSDefines data structures that can be used to read management information produced by the BIOS of the computer.) tables that anti-analysis code frequently fingerprints.

The sanitizer function starts with strict checks:

if (SystemInformationClass != SystemFirmwareTableInformation ||
    !NT_SUCCESS(Status) ||
    SystemInformation == NULL ||
    SystemInformationLength < sizeof(SYSTEM_FIRMWARE_TABLE_INFORMATION))
{
    return;
}

It only proceeds for the RSMB (raw SMBIOS) provider and the "get table" action. Then it validates the embedded lengths to make sure nothing gets corrupted.

Once that's cleared, it hands the SMBIOS payload off to a walker routine that steps through the table structure by structure (reading the type, formatted length, string section, and advancing to the next record via the double-null terminator).

For certain structure types it replaces identifying strings with generic but plausible values:

  • Type 0 (BIOS Information): Vendor -> "American Megatrends Inc.", Version -> "F.27", Release Date -> "07/15/2021"
  • Type 1 (System Information): Manufacturer -> "Dell Inc.", Product -> "XPS 8940", Version -> "1.0", Serial -> "8CG1234", UUID -> randomized version-4 style bytes when the structure is long enough
  • Type 2 (Baseboard Information): Manufacturer, Product, Version, and Serial -> Dell values
  • Type 17 (Memory Device): Locator strings like "DIMM A1" / "BANK 0"

The string patching is done in-place with a bounded copy helper. It writes as much of the new string as fits, then pads the rest with spaces.

The UUID detail matters because a lot of anti-VM code does not stop at strings. Strings are the easy check. UUIDs, serials, baseboard identifiers and registry-backed device metadata are what make the machine identity either feel real or fall apart.

Registry-Based Anti-Virtualization Concealment

NtQuerySystemInformation is only one visibility surface. A lot of anti-VM logic also reaches straight into the registry looking for vendor artefacts, device metadata, service names, BIOS strings, and other fingerprints that are much less convenient to sanitize from a syscall return buffer alone.

Blackbird addresses that with a separate registry concealment layer. Instead of fabricating a large synthetic registry view, it focuses on the values that anti-analysis code tends to care about and either suppresses them entirely or rewrites them in place with more plausible hardware-backed data.

At the path level Blackbird suppresses registry lookups associated with common virtualization artefacts. This includes Hyper-V services such as vmicheartbeat, vmicvmsession, vmictimesync, and vmicvss, VMware drivers like vmhgfs, vmmouse, vmxnet, vmci, and vsock, VirtualBox components such as vboxguest, vboxmouse, vboxservice, and vboxsf, and vendor identifiers like VEN_15AD or VMware Tools registry paths.

A simplified excerpt from the registry path filter looks like this:

BOOLEAN BkavRegNullPath(_In_opt_ PCUNICODE_STRING Path)
{
    if (Path == NULL || Path->Buffer == NULL)
    {
        return FALSE;
    }

    if (BkrtIsAntiVirtualizationEnabled() &&
        (BkstrUnicodeContainsInsensitive(Path, L"\\services\\vmicheartbeat", 23) ||
         BkstrUnicodeContainsInsensitive(Path, L"\\services\\vmicvmsession", 23) ||
         BkstrUnicodeContainsInsensitive(Path, L"\\services\\vmictimesync", 22) ||
         BkstrUnicodeContainsInsensitive(Path, L"\\services\\vmicvss", 17)))
    {
        return TRUE;
    }

    if (BkrtIsAntiVirtualizationEnabled() &&
        (BkstrUnicodeContainsInsensitive(Path, L"\\services\\vmhgfs", 16) ||
         BkstrUnicodeContainsInsensitive(Path, L"\\services\\vmmouse", 17) ||
         BkstrUnicodeContainsInsensitive(Path, L"\\services\\vmrawdsk", 18) ||
         BkstrUnicodeContainsInsensitive(Path, L"\\services\\vmusbmouse", 20) ||
         BkstrUnicodeContainsInsensitive(Path, L"\\services\\vmxnet", 16) ||
         BkstrUnicodeContainsInsensitive(Path, L"\\services\\vmci", 14) ||
         BkstrUnicodeContainsInsensitive(Path, L"\\services\\vsock", 15) ||
         BkstrUnicodeContainsInsensitive(Path, L"\\services\\vmbus", 15) ||
         BkstrUnicodeContainsInsensitive(Path, L"\\services\\hyperkbd", 18) ||
         BkstrUnicodeContainsInsensitive(Path, L"\\services\\storflt", 17) ||
         BkstrUnicodeContainsInsensitive(Path, L"\\services\\vmstorfl", 18)))
    {
        return TRUE;
    }
...

If a queried path matches one of those artefacts, Blackbird can treat it as a concealment target instead of exposing the expected virtualization indicator.

For values that are more useful to spoof than to remove, Blackbird applies targeted spoofing instead. In the NIC class key it rewrites fields such as DriverDesc, ProviderName, AdapterHardwareAddress, and NetworkAddress, replacing them with Intel-branded values and even swapping the MAC OUI prefix to 8C:8D:28. That keeps the registry data looking coherent instead of just empty.

The same pattern shows up for other hardware-facing registry surfaces. Display adapter values are rewritten to NVIDIA-branded strings, SCSI identifiers are replaced with Samsung SSD 970 EVO Plus, and BIOS-facing values such as BIOSVendor, SystemManufacturer, SystemProductName, and BaseBoardProduct are rewritten to plausible OEM data like American Megatrends Inc., Dell Inc., and XPS 8940.

This only works if the story stays consistent. SMBIOS data, registry values, adapter metadata, and BIOS strings all need to agree on what machine this is. Blackbird modifies multiple telemetry surfaces so the system identity remains coherent instead of obviously fabricated.

The registry layer is not just a mask anymore either. It still hides virtualization artefacts, but it also treats registry activity as a signal. Queries and writes against LSA packages, Defender exclusions, AppInit DLLs, BootExecute, Winlogon, IFEO, credential hives, services, COM hijacks, WMI, scheduled tasks, EDR product keys and enterprise identity surfaces can all become telemetry. That means a sample asking "am I in a sandbox?" can be answered with a lie while Blackbird still records that it asked.

Timing-Based Anti-Analysis

The nastier checks do not always ask Windows for a string.

Sometimes they time things.

Malware can call QueryPerformanceCounter in tight pairs, suspend a thread, single-step through suspicious regions, or measure whether API calls are taking just a little too long. If the registry says "Dell XPS" but timing says "debugger just parked me for 600ms", the lie starts leaking.

Current Blackbird handles that through NtQueryPerformanceCounter.

The hook calls the original routine first, reads the real counter, then passes it through BkqpcApplyTimingAdjustment. If adjustment is enabled for that runtime profile, Blackbird writes a virtual counter back to the caller. It tracks per-thread and per-process timing state, then applies corrections for things like:

  • Suspend/pause gaps
  • Blackbird's own instrumentation overhead
  • Manual or automatic timing bias
  • Tight-pair outliers
  • Monotonic counter clamping

The goal is not to freeze time. Frozen time is suspicious. The goal is to keep time boring.

If a sample is doing tight QPC pairs, Blackbird can collapse the artificial overhead without making the counter go backwards. If a suspend introduced a giant gap, Blackbird can subtract the pause from the view returned to usermode. The malware gets a believable clock, and Blackbird still gets QPC timing telemetry about what happened.

Telemetry, Detections & Capture Evidence

The original version of this post mostly stopped at concealment: make Windows lie, keep the sample running.

That is still the point, but Blackbird now uses the same surfaces as sensors. NTAPI hooks emit ETW and internal events. Registry callbacks can become detections. Memory hooks can catch cross-process writes, PE injection patterns, suspicious protection changes, section mapping, APC/thread execution paths, and direct-syscall style behaviour from usermode instrumentation.

This is where the project starts looking less like a single anti-VM trick and more like an analysis fabric.

The flow is roughly:

  1. Malware asks the machine a question
  2. Blackbird lets Windows build the real answer
  3. Blackbird sanitizes the answer if exposing it would kill execution
  4. Blackbird emits telemetry about the question
  5. The controller, capture pipeline, SignatureIntel, YARA/Sigma matching, and exports turn that into evidence

So the lie is not just defensive. It is a way to keep the sample alive long enough to confess through behaviour.

Conclusion

APIs like EnumProcesses and OpenProcess look independent from usermode, but most of them collapse onto the same syscall layer. Once execution crosses from ntdll into ntoskrnl, the kernel decides what the caller sees.

Blackbird uses NtQuerySystemInformation and the wider NTAPI surface because the kernel validates parameters, builds the structures, and reports the correct status codes. Blackbird lets that process complete, then edits only the fields that reveal the analysis environment.

But syscall manipulation alone is not enough. Anti-analysis software frequently checks the registry for virtualization artefacts, reads SMBIOS, measures time, queries debugger state, walks handles, probes memory, and looks for anything that smells like a lab. Blackbird therefore applies the same strategy across multiple surfaces, suppressing known VM indicators and spoofing selected hardware identifiers so the system presents a consistent identity.

Blackbird exists to keep malware running long enough to observe it. Many samples terminate immediately when they detect virtualization. By removing those indicators, smoothing timing artefacts, and presenting a believable machine profile, the analysis environment stays on the execution path that actually matters.

The important part is that Blackbird does not throw the evidence away. It lies to the sample, but it tells the analyst what the sample tried to learn.