Criando um EDR/AV Part-1


Posted Aug 6, 2024. 12 min read


Bom, neste post vou criar uma ideia para o começo de um EDR/AV básico que provavelmente eu nunca vou terminar.
A ideia principal agora é criar uma DLL simples que utilize a MinHook para conseguir realizar um hook em APIs.

Como vai funcionar?

Primeiro vamos fazer o código principal que será responsável por injetar nossa dll no executavel que queremos monitorar.
O principal intuito vai ser monitorar as APIs utilizadas por loaders. O intuito é apenas monitorar as chamadas de API da kernel32.
Ou seja, se o programa utilizar técnicas como syscalls indiretas ou diretas, nosso EDR/AV não terá como detectar o loader.

Código Principal:

Código que obtém o ID do processo com o nome fornecido:


DWORD GetProcessIdByName(const wstring& processName) {
    DWORD processId = 0;
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
    if (Process32First(snapshot, &processEntry)) {
        do {
            if (processName == processEntry.szExeFile) {
                processId = processEntry.th32ProcessID;
                break;
            }
        } while (Process32Next(snapshot, &processEntry));
    }
    CloseHandle(snapshot);
    return processId;
}

Código para Injeção da DLL:

  1. 1 Abertura do Processo Alvo: Utiliza a API OpenProcess para obter um identificador para o processo alvo, especificado pelo processId.
  2. 2 Localização da Função LoadLibraryW: Obtém o endereço da função LoadLibraryW na biblioteca kernel32.dll que será utilizada para carregar a DLL no processo alvo.
  3. 3 Alocação de Memória no Processo Alvo: Usa VirtualAllocEx para alocar memória no processo alvo para armazenar o caminho da DLL.
  4. 4 Escrita do Caminho da DLL na Memória do Processo Alvo: Com WriteProcessMemory.
  5. 5 Criação de um Novo Thread: Cria um novo thread no processo alvo com CreateRemoteThread, que executa a função LoadLibraryW para carregar a DLL.
  6. 6 Aguarda a Conclusão da Injeção: Utiliza WaitForSingleObject para aguardar o término do thread.
  7. 7 Limpeza e Fechamento: Após a execução, libera a memória alocada com VirtualFreeEx e fecha o handle do processo e do thread com CloseHandle.

int InjectDll(DWORD processId, const wstring& dllPath) {
    HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId);
    if (processHandle == NULL) {
        return -1;
    }

    LPVOID loadLibraryAddress = (LPVOID)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
    if (loadLibraryAddress == NULL) {
        CloseHandle(processHandle);
        return -2;
    }

    size_t pathLength = (dllPath.size() + 1) * sizeof(wchar_t);
    LPVOID remoteDllPath = VirtualAllocEx(processHandle, NULL, pathLength, MEM_COMMIT, PAGE_READWRITE);
    if (remoteDllPath == NULL) {
        CloseHandle(processHandle);
        return -3;
    }

    if (!WriteProcessMemory(processHandle, remoteDllPath, dllPath.c_str(), pathLength, NULL)) {
        VirtualFreeEx(processHandle, remoteDllPath, 0, MEM_RELEASE);
        CloseHandle(processHandle);
        return -4;
    }

    HANDLE threadHandle = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)loadLibraryAddress, remoteDllPath, 0, NULL);
    if (threadHandle == NULL) {
        VirtualFreeEx(processHandle, remoteDllPath, 0, MEM_RELEASE);
        CloseHandle(processHandle);
        return -5;
    }

    WaitForSingleObject(threadHandle, INFINITE);

    DWORD exitCode = 0;
    GetExitCodeThread(threadHandle, &exitCode);

    CloseHandle(threadHandle);
    VirtualFreeEx(processHandle, remoteDllPath, 0, MEM_RELEASE);
    CloseHandle(processHandle);

    return exitCode;
}

NamedPipeServer para Comunicação:

O NamedPipeServer é basicamente responsável por criar um servidor de Named Pipe que escuta por conexões e processa mensagens recebidas.

Explicação do Código:

  1. 1 Criação do Named Pipe: Utiliza CreateNamedPipe para criar um pipe nomeado (MyPipe) que aceita conexões. Configurado para acesso de entrada (PIPE_ACCESS_INBOUND), com suporte a mensagens e leitura no modo de mensagens.
  2. 2 Conexão com o Pipe: ConnectNamedPipe aguarda a conexão de um cliente ao pipe. Se falhar, o erro é exibido e o pipe é fechado.
  3. 3 Leitura de Dados: Usa ReadFile para ler os dados do pipe. Se ocorrer um erro de leitura ou o pipe for quebrado (ERROR_BROKEN_PIPE), o loop de leitura é interrompido. Se a leitura for bem-sucedida, os dados são exibidos no console.
  4. 4 Fechamento e Repetição: Após a leitura ou se a conexão falhar, o handle do pipe é fechado. O servidor continua a executar e criar novos pipes em um loop infinito.

void StartNamedPipeServer() {
    while (true) {
        HANDLE hPipe = CreateNamedPipe(
            TEXT("\\\\.\\pipe\\MyPipe"),
            PIPE_ACCESS_INBOUND,
            PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
            1, 0, 0, 0, NULL);

        if (hPipe == INVALID_HANDLE_VALUE) {
            cerr << "Failed to create named pipe. Error: " << GetLastError() << endl;
            return;
        }

        if (ConnectNamedPipe(hPipe, NULL) != FALSE) {
            char buffer[1024];
            DWORD bytesRead;

            while (true) {
                if (!ReadFile(hPipe, buffer, sizeof(buffer) - 1, &bytesRead, NULL)) {
                    if (GetLastError() == ERROR_BROKEN_PIPE) {
                        break;
                    }
                    else if (GetLastError() != ERROR_MORE_DATA) {
                        cerr << "ReadFile failed. Error: " << GetLastError() << endl;
                        break;
                    }
                }
                else {
                    buffer[bytesRead] = '\0';
                    cout << "Received: " << buffer << endl;
                }
            }
        }
        else {
            cerr << "Failed to connect to named pipe. Error: " << GetLastError() << endl;
            CloseHandle(hPipe);
            break;
        }

        CloseHandle(hPipe);
    }
}

int main() {
    PrintBanner();

    cout << "Escolha o modo de operacao:" << endl;
    cout << "1. Fornecer caminho completo do executavel" << endl;
    cout << "Digite sua escolha (1): ";
    int choice;
    cin >> choice;
    cin.ignore();

    wstring exePathW;
    DWORD processId = 0;
    wstring dllName(L"hook.dll");

    if (choice == 1) {
        cout << "Digite o caminho completo do executavel para abrir e escanear: ";
        string exePath;
        getline(cin, exePath);
        exePathW = wstring(exePath.begin(), exePath.end());

        STARTUPINFO si = { sizeof(si) };
        PROCESS_INFORMATION pi;
        if (!CreateProcess(NULL, &exePathW[0], NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
            cerr << "Falha ao iniciar o processo. Erro: " << GetLastError() << endl;
            return -1;
        }

        int result = InjectDll(pi.dwProcessId, dllName);
        if (result < 0) {
            cout << "Falha na injecao da DLL. Codigo de erro: " << result << endl;
            TerminateProcess(pi.hProcess, result);
            CloseHandle(pi.hProcess);
            CloseHandle(pi.hThread);
            return -1;
        }

        ResumeThread(pi.hThread);
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);

        cout << "Injecao realizada com sucesso. DLL conectada." << endl;
    }
    else {
        cerr << "Opcao invalida. Por favor, escolha 1." << endl;
        return -1;
    }

    StartNamedPipeServer();

    cout << "Pressione Enter para sair...";
    cin.get();

    return 0;
}

Código Responsavel pelo hook:

Inclusão da MinHook:

Este trecho de código inclui a biblioteca MinHook, uma popular biblioteca de hooking para a API do Windows.


#include "MinHook.h"

#ifdef _WIN64
#pragma comment(lib, "minhook.x64.lib")
#elif _WIN32
#pragma comment(lib, "minhook.x32.lib")
#endif

Funções:

Aqui definidos tipos de função para algumas das APIs do Windows que serão hooked.
Cada tipo de função corresponde a uma função da API do Windows que será substituída por uma função personalizada para monitorar ou modificar seu comportamento.

typedef BOOL(WINAPI* fnWriteProcessMemory)(
    HANDLE hProcess,
    LPVOID lpBaseAddress,
    LPCVOID lpBuffer,
    SIZE_T nSize,
    SIZE_T* lpNumberOfBytesWritten);

typedef BOOL(WINAPI* fnOpenProcess)(
    WORD dwDesiredAccess,
    BOOL  bInheritHandle,
    DWORD dwProcessId
);

typedef BOOL(WINAPI* fnVirtualAllocEx)(
    HANDLE hProcess,
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD  flAllocationType,
    DWORD  flProtect
);

typedef BOOL(WINAPI* fnCreateRemoteThread)(
    HANDLE                 hProcess,
    LPSECURITY_ATTRIBUTES  lpThreadAttributes,
    SIZE_T                 dwStackSize,
    LPTHREAD_START_ROUTINE lpStartAddress,
    LPVOID                 lpParameter,
    DWORD                  dwCreationFlags,
    LPDWORD                lpThreadId
);

Ponteiros para as funções originais:

Essa parte define ponteiros para as funções originais da API do Windows que serão substituídas pelos hooks. Esses ponteiros são necessários para que o código possa chamar as funções originais após interceptá-las.


fnWriteProcessMemory g_WriteProcessMemory = NULL;
fnOpenProcess g_OpenProcess = NULL;
fnVirtualAllocEx g_VirtualAllocEx = NULL;
fnCreateRemoteThread g_CreateRemoteThread = NULL;

Enviar Mensagem para o Named Pipe:

Essa parte é responsável por enviar mensagens para o nosso Named Pipe.


void SendMessageToPipe(const char* message) {
    HANDLE hPipe = CreateFile(
        TEXT("\\\\.\\pipe\\MyPipe"),
        GENERIC_WRITE,
        0,
        NULL,
        OPEN_EXISTING,
        0,
        NULL);

    if (hPipe != INVALID_HANDLE_VALUE) {
        DWORD bytesWritten;
        BOOL result = WriteFile(hPipe, message, strlen(message), &bytesWritten, NULL);

        if (!result) {
            char errorMsg[128];
            snprintf(errorMsg, sizeof(errorMsg), "Failed to write to pipe. Error: %lu\n", GetLastError());
            WriteFile(hPipe, errorMsg, strlen(errorMsg), &bytesWritten, NULL);
        }

        CloseHandle(hPipe);
    }
    else {
        char errorMsg[128];
        snprintf(errorMsg, sizeof(errorMsg), "Failed to open pipe. Error: %lu\n", GetLastError());
        HANDLE hPipeError = CreateFile(
            TEXT("\\\\.\\pipe\\MyPipe"),
            GENERIC_WRITE,
            0,
            NULL,
            OPEN_EXISTING,
            0,
            NULL);
        if (hPipeError != INVALID_HANDLE_VALUE) {
            DWORD bytesWrittenError;
            WriteFile(hPipeError, errorMsg, strlen(errorMsg), &bytesWrittenError, NULL);
            CloseHandle(hPipeError);
        }
    }
}

Código Responsavel por terminar o processo:

Essa parte do código exibe uma caixa de mensagem de alerta e em seguida encerra o processo atual.
essa função será usada para bloquear a execução do processo caso ele faça uso de uma API que consideramos maliciosa.


void BlockExecution(BOOL showMessageBox) {
    if (showMessageBox) {
        MessageBox(NULL, TEXT("Acao maliciosa detectada! O processo sera encerrado."), TEXT("EDR Alerta"), MB_ICONWARNING | MB_OK);
    }

    TerminateProcess(GetCurrentProcess(), 1);
}

Função para o hook OpenProcess:

Esta parte é uma função de um hook para a função OpenProcess. Ela vai interceptar as chamadas para a API OpenProcess, então vai enviar uma mensagem com o ID do processo alvo para nosso Named Pipe, e depois irá chamar a função original do OpenProcess.


BOOL WINAPI Hooked_OpenProcess(
    WORD dwDesiredAccess,
    BOOL  bInheritHandle,
    DWORD dwProcessId) {

    char message[512];
    snprintf(message, sizeof(message), "[#] OpenProcess Detected! Process ID: %lu", dwProcessId);
    SendMessageToPipe(message);

    return g_OpenProcess(dwDesiredAccess, bInheritHandle, dwProcessId);
}

Função para o hook WriteProcessMemory:

Essa é outra função de hook mas para a API WriteProcessMemory. essa parte registra detalhes sobre o endereço de memória que está sendo modificado e envia para o nosso Named Pipe, e depois chama a função original WriteProcessMemory para garantir que a operação de escrita ocorra normalmente.


BOOL WINAPI Hooked_WriteProcessMemory(
    HANDLE hProcess,
    LPVOID lpBaseAddress,
    LPCVOID lpBuffer,
    SIZE_T nSize,
    SIZE_T* lpNumberOfBytesWritten) {

    char message[512];
    snprintf(message, sizeof(message), "[#] WriteProcessMemory Detected! Address: %p", lpBaseAddress);
    SendMessageToPipe(message);

    return g_WriteProcessMemory(hProcess, lpBaseAddress, lpBuffer, nSize, lpNumberOfBytesWritten);
}

Função para o hook VirtualAllocEx:


BOOL WINAPI Hooked_VirtualAllocEx(
    HANDLE hProcess,
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD  flAllocationType,
    DWORD  flProtect) {

    char message[512];
    snprintf(message, sizeof(message), "[#] VirtualAllocEx Detected!");
    SendMessageToPipe(message);

    return g_VirtualAllocEx(hProcess, lpAddress, dwSize, flAllocationType, flProtect);
}

Função para o hook CreateRemoteThread:

Essa é outra função de hook, mas para a API CreateRemoteThread. Diferente das outras, essa irá chamar a função BlockExecution que irá barrar a execução do programa e em seguida, chamará a função original CreateRemoteThread.


BOOL WINAPI HookedCreateRemoteThread(
    HANDLE                 hProcess,
    LPSECURITY_ATTRIBUTES  lpThreadAttributes,
    SIZE_T                 dwStackSize,
    LPTHREAD_START_ROUTINE lpStartAddress,
    LPVOID                 lpParameter,
    DWORD                  dwCreationFlags,
    LPDWORD                lpThreadId) {

    char message[512];
    snprintf(message, sizeof(message), "[#] CreateRemoteThread Detected! Process: %ls");
    SendMessageToPipe(message);

    BlockExecution(TRUE);

    return g_CreateRemoteThread(hProcess, lpThreadAttributes, dwStackSize, lpStartAddress, lpParameter, dwCreationFlags, lpThreadId);
}

Função para Implementar o hook:

Agora nossa função SetupHook vai configurar todos os hooks necessários utilizando a biblioteca MinHook. então ela irá criar os hooks para VirtualAllocEx, CreateRemoteThread, OpenProcess, e WriteProcessMemory, e finalmente habilita todos os hooks criados. Se houver falhas em qualquer uma dessas operações, uma mensagem de erro é enviada para o Named Pipe.


void SetupHook() {

    if (MH_Initialize() != MH_OK) {
        SendMessageToPipe("Failed to initialize MinHook.\n");
        return;
    }

    if (MH_CreateHookApi(TEXT("kernel32"), "VirtualAllocEx", Hooked_VirtualAllocEx, (LPVOID*)&g_VirtualAllocEx) != MH_OK) {
        SendMessageToPipe("Failed to create hook for VirtualAllocEx.\n");
        return;
    }

    if (MH_CreateHookApi(TEXT("kernel32"), "CreateRemoteThread", HookedCreateRemoteThread, (LPVOID*)&g_CreateRemoteThread) != MH_OK) {
        SendMessageToPipe("Failed to create hook for CreateRemoteThread.\n");
        return;
    }

    if (MH_CreateHookApi(TEXT("kernel32"), "OpenProcess", Hooked_OpenProcess, (LPVOID*)&g_OpenProcess) != MH_OK) {
        SendMessageToPipe("Failed to create hook for OpenProcess.\n");
        return;
    }

    if (MH_CreateHookApi(TEXT("kernel32"), "WriteProcessMemory", Hooked_WriteProcessMemory, (LPVOID*)&g_WriteProcessMemory) != MH_OK) {
        SendMessageToPipe("Failed to create hook for WriteProcessMemory.\n");
        return;
    }

    if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK) {
        SendMessageToPipe("Failed to enable hook.\n");
        return;
    }

    SendMessageToPipe("Hook enabled.\n");
}

Iniciar thread após a dll ser carregada:

Agora por fim a função DllMain é o ponto de entrada para a DLL. Quando a DLL é carregada DLL_PROCESS_ATTACH, ela desativa as chamadas de thread para a DLL e configura os hooks. Quando a DLL é descarregada DLL_PROCESS_DETACH, ela desativa todos os hooks e desinicializa a biblioteca MinHook.


#ifdef _WINDLL
bool __stdcall DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
    switch (fdwReason) {
    case DLL_PROCESS_ATTACH:
        DisableThreadLibraryCalls(hinstDLL);
        SetupHook();
        break;
    case DLL_PROCESS_DETACH:
        MH_DisableHook(MH_ALL_HOOKS);
        MH_Uninitialize();
        break;
    }
    return TRUE;
}
#endif

Entendendo o que fizemos:

O código que fizemos implementa um sistema de hooking para monitorar e controlar chamadas para funções críticas da API do Windows, como OpenProcess, WriteProcessMemory, VirtualAllocEx, e CreateRemoteThread.
O uso de hooks nos permite interceptar essas funções para detectar e bloquear ações que possam indicar comportamento malicioso.
e enviar mensagens de alerta sobre o uso dessas APIs para o nosso "painel".

Prova de Conceito: