Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009
.pdf
706 Часть IV. Динамически подключаемые библиотеки
TCHAR szName[MAX_PATH];
DWORD cbValueName = _countof(szName);
POINT pt;
DWORD cbData = sizeof(pt), nItem;
// Read a value name and position from the registry. DWORD dwType;
l = RegEnumValue(hkey, nIndex, szName, &cbValueName, NULL, &dwType, (PBYTE) &pt, &cbData);
if (l == ERROR_NO_MORE_ITEMS) continue;
if ((dwType == REG_BINARY) && (cbData == sizeof(pt))) {
//The value is something that we recognize; try to find
//an item in the ListView control that matches the name. LV_FINDINFO lvfi;
lvfi.flags = LVFI_STRING; lvfi.psz = szName;
nItem = ListView_FindItem(hWndLV, -1, &lvfi); if (nItem != -1) {
// We found a match; change the item's position. ListView_SetItemPosition(hWndLV, nItem, pt.x, pt.y);
}
}
}
// Turn AutoArrange back on if it was originally on. SetWindowLong(hWndLV, GWL_STYLE, dwStyle); RegCloseKey(hkey);
}
}
///////////////////////////////////////////////////////////////////////////////
INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
chHANDLE_DLGMSG(hWnd, WM_CLOSE, Dlg_OnClose);
case WM_APP:
//Uncomment the line below to invoke the debugger
//on the process that just got the injected DLL.
//ForceDebugBreak();
Глава 22. Внедрение DLL и перехват API-вызовов.docx 707
if (lParam)
SaveListViewItemPositions((HWND) wParam); else
RestoreListViewItemPositions((HWND) wParam); break;
}
return(FALSE);
}
//////////////////////////////// End of File //////////////////////////////////
Внедрение DLL с помощью удаленных потоков
Третий способ внедрения DLL — самый гибкий. В нем используются многие особенности Windows: процессы, потоки, синхронизация потоков, управление виртуальной памятью, поддержка DLL и Unicode. (Если вы плаваете в каких-то из этих тем, прочтите сначала соответствующие главы книги.) Большинство Windowsфункций позволяет процессу управлять лишь самим собой, исключая тем самым риск повреждения одного процесса другим. Однако есть и такие функции, которые дают возможность управлять чужим процессом. Изначально многие из них были рассчитаны на применение в отладчиках и других инструментальных средствах. Но ничто не мешает использовать их и в обычном приложении.
Внедрение DLL этим способом предполагает вызов функции LoadLibrary потоком целевого процесса для загрузки нужной DLL. Так как управление потоками чужого процесса сильно затруднено, вы должны создать в нем свой поток. К счастью, Windows-функция CreateRemoteThread делает эту задачу несложной:
HANDLE CreateRemoteThread(
HANDLE hProcess,
PSECURITY_ATTRIBUTES psa,
DWORD dwStackSize,
PTHREAD_START_ROUTINE pfnStartAddr,
PVOID pvParam,
DWORD fdwCreate,
PDWORD pdwThreadld);
Она идентична Create Thread, но имеет дополнительный параметр hProcess, идентифицирующий процесс, которому будет принадлежать новый поток. Параметр pfnStartAddr определяет адрес функции потока. Этот адрес, разумеется, относится к удаленному процессу — функция потока не может находиться в адресном пространстве вашего процесса.
708 Часть IV. Динамически подключаемые библиотеки
Теперь вы знаете, как создать поток в другом процессе. Но как заставить этот поток загрузить нашу DLL? Ответ прост: нужно, чтобы он вызвал функцию LoadLibrary:
HMODULE LoadLibrary(PCTSTR pszLibFile);
Заглянув в заголовочный файл WinBase.h, вы увидите, что для LoadLibrary там есть такие строки:
HMODULE WINAPI LoadLibraryA(LPCSTR lpLibFileName); HMODULE WINAPI LoadLibraryW(LPCWSTR lpLibFileName); #ifdef UNICODE
#define LoadLibrary LoadLibraryW #else
#define LoadLibrary LoadLibraryA #endif // !UNICODE
В действительности существует две функции LoadLibrary: LoadLibraryA и LoadLibraryW. Они различаются только типом передаваемого параметра. Если имя файла библиотеки хранится как ANSI-строка, вызывайте LoadLibraryA; если же имя файла представлено Unicode-строкой — LoadLibraryW. Самой функции LoadLibrary нет. В большинстве программ макрос LoadLibrary раскрывается в
LoadLibraryA.
К счастью, прототипы LoadLibrary и функции потока идентичны. Вот как выглядит прототип функции потока:
DWORD WINAPI ThreadFunc(PVOID pvParam);
Хорошо, не идентичны, но очень похожи друг на друга. Обе функции принимают единственный параметр и возвращают некое значение. Кроме того, обе используют одни и те же правила вызова — WINAPI. Это крайне удачное стечение обстоятельств, потому что нам как раз и нужно создать новый поток, адрес функции которого является адресом LoadLibraryA или LoadLibraryW. По сути, требуется выполнить примерно такую строку кода:
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0,
LoadLibraryW, L"C:\\MyLib.dll", 0, NULL);
Или, если вы предпочитаете Unicode:
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0,
LoadLibraryA, "C:\\MyLib.dll", 0, NULL);
Новый поток в удаленном процессе немедленно вызывает LoadLibraryA (или LoadLibraryW), передавая ей адрес полного имени DLL. Все просто. Однако вас ждут две проблемы.
Первая в том, что нельзя вот так запросто, как я показал выше, передать
LoadLibraryA или LoadLibraryW в четвертом параметре функции CreateRemoteThread. Причина этого весьма неочевидна. При сборке программы в конечный двоичный файл помещается раздел импорта (описанный в главе
Глава 22. Внедрение DLL и перехват API-вызовов.docx 709
19). Этот раздел состоит из серии шлюзов к импортируемым функциям. Так что, когда ваш код вызывает функцию вроде LoadLibraryA, в разделе импорта модуля генерируется вызов соответствующего шлюза. А уже от шлюза происходит переход к реальной функции.
Следовательно, прямая ссылка на LoadLibraryW в вызове CreateRemote-Thread преобразуется в обращение к шлюзу LoadLibraryW в разделе импорта вашего модуля. Передача адреса шлюза в качестве стартового адреса удаленного потока заставит этот поток выполнить неизвестно что. И скорее всего это закончится нарушением доступа. Чтобы напрямую вызывать LoadLibraryW, минуя шлюз, вы должны выяснить ее точный адрес в памяти с помощью GetProcAddress.
Вызов Create Remote Thread предполагает, что Kernel32.dll спроецирована в локальном процессе на ту же область памяти, что и в удаленном. Kernel32.dll используется всеми приложениями, и, как показывает опыт, система проецирует эту DLL в каждом процессе по одному и тому же адресу. Так что CreateRemoteThread надо вызвать так:
// получаем истинный адрес LoadLibraryW в Kernel32.dll PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0,
pfnThreadRtn, L"C:\\MyLib.dll", 0, NULL);
Или, если вы предпочитаете Unicode:
// получаем истинный адрес LoadLibraryA в Kernel32.dll PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0,
pfnThreadRtn, "C:\\MyLib.dll", 0, NULL);
Отлично, одну проблему мы решили. Но я говорил, что их две. Вторая связана со строкой, в которой содержится полное имя файла DLL. Строка «С:\\MyLib.dll» находится в адресном пространстве вызывающего процесса. Ее адрес передается только что созданному потоку, который в свою очередь передает его в LoadLibraryW. Но, когда LoadLibraryW будет проводить разыменование (dereferencing) этого адреса, она не найдет по нему строку с полным именем файла DLL и скорее всего вызовет нарушение доступа в потоке удаленного процесса; пользователь увидит сообщение о необрабатываемом исключении, и удаленный процесс будет закрыт. Все верно: вы благополучно угробили чужой процесс, сохранив свой в целости и сохранности!
Эта проблема решается размещением строки с полным именем файла DLL в адресном пространстве удаленного процесса Впоследствии, вызывая CreateRemoteThread, мы передадим ее адрес (в удаленном процессе). На этот случай в Windows предусмотрена функция VirtualAllocEx, которая позволяет процессу выделять память в чужом адресном пространстве:
710 Часть IV. Динамически подключаемые библиотеки
PVOID VirtualAllocEx(
HANDLE hProcess,
PVOID pvAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect);
А освободить эту память можно с помощью функции VirtualFreeEx.
BOOL VirtualFreeEx(
HANDLE hProcess,
PVOID pvAddress,
SIZE_T dwSize,
DWORD dwFreeType);
Обе функции аналогичны своим версиям без суффикса Ех в конце (о них я рассказывал в главе 15). Единственная разница между ними в том, что эти две функции требуют передачи в первом параметре описателя удаленного процесса.
Выделив память, мы должны каким-то образом скопировать строку из локального адресного пространства в удаленное. Для этого в Windows есть две функции:
BOOL ReadProcessMemory(
HANDLE hProcess,
LPCVOID pvAddressRemote,
PVOID pvBufferLocal,
SIZE_T dwSize,
SIZE_T* pdwNumBytesRead);
BOOL WriteProcessMemory(
HANDLE hProcess,
PVOID pvAddressRemote,
LPCVOID pvBufferLocal,
SIZE_T dwSize,
SIZE_T* pdwNumBytesWritten);
Параметр hProcess идентифицирует удаленный процесс, pvAddressRemote и pvBufferLocal определяют адреса в адресных пространствах удаленного и локального процесса, a dwSize — число передаваемых байтов. По адресу, на который указывает параметр pdwNumBytesRead или pdwNumBytesWritten, возвращается число фактически считанных или записанных байтов.
Теперь, когда вы понимаете, что я пытаюсь сделать, давайте суммируем все сказанное и запишем это в виде последовательности операций, которые вам надо будет выполнить.
1.Выделите блок памяти в адресном пространстве удаленного процесса через VirtualAllocEx.
2.Вызвав WriteProcessMemory, скопируйте строку с полным именем файла DLL в блок памяти, выделенный в п.1.
712 Часть IV. Динамически подключаемые библиотеки
Некоторые процессы вроде WinLogon, SvcHost и Csrss выполняются по локальной системной учетной записи, которую зарегистрированный пользователь не имеет права изменять. Описатель такого процесса можно открыть, только если вы получили полномочия на отладку этих процессов. Программа ProcessInfo из главы 4 демонстрирует, как это делается.
При успешном выполнении OpenProcess записывает в буфер полное имя внедряемой DLL. Далее программа вызывает InjectLib и передает ей описатель удаленного процесса. И, наконец, после возврата из InjectLib программа выводит окно, где сообщает, успешно ли внедрена DLL, а потом закрывает описатель процесса.
Наверное, вы заметили, что я специально проверяю, не равен ли идентификатор процесса нулю. Если да, то вместо идентификатора удаленного процесса я передаю идентификатор процесса самой InjLib.exe, получаемый вызовом GetCurrentProcessId. Тогда при вызове InjectLib библиотека внедряется в адресное пространство процесса InjLib. Я сделал это для упрощения отладки. Сами понимаете, при возникновении ошибки иногда трудно определить, в каком процессе она находится: локальном или удаленном. Поначалу я отлаживал код с помощью двух отладчиков: один наблюдал за InjLib, другой — за удаленным процессом. Это оказалось страшно неудобно. Потом меня осенило, что InjLib способна внедрить DLL и в себя, т. е. в адресное пространство вызывающего процесса. И это сразу упростило отладку;
Просмотрев начало исходного кода модуля, вы увидите, что InjectLib — на самом деле макрос, заменяемый на InjectLibА или InjectLibW в зависимости от того, как компилируется исходный код. В исходном коде достаточно комментариев, и я добавлю лишь одно. Функциям InjectLibА весьма компактна. Она просто преобразует полное имя DLL из ANSI в Unicode и вызывает InjectLibW, которая и делает всю работу. Тут я придерживаюсь того подхода, который я рекомендовал в главе
2.
InjLib.cpp
/****************************************************************************** Module: InjLib.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h" /* See Appendix A. */ #include <windowsx.h>
#include <stdio.h> #include <tchar.h>
#include <malloc.h> // For alloca #include <TlHelp32.h>
#include "Resource.h" #include <StrSafe.h>
Глава 22. Внедрение DLL и перехват API-вызовов.docx 713
///////////////////////////////////////////////////////////////////////////////
#ifdef UNICODE
#define InjectLib InjectLibW #define EjectLib EjectLibW
#else
#define InjectLib InjectLibA #define EjectLib EjectLibA
#endif // !UNICODE
///////////////////////////////////////////////////////////////////////////////
BOOL WINAPI InjectLibW(DWORD dwProcessId, PCWSTR pszLibFile) {
BOOL bOk = FALSE; // Assume that the function fails
HANDLE hProcess = NULL, hThread = NULL;
PWSTR pszLibFileRemote = NULL;
|
|
|
|
__try |
{ |
|
|
// Get a handle for the target process. |
|||
hProcess = OpenProcess( |
|
|
|
|
PROCESS_QUERY_INFORMATION | |
// Required by Alpha |
|
|
PROCESS_CREATE_THREAD |
| |
// For CreateRemoteThread |
|
PROCESS_VM_OPERATION |
| |
// For VirtualAllocEx/VirtualFreeEx |
|
PROCESS_VM_WRITE, |
|
// For WriteProcessMemory |
|
FALSE, dwProcessId); |
|
|
if (hProcess == NULL) __leave;
//Calculate the number of bytes needed for the DLL's pathname int cch = 1 + lstrlenW(pszLibFile);
int cb = cch * sizeof(wchar_t);
//Allocate space in the remote process for the pathname pszLibFileRemote = (PWSTR)
VirtualAllocEx(hProcess, NULL, cb, MEM_COMMIT, PAGE_READWRITE); if (pszLibFileRemote == NULL) __leave;
//Copy the DLL's pathname to the remote process' address space if (!WriteProcessMemory(hProcess, pszLibFileRemote,
(PVOID) pszLibFile, cb, NULL)) __leave;
//Get the real address of LoadLibraryW in Kernel32.dll
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE) GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
if (pfnThreadRtn == NULL) __leave;
714Часть IV. Динамически подключаемые библиотеки
//Create a remote thread that calls LoadLibraryW(DLLPathname) hThread = CreateRemoteThread(hProcess, NULL, 0,
pfnThreadRtn, pszLibFileRemote, 0, NULL); if (hThread == NULL) __leave;
//Wait for the remote thread to terminate WaitForSingleObject(hThread, INFINITE);
bOk = TRUE; // Everything executed successfully
}
__finally { // Now, we can clean everything up
// Free the remote memory that contained the DLL's pathname if (pszLibFileRemote != NULL)
VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE);
if (hThread != NULL) CloseHandle(hThread);
if (hProcess != NULL) CloseHandle(hProcess);
}
return(bOk);
}
///////////////////////////////////////////////////////////////////////////////
BOOL WINAPI InjectLibA(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(InjectLibW(dwProcessId, pszLibFileW));
}
///////////////////////////////////////////////////////////////////////////////
BOOL WINAPI EjectLibW(DWORD dwProcessId, PCWSTR pszLibFile) {
Глава 22. Внедрение DLL и перехват API-вызовов.docx 715
BOOL bOk = FALSE; // Assume that the function fails
HANDLE hthSnapshot = NULL;
HANDLE hProcess = NULL, hThread = NULL;
__try {
// Grab a new snapshot of the process
hthSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId); if (hthSnapshot == INVALID_HANDLE_VALUE) __leave;
//Get the HMODULE of the desired library MODULEENTRY32W me = { sizeof(me) };
BOOL bFound = FALSE;
BOOL bMoreMods = Module32FirstW(hthSnapshot, &me);
for (; bMoreMods; bMoreMods = Module32NextW(hthSnapshot, &me)) { bFound = (_wcsicmp(me.szModule, pszLibFile) == 0) ||
(_wcsicmp(me.szExePath, pszLibFile) == 0); if (bFound) break;
}
if (!bFound) __leave;
//Get a handle for the target process.
hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD |
PROCESS_VM_OPERATION, // For CreateRemoteThread FALSE, dwProcessId);
if (hProcess == NULL) __leave;
//Get the real address of FreeLibrary in Kernel32.dll PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "FreeLibrary"); if (pfnThreadRtn == NULL) __leave;
//Create a remote thread that calls FreeLibrary()
hThread = CreateRemoteThread(hProcess, NULL, 0, pfnThreadRtn, me.modBaseAddr, 0, NULL);
if (hThread == NULL) __leave;
// Wait for the remote thread to terminate WaitForSingleObject(hThread, INFINITE);
bOk = TRUE; // Everything executed successfully
}
__finally { // Now we can clean everything up
if (hthSnapshot != NULL)
