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