Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6639
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

716 Часть IV. Динамически подключаемые библиотеки

CloseHandle(hthSnapshot);

if (hThread

!= NULL)

CloseHandle(hThread);

 

 

if (hProcess

!= NULL)

CloseHandle(hProcess);

}

return(bOk);

}

///////////////////////////////////////////////////////////////////////////////

BOOL WINAPI EjectLibA(DWORD dwProcessId, PCSTR pszLibFile) {

//Allocate a (stack) buffer for the Unicode version of the pathname SIZE_T cchSize = lstrlenA(pszLibFile) + 1;

PWSTR pszLibFileW = (PWSTR) _alloca(cchSize * sizeof(wchar_t));

//Convert the ANSI pathname to its Unicode equivalent StringCchPrintfW(pszLibFileW, cchSize, L"%S", pszLibFile);

//Call the Unicode version of the function to actually do the work. return(EjectLibW(dwProcessId, pszLibFileW));

}

///////////////////////////////////////////////////////////////////////////////

BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {

chSETDLGICONS(hWnd, IDI_INJLIB); return(TRUE);

}

///////////////////////////////////////////////////////////////////////////////

void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {

switch (id) { case IDCANCEL:

EndDialog(hWnd, id); break;

Глава 22. Внедрение DLL и перехват API-вызовов.docx 717

case IDC_INJECT:

DWORD dwProcessId = GetDlgItemInt(hWnd, IDC_PROCESSID, NULL, FALSE); if (dwProcessId == 0) {

//A process ID of 0 causes everything to take place in the

//local process; this makes things easier for debugging. dwProcessId = GetCurrentProcessId();

}

TCHAR szLibFile[MAX_PATH];

GetModuleFileName(NULL, szLibFile, _countof(szLibFile)); PTSTR pFilename = _tcsrchr(szLibFile, TEXT('\\')) + 1;

_tcscpy_s(pFilename, _countof(szLibFile) - (pFilename - szLibFile), TEXT("22-ImgWalk.DLL"));

if (InjectLib(dwProcessId, szLibFile)) { chVERIFY(EjectLib(dwProcessId, szLibFile)); chMB("DLL Injection/Ejection successful.");

} else {

chMB("DLL Injection/Ejection failed.");

}

break;

}

}

///////////////////////////////////////////////////////////////////////////////

INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

switch (uMsg) {

chHANDLE_DLGMSG(hWnd, WM_INITDIALOG,

Dlg_OnInitDialog);

chHANDLE_DLGMSG(hWnd, WM_COMMAND,

Dlg_OnCommand);

}

 

return(FALSE);

 

}

///////////////////////////////////////////////////////////////////////////////

int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int) {

DialogBox(hInstExe, MAKEINTRESOURCE(IDD_INJLIB), NULL, Dlg_Proc); return(0);

}

//////////////////////////////// End of File //////////////////////////////////

718 Часть IV. Динамически подключаемые библиотеки

Библиотека ImgWalk.dll

ImgWalk.dll — это DLL, которая, будучи внедрена в адресное пространство процесса, выдает список всех DLL, используемых этим процессом. Файлы исходного кода и ресурсов этой DLL находятся в каталоге 22-ImgWalk внутри архива, доступного на веб-сайте поддержки этой книги. Если, например, сначала запустить Notepad, а потом InjLib, передав ей идентификатор процесса Notepad, то InjLib внедрит ImgWalk.dll в адресное пространство Notepad. Попав туда, ImgWalk определит, образы каких файлов (ЕХЕ и DLL) используются процессом Notepad, и покажет результаты в следующем окне.

Модуль ImgWalk сканирует адресное пространство процесса и ищет спроецированные файлы, вызывая в цикле функцию VirtualQuery, которая заполняет структуру MEMORYBASIC INFORMATION. На каждой итерации цикла ImgWalk проверяет, нет ли строки с полным именем файла, которую можно было бы добавить в список, выводимый на экран.

/******************************************************************************

Module: ImgWalk.cpp

Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre

******************************************************************************/

#include "..\CommonFiles\CmnHdr.h" /* See Appendix A. */ #include <tchar.h>

Глава 22. Внедрение DLL и перехват API-вызовов.docx 719

///////////////////////////////////////////////////////////////////////////////

BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) {

if (fdwReason == DLL_PROCESS_ATTACH) { char szBuf[MAX_PATH * 100] = { 0 };

PBYTE pb = NULL; MEMORY_BASIC_INFORMATION mbi;

while (VirtualQuery(pb, &mbi, sizeof(mbi)) == sizeof(mbi)) {

int nLen;

char szModName[MAX_PATH];

if (mbi.State == MEM_FREE) mbi.AllocationBase = mbi.BaseAddress;

if ((mbi.AllocationBase == hInstDll) || (mbi.AllocationBase != mbi.BaseAddress) || (mbi.AllocationBase == NULL)) {

//Do not add the module name to the list

//if any of the following is true:

//1. If this region contains this DLL

//2. If this block is NOT the beginning of a region

//3. If the address is NULL

nLen = 0; } else {

nLen = GetModuleFileNameA((HINSTANCE) mbi.AllocationBase, szModName, _countof(szModName));

}

if (nLen > 0) {

wsprintfA(strchr(szBuf, 0), "\n%p-%s", mbi.AllocationBase, szModName);

}

pb += mbi.RegionSize;

}

//NOTE: Normally, you should not display a message box in DllMain

//due to the loader lock described in Chapter 20. However, to keep

//this sample application simple, I am violating this rule. chMB(&szBuf[1]);

}

720 Часть IV. Динамически подключаемые библиотеки

return(TRUE);

}

//////////////////////////////// End of File //////////////////////////////////

Сначала я проверяю, не совпадает ли базовый адрес региона с базовым адресом внедренной DLL. Если да, я обнуляю nLen, чтобы не показывать в окне имя внедренной DLL. Нет — пытаюсь получить имя модуля, загруженного по базовому адресу данного региона. Если значение nLen больше 0, система распознает, что указанный адрес идентифицирует загруженный модуль, и помещает в буфер szModName полное имя (вместе с путем) этого модуля. Затем я присоединяю HINSTANCE данного модуля (базовый адрес) и его полное имя к строке szBuf, которая в конечном счете и появится в окне. Когда цикл заканчивается, DLL открывает на экране окно со списком.

Внедрение троянской DLL

Другой способ внедрения состоит в замене DLL, загружаемой процессом, на другую DLL. Например, зная, что процессу нужна Xyz.dll, вы можете создать свою DLL и присвоить ей то же имя. Конечно, перед этим вы должны переименовать исходную Xyz.dll.

В своей Xyz.dll вам придется экспортировать те же идентификаторы, что и в исходной Xyz.dll. Это несложно, если задействовать механизм переадресации функций (см. главу 20); однако его лучше не применять, иначе вы окажетесь в зависимости от конкретной версии DLL. Если вы замените, скажем, системную DLL, а Майкрософт потом добавит в нее новые функции, в вашей версии той же DLL их не будет. А значит, не удастся загрузить приложения, использующие эти новые функции.

Если вы хотите применить этот метод только для одного приложения, то можете присвоить своей DLL уникальное имя и записать его в раздел импорта исполняемого модуля приложения. Дело в том, что раздел импорта содержит имена всех DLL, нужных ЕХЕ-модулю. Вы можете «покопаться» в этом разделе и изменить его так, чтобы загрузчик операционной системы загружал вашу DLL. Этот прием совсем неплох, но требует глубоких знаний о формате ЕХЕ- и DLL-файлов.

Внедрение DLL как отладчика

Отладчик может выполнять особые операции над отлаживаемым процессом. Когда отлаживаемый процесс загружен и его адресное пространство создано, но первичный поток еще не выполняется, система автоматически уведомляет об этом отладчик. В этот момент отладчик может внедрить в него нужный код (используя, например, WriteProcessMemory), а затем заставить его первичный поток выполнить внедренный код.

Глава 22. Внедрение DLL и перехват API-вызовов.docx 721

Этот метод требует манипуляций со структурой CONTEXT потока отлаживаемого процесса, а значит, ваш код будет зависим от типа процессора, и его придется модифицировать при переносе на другую процессорную платформу. Кроме того, вам почти наверняка придется вручную корректировать машинный код, который должен быть выполнен отлаживаемым процессом. Не забудьте и о жесткой связи между отладчиком и отлаживаемой программой: как только отладчик закрывается, Windows немедленно закрывает и отлаживаемую программу. Можно изменить это, вызвав функуию DebugSetProc essKillOnExitc параметром FALSE, а функция DebugActiveProcessStop позволяет остановить отладку процесса, не закрывая отлаживаемый процесс.

Внедрение кода через функцию CreateProcess

Если ваш процесс порождает дочерний, в который надо внедрить какой-то код, то задача значительно упрощается. Родительский процесс может создать новый процесс и сразу же приостановить его. Это позволит изменить состояние дочернего процесса до начала его выполнения. В то же время родительский процесс получает описатель первичного потока дочернего процесса. Зная его, вы можете модифицировать код, который будет выполняться этим потоком. Тем самым вы решите проблему, упомянутую в предыдущем разделе: в данном случае нетрудно установить регистр указателя команд, принадлежащий потоку, на код в проекции файла.

Вот один из способов контроля того, какой код выполняется первичным потоком дочернего процесса:

1.Создайте дочерний процесс в приостановленном состоянии.

2.Получите стартовый адрес его первичного потока, считав его из заголовка исполняемого модуля.

3.Сохраните где-нибудь машинные команды, находящиеся по этому адресу памяти.

4.Введите на их место свои команды. Этот код должен вызывать LoadLibrary для загрузки DLL.

5.Разрешите выполнение первичного потока дочернего процесса.

6.Восстановите ранее сохраненные команды по стартовому адресу первичного потока.

7.Пусть процесс продолжает выполнение со стартового адреса так, будто ничего и не было.

Этапы 6 и 7 довольно трудны, но реализовать их можно — такое уже делалось.

У этого метода масса преимуществ. Во-первых, мы получаем адресное пространство до выполнения приложения. Во-вторых, данный метод применим как в Windows 98, так и в Windows 2000. В третьих, мы можем без проблем отлаживать приложение с внедренной DLL, не пользуясь отладчиком. Наконец, он работает как в консольных, так и в GUI-приложениях.

722 Часть IV. Динамически подключаемые библиотеки

Однако у него есть и недостатки. Внедрение DLL возможно, только если это делается из родительского процесса. И, конечно, этот метод создает зависимость программы от конкретного процессора; при ее переносе на другую процессорную платформу потребуются определенные изменения в коде.

Перехват API-вызовов: пример

Внедрение DLL в адресное пространство процесса — замечательный способ узнать, что происходит в этом процессе. Однако простое внедрение DLL не дает достаточной информации. Зачастую надо точно знать, как потоки определенного процесса вызывают различные функции, а иногда и изменять поведение той или иной Windows-функции.

Мне известна одна компания, которая выпустила DLL для своего приложения, работающего с базой данных. Эта DLL должна была расширить возможности основного продукта. При закрытии приложения DLL получала уведомление DLL_PROCESS_DETACH и только после этого проводила очистку ресурсов. При этом DLL должна была вызывать функции из других DLL для закрытия сокетов, файлов и других ресурсов, но к тому моменту другие DLL тоже получали уведомление DLL_PROCESS_DETACH, так что корректно завершить работу никак не удавалось.

Для решения этой проблемы компания наняла меня, и я предложил поставить ловушку на функцию ExitProcess. Как вам известно, вызов ExitProcess заставляет систему посылать библиотекам уведомление DLL_PROCESS_DETACH. Перехватывая вызов ExitProcess, мы гарантируем своевременное уведомление внедренной DLL о вызове этой функции. Причем уведомление приходит до того, как аналогичные уведомления посылаются другим DLL. В этот момент внедренная DLL узнаѐт о завершении процесса и успевает провести корректную очистку. Далее вызывается функция ExitProcess, что приводит к рассылке уведомлений DLL_PROCESS_DETACH остальным DLL, и они корректно завершаются. Это же уведомление получает и внедренная DLL, но ничего особенного она не делает, так как уже выполнила свою задачу.

В этом примере внедрение DLL происходило как бы само по себе: приложение было рассчитано на загрузку именно этой DLL. Оказываясь в адресном пространстве процесса, DLL должна была просканировать ЕХЕ-модуль и все загружаемые DLL-модули, найти все обращения к ExitProcess и заменить их вызовами функции, находящейся во внедренной DLL. (Эта задача не так сложна, как кажется.) Подставная функция (функция ловушки), закончив свою работу, вызывала настоящую функцию ExitProcess из Kernel32.dll.

Данный пример иллюстрирует типичное применение перехвата API-вызовов, который позволил решить насущную проблему при минимуме дополнительного кода.

Глава 22. Внедрение DLL и перехват API-вызовов.docx 723

Перехват API-вызовов подменой кода

Перехват API-вызовов далеко не новый метод — разработчики пользуются им уже многие годы. Когда сталкиваешься с проблемой, аналогичной той, о которой я только что рассказал, то первое, что приходит в голову, — установить ловушку, подменив часть исходного кода, вот как это делается.

1.Найдите адрес функции, вызов которой вы хотите перехватывать (например,

ExitProcess в Kernel32.dll).

2.Сохраните несколько первых байтов этой функции в другом участке памяти.

3.На их место вставьте машинную команду JUMP для перехода по адресу подставной функции. Естественно, сигнатура вашей функции должна быть такой же, как и исходной, т. е. все параметры, возвращаемое значение и правила вызова должны совпадать.

4.Теперь, когда поток вызовет перехватываемую функцию, команда JUMP перенаправит его к вашей функции. На этом этапе вы можете выполнить любой нужный код.

5.Снимите ловушку, восстановив ранее сохраненные (в п. 2) байты.

6.Если теперь вызвать перехватываемую функцию (таковой больше не являющуюся), она будет работать так, как работала до установки ловушки.

7.После того как она вернет управление, вы можете выполнить операции 2 и 3 и тем самым вновь поставить ловушку на эту функцию.

Этот метод был очень популярен среди программистов, создававших приложения для 16-разрядной Windows, и отлично работал в этой системе. В современных системах у этого метода возникло несколько серьезных недостатков, и я настоятельно не рекомендую его применять. Во-первых, он создает зависимость от конкретного процессора из-за команды JUMP, и, кроме того, приходится вручную писать машинные коды. Во-вторых, в системе с вытесняющей многозадачностью данный метод вообще не годится. На замену кода в начале функции уходит какоето время, а в этот момент перехватываемая функция может понадобиться другому потоку. Результаты могут быть просто катастрофическими! Так что этот метод работает только в ситуациях, когда потоки вызывают функции строго поочередно.

Перехват API-вызовов с использованием раздела импорта

Данный способ API-перехвата решает обе упомянутые мной проблемы. Он прост и довольно надежен. Но для его понимания нужно иметь представление о том, как осуществляется динамическое связывание. В частности, вы должны разбираться в структуре раздела импорта модуля. В главе 19 я достаточно подробно объяснил, как создается этот раздел и что в нем находится. Читая последующий материал, вы всегда можете вернуться к этой главе.

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

724 Часть IV. Динамически подключаемые библиотеки

Поэтому, чтобы перехватить определенную функцию, надо лишь изменить ее адрес в разделе импорта. Все! И никакой зависимости от процессорной платформы. А поскольку вы ничего не меняете в коде функции, то и о синхронизации потоков можно не беспокоиться.

Вот функция, которая делает эту сказку былью. Она ищет в разделе импорта модуля ссылку на идентификатор по определенному адресу и, найдя ее, подменяет адрес соответствующего идентификатора.

void CAPIHook::ReplaceIATEntryInOneMod(PCSTR pszCalleeModName, PROC pfnCurrent, PROC pfnNew, HMODULE hmodCaller) {

//в этом модуле нет раздела импорта ULONG ulSize;

//Explorer сгенерировал исключение, обратившись при просмотре

//содержимого палки к imagehlp.dll. Похоже, один из модулей был

//выгружен. Также возможны проблемы из-за многопоточности: Toolhelp может

//дать неточный список модулей, если во время их перечисления будет вызвана

//функция FreeLibrary.

PIMAGE_IMPORT_DESCRIPTOR pImportDesc = NULL; __try {

pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR) ImageDirectoryEntryToData( hmodCaller, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);

}

__except (InvalidReadExceptionFilter(GetExceptionInformation())) {

//здесь делать нечего, поток продолжает нормальную работу,

//получив NULL в pImportDesc

}

 

if (pImportDesc == NULL)

 

return;

// в этом модуле нет раздела импорта

//находим дескриптор раздела импорта со ссылками

//на функции DLL (вызываемого модуля)

for (; pImportDesc->Name; pImportDesc++) {

PSTR pszModName = (PSTR) ((PBYTE) hmodCaller + pImportDesc->Name); if (lstrcmpiA(pszModName, pszCalleeModName) == 0) {

//получаем таблицу адресов импорта (IAT) для функций DLL PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)

((PBYTE) hmodCaller + pImportDesc->FirstThunk);

//заменяем адреса исходных функций адресами своих функций for (; pThunk->u1.Function; pThunk++) {

// получаем адрес адреса функции

PROC* ppfn = (PROC*) &pThunk->u1.Function;

Глава 22. Внедрение DLL и перехват API-вызовов.docx 725

// та ли это функция, которая нас интересует?

BOOL bFound = (*ppfn == ofnCurrent); if (bFound) {

if (!WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew, sizeof(pfnNew), NULL) && (ERROR_NOACCESS == GetLastError())) {

DWORD dwOldProtect;

if (VirtualProtect(ppfn, sizeof(pfnNew), PAGE_WRITECOPY, &dwOldProtect)) {

WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew, sizeof(pfnNew), NULL);

VirtualProtect(ppfn, sizeof(pfnNew), dwOldProtect, &dwOldProtect);

}

 

}

 

return;

// получилось; выходим

}

}

}// Система анализирует все разделы импорта, пока не будет // найдена и настроена подходящая запись

}

}

Чтобы понять, как вызывать эту функцию, представьте, что у нас есть модуль с именем DataBase.exe. Он вызывает ExitProcess из Kernel32.dll, но мы хотим, чтобы он обращался к MyExitProcess в нашем модуле DBExtend.dll. Для этого надо вызвать ReplacelATEntryInOneMod следующим образом.

PROC pfnOrig = GetProcAddress(GetModuleHandle("Kernel32"), "ExitProcess");

HMODULE hmodCaller = GetModuleHandle("Database.exe");

ReplaceIATEntryInOneMod(

"Kernel32.dll",

// модуль, содержащий ANSI-функцию

pfnOrig,

// адрес исходной функции в вызываемой DLL

MyExitProcess,

// адрес заменяющей функции

hmodCaller);

// описатель модуля, из которого надо вызывать новую функцию

Первое, что делает ReplaceIATEntryInOneMod, — находит в модуле hmodCaller раздел импорта. Для этого она вызывает ImageDirectoryEntryToData и передает ей IMAGE_DIRECTORY_ENTRY_IMPORT. Если последняя функция возвращает NULL, значит, в модуле DataBase.exe такого раздела нет, и на этом все заканчивается. Вызов ImageDirectoryEntryToData защищен блоком __try/__except (см. главу 24), перехватывающим неожиданные исключения, которые могут сгенерировать функции из ImageHlp.dll. Это необходимо, поскольку ReplaceIATEntryInOneMod может быть вызвана с не-

Соседние файлы в предмете Программирование на C++