Warning (12.08.2016) |
Статья не применима к ядру Windows 10.0.14393. Описанный в конце статьи способ поиска не применим к этой версии ОС. |
Сразу скажу, что под x64-системами, здесь и далее будет идти речь о AMD64-сборках Windows.С 64-х разрядными Itanium-сборками (IA-64) ядра Windows я так тесно не работал, да и Microsoft уже отказалась от этой платформы. Поэтому Itanium-платформа в рамках этой статьи рассматриваться не будет.
С приходом x64-ядер Windows разработчики драйверов, в частности разработчики защит и прочих продуктов, использующих перехваты вызовов системных сервисов, сразу столкнулись с рядом трудностей. Среди этого длинного списка: PatchGuard, Kernel Mode Code Signing (KMCS). Впрочем, сейчас существует достаточно надежные способы как отключения защиты от модификации ядра, так и способы загрузки неподписанного кода в ядро Windows. Хотя количество способов сильно ограничено, но все таки они есть и успешно применяются.
И тут разработчик сталкивается со следующей особенностью x64-систем: переменная nt!KeServiceDescriptorTable больше не экспортируется из ядра ОС. Как известно, nt!KeServiceDescriptorTable является массивом, в котором хранятся описатели таблицы системных вызовов: указатель на начало таблицы, ее размер и еще некоторые служебные поля. На 32-х разрядных системах в таблице системных вызовов хранятся указатели на соответствующие функции-обработчики. И это утверждение неверно для x64-систем, при загруженном ядре в таблице системных вызовов хранятся не указатели, а знаковые 4-х байтные смещения. Но и на этом проблемы не закачиваются, формат хранения данных в таблицах системных вызовов различается на ОС раньше Vista и системах Vista и старше. Поэтому начнем именно с формата таблицы системных вызовов.
Формат данных таблицы системных вызовов SSDT на x64-системах
Вначале рассмотрим формат SSDT для систем младше Vista. Ни для кого не секрет, что ядра Windows XP Professional x64 это не что иное, как ядро Windows 2003 Server x64. В этом легко убедится, если выполнить простую команду в WinDbg:
Номер сборки 3790 всегда был закреплен за 2003-им сервером (напомню, что для XP номер сборки всегда был 2600), о чем нам и говорит WinDbg при соединении с Windows XP Professional x64:
Поэтому сейчас речь пойдет о Windows 2003 Server x64, подразумевая, что на Windows XP Professional x64 дела обстоят аналогично. При рассмотрении формата SSDT будем использовать таблицу системных сервисов ядра, которая расположена в KiServiceTable. Именно на этот символ указывает поле начала таблицы системных сервисов дескриптора с индексом 0 из массива nt!KeServiceDescriptorTable. Итак, откроем файл ntoskrnl.exe и взглянем на содержимое начала таблицы системных сервисов:
Вроде бы все привычно, но вот только немного смущает символ ntoskrnl!NtReadFile+0x5. На самом деле все просто: в значениях таблицы системных сервисов, точнее в младших четырех битах, закодировано количество параметров, передаваемых через стек. У функции ZwReadFile(...) девять параметров, но, учитывая специфику передачи параметров на x64 платформе (Calling Conventions: x64 Architecture: первые четыре параметра передаются через регистры), как раз и получается, что при вызове пять параметров будут переданы в эту функцию через стек.
Теперь посмотрим на тот же символ nt!KiServiceTable, но уже на загруженной системе:
На очень многих форумах по низкоуровневому программированию поднималась примерно одна и та же тема: почему команда WinDbg "dps nt!KiServiceTable" в начале таблицы отображает "мусор"? А все потому, что это не мусор, а, как было написано выше, 4-х байтные смещения. Но вот только количество параметров, закодированное в исходной таблице системных вызовов, никуда не делось. Поэтому младшие четыре бита в каждом 4-х байтном слове таблицы это все то же количество параметров, передаваемых через стек. В этом легко убедиться, выполнив следующую команду:
Аналогично тому, как мы видели в файле ntoskrnl.exe, по индексу 3 лежит смещение до NtReadFile(...). Преобразованием элементов таблицы из 8-ми байтовых указателей а 4-х байтовые смещения занимается цикл в функции nt!KeCompactServiceTable (в первом параметре rcx передан указатель на таблицу, а во втором параметре edx передано количество элементов таблицы):
mov r11d,edx ; количество элементов mov r10,rcx ; начало таблицы описателей mov rdx,rcx ; начальный указатель массива и ULONG’ов, и PVOID’ов test r11d,r11d je nt!KeCompactServiceTable+0x35 mov r9,r11 ; количество сервисов ; ... nt!KeCompactServiceTable+0x20: mov eax,dword ptr [rdx] ; очередной (указатель | кол-во параметров) add rcx,4 ; смещаем указатель ULONG’ов add rdx,8 ; смещаем указатель PVOID’ов sub eax,r10d ; указатель –> смещение от начала таблицы dec r9 mov dword ptr [rcx-4],eax jne nt!KeCompactServiceTable+0x20
Теперь перейдем к системам Windows Vista и старше. Откроем файл ntoskrnl.exe от Windows 7 RTM x64 (7600) и взглянем на KiServiceTable:
То есть на системах Vista и старше в файле элементы таблицы системных сервисов больше не содержат количество параметров функций. Для этого существует, как и на x32, отдельная таблица KiArgumentTable. Естественно, что в ней учтено, что первые 4-е параметра передается через стек, поэтому значения в ней (размер параметров на стеке в байтах, из расчета 4-х байт на параметр) отличаются по содержимому от таблицы в x32-сборке. Например, для того же ntoskrnl.exe из Windows 7 x64:
Но, как и для систем Win2k3, после загрузки ядра массив KiServiceTable преобразуется в 4-х байтные смещения, младшие четыре бита которых содержат количество параметров, передаваемых через стек:
В данном формате смешением являются старшие только 28 бит 4-х байтового слова элемента таблицы (т.е. нужно отбросить младшие 4-е бита). Например, можно вычислить адрес того же NtReadFile'а по индексу 3:
Преобразованием таблицы занимается все та же функция nt!KeCompactServiceTable, но формат ее вызова расширен, ведь в изначальной таблице (той, что записана в файле) не было количества параметров, передаваемых через стек:
nt!KiInitializeKernel+0x358: mov r8d,dword ptr [nt!KiServiceLimit] lea rdx,[nt!KiArgumentTable] lea rcx,[nt!KiServiceTable] call nt!KeCompactServiceTable
Ну и логика преобразования таблицы поменялась, как и поменялся формат содержимого:
mov rdi,rdx mov r10d,r8d mov rbx,rcx mov rdx,rcx test r8d,r8d je nt!KeCompactServiceTable+0x45 mov r11,r10 nt!KeCompactServiceTable+0x1e: mov r8d,dword ptr [rdx] movzx eax,byte ptr [rdi] add rdx,8 sub r8d,ebx ; указатель -> смещение shr eax,2 ; 'элемент KiArgumentTable -> количество параметров inc rdi shl r8d,4 ; перенос значения смещения в старшие 28 бит or r8d,eax ; сохранение количества параметров mov dword ptr [rcx],r8d add rcx,4 sub r11,1 jne nt!KeCompactServiceTable+0x1e
Реализацию разбора содержимого таблицы на языке Python можно посмотреть в примере ~\samples\ssdt.py из проекта PYKD.
Поиск таблицы системных вызовов ядра: nt!KiServiceTable
Таблица системных вызовов ядра (указатель на которую хранится в nt!KeServiceDescriptorTable по нулевому индексу) расположена по адресу символа nt!KiServiceTable, который не экспортируется ни в x32, ни в x64 сборках ядра ОС. В интернете можно найти достаточное количество способов нахождения массива nt!KeServiceDescriptorTable, по данным которого определяется адрес nt!KiServiceTable. Но я хочу рассмотреть способ "эвристического" поиска nt!KiServiceTable на x64-платформе напрямую.
Способ основан на формате SSDT, описанном выше. Фактически, nt!KeCompactServiceTable перетирает первую половину таблицы nt!KiServiceTable смещениями, оставляя вторую половину нетронутой. Это означает, что во второй половине таблицы остаются "настоящие" указатели на Nt-функции ядра. Это можно использовать:
- Выбираем экспортируемую Nt-функцию, которая всегда находится во второй половине таблицы и получаем ее адрес. Я бы рекомендовал выбрать функции с количеством параметров <= 4, что бы при поиске не закладываться на номер билда ОС, например это функция NtSetSecurityObject
- Проходим все not-paged секции PE-модуля ядра
- В каждой секции с шагом в размер указателя сравниваем очередные 8-мь байт с полученным на первом этапе указателем функции. Когда значения совпадут - мы внутри nt!KiServiceTable
- Перед таблицей nt!KiServiceTable всегда (во всяком случае я иного не встречал) есть область padding’а заполненная nop’ами. Следовательно, для поиска начала таблицы нужно идти вверх от NtSetSecurityObject, пока не встретим значение 0x9090909090909090
- Что бы найти конец таблицы (и узнать ее размер) необходимо идти по таблице от NtSetSecurityObject вниз и проверять, что очередные 8-мь байт являются указателем в диапазон одной из секций кода PE-модуля ядра
Метод не самый надежный, но был протестирован на нескольких машинах с разными версиями x64-ОС. Во всех случаях таблица nt!KiServiceTable была найдена успешно.
ΞρεΤΙκ