Перехват API-функцій в Windows NT/2000/XP h2>
Тихомиров В.А. p>
Системні програмісти, які працювали
під MS DOS, прекрасно пам'ятають технологію перехоплення системних переривань,
дозволяла брати під контроль практично всі процеси, що відбувалися в улюбленій
операційній системі. p>
З переходом на Windows використання системних
ресурсів програмістами у великому обсязі стало здійснюватися через функції
API, і у багатьох «сіспрогов» став виникати питання: «чи існують в Windows
технології перехоплень цих системних функцій? »Особливий інтерес це викликає
стосовно високозахищену ОС, виконаним на ядрі NT. Дана стаття
найдокладнішим чином, з діючими прикладами покаже практичну реалізацію
такої технології (передбачається, що читач знайомий з принципами системного
програмування в Windows і вміє застосовувати в своїй роботі Visual C ++). p>
Що таке «перехоплення API-функцій» h2>
Перехоплення системної функції API полягає в зміні
деякого адреси в пам'яті процесу чи деякого коду в тілі функції таким
чином, щоб при виклику цієї самої API-функції управління передавалося не їй,
а вашій функції, підміняє системну. Ця функція, працюючи замість системної,
виконує якісь заплановані вами дії, а потім, в залежності від
вашого бажання, або викликає оригінальну функцію, або не викликає її взагалі.
Перехоплення функцій є дуже корисним засобом у тому випадку, якщо ви хочете
відстежити, змінити або заблокувати деякі конкретні дії програми.
p>
Перехоплення функцій чужого процесу Кращий час
здійснювати впровадженням власної DLL з функцією-двійником в адресний
простір того процесу, контроль над функціями API якого ви хочете
встановити. При написанні двійників функцій слід особливу увагу звернути на
угоди про виклики функцій __cdecl і __stdcall. У __cdecl функціях
мається на увазі, що параметри кладуться в стек справа наліво, і викликає
функція очищає стек від аргументів. У __stdcall функціях мається на увазі, що
параметри кладуться в стек справа наліво, але стек від аргументів очищає
викликається функція. Крім того, слід враховувати, що в Windows API багато
функції зустрічається в 2-х примірниках: ANSI і UNICODE. Перші позначаються
суфіксом A: наприклад MessageBoxA, другий - суфіксом W - наприклад MessageBoxW.
p>
Розглянемо два методи перехоплення API функцій: p>
Записуйте безпосередньо в код функції. p>
Підміна адреси функції в таблиці імпорту. p>
Метод 1. Перехоплення API безпосередній записом у код
системної функції.
p>
Прийом полягає в тому, щоб в початок
перехоплюваних функції записати команду jmp ваша_функція_двойнік або
еквівалентну їй. Затираємо байти бажано десь зберегти. Після
виклику виправленою функції додатком управління буде передано вашої
функції. Вона повинна коректно обробити стек, тобто витягти передані їй
параметри і провести необхідні вам дії. Потім, якщо ви збираєтеся
викликати оригінальну функцію, необхідно відновити затерті байти на початку
оригінальній функції. Викликати її, передавши їй всі необхідні параметри. Після
повернення з оригінальної функції, необхідно знову в початок коду функції
записати команду переходу на вашу функцію. Повернути управління викликала
програмі. p>
Гідність даного методу полягає в тому, що він
дозволяє перехоплювати будь-які функції, а не тільки ті, котрі зазначені в
таблиці імпорту. p>
Недолік: в багатопоточних додатках може
виникнути така ситуація, коли один потік викликав перехоплену вами функцію,
управління було передано функції-двійника, вона відновила оригінальне початок
функції, але у цей момент паралельно виконувати потік справив виклик тієї ж
функції. В результаті управління буде передано одразу оригінальній функції,
минаючи вашу :(. p>
Розглянемо приклад програми (у вигляді DLL-файлу),
перехоплює функцію MessageBoxA методом 1. p>
Для роботи нам будуть потрібні наступні заголовки
файли: p>
# include
"stdafx.h" p>
# include
"intercpt.h" p>
Далі підготуємо структуру, яка містить код далекого
переходу на нашу функцію-двійник. Практика показала, що замість звичайного jmp
краще застосовувати комбінацію p>
push xxxxxxxx p>
ret p>
де ХХХХХХХХ - це адреса функції-двійника. У
результаті структура, яка буде зберігати потрібний код переходу, виглядає так: p>
struct jmp_far p>
( p>
BYTE
instr_push;// тут буде код інструкції push p>
DWORD
arg;// аргумент push p>
BYTE
instr_ret;// тут буде код інструкції ret p>
); p>
Задамо потрібні змінні: p>
BYTE old [6];// область для зберігання
6-ти затираємо байт початку функції p>
DWORD adr_MessageBoxA// майбутню адресу
оригінальній функції p>
DWORD written;// Допоміжна
мінлива p>
jmp_far jump;// тут буде машинний
код інструкції переходу p>
Головна функція DLL буде виглядати наступним чином: p>
BOOL APIENTRY DllMain (HANDLE hModule, DWORD ul_reason_for_call, p>
LPVOID lpReserved) p>
( p>
// Якщо система підключає
DLL до якого-небудь процесу, p>
// вона спочатку викличе
головну функцію DLL з параметром p>
// DLL_PROCESS_ATTACH, на
що ми відразу викличемо нашу функцію p>
// InterceptFunctions,
яка виробить підміну стандартної API функції p>
// MessageBoxA нашої функцією Intercept_MessageBoxA (див. нижче) p>
if (ul_reason_for_call = =
DLL_PROCESS_ATTACH) p>
( p>
InterceptFunctions (); p>
) p>
return TRUE; p>
) p>
Функція, яку ми тільки що викликали і яка
виконує основну хитрість, перехоплення API перезаписом початкових байт
стандартної функції, виглядає таким чином: p>
void InterceptFunctions (void) p>
( p>
DWORD op; p>
// спочатку отримаємо абсолютний адреса функції для перехоплення p>
adr_MessageBoxA =
(DWORD) GetProcAddress (GetModuleHandle ( "user32.dll "), p>
"MessageBoxA "); p>
if (adr_MessageBoxA == 0) p>
( p>
MessageBox (NULL, "Can` t
get adr_MessageBoxA, "Error!", 0); p>
return; p>
) p>
// Задамо машинний код інструкції переходу,
який потім впишемо p>
// в початок отриманого адреса: p>
jump.instr_push = 0x68; p>
jump.arg = (DWORD) & Intercept_MessageBoxA; p>
jump.instr_ret = 0xC3; p>
// Прочитаємо і збережемо перші оригінальні 6
байт стандартної API функції p>
ReadProcessMemory (GetCurrentProcess (), (void *)
adr_MessageBoxA, p>
(void *) & old,
6, & written); p>
// Запишемо команду переходу
на нашу функцію поверх цих 6-ти байт p>
WriteProcessMemory (GetCurrentProcess (), (void *) adr_MessageBoxA, p>
(void *) & jump,
sizeof (jmp_far), & written); p>
) p>
Тепер подивимося, як виглядає сама функція-двійник.
Вона повинна замінити стандартну MessageBoxA, тому її тип і склад параметрів
повинні точно відповідати оригіналу: p>
// дане визначення
аналогічно __srtdcall p>
BOOL WINAPI
Intercept_MessageBoxA (HWND hwnd, char * text, char * hdr, UINT utype) p>
( p>
// Спочатку відновлюємо 6 перших байт
функції. Це не обов'язкове p>
// дію, просто ми вирішили пожартувати над
користувачем, і всі p>
// повідомлення функції MessageBoxA переробити
на свої, тому нам доведеться p>
// викликати оригінальну функцію, а для цього
слід відновити її адреса: p>
WriteProcessMemory (GetCurrentProcess (),
(void *) adr_MessageBoxA, p>
(void *) & old, 6, & written); p>
// Тут ви можете повеселитися від душі і
виконати будь-які, що прийшли вам p>
// в голову дії. Ми просто замінили
повідомлення функції на своє: p>
char * str = "Hi From
MessageBOX !!!!"; p>
// Викликаємо оригінальну функцію через вказівник p>
((BOOL (__stdcall *) (HWND,
char *, char *, UINT)) adr_MessageBoxA) (hwnd, p>
str, hdr, utype); p>
// Знову замінюємо 6 байт функції на команду
переходу на нашу функцію p>
WriteProcessMemory (GetCurrentProcess (),
(void *) adr_MessageBoxA, p>
(void *) & jump,
6, & written); p>
return TRUE; p>
) p>
Якщо відкомпілювати цей код як DLL, то отримаємо
файл, який надалі (см.ниже) варто впровадити в процес, в якому ми
хочемо перехопити API MessageBoxA. p>
Метод 2. Перехоплення API через таблицю імпорту.
p>
Прийом полягає в заміні адреси функції в таблиці
імпорту на адресу функції-двійника. Для розуміння даного методу буде потрібно
знання формату PE виконуваних файлів Windows. Як відомо, більшість
додатків викликає функції з dll через таблицю імпорту, що є
після завантаження exe файлу в пам'ять списки адрес функцій, що імпортуються з
різних Dll. Скомпільованій виклик функції через таблицю імпорту виглядає
наступним чином: p>
Call
dword ptr [address_of_function] p>
або щось на зразок. Тут address_of_function --
адреса в таблиці імпорту, за яким знаходиться адреса викликається функції. (Тим,
хто не знайомий зі структурою PE EXE заголовка файла, рекомендуємо заглянути в
Інтернет за відповідною інформацією.) P>
При перехоплення API через таблицю імпорту треба: p>
знайти в таблиці імпорту елемент
IMAGE_IMPORT_DESCRIPTOR, що відповідає тій DLL, з якої імпортована
функція; p>
дізнатися адресу перехоплюваних функції за допомогою
GetProcAddress; p>
перебираючи елементи масиву, на який вказує поле
FirstThunk, знайти адресу перехоплюваних функції; p>
запам'ятати цю адресу де-небудь і записати на його
місце адреса функції-двійника. p>
Тепер при виклику підміненого функції спочатку буде
викликатися функція-двійник. Після цього вона може викликати (або не викликати) оригінальну
функцію. p>
Гідність даного методу в тому, що він буде
коректно працювати в багатопотоковому додатку, коли декілька потоків
одночасно викликають підміненого функцію. Так само даний метод буде працювати в
ОС WINDOWS 9.x. p>
Недолік - не всі функції викликаються через таблицю
імпорту. p>
Нижче наведено приклад програми, аналогічної
наведеної вище, але використовує другий метод перехоплення функції: p>
DWORD adr_MessageBoxA; p>
BOOL APIENTRY DllMain (HANDLE hModule, DWORD ul_reason_for_call, p>
LPVOID lpReserved) p>
( p>
if (ul_reason_for_call ==
DLL_PROCESS_ATTACH) p>
InterceptFunctions (); p>
return TRUE; p>
) p>
// Ця функція шукає в
таблиці імпорту -. idata потрібну адресу і змінює на p>
// адреса процедури-двійника
p>
void
InterceptFunctions (void) p>
( p>
// Початок відображення в пам'яті процесу p>
BYTE * pimage = (BYTE *) GetModuleHandle (NULL);
p>
BYTE * pidata; p>
// Стандартні структури опису PE
заголовка p>
IMAGE_DOS_HEADER * idh; p>
IMAGE_OPTIONAL_HEADER * ioh; p>
IMAGE_SECTION_HEADER * ish; p>
IMAGE_IMPORT_DESCRIPTOR * iid; p>
DWORD * isd;// image_thunk_data
dword p>
// Отримуємо покажчики на стандартні структури даних PE заголовка p>
idh = (IMAGE_DOS_HEADER *) pimage; p>
ioh =
(IMAGE_OPTIONAL_HEADER *) (pimage + idh-> e_lfanew p>
+ 4 + sizeof (IMAGE_FILE_HEADER )); p>
ish =
(IMAGE_SECTION_HEADER *) ((BYTE *) ioh + sizeof (IMAGE_OPTIONAL_HEADER )); p>
// якщо не виявлений магічний код, то в цієї програми немає PE
заголовка p>
if (idh-> e_magic! = 0x5A4D) p>
( p>
MessageBox (NULL, "Not exe
hdr "," Error! ", 0); p>
return; p>
) p>
// шукаємо секцію. idata p>
for (int i = 0; i <16; i ++) p>
if (strcmp ((char *) ((ish +
i) -> Name), ". idata") == 0) break; p>
if (i == 16) p>
( p>
MessageBox (NULL, "Unable
to find. idata section "," Error! ", 0); p>
return; p>
) p>
// Отримуємо адреса секції. idata (першого елемента IMAGE_IMPORT_DESCRIPTOR) p>
iid =
(IMAGE_IMPORT_DESCRIPTOR *) (pimage + (ish + i) -> VirtualAddress); p>
p>
// Отримуємо абсолютний адреса функції для перехоплення p>
adr_MessageBoxA = (DWORD) GetProcAddress ( p>
GetModuleHandle ( "user32.dll"),
"MessageBoxA "); p>
if (adr_MessageBoxA == 0) p>
( p>
MessageBox (NULL, "Can` t
get addr_MessageBoxA "," Error! ", 0); p>
return; p>
) p>
// У таблиці імпорту шукаємо відповідний
елемент для p>
// бібліотеки user32.dll p>
while (iid-> Name)// до тих пір поки поле
структури не містить 0 p>
( p>
if (strcmp ((char *) (pimage + iid-> Name),
"USER32.dll") == 0) break; p>
iid ++; p>
) p>
// Шукаємо в IMAGE_THUNK_DATA потрібну адресу p>
isd = (DWORD *) (pimage +
iid-> FirstThunk); p>
while (* isd! = adr_MessageBoxA
& & * Isd! = 0) isd ++; p>
if (* isd == 0) p>
( p>
MessageBox (NULL,
"adr_MessageBoxA not found in. idata", "Error!", 0); p>
return; p>
) p>
p>
// Замінюємо адресу на свою функцію p>
p>
DWORD buf =
(DWORD) & Intercept_MessageBoxA; p>
DWORD op; p>
p>
// Звичайно сторінки в цій області недоступні
для запису p>
// тому примусово дозволяємо запис p>
VirtualProtect ((void *) (isd), 4, PAGE_READWRITE,
& op); p>
p>
// Пишемо нова адреса p>
WriteProcessMemory (GetCurrentProcess (),
(void *) (isd), p>
(void *) & buf, 4, & written); p>
// відновлюємо первісну захист
області за записом p>
VirtualProtect ((void *) (isd), 4, op, & op); p>
// якщо записати не вдалося - на жаль, все пішло прахом ... p>
if (written! = 4) p>
( p>
MessageBox (NULL, "Unable
rewrite address "," Error! ", 0); p>
return; p>
) p>
) p>
А ось так виглядає символів функція: p>
BOOL
WINAPI Intercept_MessageBoxA (HWND hwnd, char * text, p>
char * hdr, UINT utype) p>
( p>
// тут
ви виконуєте будь-які свої дії p>
char * str =
"Hi From MessageBOX !!!!"; p>
// викликаємо оригінальну функцію через вказівник p>
((BOOL (__stdcall *) (HWND, char *, char *,
UINT)) adr_MessageBoxA) (hwnd, p>
str,
hdr, utype); p>
return TRUE; p>
) p>
Впровадження коду в чужій процес в Windows NT
h2>
Тепер залишилося показати, як вищеописані DLL можна
впровадити в процес, обраний в якості жертви експерименту. (Не зайве
нагадати, що для нашого прикладу процес-жертва повинен мати вікна зі
стандартними повідомленнями MessageBox). p>
Впровадити код - значить, записати деяку програму в
чужий процес і виконати її від імені цього процесу. Таким чином, впроваджений
код стає частиною процесу і отримує доступ до всіх ресурсів, якими
має процес. На відміну від DOS, сімейство ОС Windows (на ядрі NT) --
операційні системи з пам'яттю, що розділяється, тобто кожен додаток виконується в
своєму адресному просторі, не перетинається з іншими, і не має
безпосереднього доступу до пам'яті чужого програми. Таким чином, впровадження
коду є нетривіальною завданням. Існує декілька способів впровадити
свій код: p>
1. «Вручну». P>
2. За допомогою хуков. P>
Впровадження 1
p>
Розглянемо найбільш ефективний, на наш погляд, спосіб
впровадження - перша. Він полягає в записі короткого ділянки машинного коду в
пам'ять процесу, який повинен приєднати DLL до цього процесу, запустити її
код, після чого Dll зможе виконувати будь-які дії від імені даного процесу.
У принципі можна і не приєднувати DLL, а реалізувати необхідні дії у
впроваджуване машинному коді, але це буде занадто трудомістким завданням, оскільки
всі зміщення для даних втратять сенс, і ви не зможете коректно звернутися до
ним, не настроївши відповідним чином зміщення (морока: (). p>
При приєднанні DLL завантажувач автоматично
встановлює всі зміщення згідно з адресою, по якому завантажена DLL.
Слід також відзначити, що для запису коду в процес і його виконання
необхідно відкрити процес з доступом як мінімум: p>
PROCESS_CREATE_THREAD | PROCESS_VM_WRITE | PROCESS_VM_OPERATION. p>
ПОПЕРЕДЖЕННЯ p>
При реалізації даного методу
необхідно вказати компілятору вирівнювати структури ПОБАЙТОВО. Інакше
структура з машинним кодом буде містити абсолютно не той код, що був
запланований. p>
Загальна схема впровадження: p>
Відкрити процес (OpenProcess). p>
Виділити в ньому пам'ять (VirtualAllocEx - доступно
тільки для WinNT). p>
Записати впроваджуваний код в цю пам'ять
(WriteProcessMemory). p>
Виконати його (CreateRemoteThread). p>
Впроваджувані машинний код повинен (за нашим сценарієм)
здійснити такі дії: p>
call LoadLibrary - викликати функцію LoadLibrary з
kernel32.dll для завантаження приєднуваної бібліотеки (однієї з розбиралися
вище). p>
Call ExitThread - викликати функцію ExitThread з
kernel32.dll для коректного завершення даного потоку. p>
У WinNT стартовий адреса відображення системних DLL
(user32, kernel32 і т. д.) один і той же для всіх додатків. Це означає, що
адреса деякої функції з системної DLL в одному додатку буде актуальне і в
іншому додатку. Тобто точки входу функцій системних DLL завжди одні й ті
ж. p>
Нижче наведено приклад процедури, впроваджувальною dll з
заданим ім'ям у процес з заданим PID (ідентифікатором процесу) (їх можна
спостерігати в закладці «процеси» диспетчера завдань або отримати за допомогою
стандартних API-функцій). p>
// структура описує поля,
в яких міститься код впровадження p>
struct INJECTORCODE p>
( p>
BYTE
instr_push_loadlibrary_arg;// інструкція push p>
DWORD loadlibrary_arg;// аргумент push p>
WORD instr_call_loadlibrary;// інструкція call [] p>
DWORD
adr_from_call_loadlibrary; p>
BYTE instr_push_exitthread_arg; p>
DWORD exitthread_arg; p>
WORD instr_call_exitthread; p>
DWORD adr_from_call_exitthread; p>
DWORD addr_loadlibrary; p>
DWORD addr_exitthread;// адреса функції ExitTHread p>
BYTE libraryname [100];// ім'я та шлях до завантажується бібліотеці p>
); p>
BOOL InjectDll (DWORD pid, char * lpszDllName) p>
( p>
HANDLE hProcess; p>
BYTE * p_code; p>
INJECTORCODE cmds; p>
DWORD wr, id; p>
// відкрити процес з потрібним доступом p>
hProess = OpenProcess (PROCESS_CREATE_THREAD | PROCESS_VM_WRITE | p>
PROCESS_VM_OPERATION, FALSE,
pid); p>
if (hProcess == NULL) p>
( p>
MessageBoxA (NULL, "You
have not enough rights to attach dlls ", p>
"Error!", 0); p>
return FALSE; p>
) p>
p>
// зарезервувати пам'ять у процесі p>
p_code = (BYTE *) VirtualAllocEx (hProcess, 0,
sizeof (INJECTORCODE), p>
MEM_COMMIT, PAGE_EXECUTE_READWRITE); p>
if (p_code == NULL) p>
( p>
MessageBox (NULL, "Unable
to alloc memory in remote process ", p>
"Error!", 0); p>
return FALSE; p>
) p>
// ініціалізувати машинний код p>
cmds.instr_push_loadlibrary_arg = 0x68;
//машинний код інструкції push p>
cmds.loadlibrary_arg = (DWORD) ((BYTE *) p_code p>
+ offsetof (INJECTORCODE, libraryname )); p>
p>
cmds.instr_call_loadlibrary =
0x15ff;// машинний код інструкції call p>
cmds.adr_from_call_loadlibrary
= P>
(DWORD) (p_code +
offsetof (INJECTORCODE, addr_loadlibrary )); p>
p>
cmds.instr_push_exitthread_arg
= 0x68; p>
cmds.exitthread_arg = 0; p>
p>
cmds.instr_call_exitthread =
0x15ff; p>
cmds.adr_from_call_exitthread =
p>
(DWORD) (p_code +
offsetof (INJECTORCODE, addr_exitthread )); p>
p>
cmds.addr_loadlibrary = p>
(DWORD) GetProcAddress (GetModuleHandle ( "kernel32.dll"),
"LoadLibraryA "); p>
p>
cmds.addr_exitthread = p>
(DWORD) GetProcAddress (GetModuleHandle ( "kernel32.dll"), "ExitThread "); p>
p>
if (strlen (lpszDllName)> 99) p>
( p>
MessageBox (NULL, "Dll Name
too long "," Error! ", 0); p>
return FALSE; p>
) p>
strcpy ((char *) cmds.libraryname,
lpszDllName); p>
p>
/* Після ініціалізації cmds в мнемоніки асемблера виглядає наступним p>
чином: p>
push adr_library_name; аргумент ф-ції loadlibrary p>
call dword ptr
[loadlibrary_adr]; викликати LoadLibrary p>
push exit_thread_arg; аргумент для ExitThread p>
call dword ptr
[exit_thread_adr]; викликати ExitThread p>
*/ p>
p>
// записати машинний код по
зарезервованого адресою p>
WriteProcessMemory (hProcess, p_code,
& cmds, sizeof (cmds), & wr); p>
p>
// виконати машинний код p>
HANDLE z =
CreateRemoteThread (hProcess, NULL, 0, p>
(unsigned long (__stdcall
*) (void *)) p_code, 0, 0, & id); p>
// чекати завершення віддаленого потоку p>
WaitForSingleObject (z, INFINITE); p>
// звільнити пам'ять p>
VirtualFreeEx (hProcess, (void *) p_code,
sizeof (cmds), MEM_RELEASE); p>
return TRUE; p>
) p>
Впровадження 2
p>
Другий спосіб впровадження виконуваного коду (через хукі)
найбільш простий у використанні. Він заснований на технології хуков, а саме: якщо
встановити хук на потік чужого процесу, то, як тільки потік отримає
повідомлення, що відповідає заданому типу хука, система автоматично підключить
DLL c хуком до даного процесу. Недоліком цього способу в тому, що не можна
впровадити DLL в процес, який не має черги повідомлень. Дана DLL буде
приєднана до чужого процесу лише до тих пір, поки запущена програма,
встановила хук. Як тільки ви закінчите цю програму, dll автоматично
буде відключена. Перший спосіб позбавлений таких недоліків. P>
З іншого боку, перший спосіб буде працювати лише в
WinNT, через використання функції VirtualAllocEx, яка резервує
пам'ять в заданому (відмінному від того, в якому відбувається виклик цієї функції)
процесі. Теоретично, цю проблему можна обійти, якщо писати код в
деяку частину відображення exe-файлу чужого процесу, наприклад в заголовок
DOS, який після завантаження не використовується. Але ОС не завжди дозволяє писати в
цю область пам'яті, навіть якщо спробувати змінити права за допомогою
VirtualProtextEx. P>
Є ще й третій спосіб впровадження, але він найбільш
небезпечний, тому що може призвести до краху системи. За допомогою даного методу ОС
сама впроваджує зазначену dll у всі без винятку процеси операційної
системи, навіть захищені. Для реалізації необхідно прописати в реєстрі по дорозі
Hkey_local_machinesoftwaremicrosoftwindowsntcurrentversionwindows в ключі
AppInit_DLLs повний шлях до своєї dll. P>
Як налагоджувати такі викрутаси
p>
Більшість програмістів для налагодження своїх програм
використовують вбудовані відладчик компіляторів. Вони прості у використанні і
задовольняють більшості вимог, що пред'являються при налагодженні. Але якщо
деякий програмний код буде впроваджено і виконаний в рамках іншого,
стороннього процесу вбудований відладчик використовувати дуже важко. Для цих
цілей зручно застосувати системний відладчик SoftIce, який вантажиться раніше
операційної системи, працює в нульовому кільці і тому має доступ до будь-яких
об'єктах ОС. Давай обговоримо, як налагоджувати впроваджений код, який виконує перехоплення API
функцій всередині стороннього процесу. p>
Всі види налагодження умовно можна розділити на 3 групи: p>
налагодження коду завантажувача (на асемблері), який,
будучи впровадили в чужий процес, виконується як окремий потік і приєднує
Dll від імені процесу. p>
налагодження функцій, що виконуються при старті даної Dll.
Зазвичай це функції, які виконують підміну коду всередині тіла API-функції для
передачі управління функції-двійника. p>
налагодження функцій-двійників, які отримують управління
при виклику перехопленою API-функції. p>
Налагодження коду завантажувача
p>
Отже, є 2 процесу: p>
Процес, який впроваджує код. Позначимо його П1. p>
Процес, в який впроваджують код. Позначимо його П2. p>
Завдання полягає в тому, щоб поставити крапку зупину
в П2 перед виконанням впровадженого коду. Спочатку невідомо, за яким
адресою буде впроваджено код в П2. При цьому передбачається, що П2 вже завантажений і
висить десь в пам'яті. Для простоти запускаємо П1 в будь-якому вбудованому
відладчик і трассіруем, для того, щоб дізнатися за якою адресою в П2 буде
виділена пам'ять. Дізнавшись цю адресу, включаємо SoftIce (ctrl + d). Підключаємося до П2
(addr П2-name), при цьому SoftIce встановить контекст адрес, відповідний
процесу П2. Встановлюємо точку зупину по дізнатися адресу (bpx address).
Закриваємо SoftIce (ctrl + d). Виконуємо П1. При цьому він створює потік в П2. Коли
цей потік починає крутитися, на першому дії впровадженого коду
вискакує вікно SoftIce. p>
Налагодження функцій, що виконуються при старті DLL
p>
У прикладі це функція InterceptFunctions бібліотеки
intercpt.dll, яка викликається з DllMain при приєднанні до бібліотеки
процесу і виконує перехоплення функцій. p>
Для початку необхідно відкомпілювати цю бібліотеку з
налагоджування, яку надалі SoftIce буде використовувати для
виведення інструкцій на мові С. В MS Visual C це робиться так:
Project-> Settings-> C/C + + список Debug Info - там необхідно вибрати тип
символьної інформації - Program database for Edit and Continue, а так само Project-> Settings-> Link
список Category -> debug, встановити галочку в поле Debug info і вибрати
формат налагоджувальної інформації, наприклад Microsoft Format. p>
Альтернатива - можна просто встановити тип
конфігурації проекту, при цьому всі параметри для отримання налагоджувальної
інформації будуть встановлені автоматично. Це робиться так: Build-> Set
Active Configuration -> Win32 Debug. P>
Тепер можна приступати до використання SoftIce. Для
початку потрібно завантажити в відладчик символьну інформацію з даної Dll, при цьому
сама dll ще завантажена не буде. Символьна інформація знадобиться
згодом, для встановлення точок зупинення і подання кодів на мові
високого рівня. Це робиться за допомогою утиліти Symbol Loader з комплекту
SoftIce. P>
Спочатку необхідно відкрити модуль dll (пункт
File-> Open Module). P>
Потім необхідно завантажити його в відладчик (Module
-> Load). При успішному виконанні всіх цих операцій на екрані Symbol Loader
має бути щось на кшталт цього: p>
p>
Малюнок 1 p>
Тепер приступимо до головного. Необхідно поставити
точку зупину на функцію InterceptFunctions з dll, при цьому сама Dll поки що
не приєднають до процесу! Запускаємо SoftIce. Створюємо точку зупину по
символьному імені: p>
bpx InterceptFunctions, (InterceptFunctions --
символьне ім'я функції з таблиці символів. Щоб проглянути всю таблицю,
можна скористатися командою sym). Тепер за допомогою написаної раніше
програми впроваджуємо цю dll в зазначений процес. Повинно відбутися наступне: Dll
приєднується до процесу, виконується DllMain, яка викликає
IntercptFunctions і в цей момент повинен відбутися зупинка і вилізти вікно
відладчика. При цьому весь код з dll буде представлений на мові високого рівня. P>
Налагодження функцій - двійників, що одержують керування при
виклику перехоплених API функцій
p>
Для початку необхідно завантажити символьну інформацію
про Dll перехоплення. p>
У даному прикладі це intercpt.dll. p>
У утиліті Symbol Loader вибираємо File-> Open Module,
потім, в меню Module-> Load, завантажуємо символьну інформацію в відладчик. Dll
поки що не приєднана до жодного процесу. p>
Далі, знаючи імена функцій-двійників ставимо крапку
зупину на ім'я функції. Наприклад, функція-двійник Intercept_MessageBoxA,
яка буде викликатися кожного разу, коли відбудеться виклик функції MessageBoxA
з програми. Поставимо крапку зупинки на неї - у вікні відладчика набираємо: bpx
Intercept_MessageBoxA. P>
Тепер можна приєднати intercpt.dll до якого-небудь
процесу. p>
Коли цей процес викличе перехоплену функцію
MessageboxA, управління буде передано на функцію Intercpt_MessageBoxA і
спрацює точка зупину. p>
Тестування
p>
Щоб випробувати все вищесказане у справі, спочатку
підшукайте на своєму комп'ютері який-небудь додаток, що має в своєму складі
вікна повідомлень типу MessageBox. Піддослідні додаток написано нами самими, воно
називається MESS.EXE і виводить один за одним три вікна повідомлень, колаж з
яких показано на малюнку: p>
p>
Малюнок 2 p>
Потім, відкомпілюйте приклади впроваджуваних DLL,
описаних вище. Результат компіляції ми назвали у себе METOD1.DLL і METOD2.DLL. P>
Відкомпілюйте приклад процедури впровадження цих DLL в
код зовнішнього процесу. Для працездатності цієї процедури до неї потрібно
додати код головного модуля програми, щось на кшталт: p>
int main (int argc, char * argv []) p>
( p>
if (argc <3) p>
( p>
printf ( "Parameters: PID,
Dllname "); p>
getch (); p>
return 0; p>
) p>
InjectDll (atol (argv [1]),
argv [2 ]); p>
return 0; p>
) p>
При запуску цієї програми (назвемо її ATTACH. EXE) в
Як параметри треба буде вказати ідентифікатор процесу, у який ми
впроваджуємо свій код, і ім'я DLL, яку слід причепити до зовнішнього процесу. p>
Скопіюйте всі три отриманих модуля METOD1.DLL,
METOD2.DLL, ATTACH. EXE в один каталог (наприклад, C: TEST). Тепер можна
приступати до тестування. p>
Запустіть програму-жертву (у нашому випадку це
MESS.EXE). Відкрийте Диспетчер завдань, знайдіть у ньому запущений процес
(mess.exe): p>
p>
Малюнок 3 p>
і визначте його PID (у нашому випадку PID mess.exe
дорівнює 1076). p>
Тепер з командного рядка запустіть програму
впровадження коду перших DLL: p>
АТТАСН.EXE 1076 C: TEST
METOD1.DLL p>
У результаті при спробі викликати вікно MessageBox в
програмі MESS.EXE ви будете отримувати одне й те саме зображення: p>
p>
Малюнок 4 p>
Перехоплення функції API стався! p>
Висновок
p>
"Не такий страшний чорт, як програми MicroSoft ..." Тим не
менше, якщо читач вдумливо пропустив через себе викладений матеріал, то
побачив, що, як завжди, все геніальне - просто. І навіть така річ, як
перехоплення API в Windows NT, не вимагає надскладного програмного коду і може
бути реалізована за першим бажанням. p>
Список літератури h2>
Для підготовки даної роботи були використані
матеріали з сайту http://www.rsdn.ru/
p>