
Motore di hooking in modalità utente senza patch e di strumentazione di telemetria basato su hardware breakpoint (DR0-DR7) (AMSI, WLDP e ETW PoC).
Una proof-of-concept (POC) di ricerca sulla sicurezza che dimostra l'hooking di funzioni basato su hardware breakpoint (registri di debug della CPU) come alternativa alle tradizionali tecniche di patching del codice in memoria.
Scopo e ambito
Questa repository è pubblicata esclusivamente per ricerca difensiva sulla sicurezza, formazione red-team/purple-team, ingegneria del rilevamento e studio accademico degli internals di Windows. Dimostra come un attaccante potrebbe abusare dei registri di debug del processore per neutralizzare la telemetria di sicurezza in modalità utente — e, altrettanto importante, cosa i difensori dovrebbero monitorare per rilevare tali tecniche. L'autore non è responsabile di qualsiasi uso improprio di questo codice. L'utilizzo di questa tecnica contro sistemi senza autorizzazione esplicita è illegale e viola le leggi pertinenti contro le frodi informatiche e gli abusi nella maggior parte delle giurisdizioni. Non distribuire questo codice in ambienti che non possiedi o per i quali non hai esplicita autorizzazione scritta a testare.
mora_hwbp.c implementa una DLL che, una volta caricata/iniettata in un processo target (ad esempio, un host PowerShell), aggancia quattro funzioni in modalità utente esclusivamente tramite hardware breakpoint della CPU memorizzati nei registri di debug architetturali (DR0–DR7) di ogni thread del processo:
Un Vectored Exception Handler (VEH) per processo riceve le eccezioni EXCEPTION_SINGLE_STEP (0x80000004) generate dai registri di debug, simula il percorso di ritorno riuscito della funzione originale riscrivendo il contesto dell'eccezione e riprende l'esecuzione — tutto senza modificare un singolo byte di memoria eseguibile.
Questo rende la tecnica particolarmente interessante sia dal punto di vista offensivo che difensivo:
.text modificate (classico inline hooking, patching di EAT/IAT o stubbing di Etwp*).GetThreadContext/SetThreadContext) utilizzabili per il rilevamento.Gli approcci tradizionali di hooking in modalità utente — detour inline (sovrascritture da 5 a 14 byte), hooking della import address table (IAT) e hooking della export address table (EAT) — condividono una debolezza comune: modificano memoria che gli scanner di integrità e l'ETW possono osservare.
I prodotti AV/EDR moderni implementano:
pageguard/guard-page, transizioni VirtualProtect a PAGE_EXECUTE_READWRITE e discrepanze negli hash delle sezioni.Gli hardware breakpoint aggirano tutto questo:
.text.SetThreadContext, che non innesca i classici segnali di "memoria modificata" usati dagli scanner di integrità.Questa POC esplora l'efficacia e la rilevabilità di questa tecnica contro AMSI (Antimalware Scan Interface), WLDP (Windows Lockdown Policy) e ETW (Event Tracing for Windows) — le tre primitive di sicurezza in modalità utente più utilizzate nello stack di sicurezza Windows moderno.
AMSI è il punto di integrazione della piattaforma Windows che consente alle applicazioni (PowerShell, Office, VBScript, host .NET, ecc.) di richiedere la scansione dei contenuti ai provider antimalware registrati. Due punti di ingresso sono di primario interesse:
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)Forzando il valore AMSI_RESULT restituito a AMSI_RESULT_CLEAN (0), il motore di scripting ritiene che il contenuto sia stato ispezionato e ritenuto benigno, quindi l'esecuzione prosegue senza interruzioni.
WLDP implementa la valutazione dei criteri per Windows Defender Application Control (WDAC / Device Guard). WldpIsClassInApprovedList risponde se una determinata classe COM (identificata da GUID) è consentita dai criteri correnti. AMSI consulta internamente WLDP per decidere se determinate classi di script/contenuti sono "attendibili" (nella lista approvata). Se la funzione segnala la classe come approvata, AMSI può saltare ulteriori controlli per quel tipo di contenuto.
La DLL imposta il parametro di output isApproved (RDX) a TRUE e restituisce S_OK, facendo apparire la classe valutata come attendibile.
EtwEventWrite in ntdll.dll è il sink user-mode principale per praticamente tutta l'emissione di eventi ETW sul sistema. La sua soppressione ha ampi effetti collaterali rilevanti per il monitoraggio della sicurezza:
Microsoft-Windows-DotNETRuntime)La DLL restituisce semplicemente ERROR_SUCCESS (0) senza eseguire la funzione reale.
┌──────────────────────────────────────────────────────────────────────────┐ │ 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) └──────┘ │ ││ │ └──────────────────────────┘ └──────────────────────────────────┘│ └──────────────────────────────────────────────────────────────────────────┘
**Flusso ad alto livello:**
1. La DLL viene caricata nel processo di destinazione (tramite qualsiasi tecnica di iniezione — vedi [Utilizzo](#injection--usage-example)).
2. In corrispondenza di `DLL_PROCESS_ATTACH` (o tramite l'export `InstallHook`), gli export di destinazione vengono risolti con `GetProcAddress` (forzando facoltativamente il caricamento dei moduli tramite `LoadLibraryW`).
3. Un **Vectored Exception Handler** viene registrato come **primo** handler del processo (`AddVectoredExceptionHandler(1, ...)`).
4. Il **thread corrente** viene hookato immediatamente, quindi **tutti i thread esistenti** nel processo vengono enumerati tramite `CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)` e hookati.
5. Un **thread di monitoraggio** si risveglia ogni 500 ms e riapplica i breakpoint a ogni thread — inclusi i **thread di nuova creazione** — garantendo la persistenza dell'hook anche se un thread viene generato dopo l'hooking o se i breakpoint vengono cancellati esternamente.
6. Quando una qualsiasi funzione hookata viene chiamata su un qualsiasi thread, la CPU genera un'eccezione single-step `#DB`; Windows la inoltra al VEH, che simula un ritorno benigno e continua l'esecuzione.
### Output di debug di esempio

*Figura 1 — Output diagnostico di esempio catturato con Sysinternals DebugView. Ogni riga riporta l'indirizzo di destinazione risolto e il contatore di hit live per il rispettivo registro di debug.*
---
## Approfondimento Tecnico
### 5.1 Breakpoint Hardware su x64
Su x86/x64, ogni CPU fornisce **quattro registri hardware per indirizzi di debug** (`DR0`–`DR3`) e un registro di controllo (`DR7`). Qualsiasi thread in esecuzione con un breakpoint diverso da zero in `DR0`–`DR3` genera un fault ogni volta che il program counter raggiunge quell'indirizzo (o quando un accesso ai dati corrisponde alle condizioni configurate). Il registro di stato `DR6` registra quale breakpoint è scattato.
I breakpoint hardware sono **sensibili al contesto**: sono memorizzati nella struttura `CONTEXT` del thread e si applicano solo al thread su cui sono impostati. Questo è il motivo per cui un'implementazione robusta deve impostare i breakpoint su **ogni thread** del processo (e riapplicarli continuamente per i nuovi thread).
### 5.2 Layout dei Registri di Debug (DR0–DR7)
`DR7` è un campo di bit che controlla l'abilitazione e il comportamento dei breakpoint:
| Bit | Campo | Significato |
|--------|--------|-----------------------------------------------------|
| `0` | `L0` | Abilitazione locale per il breakpoint 0 (DR0) |
| `2` | `L1` | Abilitazione locale per il breakpoint 1 (DR1) |
| `4` | `L2` | Abilitazione locale per il breakpoint 2 (DR2) |
| `6` | `L3` | Abilitazione locale per il breakpoint 3 (DR3) |
| `8` | `LE` | Abilitazione locale legacy (mantenuta per compatibilità) |
| `9` | `GE` | Abilitazione globale legacy (mantenuta per compatibilità) |
| `16–17`| `R/W0` | Tipo di accesso per BP0 (`00` = esecuzione istruzione) |
| `18–19`| `Len0` | Lunghezza per BP0 (`00` = 1 byte) |
| `20–21`| `R/W1` | Tipo di accesso per BP1 (`00` = esecuzione istruzione) |
| `22–23`| `Len1` | Lunghezza per BP1 (`00` = 1 byte) |
| `24–25`| `R/W2` | Tipo di accesso per BP2 (`00` = esecuzione istruzione) |
| `26–27`| `Len2` | Lunghezza per BP2 (`00` = 1 byte) |
| `28–29`| `R/W3` | Tipo di accesso per BP3 (`00` = esecuzione istruzione) |
| `30–31`| `Len3` | Lunghezza per BP3 (`00` = 1 byte) |
Tutti e quattro i breakpoint sono configurati per l'**esecuzione (instruction-fetch) su un singolo byte**, condizione appropriata per gli hook in corrispondenza dell'ingresso delle funzioni.
### 5.3 Il Vectored Exception Handler (VEH)
Quando un breakpoint scatta, il processore genera un'eccezione `#DB`. Su Windows x64 la routine di dispatch di `ntdll` la instrada attraverso la **catena VEH** a livello di processo prima della catena dello Structured Exception Handler (SEH) del thread. L'handler di questo progetto:
1. **Filtra** — gestisce solo `EXCEPTION_SINGLE_STEP` (`0x80000004`); tutto il resto passa a `EXCEPTION_CONTINUE_SEARCH`.
2. **Confronta** — confronta `ExceptionAddress` con i quattro indirizzi di funzione noti.
3. **Riscrive il contesto**:
- `RIP = *(RSP)` → "ritorna" al chiamante originale facendo pop dell'indirizzo di ritorno.
- `RSP += 8` → simula una `ret` (unwind x64 a istruzione singola).
- `RAX = 0` → falsifica `S_OK` / `ERROR_SUCCESS` (codice di ritorno di successo).
- `DR6 &= ~0xF` → azzera i bit di stato del breakpoint così l'istruzione può essere rieseguita in seguito senza stati spuri.
4. **Modifica i parametri di output** (vedi [5.4](#54-per-component-interception-logic)).
5. **Restituisce `EXCEPTION_CONTINUE_EXECUTION`**, che indica a Windows di riavviare il thread con il contesto modificato — cioè l'esecuzione riprende dal *chiamante* e la funzione di destinazione reale **non viene mai eseguita**.
Ogni punto di intercettazione è inoltre avvolto in una protezione SEH `__try/__except` in modo che un layout dello stack malformato o inatteso non possa mandare in crash il processo — una considerazione di robustezza per target ostili/irrobustiti.
### 5.4 Logica di Intercettazione per Componente
**DR0 — `AmsiScanBuffer`** (x64, primi 6 argomenti in `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) nel sesto parametro (pResult, a [RSP+0x30]).S_OK (0) in RAX.DR1 — AmsiScanString (x64, primi 5 argomenti in RCX, RDX, R8, R9, [RSP+0x28]):```
HRESULT AmsiScanString(HANDLE, LPCWSTR, LPCWSTR, HANDLE, AMSI_RESULT* pResult);
[RSP+0x28]
- Scrive `AMSI_RESULT_CLEAN (0)` nel 5° parametro (`pResult`, a `[RSP+0x28]`).
- Restituisce `S_OK (0)` in `RAX`.
**DR2 — `WldpIsClassInApprovedList`** (primi 3 argomenti in `RCX, RDX, R8`):```
HRESULT WldpIsClassInApprovedList(const GUID* classId, PBOOL isApproved, DWORD evalCriteria);
RCX RDX R8
TRUE in *isApproved (tramite RDX).S_OK (0) in RAX.DR3 — EtwEventWrite (primi 4 argomenti in RCX, RDX, R8, R9):```
ULONG EtwEventWrite(HANDLE RegHandle, PCEVENT_DESCRIPTOR EventDescriptor,
ULONG UserDataCount, PEVENT_DATA_DESCRIPTOR UserData);
- Restituisce `ERROR_SUCCESS (0)` in `RAX` senza toccare alcun parametro di output.
- Conseguenza: i provider ETW non ricevono **nessun** evento dal processo sottoposto a hook, sopprimendo la registrazione dell'esecuzione di script, del caricamento dei moduli, della creazione di processi e della telemetria AMSI.
### 5.5 Gestione dei thread e persistenza degli hook
Poiché i registri di debug sono per-thread, il motore deve mantenere continuamente gli hook:
1. **Hook immediato** — `DllMain` (o `InstallHook`) applica l'hook al thread chiamante con `SetHwbpOnThread(GetCurrentThread())`.
2. **Scansione di tutti i thread** — `HookAllThreads()` enumera ogni thread del processo tramite uno snapshot `TH32CS_SNAPTHREAD`, sospende ogni thread esterno (`SuspendThread`), applica i breakpoint (`SetHwbpOnThread`), lo riprende e chiude l'handle. La sospensione previene una race condition in cui il thread genera un fault a metà dello scambio di contesto tra `GetThreadContext` e `SetThreadContext`.
3. **Monitor di persistenza** — `MonitorThreadProc` esegue un loop con `Sleep(500)` e chiama `HookAllThreads()` ogni 500 ms. Questo **riarma tutti i breakpoint che sono stati rimossi** (ad es. da una chiamata esterna a `SetThreadContext`, da uno strumento di debug o da terminazione/creazione di thread) e **copre i thread creati dopo l'hook iniziale**.
4. **Sincronizzazione** — `HookAllThreads` viene eseguito sotto una `CRITICAL_SECTION` (`g_HookLock`) così che il thread del monitor e la routine di hook iniziale non si sovrappongano mai nei context switch.
5. **Teardown pulito** — `UninstallHook` ferma il monitor, azzera `DR0–DR7` su ogni thread e deregistra il VEH.
> **Risposta esplicita alla domanda sulla "persistenza degli hook":** sì — se i breakpoint vengono rimossi da qualsiasi thread (da un altro agente, un debugger o un EDR), il thread del monitor **li riapplica entro 500 ms**. Inoltre, qualsiasi thread creato dopo il caricamento della DLL viene sottoposto a hook entro un ciclo del monitor. L'unico modo affidabile per sconfiggere questo specifico motore è terminare il thread del monitor *e* cancellare il VEH *e* rimuovere i registri nella stessa finestra temporale — oppure usare tecniche anti-debug che neghino `SetThreadContext` fin dall'inizio.
---
## API esportate
| Esportazione | Firma | Comportamento |
|------------------|-----------------------------------|------------------------------------------------------------------------|
| `InstallHook` | `BOOL WINAPI InstallHook(void)` | Risolve i target, registra il VEH, applica hook a tutti i thread, avvia il monitor. |
| `UninstallHook` | `BOOL WINAPI UninstallHook(void)` | Ferma il monitor, azzera i breakpoint su tutti i thread, rimuove il VEH. |
| `GetStats` | `void WINAPI GetStats(void)` | Emette lo stato corrente dell'hook (tramite `OutputDebugStringA`) — indirizzo, contatori di hit. |
I contatori di hit (`g_HaveAmsiBuf`, `g_HaveAmsiStr`, `g_HaveWldp`, `g_HaveEtw`) sono aggiornati con `InterlockedIncrement` e vengono esposti nell'output di debug, il che è utile per verificare che l'intercettazione stia effettivamente avvenendo in un ambiente di laboratorio.
Nota che `DllMain` stesso esegue l'intera sequenza di hook su `DLL_PROCESS_ATTACH`, quindi le export sono comodità opzionali per scenari di caricamento/scaricamento a runtime.
---
## Istruzioni di compilazione
**Requisiti:** Windows 10/11 x64, Visual Studio Build Tools (`icx.exe`), SDK.
Compila 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 | Scopo |
|---|
Il risultato è mora_hwbp.dll, che può essere caricato in un processo target.
La DLL deve essere caricata in un processo che utilizza AMSI/WLDP/ETW — un host PowerShell è il banco di prova canonico. Il caricamento può essere effettuato con qualsiasi tecnica standard di iniezione DLL. Una dimostrazione minima e autonoma che utilizza l'iniezione Reflective/LoadLibrary può essere eseguita con un piccolo loader in C:```bat
rem Run from an x64 developer prompt (example with a generic loader)
loader.exe mora_hwbp.dll powershell.exe
Oppure, per un controllo manuale in laboratorio, inietta con i tuoi strumenti preferiti e poi valida da 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"
Solo validazione in laboratorio. Osservare con un debugger o
GetStats/OutputDebugStringche tutti e quattro i breakpoint riportino successi durante l'esecuzione del contenuto dello script.
Questa POC ha un duplice scopo: le stesse caratteristiche che la rendono efficace in ambito offensivo sono esattamente ciò che i difensori dovrebbero cercare.
GetThreadContext(CONTEXT_DEBUG_REGISTERS) su processi ad alto valore e controllano qualsiasi thread con DR0–DR3 diversi da zero al di fuori dei profili debugger approvati.Microsoft-Windows-Kernel-Process + Thread e generare avvisi su NtGetContextThread/NtSetContextThread che prendono di mira processi rilevanti per la sicurezza.EtwEventWrite, ETW del kernel, ricontrollo dei consumer AMSI) piuttosto che alla sola integrità del .text.SetThreadContext per-thread verso altri processi come un segnale esplicito ad alta severità.RCX/RDX/R8/R9 poi [RSP+0x20…]). Una variante x86 richiederebbe una ricostruzione dei parametri in stile [EBP+…].Sleep(500); una creazione di thread estremamente rapida combinata con uno stripping aggressivo potrebbe teoricamente superare il monitor per qualche centinaio di millisecondi.OutputDebugStringA — la diagnostica si basa su un canale di output di debug; in un ambiente completamente stripped/headless è necessario collegare un debugger o reindirizzare l'output per l'osservazione in laboratorio.Questo progetto è concesso in licenza secondo i termini della MIT License - vedi il file LICENSE per i dettagli.
Questo progetto è rilasciato solo a scopo di ricerca educativa e difensiva. Se sei un vendor di sicurezza, un blue team o un ingegnere del rilevamento, sei incoraggiato a utilizzare i contenuti di questo repository per migliorare la copertura di rilevamento contro l'evasione basata su breakpoint hardware. Se hai scoperto questa tecnica abusata nel mondo reale, segnalala tramite il processo di divulgazione responsabile della tua organizzazione e i canali pertinenti del vendor/autorità.
Utilizzo a proprio rischio. L'uso non autorizzato di questa tecnica potrebbe violare le leggi applicabili.
| Registro | Funzione agganciata | Modulo | Scopo |
|---|
DR0 | AmsiScanBuffer | amsi.dll | Neutralizza la scansione dei contenuti AMSI |
DR1 | AmsiScanString | amsi.dll | Neutralizza la scansione delle stringhe AMSI |
DR2 | WldpIsClassInApprovedList | wldp.dll | Forza l'approvazione delle classi WLDP (Device Guard / WDAC) |
DR3 | EtwEventWrite | ntdll.dll | Sopprime la traccia degli eventi ETW |
/O3 | Ottimizzazione massima (codice di funzione, non richiesta) |
/MT | Collegamento CRT statico (nessuna dipendenza da DLL runtime) |
/EHsc | Gestione eccezioni C++/SEH (necessaria per __try) |
/DLL | Produce una DLL con una tabella di esportazione |
| Artefatto | Osservabile |
|---|
Chiamate GetThreadContext / SetThreadContext | Cambi di contesto ad alta frequenza dei registri di debug su altri processi/thread (ETW del kernel: API Microsoft-Windows-Kernel-Process/Thread). |
DR0–DR3 diversi da zero | Qualsiasi thread il cui CONTEXT_DEBUG_REGISTERS contenga un indirizzo in modalità utente al di fuori dei flussi di lavoro noti dei debugger. |
Bit di abilitazione locale DR7 (L0–L3) con R/W = 00 | Breakpoint di sola esecuzione su thread non gestiti da debugger — una forte anomalia. |
Volume EXCEPTION_SINGLE_STEP | Alti tassi di fault #DB (0x80000004) originati dalla VEH di un processo. |
| Registrazione VEH first-chance | VEH aggiunta di recente (AddVectoredExceptionHandler) poco prima della tempesta di #DB. |
TH32CS_SNAPTHREAD + SuspendThread/ResumeThread | Enumerazione ripetuta dei thread + pattern di sospensione (usati dal monitor da 500 ms). |
Caricamento di wldp.dll/amsi.dll tramite LoadLibraryW quando non caricate in precedenza | Caricamenti anomali di moduli nel processo di destinazione. |
EtwEventWrite mai raggiunto | Assenza di eventi ETW attesi (log operativi di PowerShell silenziosi durante l'esecuzione degli script). |