Серия APC: KiUserApcDispatcher и Wow64

Воскресенье, 28 июня 2020 г.

-Эволюция KiUserApcDispatcher
-WoW64
-Совместимость APC с Wow64
-Первое решение: До Windows 7
-Wow64 QueueUserAPC Callstack
-Современное решение: Начиная с Windows 7
-Как на самом деле работает кодировка
-Преобразование 64 бит в Wow64
-Преобразование Wow64 в 64 бит
-Валидация ApcRoutine в Wow64
-Подробнее о KiUserApcDispatcher
-Резюме

Я рекомендую прочитать предыдущие сообщения, прежде чем читать это:
-API пользовательского APC: Мы обсудили API пользовательского режима пользовательского APC
-Внутренние компоненты пользовательского APC: Мы обсудили реализацию пользовательского APC в ядре

Продолжаем разговор о внутреннем устройстве APC в windows: На этот раз мы обсудим APC
диспетчеризацию в пользовательском режиме и то, как APC работает в процессах Wow64:

-Эволюция KiUserApcDispatcher
-Модификации функций APC для поддержки Wow64
-Методы внедрения APC в Wow64

Эволюция KiUserApcDispatcher

NTDLL содержит набор точек входа, которые ядро использует для запуска кода в пользовательском режиме, например:
KiUserExceptionDispatcher, KiUserCallbackDispatcher, ...

В предыдущем посте мы видели, что пользовательские APC начинают выполняться в ntdll!KiUserApcDispatcher в режиме пользователя.
пользовательском режиме. Каково назначение KiUserApcDispatcher?

Чтобы понять назначение KiUserApcDispatcher, давайте изучим эволюцию реализации функции
KiUserApcDispatcher от старых версий windows до новейшей реализации. Важно
важно сказать, что эта функция написана на ассемблере, но я создал псевдокод
который поможет вам быстрее понять логику этой функции:

//
// Windows XP SP3 32 bit OS
// the KiUserApcDispatcher function
//
VOID
KiUserApcDispatcher(
PPS_APC_ROUTINE ApcRoutine, [esp]
PVOID SystemArgument1, [esp+4]
PVOID SystemArgument2, [esp+8]
PVOID SystemArgument3, [esp+0xc]
CONTEXT ContextRecord [esp+0x10]
)
{
ApcRoutine(
SystemArgument1,
SystemArgument2,
SystemArgument3
);
NtContinue(&ContextRecord, TRUE);
}

Аргументы KiUserApcDispatcher передаются на стеке - мы знаем, что эта функция
написана на ассемблере из-за пользовательского соглашения о вызове - Эта функция считывает ApcRoutine из [esp], в то время как обычно адрес возврата хранится в [esp].
ApcRoutine из [esp], в то время как обычно адрес возврата хранится в [esp] - В данном случае отсутствует
адрес возврата в этом случае отсутствует.

Аргументы KiUserApcDispatcher:

1. ApcRoutine: "NormalRoutine" из структуры KAPC.
2. SystemArgument1-3 - аргументы APC.
3. ContextRecord - Контекст пользовательского режима, который прервал APC - В типичном случае
указатель инструкции находится в заглушке системного вызова alertable wait, но в случае специального пользовательского APC он может находиться в любом месте пользовательского режима.
APC он может находиться где угодно в пользовательском режиме.

Давайте рассмотрим реализацию: Мы видим, что вызываются 2 важные функции: Первая,
Вызывается ApcRoutine. Аргумент "ApcRoutine" к NtQueueApcThread / "NormalRoutine"
объекта KAPC - Это вызовет фактический код APC. Это указатель, который был
передан в NtQueueApcThread.

Как я объяснял в первой части серии, все ожидающие пользовательские APC должны быть выполнены один за другим
и только когда очередь опустеет, ОС должна вернуться к контексту в
"ContextRecord" - Это обрабатывается "NtContinue":

NTSTATUS
NtContinue(
PCONTEXT ContextRecord,
BOOLEAN TestAlert
);

Этот системный вызов получает запись ContextRecord от KiUserApcDispatcher. NtContinue
вызывается с TestAlert = TRUE, что приводит к выполнению большего количества ожидающих APC, если таковые имеются.

Итак, подведем итог: в оригинальном проекте NT, KiUserApcDispatcher имел 2 основные функции
обязанности:

1. Вызывать процедуру APC
2. Возврат в режим ядра через NtContinue и вызов других APC, если они есть.
3. Если APC не выполняются, вернуться к предыдущему контексту внутри записи ContextRecord
аргумент.

Теперь давайте посмотрим, как Wow64 повлиял на KiUserApcDispatcher в 64-битных операционных системах

WoW64

На 64-битных процессорах код может сильно отличаться. Например:

-Размер указателя составляет 8 байт вместо 4 байт, поскольку адресное пространство больше.
-Выравнивание структуры данных может отличаться в x64
-Соглашение о вызове отличается (чтобы использовать дополнительные регистры, которые предоставляет процессор x64).
 обеспечивает)

Как вы можете себе представить, ОС и API ОС были немного изменены. Большая часть API осталась прежней,
но ABI (Application Binary Interface) значительно изменился. В связи с этим возникает определенная проблема
с поддержкой существующих 32-битных приложений на 64-битных операционных системах. Вот почему
Microsoft создала слой под названием Wow64.

Основная цель Wow64 (Windows 32 bit на Windows 64 bit) - заставить существующие 32-битные
исполняемых файлов работать как на 32-битных, так и на 64-битных операционных системах. Это
реализован как слой эмуляции совместимости, который в основном эмулирует API Windows, которые
были изменены в связи с переходом на x64-битный код. В ОС есть много мест, которые пришлось
Как вы можете себе представить, механизм APC пришлось немного изменить, чтобы адаптировать его к Wow64.
немного, чтобы адаптировать его к Wow64.

Простое объяснение дизайна Wow64:

-Большинство библиотек пользовательского режима компилируются как 64-битные DLL и 32-битные DLL. На сайте
 64-битные библиотеки хранятся внутри c:\windows\system32, а 32-битные DLL - в C:\Windows\system32.
 в C:\Windows\SysWow64.
-Wow64 поддерживает только код пользовательского режима. Весь код режима ядра является 64-битным.
-Существует 2 версии ntdll, которые загружаются в процесс Wow64: 64-битная и 32-битная.
-Когда приложение вызывает обертку системного вызова в 32-битной ntdll (напрямую или через
 Win32), вызывается wow64cpu для изменения режима процессора на 64-битный и вызова wow64.dll.
-wow64.dll имеет обертку для каждого системного вызова. Цель этих оберток заключается в том, чтобы
 перевести параметры для 64-битных системных вызовов и вызвать соответствующую 64-битную
 NTDLL. Большинство этих обёрток представляют собой автоматически генерируемый код, но они могут иметь
 пользовательскую реализацию, как, например, обертка для NtQueueApcThread, как мы скоро увидим.
-На уровне процессора переход в 32-битный / 64-битный режим осуществляется путем изменения сегмента CS
 который используется. Таким образом, ОС переходит из длинного режима (режим x64) в режим IA32
 режим совместимости. Чтение (https://wiki.osdev.org/X86-
 64#How_do_I_enable_Long_Mode_.3F) для получения подробной информации о смене режима работы процессора. В
 сообществе безопасности это обычно называют "Небесные врата".
-Многие трюки эмуляции используются для исправления допущений в 32-битных исполняемых файлах. Например,
 c:\windows\system32 перенаправляется на c:\windows\syswow64 внутри процессов wow64.

Существует еще много вопросов, касающихся Wow64, это лишь простое объяснение, которое позволит нам понять
позволит нам понять работу Wow64 APC.

Чтобы узнать больше о Wow64, я рекомендую вам прочитать пост Петра Бенеша о Wow64
Internals: (https://wbenny.github.io/2018/11/04/wow64-internals.html).

Совместимость APC с Wow64

Итак, представьте, что вы написали следующее 32-битное приложение:

VOID
WINAPI
ApcCode(
ULONG_PTR dwData
)
{
printf("32 bit code!\n");
}
int main()
{
QueueUserAPC(ApcCode, GetCurrentThread(), 0);
SleepEx(INFINITE, TRUE);
}

Почему существует проблема совместимости при запуске этого приложения на 64-битных операционных системах? Как
мы видели в начале статьи, выполнение APC начинается по адресу
ntdll!KiUserApcDispatcher. Обратите внимание, что ядро не обрабатывает переход в
Wow64 32-битный режим, поэтому это должно быть сделано в пользовательском режиме каким-то образом. Это означает, что
нет точек входа в ядро внутри 32-битной NTDLL, а выполнение начинается в 64-битной NTDLL. I
надеюсь, теперь вы все поняли: 64-битный KiUserApcDispatcher должен как-то обрабатывать переход.
перед выполнением ApcRoutine, которая ожидает выполнения в 32-битном режиме. Кроме того, 64-битный
код может выполняться внутри процесса wow64. Как ОС узнает, нужно ли ей выполнять
APC в 32-битном или 64-битном режиме?

Первое решение: До Windows 7

Решение этой проблемы было реализовано давно (где-то в эпоху XP)
и было повторно реализовано в Windows 7. Давайте начнем с рассмотрения старой реализации и
позже мы также поговорим о современной реализации.

Чтобы понять суть решения, давайте изучим wow64.dll обертку NtQueueApcThread на
Windows Vista:

//
// В исходном коде это, вероятно, массив, но я думаю, что так будет чище.
//
typedef struct _WOW64_NT_QUEUE_APC_THREAD_ARGS {
ULONG32 ThreadHandle;
ULONG32 ApcRoutine;
ULONG32 SystemArgument1;
ULONG32 SystemArgument2;
ULONG32 SystemArgument3;
} WOW64_NT_QUEUE_APC_THREAD_ARGS, *PWOW64_NT_QUEUE_APC_THREAD_ARGS;
//
// Windows Vista реализация обертки совместимости NtQueueApcThread в
wow64.dll
//
NTSTATUS
whNtQueueApcThread(
PWOW64_NT_QUEUE_APC_THREAD_ARGS Аргументы
)
{
PPS_APC_ROUTINE EncodedApcRoutine = (PPS_APC_ROUTINE)Arguments->ApcRoutine;
PVOID EncodedSystemArgument1 = Arguments->SystemArgument1;
//
// - Замените ApcRoutine на wow64!Wow64ApcRoutine
// // - Кодируем реальный адрес в SystemArgument1
//
if (Arguments->ApcRoutine != NULL){
EncodedApcRoutine = Wow64ApcRoutine;
EncodedSystemArgument1 = (PVOID)(Arguments->ApcRoutine << 32 | Arguments-...
>SystemArgument1);
}
return NtQueueApcThread(
Arguments->ThreadHandle,
EncodedApcRoutine,
EncodedSystemArgument1,
Arguments->SystemArgument2,
Arguments->SystemArgument3
);
}

Итак, давайте проанализируем эту процедуру: Во-первых, мы видим
WOW64_NT_QUEUE_APC_THREAD_ARGS. Это структура, которая представляет
исходные аргументы, которые были переданы 32-битному NtQueueApcThread. 32-битный stdcall
передают параметры на стеке. Указатель "Arguments" на самом деле является
указатель на стек, содержащий исходные аргументы вызова 32-битной NTDLL.
7/18
Итак, как же эта функция решает проблему? Как вы можете видеть, если Arguments->ApcRoutine является
не NULL, она заменяет ApcRoutine на Wow64ApcRoutine. Wow64ApcRoutine - это
функция внутри wow64.dll, которая подготавливает аргументы для процедуры APC и обрабатывает
переход в 32-битный режим Wow64. Но подождите, а как же оригинальная ApcRoutine? На сайте
разработчики воспользовались большим размером указателей (8 байт вместо 4) и
закодировали оригинальную процедуру APC в более высоком DWORD SystemArgument1.

Кстати, если вы задавались вопросом, почему ContextRecord передается в качестве аргумента для
ApcRoutine, то это для того, чтобы Wow64ApcRoutine могла восстановить этот контекст позже. Wow64ApcRoutine
не возвращается в 64-битный KiUserApcDispatcher.

Другая функция под названием PsWrapApcWow64Thread была добавлена в ядро windows, чтобы позволить
драйверам выполнять это кодирование:

//
// Эта функция оборачивает ApcContext и ApcRoutine для процессов Wow64.
// Эта функция не так полезна. Она работает только тогда, когда текущий процесс является
Wow64 процессом.
// Если текущий процесс является 64-битным процессом, а целевой процесс является Wow64
процесс, эта функция не работает.
//
NTSTATUS
PsWrapApcWow64Thread(
__inout PVOID* ApcContext,
__inout PVOID* ApcRoutine
);

Это краткое изложение старого потока:

1. Приложение Wow64 вызывает 32-битный ntdll NtQueueApcThread.
2. 32-битный NtQueueApcThread вызывает процедуру wow64, которая изменяет режим на 64
бит.
3. Wow64.dll вызывает обертку для системных вызовов. В данном случае это whNtQueueApcThread.
4. whNtQueueApcThread заменяет ApcRoutine на Wow64ApcRoutine и кодирует
исходную программу в SystemArgument1.
5. whNtQueueApcThread вызывает настоящий системный вызов NtQueueApcThread.
6. Когда APC доставлен, KiUserApcDispatcher вызывает процедуру APC
(Wow64ApcRoutine в данном случае).
7. Wow64ApcRoutine обрабатывает переход в 32-битный режим и вызывает 32-битную ntdll
KiUserApcDispatcher.
8. 32-битный KiUserApcDispatcher обрабатывает вызов закодированной ApcRoutine.

Важным моментом в этом вопросе является то, что если вы разрабатываете программу для перехвата или мониторинга
программное обеспечение и вы подключаете системные вызовы, вы должны убедиться, что декодируете Wow64 APC
правильно.

Это дамп стека вызовов. На самом деле он сделан на windows 10, потому что, к сожалению, у меня нет
у меня нет виртуальной машины vista, но стек вызовов должен быть очень похож на Vista.


Wow64 QueueUserAPC Callstack


RetAddr Call Site
00007ffc`4d70545a ntdll!NtQueueApcThread
00007ffc`4d6f7123 wow64!whNtQueueApcThread+0x2a -> The NtQueueApcThread wrapper.
00000000`77ae1783 wow64!Wow64SystemServiceEx+0x153
00000000`77ae1199 wow64cpu!ServiceNoTurbo+0xb
758122ff ntdll_77af0000!NtQueueApcThread+0xc --> Invoke wow64 to change to 64 bit.
00652537 KERNELBASE!QueueUserAPC+0x4f
00652d53 32bitApc!main+0x47 --> The main routine of our app.
00652ba7 32bitApc!invoke_main+0x33
00652a3d 32bitApc!__scrt_common_main_seh+0x157
00652dd8 32bitApc!__scrt_common_main+0xd
75f76359 32bitApc!mainCRTStartup+0x8
77b57c24 KERNEL32!BaseThreadInitThunk+0x19
77b57bf4 ntdll_77af0000!__RtlUserThreadStart+0x2f
00000000 ntdll_77af0000!_RtlUserThreadStart+0x1b --> the 32 bit ntdll is executed
now.
00007ffc`4d6fc77a wow64cpu!BTCpuSimulate+0x9 --> At this point, we change to 32 bit
mode to start
00007ffc`4d6fc637 wow64!RunCpuSimulation+0xa executing the StartAddress of the
thread.
00007ffc`4e1f3f73 wow64!Wow64LdrpInitialize+0x127
00007ffc`4e1e1d75 ntdll!LdrpInitializeProcess+0x186b
00007ffc`4e1917d3 ntdll!_LdrpInitialize+0x50589
00007ffc`4e19177e ntdll!LdrpInitialize+0x3b
00000000`00000000 ntdll!LdrInitializeThunk+0xe --> Beginning of thread execution.

Современное решение: От Windows 7

Разработчики ОС были не в восторге от такой реализации. У меня есть теория о том, почему
они изменили реализацию, но я не совсем в этом уверен. Основными недостатками
этой схемы являются:

1. И ApcRoutine, и SystemArgument1 должны были быть изменены в случае wow64 APC.
2. Поскольку адрес Wow64ApcRoutine берется из исходного процесса, это означает, что
wow64.dll должна быть размещена по тому же адресу глобально.
3. Чтобы декодировать значение, нам нужно получить адрес Wow64ApcRoutine, чтобы мы могли проверить.
является ли это значение закодированным или нет.

wow64.dll - известная DLL, поэтому практически она отображается по одному и тому же адресу глобально, но, возможно.
это ограничение, которое разработчики ОС не хотели иметь. Помните, что это всего лишь теория,
могут быть и другие причины, хотя я не смог придумать других различий между схемы кодирования.

Реализация была изменена под Windows 7. Основные функции, которые были изменены
это whNtQueueApcThread и KiUserApcDispatcher:

//
// Подпрограммы кодирования ApcRoutine. Они не являются настоящими функциями, поэтому я помечаю их как
FORCEINLINE.
//
FORCEINLINE
ULONG64
DecodeWow64ApcRoutine(
ULONG64 ApcRoutine
)
{
return (ULONG64)(-((INT64)ApcRoutine >> 2));
}
FORCEINLINE
ULONG64
EncodeWow64ApcRoutine(
ULONG64 ApcRoutine
)
{
return (ULONG64)((-(INT64)ApcRoutine) << 2);
}
//
// Windows 10
//
NTSTATUS
whNtQueueApcThread(
PWOW64_NT_QUEUE_APC_THREAD_ARGS Аргументы
)
{
PPS_APC_ROUTINE EncodedApcRoutine = \
(PPS_APC_ROUTINE)(EncodeWow64ApcRoutine(Arguments->ApcRoutine))
//
// Кодируем только ApcRoutine
//
return NtQueueApcThread(
Arguments->ThreadHandle,
EncodedApcRoutine,
Arguments->SystemArgument1,
Arguments->SystemArgument2,
Arguments->SystemArgument3
);
}
//
// Windows 7 SP1 64 bit 6.1.7601
//
VOID
KiUserApcDispatcher(
PCONTEXT ContextRecord // rsp
)
{
NTSTATUS Статус;
PPS_APC_ROUTINE ApcRoutine;
PPS_APC_ROUTINE Wow64DecodedApcRoutine;
do {
ApcRoutine = ContextRecord->P4Home;
//
// Попытайтесь декодировать процедуру APC.
//
Wow64DecodedApcRoutine = DecodeWow64ApcRoutine(ApcRoutine);
//
// Если результатом является 32-битный адрес, попробуйте вызвать Wow64ApcRoutine.
//
if (Wow64DecodedApcRoutine <= 0xFFFFFFFF) {
if (Wow64ApcRoutine != NULL) {
PVOID WrappedArgument1 = ((ULONGLONG)Wow64DecodedApcRoutine << 32) |
ContextRecord->P1Home;
Wow64ApcRoutine(
WrappedArgument1,
ContextRecord->P2Home,
ContextRecord->P3Home,
ContextRecord
);
RtlRaiseStatus(STATUS_INVALID_PARAMETER);
}
} else {
//
// Если результат все еще 64-битный адрес, вызовите исходную программу
ApcRoutine
//
ApcRoutine(
ContextRecord->P1Home,
ContextRecord->P2Home,
ContextRecord->P3Home,
ContextRecord
);
}
Status = NtContinue(ContextRecord, TRUE);
} while (Status == STATUS_SUCCESS);
RtlRaiseStatus(Status);
}

Единственным закодированным аргументом является ApcRoutine. Кодировка довольно странная, давайте попробуем
объяснить эту кодировку. Лучший способ понять эту кодировку - посмотреть на ассемблер
код:
whNtQueueApcThread:
...
;
; Кодируем ApcRoutine и отправляем в NtQueueApcThread
;
mov edx, [rcx + _WOW64_NT_QUEUE_APC_THREAD_ARGS.ApcRoutine]
neg rdx
shl rdx, 2
...
KiUserApcDispatcher:
...
;
; Декодируем ApcRoutine
;
mov rcx, [rsp + _CONTEXT.P4Home]
sar rcx, 2
neg rcx
....
;
; Проверьте, является ли ApcRoutine подпрограммой wow64.
; Это делается путем проверки того, что декодированное значение меньше MAX_ULONG
;
shld rcx, rcx, 32
test ecx, ecx
jz short Wow64Apc

Итак, основное предположение этой кодировки следующее: Все пользовательские адреса имеют выключенный знаковый бит. Это
из-за структуры таблиц страниц. Я пытался понять одну вещь: почему бы просто не включить
включить знаковый бит для Wow64 APC и все? Я не смог понять этого, вероятно, нет
специальная причина.

Как на самом деле работает кодирование

Я рекомендую вам пропустить эти детали, если только вам это действительно не нужно. Это довольно раздражает, чтобы
понимать это.

1)Если X - правильный пользовательский адрес, то decode(X) - это 64-битный APC.
 1)1)Это происходит потому, что пользовательские адреса не могут иметь включенный знаковый бит.
 1)2)Инструкция 'sar' обнуляет старшие 2 бита.
 1)3)Инструкция 'neg' заставит эти 2 бита стать 1.
2)Может ли быть случай, когда по ошибке что-то обнуляется?
  Учитывая тот факт, что бит знака должен быть включен, этого не может произойти, потому что
  пользовательские APC должны указывать на действительный адрес пользователя.
3)Если X меньше MAX_ULONG, decode(X) будет 64-битным APC.
 Это вызвано инструкцией neg, которая превращает нули в старших 32 битах в единицы.
 в 1.

64-битная функция для внедрения Wow64

Итак, как кодировка Wow64 влияет на инъекцию?

Что касается инъекции 64 bit -> wow64, мы можем сделать одно из следующих действий:

1. Запустить 64-битный APC-код внутри процесса Wow64. Это поведение по умолчанию.
2. Запустить 32-битный APC-код внутри процесса Wow64, это можно сделать, закодировав файл
ApcRoutine.

Пример кода инъекции DLL ниже. Помните, что APC будет выполняться только тогда, когда целевой
поток будет предупрежден.

Посмотрите на код:

OpenTargetHandles(
Args.TargetProcessId,
Args.TargetThreadId,
&ProcessHandle,
&ThreadHandle
);
if (!IsWow64Process(ProcessHandle, &IsWow64)) {
printf("IsWow64Process Failed. 0x%08X\n", GetLastError());
exit(-1);
}
if (!IsWow64) {
printf("Целевой процесс не является процессом wow64.\n");
exit(-1);
}
//
// Теперь у нас есть 2 варианта:
//
// - Если DLL 32-битная, нам нужно создать Wow64 APC.
// // - Если DLL 64-битная DLL, нам нужно создать обычный APC для ntdll,
// потому что 64-битный kernel32 не загружен.
//
if (Is32bitDll(Args.DllPath)) {
//
// DLL, которую мы хотим загрузить, является 32-битной DLL.
// Сначала нам нужно записать путь к библиотеке в удаленный процесс.
//
RemoteLibraryPath = WriteLibraryNameToRemote(ProcessHandle, Args.DllPath);
//
// Чтобы загрузить эту библиотеку, мы можем использовать 32-битную процедуру LoadLibraryA внутри
kernel32.
//
LoadLibraryAWowAddress = QueryWow64LoadLibraryAddress(ProcessHandle);
//
// Так как APC должен работать в среде Wow64, нам необходимо закодировать
рутину.
//
PPS_APC_ROUTINE ApcRoutine =
(PPS_APC_ROUTINE)EncodeWow64ApcRoutine((ULONG64)LoadLibraryAWowAddress);
//
// Используйте NtQueueApcThread для постановки APC в очередь.
//
Status = NtQueueApcThread(
ThreadHandle,
ApcRoutine,
RemoteLibraryPath,
NULL,
NULL
);
/*
В ntdll есть процедура под названием "RtlQueueApcWow64Thread", которая может быть использована для выполнения
кодирования.
Status = RtlQueueApcWow64Thread(
ThreadHandle,
LoadLibraryAWowAddress,
RemoteLibraryPath,
NULL,
NULL
);
*/
}
else {
//
// DLL является 64-битной DLL, и мы хотим загрузить ее в 32-битный процесс.
// Для этого мы можем использовать ntdll!LdrLoadDll.
//
RemoteLibraryPath = WriteUnicodeLibraryNameToRemote(ProcessHandle, Args.DllPath);
Status = NtQueueApcThread(
ThreadHandle,
(PPS_APC_ROUTINE)LdrLoadDllPtr,
NULL,
0,
RemoteLibraryPath
);
}
if (!NT_SUCCESS(Status)){
printf("NtQueueApcThread Failed. 0x%08X\n", GetLastError());
exit(-1);
}

Full working code is in my apc research repository: https://github.com/repnz/apcresearch/blob/master/x64ToWow64ApcInjector/x64ToWow64ApcInjector.c

Wow64 - 64-битная установка

Проблема с постановкой в очередь 64-битного apc из процесса Wow64 заключается в том, что ApcRoutine не может быть
сохранить внутри 4-байтового указателя. 32-битная программа NtQueueApcThread может получить только 4-байтовый указатель.
байт указатель. Для некоторых системных вызовов есть решение: 32-битная NTDLL экспортирует
следующие функции:
NTSTATUS
NTAPI
NtWow64ReadVirtualMemory64(
HANDLE ProcessHandle,
PVOID64 BaseAddress,
PVOID BufferData,
ULONG64 BufferLength,
PULONG64 ReturnLength
);
NTSTATUS
NTAPI
NtWow64WriteVirtualMemory64(
HANDLE ProcessHandle,
PVOID64 BaseAddress,
PVOID BufferData,
ULONG64 BufferLength,
PULONG64 ReturnLength
);
NTSTATUS
NTAPI
NtWow64AllocateVirtualMemory64(
HANDLE ProcessHandle,
PVOID64* BaseAddress,
ULONG64 ZeroBits,
PULONG64 RegionSize,
ULONG AllocationType,
ULONG Protect
);
......
......

Эти функции позволяют выполнять операции над 64-битным процессом из 32-битного процесса.
Они не являются системными вызовами, эти функции вызывают обертки в wow64.dll, которые выполняют
фактический системный вызов. К сожалению, для NtQueueApcThread такой обертки нет. Если мы
хотим вызвать NtQueueApcThread с 64-битным указателем, нам нужно переключиться в 64-битный
режим и вызвать системный вызов. Для этого мы можем выполнить дальний переход и изменить
сегмент CS так, чтобы режим процессора был 64-битным ("Heaven's Gate"). Пример кода можно
показан ниже:
__declspec(align(16))
typedef struct _NT_QUEUE_APC_THREAD_ARGS {
DWORD64 ThreadHandle;
DWORD64 ApcRoutine;
DWORD64 SystemArgument1;
DWORD64 SystemArgument2;
DWORD64 SystemArgument3;
} NT_QUEUE_APC_THREAD_ARGS, *PNT_QUEUE_APC_THREAD_ARGS;
OpenTargetHandles(Args.TargetProcessId, Args.TargetThreadId, &ProcessHandle,
&ThreadHandle);
//
// Запишите путь DLL к удаленному процессу.
//
RemoteLibraryPath = WriteUnicodeLibraryNameToRemote(ProcessHandle, Args.DllPath);
//
// Сохраните 64-битные аргументы в struct.
//
QueueApcArgs.ThreadHandle = (DWORD64)ThreadHandle;
QueueApcArgs.ApcRoutine = x64_GetNtdllProcedure("LdrLoadDll");
QueueApcArgs.SystemArgument1 = 0;
QueueApcArgs.SystemArgument2 = 0;
QueueApcArgs.SystemArgument3 = (DWORD64)RemoteLibraryPath;
//
// Найдите адрес подпрограммы системного вызова в NTDLL
//
NtQueueApcThreadAddress = x64_GetSyscallAddress("NtQueueApcThread");
//
// Перейдите в 64-битный режим и вызовите системный вызов.
//
Status = x64_InvokeSyscall(NtQueueApcThreadAddress, &QueueApcArgs,
sizeof(QueueApcArgs));

Чтобы понять больше о реализации, прочитайте код:
https://github.com/repnz/apcresearch/blob/master/Wow64To64bitInjector/Wow64To64bitInjector.c

Валидация программы Wow64 ApcRoutine
NtQueueApcThreadEx2:
....
//
// Убедитесь, что процесс Wow64 не пытается встать в очередь в 64-битный процесс с 32-битным
битным адресом.
// Возможно, это ошибка программирования в программе Wow64.
//
if (PsIsWow64Process(SourceProcess) && PsIs64BitProcess((PEPROCESS)TargetThread-?
>Process)) {
if (DecodeWow64ApcRoutine(ApcRoutine) <= 0xFFFFFFFF) {
Status = STATUS_INVALID_HANDLE;
geto Cleanup;
}
}
....

Подробнее о KiUserApcDispatcher

У KiUserApcDispatcher больше обязанностей:

1. CFG: Убедиться, что ApcRoutine является допустимой целью косвенного вызова.
2. Обработка исключений: Обернуть ApcRoutine обработчиком исключений.


Сводка

Основными выводами из этих статей являются:

-KiUserApcDispatcher является точкой входа APC в пользовательском режиме.
-В случае Wow64, APC кодируется таким образом, чтобы KiUserApcDispatcher мог обнаружить и
 передать выполнение в среду Wow64.
-Если вы разрабатываете программное обеспечение для хукинга, убедитесь, что вы правильно декодируете Wow64 apc.
-Когда мы инжектируем в процесс Wow64, мы можем выбрать, хотим ли мы поставить в очередь 64-битный
 или 32-битный целевой код.
-Если мы хотим выполнить очередь из процесса Wow64 в 64-битный процесс, нам нужно переключиться в
 длинный режим и вызвать системный вызов напрямую.

Мы закончили наше приключение в пользовательских APC, в следующих постах мы рассмотрим APC ядра. Надеюсь, это
это было не слишком долго. Мы вернемся к пользовательским APC, когда будем говорить о специальных пользовательских APC.