Смекни!
smekni.com

Вызов функции в другом процессе (стр. 1 из 2)

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

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

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.