Сообщений 33    Оценка 195 [+1/-0]         Оценить  
Система Orphus

HOWTO: Вызов функции в другом процессе

Автор: Сергей Холодилов
The RSDN Group

Источник: RSDN Magazine #4-2004
Опубликовано: 13.02.2005
Исправлено: 10.12.2016
Версия текста: 1.0
Идея
А зачем нам DLL?
Ограничения
Получение адреса загрузки DLL
Получение адреса функции
Простой способ
Способ для настоящих программистов
Поиск экспортируемой функции в PE-файле
Как в PE-файле добраться до секции экспорта
Как в секции экспорта найти адрес функции
Код
Пример
Дополнительные источники

I just called to say I love you, 
And I mean it from the bottom of my heart.

Stevie Wonder

Исходные тексты к статье

Внедрению DLL так или иначе (обычно в связи с перехватом API) посвящено достаточно большое количество статей. Но ни в одной из тех, которые я читал, не говорится, как извне заставить эту DLL сделать что-нибудь полезное. Обычно авторы ограничиваются перехватом необходимых API-функций где-нибудь в DllMain и последующей реакцией на вызовы этих самых функций. Между тем, взаимодействие с внедрённой DLL даёт возможность корректировать и направлять её работу и, тем самым, позволяет добиваться значительно большего эффекта.

Если внедрённая DLL создаёт свой поток, задача взаимодействия легко решается, так как в этом случае можно использовать любые методы IPC: сообщения, сокеты, именованные каналы, … , при желании можно даже COM-сервер сделать :)

ПРЕДУПРЕЖДЕНИЕ

В описании DllMain сказано, что некоторые функции, в том числе CreateThread, из неё вызывать нельзя. Объяснение «почему они говорят, что нельзя» можно найти у Рихтера (в русском четвёртом издании это глава «DLL: более сложные методы программирования», раздел «Как система упорядочивает вызовы DllMain»), у него же написано, что на самом деле можно, если осторожно. :) Просто при создании потока надо не забывать, что его выполнение начнётся не раньше, чем текущий поток покинет DllMain.

Но это всё более-менее очевидные и не очень красивые (на мой взгляд) способы. Мне кажется, я нашёл более интересный и элегантный метод. Ему и посвящена эта статья.

Идея

Идея тривиальна. Алгоритм состоит всего из четырёх шагов (плюс ещё один по желанию):

ПРИМЕЧАНИЕ

«Так или иначе» означает, что DLL может быть загружена любым способом. Например, это может быть advapi32.DLL, которую процесс-жертва грузит сам. Если вы хотите, чтобы исполнялся ваш код, скорее всего, DLL придётся внедрять. Описание внедрения DLL смотрите в дополнительных источниках в конце статьи.

А зачем нам DLL?

При желании можно напрямую записать весь исполняемый код в адресное пространство процесса-жертвы и запустить его тем же CreateRemoteThread. При большом желании можно добиться, чтобы это заработало... Основная проблема, подстерегающая вас на этом пути, заключается в том, что все функции, которые вызывает ваш код, должны находиться точно по тем адресам, куда передаётся управление. С учётом того, что:

«само собой» ничего не получится. Чтобы добиться работоспособности кода, нужно модифицировать используемые вашим кодом адреса, то есть, фактически, выполнить задачу загрузчика. А зачем выполнять её вручную, если можно положиться на загрузчик :) ?

Ограничения

Использование CreateRemoteThread связано с очевидными ограничениями:

ПРИМЕЧАНИЕ

Существует платная реализация CreateRemoteThread для Windows 9x, смотрите сайт http://www.apihooks.com раздел «PrcHelp».

Кроме того, нужно иметь солидные права доступа к процессу-жертве:

ПРИМЕЧАНИЕ

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

Получение адреса загрузки DLL

В общем случае, при помощи функций EnumProcessModules и GetModuleFileNameEx можно перебрать все загруженные в процесс-жертву модули, найти среди них нужный и получить адрес его загрузки.

ПРИМЕЧАНИЕ

Эти функции являются частью Process Status API (PSAPI), поэтому будут работать только в линейке Windows NT/2000/XP. Но поскольку мы уже и так используем CreateRemoteThread, терять нам нечего.

Но если DLL внедрялась с помощью создания в процессе-жертве потока, поточной функцией которого является LoadLibrary, можно поступить проще. В этом случае код завершения потока является возвращаемым значением LoadLibrary, то есть как раз адресом загрузки DLL в процессе-жертве.

ПРЕДУПРЕЖДЕНИЕ

Вообще-то, как показывает практика, возвращаемое значение LoadLibrary – это не совсем адрес загрузки DLL. В некоторых случаях в младших битах находятся какие-то флаги. Например, при вызове функции LoadLibraryEx с флагом LOAD_LIBRARY_AS_DATAFILE младший бит возвращаемого значения всегда будет установлен в 1.

Выход достаточно прост: поскольку при загрузке модуля в адресном пространстве создаётся регион, а адреса начала регионов должны быть кратны 64К, для получения «настоящего» адреса загрузки нужно просто обнулить два младших байта.

Получение адреса функции

Есть два способа получить адрес функции: простой и для настоящих программистов. :)

Простой способ

Простой способ основан на том, что смещение начала функции от начала DLL – величина постоянная, от процесса не зависящая. Это значит, что если:

то получится адрес функции в процессе-жертве.

ПРИМЕЧАНИЕ

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

Именно на этом основана технология внедрения DLL через вызов LoadLibrary в другом процессе.

Если по каким-то причинам DLL уже загружена в процесс, то, наверное, этот способ можно рекомендовать даже самым-самым настоящим программистам. А вот если DLL нужно специально грузить, то, по-моему, опять получается некрасиво. :)

Способ для настоящих программистов

Реализовать функцию GetProcAddressInOtherProcess, принимающую в первом параметре описатель процесса. Она будет разбирать таблицу экспорта указанной DLL из указанного процесса, находить там нужную функцию и возвращать её адрес.

Если добавить функции LoadLibararyInOtherProcess и FreeLibraryInOtherProcess (которые несложно написать), получится совсем красиво, так как с чужим процессом можно будет работать почти так же, как и со своим.

Именно этот способ кажется мне интересным и элегантным, и именно его реализации посвящена статья.

Поиск экспортируемой функции в PE-файле

Как вы, наверное, знаете, формат всех исполняемых файлов в Windows (включая DLL, ocx, sys, и прочие) называется PE (расшифровывается как Portable Executable, но большого смысла не несёт, просто название, ничем не хуже других) форматом, а сами файлы, соответственно, PE-файлами. Чтобы отыскать адрес нужной функции в DLL, придётся разобраться с той частью PE-формата, которая отвечает за экспорт.

ПРИМЕЧАНИЕ

PE-формат достаточно сложен, но, к счастью, полностью он нам и не нужен. Если вас интересует более подробное описание, смотрите дополнительные источники в конце статьи.

Как в PE-файле добраться до секции экспорта

Любой PE-файл начинается с заголовка DOS, формат которого отражён в структуре IMAGE_DOS_HEADER.

        typedef
        struct _IMAGE_DOS_HEADER {   // DOS .EXE header
  ...
  LONG  e_lfanew;          // File address of new exe header
 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

Из всех полей этой структуры для нас интерес представляет только поле e_lfanew, которое является смещением от начала файла (в терминологии PE-формата такие смещения называются RVA – Relative Virtual Address) до PE-заголовка.

Формат PE-заголовка представлен структурой IMAGE_NT_HEADERS (она определена с использованием препроцессора и, на данный момент, соответствует структуре IMAGE_NT_HEADERS32):

        typedef
        struct _IMAGE_NT_HEADERS {
  ...
  IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

Из неё нас интересует только поле OptionalHeader, которое разворачивается в ещё одну структуру:

        typedef
        struct _IMAGE_OPTIONAL_HEADER {
  ...
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

И опять, нам нужно только одно поле – DataDirectory, а, точнее, только элемент DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].

Структура IMAGE_DATA_DIRECTORY описывает расположение в памяти одной из секций PE-файла. Она определёна следующим образом:

        typedef
        struct _IMAGE_DATA_DIRECTORY {
  DWORD  VirtualAddress; // RVA (смещение от начала файла) секции
  DWORD  Size;      // Размер секции
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

Элемент DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT] относится к секции экспорта.

Итого:

  1. В начале файла расположен IMAGE_DOS_HEADER.
  2. По смещению IMAGE_DOS_HEADER::e_lfanew находится IMAGE_NT_HEADERS.
  3. IMAGE_NT_HEADERS::OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT] описывает секцию экспорта. Он содержит RVA и размер секции.

Как в секции экспорта найти адрес функции

Секция экспорта начинается со структуры IMAGE_EXPORT_DIRECTORY.

        typedef
        struct _IMAGE_EXPORT_DIRECTORY {
  ...
  DWORD  Base;
  DWORD  NumberOfFunctions;
  DWORD  NumberOfNames;
  DWORD  AddressOfFunctions;   // RVA from base of image
  DWORD  AddressOfNames;     // RVA from base of image
  DWORD  AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Здесь:

ПРЕДУПРЕЖДЕНИЕ

Во-первых, элементы этого массива имеют тип WORD и размер 2 байта.

Во-вторых, MSDN и статья Мэтта Питрека «Форматы PE и COFF объектных файлов» содержат одну и туже ошибку, относящуюся к интерпретации содержимого этого массива. Правильно написано в статье Максима М. Гумерова «Загрузчик PE-файлов» и здесь :)

В результате, для поиска адреса функции, экспортируемой по имени, нужно сделать примерно следующее (в псевдокоде):

        // Ищем в массиве имён функций совпадающее имя
        int  nameIndex = FindFunctionName(AddressOfNames, NumberOfNames, name);
// Получаем соответствующий имени индекс функции
WORD funcIndex = AddressOfNameOrdinals[nameIndex];
// Получаем RVA функции
DWORD funcRVA = AddressOfFunctions[funcIndex];
ПРЕДУПРЕЖДЕНИЕ

По MSDN и Питреку, последняя строчка алгоритма должна выглядеть так:

DWORD funcRVA = AddressOfFunctions[funcIndex - Base];

Где Base – базовое значение ординала. Как показывает практика, Base вычитать не надо.

Код

В конце концов у меня получилось три функции. Первая находит секцию экспорта:

      // Определяет RVA секции экспорта 
      int GetExportSectionRVA(HANDLE hProcess, constvoid* baseAddress)
{
  // Читаем DOS-заголовок
  IMAGE_DOS_HEADER dos_header;
      ReadProcessMemory(
      hProcess, 
      baseAddress, 
      &dos_header, 
      sizeof(dos_header), 
      NULL);

  // Читаем PE-заголовок
  IMAGE_NT_HEADERS pe_header;
  ReadProcessMemory(
      hProcess, 
      reinterpret_cast<const BYTE*>(baseAddress) + dos_header.e_lfanew, 
      &pe_header, 
      sizeof(pe_header), 
      NULL);

  // Смещение секции экспортаreturn pe_header.OptionalHeader.DataDirectory
             [IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
}

Вторая перебирает массив имён функций в поиске заданного имени:

      // Ищет в массиве имён функций заданное имя, возвращает индекс или –1
      int FindName(
    HANDLE hProcess, 
    constvoid* baseAddress, 
    DWORD AddressOfNames, 
    DWORD count, 
    constchar* name)
{
  // Для сравнения имени его нужно прочитать, для этого нужно знать размерint size = lstrlenA(name) + 1;
  std::auto_ptr<char> candidate(newchar[size]);

  // Перебираем имена в массиве имён функцийfor (int index = 0; index < count; index++)
  {
    DWORD nameRVA;

    // Читаем адрес начала строки
    ReadProcessMemory(
        hProcess,
        reinterpret_cast<const BYTE*>(baseAddress) 
         + AddressOfNames + index * sizeof(DWORD), 
        &nameRVA, 
        sizeof(nameRVA), 
        NULL);

    // Читаем строку
    ReadProcessMemory(
        hProcess,
        reinterpret_cast<const BYTE*>(baseAddress) + nameRVA, 
        candidate.get(), 
        size, 
        NULL);

    if (strcmp(name, candidate.get()) == 0)
    {
      // Она! Сваливаем :)return index;
    }
  }

  // Такой функции нетreturn -1;
}

Третья функция использует первые две и находит нужную функцию в указанной DLL в указанном процессе:

      // Находит нужную функцию в указанной DLL в указанном процессе.
      void* GetProcAddress(HANDLE hProcess, HMODULE hLib, constchar* name)
{
  // Нам нужен именно адрес загрузки! А результат работы// LoadLibrary бывает иногда неожиданным..char* baseAddress = reinterpret_cast<char*>
    (reinterpret_cast<DWORD>(hLib) & 0xFFFF0000);

  // Смещение секции экспортаint export_offset = GetExportSectionRVA(hProcess, baseAddress);

  if (export_offset <= 0)
  {
    // Какие-то проблемы с экспортомreturn NULL;
  }

  // Читаем заголовок секции экспорта
  IMAGE_EXPORT_DIRECTORY export;
  ReadProcessMemory(
      hProcess, 
      baseAddress + export_offset, 
      &export, 
      sizeof(export), 
      NULL);

  // Индекс в массиве функций
  WORD funcIndex = -1;

  if (reinterpret_cast<DWORD_PTR>(name) > 0x0000ffff)
  {
    // Функция экспортируется по имени. Ищем имяint nameIndex = FindName(
      hProcess, 
      baseAddress, 
      export.AddressOfNames, 
      export.NumberOfNames, 
      name);

    if (nameIndex < 0)
    {
      // Такой функции нетreturn NULL;
    }

    // Читаем индекс (они двухбайтные!!!)
    ReadProcessMemory(
      hProcess,
      baseAddress + export.AddressOfNameOrdinals 
        + nameIndex * sizeof(WORD), 
      &funcIndex, 
      sizeof(funcIndex), 
      NULL);
  }
  else
  {
    // Функция экспортируется по ординалу
    WORD funcOrdinal = reinterpret_cast<DWORD>(name);

    if ((funcOrdinal < export.Base) 
     || (funcOrdinal >= export.Base + export.NumberOfFunctions))
    {
      // Такой функции нетreturn NULL;
    }

    // Индекс это ординал минус база
    funcIndex = funcOrdinal - export.Base;
  }

  if ((funcIndex < 0) || (funcIndex >= export.NumberOfFunctions))
  {
    // Такой функции нетreturn NULL;
  }

  // Читаем адрес
  DWORD funcRVA;
  ReadProcessMemory(
    hProcess,
    baseAddress + export.AddressOfFunctions + funcIndex * sizeof(DWORD), 
    &funcRVA, 
    sizeof(funcRVA), 
    NULL);

  // Результат это базовый адрес + RVAreturn (baseAddress + funcRVA);
}
ПРИМЕЧАНИЕ

Для оптимизации можно было бы сначала скопировать в свой процесс всю секцию экспорта (размер секции хранится в IMAGE_NT_HEADERS::OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size), а потом уже её разбирать. Но, поскольку заметных глазу задержек не возникает, я остановился на текущей реализации.

Пример

В качестве примера я написал три приложения: aggressor.exe, victim.exe и insider.dll. Victim и insider абсолютно пассивны, все действия выполняются aggressor-ом. Aggressor:

ПРИМЕЧАНИЕ

Чтобы это действительно работало, надо положить все три исполняемых модуля в один каталог.

Для реализации перечисленных действий, да и вообще на будущее, в aggressor реализованы следующие полезные функции:

      namespace OtherProcess
{
  //// Вызывает функцию из заданного процесса, возвращает // описатель потока, который эту функцию выполняет
  HANDLE AsynchronousCall(
    HANDLE hProcess, 
    void* address, 
    void* parameter, 
    DWORD* pid);

  //// Вызывает функцию из заданного процесса, дожидается завершения её работыbool SynchronousCall(
    HANDLE hProcess, 
    void* address, 
    void* parameter, 
    DWORD* result);

  //// Загружает DLL в указанный процесс
  HMODULE LoadLibrary(HANDLE hProcess, const TCHAR* path);

  //// Выгружает DLL в указанном процессеvoid FreeLibrary(HANDLE hProcess, HMODULE hLib);

  //// Находит нужную функцию в указанной DLL в указанном процессеvoid* GetProcAddress(HANDLE hProcess, HMODULE hLib, constchar* name);
};

Предназначение функций, я надеюсь, понятно из их названий и кратких комментариев. Понимание реализации также не должно вызвать затруднений, прокомментировано всё достаточно подробно, да и сам код не такой уж головоломный. Успешных вам вызовов!

Дополнительные источники

  1. Джеффри Рихтер, «Programming Application for Microsoft Windows», четвёртое издание.
  2. Тихомиров В.А. «Перехват API-функций в Windows NT/2000/XP».
  3. Мэтт Питрек «Форматы PE и COFF объектных файлов»
  4. Максим М. Гумеров «Загрузчик PE-файлов»


Эта статья опубликована в журнале RSDN Magazine #4-2004. Информацию о журнале можно найти здесь
    Сообщений 33    Оценка 195 [+1/-0]         Оценить