// Build log 02 / C2 & EDR research

I wrote a RAT in Rust to learn what EDRs actually catch

A remote access tool with a keylogger, screen capture, and persistence met Kaspersky Next EDR Optimum in my lab. The product flagged exactly one of those. Remove that one, and everything else walked through clean. What it caught, and what it ignored, taught me more about detection than any course has.

// Read this first

I wrote the agent and the server myself, and they only ever ran on virtual machines I own, inside an isolated network. The Kaspersky product was a trial, exercised the way a researcher would exercise it. Code is shown in fragments; the connective tissue that would make this a working tool is deliberately absent. The section at the end belongs to the defenders.

01Why build one at all

I can operate several commercial C2 frameworks. Give me a lab and a license and I'll have sessions flowing in minutes. That's exactly the problem: operating a tool tells you where the buttons are, not why the tool is shaped the way it is. I couldn't have answered the simplest question a red teamer should be able to answer. What does it actually take to build one, and what does a modern endpoint notice when it runs?

So I set a constraint that shaped everything else. The agent would be quiet not by adding evasion tricks, but by being boring. It should look like a background service checking in with its backend, because half the legitimate software on any Windows machine does exactly that, all day.

Scene from the movie Hackers with the text Hack the Planet
// the ambition. the implementation was two small programs and one boring loop.

02Be boring

The finished system is two programs. A Rust agent on the target, and a small Node.js server on my box holding an operator dashboard. The agent registers once, then repeats one loop forever: ask the server for a task, run it locally, send the output back, sleep.

// the entire protocol register once loop [ ask for a task do it locally report back ] sleep

Plain HTTP, no encryption layer of my own, no custom protocol, no staging. The traffic pattern is indistinguishable from a chat client refreshing, a dashboard polling, a sync service idling. I didn't hide the traffic. I made it unremarkable, which turns out to be a different and much cheaper thing to do.

Sessions carry this idea further. Every run of the agent generates its own random 16-character key, and that key is its whole identity: it registers with it, and the server addresses every command and result to it. Nothing about the session is reusable across runs, because reusable strings are how signatures get written. New run, new key, new session, and the old one evaporates.

03The OS does the work

The feature list reads like every remote access tool ever built: interactive shell, keylogging, screen capture, saved Wi-Fi credentials, mouse and keyboard control. The implementation reads differently. Almost none of it is code I brought to the machine. Almost all of it is the machine's own tooling, aimed by my agent.

The remote shell is cmd.exe invoked with cmd /C, with a working directory the agent remembers between commands so it feels like a real session. When I want PowerShell, the agent launches powershell.exe -ExecutionPolicy Bypass -NoProfile and hands it my string. The screen capture isn't an API call from Rust at all: it's a PowerShell snippet that borrows .NET's own screen classes, grabs the primary display, and returns a Base64 PNG. Wi-Fi credentials are two netsh commands, the second one asking for key=clear, which returns saved passwords in plaintext.

The keylogger was the only component with genuine state, and even it is an API polling loop. The agent walks the virtual key codes, asks GetAsyncKeyState about each one, keeps a 256-slot memory of what was pressed on the previous pass, and records only the keys that transitioned from up to down. Shift and caps lock state are checked so the log reads like typing instead of symbols.

I came to think of the agent as fingers rather than a toolbox. Every process doing real work on the target, cmd, powershell, netsh, is one of the most trusted binaries on the machine, present by design, expected by every baseline. An EDR cannot flag their existence. The question it has to answer instead is whether their usage pattern makes sense, and that's a much harder question to answer with signatures.

04Persistence, wearing a OneDrive name

Surviving a reboot takes one registry value in the Run key, which every Windows user and every malware author has known for twenty years. The interesting decision was never whether to persist, it was what to call it:

main.rsrust
// HKCU\Software\Microsoft\Windows\CurrentVersion\Run
run.set_value("OneDriveSyncHelper", &executable_path)?;

Open your own Run key sometime and count the vendor entries. One more name that sounds like sync infrastructure blends into the wallpaper. Nobody escalates OneDriveSyncHelper on a Monday morning. This is masquerading in its purest form, MITRE ATT&CK T1036: not hiding the action, just dressing it as something triage already forgives.

Dwight from The Office delivering a fact
// fact: the Run key is the most watched 200 bytes in the Windows registry

05The Kaspersky test

Against Defender, fully updated, real-time protection on and full scans before and after, the agent ran clean the entire way through. No alerts at any stage, on any feature. That established the baseline. Then I moved the same binary to a second lab machine running Kaspersky Next EDR Optimum, a proper enterprise product with behavioral telemetry, and changed exactly one thing per run. An EDR only teaches you something if you know which change produced which verdict.

The results were specific. With persistence enabled, Kaspersky flagged the agent as generic malicious activity within moments. Every feature was present, the shell worked, screenshots arrived, and it was caught. I removed the persistence routine and changed nothing else. The same binary, with the same keylogger and the same polling loop, ran without a single alert. Full remote control, established and used, invisible.

A third finding came from watching the rhythm. An agent that receives its callback and instantly fires a command looks like machinery, because machinery is what behaves with zero hesitation. Adding a short, randomized pause between check-in and action made the session breathe like a person at a terminal. The feature set was identical. The timing was the tell.

kaspersky-edr-bypass.mp4PoC recording
// full PoC: the agent operating under Kaspersky Next EDR Optimum in the lab

Kaspersky never recognized my code. It recognized what my code did, in an order that means something.

06Chains, not atoms

Sit with what actually got flagged. Writing one value into the Run key is done by every installer on earth, and it lives in the registry of every Windows machine you've ever touched. Polling a server over HTTP is every Slack client, every Teams install, every update checker. Individually, both are white noise. Together, performed by the same process, they describe an implant with total confidence.

That's the model modern detection actually runs on. No single behavior is damning; the product scores combinations. My persistence routine didn't get caught because it was written badly. It got caught because a registry write followed by continuous external polling is one of the highest-scoring chains that exists, and no amount of naming tricks changes the underlying sequence.

As a defender, I find this genuinely encouraging. The product reasoned about behavior and reached the right verdict. As an attacker, the takeaway is equally sharp: know which chains carry score for the specific product in front of you, because they differ, and you only learn them by testing in a lab you own.

// What the blue team actually sees

I know every artifact this implant leaves, because I put them there. In priority order:

  • Run key writes. The most instrumented 200 bytes in the registry. My tool was caught here, and I'd tune this alert before any other.
  • Metronome timing. Polling on a fixed 100ms clock is a pattern a statistician can find blindfolded. Jitter is cheap for me and expensive for them.
  • PowerShell with intent. -ExecutionPolicy Bypass, screen capture assembly, launched by an unsigned binary. Each step legal. The combination scores.
  • netsh asking for plaintext. key=clear is not a helpdesk request. Two commands, unambiguous.
  • Reborn sessions. New key every run defeats string matching, but the same host reappearing with a fresh identity after each boot is itself a signal worth correlating.

07What stuck

The biggest surprise was how little cleverness this took. No injection, no custom crypto, no exotic protocol. A boring loop with rotating identities ran clean under a consumer engine, and survived an enterprise one the moment I removed its single loudest habit. Every advanced evasion idea I had before writing this was worth less than deleting one feature.

The next build log steps down a level, into the bytes of a Windows executable. Of the three projects in this series, it's the one that changed how I do my day job.

← All build logs