Blog / anti-cheat · · 11 min read · Signed KyTech Research

Polymorphic Cheat Builds: How Compile-Time Morphing Beats Signature Scans

Per-user seeds, OLLVM passes, and constexpr string encryption turn N users into N unhashable binaries. Here is the pipeline.

Signature-scanning anti-cheats detect what they have already seen. A per-user build pipeline hands every customer a binary whose bytes have never touched a server. That is the entire premise of a polymorphic cheat, and it is why a two-line YARA rule can wipe out ten thousand shared-binary users overnight while barely denting a properly morphed distribution.

This post walks through the actual mechanics: what the build pipeline looks like, which LLVM passes matter, how a per-user seed threads through compile-time string encryption, what rotation cadence keeps hashes ahead of the signature databases, and what class of detection still catches every one of these builds regardless of how novel the bytes are.

Why signature scanning has a shelf life

A signature-scanning engine, whether it is EasyAntiCheat's user-mode integrity module, BattlEye's scanner, or Vanguard's ring-0 sweeps, works from a corpus. Someone submits a binary, an analyst reverses it, a hash or byte pattern or import fingerprint lands in the database, and every process on every client with matching bytes gets flagged. The pipeline is fast, cheap, and effective against shared binaries. For a detailed breakdown of how the scanners themselves work, see our earlier post on anti-cheat signature scanning.

The weakness is structural. Signatures cover known bytes. A polymorphic cheat produces unknown bytes per user, per rotation, per build. Roblox's Byfron layer (acquired 2022 and now shipped as Hyperion) and Riot's Vanguard (TPM 2.0 requirement added for League in 2024) both invest heavily in behavior telemetry precisely because the signature side of the house cannot keep up with a competent build pipeline.

What "polymorphic" actually means in a cheat context

The word is borrowed from the malware literature, but the constraint is different. Malware polymorphism usually means runtime mutation: a decryptor stub unpacks a rewritten body on each execution. Cheat polymorphism is usually compile-time. Every user gets a binary produced by a fresh compiler invocation with a different seed, a different pass order, a different layout, and different embedded strings. Runtime mutation is expensive and noisy; compile-time morphing is cheap once the pipeline exists and produces bytes that look like normal C++ output because they are normal C++ output, just with different constants and different basic-block ordering.

The observable result: N users equals N distinct SHA256 hashes. A signature database entry for one build kills exactly one user. The economics of manual reverse engineering collapse.

The per-user build pipeline

A working pipeline has five stages. The order matters because seed material has to be available before code generation, and layout randomization has to happen before final linking.

1. seed = HMAC-SHA256(server_key, user_id || build_timestamp)
2. preprocess: inject seed as -DKYT_SEED=0x<hex> plus rotated -D flags
3. compile: clang++ -O2 -fno-inline-functions with OLLVM passes
     -mllvm -fla    (control-flow flattening)
     -mllvm -sub    (instruction substitution)
     -mllvm -bcf    (bogus control flow)
     -mllvm -split  (basic block splitting)
4. link: lld with --shuffle-sections=<seed>, section ordering per seed
5. post: strip, sign with per-user cert, package with per-user loader

Each stage consumes the seed. The compiler sees it as a preprocessor macro that participates in constexpr string encryption. The OLLVM passes consume it as a PRNG seed for pass parameters (flattening depth, predicate selection). The linker consumes it via --shuffle-sections, which lld has supported since version 13. The result is that two builds from identical source with different seeds share no basic block at the same offset and no encrypted string with the same ciphertext.

The seed itself is derived from an HMAC keyed by a server-side secret so a client cannot predict or replay another user's build. If an analyst dumps their own binary and lifts the seed, they learn nothing about anyone else's.

Compile-time obfuscation with OLLVM

Obfuscator-LLVM, originally out of the University of Applied Sciences in Western Switzerland and now maintained across several forks (the most current being the goron and Hikari derivatives targeting LLVM 15+), provides the three passes that carry the load:

Control-flow flattening (-fla) rewrites every function into a single dispatch loop with a state variable. The original CFG becomes unrecognizable in a decompiler. IDA Pro and Ghidra both produce spaghetti pseudocode until the analyst writes a devirtualization script, and even then the script is specific to the flattening variant used.

Instruction substitution (-sub) replaces arithmetic with algebraically equivalent but syntactically different sequences. a + b becomes a - (-b) or (a ^ b) + 2 * (a & b). Byte patterns for common operations no longer match.

Bogus control flow (-bcf) inserts opaque predicates: branches whose direction is provably constant at compile time but expensive to prove at analysis time. A signature scanner walking the disassembly sees branch targets that never execute at runtime, poisoning any control-flow-based fingerprint.

The passes are parameterized. With a per-user seed, the flattening dispatcher's state constants differ, the substitution choices differ, and the opaque predicates use different arithmetic identities. Two builds share the same source AST and produce completely different machine code.

The cost is real. Flattened code runs roughly 2x to 4x slower on the affected functions and grows the binary by a similar factor. A production pipeline applies the passes selectively, using function attributes to flag only the security-sensitive hot paths (input dispatch, memory read primitives, IPC with the driver) and leaves math-heavy loops alone.

Applying the passes: a concrete invocation

The OLLVM-enabled clang produces the pass set through -mllvm flags, but a production build only wants the heavy passes applied to hot paths tagged with a function attribute. Applying flattening to every translation unit inflates the binary threefold and cripples release-mode performance for no security benefit, since a maths-only helper carries nothing worth hiding. The following snippet is the actual compile command that runs against a seeded translation unit in our source tree, driven by an orchestrator that writes seed.env fresh for every build slot.

# Per-user seed baked in via -D, OLLVM passes gated by attribute.
# seed.env is written by the build orchestrator per invocation.
source seed.env
clang++ -std=c++20 -O2 -fno-inline-functions \
    -DKYT_SEED=0x${KYT_SEED_HEX}ull \
    -mllvm -fla -mllvm -flaSplit -mllvm -flaSplitNum=3 \
    -mllvm -sub -mllvm -sub_loop=2 \
    -mllvm -bcf -mllvm -bcf_prob=40 \
    -mllvm -seed=0x${KYT_SEED_HEX} \
    -fannotation-attributes \
    -c src/dispatch.cpp -o build/${KYT_SEED_HEX}/dispatch.o

The -mllvm -seed= flag threads the per-user value into the OLLVM PRNG so every parameterized decision (predicate choice, split boundary, bogus branch target) diverges across users. Functions not carrying __attribute__((annotate("kyt_obf"))) skip the passes entirely, which keeps the inner loops of the aim solver running at native speed while input dispatch, driver IPC, and module resolution take the full obfuscation cost. The ${KYT_SEED_HEX} prefix on the output path is deliberate: it lets a support engineer reproduce any user's exact object files months later given only the stored seed, without keeping the binary itself on disk.

Per-user seed threaded through constexpr string encryption

Strings are the classic signature target. A single "nvidia.dll" or "amsi.dll!AmsiScanBuffer" literal in a binary is a free win for a scanner. The fix is compile-time encryption keyed by the per-user seed, decrypted only at use.

Here is a working C++20 pattern that KyTech uses in production. The key is derived at compile time from the injected seed plus __TIME__ and __DATE__, so even without OLLVM the string bytes differ per build.

#include <array>
#include <cstdint>
#include <string_view>

#ifndef KYT_SEED
#define KYT_SEED 0xDEADBEEFCAFEBABEull
#endif

// FNV-1a over __TIME__ __DATE__ mixed with the injected seed.
consteval uint64_t build_key() {
    uint64_t h = 1469598103934665603ull ^ KYT_SEED;
    for (char c : std::string_view(__DATE__ __TIME__)) {
        h ^= static_cast<uint8_t>(c);
        h *= 1099511628211ull;
    }
    return h;
}

template <size_t N>
struct XorStr {
    std::array<char, N> data{};

    consteval XorStr(const char (&s)[N]) {
        uint64_t k = build_key();
        for (size_t i = 0; i < N; ++i) {
            data[i] = s[i] ^ static_cast<char>(k >> ((i & 7) * 8));
            k = k * 6364136223846793005ull + 1442695040888963407ull;
        }
    }

    // Decrypt into a caller-provided buffer at runtime.
    void decrypt(char* out) const {
        uint64_t k = build_key();
        for (size_t i = 0; i < N; ++i) {
            out[i] = data[i] ^ static_cast<char>(k >> ((i & 7) * 8));
            k = k * 6364136223846793005ull + 1442695040888963407ull;
        }
    }
};

#define KSTR(s) (XorStr<sizeof(s)>(s))

// Usage: strings never appear as plaintext in the .rdata section.
void resolve() {
    char buf[16];
    KSTR("ntdll.dll").decrypt(buf);
    auto mod = GetModuleHandleA(buf);
    // ...
}

Three things are worth calling out. First, consteval forces the constructor to run at compile time; if any part of the expression escapes to runtime the compiler errors out, so an accidental plaintext leak is a build failure rather than a shipping bug. Second, the LCG inside the loop (Knuth's constants) means the key stream is different for every byte position, so a known-plaintext attack against one string does not recover the key for another. Third, KYT_SEED is injected by the pipeline at preprocess time, so no two users share the ciphertext even if the source is identical.

A signature rule that fires on "ntdll.dll" finds nothing. A rule that fires on the specific ciphertext works exactly once, on exactly one user's binary.

Rotation cadence: nightly and on-detection

A per-user build gives you spatial diversity across users. Rotation gives you temporal diversity within one user. Two schedules matter.

Nightly rotation rebuilds every active user's binary with a new timestamp, new seed material mixed in from the current UTC date, and a fresh pass ordering. The user pulls the new build on next launch. Signature databases populated during the day expire overnight because the byte patterns simply no longer exist in the wild after the rotation window closes.

On-detection rotation is triggered by telemetry: a spike in ban reports, a chatter thread describing a fresh scanner behavior, or a submitted binary appearing in a public malware sandbox. The pipeline flushes and rebuilds the affected user cohort immediately, invalidating whatever the vendor just added to their corpus.

Rotation is not free. Every build eats CPU time and cold storage. A reasonable production number is around 20 to 60 seconds per user on a modern build host with ccache warm on the shared translation units, dropping to under 10 seconds when only the per-user seeded translation units need to rebuild. Splitting the codebase so that the seeded, obfuscated portion is a small percentage of total code is the single biggest lever for keeping rotation cheap.

Comparison: what dies to what

Detection method Kills shared binary Kills polymorphic build Notes
SHA256 hash blacklist Yes No One entry, one dead binary
Byte-pattern YARA rule Yes Rarely Only if pattern survives OLLVM
Import table fingerprint Yes Sometimes Defeatable with dynamic resolution
String literal scan Yes No XorStr defeats this entirely
Behavior heuristics Yes Yes Input cadence, aim snap distributions
ML on gameplay telemetry Yes Yes Valve's VACnet class of system
Kernel driver page hash Yes Yes if driver shared Per-user driver rebuild also possible

The right column is the answer to a common misconception. Polymorphism defeats the top half of the table completely. It does nothing about the bottom half.

What still catches polymorphic cheats

Behavior-based detection ignores the binary entirely. Valve's VACnet, deployed on CS2 since the 2023 launch and expanded on the source-2 build 10000+ series in 2025, operates on server-side gameplay telemetry: crosshair placement distributions, snap-to-target angular velocity histograms, reaction times to occluded targets, wall-tracking patterns during smoke usage. None of it cares what bytes ran on the client.

Riot's Vanguard applies similar heuristics on top of its ring-0 attestation. Roblox Hyperion (post-Byfron, per the 2022 acquisition writeups from David Wibergh's team) does the same on the Roblox client's input pipeline. Epic's EasyAntiCheat (acquired by Epic in 2018) added a behavior module around the same time.

The takeaway for any cheat build system: polymorphism buys you time against the signature side of the vendor's stack, and time is valuable. It does not buy you invisibility against the behavior side, and pretending otherwise gets users banned. The mitigations for behavior detection are entirely different: humanized input curves, jitter injection on aim-assist output, target-selection cooldowns that mimic human decision latency, and conservative feature exposure per user based on their historical playstyle. Those live in the cheat logic, not in the build pipeline.

Build hygiene that matters more than clever passes

A few pipeline details separate a serious build system from a hobby one:

  1. No shared PDB or debug info across users. A single leaked PDB with per-user symbol names collapses the entire scheme.
  2. Per-user driver signing certificates if the loader ships a driver. A shared cert lets a vendor blocklist the entire user base with one entry.
  3. Deterministic reproducibility from the seed. If a support ticket needs a specific user's exact binary rebuilt for triage, the pipeline has to be able to do it from the seed alone. Store the seed, not the binary.
  4. CI isolation. Build hosts do not have network egress except to the signing service and the delivery CDN. A compromised build host that can exfiltrate the master HMAC key ends the pipeline's usefulness immediately.
  5. Section-name randomization in addition to --shuffle-sections. Anti-cheats key on .text layout but also on unusual section names; randomize them to plausible defaults like .text0, .rdata1, not to obviously suspicious names.

None of this is exotic. It is the same discipline any serious code-signing operation applies, adapted for a per-user output.

How KyTech handles this

KyTech was founded in 2025 by two engineers, and the build pipeline described here is what runs behind every cheat we ship: Apex Legends, CS2, Overwatch 2, Black Ops 7, Forza Horizon 6, and Roblox. Every purchase from our product catalog triggers a fresh compile keyed to a seed derived from the customer account plus the build timestamp, so the binary that lands on your machine has bytes that never existed before and never will again after the next nightly rotation.

The KyTech kernel driver participates in the same pipeline. Rather than shipping a single signed driver to every user, the driver's obfuscated portions are rebuilt per user with the same OLLVM pass set, and only the WHQL-signed shim stays constant. On the user-mode side, the constexpr string encryption pattern shown above is exactly the one in our tree, extended with per-function key derivation so that a single leaked string does not compromise the rest.

The Apex Legends HWID spoofer is currently in beta and rides the same rotation cadence: nightly rebuild plus an immediate rebuild if telemetry from the vendor side suggests the spoofer's fingerprint is being scanned. The KyTech engineering position on behavior detection matches what this post argues: polymorphism is necessary and insufficient. Our aim-assist logic ships with humanized curves and per-user tuning specifically because bytes are only half the problem, and no amount of build-pipeline cleverness saves a user whose mouse input looks like a state machine. Rotation slots and rebuild frequency scale per subscription tier, listed on the KyTech pricing page. The flagship product built end to end on this exact pipeline is KyTech Apex, where the per-user compile, seeded string encryption, and nightly rotation cadence all ship together in the delivered loader.

Further reading

  • LLVM project documentation for the pass manager, -mllvm interface, and the substrate every OLLVM fork builds on.
  • Obfuscator-LLVM upstream, including the -fla, -sub, and -bcf pass source that this post references directly.
  • lld linker docs covering --shuffle-sections, section reordering, and the reproducible-build flags a per-user pipeline depends on.
  • YARA source and rule syntax, useful for understanding exactly what a polymorphic build has to defeat.
  • KyTech's earlier walkthrough of anti-cheat signature scanning if you have not read the vendor-side half of the picture yet.

Signed by KyTech Research

We still play these games and we still push every build in production. If something in here is wrong, and eventually something will be, ping us in Discord and we will fix it.

Enough theory. get in.

The loader is one click away. Ring-0 kernel driver, polymorphic per download, memory-only injection. Six games across VAC, EAC, Ricochet, Byfron, and Warden.

Get in ›