
Движок перехвата в пользовательском режиме и инструментирования телеметрии на основе аппаратных точек останова (DR0-DR7) без модификации кода (AMSI, WLDP и ETW PoC).
Концепт-доказательство (POC) для исследований в области безопасности, демонстрирующее перехват функций на основе аппаратных точек останова (регистров отладки CPU) как альтернативу традиционному пропатчиванию кода в памяти.
Цель и область применения
Данный репозиторий опубликован строго для оборонительных исследований в области безопасности, обучения red-team/purple-team, разработки механизмов обнаружения и академического изучения внутренностей Windows. Он демонстрирует как злоумышленник может злоупотребить регистрами отладки процессора, чтобы нейтрализовать пользовательскую телеметрию безопасности, — и, что не менее важно, что защитникам следует отслеживать для обнаружения таких техник. Автор не несёт ответственности за любое неправомерное использование этого кода. Использование данной техники против систем без явного разрешения незаконно и нарушает соответствующие законы о компьютерном мошенничестве и злоупотреблениях в большинстве юрисдикций. Не разворачивайте это в среде, которой вы не владеете или на тестирование которой у вас нет явного письменного разрешения.
mora_hwbp.c реализует DLL, которая после загрузки/внедрения в целевой процесс (например, хост PowerShell) перехватывает четыре пользовательские функции исключительно через аппаратные точки останова CPU, хранящиеся в архитектурных регистрах отладки (DR0–DR7) каждого потока процесса:
Векторный обработчик исключений (VEH) уровня процесса получает исключения EXCEPTION_SINGLE_STEP (0x80000004), вызванные регистрами отладки, имитирует успешный путь возврата исходной функции, переписывая контекст исключения, и возобновляет выполнение — всё без изменения ни одного байта исполняемой памяти.
Это делает технику особенно интересной как с точки зрения атаки, так и с точки зрения защиты:
.text (классический inline-хукинг, патчинг EAT/IAT или стаббинг Etwp*).GetThreadContext/SetThreadContext), которые можно использовать для обнаружения.Традиционные подходы к перехвату в пользовательском режиме — inline-детуры (перезапись 5–14 байт), перехват таблицы импорта (IAT) и перехват таблицы экспорта (EAT) — имеют общую слабость: они модифицируют память, которую могут наблюдать сканеры целостности и ETW.
Современные AV/EDR реализуют:
pageguard/guard-page, переходы VirtualProtect к PAGE_EXECUTE_READWRITE и несовпадения хэшей секций.Аппаратные точки останова обходят всё это:
.text нечего сканировать.SetThreadContext, что не вызывает классические сигналы "память изменена", используемые сканерами целостности.Этот POC исследует эффективность и обнаруживаемость данной техники против AMSI (Antimalware Scan Interface), WLDP (Windows Lockdown Policy) и ETW (Event Tracing for Windows) — трёх наиболее широко используемых примитивов безопасности в пользовательском режиме в современном стеке безопасности Windows.
AMSI — это точка интеграции платформы Windows, которая позволяет приложениям (PowerShell, Office, VBScript, .NET-хостам и т.д.) запрашивать сканирование содержимого у зарегистрированных антивирусных провайдеров. Представляют основной интерес две точки входа:
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)Принудительно устанавливая возвращаемое значение AMSI_RESULT в AMSI_RESULT_CLEAN (0), механизм сценариев считает, что содержимое было проверено и признано безвредным, поэтому выполнение продолжается без перерыва.
WLDP реализует оценку политик для Windows Defender Application Control (WDAC / Device Guard). WldpIsClassInApprovedList отвечает на вопрос, разрешён ли данный COM-класс (идентифицируемый GUID) в рамках текущей политики. AMSI внутренне обращается к WLDP, чтобы решить, считаются ли определённые классы сценариев/содержимого "доверенными" (находящимися в списке одобренных). Если функция сообщает, что класс одобрен, AMSI может пропустить дополнительную проверку для этого типа содержимого.
DLL устанавливает выходной параметр isApproved (RDX) в TRUE и возвращает S_OK, делая проверяемый класс "доверенным".
Функция EtwEventWrite в ntdll.dll — это основной приёмник пользовательского режима практически для всей эмиссии событий ETW в системе. Её подавление имеет широкие побочные эффекты, важные для мониторинга безопасности:
Microsoft-Windows-DotNETRuntime)DLL просто возвращает ERROR_SUCCESS (0), не выполняя реальную функцию.
┌──────────────────────────────────────────────────────────────────────────┐ │ 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) └──────┘ │ ││ │ └──────────────────────────┘ └──────────────────────────────────┘│ └──────────────────────────────────────────────────────────────────────────┘
**Общий порядок работы:**
1. DLL загружается в целевой процесс (любым методом инъекции — см. [Использование](#injection--usage-example)).
2. При `DLL_PROCESS_ATTACH` (или через экспортируемую функцию `InstallHook`) целевые экспортируемые функции разрешаются с помощью `GetProcAddress` (при необходимости принудительно загружая модули через `LoadLibraryW`).
3. **Векторный обработчик исключений** регистрируется как **первый** обработчик в процессе (`AddVectoredExceptionHandler(1, ...)`).
4. **Текущий поток** перехватывается немедленно, затем **все существующие потоки** в процессе перечисляются через `CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)` и перехватываются.
5. **Поток-монитор** просыпается каждые 500 мс и повторно применяет точки останова ко всем потокам — включая **новые созданные потоки** — гарантируя сохранение перехвата, даже если поток появился после установки хуков или если точки останова были сброшены извне.
6. Когда любая перехваченная функция вызывается в любом потоке, CPU генерирует исключение одиночного шага `#DB`; Windows передаёт его в VEH, который имитирует безвредный возврат и продолжает выполнение.
### Пример диагностического вывода

*Рисунок 1 — Пример диагностического вывода, полученного с помощью Sysinternals DebugView. Каждая строка сообщает разрешённый адрес целевой функции и текущий счётчик срабатываний для соответствующего регистра отладки.*
---
## Технические подробности
### 5.1 Аппаратные точки останова на x64
В x86/x64 каждый процессор предоставляет **четыре аппаратных регистра адресов точек останова** (`DR0`–`DR3`) и управляющий регистр (`DR7`). Любой поток, выполняющийся с ненулевой точкой останова в `DR0`–`DR3`, вызовет исключение, когда счётчик команд достигнет этого адреса (или когда доступ к данным соответствует заданным условиям). Регистр состояния `DR6` фиксирует, какая точка останова сработала.
Аппаратные точки останова **зависят от контекста**: они хранятся в структуре `CONTEXT` потока и действуют только для того потока, на котором установлены. Именно поэтому надёжная реализация должна устанавливать точки останова на **каждом потоке** процесса (и постоянно повторно применять их для новых потоков).
### 5.2 Распределение регистров отладки (DR0–DR7)
`DR7` представляет собой битовое поле, управляющее включением и поведением точек останова:
| Биты | Поле | Назначение |
|--------|--------|------------------------------------------------------|
| `0` | `L0` | Локальное включение для точки останова 0 (DR0) |
| `2` | `L1` | Локальное включение для точки останова 1 (DR1) |
| `4` | `L2` | Локальное включение для точки останова 2 (DR2) |
| `6` | `L3` | Локальное включение для точки останова 3 (DR3) |
| `8` | `LE` | Устаревшее локальное включение (сохранено для совместимости) |
| `9` | `GE` | Устаревшее глобальное включение (сохранено для совместимости) |
| `16–17`| `R/W0` | Тип доступа для BP0 (`00` = выполнение инструкции) |
| `18–19`| `Len0` | Длина для BP0 (`00` = 1 байт) |
| `20–21`| `R/W1` | Тип доступа для BP1 (`00` = выполнение инструкции) |
| `22–23`| `Len1` | Длина для BP1 (`00` = 1 байт) |
| `24–25`| `R/W2` | Тип доступа для BP2 (`00` = выполнение инструкции) |
| `26–27`| `Len2` | Длина для BP2 (`00` = 1 байт) |
| `28–29`| `R/W3` | Тип доступа для BP3 (`00` = выполнение инструкции) |
| `30–31`| `Len3` | Длина для BP3 (`00` = 1 байт) |
Все четыре точки останова настроены на **выполнение (выборку инструкции) одного байта**, что является подходящим условием для хуков на входе в функцию.
### 5.3 Векторный обработчик исключений (VEH)
Когда срабатывает точка останова, процессор вызывает исключение `#DB`. В x64 Windows процедура диспетчеризации в `ntdll` направляет его через общепроцессную **цепочку VEH** перед цепочкой структурированных обработчиков исключений (SEH) потока. Обработчик в этом проекте:
1. **Фильтрует** — обрабатывает только `EXCEPTION_SINGLE_STEP` (`0x80000004`); всё остальное передаётся дальше через `EXCEPTION_CONTINUE_SEARCH`.
2. **Сопоставляет** — сравнивает `ExceptionAddress` с четырьмя известными адресами функций.
3. **Переписывает контекст**:
- `RIP = *(RSP)` → «возврат» к исходному вызывающему коду путём извлечения обратного адреса из стека.
- `RSP += 8` → имитация `ret` (одноинструкционная размотка стека в x64).
- `RAX = 0` → подмена `S_OK` / `ERROR_SUCCESS` (код успешного возврата).
- `DR6 &= ~0xF` → сброс битов состояния точки останова, чтобы инструкцию можно было повторно выполнить позже без ложного состояния.
4. **Изменяет выходные параметры** (см. [5.4](#54-per-component-interception-logic)).
5. **Возвращает `EXCEPTION_CONTINUE_EXECUTION`**, что указывает Windows перезапустить поток с изменённым контекстом — то есть выполнение возобновляется с *вызывающего кода*, а реальная целевая функция **никогда не выполняется**.
Каждая точка перехвата дополнительно обёрнута в защиту SEH `__try/__except`, чтобы некорректное или неожиданное расположение стека не могло привести к краху процесса — это соображение надёжности для враждебных/усиленно защищённых целей.
### 5.4 Логика перехвата по компонентам
**DR0 — `AmsiScanBuffer`** (x64, первые 6 аргументов в `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) в 6-й параметр (pResult, по адресу [RSP+0x30]).S_OK (0) в RAX.DR1 — AmsiScanString (x64, первые 5 аргументов в RCX, RDX, R8, R9, [RSP+0x28]):```
HRESULT AmsiScanString(HANDLE, LPCWSTR, LPCWSTR, HANDLE, AMSI_RESULT* pResult);
[RSP+0x28]
- Записывает `AMSI_RESULT_CLEAN (0)` в 5-й параметр (`pResult`, по адресу `[RSP+0x28]`).
- Возвращает `S_OK (0)` в `RAX`.
**DR2 — `WldpIsClassInApprovedList`** (первые 3 аргумента в `RCX, RDX, R8`):```
HRESULT WldpIsClassInApprovedList(const GUID* classId, PBOOL isApproved, DWORD evalCriteria);
RCX RDX R8
TRUE в *isApproved (через RDX).S_OK (0) в RAX.DR3 — EtwEventWrite (первые 4 аргумента в RCX, RDX, R8, R9):```
ULONG EtwEventWrite(HANDLE RegHandle, PCEVENT_DESCRIPTOR EventDescriptor,
ULONG UserDataCount, PEVENT_DATA_DESCRIPTOR UserData);
- Возвращает `ERROR_SUCCESS (0)` в `RAX`, не затрагивая ни один выходной параметр.
- Следствие: поставщики ETW **не** получают **никаких** событий от перехваченного процесса, что подавляет журналирование выполнения скриптов, загрузки модулей, создания процессов и телеметрии AMSI.
### 5.5 Управление потоками и постоянство хуков
Поскольку аппаратные регистры отладки являются пер-потоковыми, движок должен непрерывно поддерживать хуки:
1. **Немедленный перехват** — `DllMain` (или `InstallHook`) перехватывает вызывающий поток с помощью `SetHwbpOnThread(GetCurrentThread())`.
2. **Обход всех потоков** — `HookAllThreads()` перечисляет каждый поток в процессе через снимок `TH32CS_SNAPTHREAD`, приостанавливает каждый посторонний поток (`SuspendThread`), применяет точки останова (`SetHwbpOnThread`), возобновляет его и закрывает дескриптор. Приостановка предотвращает гонку, при которой поток вызывает сбой в середине переключения контекста между `GetThreadContext` и `SetThreadContext`.
3. **Монитор постоянства** — `MonitorThreadProc` зацикливается с `Sleep(500)` и вызывает `HookAllThreads()` каждые 500 мс. Это **повторно устанавливает любые снятые точки останова** (например, внешним вызовом `SetThreadContext`, отладочным инструментом или при завершении/создании потока) и **покрывает потоки, созданные после первоначального перехвата**.
4. **Синхронизация** — `HookAllThreads` выполняется под `CRITICAL_SECTION` (`g_HookLock`), поэтому поток монитора и первоначальная процедура перехвата никогда не перемежают переключения контекста.
5. **Чистое завершение** — `UninstallHook` останавливает монитор, очищает `DR0–DR7` в каждом потоке и удаляет VEH.
> **Ответ на вопрос о «постоянстве хуков» явно:** да — если точки останова сняты с любого потока (другим агентом, отладчиком или EDR), поток монитора **повторно применяет их в течение 500 мс**. Кроме того, любой поток, созданный после загрузки DLL, перехватывается в течение одного цикла монитора. Единственный надёжный способ победить именно этот движок — завершить поток монитора *и* очистить VEH *и* снять регистры в одном и том же окне — или использовать анти-отладку, которая с самого начала запрещает `SetThreadContext`.
---
## Экспортируемый API
| Экспорт | Сигнатура | Поведение |
|------------------|-----------------------------------|-----------------------------------------------------------------------|
| `InstallHook` | `BOOL WINAPI InstallHook(void)` | Разрешает цели, регистрирует VEH, перехватывает все потоки, запускает монитор. |
| `UninstallHook` | `BOOL WINAPI UninstallHook(void)` | Останавливает монитор, очищает точки останова во всех потоках, удаляет VEH. |
| `GetStats` | `void WINAPI GetStats(void)` | Выводит текущий статус хуков (через `OutputDebugStringA`) — адрес, счётчики попаданий. |
Счётчики попаданий (`g_HaveAmsiBuf`, `g_HaveAmsiStr`, `g_HaveWldp`, `g_HaveEtw`) поддерживаются с помощью `InterlockedIncrement` и выводятся в отладочный вывод, что полезно для проверки того, что перехват действительно происходит в лабораторной среде.
Обратите внимание, что `DllMain` сам выполняет полную последовательность перехвата при `DLL_PROCESS_ATTACH`, поэтому экспортируемые функции являются необязательными удобствами для сценариев (вы)грузки во время выполнения.
---
## Инструкции по сборке
**Требования:** Windows 10/11 x64, Visual Studio Build Tools (`icx.exe`), SDK.
Скомпилируйте 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"
Пояснение флагов:
| Flag | Purpose |
|---|
Результатом является mora_hwbp.dll, который может быть загружен в целевой процесс.
DLL должен быть загружен в процесс, использующий AMSI/WLDP/ETW — хост PowerShell является каноническим испытательным стендом. Загрузка может быть выполнена с помощью любого стандартного метода инъекции DLL. Минимальная автономная демонстрация с использованием Reflective/LoadLibrary-инъекции может быть выполнена с помощью небольшого загрузчика на C:```bat
rem Run from an x64 developer prompt (example with a generic loader)
loader.exe mora_hwbp.dll powershell.exe
Или, для ручной проверки в лабораторных условиях, внедрите с помощью предпочитаемого вами инструмента, а затем проверьте из 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"
Только лабораторная проверка. Убедитесь с помощью отладчика или
GetStats/OutputDebugString, что все четыре точки останова сообщают о срабатывании по мере выполнения содержимого скрипта.
Этот POC имеет двойное назначение: те же характеристики, которые делают его эффективным в атаке, являются именно тем, что защитникам следует искать.
GetThreadContext(CONTEXT_DEBUG_REGISTERS) в критически важных процессах и проверяйте любой поток с ненулевыми DR0–DR3 вне утверждённых профилей отладчика.Microsoft-Windows-Kernel-Process + Thread и оповещайте о NtGetContextThread/NtSetThreadContext, направленных на процессы, значимые для безопасности.EtwEventWrite, ETW ядра, повторную проверку потребителем AMSI), а не только на целостность .text.SetThreadContext для других процессов как явный сигнал высокой степени серьёзности.RCX/RDX/R8/R9, затем [RSP+0x20…]). Вариант для x86 потребовал бы реконструкции параметров в стиле [EBP+…].Sleep(500); чрезвычайно быстрое создание потоков в сочетании с агрессивной зачисткой теоретически может уйти вперёд монитора на несколько сотен миллисекунд.OutputDebugStringA — диагностика полагается на канал вывода отладки; в полностью урезанной/безголовой среде следует подключить отладчик или перенаправить вывод для лабораторного наблюдения.Этот проект распространяется под лицензией MIT — подробности см. в файле LICENSE.
Этот проект выпущен только для образовательных и оборонительных исследовательских целей. Если вы поставщик решений безопасности, участник blue team или инженер по обнаружению, мы призываем вас использовать содержимое этого репозитория для улучшения охвата обнаружения обхода на основе аппаратных точек останова. Если вы обнаружили, что эта техника используется во вредоносных целях, сообщите об этом через процесс ответственного раскрытия вашей организации и соответствующие каналы вендора/органов власти.
Используйте на свой страх и риск. Несанкционированное использование этой техники может нарушать действующее законодательство.
| Регистр | Перехватываемая функция | Модуль | Назначение |
|---|
DR0 | AmsiScanBuffer | amsi.dll | Нейтрализация сканирования содержимого AMSI |
DR1 | AmsiScanString | amsi.dll | Нейтрализация сканирования строк AMSI |
DR2 | WldpIsClassInApprovedList | wldp.dll | Принудительное одобрение класса WLDP (Device Guard / WDAC) |
DR3 | EtwEventWrite | ntdll.dll | Подавление трассировки событий ETW |
/O3 | Максимальная оптимизация (код функции, необязательно) |
/MT | Статическая компоновка CRT (без зависимости от DLL времени выполнения) |
/EHsc | Обработка исключений C++/SEH (необходимо для __try) |
/DLL | Создание DLL с таблицей экспорта |
| Артефакт | Наблюдаемый признак |
|---|
вызовы GetThreadContext / SetThreadContext | Высокочастотные переключения контекста отладочных регистров в других процессах/потоках (ETW ядра: Microsoft-Windows-Kernel-Process/Thread APIs). |
Ненулевые DR0–DR3 | Любой поток, чьи CONTEXT_DEBUG_REGISTERS содержат адрес пользовательского режима вне известных сценариев работы отладчика. |
Биты локального разрешения DR7 (L0–L3) при R/W = 00 | Точки останова только на исполнение в потоках, не управляемых отладчиком, — явная аномалия. |
Объём EXCEPTION_SINGLE_STEP | Высокая частота исключений #DB (0x80000004), исходящих от VEH процесса. |
| Регистрация VEH первого шанса | Недавно добавленный VEH (AddVectoredExceptionHandler) незадолго до шторма #DB. |
TH32CS_SNAPTHREAD + SuspendThread/ResumeThread | Повторяющиеся паттерны перечисления потоков и приостановки (используются монитором с интервалом 500 мс). |
Загрузка wldp.dll/amsi.dll через LoadLibraryW, если ранее не загружены | Аномальные загрузки модулей в целевом процессе. |
EtwEventWrite никогда не вызывается | Отсутствие ожидаемых событий ETW (операционные журналы PowerShell молчат, пока выполняются скрипты). |