Будь умным!


У вас вопросы?
У нас ответы:) SamZan.net

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

Работа добавлена на сайт samzan.net:


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

Сергей Холодилов  

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, содержащую нужную функцию.

ПРИМЕЧАНИЕ

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

Получить адрес загрузки DLL.

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

Вызвать функцию при помощи CreateRemoteThread.

(опционально) Дождаться завершения потока и получить возвращаемое значение функции вызовом GetExitCodeThread.

А зачем нам DLL?

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

код будет расположен в случайном месте адресного пространства, так как вам вряд ли удастся выделить память по тому же адресу;

DLL могут быть загружены по другим адресам,

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

Ограничения

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

Поддерживается только линейка Windows NT/2000/XP.

ПРИМЕЧАНИЕ

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

Прототип вызываемой функции должен соответствовать прототипу функции потока.

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

PROCESS_CREATE_THREAD для запуска потока.

PROCESS_VM_READ для определения адреса.

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

ПРИМЕЧАНИЕ

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

Получение адреса загрузки 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 в процессе-жертве,

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

ПРИМЕЧАНИЕ

Понятно, что если 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] относится к секции экспорта.

Итого:

В начале файла расположен IMAGE_DOS_HEADER.

По смещению IMAGE_DOS_HEADER::e_lfanew находится IMAGE_NT_HEADERS.

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;

Здесь:

AddressOfFunctions – RVA (смещение от начала файла) массива, содержащего RVA функций.

AddressOfNames – RVA массива, содержащего RVA имён функций.

AddressOfNameOrdinals – RVA массива индексов функций. Элемент n этого массива содержит индекс в массиве адресов функций, соответствующей n-ному элементу в массиве имён функций.

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

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

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

NumberOfFunctions – количество элементов массива адресов функций.

NumberOfNames – количество элементов массива имён функций и массива индексов функций.

Base – базовое значение ординала экспортируемых функций. Для получения индекса функции, экспортируемой по ординалу, надо вычесть из её ординала значение Base.

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

// Ищем в массиве имён функций совпадающее имя

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, const void* 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,

   const void* baseAddress,

   DWORD AddressOfNames,

   DWORD count,

   const char* name)

{

 // Для сравнения имени его нужно прочитать, для этого нужно знать размер

 int size = lstrlenA(name) + 1;

 std::auto_ptr<char> candidate(new char[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, const char* 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);

 // Результат это базовый адрес + RVA

 return (baseAddress + funcRVA);

}

ПРИМЕЧАНИЕ

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

Пример

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

запускает victim.exe;

загружает в него insider.dll;

получает адреса трёх экспортируемых функций;

вызывает эти функции;

выгружает insider.dll из victim.exe .

ПРИМЕЧАНИЕ

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

Для реализации перечисленных действий, да и вообще на будущее, в 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, const char* name);

};

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

Список литературы

Джеффри Рихтер, «Programming Application for Microsoft Windows», четвёртое издание.

Тихомиров В.А. «Перехват API-функций в Windows NT/2000/XP».

Мэтт Питрек «Форматы PE и COFF объектных файлов»

Максим М. Гумеров «Загрузчик PE-файлов»

Для подготовки данной работы были использованы материалы с сайта http://www.rsdn.ru/




1. 03.2014 Купон можно использовать с 13
2. Сворення і редагування графічних зображень за допомогою графічного редактора
3. по теме Заболевания желудка и пищевода
4. лекция медицинских рефератов историй болезни литературы обучающих программ тестов1
5. Введение Характеристика любой политической системы страны неразрывно связанна с методами осуществлени
6. Расчет производственного освещени
7. Спешите делать добро 6 класс 07
8. Реферат- Личностный фактор в системе теоретической педагогики
9. то и единственный способ воздействия на учащихся
10. Доклад- Возникновение парусного спорта в России
11. це та властивість відчуття яка відрізняє його від інших
12.  Цілі та суть продуктової політики Для здійснення успішної діяльності на ринку необхідна детально розробл
13. Созвездие Телец
14. Наука гражданского судопроизводства
15. Красная шапочка г1
16. на тему Державне казначейство України Виконала Студентка ~~ курсу Спеціальності Еконо
17. РЕФЕРАТдисертації на здобуття наукового ступеня кандидата технічних наук Луцьк ~ 1999 Дисерта
18. Задание Задача VI
19. Тема Дванадцать правил Е
20. Фізіологія людини і тварин А В Т О Р Е Ф Е Р А Т дисертації на здобуття наукового ступ