Правда о KeUserModeCallback() — Архив WASM.RU

Все статьи

Правда о KeUserModeCallback() — Архив WASM.RU

Это даже не статья, а маленькая заметочка. Заметочка на тему избитой всеми функции KeUserModeCallback(). Избитой потому, что многие о ней слышали, многие знают для чего она используется и как примерно работает, но вот рабочего кода, полноценно ее использующего, почти ни кто так и не выдал. Тому есть несколько причин, но обо всем по порядку…

Функция эта умеет вызывать код третьего кольца из режима ядра. Причем делает это быстрее всех других известных методов, таких, например, как APC. И хотя последний является практически единственным документированным, более универсальным решением, KeUserModeCallback() однозначно могла бы занять свою нишу среди используемых в ядре технологий. Даже в целых двух отраслях – это непосредственная передача данных на обработку от драйвера к приложению или просто передача управления юзермодному коду. Надо заметить, в первом случае приложению не требуется ждать от драйвера уведомления о появлении новых данных или постоянно опрашивать его на предмет наличия таковых. Во втором же случае, как я уже говорил выше, обеспечивается максимальная скорость вызова. Но это все плюсы, а есть еще и минусы.

Во-первых, её можно вызывать не всегда и не везде – только в контексте гуи-потока, только на IRQL равном PASSIVE_LEVEL и только если драйвер не был ранее приаттачен ни к одному процессу. В системе вызовы KeUserModeCallback() используются драйвером win32k.sys для быстрого обмена информацией с сервером графической подсистемы, а так же для вызова оконных процедур и процедур хуков, тех, что ставятся с помощью NtUserSetWindowsHookEx/AW. Во-вторых, функция эта официально не документирована Microsoft. Кроме как в проекте Windows Research Kernel, в варезных исходниках Windows, позаимствованных у корпорации, да в дизассемблерном листинге ядра, почерпнуть толковой информации о ней практически негде. На удивление, поиск в Google выдает на первых строчках ссылки, ведущие к абсолютно антисистемной статье, не в обиду ее автору будет сказано, лежащей на wasm.ru, и не имеющей прямого отношения ни к KeUserModeCallback(), ни к вызову пользовательского кода из нулевого кольца. Aquila, каким образом ее пропустила цензура? Короче говоря, пора бы внести в это дело ясность.

Функция экспортируется из модуля ntoskrnl.exe и имеет следующий прототип:

NTSTATUS
KeUserModeCallback (
    IN ULONG ApiNumber,
    IN PVOID InputBuffer,
    IN ULONG InputLength,
    OUT PVOID *OutputBuffer,
    IN PULONG OutputLength
    );
Думаю с четырьмя последними параметрами все ясно, например, InputBuffer указывает на входные данные, которые будут скопированы на стек потока еще до того, как он перейдет в пользовательский режим. InputLength, соответственно, задает размер этих данных в байтах. Неясным поначалу остается первый параметр, который по логике вещей должен был бы быть указателем на исполняемый код. Но нет, наши братья из Microsoft в очередной раз решили не идти легким путем, и сделали вызов через таблицу, имя которой KernelCallbackTable. В ней лежат указатели на некие функции внутри user32.dll, прототип которых мы рассмотрим позже. Параметр ApiNumber есть ни что иное, как индекс в этой таблице. Неужели разработчики хотели таким образом ограничить количество адресов, по которым возможно будет передавать управление? Тогда почему в коде KiUserCallbackDispatcher() – функции, которая первая принимает бразды правления из ядра и делает вызов процедуры по индексу – нет ни единой проверки вхождения индекса в границы KernelCallbackTable? На самом деле сами границы тоже нигде не обозначены, нам ни что не помешает скормить абсолютно левое значение ApiNumber функции KeUserModeCallback() – она спокойно передаст управление в юзермод, а там KiUserCallbackDispatcher() помножит его на размер двойного слова, возьмет по полученному смещению от начала таблицы указатель и произведет вызов. Смотрите сами в код из ntdll.dll:

.text:7C90EAD0 _KiUserCallbackDispatcher@12 proc near
.text:7C90EAD0      add     esp, 4
.text:7C90EAD3      pop     edx				; Это индекс
.text:7C90EAD4      mov     eax, large fs:18h
.text:7C90EADA      mov     eax, [eax+30h]		; Указатель на PEB
.text:7C90EADD      mov     eax, [eax+2Ch]		; На таблицу
.text:7C90EAE0      call    dword ptr [eax+edx*4]; Сам вызов
.text:7C90EAE3      xor     ecx, ecx
.text:7C90EAE5      xor     edx, edx
.text:7C90EAE7      int     2Bh				; Возврат в ядро
.text:7C90EAE9      int     3
.text:7C90EAEA      mov     edi, edi
.text:7C90EAEA _KiUserCallbackDispatcher@12 endp

Впрочем, отсутствие проверки нам только на руку, т.к. не придется лишний раз трогать внутренности системы и перезаписывать элементы KernelCallbackTable своими указателями. Однако знать адрес этого массива нам необходимо. Как видно из листинга, его можно получить из PEB процесса по смещению 0x2C, которое одинаково во всей линейке NT вплоть до Windows Vista. Итак, после вызова KeUserModeCallback() система запоминает указатель на стек текущего потока и сдвигает границу стека вниз на расстояние, необходимое для того, чтобы вместить передаваемые данные, а затем копирует их туда. Далее извлекает из TIB потока и запоминает указатель на последний SEH-фрейм, так как код режима пользователя может установить какие-то свои обработчики исключений. Потом следуют проверки на текущий IRQL и приаттаченность к какому-либо процессу. В случае ошибки генерируется BSOD с кодами IRQL_GT_ZERO_AT_SYSTEM_SERVICE и APC_INDEX_MISMATCH соответственно. Еще раньше BSOD может «сгенерироваться сам», если вдруг окажется, что поток не гуи – поле KTHREAD->TrapFrame в данном случае будет содержать NULL вместо валидного указателя и мы сможем лицезреть синий экран, информирующий о попытке чтения непонятно какой памяти. После производятся подготовительные работы для прыжка в юзермод с помощью KiServiceExit, а на платформе x64 с помощью sysretq. Надо заметить, что в x86 KiServiceExit так же производит проверки, о которых я говорил чуть выше. Ну что же, будем считать, что эти излишние «телодвижения» реализованы для пущей надежности, а не вследствие очередного программерского ляпа. В юзермоде, как было упомянуто ранее, управление получает KiUserCallbackDispatcher() и по индексу вызывает функцию из таблицы. Функция эта должна иметь примерно следующий вид:

ULONG
Ring3Code(
PVOID lpData,
ULONG dwDataSize);

Первым параметром идет указатель на входные данные, вторым их размер. За этими параметрами на стеке незатейливо располагаются переданные из режима ядра байты. Процедура эта выполняется уже в юзермоде! Можем делать тут все, что душа пожелает. Я, к примеру, не стал долго думать и вызвал пресловутый MassageBox. После завершения процедуры возврат происходит обратно в KiUserCallbackDispatcher(), а от туда в ядро с помощью прерывания 0x2B. То, что эта процедура оставит в регистре eax после себя и будет возвращаемым значением KeUserModeCallback().

Для вызова кода режима пользователя нам необходимо выделить память где-нибудь пониже KernelCallbackTable (следует использовать флаг MEM_TOP_DOWN в ZwAllocateVirtualMemory), положить туда указатель на нужный код, вычесть из полученного адреса памяти указатель на начало таблицы и разделить полученное смещение на четыре. Индекс готов для использования.

На этом, пожалуй, я и закончу. Надеюсь, что изложенная здесь информация окажется интересной читателю. Исходники, как всегда, прилагаются.

2002-2013 (c) wasm.ru