
Moteur de hooking en mode utilisateur sans patch et d'instrumentation de télémétrie basé sur les points d'arrêt matériels (DR0-DR7) (PoC AMSI, WLDP et ETW).
Un Proof-of-Concept (POC) de recherche en sécurité démontrant le hooking de fonctions basé sur les points d'arrêt matériels (registres de débogage du CPU) comme alternative au patch de code en mémoire traditionnel.
Objectif et portée
Ce dépôt est publié strictement pour la recherche en sécurité défensive, l'éducation red-team / purple-team, l'ingénierie de détection et l'étude académique des mécanismes internes de Windows. Il démontre comment un attaquant pourrait abuser des registres de débogage du processeur pour neutraliser la télémétrie de sécurité en mode utilisateur — et, tout aussi important, ce que les défenseurs doivent surveiller afin de détecter de telles techniques. L'auteur n'est pas responsable de toute utilisation abusive de ce code. L'utilisation de cette technique contre des systèmes sans autorisation explicite est illégale et viole les lois relatives à la fraude informatique et aux abus dans la plupart des juridictions. Ne déployez pas ceci dans un environnement que vous ne possédez pas ou pour lequel vous n'avez pas d'autorisation écrite explicite de test.
mora_hwbp.c implémente une DLL qui, une fois chargée/injectée dans un processus cible (par exemple, un hôte PowerShell), hooke quatre fonctions en mode utilisateur exclusivement via des points d'arrêt matériels du CPU stockés dans les registres de débogage architecturaux (DR0–DR7) de chaque thread du processus :
Un gestionnaire d'exceptions vectorisées (VEH) par processus reçoit les défauts EXCEPTION_SINGLE_STEP (0x80000004) déclenchés par les registres de débogage, simule le chemin de retour réussi de la fonction d'origine en réécrivant le contexte d'exception, puis reprend l'exécution — le tout sans modifier un seul octet de mémoire exécutable.
Cela rend la technique particulièrement intéressante d'un point de vue offensif comme défensif :
.text modifiées (hooking inline classique, patch EAT/IAT, ou stub Etwp*).GetThreadContext/SetThreadContext) qui peuvent être utilisés pour la détection.Les approches traditionnelles de hooking en mode utilisateur — detours inline (écrasements de 5 à 14 octets), hooking de la table d'adresses d'importation (IAT) et hooking de la table d'adresses d'exportation (EAT) — partagent une faiblesse commune : elles modifient de la mémoire que les scanners d'intégrité et l'ETW peuvent observer.
Les produits AV/EDR modernes implémentent :
pageguard/guard-page, les transitions VirtualProtect vers PAGE_EXECUTE_READWRITE, et les incohérences de hachage de section.Les points d'arrêt matériels contournent tout cela :
.text.SetThreadContext, ce qui ne déclenche pas les signaux classiques de « mémoire modifiée » utilisés par les scanners d'intégrité.Ce POC explore l'efficacité et la détectabilité de cette technique contre AMSI (Antimalware Scan Interface), WLDP (Windows Lockdown Policy) et ETW (Event Tracing for Windows) — les trois primitives de sécurité en mode utilisateur les plus utilisées dans la pile de sécurité Windows moderne.
AMSI est le point d'intégration de la plateforme Windows qui permet aux applications (PowerShell, Office, VBScript, hôtes .NET, etc.) de demander l'analyse de contenu aux fournisseurs antimalware enregistrés. Deux points d'entrée présentent un intérêt majeur :
AmsiScanBuffer(HANDLE hamsiContext, PVOID buffer, ULONG length, LPCWSTR contentName, HANDLE hamsiSession, AMSI_RESULT *pResult)AmsiScanString(HANDLE hamsiContext, LPCWSTR string, LPCWSTR contentName, HANDLE hamsiSession, AMSI_RESULT *pResult)En forçant le AMSI_RESULT retourné à AMSI_RESULT_CLEAN (0), le moteur de script croit que le contenu a été inspecté et jugé bénin, l'exécution se poursuit donc sans interruption.
WLDP implémente l'évaluation des politiques pour Windows Defender Application Control (WDAC / Device Guard). WldpIsClassInApprovedList répond si une classe COM donnée (identifiée par GUID) est autorisée par la politique actuelle. AMSI consulte en interne WLDP pour décider si certaines classes de scripts/contenus sont « de confiance » (dans la liste approuvée). Si la fonction signale la classe comme approuvée, AMSI peut éviter un examen supplémentaire pour ce type de contenu.
La DLL définit le paramètre de sortie isApproved (RDX) à TRUE et retourne S_OK, ce qui fait apparaître la classe évaluée comme digne de confiance.
EtwEventWrite dans ntdll.dll est le puits (sink) central en mode utilisateur pour pratiquement toute émission d'événements ETW sur le système. Le supprimer a des effets secondaires importants pour la surveillance de sécurité :
Microsoft-Windows-DotNETRuntime)La DLL retourne simplement ERROR_SUCCESS (0) sans exécuter la fonction réelle.
┌──────────────────────────────────────────────────────────────────────────┐ │ Target Process (e.g. powershell.exe) │ │ │ │ ┌──────────────────────────┐ ┌──────────────────────────────────┐│ │ │ mora_hwbp.dll │ │ CPU / Windows ││ │ │ │ │ ││ │ │ DllMain / InstallHook │ │ Thread A Thread B ││ │ │ │ │ │ ┌────────┐ ┌────────┐ ││ │ │ ▼ │ │ │ DR0..3 │ │ DR0..3 │ ││ │ │ Resolve exports │ │ └────────┘ └────────┘ ││ │ │ (amsi/wldp/ntdll) │ │ ││ │ │ │ │ │ #DB (single-step) ││ │ │ ▼ │ │ exception ──► Windows Dispatch ││ │ │ AddVectoredException │ │ │ ││ │ │ Handler(VEH) │ │ ▼ ││ │ │ │ │ │ ┌─────────────────────────────┐ ││ │ │ ▼ │ │ │ VectoredHandler (ours) │ ││ │ │ SetHwbpOnThread(ALL) │ │ │ • match #DB address │ ││ │ │ │ │ │ │ • rewrite context (RIP/RSP)│ ││ │ │ ▼ │ │ │ • spoof return value (RAX) │ ││ │ │ MonitorThread ◄──┐ │ │ │ • continue execution │ ││ │ │ (re-hook every │ │ │ └─────────────────────────────┘ ││ │ │ 500ms) └──────┘ │ ││ │ └──────────────────────────┘ └──────────────────────────────────┘│ └──────────────────────────────────────────────────────────────────────────┘
**Flux de haut niveau :**
1. La DLL est chargée dans le processus cible (via n'importe quelle technique d'injection — voir [Utilisation](#injection--usage-example)).
2. Sur `DLL_PROCESS_ATTACH` (ou via l'export `InstallHook`), les exports cibles sont résolus avec `GetProcAddress` (en forçant éventuellement le chargement de modules via `LoadLibraryW`).
3. Un **Vectored Exception Handler** est enregistré comme **premier** gestionnaire du processus (`AddVectoredExceptionHandler(1, ...)`).
4. Le **thread courant** est hooké immédiatement, puis **tous les threads existants** du processus sont énumérés via `CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)` et hookés.
5. Un **thread de surveillance** se réveille toutes les 500 ms et réapplique les breakpoints sur chaque thread — y compris les **threads nouvellement créés** — garantissant la persistance du hook même si un thread est créé après le hooking ou si les breakpoints sont effacés de l'extérieur.
6. Lorsqu'une fonction hookée est appelée sur n'importe quel thread, le CPU lève une exception single-step `#DB` ; Windows la distribue au VEH, qui simule un retour bénin et poursuit l'exécution.
### Exemple de sortie de débogage

*Figure 1 — Exemple de sortie de diagnostics capturée avec Sysinternals DebugView. Chaque ligne rapporte l'adresse cible résolue et le compteur de hits en direct pour son registre de débogage correspondant.*
---
## Plongée technique approfondie
### 5.1 Breakpoints matériels sur x64
Sur x86/x64, chaque CPU fournit **quatre registres d'adresses de débogage matériel** (`DR0`–`DR3`) et un registre de contrôle (`DR7`). Tout thread s'exécutant avec un breakpoint non nul dans `DR0`–`DR3` déclenchera une faute dès que le pointeur d'instruction atteint cette adresse (ou si l'accès aux données correspond aux conditions configurées). Le registre de statut `DR6` enregistre quel breakpoint a été déclenché.
Les breakpoints matériels sont **sensibles au contexte** : ils sont stockés dans la structure `CONTEXT` du thread et ne s'appliquent qu'au thread sur lequel ils sont définis. C'est pourquoi une implémentation robuste doit définir des breakpoints sur **chaque thread** du processus (et les réappliquer en continu pour les nouveaux threads).
### 5.2 Disposition des registres de débogage (DR0–DR7)
`DR7` est un champ de bits contrôlant l'activation et le comportement des breakpoints :
| Bits | Champ | Signification |
|--------|-------|--------------------------------------------------|
| `0` | `L0` | Activation locale pour le breakpoint 0 (DR0) |
| `2` | `L1` | Activation locale pour le breakpoint 1 (DR1) |
| `4` | `L2` | Activation locale pour le breakpoint 2 (DR2) |
| `6` | `L3` | Activation locale pour le breakpoint 3 (DR3) |
| `8` | `LE` | Activation locale héritée (conservée pour compatibilité) |
| `9` | `GE` | Activation globale héritée (conservée pour compatibilité) |
| `16–17`| `R/W0`| Type d'accès pour BP0 (`00` = exécution d'instruction) |
| `18–19`| `Len0`| Longueur pour BP0 (`00` = 1 octet) |
| `20–21`| `R/W1`| Type d'accès pour BP1 (`00` = exécution d'instruction) |
| `22–23`| `Len1`| Longueur pour BP1 (`00` = 1 octet) |
| `24–25`| `R/W2`| Type d'accès pour BP2 (`00` = exécution d'instruction) |
| `26–27`| `Len2`| Longueur pour BP2 (`00` = 1 octet) |
| `28–29`| `R/W3`| Type d'accès pour BP3 (`00` = exécution d'instruction) |
| `30–31`| `Len3`| Longueur pour BP3 (`00` = 1 octet) |
Les quatre breakpoints sont configurés pour **l'exécution (fetch d'instruction) sur un seul octet**, ce qui est la condition appropriée pour des hooks d'entrée de fonction.
### 5.3 Le Vectored Exception Handler (VEH)
Lorsqu'un breakpoint se déclenche, le processeur lève une exception `#DB`. Sur Windows x64, la routine de dispatch de `ntdll` la route à travers la **chaîne VEH** du processus avant la chaîne de Structured Exception Handler (SEH) du thread. Le gestionnaire de ce projet :
1. **Filtre** — ne gère que `EXCEPTION_SINGLE_STEP` (`0x80000004`) ; tout le reste est transmis à `EXCEPTION_CONTINUE_SEARCH`.
2. **Fait correspondre** — compare `ExceptionAddress` avec les quatre adresses de fonctions connues.
3. **Réécrit le contexte** :
- `RIP = *(RSP)` → « retourne » à l'appelant d'origine en dépilant l'adresse de retour.
- `RSP += 8` → simule un `ret` (déroulage à instruction unique x64).
- `RAX = 0` → simule `S_OK` / `ERROR_SUCCESS` (code de retour de succès).
- `DR6 &= ~0xF` → efface les bits de statut du breakpoint afin que l'instruction puisse être ré-exécutée plus tard sans état parasite.
4. **Mute les paramètres de sortie** (voir [5.4](#54-per-component-interception-logic)).
5. **Retourne `EXCEPTION_CONTINUE_EXECUTION`**, ce qui indique à Windows de redémarrer le thread avec le contexte modifié — c'est-à-dire que l'exécution reprend chez l'*appelant*, et que la fonction cible réelle **ne s'exécute jamais**.
Chaque site d'interception est en outre enveloppé dans une protection SEH `__try/__except` afin qu'une disposition de pile inattendue ou malformée ne puisse pas faire planter le processus — une considération de robustesse pour les cibles hostiles/renforcées.
### 5.4 Logique d'interception par composant
**DR0 — `AmsiScanBuffer`** (x64, 6 premiers arguments dans `RCX, RDX, R8, R9, [RSP+0x28], [RSP+0x30]`) :```
HRESULT AmsiScanBuffer(HANDLE, PVOID, ULONG, LPCWSTR, HANDLE, AMSI_RESULT* pResult);
[RSP+0x28] [RSP+0x30]
AMSI_RESULT_CLEAN (0) dans le 6e paramètre (pResult, à [RSP+0x30]).S_OK (0) dans RAX.DR1 — AmsiScanString (x64, les 5 premiers arguments dans RCX, RDX, R8, R9, [RSP+0x28]) :```
HRESULT AmsiScanString(HANDLE, LPCWSTR, LPCWSTR, HANDLE, AMSI_RESULT* pResult);
[RSP+0x28]
- Écrit `AMSI_RESULT_CLEAN (0)` dans le 5ᵉ paramètre (`pResult`, à `[RSP+0x28]`).
- Renvoie `S_OK (0)` dans `RAX`.
**DR2 — `WldpIsClassInApprovedList`** (les 3 premiers arguments dans `RCX, RDX, R8`):```
HRESULT WldpIsClassInApprovedList(const GUID* classId, PBOOL isApproved, DWORD evalCriteria);
RCX RDX R8
TRUE dans *isApproved (via RDX).S_OK (0) dans RAX.DR3 — EtwEventWrite (les 4 premiers arguments dans RCX, RDX, R8, R9) :```
ULONG EtwEventWrite(HANDLE RegHandle, PCEVENT_DESCRIPTOR EventDescriptor,
ULONG UserDataCount, PEVENT_DATA_DESCRIPTOR UserData);
- Renvoie `ERROR_SUCCESS (0)` dans `RAX` sans toucher à aucun paramètre de sortie.
- Conséquence : les fournisseurs ETW ne reçoivent **aucun** événement du processus hooké, supprimant ainsi la journalisation de l'exécution de scripts, du chargement de modules, de la création de processus et de la télémétrie AMSI.
### 5.5 Gestion des threads et persistance des hooks
Comme les registres de débogage sont propres à chaque thread, le moteur doit maintenir les hooks en continu :
1. **Hook immédiat** — `DllMain` (ou `InstallHook`) hook le thread appelant avec `SetHwbpOnThread(GetCurrentThread())`.
2. **Balayage de tous les threads** — `HookAllThreads()` énumère chaque thread du processus via un instantané `TH32CS_SNAPTHREAD`, suspend chaque thread externe (`SuspendThread`), applique les points d'arrêt (`SetHwbpOnThread`), le reprend et ferme le handle. La suspension empêche une condition de course dans laquelle le thread plante au milieu d'un changement de contexte entre `GetThreadContext` et `SetThreadContext`.
3. **Moniteur de persistance** — `MonitorThreadProc` boucle avec `Sleep(500)` et appelle `HookAllThreads()` toutes les 500 ms. Cela **réarme tous les points d'arrêt qui ont été supprimés** (par exemple, par un appel externe à `SetThreadContext`, un outil de débogage ou la destruction/création de threads) et **couvre les threads créés après le hook initial**.
4. **Synchronisation** — `HookAllThreads` s'exécute sous une `CRITICAL_SECTION` (`g_HookLock`) afin que le thread moniteur et la routine de hook initiale n'entrelacent jamais les changements de contexte.
5. **Arrêt propre** — `UninstallHook` arrête le moniteur, efface `DR0–DR7` sur chaque thread et désenregistre le VEH.
> **Réponse explicite à la question de la « persistance des hooks » :** oui — si les points d'arrêt sont retirés d'un thread (par un autre agent, un débogueur ou un EDR), le thread moniteur **les réapplique dans les 500 ms**. De plus, tout thread créé après le chargement de la DLL est hooké dans un cycle du moniteur. La seule façon fiable de vaincre ce moteur spécifique est de terminer le thread moniteur *et* d'effacer le VEH *et* de retirer les registres dans la même fenêtre — ou d'utiliser un anti-débogage qui refuse `SetThreadContext` dès le départ.
---
## API exportée
| Exportation | Signature | Comportement |
|-------------------|------------------------------------|-----------------------------------------------------------------------|
| `InstallHook` | `BOOL WINAPI InstallHook(void)` | Résout les cibles, enregistre le VEH, hook tous les threads, démarre le moniteur. |
| `UninstallHook` | `BOOL WINAPI UninstallHook(void)` | Arrête le moniteur, efface les points d'arrêt sur tous les threads, supprime le VEH. |
| `GetStats` | `void WINAPI GetStats(void)` | Émet l'état actuel du hook (via `OutputDebugStringA`) — adresse, compteurs de hits. |
Les compteurs de hits (`g_HaveAmsiBuf`, `g_HaveAmsiStr`, `g_HaveWldp`, `g_HaveEtw`) sont maintenus avec `InterlockedIncrement` et sont exposés dans la sortie de débogage, ce qui est utile pour valider que l'interception se produit réellement dans un environnement de laboratoire.
Notez que `DllMain` exécute lui-même la séquence de hook complète sur `DLL_PROCESS_ATTACH` ; les exports sont donc des commodités facultatives pour les scénarios de chargement/déchargement à l'exécution.
---
## Instructions de compilation
**Prérequis :** Windows 10/11 x64, Visual Studio Build Tools (`icx.exe`), SDK.
Compilez la DLL (x64) :```bat
icx.exe /nologo /O3 /MT /EHsc "mora_hwbp.c" /link /DLL /out:"mora_hwbp.dll" /LIBPATH:"C:\Program Files (x86)\Intel\oneAPI\compiler\latest\lib"
Flags explained:
| Flag | Purpose |
|---|
Le résultat est mora_hwbp.dll, qui peut être chargé dans un processus cible.
La DLL doit être chargée dans un processus qui utilise AMSI/WLDP/ETW — un hôte PowerShell est le banc d'essai canonique. Le chargement peut être effectué avec toute technique standard d'injection de DLL. Une démonstration minimale et autonome utilisant l'injection réflective/LoadLibrary peut être réalisée avec un petit chargeur C :```bat
rem Run from an x64 developer prompt (example with a generic loader)
loader.exe mora_hwbp.dll powershell.exe
Ou, pour une vérification manuelle en laboratoire, injectez avec l'outil de votre choix, puis validez depuis PowerShell :```powershell
# 1. Inject mora_hwbp.dll into the PowerShell process (via external tool).
# 2. Verify that classic AMSI test vectors now return clean.
"AmsiTestSample:7e72c3ce-861b-4339-8740-0ac1484c1386"
Validation en laboratoire uniquement. Observez avec un débogueur ou
GetStats/OutputDebugStringque les quatre points d'arrêt signalent des hits lorsque le contenu du script est exécuté.
Cette POC a un double objectif : les mêmes caractéristiques qui la rendent efficace offensivement sont précisément ce que les défenseurs doivent rechercher.
GetThreadContext(CONTEXT_DEBUG_REGISTERS) sur les processus à haute valeur et auditez tout thread dont les DR0–DR3 sont non nuls en dehors des profils de débogueur approuvés.Microsoft-Windows-Kernel-Process + Thread et alertez sur NtGetContextThread/NtSetThreadContext ciblant des processus critiques pour la sécurité.EtwEventWrite, ETW du noyau, re-vérification du consommateur AMSI) plutôt que sur la seule intégrité .text.SetThreadContext par thread vers d'autres processus comme un signal explicite de gravité élevée.RCX/RDX/R8/R9 puis [RSP+0x20…]). Une variante x86 nécessiterait une reconstruction des paramètres de type [EBP+…].Sleep(500) ; une création de threads extrêmement rapide combinée à une suppression agressive pourrait théoriquement distancer le moniteur pendant quelques centaines de millisecondes.OutputDebugStringA — les diagnostics reposent sur un canal de sortie de débogage ; dans un environnement entièrement dépouillé/sans tête, vous devriez attacher un débogueur ou rediriger la sortie pour l'observation en laboratoire.Ce projet est sous licence MIT - voir le fichier LICENSE pour plus de détails.
Ce projet est publié à des fins exclusivement éducatives et de recherche défensive. Si vous êtes un éditeur de sécurité, un membre d'une équipe bleue ou un ingénieur en détection, vous êtes encouragé à utiliser le contenu de ce dépôt pour améliorer votre couverture de détection de l'évasion basée sur les points d'arrêt matériels. Si vous découvrez que cette technique est utilisée abusivement dans la nature, signalez-le via le processus de divulgation responsable de votre organisation et les canaux du fournisseur ou des autorités concernés.
À utiliser à vos propres risques. Une utilisation non autorisée de cette technique peut enfreindre les lois applicables.
| Registre | Fonction hookée | Module | Objectif |
|---|
DR0 | AmsiScanBuffer | amsi.dll | Neutraliser l'analyse de contenu AMSI |
DR1 | AmsiScanString | amsi.dll | Neutraliser l'analyse de chaînes AMSI |
DR2 | WldpIsClassInApprovedList | wldp.dll | Forcer l'approbation de classe WLDP (Device Guard / WDAC) |
DR3 | EtwEventWrite | ntdll.dll | Supprimer le traçage d'événements ETW |
/O3 | Optimisation maximale (code de fonction, non requis) |
/MT | Liaison CRT statique (aucune dépendance à une DLL d'exécution) |
/EHsc | Gestion des exceptions C++/SEH (nécessaire pour __try) |
/DLL | Produire une DLL avec une table d'exportation |
| Artefact | Observable |
|---|
Appels GetThreadContext / SetThreadContext | Changements de contexte de registres de débogage à haute fréquence sur d'autres processus/threads (ETW du noyau : Microsoft-Windows-Kernel-Process/API Thread). |
DR0–DR3 non nuls | Tout thread dont CONTEXT_DEBUG_REGISTERS contient une adresse en mode utilisateur en dehors des flux de travail connus du débogueur. |
DR7 bits d'activation locaux (L0–L3) avec R/W = 00 | Points d'arrêt en exécution seule sur des threads non gérés par le débogueur — une forte anomalie. |
Volume EXCEPTION_SINGLE_STEP | Taux élevés de fautes #DB (0x80000004) provenant du VEH d'un processus. |
| Enregistrement VEH de première chance | VEH nouvellement ajouté (AddVectoredExceptionHandler) peu avant la tempête de #DB. |
TH32CS_SNAPTHREAD + SuspendThread/ResumeThread | Modèles répétés d'énumération de threads + de suspension (utilisés par le moniteur de 500 ms). |
Chargement de wldp.dll/amsi.dll via LoadLibraryW lorsqu'ils n'étaient pas chargés auparavant | Chargements de modules anormaux dans le processus cible. |
EtwEventWrite jamais atteint | Absence d'événements ETW attendus (journaux opérationnels PowerShell silencieux pendant l'exécution des scripts). |