Security1 publisher2 min readPublished
PavokwiLoader buries its logic in more than 2,000 mixed boolean-arithmetic predicates
Malbear Labs found PavokwiLoader resolves the 48 ntdll functions it uses by hash while importing 68 KERNEL32 functions it never calls. Static and import-based detection see nothing, so reading it means emulating the loader's own resolver.
The Watch · Security desk

What happened
- PavokwiLoader is a C++ loader whose entire WinMain compiles into a single function of more than 80,000 inlined instructions, according to Malbear Labs.
- The obfuscation lives in the predicates: the function holds more than 2,000 mixed boolean-arithmetic clusters, over 1,000 of them distinct, and 94 percent appearing exactly once.
- Its 68 declared imports are all MSVC-runtime KERNEL32 functions the loader never calls, while the functions it uses come from ntdll, which is absent from the import table.
- A runtime resolver hashes ntdll export names against 48 stored values, and Malbear Labs recovered all 48 by rebuilding a fake ntdll and hooking the hash loop.
- Its debugger, VM and sandbox checks are assembled at runtime, so their constants, including the 0x00100010 context flag, do not appear anywhere in the binary.
Compiled by The WatchSomething wrong?How this is made
Why it matters
- constraint Detection built on what a binary declares, from import tables to import-hash signatures, will not fire here; an analyst has to run the resolver under emulation to learn what it calls.
- decision Because the loader accepts only nvidia or intel display adapters, a detonation sandbox has to present one of those GPUs or it fails the anti-VM check the same way a real AMD host does.
- precedent PavokwiLoader is the loader stage of a chain that ran SEO poisoning into a custom RMM tool and Cobalt Strike, and obfuscation at this cost points to a crew spending to keep analysts out, not a commodity dropper.
A resolver walks ntdll's export directory at runtime, hashing each export name until it matches one of forty-eight 32-bit values the loader stores [7]. The hash is FNV-1a with both constants replaced: seed 0x676092FE for the usual 0x811C9DC5, multiplier 0xE6006695 for 0x01000193, and each character is OR'd with 0x20 before it goes in [8]. Malbear Labs did not reverse that by hand. They built a PE with valid headers and an export directory laid out exactly like ntdll's, filled the name table from public syscall lists, pointed the loader's own resolver at it, and hooked the hash loop to catch each name and the value it produced. All forty-eight came back [9]. Most are what a loader needs: memory, process and thread control, sections, files and registry [10].
The constants that would give any of this away are assembled at runtime by those predicates. Twelve bitwise instructions between 0x140054E32 and 0x140054E5D do nothing more than compute a minus b [5]. The FNV seed is in the open, a variable set to 1734382334, which is 0x676092FE, and it is still effectively hidden, because the code around it zeroes and sets neighbouring variables on comparisons a reader cannot follow [11].
The anti-analysis checks follow the same rule. The loader looks for a debugger three times. It reads its own thread context with ContextFlags set to 0x00100010, which tells the kernel to fill in the hardware breakpoint registers Dr0 through Dr3 and the Dr7 control register [12]. Then it calls ZwQueryInformationProcess twice, for ProcessDebugPort and ProcessDebugObjectHandle. The port is what CheckRemoteDebuggerPresent reads. The object handle catches a debugger that has nulled the port but left the object behind. Neither is the PEB BeingDebugged byte that IsDebuggerPresent checks and that an analyst clears first [13]. Search the binary for 0x00100010 and you get nothing, because the value is built when the call is made [14].
The anti-VM checks read two registry keys through ZwOpenKey, ZwEnumerateKey and ZwQueryValueKey. The display adapter class key is enumerated one subkey at a time, and the loader accepts only nvidia or intel; there is no amd entry, so a Radeon workstation fails exactly as a VM does. The disk enumeration key is checked for virtual controller strings [15]. A separate check compares the logged-in username against thirteen hardcoded names, among them sandbox, analyst, cuckoo and malware [16][18]. The loader also reads SystemBasicInformation for the processor count and physical memory [17].
PavokwiLoader is the loader stage of the intrusion chain Kostas documented, which ran from SEO poisoning into a custom RMM tool and Cobalt Strike [1].
What to watch
- Whether other samples reuse the modified FNV-1a constants (seed 0x676092FE, multiplier 0xE6006695) so they can serve as a hunting signature.
- Whether the SEO-poisoning-to-RMM chain and this loader get tied to a named actor or to overlapping tooling.
- Whether endpoint vendors add emulation-based coverage now that import-table and static-constant signatures do not fire.