ПРОГРАММИРОВАНИЕ    НА    V I S U A L   C + +
РАССЫЛКА САЙТА       
RSDN.RU  

    Выпуск No. 65 от 24 февраля 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Здравствуйте, дорогие подписчики!


 CТАТЬЯ

Взаимодействие .NET с неуправляемым кодом

Демонстрационный проект

Введение

Первоначально цель написания данной статьи заключалась в следующем: показать, как писать обертки для низкоуровневых интерфейсов на языках семейства VisualStudio 7.0. Но по мере знакомства с предметом я понял, что тему можно расширить, так как схожие механизмы используются не только для взаимодействия с COM-объектами, но и для взаимодействия с низкоуровневым системным кодом Windows, в частности - с Win32 API. Кроме того, я думаю, что многим будет интересно узнать, как же в действительности выглядит код, который создается утилитами типа TlbImp (я здесь имею в виду код на языке C#, а не реально создающийся код на MSIL).

Эта тема достаточно актуальна для переходного периода, когда существует огромное количество кода, написанного с использованием Win32 API и COM-объектов, с которым нужно взаимодействовать. Проблема несколько смягчается, если используются объекты, описанные в библиотеках типов, за счет использования утилит, автоматически генерирующих сборки. Но что делать, если библиотеки типов нет или код находится в экспортируемой функции некоторой динамической библиотеки? В этом случае выход только один - вручную написать необходимые обертки.

Механизм, используемый для взаимодействия .NET с неуправляемым кодом, достаточно хорошо должен быть знаком тем, кто описывал интерфейсы на языке IDL. В обоих случаях используются атрибуты.

Атрибуты главным образом используются для правильного обмена данными между управляемым (managed) и неуправляемым (unmanaged) кодом, но не только.

PlatformInvoke

Рассмотрение интеграции управляемого и неуправляемого кода начнем с PlatformInvoke. Эта технология позволяет достаточно просто вызывать функции динамических библиотек путем отображения объявления статического метода на точку входа PE/COFF.

Чтобы указать, что метод определен во внешней DLL, нужно пометить его как extern и использовать атрибут метода System.Runtime.InteropServices.DllImport. Этот атрибут сообщает CLR, что описание метода и дополнительные параметры (если они есть) необходимо использовать как информацию для вызова LoadLibrary и GetProcAddress, перед тем, как вызвать метод.

Атрибут DllImport имеет ряд параметров, которые можно опустить, но имя файла должно быть задано всегда. Это имя используется CLR для вызова LoadLibrary. Имя функции, которую необходимо вызвать из DLL, задается или прямым заданием параметра EntryPoint атрибута DllImport, или берется из описания самой функции. Во втором случае подразумевается, что ее название в программе соответствует ее имени в библиотеке. Пример использования этого атрибута приведен ниже:


[DllImport("KERNEL32.DLL", EntryPoint="MoveFileW",  SetLastError=true,
           CharSet=CharSet.Unicode, ExactSpelling=true,
           CallingConvention=CallingConvention.StdCall)]
public static extern bool MoveFile(String src, String dst);

Это все, что касается только технологии PlatformInvoke. Темы, рассматриваемые дальше, имеют отношение как к PlatformInvoke, так и к общению с COM-объектами из .NET. За исключением, естественно, описаний интерфейсов и классов.

Конвертирование типов

Важный вопрос, встающий при взаимодействии управляемого и неуправляемого кода: конвертирование типов. При осуществлении вызова функции ее параметры одновременно являются экземплярами и CLR, и внешнего мира. Здесь важно понимать, что каждый параметр имеет два типа - управляемый и неуправляемый. Кроме того, некоторые типы имеют одинаковый вид и в управляемом, и в неуправляемом коде, а это значит, что при их передаче никакого преобразования не требуется. К таким типам относятся следующие: Single, Double, SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64 и одномерные массивы этих типов. Все остальные типы должны преобразовываться.

Для задания правил конвертирования используется атрибут MarshalAs. Он может применяться к параметрам и результатам методов, полям структур и классов. Этот атрибут не является обязательным, так как каждый тип данных имеет встроенные правила маршалинга. Но если данный тип может быть сконвертирован во множество других типов, необходимо применение этого атрибута.

Полное описание этого атрибута можно найти в документации, дальше по тексту будут даны некоторые наиболее интересные в использовании примеры.

Для примеров я взял реализацию COM-объекта <Менеджера компонентных категорий>, а именно структуры CATEGORYINFO и интерфейсов IEnumGUID, IEnumCATEGORYINFO и ICatInformation.

Описание структур - атрибут StructLayout

Применяется ко всей структуре и позволяет управлять физическим расположением членов структуры в памяти. В общем случае CLR управляет расположением данных структур и классов самостоятельно, если же нужно передавать класс или структуру в неуправляемый код, используется атрибут StructLayout.

Поле Pack этого атрибута может иметь следующие значения:

  • Sequential - в этом случае данные будут расположены в памяти последовательно в порядке их объявления.
  • Explicit - в этом случае можно управлять точным расположением каждого члена структуры с помощью задания дополнительного атрибута FieldOffset для каждого поля.
  • CharSet - задает правила маршалинга строковых данных и может принимать следующие значения:
  • Ansi - строки передаются в виде 1-байтовых ANSI символов
  • Auto - строки автоматически конвертируются в зависимости от системы (Unicode в WindowsNT и ANSI в Windows9x)
  • None = Ansi
  • Unicode - строки передаются в виде 2-байтовых символов.

Пример использования атрибутов StructLayout и MarshalAs приведен ниже:

IDL


#define CATDESC_MAX 128
typedef struct tagCATEGORYINFO {
  CATID   catid;
  LCID    lcid;
  OLECHAR szDescription[CATDESC_MAX];
} CATEGORYINFO, *LPCATEGORYINFO;

C#


[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct CATEGORYINFO 
{
  public Guid catid;
  public uint lcid;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]
  public String szDescription;
};

Можно видеть, что в данном случае строка szDescription передается в виде массива фиксированной длины из Unicode-символов. Для маршалинга остальных полей применяются правила по умолчанию.

ПРИМЕЧАНИЕ

В документации Miscrosoft утверждается, что поле SizeConst атрибута MarshalAs задает размер массива в байтах, на самом деле это поле задает количество элементов в массиве.

Обработка ошибок в COM и .NET

COM методы сообщают об ошибках, возвращая соответствующий HRESULT, .NET методы - генерируя исключения. Здесь возникает одна проблема - .NET игнорирует любые положительные значения HRESULT, что приводит к неправильной работе перечислителей типа IEnumXXX, так как последние сигнализируют о достижении конца последовательности возвратом значения S_FALSE = 1. Чтобы решить эту проблему - для методов введен атрибут PreserveSig. Задание этого атрибута позволяет подавить генерацию исключений .NET, и гарантирует возврат точного значения HRESULT из COM метода, в противном случае результатом метода всегда будет S_OK = 0. Пример использования этого атрибута приведен ниже.

Описание интерфейсов - атрибуты ComImport, Guid, InterfaceType

Для описания интерфейсов и классов применяются атрибуты ComImport и Guid. Атрибут ComImport - показывает, что тип был ранее определен в COM. CLR обращается с такими типами не так, как с <родными>, в частности - по другому создает объекты таких типов, выполняет приведение типов, удержание объектов в памяти и т.д. Этот атрибут обязательно сопровождается атрибутом Guid, название которого говорит само за себя.

Атрибут InterfaceType применяется для описания базового COM интерфейса и может принимать следующие значения: дуальный, IDispatch или IUnknown. Если этот атрибут опущен, то считается, что интерфейс дуальный. В нашем случае все интерфейсы наследуют от IUnknown.

Описания параметров методов - атрибуты In, Out, MarshalAs

Параметры могут передаваться разными способами. Правильное описание параметров определяется не только атрибутами, но и модификаторами языка C#.

Для примера рассмотрим метод ICatInformation.GetCategoryDesc.


void ICatInformation.GetCategoryDesc([In] ref Guid rcatid,
                                     [In] uint lcid,
                                     [Out, MarshalAs(UnmanagedType.LPWStr)]
out String pszDesc);

ПРИМЕЧАНИЕ

Данный метод можно описать в виде функции:

[return : MarshalAs(UnmanagedType.LPWStr)] String ICatInformation.GetCategoryDesc([In] ref Guid rcatid, [In] uint lcid);

Такой синтаксис можно использовать для функций Win32API и методов COM-интерфейсов, имеющих последний параметр типа out и возвращающих HRESULT. Далее, в примерах интерфейсов и в демонстрационном приложении методы будут записываться подобным образом. Модификатор return нужен только при задании атрибута MarshalAs для методов COM-интерфейсов.

Если посмотреть на IDL-описание этого метода, видно, что передается ссылка на CLSID (GUID), по правилам языка C# структуры передаются по значению, а Guid является именно структурой. Поэтому, чтобы правильно передать параметр в COM метод, мало задать атрибут [In], нужно еще указать ключевое слово ref для параметра rcatid. Точно также, для задания выходных параметров нужно не только задавать атрибут [Out], но и ключевое слово out. При несоблюдении этих правил возможны ошибки компиляции или, что хуже, ошибки времени выполнения.

Атрибут MarshalAs задает правила передачи параметров, наиболее часто он используется в следующих видах:

  • MarshalAs(UnmanagedType.LPWStr) - Unicode-строка. Память под строку распределяется и освобождается через системные функции.
  • MarshalAs(UnmanagedType.LPArray, SizeParamIndex=n) - передается одномерный массив, размер массива задается параметром с номером n, нумерация параметров начинается с нуля.
  • MarshalAs(UnmanagedType.Interface) - передается COM интерфейс.

Примеры интерфейсов

IDL


[
  object,
  uuid(0002E000-0000-0000-C000-000000000046),
  pointer_default(unique)
]
interface IEnumGUID : IUnknown
{
  HRESULT Next([in] ULONG celt,
               [out, size_is(celt), length_is(*pceltFetched)] GUID *rgelt,
               [out] ULONG *pceltFetched);

  HRESULT Skip([in] ULONG celt);

  HRESULT Reset();

  HRESULT Clone([out] IEnumGUID **ppenum);
}

[
  object,
  uuid(0002E011-0000-0000-C000-000000000046),
  pointer_default(unique)
]
interface IEnumCATEGORYINFO : IUnknown
{
  HRESULT Next([in] ULONG celt,
               [out, size_is(celt), length_is(*pceltFetched)] CATEGORYINFO *rgelt,
               [out] ULONG *pceltFetched);

  HRESULT Skip([in] ULONG celt);

  HRESULT Reset();

  HRESULT Clone([out] IEnumCATEGORYINFO **ppenum);
}

[
  object,
  uuid(0002E013-0000-0000-C000-000000000046),
  pointer_default(unique)
]
interface ICatInformation : IUnknown
{
  HRESULT EnumCategories(
	[in] LCID lcid,
	[out] IEnumCATEGORYINFO** ppenumCategoryInfo);

  HRESULT GetCategoryDesc(
	[in] REFCATID rcatid,
	[in] LCID lcid,
	[out] LPWSTR* pszDesc);

  [local]
  HRESULT EnumClassesOfCategories(
	[in] ULONG cImplemented,
	[in,size_is(cImplemented)] CATID rgcatidImpl[],
	[in] ULONG cRequired,
	[in,size_is(cRequired)] CATID rgcatidReq[],
	[out] IEnumCLSID** ppenumClsid);

  [call_as(EnumClassesOfCategories)]
  HRESULT RemoteEnumClassesOfCategories(
	[in] ULONG cImplemented,
	[in,unique,size_is(cImplemented)] CATID rgcatidImpl[],
	[in] ULONG cRequired,
	[in,unique,size_is(cRequired)] CATID rgcatidReq[],
	[out] IEnumCLSID** ppenumClsid);

  [local]
  HRESULT IsClassOfCategories(
	[in] REFCLSID rclsid,
	[in] ULONG cImplemented,
	[in,size_is(cImplemented)] CATID rgcatidImpl[],
	[in] ULONG cRequired,
	[in,size_is(cRequired)] CATID rgcatidReq[]);

  [call_as(IsClassOfCategories)]
  HRESULT RemoteIsClassOfCategories(
	[in] REFCLSID rclsid,
	[in] ULONG cImplemented,
	[in,unique,size_is(cImplemented)] CATID rgcatidImpl[],
	[in] ULONG cRequired,
	[in,unique,size_is(cRequired)] CATID rgcatidReq[]);

  HRESULT EnumImplCategoriesOfClass(
	[in] REFCLSID rclsid,
	[out] IEnumCATID** ppenumCatid);

  HRESULT EnumReqCategoriesOfClass(
	[in] REFCLSID rclsid,
	[out] IEnumCATID** ppenumCatid);
}

C#


[
  ComImport,
  Guid("0002E000-0000-0000-C000-000000000046"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
]
public interface IEnumGUID 
{
  [PreserveSig()]
  int Next([In] uint celt,
           [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] Guid[] rgelt,
           [Out] out uint pceltFetched);

  [PreserveSig()]
  int Skip([In] uint celt);

  void Reset();

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumGUID Clone();
};

[
  ComImport,
  Guid("0002E011-0000-0000-C000-000000000046"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
]
public interface IEnumCATEGORYINFO 
{
  [PreserveSig()]
  int Next([In] uint celt,
           [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] CATEGORYINFO[] rgelt,
           [Out] out uint pceltFetched);

  [PreserveSig()]
  int Skip([In] uint celt);

  void Reset();

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumCATEGORYINFO Clone();
};

[
  ComImport,
  Guid("0002E013-0000-0000-C000-000000000046"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
]
public interface ICatInformation 
{
  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumCATEGORYINFO EnumCategories([In] uint lcid);

  [return : MarshalAs(UnmanagedType.LPWStr)]
  String GetCategoryDesc([In] ref Guid rcatid, [In] uint lcid);

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumGUID EnumClassesOfCategories([In] uint cImplemented,
                                    [In, MarshalAs(UnmanagedType.LPArray, 
SizeParamIndex=0)] Guid[] rgcatidImpl, [In] uint cRequired, [In, MarshalAs(UnmanagedType.LPArray,
SizeParamIndex=2)] Guid[] rgcatidReq); [return : MarshalAs(UnmanagedType.Interface)] IEnumGUID RemoteEnumClassesOfCategories([In] uint cImplemented, [In, MarshalAs(UnmanagedType.LPArray,
SizeParamIndex=0)] Guid[] rgcatidImpl, [In] uint cRequired, [In, MarshalAs(UnmanagedType.LPArray,
SizeParamIndex=2)] Guid[] rgcatidReq); [PreserveSig()] int IsClassOfCategories([In] ref Guid rclsid, [In] uint cImplemented, [In, MarshalAs(UnmanagedType.LPArray,
SizeParamIndex=0)] Guid[] rgcatidImpl, [In] uint cRequired, [In, MarshalAs(UnmanagedType.LPArray,
SizeParamIndex=2)] Guid[] rgcatidReq); [PreserveSig()] int RemoteIsClassOfCategories([In] ref Guid rclsid, [In] uint cImplemented, [In, MarshalAs(UnmanagedType.LPArray,
SizeParamIndex=0)] Guid[] rgcatidImpl, [In] uint cRequired, [In, MarshalAs(UnmanagedType.LPArray,
SizeParamIndex=2)] Guid[] rgcatidReq); [return : MarshalAs(UnmanagedType.Interface)] IEnumGUID EnumImplCategoriesOfClass([In] ref Guid rclsid); [return : MarshalAs(UnmanagedType.Interface)] IEnumGUID EnumReqCategoriesOfClass([In] ref Guid rclsid); };

Описание классов

Для описания классов также используются атрибуты ComImport и Guid. Классы с атрибутом ComImport не могут иметь никаких данных и методов.

Пример описания класса

IDL

Описание отсутствует

C#


[
  ComImport,
  Guid("0002E005-0000-0000-C000-000000000046")
]
public class StdComponentCategoriesMgr 
{
};

Пример использования класса

C#


using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using ComCatWrapper;

public class Test
{
  static void Main()
  {
    StdComponentCategoriesMgr mgr = new StdComponentCategoriesMgr();
    ICatInformation catInfoItf = (ICatInformation)mgr;
    IEnumCATEGORYINFO enumCInfoItf = сatInfoItf.EnumCategories(0);

    // и т.д.

  }
}

Из этого примера видна еще одна особенность работы с COM-объектами в .NET: вместо привычного CoCreateInstance используется оператор new, а вместо QueryInterface используется приведение типов.

Демонстрационное приложение

Демонстрационное приложение, демонстрирующее работу с COM-интерфейсами, написано на C#. Проект состоит из двух модулей: модуля, обеспечивающего интерфейс пользователя (файл MainForm.cs) и модуля, содержащего обертки COM-объекта <Менеджер категорий компонентов> (файл ComCatWrapper.cs).

Как уже упоминалось, в файле ComCatWrapper.cs содержатся описания структуры CATEGORYINFO и интерфейсов IEnumGUID, IEnumCATEGORYINFO и ICatInformation, а также кокласса StdComponentCategoriesMgr.

Файл MainForm.cs содержит код, необходимый для построения простейшего пользовательского интерфейса и использует интерфейсы из ComCatWrapper.cs.

Первоначально список категорий пуст, он заполняется при нажатии кнопки <Заполнить>. Так сделано из-за того, что заполнение идет достаточно долго, а использование, например, дополнительных потоков усложнило бы логику программы.

Вся работа с COM-интерфейсами ведется в двух функциях: FillBtn_Click и FillNodes. Эти функции просты и достаточно подробно прокомментированы.

Визуально категории компонентов представляются в виде дерева следующего вида: описание категории, соответствующий ей идентификатор (CATID) и идентификаторы классов (CLSID), реализующих данную категорию.

Ниже приведен пример работы тестового приложения, использующего обертки COM-интерфейсов


Заключение

Как видим, обеспечить взаимодействие COM и .NET довольно просто для программиста на C#. Нужно только знать, какие параметры и как передавать между управляемым и неуправляемым кодом.

К сожалению, во время подготовки статьи выяснилось, что ManagedC++ и VB.NET не позволяют писать обертки для COM-объектов без использования tlb. Задание атрибута ComImport в этих языках приводит к выбрасыванию исключений при попытке создания экземпляров классов во время выполнения программы, хотя компиляция проходит без проблем. Что это - ошибка или так было задумано, я не знаю. В то же время классы-обертки, написанные на C#, можно использовать и из ManagedC++ и VB.NET.


 ВОПРОС-ОТВЕТ

Как подменить функцию API?

Рисунок: Приложение HookAPI

Демонстрационное приложение (WTL Dialog) HookAPI (100kb) Требует наличия звуковой карты. Методы 3, 4 и 5 не будут работать под windows9x/ME.

Рисунок: Приложение HookAPI2

Демонстрационное приложение (WTL Dialog) HookAPI2 (20kb) Требует наличия WinSockets 1.0.

Переопределение с помощью препроцессора


#include <windows.h>

WINUSERAPI BOOL WINAPI MyMessageBeep(IN UINT uType)
{
	//Your code here	
}

#define MessageBeep MyMessageBeep

Теперь если в коде программы встретится MessageBeep препроцессор заменит ее на нашу MyMessageBeep. Очень просто.

Но что если хочется добавить немного своей логики в уже откомпилированный код, изменить работу чужой библиотеки, пересобрать которую нет никакой возможности? Иными словами, заставить уже откомпилированный код вызвать нашу функцию вместо стандартной. Это вполне реально. Давайте поближе рассмотрим, как под windows процедуры одного модуля используют процедуры другого.

Модификация таблиц импорта/экспорта

Весь API, доступный из какого-либо модуля, описан в так называемой таблице экспорта этого модуля. С другой стороны, список API, необходимый для нормальной работы опять-таки, любого модуля, находится в его таблице импорта.

Код вызова процедуры из другого модуля выглядит примерно так:


call        dword ptr [__imp__MessageBeep@4 (004404cc)]

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

  • Отыскать таблицу импорта функций для нужного нам модуля
  • Отыскать там указатель на перехватываемую функцию
  • Снять с этого участка памяти утрибут ReadOnly
  • Записать указатель на нашу функцию
  • Вернуть защиту обратно


HRESULT ApiHijackImports(
                HMODULE hModule,
                LPSTR szVictim,
                LPSTR szEntry,
                LPVOID pHijacker,
                LPVOID *ppOrig
                )
{
    // Check args
    if (::IsBadStringPtrA(szVictim, -1) ||
        (!IS_INTRESOURCE(szEntry) && ::IsBadStringPtrA(szEntry, -1)) ||
        ::IsBadCodePtr(FARPROC(pHijacker)))
    {
        return E_INVALIDARG;
    }

    PIMAGE_DOS_HEADER pDosHeader = PIMAGE_DOS_HEADER(hModule);

    if (::IsBadReadPtr(pDosHeader, sizeof(IMAGE_DOS_HEADER)) ||
        IMAGE_DOS_SIGNATURE != pDosHeader->e_magic)
    {
        return E_INVALIDARG;
    }

    PIMAGE_NT_HEADERS pNTHeaders = 
        MakePtr(PIMAGE_NT_HEADERS, hModule, pDosHeader->e_lfanew);

    if (::IsBadReadPtr(pNTHeaders, sizeof(IMAGE_NT_HEADERS)) ||
        IMAGE_NT_SIGNATURE != pNTHeaders->Signature)
    {
        return E_INVALIDARG;
    }

    HRESULT hr = E_UNEXPECTED;

    // Locate the victim
    IMAGE_DATA_DIRECTORY& impDir = 
        pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    PIMAGE_IMPORT_DESCRIPTOR pImpDesc = 
        MakePtr(PIMAGE_IMPORT_DESCRIPTOR, hModule, impDir.VirtualAddress),
        pEnd = pImpDesc + impDir.Size / sizeof(IMAGE_IMPORT_DESCRIPTOR) - 1;

    while(pImpDesc < pEnd)
    {
        if (0 == ::lstrcmpiA(MakePtr(LPSTR, hModule, pImpDesc->Name), szVictim))
        {
            if (0 == pImpDesc->OriginalFirstThunk)
            {
                // no import names table
                return E_UNEXPECTED;
            }

            // Locate the entry
            PIMAGE_THUNK_DATA pNamesTable =
                MakePtr(PIMAGE_THUNK_DATA, hModule, pImpDesc->OriginalFirstThunk);

            if (IS_INTRESOURCE(szEntry))
            {
                // By ordinal
                while(pNamesTable->u1.AddressOfData)
                {
                    if (IMAGE_SNAP_BY_ORDINAL(pNamesTable->u1.Ordinal) &&
                        WORD(szEntry) == IMAGE_ORDINAL(pNamesTable->u1.Ordinal))
                    {
                        hr = S_OK;
                        break;
                    }
                    pNamesTable++;
                }
            }
            else
            {
                // By name
                while(pNamesTable->u1.AddressOfData)
                {
                    if (!IMAGE_SNAP_BY_ORDINAL(pNamesTable->u1.Ordinal))
                    {
                        PIMAGE_IMPORT_BY_NAME pName = MakePtr(PIMAGE_IMPORT_BY_NAME,
                                            hModule, pNamesTable->u1.AddressOfData);

                        if (0 == ::lstrcmpiA(LPSTR(pName->Name), szEntry))
                        {
                            hr = S_OK;
                            break;
                        }
                    }
                    pNamesTable++;
                }
            }

            if (SUCCEEDED(hr))
            {
                // Get address
                LPVOID *pProc = MakePtr(LPVOID *, pNamesTable,
                            pImpDesc->FirstThunk - pImpDesc->OriginalFirstThunk);

                // Save original handler
                if (ppOrig)
                    *ppOrig = *pProc;

                // write to write-protected memory
                return WriteProtectedMemory(pProc, &pHijacker, sizeof(LPVOID));
            }
            break;
        }
        pImpDesc++;
    }
    return hr;
}

HRESULT WriteProtectedMemory(LPVOID pDest, LPCVOID pSrc, DWORD dwSize)
{
	// Make it writable
	DWORD dwOldProtect = 0;
	if (::VirtualProtect(pDest, dwSize, PAGE_READWRITE, &dwOldProtect))
	{
		::MoveMemory(pDest, pSrc, dwSize);

		// Restore protection
		::VirtualProtect(pDest, dwSize, dwOldProtect, &dwOldProtect);
		return S_OK;
	}

	return HRESULT_FROM_WIN32(GetLastError());
}

Впрочем, такой способ не будет работать если используется позднее связывание (delay load) или связывание во время исполнения (run-time load) с помощью ::GetProcAddress(). Это можно побороть если перехватить саму ::GetProcAddress(), и подменять возвращяемое значение при необходимости. А можно и подправить таблицу экспорта аналогичным способом:


HRESULT ApiHijackExports(
                HMODULE hModule,
                LPSTR szEntry,
                LPVOID pHijacker,
                LPVOID *ppOrig
                )
{
    // Check args
    if ((!IS_INTRESOURCE(szEntry) && ::IsBadStringPtrA(szEntry, -1))
        || ::IsBadCodePtr(FARPROC(pHijacker)))
    {
        return E_INVALIDARG;
    }

    PIMAGE_DOS_HEADER pDosHeader = PIMAGE_DOS_HEADER(hModule);

    if (::IsBadReadPtr(pDosHeader, sizeof(IMAGE_DOS_HEADER)) ||
        IMAGE_DOS_SIGNATURE != pDosHeader->e_magic)
    {
        return E_INVALIDARG;
    }

    PIMAGE_NT_HEADERS pNTHeaders =
        MakePtr(PIMAGE_NT_HEADERS, hModule, pDosHeader->e_lfanew);
    
    if (::IsBadReadPtr(pNTHeaders, sizeof(IMAGE_NT_HEADERS)) ||
        IMAGE_NT_SIGNATURE != pNTHeaders->Signature)
    {
        return E_INVALIDARG;
    }

    HRESULT hr = E_UNEXPECTED;

    IMAGE_DATA_DIRECTORY& expDir =
        pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
    PIMAGE_EXPORT_DIRECTORY pExpDir =
        MakePtr(PIMAGE_EXPORT_DIRECTORY, hModule, expDir.VirtualAddress);

    LPDWORD pdwAddrs = MakePtr(LPDWORD, hModule, pExpDir->AddressOfFunctions);
    LPWORD pdwOrd = MakePtr(LPWORD, hModule, pExpDir->AddressOfNameOrdinals);
    DWORD dwAddrIndex = -1;

    if (IS_INTRESOURCE(szEntry))
    {
        // By ordinal

        dwAddrIndex = WORD(szEntry) - pExpDir->Base;
        hr = S_OK;
    }
    else
    {
        // By name
        LPDWORD pdwNames = MakePtr(LPDWORD, hModule, pExpDir->AddressOfNames);
        for (DWORD iName = 0; iName < pExpDir->NumberOfNames; iName++)
        {
            if (0 == ::lstrcmpiA(MakePtr(LPSTR, hModule, pdwNames[iName]), szEntry))
            {
                dwAddrIndex = pdwOrd[iName];
                hr = S_OK;
                break;
            }
        }
    }

    if (SUCCEEDED(hr))
    {
        if (pdwAddrs[dwAddrIndex] >= expDir.VirtualAddress &&
            pdwAddrs[dwAddrIndex] < expDir.VirtualAddress + expDir.Size)
        {
            // We have a redirection
            LPSTR azRedir = MakePtr(LPSTR, hModule, pdwAddrs[dwAddrIndex]);
            ATLASSERT(!IsBadStringPtrA(azRedir, -1));

            LPSTR azDot = strchr(azRedir, '.');
            int nLen = azDot - azRedir;
            LPSTR azModule = (LPSTR)alloca(nLen);
            memcpy(azModule, azRedir, nLen);
            azModule[nLen] = '\x0';

            // Try to patch redirected function
            return ApiHijackExports(
                ::GetModuleHandle(azModule), azDot + 1, pHijacker, ppOrig);
        }

        if (ppOrig)
            *ppOrig = MakePtr(LPVOID, hModule, pdwAddrs[dwAddrIndex]);

        DWORD dwOffset = DWORD_PTR(pHijacker) - DWORD_PTR(hModule);

        // write to write-protected memory
        hr = WriteProtectedMemory(pdwAddrs + dwAddrIndex, &dwOffset, sizeof(LPVOID));
    }

    return hr;
}

Имейте в виду, под Windows9x нельзя честно подменить экспорты для разделяемых библиотек, таких как user32.dll, kernel32.dll и gdi32.dll. Это связано с тем, что область памяти начиная с адреса 7FC00000h и выше совместно используестя всеми процессами в системе, и модификация сказалась бы на каждом из них. А это нежелательно, поскольку память, занимаемая нашей функцией-перехватчиком, наоборот, принадлежит только нашему процессу. Во всех остальных процессах в системе ::GetProcAddress(), после подмены таблицы экспорта, вернула бы неправильный указатель. Тем не менее, если нельзя, но очень хочется, то можно. Для этого нам придется вручную создать новый дескриптор в GDT (вот тут-то у Windows9x проблем не возникает) и используя этот дескриптор произвести необходимые изменения. Но будьте готовы к тому, что понадобится написать свою разделяемую библиотеку, установить ее в системе и проверять ID процесса при каждом обращении. Рабочий пример есть на internals.com.

Модификация самого обработчика

Эти два способа работают в 99% случаев. Последний процент - это подмена функции, вызываемой внутри чужого модуля, т.е. когда и вызаваемая и вызывающая процедура находятся в одном и том же, да к тому же чужом, модуле. В этом случае, вызов будет сделан напрямик, а не через таблицы импорта/экспорта. Тут уже ничего сделать нельзя. Почти. Можно изменить саму функцию-обработчик, с тем чтобы перенаправить вызовы в нашу собственную. Делается это довольно просто: в начало исходного обработчика прописывается команда безусловного перехода на нашу процедуру, а если нужно вызвать оригинал, то нужно просто сохранить первые 5 байт затертых командой перехода, добавить после них опять-таки команду безусловного перехода на изначальный код +5 байт. Разумется, эти пять байт кода не дожны содержать команд перехода или вызова. Кроме того, может понадобиться больше чем 5 байт, ведь команда перехода посреди длинной инструкции работать не будет. Это случается крайне редко. Обычно код функции, как его генерит компилятор для I86 выглядит примерно так: инициализация стека, загрузка в регистры параметров функции, их проверка и переход в случае неудовлетворительных результатов. Этого вполне хватает чтобы вставить наш маленький перехватчик. Но бывает и так:


CSomeClass::Release:
FF152410E475           call    dword ptr [InterlockedDecrement]
85C0                   test    eax,eax

Или даже


CSomeClass::NonImplemented:
C20400                 ret     4

Что, впрочем, можно распознать и вернуть код ошибки если инструкции ret, jmp или call встретится слишком рано. Но вот такой случай распознать не получится:


SomeFunction:
33C0                   xor     eax,eax
SomeFunction2:
55                     push    ebp
8BEC                   mov     ebp,esp

Иными словами, модификация SomeFunction приведет к неизвестным изменениям в SomeFunction2, и, возможно, краху всей системы.

Все это сильно усложняет нам задачу. Нужно дизассемблировать эти байты и проверить каждую инструкцию. Чтобы немного облегчить нам жизнь, фирма Майкрософт разработала специальный SDK для такого рода трюков: Microsoft Detours. С этим SDK задача подмены чужой функции реализуется удивительно просто:


#include <detours.h>

DetourFunction(PBYTE(::MessageBeep), PBYTE(MyMessageBeep));

После чего все вызовы ::MessageBeep(), откуда бы они не были произведены, окажутся вызовами нашей MyMessageBeep(). Что и требовалось.

Модификация самого обработчика 2

Довольно оригинальный вариант предыдущего способа был предложен Дмитрием Крупорницким: первая инструкция перехватываемой функции заменяется инструкцией прерывания INT 3. Далее процедура обработки необработанных исключений (unhandled exception handler) подменяет регистр EIP на адрес нашей функции-перехватчика.


static DWORD_PTR m_dwFunction;

static LONG WINAPI MyUnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo)
{
    if (pExceptionInfo->ContextRecord->Eip != m_dwFunction)
        return EXCEPTION_CONTINUE_SEARCH;

    // Continue execution from MyMessageBeep
    pExceptionInfo->ContextRecord->Eip = (DWORD_PTR)MyMessageBeep;
    return EXCEPTION_CONTINUE_EXECUTION;
}

LRESULT CMainDlg::OnMethod5(WORD /*wNotifyCode*/, WORD wID, 
HWND /*hWndCtl*/, BOOL& /*bHandled*/) { m_dwFunction = (DWORD_PTR)::GetProcAddress(::GetModuleHandle("USER32.dll"),
"MessageBeep"); BYTE nSavedByte = *(LPBYTE)m_dwFunction; LPTOP_LEVEL_EXCEPTION_FILTER pOldFilter = < BR > ::SetUnhandledExceptionFilter(MyUnhandledExceptionFilter); const BYTE nInt3 = 0xCC; // Inject int 3 HRESULT hr = WriteProtectedMemory(LPVOID(m_dwFunction), &nInt3, sizeof(const BYTE)); if (SUCCEEDED(hr)) { ::MessageBeep(m_uType); // Restore function hr = WriteProtectedMemory(LPVOID(m_dwFunction), &nSavedByte, sizeof(BYTE)); } ::SetUnhandledExceptionFilter(pOldFilter); return 0; }

Недостатком такого способа является его непредсказуемость. Кто угодно может зарегистрировать свой обработчик исключений и поломать нам логику. Более того, инструкции (...)/(), часто встречающиеся в программах, могут перехватить управление и не дать нашему обработчику шанса.

Подмена с использованием оберток(wrappers)

Еще один способ, о котором я хотел бы упомянуть. Он заключается в написании собственной динамической библиотеки с тем же именем и набором экспортимых функций. Такая библиотека кладется на место ориганальной, запускается использующий эту библиотеку процесс, тот находит нашу DLL и ничего не подозревая ее использует. А если переименовать оригинальную DLL и положить рядом, то можно даже переадресовывать часть вызовов в оригинальную библиотеку, а часть оставлять себе. К сожалению, это будет работать с экспортируемыми функциями, но не с экспортируемыми переменными. Пример такого рода обертки можно найти у Алексея Остапенко.

А вот с COM-объектами и интерфейсами обертки работают как нельзя лучше. Для этого создается другой COM-объект, реализующий нужный нам интерфейс, к нему создатся аггрегированный оригинальный COM-объект, а перехватчик отдается тем кто с ним будет в дальнейшем работать. Если оригинальный COM-объект не поддерживает аггрегацию, то придется реализовать все его интерфейсы, и если у него нету никаких внутренних (недокументированных) интерфейсов, то, возможно, все и заработает.


Все это касается подмены API текущего процесса. Если понадобится перехватить вызов в чужом процессе, то следует выбрать любой из приведенных здесь методов, поместить их в DLL и закинуть ее в чужой процесс. Это тема для отдельного разговора и здесь рассматриваться не будет.


Использованные статьи и литература


Это все на сегодня. Пока!

Алекс Jenter   jenter@rsdn.ru
Duisburg, 2001.    Публикуемые в рассылке материалы принадлежат сайту RSDN.

| Предыдущие выпуски     | Статистика рассылки