26 июля 2011 г.

Spy: Windows CE API-перехватчик

В рамках исследования ПО RoadRover появилась необходимость перехватить обмен информацией между программной и аппаратной частями (модуль радио, bluetooth и т.п.). Как выяснилось в процессе анализа исполняемых файлов, общение с оборудованием происходит через COM-порты. Соответственно, для этого вызываются функции CreateFile, WriteFile и ReadFile. Вызовы этих функций и решено перехватить для получения протокола общения. 

Поиск по Интернету навел на интересную статью Spy: A Windows CE API Interceptor, перевод которой я привожу ниже. Прошу прощения за некоторую корявость перевода, это явно не мой конек.

 К сожалению, описанный в статье способ не работает на WinCE 6.0, поэтому в следующем посте я расскажу о другом методе перехвата вызова API - Shim DLL.

Spy: Windows CE API-перехватчик

Отладка программного обеспечения на Windows CE является более сложным процессом, чем отладка на десктопных версиях Windows, в частности, из-за ограниченного характера самих встраиваемых устройств. Так же играет роль отсутствие базовых средств и методов отладки для Windows CE. Такие мощные инструменты, как API-шпионы, мониторы файлов, мониторы реестра и мониторы отладочных сообщений, необходимые при разработке на десктопных версиях Windows, просто, не доступны для CE.

Поэтому, в данной статье, я представляю инструмент "Spy” - API-шпиона для Windows CE. Приведенный исходный код включает в себя движок шпиона и пример его использования, который перехватывает три функции системы: CreateProcess, CreateFile и LoadLibrary. Конечно, Вы можете изменить код, чтобы шпионить за сотней других вызовов API, например, для мониторинга доступа к реестру, синхронизации потоков, оконных сообщений, GDI процедур, и т. п.
(Также мною разработаны несколько платных мониторов, например, Registry Monitor и Thread Monitor; см. http://www.forwardlab.com/).

Обратите внимание, что Spy использует недокументированные особенности и внутренние структуры данных CE, что может привести к нежелательным поведениям системы. Тем не менее, Spy является ценным инструментом для отладки, устранения неполадок и исследования системы. Он был проверен на Windows CE 3.0 и 4.x (на платформе CEPC), эмуляторах Pocket PC 2002 и 4.x, а так же на нескольких имеющихся в наличии КПК Pocket PC 2000 и 2002.

Архитектура CE

Microsoft eMbedded Visual Tools 3.0 предоставляет среду разработки для создания приложений для Windows CE устройств, включая Pocket PC и Smartphone. Набор инструментов включает Embedded Visual C + + 3.0, Embedded Visual Basic 3.0 и SDK для Pocket PC 2002 и Smartphone 2002 (см. http://msdn.microsoft.com/vstudio/device/embedded/default.aspx). Для разработки на Windows CE NET 4.x обычно используется Embedded Visual Tools 4.0 совместно с NET Standard SDK.

SDK содержит файл справки и заголовочные файлы, которые документируют СЕ-подмножество Win32 API. Однако, файл справки SDK содержит ограниченное описание CE архитектуры, например, глава "Processes" (в разделе "Working with Processes and Thread") кратко описывает 33 виртуальных слота памяти, которые содержат до 32 процессов, а слот 0 используется для отображения текущего процесса. Но SDK не объясняет следствия слот-ориентированной модели с точки зрения маппинга указателей, межпроцессного доступа к памяти и межпроцессных вызовов.

С Platform Builder (IDE от Microsoft для построения компактных встраиваемых устройств на основе Windows CE. NET) среда разработки становится богаче, и включает в себя общий исходный код ядра CE. Отладочная версия ОС, которая может быть построена для PC-based CEPC, позволяет использовать отладчик и трассировщик ядра, а не маломощный отладчик приложений из Embedded Visual Tools.

В главе "Kernel" (из раздела "Core OS Services" в файле помощи Platform Builder) схематически изображена архитектура ОС, а в главе "Kernel Functions" перечислен ряд API-процедур (MapPtrToProcess, SetProcPermissions, SetKMode, GetCallerProcess и GetOwnerProcess) для работы с процессами и слотами памяти. Вы также можете найти эту информацию в MSDN Online. Согласно описанию GetCallerProcess, в CE потоки исполнения (threads) могут мигрировать между процессами при выполнении вызовов API, что является большим отличием от десктопных версий Windows, где потоки заключены в рамках одного процесса. Таким образом, старая функция GetCurrentProcessId дополнена новыми GetCallerProcess и GetOwnerProcess. Кроме того, поскольку API-вызовы мигрируют между процессами, возникает потребность в доступе к памяти других процессов. Процедура SetProcPermissions дает текущему потоку доступ к любому процессу. Как правило, разработчику нет необходимости беспокоиться об этом, т.к. этим автоматически занимается система. С другой стороны, из-за взаимодействия процессов и слотов могут появляться ошибки, которые трудно обнаружить. Например, процедура драйвера передает указатель из одного потока в другой, и это приводит к аварийному завершению второго потока, т.к. он не имеет доступа к адресному пространству приложения вызвавшему процедуру в первом потоке. Приложения могут использовать SetProcPermissions для предоставления совместного доступа к памяти, например, при передаче указателя в окном сообщении, но это недокументированный подход. Как объясняется в главе "Drivers and Pointer Data", в драйверах иногда требуется вызов MapPtrToProcess для преобразования указателя из слота 0 в соответствующий слот вызывающего приложения.

Хотя в документации к Platform Builder и содержится информация о кросс-платформенных вызовах, правах доступа, API-ловушках, и маппингах указателей, она по-прежнему поверхностна. Там не объясняется слот-ориентированный дизайн, почему текущий процесс всегда отображается в слот 0, как работают различные API системы и как их использовать. IsAPIReady является единственной документированной функцией для работы с API. Она часто вызывается драйверами чтобы убедиться, что конкретный системный API доступен.

К счастью, в главе 3 “Inside Microsoft Windows CE” Джона Мюррей (John Murray) (Microsoft Press, 1998), разработчики ядра CE объясняют проектные решения и наиболее важные особенности. Например, слот 0 используется для оптимизации загрузки DLL. Когда CE нагружает DLL, она оставляет место для неё в каждом слоте (см. Рисунок 1 ). Секции кода и read-only данных одной DLL загружаемой несколькими процессами, отображаются в одном физическом пространстве. Для доступных для записи секций данных предоставляется одна и та же адресная область в каждом слоте, но разные физические страницы. Так как код обычно имеет встроенные указатели на данные и разрешена только одна копия секции кода, эти указатели не могут меняться при переключении между процессами, но каждый процесс должен получить доступ к его собственным данным. Это достигается за счет направления всех указателей на слот 0, где отображается текущий процесс. Поэтому, когда выполняется код DLL, он автоматически обращается к частным данным текущего процесса. Такие указатели на слот 0 называются "unmapped". Доступ к этим же данным (и коду) может также быть получен путем маппинга (отображения) указателей на конкретный слот данного процесса при помощи процедуры MapPtrToProcess. Это необходимо при передаче указателя в другой процесс, например, как аргумент API или оконного сообщения. Отображенные (маппированные) адреса доступны любому потоку (в любом процессе), который имеет соответствующие права доступа. Права доступа (разрешения) - это 32-битная маска с установленными битами для каждого слота, который должен быть доступен.

Рисунок 1: Слот-модель виртуальной памяти.


Процедуры GetProcPermissions и SetProcPermissions позволяют манипулировать маской разрешений текущего потока. Инструмент Remote Process Viewer показывает адреса слотов запущенных процессов и права доступа потоков. В колонке Access Key на панели Process отображается бит разрешения назначеный слоту конкретного процесса. В одноименной колонке Access Key на панели Thread отображается маска разрешений каждого потока. Можно заметить, что большинство потоков имеют как минимум два установленных бита маски: один - для процесса ядра (nk.exe), другой - соответствующий процессу-владельцу. Могут быть установлены и другие биты для предоставления дополнительного доступа.

Наконец, книга Мюррея проливает свет на механизм системного API, который был разработан в качестве эффективной версии модели клиент-сервер. В отличие от традиционной модели клиент-сервер, при которой сервер работает в отдельном потоке, Windows CE заимствует вызывающий поток и помещает его в серверный процесс. Такие серверы называются "protected server libraries" (PSLs). Сервисы экспортируются в виде DLL, путем регистрации конфигурации API (API set), которая представляет собой таблицу указателей на методы (функции сервера). Кроме этого конфигурация API имеет название, таблицу сигнатур методов, хендл сервера, и dispatch type. Сигнатуры представляют собой 32-битовые маски, где отдельные биты обозначают что соответствующий аргумент метода является указателем. Когда происходит вызовов API, система маппирует аргументы-указатели из слота 0 в слот вызывающего процесса.

Однако, чтобы понять, как создать и зарегистрировать API-конфигурацию, как перечислить имеющиеся API, как найти метод конкретного API, или как перехватить API для мониторинга, необходимо изучить заголовочные файлы и общий исходный код, который поставляется со средой разработки CE. Далее я именую директории относительно корня исходников WinCE. Заголовочный файл PUBLIC\COMMON\OAK\INC\pkfuncs.h определяет некоторые структуры данных ядра CE и недокументированные процедуры, такие как CreateAPISet, RegisterAPISet, QueryAPISetID, GetAPIAddress, LocalAllocInProcess, PerformCallBack4 и GetRomFileInfo. Особенно полезной является процедура PerformCallBack4, поскольку она позволяет напрямую вызывать процедуры в другом процессе без регистрации API.

KDataStruct является важной структурой ядра, которая доступна приложениям по фиксированному адресу PUserKData. Значение PUserKData для процессоров ARM - 0xFFFFC800, для других процессоров - 0x00005800 (см. kfuncs.h из CE SDK). Встроенные (inline) версии Win32 процедур GetCurrentThreadId и GetCurrentProcessId используют фиксированное смещение SYSHANDLE_OFFSET структуры PUserKData для непосредственного доступа к данным. Дополнительные смещения определены в PUBLIC\COMMON\OAK\INC\pkfuncs.h. Смещение KINFO_OFFSET указывает на массив UserKInfo, который содержит важные системные данные, такие как список модулей, куча ядра, и указатель на таблицу API-конфигураций (SystemAPISets). Существуют два различных типа API-конфигураций: implicit и handle based. Implicit API-конфигурации являются глобальными, их методы могут быть вызваны с помощью пары значений - индекс API-хендла и индекс метода. Implicit API-конфигурации (представленные ​​структурой CINFO) зарегистрированы в глобальной таблице SystemAPISets. С другой стороны, handle-based  API связаны с объектами ядра, такими как файлы, семафоры, события и т.п. Методы в этих API, можно назвать с помощью хендла объекта и индекса метода. В pаголовочном файле SDK kfuncs.h определены индексы хендлов для неявных API-конфигураций, таких как SH_WIN32, SH_GDI, SH_WMGR и др. Индексы handle-based API, таких как HT_EVENT, HT_APISET и HT_SOCKET, определены в файле PUBLIC\COMMON\OAK\INC\psyscall.h. Одновременно может быть зарегистрировано не более 32 API-конфигурации и почти все из них посоянно используются. Вышеописанные внутренние структуры данных изображены на рисуке 2. Для получения дополнительной информации обратитесь к каталогу PRIVATE\WINCEOS\COREOS\NK\KERNEL.


Рисунок 2: Пример структур данных ядра Windows CE.

В файле Psyscall.h объявлены макросы METHOD_CALL и IMPLICIT_CALL, которые разворачиваются в специальные адреса используемые клиентами для вызова API методов. Эти адреса называются "ловушки" (“traps”), они обрабатываются диспетчером исключений ядра CE. Многие разработчики приложений CE видели эти странные адреса (начинающиеся с 0xF) при входе отладчиком в процедуры Win32 API. Часто бывает необходимо войти в процедуру API или изучить её код в окне отладчика если вызов API завершился аварийно или с ошибкой. К сожалению, почти все Win32 процедуры представляют собой тонкую обертку вокруг ловушки в системном API. Так как отладчики приложений не позволяют входить в эти ловушки, а отладчик ядра не доступен для большинства разработчиков, отладка ошибок связанных с Win32 API на CE является сложной задачей. Однако, изучив макрос IMPLICIT_CALL и таблицу SystemAPISets, можно раскрутить ловушку адреса и найти реальные обработчики методов API. Формула ((0xFFFFFE00-TRAP)/2),x или ((0xF0010000-TRAP)/4),x на процессорах ARM позволяет получить индекс метода (младший байт) и индекс API (второй байт). Идекс API используется для получения указателя на CINFO из SystemAPISets, а индекс метода - для получения адреса метода из таблицы методов. Кроме того, Вы можете использовать метод DumpApis() из CeApiSpy (в электронном виде) для получения дампа всех зарегистрированных API и их методов.

Проектирование Spy

Как видно на Рис.2, единственным способом перехвата процедуры API является замена указателя на API-конфигурацию в таблице SystemAPISets. Есть ряд других привлекательных указателей, но все они находятся в read-only секциях и могут находиться в ROM. Поэтому, Spy создает копию оригинальной структуры CINFO (также таблицу методов и таблицу сигнатур) и заменяет адрес оригинального API в таблице SystemAPISets на адрес нового CINFO. Далее Spy заменяет некоторые указатели на конкретные методы в скопированной таблице указателями на свои процедуры-перехватчики. Процедуры-перехватчики вызывают соответствующие методы из исходной таблицы методов чтобы система оставалась в рабочем состоянии. Spy также регистрирует свой API для того, чтобы было возможно вызывать процедуры Spy из других процессов. Текущая версия Spy может перехватить только глобальные API. На рисунке 3 показан пример перехвата процедуры API.

Рисунок 3: Перехват процедуры API


При проектировании Spy возникают сложности связанные с  обеспечением доступности перехватчиков API из вызывающих процессов и достижения согласованности в процессе и при переключении режима. Некоторые API методы используют GetCallerProcess (например, для проверки владения ресурсом), другие методы проверяют в каком режиме (пользовательском или привилегированном) работает вызывающий код. Spy не должен изменять результаты этих проверок. Для этого часть Spy отвечающая за перехват расположена в DLL, которая загружается во все запущенные процессы. Таким образом процедуры-перехватчики выполняются в том же процессе, что и исходный сервер API. CE 4.0 имеет документированный способ внедрения DLL во все процессы с помощью значения реестра InjectDLL. Но CE 3.0 не поддерживает этот способ, поэтому Spy использует недокументированную функцию PerformCallBack4 для того чтобы выполнить LoadLibrary в других процессах.

Последнее препятствие заключается в кэшированнии указателей на API-таблицы внутри Coredll. Похоже, что Coredll (провайдер Win32 API) с целью оптимизации получает (с помощью недокументированной функции GetRomFileInfo) указатели на  таблицы методов API ядра Win32 и вызывает непосредственно эти методы вместо того чтобы использовать ловушки. Поэтому, Spy должен просканировать Coredll для нахождения и замены этих кэшированных указателей. Это должно быть сделано для каждого запущенного процесса и для вновь запускаемых процессов. Для уведомления о запуске новых процессов Spy имеет перехватчик GetRomFileInfo.


Реализация Spy

Workspace-файл CeApiSpy.vcw содержит проекты CeApiSpy.vcp (для сборки GUI - CeApiSpy.exe) и CeApiSpyDll.vcp (для сборки DLL перехватчика - CeApiSpyDll.dll,). Я тестировал сборку в Embedded Visual C++ 3.0 с использованием Pocket PC 2002 SDK, и CE 4.0 используя стандартное SDK. Вы должны выбрать соответствующую конфигурацию сборки или создать новую конфигурацию для других SDK. Для сборки Spy не требуется Platform Builder, но его отладчик ядра может быть полезным. Чтобы исключить зависимость от системных заголовков, я собрал недокументированные функции, макросы и декларации структур данных в SysDecls.h.

Наиболее важный файл - это SpyEngine.cpp. Первая часть этого файла содержит функции заменяющие ToolHelp API (отсутствующие в версии CE 4.x). Функция HookCoredll ищет в Coredll указателии на закэшированные системные таблицы и заменяет их. Функция CallCoredllInProc вызывает экспортируемые методы из Coredll в любом процессе (с использованием PerformCallBack4). Функции AllocateMemInKernelProc и FreeMemInKernelProc соответственно выделяет и освобождает память в процессе №1 (nk.exe). Эта память используется для создания дубликата таблиц API-функций, которые т.о. доступны для всех процессов. Функции LoadHookDllIntoProcess и UnloadHookDllInProcess соотвественно загружет и выгружет DLL Spy в любом процессе, а функция LoadHookDllInAllProcesses загружает эту DLL во все запущенные процессы. Функция CreateDuplicateApi вызывает процедуры AllocateMemInKernelProc и CreateDuplicateMethodTable для создания копии структуры CINFO, а также таблиц методов и сигнатур системного API. и далее записывает адрес нового CINFO в SystemAPISets. HookMethod вызывается в кастомных мониторах для перехвата вызовов конкретного API. HookGetRomFileInfo является перехватчиком для недокументированной процедуры GetRomFileInfo, которая перехватывается всегда чтобы Spy получал уведомления о запуске новых процессов и замены указателей на системные таблицы API, когда они передаются из Coredll. StartSpy и StopSpy - экспортируемые методы Spy DLL, которые используются графический интерфейсом (GUI) для инициализации и деинициализации Spy DLL. StartSpy вызывает рабочую процедуру DoStart. DllMain выполняет необходимые инициализации внутри каждого процесса. DumpApis не используется для слежения, а просто создает дамп информации о зарегистрированных API.

Файл Interceptors.cpp содержит конкретные перехватчики и может быть изменен, чтобы перехватить дополнительные вызовы API. InstallInterceptors вызывается из DoStart и содержит вызовы HookMethod для перехваемых методов. Все процедуры-перехватчики в Interceptors.cpp имеют одинаковый шаблон. Они выводят информацию о вызове и его аргументах и вызывают оригинальный метод API через указатель, полученный ранее из HookMethod. Чтобы добавить новый перехватчик, скопируйте один из существующих процедур-перехватчиков, замените её имя и аргументы, объявите новый указатель на оригинальный метод и добавьте вызов в HookMethod.

Для вывода и отображения результатов слежения я использовал фреймворк трассировки (HTrace.cpp) представленный в моей статье "Эффективная и гибкая техника трассировки" ("An Efficient and Flexible Tracing Technique", C/C++ Users Journal, April 2002), котораый выводит трассировку в буфер разделяемой памяти так же, как отладочный монитор. Файл TrcView.cpp реализует окно, в котором отображается текст из буфера. Отображение обновляется по таймеру и не связано с процедурами-перехватчиками для снижения накладных расходов и вмешательства в систему.

Запуск Spy

Для проверки Spy можно использовать проект MissingDll (прилагается к исходникам). Он демонстрирует, как использовать Spy для отладки сбоя загрузки приложения из-за отсутствия необходимой DLL. Нужно построить проекты Spy и MissingDll и скопировать файлы CeApiSpy.exe, CeApiSpyDll.dll и MissingDllApp.exe на устройство. Запустить CeApiSpy.exe и вызывать команду Start, а затем запустить MissingDllApp.exe. Система должна отображать сообщение:
“Cannot find MissingDllApp (or one of its components). Make sure the path and filename are correct and that all the required libraries are available.“

Теперь можно посмотреть результаты трассировки в окне Spy или сохранить их в файл. Ниже показан отрывок трассировки, который показывает неудачную попытку загрузить недостающие DLL.


->HookCreateProcessW(95f52:\Windows\Start Menu\MissingDllApp.exe,95fa0:)
HookCreateFileW(a095f52:\Windows\Start Menu\MissingDllApp.exe,80000000,1,0) ret 81bfa896 err 183
Process a1bee51a #13 started
HookCreateFileW(215f530:\HckMap.txt,c0000000,0,80000080) ret ffffffff err 32
HookCreateFileW(81072d9c:\Windows\Start Menu\MissingDll.dll,80000000,1,0) ret ffffffff err 2
HookCreateFileW(81072b88:\Windows\MissingDll.dll,80000000,1,0) ret ffffffff err 2
HookCreateFileW(81072b88:\MissingDll.dll,80000000,1,0) ret ffffffff err 2
HookCreateFileW(81072d9c:\Windows\Start Menu\MissingDll.dll.dll,80000000,1,0) ret ffffffff err 2
HookCreateFileW(81072b88:\Windows\MissingDll.dll.dll,80000000,1,0) ret ffffffff err 2
HookCreateFileW(81072b88:\MissingDll.dll.dll,80000000,1,0) ret ffffffff err 2
<-HookCreateProcessW(95f52:\Windows\Start Menu\MissingDllApp.exe,95fa0:)ret 0 err 126
 

Для устранения ошибок внутри самого Spy можно включить подробную трассировку в меню Options.

Заключение

Spy будет полезен в решении общих проблем при отладке приложений и драйверов. Однако, поскольку он базируется на недокументированных API и структурах данных ядра, подход может стать нерабочим в будущих версиях Windows CE (ага, так оно и случилось в CE 6.0). Но пока этого не произошло, Вы можете использовать Spy для ускорения отладки, поиска неисправностей и изучения поведения системы.

DDJ

Комментариев нет:

Отправить комментарий