Rebuilding a Rootkit From Scratch

A captured driver is useless on an engagement — so we’re writing one that isn’t, use case by use case

Kernel
Red Team
Windows
Why we’re reimplementing a reverse-engineered kernel driver as authorized pentest tooling, and how emulating the original binary gives the rebuild a test oracle.
Published

August 6, 2026

The previous post ended with an unsigned kernel driver sitting in a lab, fully documented and waiting for instructions from a user-mode agent we never found. Grammar mapped, envelope broken, dispatch table understood.

The obvious next question, if you do offensive work, is whether you can use it. The answer is no, and the reasons are worth spelling out because they’re the entire argument for the project I actually want to talk about.

Why you can’t just use the sample

You cannot take a captured malware binary onto a client engagement. Not “shouldn’t” — cannot.

You don’t know what else is in it. We mapped the enforcement engine and the rule grammar, and there are still branches in that dispatch switch whose names we don’t know and whose behavior we’ve never triggered. Running that on a customer’s production estate means betting their network on the completeness of our own analysis. I like our analysis. I don’t like it that much.

You can’t fix it either. There’s no source, so when it bugchecks a domain controller at two in the morning — and we already know fltmc unload panics the box with a 0x7E — your options are to reboot and apologise. Every constant is baked. Every behavior is whatever the original author decided, including the parts written for someone else’s operation.

And there’s the part that isn’t technical. Deploying an unattributed criminal tool on a client’s machines is not something you want to explain afterwards, no matter how carefully scoped the rules of engagement were. The whole value of a red team exercise is that it’s controlled. A binary you don’t fully understand is the opposite.

So: rebuild it. Same architecture, same wire protocol, written from scratch, source you own and can reason about.

What “faithful” has to mean

The temptation in a project like this is to build something that achieves the same effect by easier means. Want to hide a process? There are simpler ways than the one this driver uses. Want to block a file open? You could shortcut it.

We wrote a rule against that, and it’s the most important rule in the repo: do not replace an original capability with an easier surrogate and call the use case complete. If the original behavior is unclear, stop and do more reverse engineering rather than guessing.

The reason is that fidelity is the entire product. The point of this tool is to reproduce what a real intrusion set does to a real endpoint, so the customer’s detection stack gets tested against the actual technique. If we implement process hiding by some cleaner route than the original, we’ve built a tool that tests our route. Nobody’s being attacked with our route.

That means matching the observed ABI down to details that look arbitrary. The control device follows the service name. The private IOCTL stays 0x90000404. Requests stay wrapped in the same 0x38-byte sealed envelope, which means the same DES payload, the same tweaked MD5 with its one-digit-off initialisation constant, and the same silent-drop behavior on validation failure. Command IDs keep their original numbers. The rule grammar keeps its syntax, including the parts nobody would design on purpose.

Bug-for-bug is the goal. A defender writing a signature against the real thing should catch ours.

The malware as a test oracle

Here’s the piece I think is genuinely worth stealing, whatever you’re rebuilding.

The hard part of a reimplementation isn’t writing code, it’s knowing whether your code is right. You have a decompilation that may be wrong in subtle ways, and no reference implementation to check against. Or rather, you have exactly one reference implementation, and it’s the malware.

So we run the malware. Not on Windows — under Unicorn, on Linux, as a pure function. Isolate the routine you care about in the original binary, emulate it with controlled inputs, capture the outputs, and freeze the whole thing as a cached artifact checked into the repo. Now your test suite asserts that your clean-room code produces byte-identical results to the original binary’s actual machine code.

The first one covers the UC6 rule-load path. It pins the command ID, the input digest, the encrypted envelope bytes, and the folded DES key derived from the Fwpkclnt.lib string, which collapses to the eight bytes Fibkll.t before use. That artifact is regenerable — just emul UC6 --refresh re-derives it from the binary — so if we ever change a fixture we find out immediately whether we broke fidelity or just changed a test.

It also settles arguments. When the reimplementation and the decompilation disagree, neither opinion matters. Emulate the original and see what it actually does.

The limits are real. This only works for code that can be lifted out of kernel context and run as a deterministic function: crypto helpers, parsers, digest routines. Anything that touches a callback registration or a minifilter or the WFP engine can’t be emulated this way, and has to be proven on a live Windows box instead.

Where it actually is

Early. I want to be direct about this rather than let a writeup imply more than exists.

There’s a roadmap of eleven milestones, M0 through M10, each tied to specific use cases. M0 is a buildable WDK shell: it loads, creates a control device, accepts an IOCTL, unloads cleanly. That works. M4 is process obstruction, and the first slice of it — the phl:path(...),action(deny) process-hide rule policy — parses and validates against the cached emulation truth.

Everything past that is unwritten. Rule store, process inventory, file and registry policy, module monitoring, network telemetry, network obstruction, and the Language A agent protocol are all roadmap entries with test plans and no code. The honest summary is that we have a driver that loads and one rule type that parses.

Getting even that far surfaced the usual kernel-development tax. Unsigned drivers don’t load under Driver Signature Enforcement, so builds get test-signed with a local machine certificate imported into the Root and TrustedPublisher stores, on a box with test-signing enabled. The smoke test copies the signed driver to a fresh VM, creates a kernel service, starts it, opens the device, sends a no-op and a clear-rules command, checks that an invalid command actually fails, then tears the service down. That last check matters more than the positive ones. A driver that accepts everything is indistinguishable from a driver that ignores everything, which is precisely the trap the original binary set for us with its silent drops.

Test VMs are disposable by design. A wedged kernel test gets recovered by snapshot revert, not by repair.

Building something that documents its own tells

One design choice I didn’t expect to care about as much as I do: every use case in the spec carries a section called defensive observations.

UC-01, network telemetry, notes that a defender would see a new kernel driver, WFP provider and filter changes, an unexpected shared buffer mapping, and an unusual single-client event reader. UC-02 notes repeated private IOCTL calls to an unfamiliar device. Each capability ships with a written account of what it looks like from the blue team’s chair.

That started as documentation discipline and turned into the thing that makes the project defensible. A red team tool exists to be caught — that’s the deliverable. If the customer’s stack doesn’t see it, the finding is the gap, and you can only report the gap credibly if you wrote down in advance what should have been visible. It also means that if this work ever informs detection engineering rather than testing, the detection half is already written.

Fourteen use cases, eleven milestones, two of them done. Ask me again in a few months whether the fidelity rule survived contact with M8, because reimplementing a WFP callout chain faithfully is a substantially different proposition from parsing a rule string, and that’s where I expect the argument for taking a shortcut to get genuinely tempting.