← Back to blog

Polymorphic by Default: Why Static Signatures Can't Fingerprint Krait

· Krait Team · 6 min read

One of the first things a malware analyst does with a captured implant binary is extract signatures — byte sequences, string patterns, API import hashes, code structure fingerprints. These signatures get pushed to detection engines, and every future instance of that implant is caught on sight.

This works when every instance of the implant is the same binary, or close to it. Change the C2 URL and the sleep interval, sure, but the code is identical, the hash computation is identical, the evasion patches are identical. The analyst only needs to reverse one sample to write a signature that catches all deployments.

Krait’s build system treats every compilation as a unique event. Multiple independent parameters are randomized per build, and the randomization touches enough of the binary that two builds from the same source tree produce files with no shared byte-level signatures.

What Gets Randomized

The polymorphic engine operates on several independent axes. Each axis changes a different aspect of the binary, and the axes compose — meaning the total variation is multiplicative, not additive.

API resolution seed. Krait resolves Windows API functions by walking the PEB export tables and matching hashed names. The hash function takes a seed parameter that changes per build. The same API name produces a completely different hash value with a different seed. This means the hash constants baked into the binary — the values an analyst would extract and search for — are unique to that build. A signature based on hash constants from one sample matches zero other samples.

String obfuscation key. Sensitive strings (DLL names, API names used in dynamic resolution) are XOR-encoded at compile time with a per-build key. The encoded bytes in the binary change with every build. An analyst can decode them from a single sample, but the decoded result doesn’t help identify the next sample because the encoding key is different.

Evasion patch variants. Krait’s AMSI bypass uses a small instruction sequence patched into the scan function at runtime. The polymorphic engine selects from multiple functionally equivalent sequences — they all achieve the same result (suppress the scan) but use different instruction encodings. A signature targeting one variant misses the others.

Decoy function selection. Krait’s tampered syscall engine routes all syscalls through a single rarely-used ntdll function as a decoy. The decoy is selected randomly per build from a pool of candidates. Each candidate has the same prologue structure required by the technique, but the function name hash baked into the binary is different for each selection. We wrote about this in detail in our tampered syscalls post.

Encryption keys. The AES key material for payload decryption, the XOR key for string deobfuscation, and the seeds for runtime hashing are all generated fresh per build. None of these values are derived from the source code — they’re injected at build time from a cryptographic random source.

These aren’t just different configs applied to the same binary. They produce structurally different code: different constants in immediate operands, different encoded byte sequences in the data sections, different hash tables, different patch bytes. A signature has to be generic enough to survive all of this variation, which typically means it’s also generic enough to false-positive on legitimate software.

Template Mode: Polymorphism Without a Compiler

Source-code builds are the gold standard — the polymorphic engine runs at compile time and produces a binary where the randomization is baked into the code at the instruction level. But not every operator has a build environment configured, and some deployments happen from a laptop in the field.

Krait’s template mode addresses this. Pre-compiled binary templates ship with placeholder values for every polymorphic parameter. At deployment time, a patcher reads the template, generates fresh random values, computes the derived constants (hash values, encoded strings), and writes them into the binary. The result is functionally identical to a source build — the same parameters are randomized, and the output is just as unique.

The template patcher doesn’t modify code structure — it can’t recompile functions or change instruction sequences. What it can change is every constant, key, hash value, and encoded string in the binary. For detection purposes, this is sufficient: the byte patterns that signature engines match on are exactly the values the patcher replaces.

Template mode also means that the operator server can generate unique payloads on demand. When an operator deploys a new agent, the server patches a fresh template with fresh random values. Two agents deployed from the same server thirty seconds apart produce different binaries. There’s no batch of identical payloads to build a signature against.

What This Means for Defenders

We build Krait as a red team tool, but we think honestly about the blue team implications because that’s what makes the tool realistic.

Polymorphic builds defeat static signature detection. That’s the point — a red team tool that gets caught by a hash lookup isn’t testing anything meaningful. But polymorphism doesn’t defeat behavioral detection. The implant still resolves APIs, still makes network connections, still executes BOFs in memory. An EDR that detects the behavior rather than the bytes catches Krait regardless of how unique each build is.

The honest defensive answer to polymorphic implants is behavioral analysis, memory scanning for runtime artifacts, and network traffic analysis. Static signatures have their place in catching commodity malware that doesn’t invest in polymorphism, but they shouldn’t be the primary control against sophisticated tooling. If your detection stack relies primarily on byte-matching captured samples, a polymorphic adversary — whether using Krait or anything else — will bypass it.

The Operational Tradeoff

Polymorphism adds build complexity. The polymorphic engine needs to be deterministic (given the same seed, produce the same output for debugging) while defaulting to random (no seed specified → cryptographic random). Every parameter needs a default placeholder value for sanity compiles during development, and the build system needs to validate that no placeholder values survive into a production build.

Template mode adds a verification step: after patching, the patcher scans the output for any remaining placeholder values and refuses to produce a binary if any are found. This catches configuration errors that would otherwise produce a “polymorphic” binary that’s actually running with known default values — which is worse than no polymorphism at all, because the operator believes they’re protected when they’re not.

The build system also produces a manifest alongside each binary: a record of every randomized parameter and its generated value. This lets the operator reproduce a build (for debugging a deployed agent) and lets the server correlate template patches with agent registrations. The manifest never ships with the binary — it stays on the operator’s machine.

For the operator, the complexity is invisible. Run the build script or deploy from the server, and polymorphism happens automatically. The only visible change is the build summary that prints the generated values — a confirmation that the engine ran, not something the operator needs to act on.