WriteProcessMemory — Исследование внутренних механизмов ядра Windows
Архитектура: ARM64 (AArch64) | Build 26100.1 | Windows 11 24H2
1. Цепочка вызовов (Call Chain)
Код (Text):
kernel32!WriteProcessMemory ↓ ntdll!NtWriteVirtualMemory (syscall) ↓ nt!NtWriteVirtualMemory ← тонкая обёртка (3 инструкции) ↓ nt!MiReadWriteVirtualMemory ← главный диспетчер ├→ nt!ObpReferenceObjectByHandleWithTag ← разрешение хэндла процесса ├→ guarded region entry ← защита от завершения потока ├→ nt!MiCopyVirtualMemory ← ядро копирования │ ├→ nt!KeStackAttachProcess ← подключение к адресному пространству │ │ └→ nt!KiStackAttachProcess ← реализация подключения │ ├→ nt!MiObtainReferencedVadEx ← получение VAD │ ├→ nt!MmProbeAndLockPages ← блокировка страниц │ ├→ nt!_memcpy_spec_unaligned ← копирование (обычный процесс) │ ├→ nt!VslDebugReadWriteSecureProcess← копирование (VSM/secure) │ ├→ nt!MmUnlockPages ← разблокировка страниц │ ├→ nt!MiUnlockAndDereferenceVadShared← освобождение VAD │ └→ nt!KiUnstackDetachProcess ← возврат адресного пространства ├→ guarded region exit ← выход из guarded region ├→ nt!PsIsProcessLoggingEnabled ← проверка ETW логирования └→ nt!EtwTiLogReadWriteVm ← ETW телеметрия
2. Дизассемблирование ключевых функций
2.1 nt!NtWriteVirtualMemory
Адрес:Размер:Код (Text):
FFFFF801`B4BDFEC0Код (Text):
3 инструкции (12 байт)Для сравнения, nt!NtReadVirtualMemory устанавливает w5=0x10 (Read). Обе функции используют один и тот же обработчик MiReadWriteVirtualMemory.Код (Text):
; Устанавливаем w6 = 0 (направление: запись) mov w6,#0 ; Устанавливаем w5 = 0x20 (режим доступа: Write) mov w5,#0x20 ; Переходим к общему обработчику чтения/записи b nt!MiReadWriteVirtualMemory
2.2 nt!MiReadWriteVirtualMemory
Адрес:Проверка флагов:Код (Text):
FFFFF801`B4BDFBB8
Валидация адресного пространства:Код (Text):
; Проверяем биты 2-31 (должны быть нулевыми) tst w20,#0xFFFFFFFC bne STATUS_INVALID_PARAMETER ; 0xC000000D ; Проверяем режим доступа ands w9,w20,#2 tbnz w20,#0,error_path ccmpeq w5,#0x10,#0 ; проверяем read/write bne error_path
Разрешение хэндла процесса:Код (Text):
; Проверяем что адрес + размер не превышает 0x7FFFFFFFEFFF add x9,x21,x23 ; x21 = BaseAddress, x23 = Buffer sub x9,x9,#1 cmp x9,x23 ; проверка на переполнение blo STATUS_ACCESS_VIOLATION ; 0xC0000005 ; ... вторая проверка с константой 0x7FFFFFFFEFFF ... ldr x10,[max_user_addr] ; 0x7FFFFFFFEFFF cmp x9,x10 bhi STATUS_ACCESS_VIOLATION
Проверка same-process и guarded region:Код (Text):
; Вызываем ObpReferenceObjectByHandleWithTag ldr w4,[pool_tag] ; тег 'mVmM' (0x4D6D566D) mov w3,w24 ; PreviousMode ldr w1,[fp,#0x14] ; AccessMode (0x20 = Write) mov x0,x8 ; ProcessHandle bl nt!ObpReferenceObjectByHandleWithTag ; Проверяем результат tbnz w19,#0x1F,error_path ; если NTSTATUS error → выход
Cross-process write — guarded region:Код (Text):
; Получаем указатель на текущий процесс ldr x11,[x0,#0xB0] ; ETHREAD+0xB0 → EPROCESS ; Проверяем флаг в EPROCESS+0x168 ldr x8,[x22,#0x168] ; x22 = целевой EPROCESS tst w8,#1 ccmpne x11,x22,#4 ; сравниваем current vs target bne secure_process_path ; разные процессы → особый путь ; Для записи (w5=0x20) всегда идём по cross-process пути cmp w12,#0x10 ; 0x10 = Read, 0x20 = Write bne cross_process_write
Выход из guarded region:Код (Text):
; Входим в guarded region (предотвращает завершение потока) mov w27,#1 ; флаг: в guarded region ldr x0,[fp,#0x28] ; текущий ETHREAD ; Декремент счётчика ETHREAD+0x1DE (16-битный) ldrsh w8,[x0,#0x1DE] sub w8,w8,#1 strh w8,[x0,#0x1DE] ; Устанавливаем бит 5 в ETHREAD+0x589 ldrb w8,[x0,#0x589] ubfx w20,w8,#5,#1 ; сохраняем старое значение orr w8,w8,#0x20 ; устанавливаем бит 5 strb w8,[x0,#0x589] ; Вызываем MiCopyVirtualMemory mov w7,#0 ; параметр add x6,fp,#0x18 ; &NumberOfBytesWritten mov w5,w24 ; PreviousMode mov x4,x21 ; NumberOfBytesWritten ptr mov x3,x23 ; Buffer (источник) mov x2,x22 ; Target EPROCESS ldr x1,[fp,#0x20] ; BaseAddress ldr x0,[fp,#0x30] ; Current EPROCESS bl nt!MiCopyVirtualMemory
ETW телеметрия:Код (Text):
; Восстанавливаем флаг в ETHREAD+0x589 ldrb w8,[x0,#0x589] mov w9,#0xDF ; маска: сбросить бит 5 and w8,w8,w9 orr w8,w8,w20,lsl #5 ; восстановить старое значение strb w8,[x0,#0x589] ; Выходим из guarded region bl nt!KiLeaveGuardedRegionUnsafe
2.3 nt!MiCopyVirtualMemoryКод (Text):
; Проверяем включено ли логирование для процесса bl nt!PsIsProcessLoggingEnabled cbnz w0,log_event ; Если включено — логируем событие чтения/записи VM log_event: mov w0,w19 ; NTSTATUS mov x1,x11 ; TargetProcess mov w3,w12 ; AccessMode mov x4,x23 ; Buffer ldr x5,[fp,#0x18] ; BytesWritten bl nt!EtwTiLogReadWriteVm
Адрес:Стековый кадр:Код (Text):
FFFFF801`B4BDF388Инициализация и определение режима:Код (Text):
0x3F0 байт
Подключение к адресному пространству источника:Код (Text):
; Сохраняем параметры в стековый кадр str x0,[x26,#0x20] ; Source EPROCESS str x1,[x26,#0x40] ; BaseAddress str x2,[x26,#0x68] ; Target EPROCESS str x3,[x26,#0x90] ; Buffer strb w5,[x26,#4] ; PreviousMode str x6,[x26,#0x30] ; &BytesWritten ; Определяем флаги безопасности ubfiz w10,w7,#4,#1 ; извлекаем бит из параметра add w9,w10,#1 ldr x8,[x26,#0x20] ldr x8,[x8,#0x168] ; EPROCESS+0x168 флаги tst x8,#1 ; бит 0 — secure process? orr w8,w9,#4 csincne w9,w8,w10 ; Проверяем целевой процесс ldr x8,[x26,#0x68] ldr x8,[x8,#0x168] ; Target EPROCESS+0x168 tst x8,#1 orr w8,w9,#8 cseleq w21,w9,w8 ; w21 = комбинированные флаги
Получение VAD и проверка валидности:Код (Text):
; Прикрепляемся к source process add x1,x26,#0x120 ; PKAPC_STATE ldr x0,[x26,#0x20] ; Source EPROCESS bl nt!KeStackAttachProcess ; Проверяем валидность адреса источника ldr x8,[x26,#0x40] ; BaseAddress cmp x22,x8 ; сравниваем с ожидаемым ldrsb w8,[x26,#4] ; PreviousMode ccmpeq w8,#0,#4 beq skip_probe ; если совпадает — пропускаем probe ; Проверяем адресный диапазон add x9,x10,x23 ; BaseAddress + RemainingSize mov x8,#0x7FFFFFFF0000 ; максимальный пользовательский адрес cmp x9,x8 ccmpls x9,x10,#0 bhs valid_address
Определение стратегии буфера:Код (Text):
; Получаем VAD для адресного диапазона mov x2,x26 ; контекст mov w1,#2 ; тип операции mov x0,x22 ; BaseAddress bl nt!MiObtainReferencedVadEx str x0,[x26,#0x60] ; сохраняем VAD str x0,[x26,#0x50] ; Извлекаем размер региона из VAD ldrb w8,[x0,#0x21] ; VAD флаги ldr w9,[x0,#0x1C] ; размер региона bfi x9,x8,#0x20,#8 ; комбинируем lsl x8,x9,#0xC ; конвертируем в байты orr x8,x8,#0xFFF add x9,x8,#1 ; выравненный размер
Выделение pool-буфера (для больших записей):Код (Text):
; Для буферов < 0x200 байт — используем стек cmp x20,#0x200 blo use_stack_buffer ; Проверяем нужен ли pool-буфер (бит 1 в флагах) tbz w21,#1,use_stack_buffer ; Для больших буферов ограничиваем размер 0xE000 cmp x20,#0xE,lsl #0xC ; 0xE000 = 57344 байта mov x8,#0xE000 cselhi x20,x8,x20 ; cap at 0xE000
Блокировка и копирование:Код (Text):
; Вычисляем размер pool-буфера and x8,x22,#0xFFF ; смещение внутри страницы add x8,x8,x20 add x8,x8,#0xFFF sbfx w8,w8,#0xC,#0x10 add w8,w8,#6 sbfiz w8,w8,#3,#0xD strh w8,[x26,#0x158] ; сохраняем MDL size ; Максимальный pool-буфер — 64KB ldr x9,[x26,#0x78] ; remaining bytes cmp x9,#0x10000 ; 64KB mov x8,#0x10000 cselhi x25,x8,x9 ; cap at 64KB ; Тег пула: 'MmRw' (0x77526D4D) mov w2,#0x6D4D movk w2,#0x7752,lsl #0x10 ; w2 = 0x77526D4D mov x1,x25 ; размер mov x0,#0x100 ; тип пула bl nt!MiAllocatePool ; Если выделение не удалось — уменьшаем размер в 2 раза cbz x19,allocation_failed lsr x25,x25,#1 ; половина размера cmp x25,#0x200 bhi retry_allocation
Цикл частичного копирования:Код (Text):
; Для больших буферов — блокируем страницы tbz w21,#1,skip_lock mov w2,#0 ; AccessMode ldrsb w1,[x26,#4] ; PreviousMode add x0,x26,#0x150 ; PMDL bl nt!MmProbeAndLockPages ; === КОПИРОВАНИЕ ДАННЫХ === ; Для обычных процессов: прямой memcpy tbz w21,#2,use_memcpy ; Для VSM-изолированных процессов: специальная функция mov w4,#1 ; Write direction mov x3,x20 ; размер mov x2,x19 ; source buffer ldr x0,[x26,#0x20] ; EPROCESS bl nt!VslDebugReadWriteSecureProcess ; Обычный memcpy: use_memcpy: mov x1,x22 ; BaseAddress mov x2,x20 ; количество байт mov x0,x19 ; буфер-источник bl nt!_memcpy_spec_unaligned
Отключение от адресного пространства:Код (Text):
; Проверяем статус копирования ldr w8,[x26] ; NTSTATUS cmp w8,#0x8000000D ; STATUS_PARTIAL_COPY? bne done ; если нет — выход ; Обновляем указатели для следующей итерации sub x24,x24,x8 ; уменьшаем remaining sub x20,x20,x20 ; корректируем add x22,x22,x20 ; сдвигаем BaseAddress ldr x8,[x26,#0x10] add x8,x8,x20 ; сдвигаем Buffer str x8,[x26,#0x10] b loop_start ; повторяем
2.4 nt!KeStackAttachProcess / nt!KiStackAttachProcessКод (Text):
; Отключаемся от target process mov w1,#0 add x0,x26,#0x120 ; PKAPC_STATE bl nt!KiUnstackDetachProcess ; Освобождаем pool-буфер (если был выделен) cbz x27,no_pool_buffer mov w1,#0 mov x0,x19 ; pool pointer bl nt!ExFreePoolWithTag
KeStackAttachProcess:KiStackAttachProcess:Код (Text):
FFFFF801`B4461C90 (3 инструкции)Код (Text):
FFFFF801`B44B4CB82.5 nt!KiUnstackDetachProcessКод (Text):
; KeStackAttachProcess — обёртка mov x2,x1 ; x2 = PKAPC_STATE mov w1,#0 ; flags = 0 b nt!KiStackAttachProcess ; === KiStackAttachProcess === ; Получаем текущий ETHREAD ldr x19,[xpr,#0x988] ; Быстрый путь: если целевой == текущий процесс ldr x8,[x19,#0xB0] ; ETHREAD+0xB0 → EPROCESS cmp x8,x21 ; x21 = Target EPROCESS beq same_process ; быстрый возврат ; Разные процессы: полный путь подключения ; Сохраняем IRQL и.acquire ThreadLock mov w0,#2 ; DISPATCH_LEVEL bl nt!KfRaiseIrql add x23,x19,#0x40 ; ThreadLock (KTHREAD+0x40) ; Атомарно захватываем ThreadLock через SWPA mov x22,#1 swpa x22,x8,[x23] ; Store Word Pair Atomic cbz x8,lock_acquired ; если был 0 — lock получен ; Spin-wait если lock занят yield ldr x8,[x23] cbnz x8,spin_wait b retry_swpa ; Проверяем ApcStateIndex (KTHREAD+0x26A) ldrb w8,[x19,#0x26A] cbnz w8,already_attached ; BUGCHECK если уже attached ; Сохраняем текущий KAPC_STATE в SavedApcState (KTHREAD+0x278) add x11,x19,#0x90 ; текущий ApcState add x20,x19,#0x278 ; SavedApcState ; ... копируем ApcListHead, Process, KernelApcPending, UserApcPending ... ; Устанавливаем ApcStateIndex = 1 (Attached) mov w8,#1 strb w8,[x19,#0x26A] ; Обновляем DirectoryTableBase для смены адресного пространства ; (переключение CR3/TTBR1 на ARM64)
Адрес:Код (Text):
FFFFF801`B44B5238Код (Text):
; Проверяем что ApcState[0x20] == 1 (корректный attach) ldr x8,[x0,#0x20] cmp x8,#1 beq detach_unsafe ; corrupted → BUGCHECK cbnz x8,already_detached ; 0 = не подключены ; Acquire ThreadLock (KTHREAD+0x40) через SWPA mov x22,#1 add x25,x20,#0x40 ; ThreadLock swpa x22,x8,[x25] ; Проверяем ApcStateIndex ldrb w8,[x20,#0x26A] ; KTHREAD+0x26A cbz w8,bugcheck ; 0 = не attached → BUGCHECK! ; Проверяем что APC списки пусты add x9,x20,#0x90 ; KernelApcList ldr x8,[x9] cmp x8,x9 bne bugcheck ; не пуст → BUGCHECK add x9,x20,#0xA0 ; UserApcList ldr x8,[x9] cmp x8,x9 bne bugcheck ; не пуст → BUGCHECK ; Восстанавливаем SavedApcState из KTHREAD+0x278 add x12,x20,#0x278 ; ... восстановление ApcListHead, Process, флагов ... ; Сбрасываем ApcStateIndex = 0 (Original) strb wzr,[x20,#0x26A] ; Восстанавливаем DirectoryTableBase ; Release ThreadLock
3. Ключевые структуры и смещения
3.1 Таблица NTSTATUS-кодов
3.2 Теги пула (Pool Tags)
Код Значение Значение Когда возвращается STATUS_ACCESS_VIOLATION 0xC0000005 Невалидный адрес или доступ запрещён Выход за границы / невалидная память STATUS_INVALID_PARAMETER 0xC000000D Невалидные флаги Биты 2-31 ненулевые / невалидный режим STATUS_PARTIAL_COPY 0x8000000D Частичное копирование Скопирована часть данных, нужен цикл STATUS_INVALID_HANDLE 0xC0000008 Невалидный хэндл процесса ObpReferenceObjectByHandleWithTag failed STATUS_NO_MEMORY 0xC0000017 Нехватка памяти Ошибка выделения pool-буфера
3.3 Смещения ETHREAD
Тег HEX Назначение 'mVmM' 0x4D6D566D ObpReferenceObjectByHandleWithTag — разрешение хэндла 'MmRw' 0x77526D4D Промежуточный буфер для cross-process записи
3.4 Смещения EPROCESS / KPROCESS
Смещение Поле Описание +0x040 ThreadLock Spinlock для APC/attach операций (SWPA) +0x090 ApcState.ApcListHead[0] Список Kernel APC +0x0A0 ApcState.ApcListHead[1] Список User APC +0x0B0 CrossThreadFlags → EPROCESS Указатель на текущий процесс +0x168 CrossThreadFlags Флаги (бит 0 — secure process marker) +0x1DE GuardedRegionCounter 16-битный счётчик guarded region +0x26A ApcStateIndex 0=Original, 1=Attached +0x278 SavedApcState Сохранённое KAPC_STATE при attach +0x252 PreviousMode 0=KernelMode, 1=UserMode +0x589 MemoryOperationFlags Бит 5 — active memory operation
3.5 Смещения VAD (Virtual Address Descriptor)
Смещение Поле Описание +0x028 DirectoryTableBase Базовый адрес таблицы страниц (CR3/TTBR) +0x168 ProcessFlags Бит 0 — secure/protected процесс +0x270 SecureProcessVSM Флаг VSM-изоляции процесса +0x2F8 CrossProcessAccess Флаг cross-process доступа +0x6BC VsmIsolationFlag Флаг VSM-изоляции (проверка в MiCopyVirtualMemory)
Смещение Поле Описание +0x020 Flags Флаги VAD +0x01C Size Размер региона (в страницах) +0x021 Flags2 Расширенные флаги (комбинируется с Size)
4. Механизмы безопасности
4.1 Guarded Region
При cross-process записи поток входит в guarded region — это предотвращает его завершение (APC delivery) во время копирования:
4.2 Адресное пространство — KeStackAttachProcess
- Декремент счётчика
(16-бит)Код (Text):
ETHREAD+0x1DE- Установка бита 5 в
Код (Text):
ETHREAD+0x589- После копирования — восстановление флагов и вызов
Код (Text):
KiLeaveGuardedRegionUnsafe
Переключение контекста адресного пространства через KAPC_STATE:
4.3 VAD-валидация
- Acquire ThreadLock (KTHREAD+0x40) через SWPA на DISPATCH_LEVEL
- Сохранение текущего KAPC_STATE в SavedApcState (KTHREAD+0x278)
- Установка ApcStateIndex = 1 (KTHREAD+0x26A)
- Переключение DirectoryTableBase (EPROCESS+0x28)
- При отключении — проверка пустоты APC-списков → BUGCHECK если нет
Перед копированием проверяется что целевой адрес зарезервирован/закоммичен:
4.4 VSM / Secure Process
- Вызов
для получения VADКод (Text):
MiObtainReferencedVadEx- Извлечение размера региона из VAD+0x1C и VAD+0x21
- Если VAD не найден →
Код (Text):
STATUS_ACCESS_VIOLATION- После копирования —
Код (Text):
MiUnlockAndDereferenceVadShared
Для процессов с VSM-изоляцией (Virtual Secure Mode):
4.5 Отсутствие PPL-проверки
- Проверка флагов в EPROCESS+0x168 (бит 0), EPROCESS+0x270, EPROCESS+0x6BC
- Если установлен — используется
вместо прямого memcpyКод (Text):
VslDebugReadWriteSecureProcess- Для обычных процессов —
Код (Text):
_memcpy_spec_unaligned
Важно: WriteProcessMemory НЕ проверяет Protected Process Light (PPL) статус целевого процесса в своём коде. Защита от записи в PPL-процессы реализуется через объектные хэндлы (SeAccessCheck при открытии процесса) и VSM-изоляцию, а не на уровне NtWriteVirtualMemory.
5. Стратегия буферизации
5.1 Алгоритм выбора буфера
5.2 Pool-аллокация с graceful degradation
Размер записи Буфер Max за итерацию < 0x200 (512 байт) Стек (frame+0x1F8) 0x200 байт 0x200 — 0xE000 Pool (tag 'MmRw') 0xE000 байт > 0xE000 Pool (tag 'MmRw'), цикл 0xE000 за итерацию
Код (Text):
; Начинаем с запрошенного размера (до 64KB) cselhi x25, x8, x9 ; min(remaining, 0x10000) ; MiAllocatePool с тегом 'MmRw' bl nt!MiAllocatePool ; Если не удалось — уменьшаем в 2 раза и пробуем снова cbz x19, retry_half lsr x25, x25, #1 ; /2 cmp x25, #0x200 ; минимум 512 байт bhi retry
6. Схема потока данных (ASCII Art)
Код (Text):
┌──────────────────────────────────────────────────────────────────────────┐ │ User Mode (kernel32.dll) │ │ WriteProcessMemory(hProcess, lpBaseAddress, lpBuffer, nSize, &written) │ └─────────────────────────────────────┬────────────────────────────────────┘ │ SYSCALL (NtWriteVirtualMemory) ▼ ┌──────────────────────────────────────────────────────────────────────────┐ │ nt!NtWriteVirtualMemory │ │ w6 = 0 (Write), w5 = 0x20 → branch MiReadWriteVirtualMemory │ └─────────────────────────────────────┬────────────────────────────────────┘ ▼ ┌──────────────────────────────────────────────────────────────────────────┐ │ nt!MiReadWriteVirtualMemory │ │ │ │ 1. Validate flags (bits 2-31 = 0) │ │ 2. Validate address range (< 0x7FFFFFFFEFFF) │ │ 3. ObpReferenceObjectByHandleWithTag → target EPROCESS │ │ 4. Check same-process (ETHREAD+0xB0 vs target) │ │ 5. Enter guarded region (ETHREAD+0x1DE, ETHREAD+0x589) │ │ 6. Call MiCopyVirtualMemory │ │ 7. Exit guarded region │ │ 8. ETW: PsIsProcessLoggingEnabled → EtwTiLogReadWriteVm │ └─────────────────────────────────────┬────────────────────────────────────┘ ▼ ┌──────────────────────────────────────────────────────────────────────────┐ │ nt!MiCopyVirtualMemory │ │ │ │ ┌─ Loop ────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ Phase 1: SOURCE │ │ │ │ ┌───────────────────────────────────────────────────────────────┐│ │ │ │ │ KeStackAttachProcess(source EPROCESS) ││ │ │ │ │ └→ KiStackAttachProcess: ThreadLock → Save KAPC_STATE ││ │ │ │ │ → Switch DirectoryTableBase ││ │ │ │ │ Probe source address range ││ │ │ │ │ MiObtainReferencedVadEx → validate VAD ││ │ │ │ │ MmProbeAndLockPages (if large buffer) ││ │ │ │ │ KiUnstackDetachProcess → restore original context ││ │ │ │ └───────────────────────────────────────────────────────────────┘│ │ │ │ │ │ │ │ Phase 2: DESTINATION │ │ │ │ ┌───────────────────────────────────────────────────────────────┐│ │ │ │ │ KeStackAttachProcess(target EPROCESS) ││ │ │ │ │ ││ │ │ │ │ if (VSM secure process) ││ │ │ │ │ VslDebugReadWriteSecureProcess() ││ │ │ │ │ else ││ │ │ │ │ _memcpy_spec_unaligned(target, source, size) ││ │ │ │ │ ││ │ │ │ │ MmUnlockPages / MiUnlockAndDereferenceVadShared ││ │ │ │ │ KiUnstackDetachProcess → restore original context ││ │ │ │ └───────────────────────────────────────────────────────────────┘│ │ │ │ │ │ │ │ if (STATUS_PARTIAL_COPY) → update ptrs, loop │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ Cleanup: ExFreePoolWithTag('MmRw') if pool buffer allocated │ └──────────────────────────────────────────────────────────────────────────┘
7. Сравнение с другими техниками инъекции
Критерий WriteProcessMemory CreateRemoteThread QueueUserAPC Цель Запись данных в память Создание удалённого потока Планирование APC Требует хэндл PROCESS_VM_WRITE + PROCESS_VM_OPERATION PROCESS_CREATE_THREAD + PROCESS_QUERY_INFORMATION THREAD_SET_CONTEXT PPL-проверка Нет (на уровне syscall) Да (PsTestProtectedProcessConversely) Нет Вызывает kernel memcpy Да (физическое копирование) Нет (создаёт поток) Нет (APC в очередь) KeStackAttachProcess Да (обязательно) Нет Нет VSM-изоляция VslDebugReadWriteSecureProcess Блокируется Не проверяется Guarded Region Да (защита от termination) Нет Нет Pool-теги 'MmRw' (буфер), 'mVmM' (handle) 'ThdM' (ETHREAD) 'PasP' (KAPC) ETW логирование EtwTiLogReadWriteVm EtwTiLogCreateThreadNotify Нет Детектируемость Высокая (ETW + Pool + Guarded) Средняя (PPL-check) Низкая (нет ETW)
8. Практические выводы
- Тонкая обёртка NtWriteVirtualMemory — всего 3 инструкции, всё работает через общий механизм MiReadWriteVirtualMemory, разделяемый с NtReadVirtualMemory.
- Два режима доступа — w5=0x10 для Read, w5=0x20 для Write. Разница только в значении константы, код полностью общий.
- MmCopyVirtualMemory (FFFFF801`B4BDFEA0) — legacy-обёртка, тоже 3 инструкции → MiCopyVirtualMemory. Используется для kernel-mode вызовов.
- Двухфазный процесс — сначала attach к source (probe + lock), затем attach к target (memcpy). Каждый attach — полный KeStackAttachProcess с сохранением KAPC_STATE.
- Guarded Region — критический механизм: предотвращает thread termination во время копирования. Без него частичная запись могла бы повредить целевой процесс.
- Pool-буфер 'MmRw' — для записей >512 байт выделяется промежуточный буфер из NonPaged Pool, максимум 64KB. Graceful degradation: при нехватке памяти размер уменьшается в 2 раза.
- VSM-изоляция — для secure-процессов используется VslDebugReadWriteSecureProcess вместо прямого memcpy. Это предотвращает чтение/запись памяти VSM-изолированных процессов.
- Pool-тег 'MmRw' (0x77526D4D) — уникальный идентификатор, который можно использовать для детекции через PoolMon или аналогичные инструменты.
- ETW-телеметрия — EtwTiLogReadWriteVm логирует все операции ReadVirtualMemory/WriteVirtualMemory, включая PID, адрес, размер и результат. Это мощный механизм детекции.
- VAD-валидация — MiObtainReferencedVadEx проверяет что целевой адрес зарезервирован/закоммичен. Запись в незакоммиченную память вызывает STATUS_ACCESS_VIOLATION.
- STATUS_PARTIAL_COPY (0x8000000D) — ядро может скопировать только часть данных за одну итерацию. В этом случае обновляются указатели и цикл повторяется.
- Отсутствие PPL-проверки — в отличие от CreateRemoteThread, WriteProcessMemory не проверяет PPL напрямую. Защита реализуется через SeAccessCheck при открытии хэндла процесса.
- BUGCHECK при ошибке detach — KiUnstackDetachProcess вызывает KeBugCheck(0xDEAD) если ApcStateIndex=0 или APC-списки не пусты. Это жёсткая защита от повреждения APC-механизма.
9. Таблица адресов ключевых функций
Функция Адрес Назначение nt!NtWriteVirtualMemory FFFFF801`B4BDFEC0 Точка входа syscall (запись) nt!NtReadVirtualMemory FFFFF801`B4BDFEB0 Точка входа syscall (чтение) nt!MmCopyVirtualMemory FFFFF801`B4BDFEA0 Kernel-mode legacy入口 nt!MiReadWriteVirtualMemory FFFFF801`B4BDFBB8 Главный диспетчер R/W nt!MiCopyVirtualMemory FFFFF801`B4BDF388 Ядро копирования памяти nt!KeStackAttachProcess FFFFF801`B4461C90 Подключение к адресу. пространству nt!KiStackAttachProcess FFFFF801`B44B4CB8 Реализация attach nt!KiUnstackDetachProcess FFFFF801`B44B5238 Реализация detach nt!ObpReferenceObjectByHandleWithTag FFFFF801`B4AA7918 Разрешение хэндла nt!MmProbeAndLockPages FFFFF801`B4598AB0 Блокировка страниц nt!MmUnlockPages FFFFF801`B4598E40 Разблокировка страниц nt!MiObtainReferencedVadEx FFFFF801`B462ABE8 Получение VAD nt!MiUnlockAndDereferenceVadShared FFFFF801`B462B6C0 Освобождение VAD nt!VslDebugReadWriteSecureProcess FFFFF801`B4A27388 VSM secure process R/W nt!EtwTiLogReadWriteVm FFFFF801`B4B618A0 ETW телеметрия nt!PsIsProcessLoggingEnabled FFFFF801`B44F22E0 Проверка ETW логирования nt!MiAllocatePool FFFFF801`B4589538 Выделение памяти из пула nt!ExFreePoolWithTag FFFFF801`B4CAC740 Освобождение pool-памяти nt!KiLeaveGuardedRegionUnsafe FFFFF801`B445B640 Выход из guarded region nt!_memcpy_spec_unaligned FFFFF801`B4838140 Оптимизированное копирование
WriteProcessMemory — Исследование внутренних механизмов ядра Windows
Дата публикации 3 июн 2026 в 00:48