Виклик функції в іншому процесі h2>
Сергій холодиль
p>
I just called to
say I love you, p>
And I mean it from
the bottom of my heart. p>
Stevie Wonder p>
Впровадженню DLL так чи інакше (зазвичай у зв'язку з
перехопленням API) присвячено достатньо велику кількість статей. Але в жодній
з тих, які я читав, не йдеться, як ззовні змусити цю DLL зробити
що-небудь корисне. Зазвичай автори обмежуються перехопленням необхідних
API-функцій де-небудь у DllMain і подальшою реакцією на виклики цих самих
функцій. Тим часом, взаємодія з впровадженої DLL дає можливість
коректувати і спрямовувати її роботу і, тим самим, дозволяє домагатися
значно більшого ефекту. p>
Якщо впроваджена DLL створює свій потік, завдання
взаємодії легко вирішується, тому що в цьому випадку можна використовувати будь-які
методи IPC: повідомлення, сокети, іменовані канали, ..., при бажанні можна навіть
COM-сервер зробити:) p>
ПОПЕРЕДЖЕННЯ p>
В описі DllMain сказано, що
деякі функції, у тому числі CreateThread, з неї викликати не можна.
Пояснення «чому вони кажуть, що не можна» можна знайти у Ріхтера (у російському
четвертому виданні це глава «DLL: більш складні методи програмування»,
розділ «Як система упорядковує виклики DllMain»), у нього ж написано, що
насправді можна, якщо обережно. :) Просто при створенні потоку треба не
забувати, що його виконання розпочнеться не раніше, ніж поточний потік покине
DllMain. P>
Але це все більш-менш очевидні і не дуже красиві
(на мій погляд) способи. Мені здається, я знайшов більш цікавий і елегантний
метод. Йому і присвячена ця стаття. P>
Ідея
h2>
Ідея тривіальний. Алгоритм складається всього з чотирьох
кроків (плюс ще один за бажанням): p>
Так чи інакше завантажити в адресний простір
процесу-жертви DLL, що містить потрібну функцію. p>
ПРИМІТКА p>
«Так чи інакше» означає, що DLL може
бути завантажена будь-яким способом. Наприклад, це може бути advapi32.DLL, яку
процес-жертва вантажить сам. Якщо ви хочете, щоб виконувався ваш код, швидше за
за все, DLL доведеться впроваджувати. Опис впровадження DLL дивіться в
додаткові джерела в кінці статті. p>
Отримати адреса завантаження DLL. p>
Отримати адреса функції. p>
Викликати функцію за допомогою CreateRemoteThread. p>
(опціонально) Дочекатися завершення потоку і отримати
що повертає значення функції викликом GetExitCodeThread. p>
А навіщо нам DLL?
p>
При бажанні можна прямо записати весь виконуваний
код в адресний простір процесу-жертви і запустити його тим же
CreateRemoteThread. При великому бажанні можна домогтися, щоб це запрацювало ...
Основна проблема, що чатує на вас на цьому шляху, полягає в тому, що всі
функції, які викликає ваш код, повинні знаходитися точно за тими адресами, куди
передається керування. З урахуванням того, що: p>
код буде розташований у випадковому місці адресного
простору, тому що вам навряд чи вдасться виділити пам'ять за тією ж адресою; p>
DLL можуть бути завантажені на інші адреси, p>
«само собою» нічого не вийде. Щоб домогтися
працездатності коду, потрібно модифіковані використовувані вашим кодом адреси,
тобто, фактично, виконати завдання завантажувача. А навіщо виконувати її вручну,
якщо можна покластися на завантажувач:)? p>
Обмеження
h2>
Використання CreateRemoteThread пов'язано з очевидними
обмеженнями: p>
Підтримується тільки лінійка Windows NT/2000/XP. p>
ПРИМІТКА p>
Існує платна реалізація
CreateRemoteThread для Windows 9x, дивіться сайт http:// www.apihooks.com
розділ «PrcHelp». p>
Прототип викликається функції повинен відповідати
прототипу функції потоку. p>
Крім того, потрібно мати солідні права доступу до
процесу-жертві: p>
PROCESS_CREATE_THREAD
для запуску потоку. p>
PROCESS_VM_READ для визначення адреси. p>
PROCESS_VM_OPERATION + PROCESS_VM_WRITE (дозвіл на
виділення пам'яті і запис в адресний простір процесу) може стати в нагоді,
якщо ви хочете передати викликається функції що-небудь посущественнее, ніж
чотири байти. p>
ПРИМІТКА p>
Найпростіше отримати всі ці права,
створивши процес, але, будучи досить привілейованим користувачем, можна
одержати необхідний доступ і до існуючого процесу. p>
Отримання адреси завантаження DLL
p>
У загальному випадку, за допомогою функцій EnumProcessModules
і GetModuleFileNameEx можна перебрати всі завантажені в процес-жертву модулі,
знайти серед них потрібний і отримати адресу його завантаження. p>
ПРИМІТКА p>
Ці функції є частиною Process
Status API (PSAPI), тому будуть працювати тільки в лінійці Windows
NT/2000/XP. Але оскільки ми вже й так використовуємо CreateRemoteThread, втрачати
нам нічого. p>
Але якщо DLL впроваджувалася за допомогою створення в
процесі-жертві потоку, потокової функцією якого є LoadLibrary, можна
вчинити простіше. У цьому випадку код завершення потоку є що повертається
значенням LoadLibrary, тобто саме адресою завантаження DLL в процесі-жертві. p>
ПОПЕРЕДЖЕННЯ p>
Взагалі-то, як показує практика,
повертається значення LoadLibrary - це не зовсім адреса завантаження DLL. У
деяких випадках у молодших бітах знаходяться якісь прапори. Наприклад, при
виконанні функції LoadLibraryEx з прапором LOAD_LIBRARY_AS_DATAFILE молодший біт
значення, що повертається завжди буде встановлений в 1. p>
Вихід досить простий: оскільки при
завантажити модуль в адресному просторі створюється регіон, а адреси початку
регіонів повинні бути кратні 64К, для отримання «справжнього» адреси завантаження
потрібно просто обнулити два молодших байти. p>
Отримання адреси функції
p>
Є два способи отримати адресу функції: простий і для
справжніх програмістів. :) p>
Простий спосіб
p>
Простий спосіб заснований на тому, що зміщення початку
функції від початку DLL - величина стала, яка не залежить від процесу. Це
означає, що якщо: p>
завантажити у свій процес ту ж DLL; p>
отримати адресу потрібної функції; p>
відняти з адреси функції адреса завантаження DLL; p>
додати до що вийшов зсуву адреса завантаження DLL
в процесі-жертві, p>
то вийде адреса функції в процесі-жертві. p>
ПРИМІТКА p>
Зрозуміло, що якщо DLL в обох процесах
завантажена за однією адресою, то і адреси функцій будуть збігатися. А оскільки
(у нормальних, не надто випендрюйся процесах) системні DLL вантажаться
за одними і тими ж адресами, адреси системних функцій у всіх процесах
однакові. p>
Саме на цьому заснована технологія
впровадження DLL через виклик LoadLibrary в іншому процесі. p>
Якщо з якихось причин DLL вже завантажена в процес,
то, напевно, цей спосіб можна рекомендувати навіть самим-самим справжнім
програмістам. А от якщо DLL потрібно спеціально вантажити, то, по-моєму, знову
виходить негарно. :) P>
Спосіб для справжніх програмістів
p>
Реалізувати функцію GetProcAddressInOtherProcess,
приймає в першому параметрі описувач процесу. Вона буде розбирати таблицю
експорту зазначеної DLL із зазначеного процесу, знаходити там потрібну функцію і
повертати її адресу. p>
Якщо додати функції LoadLibararyInOtherProcess і
FreeLibraryInOtherProcess (які нескладно написати), вийде зовсім
красиво, тому що з чужим процесом можна буде працювати майже так само, як і з
своїм. p>
Саме цей спосіб здається мені цікавим і
елегантним, і саме його реалізації присвячена стаття. p>
Пошук експортується функції в PE-файлі
p>
Як ви, напевно, знаєте, формат всіх виконуваних
файлів в Windows (включаючи DLL, ocx, sys, та інші) називається PE
(розшифровується як Portable Executable, але великого сенсу не несе, просто
назву, нічим не гірше за інших) форматом, а самі файли, відповідно,
PE-файлами. Щоб відшукати адресу потрібної функції в DLL, доведеться розібратися з
тією частиною PE-формату, яка відповідає за експорт. p>
ПРИМІТКА p>
PE-формат досить складний, але, на
щастя, повністю він нам і не потрібен. Якщо вас цікавить більш детальне
опис, дивіться додаткові джерела в кінці статті. p>
Як у PE-файлі дістатися до секції експорту
p>
Будь-PE-файл починається з заголовка DOS, формат
якого відображено в структурі IMAGE_DOS_HEADER. p>
typedef
struct _IMAGE_DOS_HEADER (//DOS
. EXE header p>
... p>
LONG
e_lfanew;// File
address of new exe header p>
) IMAGE_DOS_HEADER, * PIMAGE_DOS_HEADER; p>
З усіх полів цієї структури для нас інтерес представляє
тільки поле e_lfanew, яке є зміщенням від початку файлу (в
термінології PE-формату такі зміщення називаються RVA - Relative Virtual
Address) до PE-заголовка. p>
Формат PE-заголовка представлений структурою
IMAGE_NT_HEADERS (вона визначена з використанням препроцесора і, на даний
момент, відповідає структурі IMAGE_NT_HEADERS32): p>
typedef
struct _IMAGE_NT_HEADERS ( p>
... p>
IMAGE_OPTIONAL_HEADER32 OptionalHeader; p>
)
IMAGE_NT_HEADERS32, * PIMAGE_NT_HEADERS32; p>
З неї нас цікавить тільки поле OptionalHeader,
яка розгортається в ще одну структуру: p>
typedef
struct _IMAGE_OPTIONAL_HEADER ( p>
... p>
IMAGE_DATA_DIRECTORY
DataDirectory [IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; p>
)
IMAGE_OPTIONAL_HEADER32, * PIMAGE_OPTIONAL_HEADER32; p>
І знову, нам потрібно тільки одне поле - DataDirectory, а, точніше, тільки елемент DataDirectory [IMAGE_DIRECTORY_ENTRY_EXPORT]. p>
Структура IMAGE_DATA_DIRECTORY описує розташування
в пам'яті однієї із секцій PE-файлу. Вона визначена в такий спосіб: p>
typedef
struct _IMAGE_DATA_DIRECTORY ( p>
DWORD VirtualAddress;// RVA (зсув від початку
файлу) секції p>
DWORD Size;// Розмір секції p>
)
IMAGE_DATA_DIRECTORY, * PIMAGE_DATA_DIRECTORY; p>
Елемент DataDirectory [IMAGE_DIRECTORY_ENTRY_EXPORT] відноситься до секції експорту. p>
Разом: p>
На початку файлу розташований IMAGE_DOS_HEADER. p>
За зсуву IMAGE_DOS_HEADER:: e_lfanew знаходиться IMAGE_NT_HEADERS. p>
IMAGE_NT_HEADERS:: OptionalHeader.DataDirectory [IMAGE_DIRECTORY_ENTRY_EXPORT]
описує секцію експорту. Він містить
RVA і розмір секції. p>
Як у секції експорту знайти адресу функції
p>
Секція експорту починається зі структури
IMAGE_EXPORT_DIRECTORY. P>
typedef
struct _IMAGE_EXPORT_DIRECTORY ( p>
... p>
DWORD
Base; p>
DWORD
NumberOfFunctions; p>
DWORD
NumberOfNames; p>
DWORD
AddressOfFunctions;// RVA from
base of image p>
DWORD
AddressOfNames;// RVA from
base of image p>
DWORD
AddressOfNameOrdinals;// RVA from base of image p>
)
IMAGE_EXPORT_DIRECTORY, * PIMAGE_EXPORT_DIRECTORY; p>
Тут: p>
AddressOfFunctions - RVA (зсув від початку файлу)
масиву, що містить RVA функцій. p>
AddressOfNames - RVA масиву, що містить RVA імен
функцій. p>
AddressOfNameOrdinals - RVA масиву індексів функцій.
Елемент n цього масиву містить індекс у масиві адрес функцій,
відповідної n-ному елементу в масиві імен функцій. p>
ПОПЕРЕДЖЕННЯ p>
По-перше, елементи цього масиву мають
тип WORD і розмір 2 байти. p>
По-друге, MSDN і стаття Метта Пітрека
«Формати PE і COFF об'єктних файлів» містять одну й ту ж саму помилку, що відноситься
до інтерпретації вмісту цього масиву. Правильно написано в статті
Максима М. Гумерова «Завантажувач PE-файлів» і тут:) p>
NumberOfFunctions - кількість елементів масиву
адрес функцій. p>
NumberOfNames - кількість елементів масиву імен
функцій та масиву індексів функцій. p>
Base - базове значення ордінала експортуються
функцій. Для отримання індексу функції, що експортується з ордіналу, треба
відняти з її ордінала значення Base. p>
У результаті, щоб знайти адресу функції,
експортується на ім'я, потрібно зробити приблизно наступне (в псевдокоді): p>
// Шукаємо в масиві імен функцій
збігається ім'я p>
int nameIndex =
FindFunctionName (AddressOfNames, NumberOfNames, name); p>
// Отримуємо відповідний імені індекс функції p>
WORD
funcIndex = AddressOfNameOrdinals [nameIndex]; p>
// Отримуємо RVA функції p>
DWORD
funcRVA = AddressOfFunctions [funcIndex]; p>
ПОПЕРЕДЖЕННЯ p>
За MSDN і Пітреку, остання строчка
алгоритму має виглядати так: p>
DWORD
funcRVA = AddressOfFunctions [funcIndex - Base]; p>
Де Base - базове значення ордінала.
Як показує практика, Base віднімати не треба. P>
Код
p>
Врешті-решт у мене вийшло три функції. Перша
знаходить секцію експорту: p>
// Визначає RVA секції експорту p>
int
GetExportSectionRVA (HANDLE hProcess, const void * baseAddress) p>
( p>
// Читаємо DOS-заголовок p>
IMAGE_DOS_HEADER dos_header; p>
ReadProcessMemory ( p>
hProcess, p>
baseAddress, p>
& dos_header, p>
sizeof (dos_header), p>
NULL); p>
// Читаємо PE-заголовок p>
IMAGE_NT_HEADERS pe_header; p>
ReadProcessMemory ( p>
hProcess, p>
reinterpret_cast (baseAddress) + dos_header.e_lfanew, p>
& pe_header, p>
sizeof (pe_header), p>
NULL); p>
// Зміщення секції експорту p>
return
pe_header.OptionalHeader.DataDirectory p>
[IMAGE_DIRECTORY_ENTRY_EXPORT]. VirtualAddress; p>
) p>
Друга перебирає масив імен функцій в пошуку
заданого імені: p>
// Шукає в масиві імен
функцій задане ім'я, повертає індекс або -1 p>
int FindName ( p>
HANDLE hProcess, p>
const void * baseAddress, p>
DWORD AddressOfNames, p>
DWORD count, p>
const char * name) p>
( p>
// Для порівняння імені його потрібно прочитати,
для цього потрібно знати розмір p>
int size = lstrlenA (name) + 1; p>
std:: auto_ptr
candidate (new char [size ]); p>
// перебираємо імена в масиві імен функцій p>
for (int index = 0; index
( p>
DWORD nameRVA; p>
// Читаємо адреса початку рядка p>
ReadProcessMemory ( p>
hProcess, p>
reinterpret_cast (baseAddress) p>
+ AddressOfNames +
index * sizeof (DWORD), p>
& nameRVA, p>
sizeof (nameRVA), p>
NULL); p>
// Читаємо рядок p>
ReadProcessMemory ( p>
hProcess, p>
reinterpret_cast (baseAddress) + nameRVA, p>
candidate.get (), p>
size, p>
NULL); p>
if (strcmp (name,
candidate.get ()) == 0) p>
( p>
// Вона! Звалює:) p>
return index; p>
) p>
) p>
// Такий функції немає p>
return -1; p>
) p>
Третя функція використовує перші два і знаходить потрібну
функцію у зазначеній DLL в зазначеному процесі: p>
// Знаходить потрібну функцію в
зазначеної DLL у зазначеному процесі. p>
void * GetProcAddress (HANDLE hProcess, HMODULE hLib, const char * name) p>
( p>
// Нам потрібен саме адреса завантаження! А
результат роботи p>
// LoadLibrary буває іноді несподіваним .. p>
char * baseAddress = reinterpret_cast p>
(reinterpret_cast (hLib) & 0xFFFF0000); p>
// Зміщення секції експорту p>
int export_offset =
GetExportSectionRVA (hProcess, baseAddress); p>
if (export_offset <= 0) p>
( p>
// Якісь проблеми з експортом p>
return NULL; p>
) p>
// Читаємо заголовок секції експорту p>
IMAGE_EXPORT_DIRECTORY export; p>
ReadProcessMemory ( p>
hProcess, p>
baseAddress +
export_offset, p>
& export, p>
sizeof (export), p>
NULL); p>
// Індекс у масиві функцій p>
WORD funcIndex = -1; p>
if
(reinterpret_cast (name)> 0x0000ffff) p>
( p>
// Функція експортується по імені. Шукаємо
ім'я p>
int nameIndex = FindName ( p>
hProcess, p>
baseAddress, p>
export.AddressOfNames, p>
export.NumberOfNames, p>
name); p>
if (nameIndex <0) p>
( p>
// Такий функції немає p>
return NULL; p>
) p>
// Читаємо індекс (вони двухбайтние !!!) p>
ReadProcessMemory ( p>
hProcess, p>
baseAddress +
export.AddressOfNameOrdinals p>
+ nameIndex *
sizeof (WORD), p>
& funcIndex, p>
sizeof (funcIndex), p>
NULL); p>
) p>
else p>
( p>
// Функція експортується за ордіналу p>
WORD funcOrdinal =
reinterpret_cast (name); p>
if ((funcOrdinal <
export.Base) p>
| | (funcOrdinal> =
export.Base + export.NumberOfFunctions)) p>
( p>
// Такий функції немає p>
return NULL; p>
) p>
// Індекс це ордінал мінус база p>
funcIndex = funcOrdinal - export.Base; p>
) p>
if ((funcIndex <0) | |
(funcIndex> = export.NumberOfFunctions)) p>
( p>
// Такий функції немає p>
return NULL; p>
) p>
// Читаємо адреса p>
DWORD funcRVA; p>
ReadProcessMemory ( p>
hProcess, p>
baseAddress +
export.AddressOfFunctions + funcIndex * sizeof (DWORD), p>
& funcRVA, p>
sizeof (funcRVA), p>
NULL); p>
// Результат це базовий адреса + RVA p>
return (baseAddress + funcRVA); p>
) p>
ПРИМІТКА p>
Для оптимізації можна було
б спочатку скопіювати в свій процес всю секцію експорту (розмір секції
зберігається в
IMAGE_NT_HEADERS:: OptionalHeader.DataDirectory [IMAGE_DIRECTORY_ENTRY_EXPORT]. Size),
а потім вже її розбирати. Але, оскільки помітних оку затримок не виникає,
я зупинився на поточній реалізації. p>
Приклад
p>
Як приклад я написав три програми:
aggressor.exe, victim.exe і insider.dll. Victim і insider абсолютно пасивні,
всі дії виконуються aggressor-му. Aggressor: p>
запускає victim.exe; p>
завантажує в нього insider.dll; p>
отримує адреси трьох експортованих функцій; p>
викликає ці функції; p>
вивантажує insider.dll з victim.exe. p>
ПРИМІТКА p>
Щоб це дійсно працювало, треба
покласти всі три виконуваних модуля в один каталог. p>
Для реалізації перерахованих дій, та й взагалі на
майбутнє, в aggressor реалізовані наступні корисні функції: p>
namespace OtherProcess p>
( p>
// p>
// Викликає функцію із заданого процесу,
повертає p>
// описувач потоку, що цю функцію
виконує p>
HANDLE AsynchronousCall ( p>
HANDLE hProcess, p>
void * address, p>
void * parameter, p>
DWORD * pid); p>
// p>
// Викликає функцію із заданого процесу,
чекає завершення її роботи p>
bool SynchronousCall ( p>
HANDLE hProcess, p>
void * address, p>
void * parameter, p>
DWORD * result); p>
// p>
// Завантажує DLL в зазначений процес p>
HMODULE LoadLibrary (HANDLE
hProcess, const TCHAR * path); p>
// p>
// вивантажувати DLL в зазначеному процесі p>
void FreeLibrary (HANDLE
hProcess, HMODULE hLib); p>
// p>
// Знаходить потрібну функцію у зазначеній DLL в
зазначеному процесі p>
void * GetProcAddress (HANDLE hProcess, HMODULE
hLib, const char * name); p>
); p>
Призначення функцій, я сподіваюся, зрозуміло з їх
назв і коротких коментарів. Розуміння реалізації також не повинно викликати
труднощів, прокоментовано все досить докладно, та й сам код не такий
вже складний. Успішних вам дзвінків! P>
Список літератури h2>
Джеффрі Ріхтер, «Programming Application for Microsoft Windows»,
четверте видання. p>
Тихомиров В.А. «Перехват API-функцій в Windows
NT/2000/XP ». p>
Метт Пітрек «Формати PE і COFF об'єктних файлів» p>
Максим М. Гумер «Завантажувач PE-файлів» p>
Для підготовки даної роботи були використані
матеріали з сайту http://www.rsdn.ru/
p>