Может кому интересно как работает SetWindowsHookExW Цепочка вызовов (Call Chain) user32!SetWindowsHookExW(idHook, lpfn, hMod, dwThreadId) → syscall → win32k!stub_UserSetWindowsHookEx → win32k!NtUserSetWindowsHookEx (ApiSet dispatch через W32GetWin32kApiSetTable+0xDB8) → win32kfull!NtUserSetWindowsHookEx (реализация) → Feature_656357688 ? zzzSetWindowsHookEx_New() : zzzSetWindowsHookEx() NtUserSetWindowsHookEx (win32kfull!0x6674600) Аргументы (ARM64 ABI): - x0 = hMod (module handle) - x1 = lpfn (hook procedure) - x2 = dwThreadId - x3 = idHook (w24) - x4 = flags - x5 = additional flags (w21) Ключевые шаги: 1. Валидация idHook — idHook + 1 <= 0xF (типы от -1 до 13) 2. EnterLeaveCritShared — вход в критическую секцию win32k 3. PtiFromThreadId — если dwThreadId != 0, находит THREADINFO; если поток не найден → ERROR_INVALID_PARAMETER (0x57) 4. PEB validation — получает PEB через PsGetCurrentProcess → PsGetProcessPeb, ProbeForRead на 0x7D0 байт, проверяет PEB+0x10 (BaseDllName/Ldr) совпадает с hMod 5. Module string validation — проверяет UNICODE_STRING из таблицы модулей: длина, alignment, bounds 6. Feature gate — Feature_656357688__private_IsEnabledDeviceUsageNoInline: - Включён → zzzSetWindowsHookEx_New (новый путь) - Выключен → zzzSetWindowsHookEx (классический путь) 7. EtwTraceAuditApiSetWindowsHookEx — ETW аудит 8. THREADINFO flag — устанавливает бит 0x1000 в THREADINFO+0xE0 (отметка что поток имеет хуки) Хранение хуков Из анализа функций PhkFirstValid, PhkNextValid, xxxCallHook2: - HOOK структура (tagHOOK) — выделается через handle manager (HMPheFromObject) - Hook chain — связный список; обход через PhkNextValid(phk) - Хранятся в THREADINFO — поле THREADINFO+0x210 указывает на hook chain - Desktop-level — глобальные хуки (WH_KEYBOARD_LL, WH_MOUSE_LL) через W32GetUserSessionState+0xA588 (session hook table) - DLT_HOOK — Domain Lock Type для синхронизации (gDomainHookLock в win32kbase) - Locking — Win32HMThreadLock<tagHOOK> / Win32HMOptionalThreadLockAlways<tagHOOK> для thread-safe доступа Диспетчеризация хуков (xxxCallHook2) Это центральная функция dispatch — вызывается при каждом событии, которое может попасть в хук: 1. Session state — получает через W32GetUserSessionState 2. Hook type check — tagHOOK+0x30 содержит idHook (w23) 3. UIPI (User Interface Privilege Isolation) — вызывает: - UIPrivilegeIsolation::Enforced() — включена ли изоляция - UIPrivilegeIsolation::CheckAccess() — может ли хук быть вызван - _IsProcessDwm() — проверяет DWM (особые привилегии) - IsRestricted() / IsImmersiveBroker() / IsImmersiveAppRestricted() - Если UIPI блокирует → EtwTraceUIPIHookError 4. Hook iteration — проходит цепочку через PhkNextValid(), проверяя каждый хук 5. Module validation — xxxLoadHmodIndex проверяет что DLL хука ещё загружена 6. Callback invocation — xxxHkCallHook() — фактический вызов hook procedure 7. Free if needed — FreeHook() если хук помечен для удаления Вызов hook callback (xxxHkCallHook → KeUserModeCallback) xxxHkCallHook в конечном итоге вызывает KeUserModeCallback для передачи управления в usermode. Для каждого типа хука есть свой wrapper: user32!SetWindowsHookExW(idHook, lpfn, hMod, dwThreadId) → syscall → win32k!stub_UserSetWindowsHookEx → win32k!NtUserSetWindowsHookEx (ApiSet dispatch через W32GetWin32kApiSetTable+0xDB8) → win32kfull!NtUserSetWindowsHookEx (реализация) → Feature_656357688 ? zzzSetWindowsHookEx_New() : zzzSetWindowsHookEx() NtUserSetWindowsHookEx (win32kfull!0x6674600) Аргументы (ARM64 ABI): - x0 = hMod (module handle) - x1 = lpfn (hook procedure) - x2 = dwThreadId - x3 = idHook (w24) - x4 = flags - x5 = additional flags (w21) Ключевые шаги: 1. Валидация idHook — idHook + 1 <= 0xF (типы от -1 до 13) 2. EnterLeaveCritShared — вход в критическую секцию win32k 3. PtiFromThreadId — если dwThreadId != 0, находит THREADINFO; если поток не найден → ERROR_INVALID_PARAMETER (0x57) 4. PEB validation — получает PEB через PsGetCurrentProcess → PsGetProcessPeb, ProbeForRead на 0x7D0 байт, проверяет PEB+0x10 (BaseDllName/Ldr) совпадает с hMod 5. Module string validation — проверяет UNICODE_STRING из таблицы модулей: длина, alignment, bounds 6. Feature gate — Feature_656357688__private_IsEnab ledDeviceUsageNoInline: - Включён → zzzSetWindowsHookEx_New (новый путь) - Выключен → zzzSetWindowsHookEx (классический путь) 7. EtwTraceAuditApiSetWindowsHookEx — ETW аудит 8. THREADINFO flag — устанавливает бит 0x1000 в THREADINFO+0xE0 (отметка что поток имеет хуки) Хранение хуков Из анализа функций PhkFirstValid, PhkNextValid, xxxCallHook2: - HOOK структура (tagHOOK) — выделается через handle manager (HMPheFromObject) - Hook chain — связный список; обход через PhkNextValid(phk) - Хранятся в THREADINFO — поле THREADINFO+0x210 указывает на hook chain - Desktop-level — глобальные хуки (WH_KEYBOARD_LL, WH_MOUSE_LL) через W32GetUserSessionState+0xA588 (session hook table) - DLT_HOOK — Domain Lock Type для синхронизации (gDomainHookLock в win32kbase) - Locking — Win32HMThreadLock<tagHOOK> / Win32HMOptionalThreadLockAlways<tagHOOK> для thread-safe доступа Диспетчеризация хуков (xxxCallHook2) Это центральная функция dispatch — вызывается при каждом событии, которое может попасть в хук: 1. Session state — получает через W32GetUserSessionState 2. Hook type check — tagHOOK+0x30 содержит idHook (w23) 3. UIPI (User Interface Privilege Isolation) — вызывает: - UIPrivilegeIsolation::Enforced() — включена ли изоляция - UIPrivilegeIsolation::CheckAccess() — может ли хук быть вызван - _IsProcessDwm() — проверяет DWM (особые привилегии) - IsRestricted() / IsImmersiveBroker() / IsImmersiveAppRestricted() - Если UIPI блокирует → EtwTraceUIPIHookError 4. Hook iteration — проходит цепочку через PhkNextValid(), проверяя каждый хук 5. Module validation — xxxLoadHmodIndex проверяет что DLL хука ещё загружена 6. Callback invocation — xxxHkCallHook() — фактический вызов hook procedure 7. Free if needed — FreeHook() если хук помечен для удаления Вызов hook callback (xxxHkCallHook → - IsRestricted() / IsImmersiveBroker() / IsImmersiveAppRestricted() - Если UIPI блокирует → EtwTraceUIPIHookError 4. Hook iteration — проходит цепочку через PhkNextValid(), проверяя каждый хук 5. Module validation — xxxLoadHmodIndex проверяет что DLL хука ещё загружена 6. Callback invocation — xxxHkCallHook() — фактический вызов hook procedure 7. Free if needed — FreeHook() если хук помечен для удаления Вызов hook callback (xxxHkCallHook → KeUserModeCallback) xxxHkCallHook в конечном итоге вызывает KeUserModeCallback для передачи управления в usermode. Для каждого типа хука есть свой wrapper: - fnHkINLPKBDLLHOOKSTRUCT — WH_KEYBOARD_LL - fnHkINLPMSLLHOOKSTRUCT — WH_MOUSE_LL - fnHkINLPMOUSEHOOKSTRUCTEX — WH_MOUSE - fnHkINLPDEBUGHOOKSTRUCT — WH_DEBUG - fnHkINLPCHARHOOKSTRUCT — keyboard char hooks Эти wrappers упаковывают данные хука в формат для usermode callback и вызывают NtUserCallNextHookEx для продолжения цепочки. Unhook - NtUserUnhookWindowsHookEx → zzzUnhookWindowsHookEx → UnlinkHook + FreeHook - DelayHookFree / FreeDelayedHooks — отложенное освобождение (если хук вызывается рекурсивно) - DeBoostHook / BoostHook — управление приоритетом потока при вызове хуков Устаревший путь NtUserSetWindowsHookAW → zzzSetWindowsHookAW / zzzSetWindowsHookAW_New — старый API SetWindowsHookA/W (без Ex), почти тот же механизм но без поддержки dwThreadId. ⏺ Вот что изменилось в новом пути (zzzSetWindowsHookEx_New): Ключевые отличия нового vs старого SetWindowsHookEx 1. Тип возвращаемого значения СтарыйНовыйСигнатураtagHOOK* zzzSetWindowsHookEx(...)HHOOK__* zzzSetWindowsHookEx_New(...)ВозвращаетВнутренний объект ядраПользовательский хэндл напрямую В NtUserSetWindowsHookEx: - Старый: ldr x19, [x0] — разыменовывает tagHOOK* чтобы достать HHOOK (handle хранится в первом поле структуры) - Новый: mov x19, x0 — использует возвращаемое значение как есть, без разыменования Это говорит о том, что новый путь инкапсулирует работу с handle manager внутри себя и не выставляет наружу сырой kernel object. 2. Feature gate Включается через Feature_656357688 (Velocity feature flag). На текущем стенде featureState = 0x147 — судя по коду в NtUserSetWindowsHookEx, при cbz w0 (feature disabled) идёт по старому пути. Фича проверяется через IsEnabledDeviceUsageNoInline — значит зависит отEdition/Device configuration. 3. Дополнительные проверки в новом пути Из дизассемблера _New версии видны проверки, которых нет на пути NtUserSetWindowsHookEx→zzzSetWindowsHookEx: - lpfn (hook procedure) == NULL → ERROR_INVALID_PARAMETER (0x593) — новый путь валидирует callback pointer раньше - Hook type flags validation — через таблицу по адресу string+0xA64 проверяет биты разрешённого типа хука (tbnz/tbz проверки битов 1, 5). Старый путь делегировал это в zzzSetWindowsHookEx - Global vs Thread hook logic — раздельные ошибки: 0x593 (нет callback), 0x594 (глобальный хук без hMod), 0x595 (thread hook без прав) - WPP tracing — более гранулярная трассировка с разными event ID (0xB, 0xC, 0xD) 4. SetWindowsHookAW (deprecated API) Тоже раздвоен: - Старый: zzzSetWindowsHookAW возвращает __int64 (__cdecl*)(void) — адрес hook function (deprecated поведение) - Новый: zzzSetWindowsHookAW_New возвращает HHOOK__* — корректный handle Старый SetWindowsHookA/W (без Ex) исторически возвращал адрес hook procedure вместо хэндла — это баг, который сохранялся для обратной совместимости. Новый путь это исправляет. Итог Новое — это рефакторинг/улучшение: 1. Возвращает HHOOK напрямую вместо сырого kernel object 2. Ранняя валидация параметров (больше не нужно ждать до внутреннего вызова) 3. Более точные коды ошибок (разные для каждого случая) 4. Исправлен deprecated SetWindowsHookAW (теперь тоже возвращает handle) 5. Закрыто за Velocity feature flag для контролируемого rollout ⏺ Из всего ресерча вот что реально интересное: 1. Win32K split на 4 модуля — это не просто dll, это архитектурное разделение: - win32k — тонкие стабы, только dispatch через ApiSet таблицу - win32kbase — базовая инфраструктура (handles, locks, UIPI) - win32kfull — вся реальная реализация (hooks, windows, menus) - win32kbase_rs — Rust код (вероятно новые security checks) То есть win32k по сути стал "ntdll для GUI" — пустой прокси. 2. UIPI в dispatch хуков — xxxCallHook2 это не просто "вызови callback". Перед каждым вызовом: - UIPrivilegeIsolation::Enforced() → UIPrivilegeIsolation::CheckAccess() - _IsProcessDwm() — DWM имеет особые привилегии - IsRestricted(), IsImmersiveBroker(), IsImmersiveAppRestricted() То есть integrity level проверяется на каждом хуке в цепочке, а не один раз при установке. Процесс с низким IL не может перехватить хуки процесса с высоким IL, даже если хук global. 3. Velocity feature flags прямо в ядре — Feature_656357688 — это не compile-time switch, это runtime feature flag. featureState = 0x147 — значит ядро может включать/выключать кодовые пути без пересборки. Это A/B тестирование на уровне kernel. 4. Delayed free для хуков — DelayHookFree / FreeDelayedHooks. Проблема: hook callback может триггерить другое событие, которое пройдёт через тот же hook chain. Если удалить хук во время его выполнения — crash. Поэтому хуки помечаются для удаления и реально фришатся позже (FreeDelayedHooks). Это тот паттерн, который в kernel programming называется "deferred deletion with RCU-like semantics". 5. Priority boosting — BoostHook / DeBoostHook. Когда хук вызывается, поток получает boost приоритета через SetPriorityFloor(tagTHREADINFO, tagThreadPriorityFloor). Это значит hook callbacks временно повышают приоритет потока, чтобы не быть вытесненными. Low-level hooks (keyboard/mouse) особенно критичны — там ещё есть gnllHooksTimeout — если LL hook зависнет больше чем на N мс, он пропускается. 6. Per-session hook isolation — всё через W32GetUserSessionState, а не глобальные переменные. Offset 0xA588 в session state — таблица глобальных хуков. Это значит каждый RDP/session имеет свой hook table. 7. Handle Manager для HOOK — tagHOOK создаётся через HM (Handle Manager) — HMPheFromObject, HMAssignmentLock. Не просто ExAllocatePool, а полноценный kernel object с reference counting. Поэтому HHOOK — это настоящий handle, который можно закрыть только через NtUserUnhookWindowsHookEx. 8. ARM64 security — каждый вызов hook callback проходит через KscpCfgCheckUserCallTargetEs — это CET (Control-flow Enforcement Technology) для indirect calls в ядре. Даже kernel-mode code не может просто прыгнуть на произвольный адрес. Самое практичное для понимания — это UIPI. Если пишешь security tool на хуках, нужно понимать что хуки фильтруются по integrity level на этапе dispatch, а не установки. SetWindowsHookEx может "успешно" установить хук, но он просто не будет вызываться для процессов с более высоким IL. стенд: Хост: macOS на Apple Silicon (Darwin 25.5.0) Две VM Parallels Desktop ARM64: VMРольСтатусWindows 11 Pro (Debugger)Отладчик (WinDbg)runningWindows 11 Pro (Target)Цель отладкиrunning ОС: Windows 11 Build 26100 (24H2), ARM64 (AArch64), 4 vCPU, build lab ge_release.240331-1435 Подключение: - Serial port через сокеты Parallels → socat relay на macOS → WinDbg на Debugger VM - WinDbg MCP agent (windbg_agent.dll) загружен в WinDbg, MCP сервер на 10.211.55.5:44444 - Claude Code подключается к MCP серверу по HTTP
CreateRemoteThread — Исследование внутренних механизмов ядра Windows Введение Код (Text): kernel32!CreateRemoteThread → ntdll!NtCreateThreadEx → nt!NtCreateThreadEx → nt!PspCreateThread → nt!PspAllocateThread + nt!PspInsertThread → nt!KeStartThread В данном исследовании рассматривается полный путь создания удалённого потока через CreateRemoteThread от usermode до планировщика ядра. Исследование проведено на Windows 11 Build 26100 (ARM64) с помощью WinDbg kernel debugging. 1. kernel32!CreateRemoteThread Функция CreateRemoteThread — это thin wrapper, который подготавливает параметры и вызывает NtCreateThreadEx: Код (Text): CreateRemoteThread(hProcess, lpThreadAttributes, dwStackSize, lpStartAddress, lpParameter, dwCreationFlags, lpThreadId) → NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProcess, lpStartAddress, lpParameter, FALSE, 0, dwStackSize, NULL) 2. nt!NtCreateThreadEx (0xFFFFF801`B4AE7480) Точка входа syscall. Функция выделяет локальный контекст CREATE_THREAD_CONTEXT (0x1F0 байт), заполняет его и вызывает PspCreateThread. Ключевой дизассемблерный поток: Код (Text): nt!NtCreateThreadEx: PACIBSP ; ← ARM64 PAC security cookie memset(context, 0, 0x1F0) ; Zero CREATE_THREAD_CONTEXT ; Если передан хэндл процесса — построить контекст создания if (hProcess != NULL) → PspBuildCreateProcessContext(hProcess, 1, context, ...) ; Получить ссылку на объект процесса по хэндлу ObpReferenceObjectByHandleWithTag( hProcess, PROCESS_THREAD_INJECTION, ; ← Требуемый доступ: THREAD_INJECTION 'CrP', ; ← Tag: 0x72437350 (object tag) &EPROCESS ) ; Проверка: тот же процесс или межпроцессный вызов? if (EPROCESS+0x168 bit 0 == 0) ; Флаг ProtectedProcess не установлен → cross-process path ; Подготовка расширенного контекста (ARM64 specific) RtlGetExtendedContextLength2(...) RtlInitializeExtendedContext2(...) ; Основной вызов — создаём поток PspCreateThread( &ThreadHandle, AccessMask, StartRoutine, ProcessHandle, context, ... ) PspDeleteCreateProcessContext(context) AUTIBSP ret Ключевые моменты: Tag объекта: 'CrP' (0x72437350) — используется для отслеживания ссылок на процесс при создании потока Требуемый доступ: PROCESS_THREAD_INJECTION — для межпроцессного создания потока Проверка EPROCESS+0x168 bit 0: если установлен — это Protected Process, и путь меняется 3. nt!PspCreateThread (0xFFFFF801`B4ADBD60) Центральная функция создания потока. Получает ссылку на EPROCESS, проверяет готовность процесса, выделяет защиту от rundown, и вызывает PspAllocateThread + PspInsertThread. Код (Text): nt!PspCreateThread: PACIBSP memset(local_ctx, 0, 0x190) ; Получаем EPROCESS по хэндлу ObpReferenceObjectByHandleWithTag(hProcess, ..., 'CrP', &EPROCESS) EPROCESS+0x168 → x21 = Flags (bit 0 = ProtectedProcess) ; Межпроцессная проверка if (ProcessHandle != NULL && EPROCESS != PsGetCurrentProcess) PspIsProcessReadyForRemoteThread(EPROCESS) ; Парсинг контекста создания if (CreateContext != NULL) check EPROCESS+0x6BC flags (bit 0, bit 12) Parse context flags (bits 0-6) → setup local flags Various flag combinations (1, 2, 4, 0x80) ; Захват Rundown Protection ExAcquireRundownProtection(EPROCESS+0x1D8) ObfReferenceObjectWithTag(EPROCESS) ; Выделение и инициализация ETHREAD PspAllocateThread(EPROCESS, PreviousMode, CrossProcessFlags, ...) ; Вставка потока в процесс PspInsertThread(EPROCESS, Thread, CreateContext, ...) ; Освобождение ExReleaseRundownProtection KeLeaveCriticalRegionThread 4. nt!PspIsProcessReadyForRemoteThread (0xFFFFF801`B44F06E8) Проверяет, готов ли целевой процесс принять удалённый поток. Код (Text): nt!PspIsProcessReadyForRemoteThread: ; Проверка EPROCESS+0x6BC ldr w8, [x0, #0x6BC] if (w8 bit 0 || w8 bit 12) → return 1 ; Ready ; Проверка EPROCESS+0x168 ldr w8, [x0, #0x168] if (w8 bit 0) → return 1 ; Protected process — ready return 0 ; Not ready Значения флагов: EPROCESS+0x6BC bit 0: Process is fully initialized EPROCESS+0x6BC bit 12: Process allows cross-process thread creation EPROCESS+0x168 bit 0: ProtectedProcess flag 5. nt!PspAllocateThread (0xFFFFF801`B4AEA760) Самая объёмная функция — выделяет память под ETHREAD, инициализирует все поля, создаёт стек и TEB. 5.1 Инициализация и валидация Код (Text): nt!PspAllocateThread: PACIBSP sub sp, sp, #0x1E0 ; Большой локальный кадр ; Сохранение параметров в контекст x21 = EPROCESS w20 = PreviousMode (Kernel/User) x19 = StartAddress ; Проверка EPROCESS+0x168 bit 0 — флаг ProtectedProcess ldr x8, [x21, #0x168] and x8, x8, #1 strb w8, [local_ctx] ; SameProcessFlag ; Валидация PS_CREATE_INFO if (CreateInfo != NULL) ldr x10, [CreateInfo+8] ; Flags if (Flags bit 12 == 0) x1 = CreateInfo+0x140 ; Attribute list if (Flags bit 14 == 0) ; Загрузка debug register context ; Проверка совместимости процессора ldrb w8, [ProcessorCtx+0x788] ; Processor group ldrh w9, [AttrList+8] cmp w8, w9 bne → STATUS_NOT_SUPPORTED 5.2 Выбор процессора Код (Text): ; Выбор идеального процессора для потока KeSelectInitialIdealProcessorForThread(EPROCESS) uxth w0, w0 ; Processor number str w0, [CreateInfo+0x14] ; Save to context ; Установка флага processor assigned ldr x8, [CreateInfo+8] orr x8, x8, #0x4000 str x8, [CreateInfo+8] ; Получение NUMA-узла KeGetProcessorNodeNumberByIndex(...) add w24, w8, #1 ; Node+1 5.3 Создание объекта ETHREAD Код (Text): ; Вычисление размера ETHREAD с учётом групп процессоров KeQueryMaximumGroupCount() if (groups > 1) ; Дополнительное выравнивание для multi-group w5 = total ETHREAD size ; Создание объекта через Object Manager ObCreateObjectEx( PreviousMode, NULL, ; ObjectType (Thread) ObjectAttributes, PreviousMode, NULL, ETHREAD_SIZE, 0, 0, ÐREAD ) ; Обнуление ETHREAD _memset_spec_unaligned_zva(ETHREAD, 0, ETHREAD_SIZE) 5.4 Инициализация полей ETHREAD Код (Text): ; Энергопотребление PoEnergyEstimationEnabled() if (enabled) ETHREAD+0x778 = EnergyEstimationBuffer ; Подсчёт групп и размеров KeQueryMaximumGroupCount() ; Расчёт Offsets: GroupAffinity, IdealProcessor, ... ; Инициализация списков ETHREAD ETHREAD+0x490 → first list head ETHREAD+0x498 → second list head ETHREAD+0x248 → group affinity array ETHREAD+0x260 → ideal processors array ; Копирование контекста процесса ETHREAD+0x4E8 = EPROCESS+0x1C0 ; Process reference ETHREAD+0x4C0 = StartAddress ETHREAD+0x540 = StartAddress ; Инициализация синхронизации KeInitializeSemaphore(ETHREAD+0x4F8, 0, 1) ; LpcReplySemaphore ; Списки: ETHREAD+0x4A8, 0x5D0, 0x5E0, 0x638, 0x520 ; Timer: ETHREAD+0xF8 ETHREAD+0x678 = -3 ; Initial TID sentinel ; Время создания KeQuerySystemTimePrecise(ETHREAD+0x4A0) ; Присвоение Thread ID PsAssignThreadId(ETHREAD) 5.5 Создание пользовательского стека Код (Text): ; Для пользовательского потока (PreviousMode == User) if (EPROCESS+0x300 != NULL) ; WoW64 process PspSetupUserStack(EPROCESS, NULL, ..., local_ctx+0x108) → RtlpWow64CreateUserStack(...) else PspSetupUserStack(EPROCESS, StartCtx, ..., local_ctx+0x108) → RtlCreateUserStack(...) ; PspSetupUserStack: ; KiStackAttachProcess() ; Привязка к адресному пространству ; RtlCreateUserStack(...) ; Выделение стека в usermode ; ExGenRandom() ; Рандомизация стека (ASLR) ; KiUnstackDetachProcess() ; Отключение от address space 5.6 Создание TEB Код (Text): ; Создание Thread Environment Block MmCreateTeb( EPROCESS, NULL, ; Start context (NULL for remote) ETHREAD+0x4E8, ; Process reference local_ctx+0x10 ; Thread context ) ; TEB размещается в пользовательском адресном пространстве ; Инициализация WoW64 (если процесс WoW64) PspWow64InitThread(EPROCESS, ...) ; ARM64 EC (Emulation Compatible) инициализация PsGetProcessMachine() ; Проверка архитектуры if (machine != IMAGE_FILE_MACHINE_ARM64 && !WoW64) PspArm64EcInitThread(EPROCESS, ETHREAD, ...) 5.7 Подготовка контекста запуска и вызов KeInitThread Код (Text): ; Формирование контекста для KeInitThread ; Normal user thread path: local_ctx+0xC8 = {0, PspUserThreadStartup} ; KernelRoutine, RundownRoutine local_ctx+0xD8 = {StartAddress, TEB} ; StartAddress, Teb local_ctx+0xE8 = {CreateInfo, AffinityCtx} local_ctx+0xF8 = EPROCESS local_ctx+0x100 = {NodeNumber, 0} ; System thread path (x25 == NULL): local_ctx+0xC8 = {0, PspSystemThreadStartup} local_ctx+0xD8 = {StartRoutine, NULL} ; Вызов KeInitThread — финальная инициализация KTHREAD KeInitThread(ETHREAD, local_ctx+0xC8) 6. nt!KeInitThread (0xFFFFF801`B4CA7CA8) Инициализирует KTHREAD (встроенный в ETHREAD), устанавливает APC, создаёт стек ядра. Код (Text): nt!KeInitThread: ETHREAD = x19 Context = x20 ; Инициализация списков ожидания ETHREAD+0x8 → WaitListEntry (self-linked) ETHREAD+0x328 → TimerWaitBlock (self-linked) ; Инициализация mutex列表 for (i = 0; i < 4; i++) ETHREAD+0x150+i*0x30 → Mutex links ; Настройка планирования KPROCESS = Context+0x30 ; Process KPROCESS pointer ETHREAD+0x70 = Thread flags (xor with KPROCESS scheduling) ETHREAD+0x54 = Quantum ; Wait list и APC list ETHREAD+0x90 → WaitList (self-linked) ETHREAD+0xA0 → QueueList (self-linked) ETHREAD+0xB0 = KPROCESS ETHREAD+0x240 = KPROCESS ; Process reference ; Thread state flags ETHREAD+0x6C = flags (possibly OR 0x4000 for wr-fasting) ; Инициализация APC (КРИТИЧЕСКИЙ ШАГ) KeInitializeApc( ETHREAD+0x2A8, ; ← APC structure at offset 0x2A8 ETHREAD, ; Thread object 0, ; No rundown routine KiSchedulerAccountingRoutine, ; KernelRoutine NULL, ; RundownRoutine PspUserThreadStartup, ; ← NormalRoutine (USER APC!) 0, ; ApcModeIndex NULL ; NormalContext ) ; Инициализация события завершения KeInitializeEvent(ETHREAD+0x300, NotificationEvent, FALSE) ; Инициализация таймера KeInitializeTimer(ETHREAD+0xF8) ; Подключение к timer списку ETHREAD+0x1C8 ; Хеширование timer ID ; Создание стека ядра MmCreateKernelStack(stackParams) ETHREAD+0x38 = KernelStack ETHREAD+0x28 = {StackBase, StackLimit} ; Инициализация Anti-Boost KeAbInitializeThreadState(ETHREAD) ETHREAD+0x390 = 1 ETHREAD+0x410 = 1 ; Thread state = Initialized (6) stlrb w8=6, [ETHREAD+0x0] ; KTHREAD.State = Initialized ; Получение идеального NUMA-узла KiGetIdealNodeProcessByGroup(KPROCESS) ret STATUS_SUCCESS Значение APC при offset 0x2A8: APC инициализируется с NormalRoutine = PspUserThreadStartup. Когда поток впервые начнёт выполнение, этот APC будет доставлен, и PspUserThreadStartup выполнится в контексте нового потока. 7. nt!PspInsertThread (0xFFFFF801`B4AEEDE0) Вставляет созданный ETHREAD в список потоков процесса и запускает его через KeStartThread. 7.1 Захват блокировок Код (Text): nt!PspInsertThread: ; Acquire process Push Lock (exclusive) ExAcquirePushLockExclusive(EPROCESS+0x1B8) ; Acquire parent process resource (shared) если есть parentCreateInfo if (ParentCreateInfo != NULL) ExAcquireResourceSharedLite(EPROCESS+0x290+0x38, TRUE) 7.2 Проверки состояния процесса Код (Text): ; Проверка EPROCESS+0x1E4 флагов ldr w8, [EPROCESS+0x1E4] if (w8 bit 0x1A == 0) ; Process not exiting if (w8 bit 3 == 0) ; Process not in delete if (w8 bit 0x1E == 0) ; Not process snapshot → proceed to start thread ; Проверка ProtectedProcess (EPROCESS+0x168 bit 0) ldr x8, [EPROCESS+0x168] if (w8 bit 0) → cross-process injection blocked ; Если проверки не пройдены → STATUS_PROCESS_IS_TERMINATING 7.3 Запуск потока Код (Text): ; Вызов KeStartThread — поток начинает выполнение KeStartThread(ETHREAD, SchedulingContext, ...) ; Обновление счётчика потоков процесса EPROCESS+0x370 += 1 ; ThreadCount if (ThreadCount > EPROCESS+0x698) ; MaxThreads → track high water mark ; Освобождение блокировок ExReleaseResourceLite(...) ReleasePushLock(EPROCESS+0x1B8) ReleaseRundownProtection(EPROCESS+0x1D8) 8. nt!KeStartThread (0xFFFFF801`B4466BE8) Финальная стадия — поток добавляется в планировщик. Выполняется на DPC уровне. Код (Text): nt!KeStartThread: ; Получить KPROCESS из ETHREAD x20 = ETHREAD+0xB0 ; KPROCESS pointer ; Raise IRQL → DPC level KfRaiseIrql() w26 = old IRQL ; Acquire process spinlock ExAcquireSpinLockExclusiveAtDpcLevel(KPROCESS+0x48) ; Настройка идеального процессора KiAdjustProcessIdealProcessorSetsForThreadCreation(KPROCESS) ; Копирование scheduling parameters из KPROCESS ldr w8, [KPROCESS+0x90] ; Processor affinity ldr w9, [ETHREAD+0x70] ; Thread flags lsr w8, w8, #1 ; Divide affinity eor w8, w9, w8, lsl #3 and w8, w8, #8 eor w8, w8, w9 str w8, [ETHREAD+0x70] ; Update thread flags ; Копирование processor group ldrsb w8, [KPROCESS+0x98] ; Ideal processor strb w8, [ETHREAD+0x253] ; Thread ideal processor strb w8, [ETHREAD+0xBB] ; Current ideal processor mov w8, #0x20 strb w8, [ETHREAD+0x33B] ; System thread ideal ; Проверка affinity if (AffinityOverride != NULL) KeIsEmptyAffinityEx(override) if (empty) → use process affinity KeIsSubsetAffinityEx(override, process_affinity) if (!subset) KiExtendProcessAffinity(KPROCESS, override) ; Определение целевого процессора if (KPROCESS == current_process) ldrh w24, [ETHREAD+0x268] ; Ideal group else ldrh w24, [KPROCESS+0x190] ; Process group ; Распределение по группам for each processor in group: copy affinity bits to local array ; Добавление в ready queue if (KPROCESS has active threads) KiUpdateSharedReadyQueueAffinityThread(...) ; Копирование scheduling group ETHREAD+0x60 = KPROCESS+0x138 ; Scheduling group ; Обновление счётчика активных потоков ldaddal w8=8, [KPROCESS+0x110] ; ActiveThreadCount += 8 ; Освобождение spinlock ExReleaseSpinLockExclusiveFromDpcLevel(KPROCESS+0x48) KfLowerIrql(oldIrql) ; ETW трассировка EtwTraceThreadAffinity(ETHREAD, group, ...) EtwTraceIdealProcessor(ETHREAD, ...) ; Снятие блокировки ldaddal w8=8, [KPROCESS+0x110] ; Thread started ret 9. nt!PspUserThreadStartup (0xFFFFF801`B4AF12C0) Kernel APC routine — выполняется когда поток впервые получает процессорное время. Это точка входа нового потока в контексте ядра. Код (Text): nt!PspUserThreadStartup: ; Сброс IRQL KfLowerIrql(0) ; Получение текущего ETHREAD x19 = ETHREAD (current thread) ; Отключение обмена primary token (security) PspDisablePrimaryTokenExchange(ETHREAD) ; Проверка: процесс завершается? ldr w8, [ETHREAD+0x580] ; Thread flags if (w8 bit 1) ; Thread is terminating → PspTerminateThreadByPointer(ETHREAD, STATUS_THREAD_IS_TERMINATING, TRUE) ; Проверка EPROCESS+0x6BC (process ready) ldr x8, [ETHREAD+0xB0] ; KPROCESS ldr w8, [KPROCESS+0x6BC] if (w8 bit 0) ; Process fully initialized → DbgkCreateMinimalThread(ETHREAD) ; Debug support msr TPIDR_EL0, x8 ; Set TLS register ; Обновление TEB scheduling properties KeUpdateTebSchedulingPropertiesCurrentThread(ETHREAD) ; Уведомление о создании потока (callbacks, ETW) PspNotifyThreadCreation(ETHREAD) ; Проверка ready flag ldr w8, [ETHREAD+0x580] if (w8 bit 0) ; Need to wait → KeWaitForSingleObject(ETHREAD, ...) ; Wait for process initialization ; Проверка EPROCESS+0x6BC ldr w8, [EPROCESS+0x6BC] if (w8 bit 0 == 0) → PspInitializeThunkContext() ; Настройка usermode контекста ; Возврат — поток переходит в usermode AUTIBSP ret Что происходит в usermode: Когда PspUserThreadStartup завершается, поток переходит в usermode и начинает выполнение с: ntdll!LdrInitializeThunk — точка входа в usermode LdrpInitializeProcess (для первого потока) или LdrpInitializeThread (для последующих) Инициализация CRT, TLS, загрузка DLL Вызов ThreadStartRoutine (переданный через CreateRemoteThread) 10. Полная диаграмма потока вызовов Код (Text): kernel32!CreateRemoteThread │ ▼ ntdll!NtCreateThreadEx (syscall) │ ▼ nt!NtCreateThreadEx (0xFFFFF801`B4AE7480) ├─ PspBuildCreateProcessContext() ├─ ObpReferenceObjectByHandleWithTag('CrP') ├─ RtlGetExtendedContextLength2() ├─ RtlInitializeExtendedContext2() │ ▼ nt!PspCreateThread (0xFFFFF801`B4ADBD60) ├─ ObpReferenceObjectByHandleWithTag('CrP') → EPROCESS ├─ PspIsProcessReadyForRemoteThread() │ ├─ EPROCESS+0x6BC bit 0 || bit 12? │ └─ EPROCESS+0x168 bit 0? ├─ ExAcquireRundownProtection(EPROCESS+0x1D8) │ ├─► nt!PspAllocateThread (0xFFFFF801`B4AEA760) │ ├─ KeSelectInitialIdealProcessorForThread() │ ├─ KeGetProcessorNodeNumberByIndex() │ ├─ ObCreateObjectEx() → ETHREAD │ ├─ KeInitializeSemaphore(ETHREAD+0x4F8) │ ├─ PsAssignThreadId() │ ├─ PspSetupUserStack() │ │ ├─ KiStackAttachProcess() │ │ ├─ RtlCreateUserStack() │ │ ├─ ExGenRandom() (ASLR) │ │ └─ KiUnstackDetachProcess() │ ├─ MmCreateTeb() │ ├─ PspWow64InitThread() / PspArm64EcInitThread() │ └─ KeInitThread() │ ├─ KeInitializeApc(ETHREAD+0x2A8, NormalRoutine=PspUserThreadStartup) │ ├─ KeInitializeEvent(ETHREAD+0x300) │ ├─ KeInitializeTimer(ETHREAD+0xF8) │ └─ MmCreateKernelStack() │ └─► nt!PspInsertThread (0xFFFFF801`B4AEEDE0) ├─ ExAcquirePushLockExclusive(EPROCESS+0x1B8) ├─ ExAcquireResourceSharedLite(EPROCESS+0x290+0x38) ├─ Check EPROCESS+0x1E4 flags ├─ Check EPROCESS+0x168 (ProtectedProcess) │ └─► nt!KeStartThread (0xFFFFF801`B4466BE8) ├─ KfRaiseIrql(DPC) ├─ ExAcquireSpinLockExclusiveAtDpcLevel(KPROCESS+0x48) ├─ KiAdjustProcessIdealProcessorSetsForThreadCreation() ├─ Setup processor affinity from KPROCESS ├─ KiUpdateSharedReadyQueueAffinityThread() ├─ ExReleaseSpinLockExclusiveFromDpcLevel() ├─ KfLowerIrql() └─ EtwTraceThreadAffinity() └─ Release locks └─ Release rundown protection ═════════════════════════════════════════ Поток получает процессор ═════════════════════════════════════════ │ ▼ nt!PspUserThreadStartup (Kernel APC) ├─ KfLowerIrql(0) ├─ PspDisablePrimaryTokenExchange() ├─ DbgkCreateMinimalThread() ├─ KeUpdateTebSchedulingPropertiesCurrentThread() ├─ PspNotifyThreadCreation() └─ PspInitializeThunkContext() │ ▼ [Transition to User Mode] │ ntdll!LdrInitializeThunk ├─ LdrpInitializeThread() ├─ CRT initialization ├─ DLL notifications └─ UserThreadStart(lpStartAddress, lpParameter) 11. Структуры и смещения EPROCESS offsets: Код (Text): +0x0B0 KPROCESS pointer (embedded) +0x168 Flags (bit 0 = ProtectedProcess / SameProcess) +0x1B8 Push Lock (thread list, exclusive for insert) +0x1C0 Process reference +0x1D8 Rundown Protection +0x1E0 Additional flags +0x1E4 Process state flags (bit 3 = deleting, bit 0x1A = exiting, bit 0x1E = snapshot) +0x290 Active process links +0x300 WoW64 process structure +0x370 ThreadCount +0x580 Thread creation flags +0x698 MaxThreads (high water mark) +0x6BC Process ready flags (bit 0 = initialized, bit 12 = allows remote threads) +0x700 Process extension +0x7AC Process machine type +0x810 Process flags (bit 6 = stack randomization) ETHREAD offsets: Код (Text): +0x000 KTHREAD.State (6 = Initialized, 1 = Ready, 2 = Running) +0x008 WaitListEntry +0x028 KernelStack {Base, Limit} +0x038 KernelStack pointer +0x054 Quantum +0x060 Scheduling group +0x06C Thread flags (bit 0xA = wr-fasting, bit 0x14 = created suspended) +0x070 Cross-process flags +0x088 Thread flags from process +0x090 WaitList +0x0A0 QueueList +0x0B0 KPROCESS pointer +0x0BB Current ideal processor +0x0E8 Win32Thread / TEB pointer +0x0F8 Timer +0x150 Mutex links (4 entries) +0x17E IdealNode count +0x1C8 Timer list entry +0x240 KPROCESS (second reference) +0x248 GroupAffinity array pointer +0x253 Ideal processor +0x260 IdealProcessor array pointer +0x268 Ideal group +0x290 ActiveProcessLinks +0x2A8 KAPC structure (initial user APC) +0x300 Event (thread termination) +0x328 TimerWaitBlock +0x33B System ideal processor +0x390 Anti-Boost flag 1 +0x410 Anti-Boost flag 2 +0x424 Unknown field (0x20) +0x490 List head 1 +0x498 List head 2 +0x4A0 CreateTime +0x4A8 LpcReplyChain +0x4C0 StartAddress / Win32StartAddress +0x4D0 List head +0x4D8 List head +0x4E8 Process reference (from EPROCESS+0x1C0) +0x4F8 LpcReplySemaphore +0x520 IrpList +0x540 StartAddress (copy) +0x568 Unknown pointer +0x570 Push lock (thread level) +0x578 Unknown (0xF) +0x580 Thread flags (bit 1 = terminating, bit 0 = wait for init) +0x584 Additional flags (bit 0x200 = CFG) +0x5D0 Callback list +0x5E0 Callback list +0x5F0 Unknown +0x5F8 Unknown +0x608 Stack pointer from CreateInfo +0x638 List head +0x658 WoW64 context pointer +0x660 WoW64 context pointer 2 +0x668 Energy estimation buffer pointer +0x670 GroupAffinity extended +0x678 Thread ID sentinel (-3) +0x698 High water mark +0x6A8 Unknown list +0x6B0 Unknown list +0x700 Thread extension +0x707 Priority (0xFF) +0x758 Unknown pointer +0x770 Fast reference (process) KPROCESS offsets: Код (Text): +0x000 Flags +0x048 Process lock (spinlock) +0x04C Affinity +0x058 GroupAffinity array +0x090 Scheduling affinity +0x098 Ideal processor +0x0B0 KPROCESS pointer (self) +0x110 ActiveThreadCount +0x118 ThreadListHead +0x138 Scheduling group +0x158 Some flags +0x188 Additional scheduling +0x190 Default processor group +0x2D8 Affinity context 12. Механизмы безопасности 12.1 Защита от инъекции в Protected Processes Код (Text): NtCreateThreadEx: → ObpReferenceObjectByHandleWithTag(PROCESS_THREAD_INJECTION) ; Требуется право THREAD_INJECTION для целевого процесса PspCreateThread: → PspIsProcessReadyForRemoteThread() ; Проверяет EPROCESS+0x6BC и EPROCESS+0x168 ; Protected processes (bit 0 at +0x168) могут отклонять инъекцию PspInsertThread: → Проверка EPROCESS+0x168 bit 0 (ProtectedProcess) ; Если целевой процесс Protected — инъекция блокируется → Проверка EPROCESS+0x1E4 флагов ; Bit 3: process deleting ; Bit 0x1A: process exiting ; Bit 0x1E: process snapshot 12.2 Rundown Protection Код (Text): PspCreateThread: ExAcquireRundownProtection(EPROCESS+0x1D8) ; Защищает от уничтожения процесса во время создания потока ... ExReleaseRundownProtection(EPROCESS+0x1D8) 12.3 Push Lock для списка потоков Код (Text): PspInsertThread: ExAcquirePushLockExclusive(EPROCESS+0x1B8) ; Гарантирует атомарность добавления потока в список ... ReleasePushLock(EPROCESS+0x1B8) 12.4 PAC (Pointer Authentication Codes) на ARM64 Все функции начинаются с PACIBSP и заканчиваются AUTIBSP — это ARM64 Pointer Authentication, защищающая return address от перезаписи. 13. Практические выводы CreateRemoteThread требует PROCESS_THREAD_INJECTION на целевом процессе Внутри ядра используется tag 'CrP' (0x72437350) для отслеживания объектных ссылок Перед созданием потока ядро проверяет PspIsProcessReadyForRemoteThread — процесс может быть "не готов" Защита Rundown Protection предотвращает разрушение процесса во время создания потока Push Lock на EPROCESS+0x1B8 обеспечивает целостность списка потоков Для пользовательского потока ядро создаёт стек (RtlCreateUserStack), TEB (MmCreateTeb), APC (KeInitializeApc) Начальная доставка APC через PspUserThreadStartup — первый код, выполняющийся в новом потоке Финальный переход в usermode: PspUserThreadStartup → PspInitializeThunkContext → ntdll!LdrInitializeThunk → ThreadStartRoutine KeStartThread работает на DPC level с spinlock на KPROCESS+0x48 для настройки affinity ASLR для стека обеспечивается через ExGenRandom в PspSetupUserStack Защита от инъекции в PPL (Protected Process Light): проверка EPROCESS+0x168 bit 0 и EPROCESS+0x1E4 флагов Исследование выполнено на Windows 11 Build 26100 ARM64 через WinDbg kernel debugging. Все адреса и смещения актуальны для данной сборки.