Реферат по предмету "Программирование"


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

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

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


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(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
candidate(new char[size]);



  // Перебираем имена в массиве имён функций


  for (int index = 0; index


  {


    DWORD nameRVA;



    // Читаем адрес начала строки


    ReadProcessMemory(


        hProcess,


       
reinterpret_cast(baseAddress)


         + AddressOfNames +
index * sizeof(DWORD),


        &nameRVA,


        sizeof(nameRVA),


        NULL);



    // Читаем строку


    ReadProcessMemory(


        hProcess,


       
reinterpret_cast(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


   
(reinterpret_cast(hLib) & 0xFFFF0000);



  // Смещение секции экспорта


  int export_offset =
GetExportSectionRVA(hProcess, baseAddress);



  if (export_offset


  {


    // Какие-то проблемы с экспортом


    return NULL;


  }



  // Читаем заголовок секции экспорта


  IMAGE_EXPORT_DIRECTORY export;


  ReadProcessMemory(


      hProcess,


      baseAddress +
export_offset,


      &export,


      sizeof(export),


      NULL);



  // Индекс в массиве функций


  WORD funcIndex = -1;



  if
(reinterpret_cast(name) > 0x0000ffff)


  {


    // Функция экспортируется по имени. Ищем
имя


    int nameIndex = FindName(


      hProcess,


      baseAddress,


      export.AddressOfNames,


      export.NumberOfNames,


      name);



    if (nameIndex


    {


      // Такой функции нет


      return NULL;


    }



    // Читаем индекс (они двухбайтные!!!)


    ReadProcessMemory(


      hProcess,


      baseAddress +
export.AddressOfNameOrdinals


        + nameIndex *
sizeof(WORD),


      &funcIndex,


      sizeof(funcIndex),


      NULL);


  }


  else


  {


    // Функция экспортируется по ординалу


    WORD funcOrdinal =
reinterpret_cast(name);



    if ((funcOrdinal


     || (funcOrdinal >=
export.Base + export.NumberOfFunctions))


    {


      // Такой функции нет


      return NULL;


    }



    // Индекс это ординал минус база


    funcIndex = funcOrdinal - export.Base;


  }



  if ((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/


Не сдавайте скачаную работу преподавателю!
Данный реферат Вы можете использовать для подготовки курсовых проектов.

Поделись с друзьями, за репост + 100 мильонов к студенческой карме :

Пишем реферат самостоятельно:
! Как писать рефераты
Практические рекомендации по написанию студенческих рефератов.
! План реферата Краткий список разделов, отражающий структура и порядок работы над будующим рефератом.
! Введение реферата Вводная часть работы, в которой отражается цель и обозначается список задач.
! Заключение реферата В заключении подводятся итоги, описывается была ли достигнута поставленная цель, каковы результаты.
! Оформление рефератов Методические рекомендации по грамотному оформлению работы по ГОСТ.

Читайте также:
Виды рефератов Какими бывают рефераты по своему назначению и структуре.