GhostCrypt is one of the build-time tools in the AXIS toolchain: feed it any x64 native Windows executable and it emits a unique-per-build binary that carries the payload encrypted, decrypts it in memory, and runs it there — the file never touches disk in the clear. Every build differs: a fresh ChaCha-20 key split into three-to-six random XOR shares, a fresh seed for the API-name hashes (so hash constants can't become IOCs), fresh junk functions, a fresh section name, a benign version resource. The static layer of this problem is honestly close to solved — unique bytes, no payload on disk, an import table that reads like a calculator. The hard question was behavioral: what does a modern EDR actually punish?
First, cheat the emulator
Before any behavioral question matters, the sample has to survive the static-and-emulation layer — and emulation is where most packers die. Defender and similar engines execute unknown binaries in a synthetic CPU, waiting for the decrypted payload to materialize in emulated memory where the scanner can see it. GhostCrypt's answer is the piece of this tool we're proudest of, because it turns the emulator's budget into a weapon against itself: the final decryption key does not exist anywhere in the binary. It is recomputed at startup by folding a proof-of-work into the key schedule.
key_eff = key(from 3–6 XOR shares) XOR sha256-chain(seed, iters) # iters: random per build, 500,000–1,200,000 # real CPU: ~0.2–0.5 s, once, at startup # emulator: instruction budget exhausts long before # the chain completes — so plaintext never # exists inside the emulated machine at all
The economics are brutal and asymmetric. Half a second of real CPU work is invisible to a user; an emulator's instruction budget is orders of magnitude too small to finish the chain, so emulation aborts before a single plaintext byte exists anywhere its scanner can look. What remains is the static scan — which a polymorphic stub with benign imports and low-entropy storage is built to pass. The builder recomputes the identical chain and compares it byte-for-byte with what the stub will compute, so a build either folds correctly or doesn't ship.
The experiment: one variable per bait
With the static layer handled, we built a bait series to interrogate the behavioral layer. Each bait program is benign — it writes a proof file and exits — but each carries exactly one injection pattern, staged so the series isolates variables instead of confounding them. Two more samples served as controls. All of it ran live on Windows 11 24H2 with SentinelOne and Defender hardened to maximum (cloud block high, samples kept off the vendor cloud), with Defender's detection events diffed per run and the S1 console checked for management-side convictions. Every row was reproduced multiple times.
| Sample | What it isolates | Verdict |
|---|---|---|
| Self-injection bait | Packed payload in its own process — even with RWX + CreateThread | CLEAN |
| v1 — classic chain | RWX + CreateRemoteThread into a suspended child, maximum ETW-TI surface | CONVICTED |
| v2 — early-bird APC | W^X discipline, no RWX, no CreateRemoteThread — but still an APC into the child | CONVICTED |
| v3 — shared section | Zero WriteProcessMemory, zero VirtualProtectEx — local RW map + remote RX map, APC again | CONVICTED |
| v4 — hollowing-style | Redirect the child's own thread via Get/SetThreadContext — no CRT, no APC | CONVICTED |
| v5 — silent child | Redirected thread runs a pure byte-copy loop: zero syscalls, ever | CONVICTED |
| v6 — stomp | Code written over the child's own signed-image .text, entry redirected | CLEAN |
| control 1 | Staging identical to v2 (allocate, write, flip to RX in the child) — but never executes | CLEAN |
| control 2 | CreateRemoteThread running signed LoadLibraryA on legitimate user32.dll — no foreign code at all | CONVICTED |
Three conclusions, none of them folklore
One: conviction is execution-based, not behavior-based. The silent child is the row that should end arguments — a redirected thread executing a byte-copy loop with literally zero syscalls still burned. Whatever the engine inspected, it was not the code's behavior. Meanwhile control one, which performs every suspicious staging step but never executes, walked free.
The sensor doesn't care what your code does. It cares where your code lives.
Two: the scan keys on private unsigned pages. Every convicted variant executed from freshly allocated private memory. The one survivor — v6 — executed from the child's own signed-image .text, bytes stomped in place over copy-on-write image pages. Code running from an image-backed region, even a modified one, was simply not in the scanned set.
Three: staging is free; only execution convicts. Allocate, write, change protections — none of it registered. Even control two is instructive in reverse: a CreateRemoteThread call died while running Microsoft's own signed library code, with zero foreign bytes involved. The API pattern itself — a remote thread start — is what the sensor punishes, regardless of what the thread runs.
What survived — the shape of it
The surviving recipe, at category level: spawn the child suspended, locate its image in memory, write your code over its own .text, point the already-existing thread at it with SetThreadContext, resume. Zero new pages, zero remote allocations, zero protection changes, zero thread-creation calls. Afterwards: restore the original bytes, keep the child's visible behavior minimal, terminate cleanly. One production note the hard way — at CREATE_SUSPENDED only the EXE and ntdll are mapped, so the main module is the only guaranteed stomp target; your favorite DLL does not exist yet.
The honest limits
The doctrine we ship with the tool says it plainly, because our lab's rule applies to every claim on this page: results are pinned to the engines and the day we tested, and the payload's own behavior after execution — network, registry, persistence — is outside the loader's control and is what advanced EDRs score next. GhostCrypt refuses .NET assemblies at build time (CLR hosting is milestone two), and its OPSEC section leads with the two rules that matter most: never upload a sample to a multi-scanner — submissions burn a build into signatures within days — and never reuse one packed binary across targets. Reuse is what gets builds signatured; per-target uniqueness is the whole point of the tool.
The defender's angle
This experiment is a gift to the other side, and we publish it knowing that. If conviction keys on execution from private unsigned pages, then VAD auditing — not API sequence matching — is the high-fidelity signal: alert when a thread's instruction pointer lands in image memory that has gone copy-on-write, or in any executable private region. The surviving pattern still has seams: a process created suspended that does nothing visible, a SetThreadContext that moves a child's RIP into its own .text, image pages that flipped COW. Each is quiet; correlated, they're a story. And the PoW key fold carries a lesson for emulator design: a fixed instruction budget is a wall an attacker can simply walk around — budget on time and syscalls, not instructions, or accept that emulation only sees the artifacts that want to be seen. We'd rather you build that rule than read that we said "undetectable." We never said it.
Next note: The shell that was never spawned — the Phantom Shell, layer by layer: builtins that create nothing, a lazy runspace, return-clean stubs with a per-command lifetime, and the policy cache nobody mentions.