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 RESX:
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]
...
KernelBase.dllEnumProcesses 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 RESX 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]
KernelBase.dllThe 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 RESX 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
ntdll.dllTo 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
ntdll.dllAll 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_LSTAR MSR.
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 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.
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]
kernelAfter 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.
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.
- Call the original routine through a trampoline
- Let the kernel populate the buffer normally
- Patch specific fields before returning to usermode
- 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));
}
BLACKBIRD.sysThat 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 (SMBIOS) 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;
}
BLACKBIRD.sysIt 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;
}
...
BLACKBIRD.sysIf 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:
- Malware asks the machine a question
- Blackbird lets Windows build the real answer
- Blackbird sanitizes the answer if exposing it would kill execution
- Blackbird emits telemetry about the question
- 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.