QueueUserAPC — Исследование внутренних механизмов ядра Windows (ARM64)
Среда отладки: Windows 11 Build 26100, ARM64 (Parallels Desktop VM)
Метод: Live Kernel Debugging через WinDbg → socat serial bridge → MCP
Дата: Июнь 2026
1. Цепочка вызовов (Call Chain)
2. nt!NtQueueApcThread — Тонкая обёрткаКод (Text):
Пользовательский режим: kernel32!QueueUserAPC(pfnAPC, hThread, dwData) ↓ ntdll!NtQueueApcThread(ThreadHandle, ApcRoutine, ApcContext, Arg1, Arg2) ↓ (syscall) Режим ядра: nt!NtQueueApcThread → 0xFFFFF801`B4923410 ↓ (thin wrapper) nt!NtQueueApcThreadEx2 → 0xFFFFF801`B4B00AD0 ├── nt!ObReferenceObjectByHandle → получает ETHREAD по хэндлу ├── nt!ExAllocatePool2 → выделяет KAPC (0x58 байт, тег 'PasP') ├── nt!KeInitializeApc → заполняет структуру KAPC └── nt!KeInsertQueueApc → вставляет APC в очередь потока ├── nt!KiInsertQueueApc → манипуляция со списком (LIST_ENTRY) ├── nt!KiSignalThreadForApc → пробуждение целевого потока └── nt!KiExitDispatcher → диспетчеризация потоков Доставка APC (при возврате из ядра): nt!KiDeliverApc → 0xFFFFF801`B448A030 ├── Kernel APC: вызывает KernelRoutine напрямую └── User APC: вызывает nt!KiInitializeUserApc └── Модифицирует trap frame → переход в ntdll!KiUserApcDispatcher └── Вызывает пользовательский回调 (ApcRoutine)
Анализ: Функция состоит из 7 инструкций. Перемаппит параметры ABI и прыгает в расширенную версию. Никаких собственных проверок — вся логика в NtQueueApcThreadEx2.Код (Text):
nt!NtQueueApcThread: fffff801`b4923410 mov x5, x3 ; x5 = Arg1 fffff801`b4923414 mov x6, x4 ; x6 = Arg2 fffff801`b4923418 mov x4, x2 ; x4 = ApcContext fffff801`b492341c mov x3, x1 ; x3 = ApcRoutine fffff801`b4923420 mov w2, #0 ; Flags = 0 fffff801`b4923424 mov x1, #0 ; UserApcReserveHandle = NULL fffff801`b4923428 b nt!NtQueueApcThreadEx2 ; прямой переход
Маппинг параметров NtQueueApcThread → NtQueueApcThreadEx2:
3. nt!NtQueueApcThreadEx2 — Основная логика
NtQueueApcThread Регистр NtQueueApcThreadEx2 ThreadHandle (x0) x0 ThreadHandle — x1 = 0 UserApcReserveHandle = NULL — w2 = 0 Flags = 0 ApcRoutine (x1) x3 ApcRoutine ApcContext (x2) x4 SystemArgument1 Arg1 (x3) x5 SystemArgument2 Arg2 (x4) x6 SystemArgument3
Адрес: 0xFFFFF801`B4B00AD0 (самая длинная функция в цепочке)
3.1 Валидация флагов
3.2 Получение ETHREAD по хэндлуКод (Text):
fffff801`b4b00b10 tst w24, #0xFFFEFFFE ; проверить лишние биты в Flags fffff801`b4b00b14 bne invalid_parameter ; → STATUS_INVALID_PARAMETER fffff801`b4b00b18 tbnz w24, #0, check_reserve ; бит 0 = UserApcReserveHandle ; (NtQueueApcThread передаёт 0)
DesiredAccess = 0x10 = THREAD_SET_CONTEXT — для APC достаточно этого права доступа к потоку.Код (Text):
fffff801`b4b00b2c ldr x2, [x21] ; CurrentThread для access check fffff801`b4b00b30 mov x5, #0 ; HandleInformation = NULL fffff801`b4b00b34 add x4, sp, #0x10 ; &Object (выходной параметр) fffff801`b4b00b38 mov w3, w20 ; ProcessorMode (User/Kernel) fffff801`b4b00b3c mov w1, #0x10 ; DesiredAccess = THREAD_SET_CONTEXT fffff801`b4b00b40 bl nt!ObReferenceObjectByHandle
3.3 Проверка terminated-флага
Ключевой момент: Проверка ETHREAD+0x6C бит 10 (PS_CROSS_THREAD_FLAGS_TERMINATED). Если целевой поток уже завершён — APC не ставится в очередь, возвращается STATUS_INVALID_HANDLE.Код (Text):
fffff801`b4b00b4c ldr w8, [x22, #0x6C] ; ETHREAD+0x6C (CrossThreadFlags) fffff801`b4b00b50 tbz w8, #0xA, continue ; бит 10 = PS_CROSS_THREAD_FLAGS_TERMINATED ; если поток уже завершён — ошибка fffff801`b4b00b54 ldr w19, =0xC0000008 ; STATUS_INVALID_HANDLE fffff801`b4b00b58 mov x0, x22 ; ETHREAD fffff801`b4b00b5c bl nt!ObDereferenceObject ; освободить ссылку fffff801`b4b00b60 mov w0, w19 ; вернуть ошибку
3.4 Проверка WoW64 / ARM64EC
KPROCESS+0x300 = WoW64Process. Если процесс является WoW64 (x86 на ARM64), выполняется проверка архитектуры целевого потока:Код (Text):
fffff801`b4b00b84 ldr x8, [xpr, #0x988] ; текущий EPROCESS (CurrentThread→Process) fffff801`b4b00b88 ldr x9, [x8, #0xB0] ; EPROCESS+0xB0 → KPROCESS (Pcb) fffff801`b4b00b8c ldr x8, [x9, #0x300] ; KPROCESS+0x300 (WoW64Process) fffff801`b4b00b90 cbnz x8, check_arch ; если WoW64 — дополнительная проверка
3.5 CAS-блокировка потока (UserApcReserve path)Код (Text):
fffff801`b4b00c84 ldrh w9, [x9, #0x7AC] ; KPROCESS+0x7AC (Machine) fffff801`b4b00c88 mov w8, #0x1C4 ; IMAGE_FILE_MACHINE_ARM64EC fffff801`b4b00c8c cmp w9, #0x14C ; IMAGE_FILE_MACHINE_I386 fffff801`b4b00c90 ccmpne w9, w8, #4 ; или ARM64EC? fffff801`b4b00c94 bne continue_path ; если нет — пропустить проверку
CAS-механизм: Атомарная операция casal (Compare-And-Swap, Acquire semantics) гарантирует, что только один поток может одновременно ставить APC через reserve handle.Код (Text):
; При использовании UserApcReserveHandle (NtQueueApcThreadEx): fffff801`b4b00bc0 ldr x0, [sp, #0x10] ; Object pointer fffff801`b4b00bc4 mov w9, #1 ; desired = 1 fffff801`b4b00bc8 mov w8, #0 ; expected = 0 fffff801`b4b00bcc casal w8, w9, [x0] ; Compare-And-Swap (acquire) fffff801`b4b00bd0 cbz w8, acquired ; если было 0 — захватили fffff801`b4b00bd4 bl nt!ObDereferenceObject ; не удалось — освободить fffff801`b4b00bd8 ldr w19, =0xC00000F0 ; STATUS_INVALID_PARAMETER_1
3.6 Выделение KAPC
Pool Tag: 'PasP' (0x50617370) — тег для пользовательских APC. Легко отслеживать через PoolMon.Код (Text):
; Path при UserApcReserveHandle == NULL (QueueUserAPC): fffff801`b4b00be0 ldr w2, =0x50617370 ; Pool tag = 'PasP' fffff801`b4b00be4 mov x1, #0x58 ; Size = 0x58 = sizeof(KAPC) fffff801`b4b00be8 mov x0, #0x41 ; PoolType = NonPagedPoolNx fffff801`b4b00bec bl nt!ExAllocatePool2 ; выделить из пула
Размер: 0x58 (88 байт) = sizeof(KAPC)
Тип пула: NonPagedPoolNx (0x41) — невыграждаемая память без исполнения
3.7 Инициализация KAPC — вызов KeInitializeApc
3.8 Вставка APC в очередь — вызов KeInsertQueueApcКод (Text):
fffff801`b4b00c34 ldr x7, [sp, #0x18] ; SystemArgument2 (ApcContext) fffff801`b4b00c38 mov w6, w23 ; ApcMode = 1 (UserMode) fffff801`b4b00c3c mov x5, x26 ; NormalRoutine = ApcRoutine fffff801`b4b00c40 mov x4, x21 ; RundownRoutine fffff801`b4b00c44 mov w2, #0 ; ApcStateIndex = OriginalApcEnvironment fffff801`b4b00c48 mov x1, x22 ; Thread = ETHREAD fffff801`b4b00c4c mov x0, x20 ; KAPC object fffff801`b4b00c50 bl nt!KeInitializeApc
Если KeInsertQueueApc возвращает FALSE (поток уже уничтожается):Код (Text):
fffff801`b4b00c58 ldr x1, [sp, #0x20] ; SystemArgument1 fffff801`b4b00c5c mov w3, #0 ; Increment = 0 fffff801`b4b00c60 mov x2, x27 ; SystemArgument2 fffff801`b4b00c64 mov x0, x20 ; KAPC fffff801`b4b00c68 bl nt!KeInsertQueueApc
4. nt!KeInitializeApc — Заполнение структуры KAPCКод (Text):
fffff801`b4b00cf0 mov x0, x20 ; KAPC fffff801`b4b00cf4 mov x15, x21 ; RundownRoutine fffff801`b4b00cf8 bl nt!KscpCfgCheckUserCallTargetEs ; PAC проверка fffff801`b4b00cfc blr x15 ; вызвать RundownRoutine для очистки fffff801`b4b00d00 mov w19, #0xC0000001 ; STATUS_UNSUCCESSFUL
Адрес: 0xFFFFF801`B44720F0
Примечание: Если NormalRoutine == NULL, APC помечается как kernel-only (ApcMode = 0), даже если передан ApcMode = 1. Это prevents пользовательские APC без callback.Код (Text):
nt!KeInitializeApc: fffff801`b44720f0 mov w8, #0x12 ; Type = 0x12 (ApcObject) fffff801`b44720f4 strb w8, [x0] ; KAPC.Type = 0x12 fffff801`b44720f8 mov w8, #0x58 ; Size = 88 байт fffff801`b44720fc strb w8, [x0, #2] ; KAPC.Size = 0x58 fffff801`b4472100 cmp w2, #2 ; ApcStateIndex == AttachedApcEnvironment? fffff801`b4472104 beq attached_env ; специальный путь fffff801`b4472108 mov w8, w2 ; fffff801`b447210c strb w8, [x0, #0x50] ; KAPC.ApcStateIndex fffff801`b4472110 cbz x5, no_normal_routine ; NormalRoutine == NULL? fffff801`b4472114 str x1, [x0, #8] ; KAPC.Thread = ETHREAD fffff801`b4472118 csel w8, wzr, w6, eq ; если NormalRoutine == NULL: ApcMode = 0 fffff801`b447211c stp x3, x4, [x0, #0x20] ; KAPC.KernelRoutine, KAPC.RundownRoutine fffff801`b4472120 str x5, [x0, #0x30] ; KAPC.NormalRoutine fffff801`b4472124 strb w8, [x0, #0x51] ; KAPC.ApcMode (1 = UserMode) fffff801`b4472128 csel x8, xzr, x7, eq ; если NormalRoutine == NULL: Context = 0 fffff801`b447212c str x8, [x0, #0x38] ; KAPC.NormalContext fffff801`b4472130 strb wzr, [x0, #0x52] ; KAPC.Inserted = FALSE fffff801`b4472134 strb wzr, [x0, #1] ; KAPC.CallbackDataContext = 0 fffff801`b4472138 ret
5. nt!KeInsertQueueApc — Вставка APC в очередь
Адрес: 0xFFFFF801`B4472150
Это самая сложная функция. Ключевые операции:
5.1 ETW трассировка
5.2 Подъём IRQL и захват спин-блокировки потокаКод (Text):
fffff801`b4472170 adrp x8, ... ; ETW GUID fffff801`b4472174 ldr x10, [x8, #0xE10] ; EtwTiLogInsertQueueUserApc провайдер fffff801`b447218c cbz x10, skip_etw ; если ETW не активен — пропустить fffff801`b4472190 ldr x8, [x10, #0x20] ; fffff801`b4472194 mov x2, #0x3000 ; keyword mask fffff801`b44721a0 bl nt!EtwpLevelKeywordEnabled
Механизм блокировки:Код (Text):
fffff801`b447221c mov w0, #2 ; DISPATCH_LEVEL fffff801`b4472220 bl nt!KfRaiseIrql ; поднять IRQL fffff801`b4472244 add x27, x19, #0x40 ; KTHREAD+0x40 (ThreadLock) fffff801`b4472248 swpa x21, x8, [x27] ; Atomic swap: захват блокировки ; x21 = 1, x8 = предыдущее значение fffff801`b4472250 cbz x8, lock_acquired ; если было 0 — захватили ; Spin-wait loop (блокировка занята): fffff801`b4472258 ldr w8, [x26, #0x28C] ; Prcb spin count fffff801`b4472268 dmb ishst ; Data Memory Barrier fffff801`b447226c yield ; CPU hint (сэкономить энергию) fffff801`b4472270 ldr x8, [x27] ; перечитать блокировку fffff801`b4472274 cbnz x8, spin_wait ; если всё ещё занята — крутиться
KTHREAD+0x40 = ThreadLock (спин-блокировка).
Используется инструкция SWPA (Store Word Pair Atomic) — атомарный обмен с acquire-семантикой.
Ожидание (spin-wait) использует yield + DMB — cooperative spinning.
5.3 Вызов KiInsertQueueApc
Перед вызовом: Устанавливается KAPC.Inserted = 1 и записываются SystemArgument1/2.Код (Text):
fffff801`b4472298 ldr x8, [sp, #0x18] ; SystemArgument1 fffff801`b447229c strb w21, [x20, #0x52] ; KAPC.Inserted = TRUE fffff801`b44722a0 mov x0, x20 ; KAPC fffff801`b44722a4 stp x8, x25, [x20, #0x40] ; KAPC.SystemArgument1/2 fffff801`b44722a8 bl nt!KiInsertQueueApc ; вставить в связный список
5.4 Пробуждение целевого потока (KiSignalThreadForApc)
После вставки APC в список, KeInsertQueueApc проверяет, нужно ли разбудить целевой поток:
Для целевого потока на другом процессоре:Код (Text):
; Проверка: является ли целевой поток текущим? fffff801`b44722b0 ldrsb w8, [x20, #0x50] ; KAPC.ApcStateIndex fffff801`b44722b8 ldrb w9, [x19, #0x26A] ; KTHREAD.ApcStateIndex fffff801`b44722bc cmp w8, w9 ; APC state == Thread state? fffff801`b44722c4 ldr x8, [x23, #8] ; CurrentThread fffff801`b44722c8 cmp x19, x8 ; target == current? fffff801`b44722d0 cbnz w10, signal_thread ; если ApcMode != 0 → сигналить
Если поток ждёт (Waiting state):Код (Text):
fffff801`b44722ec strb w21, [x19, #0xB9] ; KTHREAD+0xB9 = KernelApcPending = 1 fffff801`b44722f0 cbz w24, no_signal ; если IRQL не поднят — пропустить IPI
Для потока на другом процессоре отправляется программное прерывание:Код (Text):
fffff801`b4472450 add x8, x19, #0xB9 ; &KTHREAD.KernelApcPending fffff801`b4472454 stlrb w21, [x8] ; store-release: KernelApcPending = 1 ; Проверка состояния потока: fffff801`b447245c ldrsb w8, [x19, #0x17C] ; KTHREAD+0x17C (State) fffff801`b4472464 cmp w8, #2 ; == Waiting? fffff801`b4472468 beq send_ipi ; отправить IPI fffff801`b447246c cmp w8, #5 ; == DeferredReady? fffff801`b4472484 ldr x8, [x20, #0x30] ; KAPC.NormalRoutine fffff801`b4472488 cbnz x8, check_ready ; если есть NormalRoutine fffff801`b4b00c68 bl nt!KiSignalThread ; разбудить поток
5.5 Освобождение блокировки и диспетчеризацияКод (Text):
fffff801`b4472530 ldr w8, [x19, #0x238] ; KTHREAD+0x238 (WaitIrql) fffff801`b4472548 bl nt!KiSendSoftwareInterrupt ; отправить IPI ; или: fffff801`b4472550 mov w0, #1 ; APC_LEVEL request fffff801`b4472554 bl nt!HalRequestSoftwareInterrupt
6. nt!KiInsertQueueApc — Манипуляция со связным спискомКод (Text):
fffff801`b4472334 stlr xzr, [x8] ; store-release: ThreadLock = 0 fffff801`b4472344 bl nt!KiExitDispatcher ; диспетчеризация fffff801`b447234c mov w0, w20 ; вернуть TRUE/FALSE
Адрес: 0xFFFFF801`B448A6F0
Это низкоуровневая функция, работающая напрямую с LIST_ENTRY в ETHREAD.
6.1 Выбор APC-списка
6.2 Вычисление адреса списка по ApcModeКод (Text):
fffff801`b448a6f0 ldr x10, [x0, #8] ; x10 = KAPC.Thread (KTHREAD) fffff801`b448a6f4 ldrsb w8, [x0, #0x50] ; w8 = KAPC.ApcStateIndex fffff801`b448a6f8 add x9, x10, #0x26A ; &KTHREAD.ApcStateIndex fffff801`b448a6fc cbnz w8, special_state ; если ApcStateIndex != 0 → special ; ApcStateIndex == 0 (OriginalApcEnvironment): fffff801`b448a700 ldrb w8, [x10, #0x26A] ; thread's ApcStateIndex fffff801`b448a704 cbnz w8, attached_thread ; если поток attached → +0x278 ; Нормальный путь: KTHREAD+0x90 (ApcState) fffff801`b448a708 ldrsb w8, [x9] ; ApcStateIndex fffff801`b448a70c add x13, x10, #0x90 ; x13 = KTHREAD.ApcState base ; Если ApcMode != 0 (User APC): fffff801`b448a790 add x13, x10, #0x278 ; x13 = KTHREAD.SavedApcState base fffff801`b448a794 b continue_path
Формула:Код (Text):
; x13 = APC state base (ApcState или SavedApcState) ; w12 = KAPC.ApcMode (0 = Kernel, 1 = User) fffff801`b448a724 add x9, x13, w12, sxtw #4 ; x9 = base + (ApcMode * 16) ; Kernel: base + 0x00 (ApcListHead[0]) ; User: base + 0x10 (ApcListHead[1])Код (Text):
ListHead = KAPC_STATE_BASE + (ApcMode * sizeof(LIST_ENTRY))6.3 Вставка в связный список
ApcMode Offset от ApcState Offset от KTHREAD Тип APC 0 (Kernel) +0x00 +0x090 Kernel APC 1 (User) +0x10 +0x0A0 User APC
Порядок: FIFO (First In, First Out) — новые APC добавляются в конец списка.Код (Text):
; Поиск конца списка (FIFO — вставка в tail): fffff801`b448a744 add x8, x13, w12, sxtw #4 ; x8 = &ApcListHead[ApcMode] fffff801`b448a748 ldr x10, [x8, #8] ; x10 = ListHead.Blink (последний элемент) fffff801`b448a74c cmp x10, x8 ; список пуст? (Blink == Head?) fffff801`b448a750 bne list_not_empty ; Список пуст — первая вставка: fffff801`b448a798 stp x9, x8, [x11] ; KAPC.ApcListEntry.Flink = Head ; KAPC.ApcListEntry.Blink = Head fffff801`b448a79c str x11, [x8] ; Head.Flink = KAPC fffff801`b448a7a0 str x11, [x9, #8] ; Head.Blink = KAPC fffff801`b448a7a4 ret ; Список не пуст — вставка в конец: fffff801`b448a754 ldr x9, [x10] ; x9 = last_entry.Flink (== Head) fffff801`b448a758 add x11, x0, #0x10 ; x11 = &KAPC.ApcListEntry fffff801`b448a75c ldr x8, [x9, #8] ; проверка целостности: Blink fffff801`b448a760 cmp x8, x10 fffff801`b448a764 bne list_corrupt ; BRK #0xF003 (bugcheck) fffff801`b448a768 stp x9, x10, [x11] ; KAPC.Flink = Head, KAPC.Blink = last fffff801`b448a76c str x11, [x9, #8] ; Head.Blink = KAPC fffff801`b448a770 str x11, [x10] ; last.Flink = KAPC fffff801`b448a774 ret
7. nt!KiDeliverApc — Доставка APC
Адрес: 0xFFFFF801`B448A030
Вызывается при возврате из ядра в user-mode (KiSystemServiceExit или KiExceptionExit).
7.1 Сохранение контекста
7.2 Доставка Kernel APCКод (Text):
fffff801`b448a08c ldr x20, [xpr, #0x988] ; KTHREAD текущего потока fffff801`b448a090 ldr x8, [x20, #0x88] ; KTHREAD+0x88 (TrapFrame) fffff801`b448a094 strb wzr, [x20, #0xB9] ; KernelApcPending = 0 fffff801`b448a098 str x2, [x20, #0x88] ; сохранить новый trap frame fffff801`b448a09c str x8, [sp, #0x58] ; сохранить старый trap frame
7.3 Доставка User APCКод (Text):
fffff801`b448a0b4 add x21, x20, #0x90 ; x21 = KTHREAD.ApcState fffff801`b4b00b0 add x26, x20, #0x90 ; KTHREAD.ApcState (kernel list base) fffff801`b448a0d0 ldr x8, [x26] ; ApcListHead[0].Flink (kernel) fffff801`b448a0d4 cmp x8, x26 ; список пуст? fffff801`b448a0d8 beq user_apc_check ; пустой → перейти к user APC ; Поднять IRQL и захватить ThreadLock: fffff801`b448a0e0 bl nt!KfRaiseIrql ; DISPATCH_LEVEL fffff801`b448a0e4 add x22, x20, #0x40 ; ThreadLock fffff801`b448a0ec swpa x24, x8, [x22] ; захват ThreadLock ; Извлечь KAPC из списка: fffff801`b448a118 ldr x19, [x20, #0x90] ; первый KAPC (kernel list) fffff801`b448a130 ldr x9, [x19, #0x20] ; KAPC.KernelRoutine fffff801`b448a134 ldr x21, [x19, #0x10] ; KAPC.ApcListEntry.Flink fffff801`b448a138 str x9, [sp, #0x30] ; сохранить KernelRoutine fffff801`b448a144 ldr x8, [x19, #0x30] ; KAPC.NormalRoutine fffff801`b448a14c ldr x8, [x19, #0x38] ; KAPC.NormalContext ; Проверка: NormalRoutine == NULL (kernel-only APC)? fffff801`b448a154 cbz x9, normal_routine_path ; да → специальная обработка ; Удалить из списка (unlink): fffff801`b448a168 ldr x10, [x19] ; KAPC.ApcListEntry.Flink fffff801`b448a178 ldr x9, [x19, #8] ; KAPC.ApcListEntry.Blink fffff801`b448a188 str x10, [x9] ; Blink.Flink = Flink fffff801`b448a18c str x9, [x10, #8] ; Flink.Blink = Blink ; Освободить ThreadLock и понизить IRQL: fffff801`b448a198 stlr xzr, [x22] ; ThreadLock = 0 fffff801`b448a19c bl nt!KfLowerIrql ; вернуться к предыдущему IRQL ; Вызвать KernelRoutine через PAC: fffff801`b448a1dc mov x15, x21 ; KernelRoutine fffff801`b448a1e0 bl nt!KscpCfgCheckUserCallTargetEs ; PAC проверка fffff801`b448a1e4 blr x15 ; вызвать KernelRoutine(KAPC, ...)
7.4 KiInitializeUserApc — подготовка user-mode переходаКод (Text):
; Проверка user APC списка: fffff801`b448a388 add x9, x20, #0xA0 ; KTHREAD+0xA0 (User APC list) fffff801`b448a38c ldr x8, [x9] ; User ApcListHead.Flink fffff801`b448a390 add x26, x20, #0xA0 ; save list head fffff801`b448a394 cmp x8, x9 ; список пуст? fffff801`b448a398 beq no_user_apcs ; пустой → выход ; Поиск первого user APC с NormalRoutine: fffff801`b448a404 ldr x8, [x26] ; traverse list fffff801`b448a40c cmp x8, x26 ; end of list? fffff801`b448a410 beq user_apc_done ; Извлечь KAPC и его поля: fffff801`b448a414 sub x25, x8, #0x10 ; KAPC = entry - 0x10 fffff801`b448a41c ldr x9, [x25, #0x20] ; KAPC.KernelRoutine fffff801`b448a44c ldr x9, [x25, #0x30] ; KAPC.NormalRoutine fffff801`b448a45c ldr x9, [x25, #0x38] ; KAPC.NormalContext fffff801`b448a470 ldr x9, [x25, #0x40] ; KAPC.SystemArgument1 fffff801`b448a480 ldr x9, [x25, #0x48] ; KAPC.SystemArgument2
Параметры KiInitializeUserApc:Код (Text):
; Подготовка вызова KiInitializeUserApc: fffff801`b448a50c ldp x5, x4, [sp, #0x10] ; SystemArgument1, SystemArgument2 fffff801`b448a510 mov w6, w21 ; flags fffff801`b448a514 ldr x3, [sp, #0x20] ; NormalContext fffff801`b448a518 mov x1, x19 ; ExceptionFrame / TrapFrame fffff801`b448a51c ldr x0, [sp, #0x48] ; TrapFrame fffff801`b448a520 bl nt!KiInitializeUserApc
8. nt!KiInitializeUserApc — Подготовка user-mode перехода
Параметр Регистр Значение TrapFrame x0 Адрес trap frame потока ExceptionFrame x1 Адрес exception frame NormalRoutine x2 Адрес APC callback (user-mode) NormalContext x3 Контекст APC (ApcContext) SystemArgument1 x4 Arg1 (из NtQueueApcThread) SystemArgument2 x5 Arg2 Flags w6 Флаги доставки
Адрес: 0xFFFFF801`B44B70C0
Функция модифицирует trap frame так, что при возврате из ядра в user-mode, выполнение начинается не с адреса прерывания, а с ntdll!KiUserApcDispatcher.
8.1 Определение machine type и setting up CONTEXT
8.2 Манипуляция trap frameКод (Text):
fffff801`b44b7144 bl nt!PsGetProcessMachine ; получить архитектуру процесса fffff801`b44b7148 mov w8, #0xAA64 ; IMAGE_FILE_MACHINE_ARM64 fffff801`b44b714c cmp w8, w0, uxth #0 ; ARM64 native? fffff801`b44b7150 bne not_arm64_native ; нет → другой путь ; ARM64 native + XState: fffff801`b44b7158 adrp x8, ... fffff801`b44b715c ldr w8, [x8, #0xB9C] ; XState feature flag fffff801`b44b7160 tbz w8, #0, not_arm64_native fffff801`b44b7164 ldr w24, ... ; CONTEXT_EX flag
8.3 Перенаправление возврата в user-modeКод (Text):
fffff801`b44b7184 ldr x8, [x19, #0x98] ; KTHREAD.TrapFrame->Sp (User stack) fffff801`b44b7188 str x8, [x26, #0x38] ; сохранить оригинальный SP fffff801`b44b718c sub x21, x8, #0x10 ; выделить место на стеке ... fffff801`b44b7278 ldr x2, [x26, #0x18] ; CONTEXT size fffff801`b44b727c str w24, [x2] ; CONTEXT flags fffff801`b44b7280 ldr x1, [x26, #0x40] ; source context fffff801`b44b7284 mov x0, x19 ; TrapFrame fffff801`b44b7288 bl nt!KeContextFromKframes ; заполнить CONTEXT из trap frame
Результат: Trap frame модифицирован так, что:Код (Text):
; Установить новый PC (Program Counter) в trap frame: fffff801`b44b72d0 ldp x8, x0, [x26, #0x60] ; dispatcher address и stack layout fffff801`b44b72d4 stp x8, x20, [x0, #8] ; записать параметры APC fffff801`b44b72d8 ldr x8, [x26, #0x70] fffff801`b44b72dc str x8, [x0, #0x18] fffff801`b44b72e0 ldr x8, [x26, #0x78] fffff801`b44b72e4 str x8, [x0] ; NormalRoutine fffff801`b44b72e8 str w22, [x0, #0x20] ; flags fffff801`b44b72ec str x0, [x19, #0x98] ; обновить SP в trap frame
8.4 Выбор адреса возврата по архитектуре
- PC → ntdll!KiUserApcDispatcher
- SP → уменьшен, на стеке размещены параметры APC
- Восстановление оригинального контекста произойдёт через ntdll!NtContinue
9. Структуры данныхКод (Text):
fffff801`b44b72f0 ldrh w9, [x27, #0x7AC] ; EPROCESS.Machine fffff801`b44b72f4 mov w8, #0x8664 ; IMAGE_FILE_MACHINE_AMD64 fffff801`b44b72f8 cmp w9, w8 fffff801`b44b72fc adrp x8, ... fffff801`b44b7304 ldr x9, [x8, #0x28] ; ARM64 dispatcher address fffff801`b44b7308 ldr x8, [x8, #0x268] ; x64/ARM64EC dispatcher address fffff801`b44b730c cselne x8, x9, x8 ; выбрать нужный адрес fffff801`b44b7310 str x8, [x19, #0x148] ; KTHREAD+0x148 = Dispatcher address
9.1 KAPC (0x58 байт)
9.2 KAPC_STATE (0x30 байт)
Смещение Размер Поле Описание +0x00 1 Type 0x12 (ApcObject) +0x01 1 AllFlags Бит 0: CallbackDataContext +0x02 1 Size 0x58 (88 байт) +0x03 1 SpareByte1 Зарезервировано +0x04 4 SpareLong0 Зарезервировано +0x08 8 Thread Указатель на KTHREAD +0x10 16 ApcListEntry LIST_ENTRY (связный список) +0x20 8 KernelRoutine Функция ядра (очистка) +0x28 8 RundownRoutine Функция при rundown +0x30 8 NormalRoutine Пользовательский callback +0x38 8 NormalContext Контекст для NormalRoutine +0x40 8 SystemArgument1 Аргумент 1 +0x48 8 SystemArgument2 Аргумент 2 +0x50 1 ApcStateIndex 0=Original, 1=Attached, 2=Attached +0x51 1 ApcMode 0=KernelMode, 1=UserMode +0x52 1 Inserted Флаг: APC в списке? (0/1)
9.3 APC-поля в KTHREAD
Смещение Размер Поле Описание +0x00 16 ApcListHead[0] Kernel APC list (LIST_ENTRY) +0x10 16 ApcListHead[1] User APC list (LIST_ENTRY) +0x20 8 Process Указатель на KPROCESS +0x28 1 InProgressFlags KernelApcInProgress, SpecialApcInProgress +0x29 1 KernelApcPending Флаг: есть kernel APC для доставки +0x2A 1 UserApcPendingAll Бит 0: SpecialUserApcPending, Бит 1: UserApcPending
10. Диаграмма потока выполнения
Смещение Поле Описание +0x040 ThreadLock Спин-блокировка для APC операций (SWPA) +0x088 TrapFrame Указатель на текущий trap frame +0x090 ApcState KAPC_STATE (текущее состояние APC) +0x0B0 ApcState.Process KPROCESS (владелец APC state) +0x0B8 ApcState.InProgressFlags Флаги выполнения APC +0x0B9 KernelApcPending Флаг: kernel APC ожидает доставки +0x0BA UserApcPendingAll Флаг: user APC ожидает доставки +0x148 ApcStatePointer Массив указателей на ApcState/SavedApcState +0x26A ApcStateIndex Текущий APC state index (0=Original) +0x278 SavedApcState KAPC_STATE (сохранённый при KeStackAttach)
11. Сравнение с CreateRemoteThreadКод (Text):
┌──────────────────────┐ │ kernel32 │ │ QueueUserAPC() │ └──────────┬───────────┘ │ ┌──────────▼───────────┐ │ ntdll │ │ NtQueueApcThread() │ │ (syscall #x1F4) │ └──────────┬───────────┘ │ ═══════════════╪═══════════════ Kernel boundary ═══════════════╪═══════════════ │ ┌──────────▼───────────┐ │ nt!NtQueueApcThread │ ← Thin wrapper │ Rearrange ABI args │ └──────────┬───────────┘ │ ┌──────────▼───────────┐ │ nt!NtQueueApcThreadEx2│ ← Основная логика │ │ │ 1. Validate flags │ │ 2. ObRefByHandle→ETHREAD│ │ 3. Check terminated │ │ 4. Check WoW64/EC │ │ 5. CAS lock (reserve) │ │ 6. ExAllocatePool2 │ ← KAPC (0x58, 'PasP') │ 7. KeInitializeApc │ │ 8. KeInsertQueueApc │ └──────────┬───────────┘ │ ┌───────────────┼───────────────┐ │ │ │ ┌─────────▼──┐ ┌────────▼──────┐ ┌─────▼──────────┐ │KiInsertQueue│ │SignalThread │ │KiExitDispatcher │ │Apc │ │ForApc │ │ │ │ │ │ │ │ Переключение │ │ Insert into │ │ Wake target │ │ контекста │ │ LIST_ENTRY │ │ thread │ │ │ └─────────────┘ └──────┬───────┘ └────────────────┘ │ ┌────────────▼─────────────┐ │ Target thread wakes │ │ (returns from wait) │ └────────────┬──────────────┘ │ ┌────────────▼──────────────┐ │ nt!KiDeliverApc │ │ │ │ 1. Process Kernel APCs │ ← ApcListHead[0] (+0x90) │ → Call KernelRoutine │ │ │ │ 2. Process User APCs │ ← ApcListHead[1] (+0xA0) │ → KiInitializeUserApc │ └────────────┬──────────────┘ │ ═════════════╪════════════════ User-mode boundary ═════════════╪════════════════ │ ┌────────────▼──────────────┐ │ ntdll!KiUserApcDispatcher│ │ │ │ Call ApcRoutine(Context, │ │ Arg1, Arg2) │ │ │ │ NtContinue() → restore │ └───────────────────────────┘
12. Практические выводы
Критерий QueueUserAPC CreateRemoteThread Выделение объекта ExAllocatePool2 (0x58 байт) ObCreateObjectEx (ETHREAD ~0x770 байт) Pool tag 'PasP' 'Thre' / 'CrP' IRQL DISPATCH_LEVEL (2) DPC_LEVEL (3) в KeStartThread Блокировка ThreadLock (SWPA @ KTHREAD+0x40) PushLock (EPROCESS+0x1B8) Стек Не создаётся PspSetupUserStack (новый стек) TEB Не создаётся MmCreateTeb Rundown Protection Нет ExAcquireRundownProtection Protected Process check Нет PspIsProcessReadyForRemoteThread Размер кода ~200 инструкций (Ex2) ~1500+ инструкций (PspAllocateThread) Сложность внедрения Низкая Высокая Обнаружение PoolMon: 'PasP' теги PoolMon: 'Thre' теги + новый поток
13. Ключевые адреса (Build 26100 ARM64)
- QueueUserAPC значительно проще CreateRemoteThread — не создаёт поток, стек, TEB. Просто выделяет KAPC (88 байт) и вставляет в связный список целевого потока.
- Нет проверки Protected Process — в отличие от CreateRemoteThread (PspIsProcessReadyForRemoteThread), QueueUserAPC не проверяет флаг ProtectedProcess в EPROCESS+0x168. Это может позволить APC-инъекцию в защищённые процессы.
- CAS-блокировка вместо Rundown Protection — QueueUserAPC использует атомарный Compare-And-Swap (casal), а не ExAcquireRundownProtection. Это легче, но менее надёжно при конкуренции.
- Тег пула 'PasP' — легко отслеживать через PoolMon / WPA. Может быть использован для обнаружения APC-инъекций.
- DISPATCH_LEVEL IRQL — KeInsertQueueApc поднимает IRQL до DISPATCH_LEVEL и использует спин-блокировку (SWPA). Это блокирует все потоковые переключения на текущном процессоре.
- FIFO порядок доставки — APC добавляются в конец списка и доставляются в порядке очереди.
- Kernel APC доставляются раньше User APC — KiDeliverApc сначала обрабатывает все kernel APC, затем user APC. Это гарантирует, что kernel APC не блокируются user APC.
- PAC (Pointer Authentication) — при вызове KernelRoutine выполняется KscpCfgCheckUserCallTargetEs — проверка PAC кода. Это защитный механизм ARM64, предотвращающий ROP-атаки.
- User-mode переход через KiInitializeUserApc — trap frame модифицируется так, что поток "возвращается" в ntdll!KiUserApcDispatcher, а не по оригинальному адресу. После выполнения APC callback вызывается NtContinue для восстановления контекста.
- IPI для пробуждения — если целевой поток ждёт на другом процессоре, отправляется Inter-Processor Interrupt (KiSendSoftwareInterrupt или HalRequestSoftwareInterrupt).
- ETHREAD+0x6C бит 10 — CrossThreadFlags.Terminated. Проверяется перед постановкой APC в очередь. Если поток уже завершён — возвращает STATUS_INVALID_HANDLE.
- WoW64/ARM64EC проверка — при инъекции APC в WoW64-процесс проверяется архитектура целевого потока. Несовпадение x86/ARM64/ARM64EC может привести к блокировке.
Функция Адрес nt!NtQueueApcThread 0xFFFFF801`B4923410 nt!NtQueueApcThreadEx 0xFFFFF801`B4B00AA0 nt!NtQueueApcThreadEx2 0xFFFFF801`B4B00AD0 nt!KeInitializeApc 0xFFFFF801`B44720F0 nt!KeInsertQueueApc 0xFFFFF801`B4472150 nt!KiInsertQueueApc 0xFFFFF801`B448A6F0 nt!KiSignalThreadForApc 0xFFFFF801`B448A810 nt!KiDeliverApc 0xFFFFF801`B448A030 nt!KiInitializeUserApc 0xFFFFF801`B44B70C0 nt!KiExitDispatcher 0xFFFFF801`B449A918 nt!KiSignalThread 0xFFFFF801`B449B940 nt!KiSendSoftwareInterrupt 0xFFFFF801`B4477240 nt!HalRequestSoftwareInterrupt 0xFFFFF801`B4409F60 nt!ObReferenceObjectByHandle 0xFFFFF801`B4AA6B70 nt!ExAllocatePool2 0xFFFFF801`B4CAC000
QueueUserAPC — Исследование внутренних механизмов ядра Windows (ARM64)
Дата публикации 3 июн 2026 в 00:50