EreTIk's Box » Cтатьи, исходники » Соглашение о вызовах на x64-платформе и обход проверки цифровой подписи модуля при установке OB-callback'а


Платформа x64 (здесь и далее имеется ввиду AMD64) стала достаточно распространенной. Это принесло разработчикам уровня ядра (user mod'ным тоже, но в меньшей степени) множество сюрпризов. В частности, изменилось соглашение о вызове функций. Как видно из документации, первые четыре параметра передаются в регистрах. То есть теперь, набрав команду отладчика просмотра стека вызовов с аргументами (kb), определить параметры вызывающих функций становится нетривиальной задачей, требующей дизассемблирования тел этих функций.


Но передача параметров в регистрах дает очень широкий простор для построения и поиска detour-кода. Под detour-кодом здесь я подразумеваю некоторый кусок кода, который будет расположен в "чужом" (внешнем по отношению к нашему) модуле, который может быть использован для сокрытия обработчика или обхода некоторых проверок. Рассмотрим описание функции регистрации функции обратного вызова при операциях над описателями - ObRegisterCallbacks(...). В конце MSDN'овской статьи недвусмысленно сказано, что если указанный обработчик расположен в неподписанном модуле, то функция вернет STATUS_ACCESS_DENIED. Допустим, что загруженному неподписанному драйверу на x64 системе (методы загрузки в обход проверок подписи публично описаны и используются, например, в boot-kit'ах) нужно "фильтровать" создание описателей объектов. Классически, обход реализуется записью небольшого detour-кода в выравнивание одной из секций "чужого" подписанного модуля. Далее я опишу метод, который позволит не использовать запись.


Для начала немного по-дизассемблируем ядро и посмотрим как реализуется проверка. Проследив логику исполнения функции ObRegisterCallbacks(...), можно увидеть, что ошибка STATUS_ACCESS_DENIED будет возвращена, если неэкспортируемая функция nt!MmVerifyCallbackFunction(PVOID CallbackRoutine) вернет FALSE на ненулевой адрес nt!_OB_OPERATION_REGISTRATION.PreOperation или nt!_OB_OPERATION_REGISTRATION.PostOperation:


mov         rcx,[rsi][rbp][010]
cmp         rcx,rbx
jnz        .00000001`404B5C5D
cmp         [rsi][rbp][018],rbx
jz         .00000001`404B5CE8
cmp         rcx,rbx
jz         .00000001`404B5C66
call        MmVerifyCallbackFunction
cmp         eax,ebx
jz         .00000001`404B5CE1
mov         rcx,[rsi][rbp][018]
cmp         rcx,rbx
jz         .00000001`404B5C79
call        MmVerifyCallbackFunction
cmp         eax,ebx
jz         .00000001`404B5CE1

; ...
; 00000001`404B5CE1:
mov         esi,0C0000022
jmps       .00000001`404B5CF5
                

Код функции nt!MmVerifyCallbackFunction(...) достаточно простой. Ключевым для нас местом является вызов nt!MiLookupDataTableEntry(...) и анализ полученного указателя:


call        MiLookupDataTableEntry
cmp         rax,r12
jz         .00000001`4046EF0C
test        b,[rax][068],020
cmovnz      edi,ebp

; ...
mov         eax,edi

; ...
retn
                

Функция nt!MiLookupDataTableEntry(...) проходит неэкспортируемый список загруженных в ядро модулей nt!PsLoadedModuleList. Каждый элемент списка это недокументированная структура nt!_KLDR_DATA_TABLE_ENTRY в которой, помимо прочего, хранятся флаги. Как видно из листинга nt!MiLookupDataTableEntry(...), по смещению 0x68 (для AMD64) хранятся флаги модуля (что соответствует описанию из WRK), а маска 0x20 сигнализирует ядру, что модуль подписан.


Из вышеприведенного можно сделать вывод, что адрес кода, вызывающего ObRegisterCallbacks(...) не подвергают проверке на расположение в подписанном модуле. Так же нет проверки на то, что бы вызывающий код и регистрируемые функции обратного вызова находились в одном модуле. Смещение до флагов в недокументированной структуре nt!_KLDR_DATA_TABLE_ENTRY, как и само значение флага подписанности модуля, не менялось (проверено на линейке ядер VistaSP1 6001 – Win7 SP1 7001). В нашей задаче нам поможет то, что нет необходимости искать неэкспортируемую голову списка загруженных модулей nt!PsLoadedModuleList. На элемент этого списка (nt!_KLDR_DATA_TABLE_ENTRY *) указывает поле nt!_DRIVER_OBJECT.DriverSection. А функция перечисления директории дерева объектов nt!ZwQueryDirectoryObject(...) экспортируется из ядра. С ее помощью, перечислив директории "\Driver" и "\FileSystem", мы найдем большинство загруженных модулей ядра.


Теперь взглянем на прототип регистрируемого обработчика, например ObjectPreCallback(...). Как видно из прототипа, первым параметром функции передается некоторый контекст, который был указан драйвером при регистрации. Таким образом, учитывая что первый параметр на x64 системе передается в регистре rcx, для обхода проверки подписи можно найти некоторый код в подписанном модуле, который передавал управление по значению rcx. Так как размер команд регистровых операций достаточно мал, то вероятность нахождения "готового" detour-кода достаточно велика. Для реализации тестового драйвера, который я назвал x64ModDt, было выбрано 2-е последовательности инструкций, которые передают управление по значению регистра rcx:

  • jmp rcx: FF E1
  • push rcx/ret: 51 C3

Думаю, можно придумать еще несколько последовательностей. Общая суть работы показана на рисунке:


Bypassing sign check for register OB-callback. AMD64 PoC


Драйвер x64ModDt.sys нашел подходящую инструкцию в модуле tcpip.sys. Первым действием x64ModDt.sys регистрирует код из tcpip.sys в качестве функции обратного вызова, указывая в качестве параметра указатель на свой обработчик TestPreCallback(...). Далее, вторым действием, ядро вызывает функцию обратного вызова из tcpip.sys, которая в свою очередь (третьим действием) передает управление функции TestPreCallback(...). Если загрузить демонстрационный драйвер, то можно увидеть, что такой подход работоспособен.


P.S.

Можно реализовать более универсальный подход к обходу проверки цифровой подписи, используя идею Cr4sh'а, описанную в статье "Обход детектирования скрытого исполняемого кода". Суть идеи - выделение и использование памяти на месте выгруженной области PE-образа, в частности discardable-секции PE файла. Такой подход должен работать для любой архитектуры процессора.




Скачать демонстрационный драйвер x64ModDt с исходным кодом.


ΞρεΤΙκ