WriteProcessMemory — Исследование внутренних механизмов ядра Windows

Дата публикации 3 июн 2026 в 00:48
WriteProcessMemory — Исследование внутренних механизмов ядра Windows
Архитектура: ARM64 (AArch64) | Build 26100.1 | Windows 11 24H2

1. Цепочка вызовов (Call Chain)
Код (Text):
  1.  
  2. kernel32!WriteProcessMemory
  3.     ↓
  4. ntdll!NtWriteVirtualMemory (syscall)
  5.     ↓
  6. nt!NtWriteVirtualMemory                    ← тонкая обёртка (3 инструкции)
  7.     ↓
  8. nt!MiReadWriteVirtualMemory                ← главный диспетчер
  9.     ├→ nt!ObpReferenceObjectByHandleWithTag ← разрешение хэндла процесса
  10.     ├→ guarded region entry                 ← защита от завершения потока
  11.     ├→ nt!MiCopyVirtualMemory              ← ядро копирования
  12.     │   ├→ nt!KeStackAttachProcess          ← подключение к адресному пространству
  13.     │   │   └→ nt!KiStackAttachProcess      ← реализация подключения
  14.     │   ├→ nt!MiObtainReferencedVadEx       ← получение VAD
  15.     │   ├→ nt!MmProbeAndLockPages           ← блокировка страниц
  16.     │   ├→ nt!_memcpy_spec_unaligned        ← копирование (обычный процесс)
  17.     │   ├→ nt!VslDebugReadWriteSecureProcess← копирование (VSM/secure)
  18.     │   ├→ nt!MmUnlockPages                 ← разблокировка страниц
  19.     │   ├→ nt!MiUnlockAndDereferenceVadShared← освобождение VAD
  20.     │   └→ nt!KiUnstackDetachProcess        ← возврат адресного пространства
  21.     ├→ guarded region exit                  ← выход из guarded region
  22.     ├→ nt!PsIsProcessLoggingEnabled         ← проверка ETW логирования
  23.     └→ nt!EtwTiLogReadWriteVm              ← ETW телеметрия
  24.  

2. Дизассемблирование ключевых функций
2.1 nt!NtWriteVirtualMemory

Адрес:
Код (Text):
  1. FFFFF801`B4BDFEC0
Размер:
Код (Text):
  1. 3 инструкции (12 байт)
Код (Text):
  1. ; Устанавливаем w6 = 0 (направление: запись)
  2. mov         w6,#0
  3. ; Устанавливаем w5 = 0x20 (режим доступа: Write)
  4. mov         w5,#0x20
  5. ; Переходим к общему обработчику чтения/записи
  6. b           nt!MiReadWriteVirtualMemory
Для сравнения, nt!NtReadVirtualMemory устанавливает w5=0x10 (Read). Обе функции используют один и тот же обработчик MiReadWriteVirtualMemory.
2.2 nt!MiReadWriteVirtualMemory
Адрес:
Код (Text):
  1. FFFFF801`B4BDFBB8
Проверка флагов:
Код (Text):
  1. ; Проверяем биты 2-31 (должны быть нулевыми)
  2. tst         w20,#0xFFFFFFFC
  3. bne         STATUS_INVALID_PARAMETER    ; 0xC000000D
  4. ; Проверяем режим доступа
  5. ands        w9,w20,#2
  6. tbnz        w20,#0,error_path
  7. ccmpeq      w5,#0x10,#0                 ; проверяем read/write
  8. bne         error_path
Валидация адресного пространства:
Код (Text):
  1. ; Проверяем что адрес + размер не превышает 0x7FFFFFFFEFFF
  2. add         x9,x21,x23                  ; x21 = BaseAddress, x23 = Buffer
  3. sub         x9,x9,#1
  4. cmp         x9,x23                      ; проверка на переполнение
  5. blo         STATUS_ACCESS_VIOLATION     ; 0xC0000005
  6. ; ... вторая проверка с константой 0x7FFFFFFFEFFF ...
  7. ldr         x10,[max_user_addr]          ; 0x7FFFFFFFEFFF
  8. cmp         x9,x10
  9. bhi         STATUS_ACCESS_VIOLATION
Разрешение хэндла процесса:
Код (Text):
  1. ; Вызываем ObpReferenceObjectByHandleWithTag
  2. ldr         w4,[pool_tag]               ; тег 'mVmM' (0x4D6D566D)
  3. mov         w3,w24                       ; PreviousMode
  4. ldr         w1,[fp,#0x14]               ; AccessMode (0x20 = Write)
  5. mov         x0,x8                        ; ProcessHandle
  6. bl          nt!ObpReferenceObjectByHandleWithTag
  7. ; Проверяем результат
  8. tbnz        w19,#0x1F,error_path         ; если NTSTATUS error → выход
Проверка same-process и guarded region:
Код (Text):
  1. ; Получаем указатель на текущий процесс
  2. ldr         x11,[x0,#0xB0]               ; ETHREAD+0xB0 → EPROCESS
  3. ; Проверяем флаг в EPROCESS+0x168
  4. ldr         x8,[x22,#0x168]              ; x22 = целевой EPROCESS
  5. tst         w8,#1
  6. ccmpne      x11,x22,#4                   ; сравниваем current vs target
  7. bne         secure_process_path          ; разные процессы → особый путь
  8. ; Для записи (w5=0x20) всегда идём по cross-process пути
  9. cmp         w12,#0x10                    ; 0x10 = Read, 0x20 = Write
  10. bne         cross_process_write
Cross-process write — guarded region:
Код (Text):
  1. ; Входим в guarded region (предотвращает завершение потока)
  2. mov         w27,#1                       ; флаг: в guarded region
  3. ldr         x0,[fp,#0x28]               ; текущий ETHREAD
  4. ; Декремент счётчика ETHREAD+0x1DE (16-битный)
  5. ldrsh       w8,[x0,#0x1DE]
  6. sub         w8,w8,#1
  7. strh        w8,[x0,#0x1DE]
  8. ; Устанавливаем бит 5 в ETHREAD+0x589
  9. ldrb        w8,[x0,#0x589]
  10. ubfx        w20,w8,#5,#1                ; сохраняем старое значение
  11. orr         w8,w8,#0x20                 ; устанавливаем бит 5
  12. strb        w8,[x0,#0x589]
  13. ; Вызываем MiCopyVirtualMemory
  14. mov         w7,#0                        ; параметр
  15. add         x6,fp,#0x18                  ; &NumberOfBytesWritten
  16. mov         w5,w24                       ; PreviousMode
  17. mov         x4,x21                       ; NumberOfBytesWritten ptr
  18. mov         x3,x23                       ; Buffer (источник)
  19. mov         x2,x22                       ; Target EPROCESS
  20. ldr         x1,[fp,#0x20]               ; BaseAddress
  21. ldr         x0,[fp,#0x30]               ; Current EPROCESS
  22. bl          nt!MiCopyVirtualMemory
Выход из guarded region:
Код (Text):
  1. ; Восстанавливаем флаг в ETHREAD+0x589
  2. ldrb        w8,[x0,#0x589]
  3. mov         w9,#0xDF                    ; маска: сбросить бит 5
  4. and         w8,w8,w9
  5. orr         w8,w8,w20,lsl #5            ; восстановить старое значение
  6. strb        w8,[x0,#0x589]
  7. ; Выходим из guarded region
  8. bl          nt!KiLeaveGuardedRegionUnsafe
ETW телеметрия:
Код (Text):
  1. ; Проверяем включено ли логирование для процесса
  2. bl          nt!PsIsProcessLoggingEnabled
  3. cbnz        w0,log_event
  4. ; Если включено — логируем событие чтения/записи VM
  5. log_event:
  6. mov         w0,w19                       ; NTSTATUS
  7. mov         x1,x11                       ; TargetProcess
  8. mov         w3,w12                       ; AccessMode
  9. mov         x4,x23                       ; Buffer
  10. ldr         x5,[fp,#0x18]               ; BytesWritten
  11. bl          nt!EtwTiLogReadWriteVm
2.3 nt!MiCopyVirtualMemory
Адрес:
Код (Text):
  1. FFFFF801`B4BDF388
Стековый кадр:
Код (Text):
  1. 0x3F0 байт
Инициализация и определение режима:
Код (Text):
  1. ; Сохраняем параметры в стековый кадр
  2. str         x0,[x26,#0x20]               ; Source EPROCESS
  3. str         x1,[x26,#0x40]               ; BaseAddress
  4. str         x2,[x26,#0x68]               ; Target EPROCESS
  5. str         x3,[x26,#0x90]               ; Buffer
  6. strb        w5,[x26,#4]                  ; PreviousMode
  7. str         x6,[x26,#0x30]               ; &BytesWritten
  8. ; Определяем флаги безопасности
  9. ubfiz       w10,w7,#4,#1                 ; извлекаем бит из параметра
  10. add         w9,w10,#1
  11. ldr         x8,[x26,#0x20]
  12. ldr         x8,[x8,#0x168]               ; EPROCESS+0x168 флаги
  13. tst         x8,#1                        ; бит 0 — secure process?
  14. orr         w8,w9,#4
  15. csincne     w9,w8,w10
  16. ; Проверяем целевой процесс
  17. ldr         x8,[x26,#0x68]
  18. ldr         x8,[x8,#0x168]               ; Target EPROCESS+0x168
  19. tst         x8,#1
  20. orr         w8,w9,#8
  21. cseleq      w21,w9,w8                    ; w21 = комбинированные флаги
Подключение к адресному пространству источника:
Код (Text):
  1. ; Прикрепляемся к source process
  2. add         x1,x26,#0x120               ; PKAPC_STATE
  3. ldr         x0,[x26,#0x20]               ; Source EPROCESS
  4. bl          nt!KeStackAttachProcess
  5. ; Проверяем валидность адреса источника
  6. ldr         x8,[x26,#0x40]               ; BaseAddress
  7. cmp         x22,x8                       ; сравниваем с ожидаемым
  8. ldrsb       w8,[x26,#4]                  ; PreviousMode
  9. ccmpeq      w8,#0,#4
  10. beq         skip_probe                   ; если совпадает — пропускаем probe
  11. ; Проверяем адресный диапазон
  12. add         x9,x10,x23                   ; BaseAddress + RemainingSize
  13. mov         x8,#0x7FFFFFFF0000           ; максимальный пользовательский адрес
  14. cmp         x9,x8
  15. ccmpls      x9,x10,#0
  16. bhs         valid_address
Получение VAD и проверка валидности:
Код (Text):
  1. ; Получаем VAD для адресного диапазона
  2. mov         x2,x26                       ; контекст
  3. mov         w1,#2                        ; тип операции
  4. mov         x0,x22                       ; BaseAddress
  5. bl          nt!MiObtainReferencedVadEx
  6. str         x0,[x26,#0x60]               ; сохраняем VAD
  7. str         x0,[x26,#0x50]
  8. ; Извлекаем размер региона из VAD
  9. ldrb        w8,[x0,#0x21]                ; VAD флаги
  10. ldr         w9,[x0,#0x1C]                ; размер региона
  11. bfi         x9,x8,#0x20,#8              ; комбинируем
  12. lsl         x8,x9,#0xC                   ; конвертируем в байты
  13. orr         x8,x8,#0xFFF
  14. add         x9,x8,#1                     ; выравненный размер
Определение стратегии буфера:
Код (Text):
  1. ; Для буферов < 0x200 байт — используем стек
  2. cmp         x20,#0x200
  3. blo         use_stack_buffer
  4. ; Проверяем нужен ли pool-буфер (бит 1 в флагах)
  5. tbz         w21,#1,use_stack_buffer
  6. ; Для больших буферов ограничиваем размер 0xE000
  7. cmp         x20,#0xE,lsl #0xC            ; 0xE000 = 57344 байта
  8. mov         x8,#0xE000
  9. cselhi      x20,x8,x20                   ; cap at 0xE000
Выделение pool-буфера (для больших записей):
Код (Text):
  1. ; Вычисляем размер pool-буфера
  2. and         x8,x22,#0xFFF               ; смещение внутри страницы
  3. add         x8,x8,x20
  4. add         x8,x8,#0xFFF
  5. sbfx        w8,w8,#0xC,#0x10
  6. add         w8,w8,#6
  7. sbfiz       w8,w8,#3,#0xD
  8. strh        w8,[x26,#0x158]             ; сохраняем MDL size
  9. ; Максимальный pool-буфер — 64KB
  10. ldr         x9,[x26,#0x78]               ; remaining bytes
  11. cmp         x9,#0x10000                  ; 64KB
  12. mov         x8,#0x10000
  13. cselhi      x25,x8,x9                   ; cap at 64KB
  14. ; Тег пула: 'MmRw' (0x77526D4D)
  15. mov         w2,#0x6D4D
  16. movk        w2,#0x7752,lsl #0x10         ; w2 = 0x77526D4D
  17. mov         x1,x25                       ; размер
  18. mov         x0,#0x100                    ; тип пула
  19. bl          nt!MiAllocatePool
  20. ; Если выделение не удалось — уменьшаем размер в 2 раза
  21. cbz         x19,allocation_failed
  22. lsr         x25,x25,#1                   ; половина размера
  23. cmp         x25,#0x200
  24. bhi         retry_allocation
Блокировка и копирование:
Код (Text):
  1. ; Для больших буферов — блокируем страницы
  2. tbz         w21,#1,skip_lock
  3. mov         w2,#0                         ; AccessMode
  4. ldrsb       w1,[x26,#4]                  ; PreviousMode
  5. add         x0,x26,#0x150                ; PMDL
  6. bl          nt!MmProbeAndLockPages
  7. ; === КОПИРОВАНИЕ ДАННЫХ ===
  8. ; Для обычных процессов: прямой memcpy
  9. tbz         w21,#2,use_memcpy
  10. ; Для VSM-изолированных процессов: специальная функция
  11. mov         w4,#1                         ; Write direction
  12. mov         x3,x20                        ; размер
  13. mov         x2,x19                        ; source buffer
  14. ldr         x0,[x26,#0x20]               ; EPROCESS
  15. bl          nt!VslDebugReadWriteSecureProcess
  16. ; Обычный memcpy:
  17. use_memcpy:
  18. mov         x1,x22                        ; BaseAddress
  19. mov         x2,x20                        ; количество байт
  20. mov         x0,x19                        ; буфер-источник
  21. bl          nt!_memcpy_spec_unaligned
Цикл частичного копирования:
Код (Text):
  1. ; Проверяем статус копирования
  2. ldr         w8,[x26]                      ; NTSTATUS
  3. cmp         w8,#0x8000000D               ; STATUS_PARTIAL_COPY?
  4. bne         done                          ; если нет — выход
  5. ; Обновляем указатели для следующей итерации
  6. sub         x24,x24,x8                    ; уменьшаем remaining
  7. sub         x20,x20,x20                   ; корректируем
  8. add         x22,x22,x20                   ; сдвигаем BaseAddress
  9. ldr         x8,[x26,#0x10]
  10. add         x8,x8,x20                     ; сдвигаем Buffer
  11. str         x8,[x26,#0x10]
  12. b           loop_start                    ; повторяем
Отключение от адресного пространства:
Код (Text):
  1. ; Отключаемся от target process
  2. mov         w1,#0
  3. add         x0,x26,#0x120                ; PKAPC_STATE
  4. bl          nt!KiUnstackDetachProcess
  5. ; Освобождаем pool-буфер (если был выделен)
  6. cbz         x27,no_pool_buffer
  7. mov         w1,#0
  8. mov         x0,x19                        ; pool pointer
  9. bl          nt!ExFreePoolWithTag
2.4 nt!KeStackAttachProcess / nt!KiStackAttachProcess
KeStackAttachProcess:
Код (Text):
  1. FFFFF801`B4461C90 (3 инструкции)
KiStackAttachProcess:
Код (Text):
  1. FFFFF801`B44B4CB8
Код (Text):
  1. ; KeStackAttachProcess — обёртка
  2. mov         x2,x1                         ; x2 = PKAPC_STATE
  3. mov         w1,#0                         ; flags = 0
  4. b           nt!KiStackAttachProcess
  5. ; === KiStackAttachProcess ===
  6. ; Получаем текущий ETHREAD
  7. ldr         x19,[xpr,#0x988]
  8. ; Быстрый путь: если целевой == текущий процесс
  9. ldr         x8,[x19,#0xB0]               ; ETHREAD+0xB0 → EPROCESS
  10. cmp         x8,x21                        ; x21 = Target EPROCESS
  11. beq         same_process                  ; быстрый возврат
  12. ; Разные процессы: полный путь подключения
  13. ; Сохраняем IRQL и.acquire ThreadLock
  14. mov         w0,#2                         ; DISPATCH_LEVEL
  15. bl          nt!KfRaiseIrql
  16. add         x23,x19,#0x40                 ; ThreadLock (KTHREAD+0x40)
  17. ; Атомарно захватываем ThreadLock через SWPA
  18. mov         x22,#1
  19. swpa        x22,x8,[x23]                  ; Store Word Pair Atomic
  20. cbz         x8,lock_acquired              ; если был 0 — lock получен
  21. ; Spin-wait если lock занят
  22. yield
  23. ldr         x8,[x23]
  24. cbnz        x8,spin_wait
  25. b           retry_swpa
  26. ; Проверяем ApcStateIndex (KTHREAD+0x26A)
  27. ldrb        w8,[x19,#0x26A]
  28. cbnz        w8,already_attached           ; BUGCHECK если уже attached
  29. ; Сохраняем текущий KAPC_STATE в SavedApcState (KTHREAD+0x278)
  30. add         x11,x19,#0x90                 ; текущий ApcState
  31. add         x20,x19,#0x278                ; SavedApcState
  32. ; ... копируем ApcListHead, Process, KernelApcPending, UserApcPending ...
  33. ; Устанавливаем ApcStateIndex = 1 (Attached)
  34. mov         w8,#1
  35. strb        w8,[x19,#0x26A]
  36. ; Обновляем DirectoryTableBase для смены адресного пространства
  37. ; (переключение CR3/TTBR1 на ARM64)
2.5 nt!KiUnstackDetachProcess
Адрес:
Код (Text):
  1. FFFFF801`B44B5238
Код (Text):
  1. ; Проверяем что ApcState[0x20] == 1 (корректный attach)
  2. ldr         x8,[x0,#0x20]
  3. cmp         x8,#1
  4. beq         detach_unsafe                ; corrupted → BUGCHECK
  5. cbnz        x8,already_detached          ; 0 = не подключены
  6. ; Acquire ThreadLock (KTHREAD+0x40) через SWPA
  7. mov         x22,#1
  8. add         x25,x20,#0x40                ; ThreadLock
  9. swpa        x22,x8,[x25]
  10. ; Проверяем ApcStateIndex
  11. ldrb        w8,[x20,#0x26A]              ; KTHREAD+0x26A
  12. cbz         w8,bugcheck                  ; 0 = не attached → BUGCHECK!
  13. ; Проверяем что APC списки пусты
  14. add         x9,x20,#0x90                 ; KernelApcList
  15. ldr         x8,[x9]
  16. cmp         x8,x9
  17. bne         bugcheck                     ; не пуст → BUGCHECK
  18. add         x9,x20,#0xA0                 ; UserApcList
  19. ldr         x8,[x9]
  20. cmp         x8,x9
  21. bne         bugcheck                     ; не пуст → BUGCHECK
  22. ; Восстанавливаем SavedApcState из KTHREAD+0x278
  23. add         x12,x20,#0x278
  24. ; ... восстановление ApcListHead, Process, флагов ...
  25. ; Сбрасываем ApcStateIndex = 0 (Original)
  26. strb        wzr,[x20,#0x26A]
  27. ; Восстанавливаем DirectoryTableBase
  28. ; Release ThreadLock

3. Ключевые структуры и смещения
3.1 Таблица NTSTATUS-кодов

КодЗначениеЗначениеКогда возвращается
STATUS_ACCESS_VIOLATION0xC0000005Невалидный адрес или доступ запрещёнВыход за границы / невалидная память
STATUS_INVALID_PARAMETER0xC000000DНевалидные флагиБиты 2-31 ненулевые / невалидный режим
STATUS_PARTIAL_COPY0x8000000DЧастичное копированиеСкопирована часть данных, нужен цикл
STATUS_INVALID_HANDLE0xC0000008Невалидный хэндл процессаObpReferenceObjectByHandleWithTag failed
STATUS_NO_MEMORY0xC0000017Нехватка памятиОшибка выделения pool-буфера
3.2 Теги пула (Pool Tags)
ТегHEXНазначение
'mVmM'0x4D6D566DObpReferenceObjectByHandleWithTag — разрешение хэндла
'MmRw'0x77526D4DПромежуточный буфер для cross-process записи
3.3 Смещения ETHREAD
СмещениеПолеОписание
+0x040ThreadLockSpinlock для APC/attach операций (SWPA)
+0x090ApcState.ApcListHead[0]Список Kernel APC
+0x0A0ApcState.ApcListHead[1]Список User APC
+0x0B0CrossThreadFlags → EPROCESSУказатель на текущий процесс
+0x168CrossThreadFlagsФлаги (бит 0 — secure process marker)
+0x1DEGuardedRegionCounter16-битный счётчик guarded region
+0x26AApcStateIndex0=Original, 1=Attached
+0x278SavedApcStateСохранённое KAPC_STATE при attach
+0x252PreviousMode0=KernelMode, 1=UserMode
+0x589MemoryOperationFlagsБит 5 — active memory operation
3.4 Смещения EPROCESS / KPROCESS
СмещениеПолеОписание
+0x028DirectoryTableBaseБазовый адрес таблицы страниц (CR3/TTBR)
+0x168ProcessFlagsБит 0 — secure/protected процесс
+0x270SecureProcessVSMФлаг VSM-изоляции процесса
+0x2F8CrossProcessAccessФлаг cross-process доступа
+0x6BCVsmIsolationFlagФлаг VSM-изоляции (проверка в MiCopyVirtualMemory)
3.5 Смещения VAD (Virtual Address Descriptor)
СмещениеПолеОписание
+0x020FlagsФлаги VAD
+0x01CSizeРазмер региона (в страницах)
+0x021Flags2Расширенные флаги (комбинируется с Size)

4. Механизмы безопасности
4.1 Guarded Region

При cross-process записи поток входит в guarded region — это предотвращает его завершение (APC delivery) во время копирования:
  1. Декремент счётчика
    Код (Text):
    1. ETHREAD+0x1DE
    (16-бит)
  2. Установка бита 5 в
    Код (Text):
    1. ETHREAD+0x589
  3. После копирования — восстановление флагов и вызов
    Код (Text):
    1. KiLeaveGuardedRegionUnsafe
4.2 Адресное пространство — KeStackAttachProcess
Переключение контекста адресного пространства через KAPC_STATE:
  1. Acquire ThreadLock (KTHREAD+0x40) через SWPA на DISPATCH_LEVEL
  2. Сохранение текущего KAPC_STATE в SavedApcState (KTHREAD+0x278)
  3. Установка ApcStateIndex = 1 (KTHREAD+0x26A)
  4. Переключение DirectoryTableBase (EPROCESS+0x28)
  5. При отключении — проверка пустоты APC-списков → BUGCHECK если нет
4.3 VAD-валидация
Перед копированием проверяется что целевой адрес зарезервирован/закоммичен:
  1. Вызов
    Код (Text):
    1. MiObtainReferencedVadEx
    для получения VAD
  2. Извлечение размера региона из VAD+0x1C и VAD+0x21
  3. Если VAD не найден →
    Код (Text):
    1. STATUS_ACCESS_VIOLATION
  4. После копирования —
    Код (Text):
    1. MiUnlockAndDereferenceVadShared
4.4 VSM / Secure Process
Для процессов с VSM-изоляцией (Virtual Secure Mode):
  1. Проверка флагов в EPROCESS+0x168 (бит 0), EPROCESS+0x270, EPROCESS+0x6BC
  2. Если установлен — используется
    Код (Text):
    1. VslDebugReadWriteSecureProcess
    вместо прямого memcpy
  3. Для обычных процессов —
    Код (Text):
    1. _memcpy_spec_unaligned
4.5 Отсутствие PPL-проверки
Важно: WriteProcessMemory НЕ проверяет Protected Process Light (PPL) статус целевого процесса в своём коде. Защита от записи в PPL-процессы реализуется через объектные хэндлы (SeAccessCheck при открытии процесса) и VSM-изоляцию, а не на уровне NtWriteVirtualMemory.

5. Стратегия буферизации
5.1 Алгоритм выбора буфера

Размер записиБуферMax за итерацию
< 0x200 (512 байт)Стек (frame+0x1F8)0x200 байт
0x200 — 0xE000Pool (tag 'MmRw')0xE000 байт
> 0xE000Pool (tag 'MmRw'), цикл0xE000 за итерацию
5.2 Pool-аллокация с graceful degradation
Код (Text):
  1. ; Начинаем с запрошенного размера (до 64KB)
  2. cselhi      x25, x8, x9        ; min(remaining, 0x10000)
  3. ; MiAllocatePool с тегом 'MmRw'
  4. bl          nt!MiAllocatePool
  5. ; Если не удалось — уменьшаем в 2 раза и пробуем снова
  6. cbz         x19, retry_half
  7. lsr         x25, x25, #1       ; /2
  8. cmp         x25, #0x200        ; минимум 512 байт
  9. bhi         retry

6. Схема потока данных (ASCII Art)
Код (Text):
  1.  
  2. ┌──────────────────────────────────────────────────────────────────────────┐
  3. │                        User Mode (kernel32.dll)                         │
  4. │  WriteProcessMemory(hProcess, lpBaseAddress, lpBuffer, nSize, &written) │
  5. └─────────────────────────────────────┬────────────────────────────────────┘
  6.                                       │ SYSCALL (NtWriteVirtualMemory)
  7.                                       ▼
  8. ┌──────────────────────────────────────────────────────────────────────────┐
  9. │                     nt!NtWriteVirtualMemory                             │
  10. │  w6 = 0 (Write), w5 = 0x20 → branch MiReadWriteVirtualMemory          │
  11. └─────────────────────────────────────┬────────────────────────────────────┘
  12.                                       ▼
  13. ┌──────────────────────────────────────────────────────────────────────────┐
  14. │                   nt!MiReadWriteVirtualMemory                           │
  15. │                                                                         │
  16. │  1. Validate flags (bits 2-31 = 0)                                     │
  17. │  2. Validate address range (< 0x7FFFFFFFEFFF)                          │
  18. │  3. ObpReferenceObjectByHandleWithTag → target EPROCESS                 │
  19. │  4. Check same-process (ETHREAD+0xB0 vs target)                        │
  20. │  5. Enter guarded region (ETHREAD+0x1DE, ETHREAD+0x589)                │
  21. │  6. Call MiCopyVirtualMemory                                            │
  22. │  7. Exit guarded region                                                 │
  23. │  8. ETW: PsIsProcessLoggingEnabled → EtwTiLogReadWriteVm              │
  24. └─────────────────────────────────────┬────────────────────────────────────┘
  25.                                       ▼
  26. ┌──────────────────────────────────────────────────────────────────────────┐
  27. │                      nt!MiCopyVirtualMemory                              │
  28. │                                                                         │
  29. │  ┌─ Loop ────────────────────────────────────────────────────────────┐  │
  30. │  │                                                                   │  │
  31. │  │  Phase 1: SOURCE                                                  │  │
  32. │  │  ┌───────────────────────────────────────────────────────────────┐│  │
  33. │  │  │ KeStackAttachProcess(source EPROCESS)                        ││  │
  34. │  │  │  └→ KiStackAttachProcess: ThreadLock → Save KAPC_STATE      ││  │
  35. │  │  │     → Switch DirectoryTableBase                               ││  │
  36. │  │  │ Probe source address range                                     ││  │
  37. │  │  │ MiObtainReferencedVadEx → validate VAD                        ││  │
  38. │  │  │ MmProbeAndLockPages (if large buffer)                         ││  │
  39. │  │  │ KiUnstackDetachProcess → restore original context             ││  │
  40. │  │  └───────────────────────────────────────────────────────────────┘│  │
  41. │  │                                                                   │  │
  42. │  │  Phase 2: DESTINATION                                             │  │
  43. │  │  ┌───────────────────────────────────────────────────────────────┐│  │
  44. │  │  │ KeStackAttachProcess(target EPROCESS)                         ││  │
  45. │  │  │                                                               ││  │
  46. │  │  │ if (VSM secure process)                                       ││  │
  47. │  │  │     VslDebugReadWriteSecureProcess()                          ││  │
  48. │  │  │ else                                                          ││  │
  49. │  │  │     _memcpy_spec_unaligned(target, source, size)              ││  │
  50. │  │  │                                                               ││  │
  51. │  │  │ MmUnlockPages / MiUnlockAndDereferenceVadShared               ││  │
  52. │  │  │ KiUnstackDetachProcess → restore original context             ││  │
  53. │  │  └───────────────────────────────────────────────────────────────┘│  │
  54. │  │                                                                   │  │
  55. │  │  if (STATUS_PARTIAL_COPY) → update ptrs, loop                     │  │
  56. │  │                                                                   │  │
  57. │  └───────────────────────────────────────────────────────────────────┘  │
  58. │                                                                         │
  59. │  Cleanup: ExFreePoolWithTag('MmRw') if pool buffer allocated           │
  60. └──────────────────────────────────────────────────────────────────────────┘
  61.  

7. Сравнение с другими техниками инъекции
КритерийWriteProcessMemoryCreateRemoteThreadQueueUserAPC
ЦельЗапись данных в памятьСоздание удалённого потокаПланирование APC
Требует хэндлPROCESS_VM_WRITE + PROCESS_VM_OPERATIONPROCESS_CREATE_THREAD + PROCESS_QUERY_INFORMATIONTHREAD_SET_CONTEXT
PPL-проверкаНет (на уровне syscall)Да (PsTestProtectedProcessConversely)Нет
Вызывает kernel memcpyДа (физическое копирование)Нет (создаёт поток)Нет (APC в очередь)
KeStackAttachProcessДа (обязательно)НетНет
VSM-изоляцияVslDebugReadWriteSecureProcessБлокируетсяНе проверяется
Guarded RegionДа (защита от termination)НетНет
Pool-теги'MmRw' (буфер), 'mVmM' (handle)'ThdM' (ETHREAD)'PasP' (KAPC)
ETW логированиеEtwTiLogReadWriteVmEtwTiLogCreateThreadNotifyНет
ДетектируемостьВысокая (ETW + Pool + Guarded)Средняя (PPL-check)Низкая (нет ETW)

8. Практические выводы
  1. Тонкая обёртка NtWriteVirtualMemory — всего 3 инструкции, всё работает через общий механизм MiReadWriteVirtualMemory, разделяемый с NtReadVirtualMemory.
  2. Два режима доступа — w5=0x10 для Read, w5=0x20 для Write. Разница только в значении константы, код полностью общий.
  3. MmCopyVirtualMemory (FFFFF801`B4BDFEA0) — legacy-обёртка, тоже 3 инструкции → MiCopyVirtualMemory. Используется для kernel-mode вызовов.
  4. Двухфазный процесс — сначала attach к source (probe + lock), затем attach к target (memcpy). Каждый attach — полный KeStackAttachProcess с сохранением KAPC_STATE.
  5. Guarded Region — критический механизм: предотвращает thread termination во время копирования. Без него частичная запись могла бы повредить целевой процесс.
  6. Pool-буфер 'MmRw' — для записей >512 байт выделяется промежуточный буфер из NonPaged Pool, максимум 64KB. Graceful degradation: при нехватке памяти размер уменьшается в 2 раза.
  7. VSM-изоляция — для secure-процессов используется VslDebugReadWriteSecureProcess вместо прямого memcpy. Это предотвращает чтение/запись памяти VSM-изолированных процессов.
  8. Pool-тег 'MmRw' (0x77526D4D) — уникальный идентификатор, который можно использовать для детекции через PoolMon или аналогичные инструменты.
  9. ETW-телеметрия — EtwTiLogReadWriteVm логирует все операции ReadVirtualMemory/WriteVirtualMemory, включая PID, адрес, размер и результат. Это мощный механизм детекции.
  10. VAD-валидация — MiObtainReferencedVadEx проверяет что целевой адрес зарезервирован/закоммичен. Запись в незакоммиченную память вызывает STATUS_ACCESS_VIOLATION.
  11. STATUS_PARTIAL_COPY (0x8000000D) — ядро может скопировать только часть данных за одну итерацию. В этом случае обновляются указатели и цикл повторяется.
  12. Отсутствие PPL-проверки — в отличие от CreateRemoteThread, WriteProcessMemory не проверяет PPL напрямую. Защита реализуется через SeAccessCheck при открытии хэндла процесса.
  13. BUGCHECK при ошибке detach — KiUnstackDetachProcess вызывает KeBugCheck(0xDEAD) если ApcStateIndex=0 или APC-списки не пусты. Это жёсткая защита от повреждения APC-механизма.

9. Таблица адресов ключевых функций
ФункцияАдресНазначение
nt!NtWriteVirtualMemoryFFFFF801`B4BDFEC0Точка входа syscall (запись)
nt!NtReadVirtualMemoryFFFFF801`B4BDFEB0Точка входа syscall (чтение)
nt!MmCopyVirtualMemoryFFFFF801`B4BDFEA0Kernel-mode legacy入口
nt!MiReadWriteVirtualMemoryFFFFF801`B4BDFBB8Главный диспетчер R/W
nt!MiCopyVirtualMemoryFFFFF801`B4BDF388Ядро копирования памяти
nt!KeStackAttachProcessFFFFF801`B4461C90Подключение к адресу. пространству
nt!KiStackAttachProcessFFFFF801`B44B4CB8Реализация attach
nt!KiUnstackDetachProcessFFFFF801`B44B5238Реализация detach
nt!ObpReferenceObjectByHandleWithTagFFFFF801`B4AA7918Разрешение хэндла
nt!MmProbeAndLockPagesFFFFF801`B4598AB0Блокировка страниц
nt!MmUnlockPagesFFFFF801`B4598E40Разблокировка страниц
nt!MiObtainReferencedVadExFFFFF801`B462ABE8Получение VAD
nt!MiUnlockAndDereferenceVadSharedFFFFF801`B462B6C0Освобождение VAD
nt!VslDebugReadWriteSecureProcessFFFFF801`B4A27388VSM secure process R/W
nt!EtwTiLogReadWriteVmFFFFF801`B4B618A0ETW телеметрия
nt!PsIsProcessLoggingEnabledFFFFF801`B44F22E0Проверка ETW логирования
nt!MiAllocatePoolFFFFF801`B4589538Выделение памяти из пула
nt!ExFreePoolWithTagFFFFF801`B4CAC740Освобождение pool-памяти
nt!KiLeaveGuardedRegionUnsafeFFFFF801`B445B640Выход из guarded region
nt!_memcpy_spec_unalignedFFFFF801`B4838140Оптимизированное копирование

0 28
galenkane

galenkane
Active Member

Регистрация:
13 янв 2017
Публикаций:
4