插入D L L的第三种方法是使用远程线程。这种方法具有更大的灵活性。它要求你懂得若干个Wi n d o w s特性、如进程、线程、线程同步、虚拟内存管理、D L L和U n i c o d e等(如果对这些特性不清楚,请参阅本书中的有关章节)。Wi n d o w s的大多数函数允许进程只对自己进行操作。这是很好的一个特性,因为它能够防止一个进程破坏另一个进程的运行。但是,有些函数却允许一个进程对另一个进程进行操作。这些函数大部分最初是为调试程序和其他工具设计的。不过任何函数都可以调用这些函数。
这个D L L插入方法基本上要求目标进程中的线程调用L o a d L i b r a r y函数来加载必要的D L L。由于除了自己进程中的线程外,我们无法方便地控制其他进程中的线程,因此这种解决方案要求我们在目标进程中创建一个新线程。由于是自己创建这个线程,因此我们能够控制它执行什么代码。幸好,Wi n d o w s提供了一个称为C r e a t e R e m o t e T h r e a d的函数,使我们能够非常容易地在另一个进程中创建线程:
HANDLE CreateRemoteThread( HANDLE hProcess, PSECURITY_ATTRIBUTES psa, DWORD dwStackSize, PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD fdwCreate, PDWORD pdwThreadId);
注意在Windows 2000中,更常用的函数CreateThread是在内部以下面的形式来实现的:
HANDLE CreateThread(PSECURITY_ATTRIBUTES psa, DWORD dwStackSize, PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD fdwCreate, PDWORD pdwThreadID) { return(CreateRemoteThread(GetCurrentProcess(), psa, dwStackSize, pfnStartAddr, pvParam, fdwCreate, pdwThreadID)); }
好了,现在你已经知道如何在另一个进程中创建线程了,但是,如何才能让该线程加载我们的D L L呢?答案很简单,那就是需要该线程调用L o a d L i b r a r y函数:
HINSTANCE LoadLibrary(PCTSTR pszLibFile);
HINSTANCE WINAPI LoadLibraryA(LPCSTR pszLibFileName); HINSTANCE WINAPI LoadLibraryW(LPCWSTR pszLibFileName); #ifdef UNICODE #define LoadLibrary LoadLibraryW #else #define LoadLibrary LoadLibraryA #endif // !UNICODE
幸好L o a d L i b r a r y函数的原型与一个线程函数的原型是相同的。下面是一个线程函数的原型:
DWORD WINAPI ThreadFunc(PVOID pvParam);
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, LoadLibraryA, "C:\\MyLib.dll", 0, NULL);
HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, LoadLibraryW, L"C:\\MyLib.dll", 0, NULL);
第一个问题是,不能像我在上面展示的那样,将L o a d L i b r a r y A或L o a d L i b r a r y W作为第四个参数传递给C r e a t e R e m o t e T h r e a d。原因很简单。当你编译或者链接一个程序时,产生的二进制代码包含一个输入节(第1 9章中做了介绍)。这一节由一系列输入函数的形式替换程序(t h u n k)组成。所以,当你的代码调用一个函数如L o a d L i b r a r y A时,链接程序将生成一个对你模块的输入节中的形实替换程序的调用。接着,该形实替换程序便转移到实际的函数。
如果在对C r e a t e R e m o t e T h r e a d的调用中使用一个对L o a d L i b r a r y A的直接引用,这将在你的模块的输入节中转换成L o a d L i b r a r y A的形实替换程序的地址。将形实替换程序的地址作为远程线程的起始地址来传递,会导致远程线程开始执行一些令人莫名其妙的东西。其结果很可能造成访问违规。若要强制直接调用L o a d L i b r a r y A函数,避开形实替换程序,必须通过调用G e t P r o c A d d r e s s函数,获取L o a d L i b r a r y A的准确内存位置。
对C r e a t e R e m o t e T h r e a d进行调用的前提是,K e r n e l 3 2 . d l l已经被同时映射到本地和远程进程的地址空间中。每个应用程序都需要K e r n e l 3 2 . d l l,根据我的经验,系统将K e r n e l 3 2 . d l l映射到每个进程的同一个地址。因此,必须调用下面的C r e a t e R e m o t e T h r e a d函数:
// Get the real address of LoadLibraryA in 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);
// Get the real address of LoadLibraryW in 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);
为了解决这个问题,必须将D L L的路径名字符串放入远程进程的地址空间中。然后,当C r e a t e R e m o t e T h r e a d函数被调用时,我们必须将我们放置该字符串的地址(相对于远程进程的地址)传递给它。同样,Wi n d o w s提供了一个函数,即Vi r t u a l A l l o c E x,使得一个进程能够分配另一个进程的地址空间中的内存:
PVOID VirtualAllocEx( HANDLE hProcess, PVOID pvAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);
BOOL VirtualFreeEx( HANDLE hProcess, PVOID pvAddress, SIZE_T dwSize, DWORD dwFreeType);
一旦为该字符串分配了内存,我们还需要一种方法将该字符串从我们的进程的地址空间拷贝到远程进程的地址空间中。Wi n d o w s提供了一些函数,使得一个进程能够从另一个进程的地址空间中读取数据,并将数据写入另一个进程的地址空间。
BOOL ReadProcessMemory( HANDLE hProcess, PVOID pvAddressRemote, PVOID pvBufferLocal, DWORD dwSize, PDWORD pdwNumBytesRead); BOOL WriteProcessMemory( HANDLE hProcess, PVOID pvAddressRemote, PVOID pvBufferLocal, DWORD dwSize, PDWORD pdwNumBytesWritten);
既然已经知道了要进行操作,下面让我们将必须执行的操作步骤做一个归纳:
1) 使用Vi r t u a l A l l o c E x函数,分配远程进程的地址空间中的内存。
2) 使用Wr i t e P r o c e s s M e m o r y函数,将D L L的路径名拷贝到第一个步骤中已经分配的内存中。
3) 使用G e t P r o c A d d r e s s函数,获取L o a d L i b r a r y A或L o a d L i b r a t y W函数的实地址(在K e r n e l 3 2 . d l l中)。
4) 使用C r e a t e R e m o t e T h r e a d函数,在远程进程中创建一个线程,它调用正确的L o a d L i b r a r y函数,为它传递第一个步骤中分配的内存的地址。
这时, D L L已经被插入远程进程的地址空间中,同时D L L的D l l M a i n函数接收到一个D L L _ P R O C E S S _ AT TA C H通知,并且能够执行需要的代码。当D l l M a i n函数返回时,远程线程从它对L o a d L i b r a r y的调用返回到B a s e T h r e a d S t a r t 函数(第6 章中已经介绍)。然后B a s e T h r e a d S t a r t调用E x i t T h r e a d,使远程线程终止运行。
现在远程进程拥有第一个步骤中分配的内存块,而D L L则仍然保留在它的地址空间中。若要将它删除,需要在远程线程退出后执行下面的步骤:
5) 使用Vi r t u a l F r e e E x函数,释放第一个步骤中分配的内存。
6) 使用G e t P r o c A d d r e s s函数,获得F r e e L i b r a r y函数的实地址(在K e r n e l 3 2 . d l l中)。
7) 使用C r e a t e R e m o t e T h r e a d函数,在远程进程中创建一个线程,它调用F r e e L i b r a r y函数,传递远程D L L的H I N S TA N C E。
这就是它的基本操作步骤。这种插入D L L的方法存在的唯一一个不足是, Windows 98并不支持这样的函数。只能在Windows 2000上使用这种方法。
22.4.1 Inject Library示例应用程序
清单2 2 - 2中列出的I n j L i b . e x e应用程序使用C r e a t e R e m o t e T h r e a d函数来插入D L L。该应用程序和D L L的源代码和资源文件位于本书所附光盘上的2 2 -I n j L i b和2 2 - I m g Wa l k目录下。该程序使用图2 2 - 4所示的对话框来接收运行的进程I D。
图22-4 Inject Library Te s t e r对话框
可以使用Windows 2000配有的Task Manager(任务管理器)获取进程的I D。使用这个I D,该程序将设法通过调用O p e n P r o c e s s函数来打开正在运行的进程的句柄,申请相应的访问权:
hProcess = OpenProcess( PROCESS_CREATE_THREAD | // For CreateRemoteThread PROCESS_VM_OPERATION | // For VirtualAllocEx/VirtualFreeEx PROCESS_VM_WRITE, // For WriteProcessMemory FALSE, dwProcessId);
如果O p e n P r o c e s s函数运行成功,便使用要插入的D L L的全路径名对一个缓存进程初始化。然后I n j e c t L i b被调用,为它传递需要的远程进程的句柄和要插入的D L L的路径名。最后,当I n j e c t L i b返回时,该程序显示一个消息框,指明D L L是否已经成功地加载到远程进程中,然后它关闭进程的句柄。这就是它的全部运行过程。
你可能在代码中发现,我专门查看了传递的进程I D是否是0。如果是0,我就调用G e t C u r r e n tP r o c e s s I d函数,将进程的I D设置为I n j e c t L i b . e x e自己的进程I D。这样,当I n j e c t L i b被调用时,D L L被插入到进程自己的地址空间中。这使得程序的调用比较容易。可以想象,当出现错误时,有时很难确定这些错误是在本地进程中还是在远程进程中。原先我用两个调试程序来调试我的代码,一个调试程序负责观察I n j L i b,另一个调试程序负责观察远程进程。结果表明这样做是很不方便的。后来我才明白, I n j L i b也能将D L L插入本身的程序中,也就是说插入与调用程序相同的地址空间中。这样调试代码就容易多了。
在源代码模块的顶部你会发现,I n j e c t L i b实际上是个符号,根据你编译源代码所用的方法,它可以展开为I n j e c t L i b A或I n j e c t L i b W。函数I n j e c t L i b W正是一切魔力之所在。程序的注释本身就说明了问题,当然也可以进一步补充说明。不过你将发现函数I n j e c t L i b A比较短。它只是将ANSI DLL的路径名转换成对应的U n i c o d e路径名,然后调用I n j e c t L i b W函数进行实际的操作。这种方法正是我在第2章中建议你使用的。它也意味着只需要使插入代码运行一次就行了。
清单22-2 InjLib示例应用程序
/****************************************************************************** Module: InjLib.cpp Notices: Copyright (c) 2000 Jeffrey Richter ******************************************************************************/ #include "..\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" /////////////////////////////////////////////////////////////////////////////// #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 fOk = 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); // 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's 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; // 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); fOk = TRUE; // Everything executed successfully } __finally { // Now, we can clean everthing 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(fOk); } /////////////////////////////////////////////////////////////////////////////// BOOL WINAPI InjectLibA(DWORD dwProcessId, PCSTR pszLibFile) { // Allocate a (stack) buffer for the Unicode version of the pathname PWSTR pszLibFileW = (PWSTR) _alloca((lstrlenA(pszLibFile) + 1) * sizeof(WCHAR)); // Convert the ANSI pathname to its Unicode equivalent wsprintfW(pszLibFileW, 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) { BOOL fOk = 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 == NULL) __leave; // Get the HMODULE of the desired library MODULEENTRY32W me = { sizeof(me) }; BOOL fFound = FALSE; BOOL fMoreMods = Module32FirstW(hthSnapshot, &me); for (; fMoreMods; fMoreMods = Module32NextW(hthSnapshot, &me)) { fFound = (lstrcmpiW(me.szModule, pszLibFile) == 0) || (lstrcmpiW(me.szExePath, pszLibFile) == 0); if (fFound) break; } if (!fFound) __leave; // Get a handle for the target process. hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | // Required by Alpha PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION, // For CreateRemoteThread FALSE, dwProcessId); if (hProcess == NULL) __leave; // Get the real address of LoadLibraryW 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 LoadLibraryW(DLLPathname) hThread = CreateRemoteThread(hProcess, NULL, 0, pfnThreadRtn, me.modBaseAddr, 0, NULL); if (hThread == NULL) __leave; // Wait for the remote thread to terminate WaitForSingleObject(hThread, INFINITE); fOk = TRUE; // Everything executed successfully } __finally { // Now we can clean everything up if (hthSnapshot != NULL) CloseHandle(hthSnapshot); if (hThread != NULL) CloseHandle(hThread); if (hProcess != NULL) CloseHandle(hProcess); } return(fOk); } /////////////////////////////////////////////////////////////////////////////// BOOL WINAPI EjectLibA(DWORD dwProcessId, PCSTR pszLibFile) { // Allocate a (stack) buffer for the Unicode version of the pathname PWSTR pszLibFileW = (PWSTR) _alloca((lstrlenA(pszLibFile) + 1) * sizeof(WCHAR)); // Convert the ANSI pathname to its Unicode equivalent wsprintfW(pszLibFileW, 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; 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, sizeof(szLibFile)); _tcscpy(_tcsrchr(szLibFile, TEXT('\\')) + 1, 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) { chWindows9xNotAllowed(); DialogBox(hinstExe, MAKEINTRESOURCE(IDD_INJLIB), NULL, Dlg_Proc); return(0); } //////////////////////////////// End of File //////////////////////////////////
//Microsoft Developer Studio generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_INJLIB ICON DISCARDABLE "InjLib.ico" #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE DISCARDABLE BEGIN "resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_INJLIB DIALOG DISCARDABLE 15, 24, 158, 24 STYLE DS_3DLOOK | DS_CENTER | WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Inject Library Tester" FONT 8, "MS Sans Serif" BEGIN LTEXT "&Process Id (decimal):",-1,4,6,69,8 EDITTEXT IDC_PROCESSID,78,4,36,12,ES_AUTOHSCROLL DEFPUSHBUTTON "&Inject",IDC_INJECT,120,4,36,12,WS_GROUP END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO DISCARDABLE BEGIN IDD_INJLIB, DIALOG BEGIN RIGHTMARGIN, 134 BOTTOMMARGIN, 20 END END #endif // APSTUDIO_INVOKED #endif // English (U.S.) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED
清单2 2 - 3列出的I m g Wa l k . d l l是个D L L,一旦它被插入进程的地址空间,就能够报告该进程正在使用的所有D L L(该D L L的源代码和资源文件均在本书所附光盘上的2 2 - I m g Wa l k目录下)。例如,如果我首先运行N o t e p a d,然后运行I n j L i b,为它传递N o t e p a d的进程I D,I n j L i b将I m g Wa l k . d l l插入N o t e p a d的地址空间中。一旦进入该地址空间, I m g Wa l k便确定N o t e p a d正在使用哪些文件映像(可执行文件和D L L),并且显示图2 2 - 5所示的消息框,它显示了查找的结果。
图22-5 查找结果对话框
I m g Wa l k遍历进程的地址空间,查找已经映射的文件映像,反复调用Vi r t u a l Q u e r y函数,填入一个M E M O RY_BASIC_ INFORMAT I O N结构中。运用循环的每个重复操作, I m g Wa l k找出一个文件路径名,并与一个字符串相连接。该字符串显示在消息框中。
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. This region contains this DLL. // 2. This block is NOT the beginning of a region. // 3. The address is NULL. nLen = 0; } else { nLen = GetModuleFileNameA((HINSTANCE) mbi.AllocationBase, szModName, chDIMOF(szModName)); } if(nLen > 0) { wsprintfA(strchr(szBuf, 0), "\n%08X-%s", mbi.AllocationBase, szModName); } pb += mbi.RegionSize; } chMB(&szBuf[1]);
清单2 2 - 3 Im g Wa l k . d l l的源代码
/****************************************************************************** Module: ImgWalk.cpp Notices: Copyright (c) 2000 Jeffrey Richter ******************************************************************************/ #include "..\CmnHdr.h" /* See Appendix A. */ #include <tchar.h> /////////////////////////////////////////////////////////////////////////////// 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, chDIMOF(szModName)); } if (nLen > 0) { wsprintfA(strchr(szBuf, 0), "\n%p-%s", mbi.AllocationBase, szModName); } pb += mbi.RegionSize; } chMB(&szBuf[1]); } return(TRUE); } //////////////////////////////// End of File //////////////////////////////////
22.5 使用特洛伊DLL来插入DLL
插入D L L的另一种方法是取代你知道进程将要加载的D L L。例如,如果你知道一个进程将要加载X y z . d l l,就可以创建你自己的D L L,为它赋予相同的文件名。当然,你必须将原来的X y z . d l l改为别的什么名字。
在你的X y z . d l l中,输出的全部符号必须与原始的X y z . d l l输出的符号相同。使用函数转发器(第2 0章做了介绍),很容易做到这一点。虽然函数转发器使你能够非常容易地挂接某些函数,你应该避免使用这种方法,因为它不具备版本升级能力。例如,如果你取代了一个系统D L L,而M i c r o s o f t在将来增加了一些新函数,那么你的D L L将不具备它们的函数转发器。引用这些新函数的应用程序将无法加载和执行。
如果你只想在单个应用程序中使用这种方法,那么可以为你的D L L赋予一个独一无二的名字,并改变应用程序的. e x e模块的输入节。更为重要的是,输入节只包含模块需要的D L L的名字。你可以仔细搜索文件中的这个输入节,并且将它改变,使加载程序加载你自己的D L L。这种方法相当不错,但是必须要非常熟悉. e x e和D L L文件的格式。
调试程序能够对被调试的进程执行特殊的操作。当被调试进程加载时,在被调试进程的地址空间已经作好准备,但是被调试进程的主线程尚未执行任何代码之前,系统将自动将这个情况通知调试程序。这时,调试程序可以强制将某些代码插入被调试进程的地址空间中(比如使用Wr i t e P r o c e s s M e m o r y函数来插入),然后使被调试进程的主线程执行该代码。
这种方法要求你对被调试线程的C O N T E X T结构进行操作,意味着必须编写特定C P U的代码。必须修改你的源代码,使之能够在不同的C P U平台上正确地运行。另外,必须对你想让被调试进程执行的机器语言指令进行硬编码。而且调试程序与它的被调试程序之间必须存在固定的关系。如果调试程序终止运行,Wi n d o w s将自动撤消被调试进程。而你则无法阻止它。
在Windows 98 上插入你自己的代码是非常简单的。在Windows 98 上运行的所有3 2位Wi n d o w s应用程序均共享同样的最上面的2 GB地址空间。如果你分配这里面的某些存储器,那么该存储器在每个进程的地址空间中均可使用。若要分配2 GB以上的存储器,只要使用内存映射文件(第1 7章已经介绍)。可以创建一个内存映射文件,然后调用M a p Vi e w O f F i l e函数,使它显示出来。然后将数据填入你的地址空间区域(这是所有进程地址空间中的相同区域)。必须使用硬编码的机器语言来进行这项操作,其结果是这种解决方案很难移植到别的C P U平台。不过,如果进行这项操作,那么不必考虑不同的C P U平台,因为Windows 98只能在x86 CPU上运行。
这种方法的困难之处在于仍然必须让其他进程中的线程来执行内存映射文件中的代码。要做到这一点,需要某种方法来控制远程进程中的线程。C r e a t e R e m o t e T h r e a d函数能够很好地执行这个任务,可惜Windows 98不支持该函数的运行,而我也无法提供相应的解决方案。
如果你的进程生成了你想插入代码的新进程,那么事情就会变得稍稍容易一些。原因之一是,你的进程(父进程)能够创建暂停运行的新进程。这就使你能够改变子进程的状态,而不影响它的运行,因为它尚未开始运行。但是父进程也能得到子进程的主线程的句柄。使用该句柄,可以修改线程执行的代码。你可以解决上一节提到的问题,因为可以设置线程的指令指针,以便执行内存映射文件中的代码。
下面介绍一种方法,它使你的进程能够控制子进程的主线程执行什么代码:
1) 使你的进程生成暂停运行的子进程。
2) 从. e x e模块的头文件中检索主线程的起始内存地址。
3) 将机器指令保存在该内存地址中。
4) 将某些硬编码的机器指令强制放入该地址中。这些指令应该调用L o a d L i b r a r y函数来加载D L L。
5) 继续运行子进程的主线程,使该代码得以执行。
6) 将原始指令重新放入起始地址。
7) 让进程继续从起始地址开始执行,就像没有发生任何事情一样。
上面的步骤6和7要正确运行是很困难的,因为你必须修改当前正在执行的代码。不过这是可能的。
这种方法具有许多优点。首先,它在应用程序执行之前就能得到地址空间。第二,它既能在Windows 98上使用,也能在Windows 2000上使用。第三,由于你不是调试者,因此能够很容易使用插入的D L L来调试应用程序。最后,这种方法可以同时用于控制台和G U I应用程序。
当然,这种方法也有某些不足。只有当你的代码是父进程时,才能插入D L L。另外,这种方法当然不能跨越不同的C P U来运行,必须对不同的C P U平台进行相应的修改。
将D L L插入进程的地址空间是确定进程运行状况的一种很好的方法。但是,仅仅插入D L L无法提供足够的信息,人们常常需要知道某个进程中的线程究竟是如何调用各个函数的,也可能需要修改Wi n d o w s函数的功能。
例如,我知道一家公司生产的D L L是由一个数据库产品来加载的。该D L L的作用是增强和扩展数据库产品的功能。当数据库产品终止运行时,该D L L就会收到一个D L L _ P R O C E S S_ D E TA C H通知,并且只有在这时,它才执行它的所有清除代码。该D L L将调用其他D L L中的函数,以便关闭套接字连接、文件和其他资源,但是当它收到D L L _ P R O C E S S _ D E TA C H通知时,进程的地址空间中的其他D L L已经收到它们的D L L _ P R O C E S S _ D E TA C H通知。因此,当该公司的D L L试图清除时,它调用的许多函数的运行将会失败,因为其他D L L已经撤消了初始化信息。
该公司聘请我去帮助他们解决这个问题,我建议挂接函数E x i t P r o c e s s。如你所知,调用E x i t P r o c e s s将导致系统向该D L L发送D L L _ P R O C E S S _ D E TA C H通知。通过挂接E x i t P r e c e s s函数,我们就能确保当E x i t P r o c e s s函数被调用时,该公司的D L L能够得到通知。这个通知将在任何D L L得到D L L _ P R O C E S S _ D E TA C H通知之前进来,因此进程中的所有D L L仍然处于初始化状态中,并且能够正常运行。此时,该公司的D L L知道进程将要终止运行,并且能够成功地执行它的全部清除操作。然后,操作系统的E x i t P r o c e s s函数被调用,使所有D L L收到它们的D L L _ P R O C E S S _ D E TA C H通知并进行清除操作。当该公司的D L L收到这个通知时,它将不执行专门的清除操作,因为它已经做了它必须做的事情。
在这个例子中,插入D L L是可以随意进行的,因为数据库应用程序的设计已经允许进行这样的插入,并且它加载了公司的D L L。当该公司的D L L被加载时,它必须扫描所有已经加载的可执行模块和D L L模块,以便找出对E x i t P r o c e s s的调用。当它发现对E x i t P r o c e s s的调用后,D L L必须修改这些模块,这样,这些模块就能调用公司的D L L中的函数,而不是调用操作系统的E x i t P r o c e s s函数(这个过程比想象的情况要简单的多)。一旦公司的E x i t P r o c e s s替换函数(即通常所说的挂钩函数)执行它的清除代码,操作系统的E x i t P r o c e s s函数(在K e r n e l 3 2 . d l l文件中)就被调用。
这个例子显示了挂接A P I的一种典型用法。它用很少的代码解决了一个非常实际的问题。
22.9.1 通过改写代码来挂接API
A P I挂接并不是一个新技术,多年来编程人员一直在使用A P I挂接方法。如果要解决上面所说的问题,那么人们首先看到的“解决方案”是通过改写代码来进行挂接。下面是具体的操作方法:
1) 找到你想挂接的函数在内存中的地址(比如说K e r n e l 3 2 . d l l中的E x i t P r o c e s s)。
2) 将该函数的头几个字节保存在你自己的内存中。
3) 用一个JUMP CPU指令改写该函数的头几个字节,该指令会转移到你的替换函数的内存地址。当然,你的替换函数的标记必须与你挂接的函数的标记完全相同,即所有的参数必须一样,返回值必须一样,调用规则必须一样。
4) 现在,当一个线程调用已经挂接的函数时, J U M P指令实际上将转移到你的替换函数。这时,你就能够执行任何代码。
5) 取消函数的挂接状态,方法是取出(第二步)保存的字节,将它们放回挂接函数的开头。
6) 调用挂接的函数(它已不再被挂接),该函数将执行其通常的处理操作。
7) 当原始函数返回时,再次执行第二和第三步,这样你的替换函数就可以被调用。
这种方法在1 6位Wi n d o w s编程员中使用得非常普遍,并且用得很好。今天,这种方法存在着若干非常严重的不足,因此建议尽量避免使用它。首先,它对C P U的依赖性很大,在x 8 6、A l p h a和其他的C P U上的J U M P指令是不同的,必须使用手工编码的机器指令才能使这种方法生效。第二,这种方法在抢占式多线程环境中根本不起作用。线程需要占用一定的时间来改写函数开头的代码。当代码被改写时,另一个线程可能试图调用该同一个函数。结果将是灾难性的。因此,只有当你知道在规定的时间只有一个线程试图调用某个函数时,才能使用这种方法。
在Windows 98上,主要的Windows DLL(K e r n e l 3 2、A d v A P I 3 2、U s e r 3 2和G D I 3 2)是这样受到保护的,即应用程序不能改写它们的代码页面。通过编写虚拟设备驱动程序(V x D)才能够获得这种保护。
22.9.2 通过操作模块的输入节来挂接API
另一种A P I挂接方法能够解决我前面讲到的两个问题。这种方法实现起来很容易,并且相当健壮。但是,要理解这种方法,必须懂得动态连接是如何工作的。尤其必须懂得模块的输入节中保护的是什么信息。第1 9章已经用了较多的篇幅介绍了输入节是如何生成的以及它包含的内容。当阅读下面的内容时,可以回头参考第1 9章的有关说明。
如你所知,模块的输入节包含一组该模块运行时需要的D L L。另外,它还包含该模块从每个D L L输入的符号的列表。当模块调用一个输入函数时,线程实际上要从模块的输入节中捕获需要的输入函数的地址,然后转移到该地址。
要挂接一个特定的函数,只需要改变模块的输入节中的地址,就这么简单。它不存在依赖C P U的问题。同时,由于修改了函数的代码,因此不需要担心线程的同步问题。
下面这个函数就负责执行这个重要的操作。它接受一个模块的输入节,以便引用特定地址上的一个符号。如果存在这样的引用,那么它就改变该符号的地址。
void ReplaceIATEntryInOneMod(PCSTR pszCalleeModName, PROC pfnCurrent, PROC pfnNew, HMODULE hmodCaller) { ULONG ulSize; PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR) ImageDirectoryEntryToData(hmodCaller, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize); if(pImportDesc == NULL) return; // This module has no import section. //Find the import descriptor containing references //to callee's functions. for(; pImportDesc->Name; pImportDesc++) { PSTR pszModName = (PSTR) ((PBYTE) hmodCaller + pImportDesc->Name); if(lstrcmpiA(pszModName, pszCalleeModName) == 0) break; } if(pImportDesc->Name == 0) // This module doesn't import any functions from this callee. return; //Get caller's import address table (IAT) //for the callee's functions. PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA) ((PBYTE) hmodCaller + pImportDesc->FirstThunk); //Replace current function address with new function address. for(; pThunk->u1.Function; pThunk++) { // Get the address of the function address. PROC* ppfn = (PROC*) &pThunk->u1.Function; // Is this the function we're looking for? BOOL fFound = (*ppfn == pfnCurrent); // See the sample code for some tricky Windows 98 // stuff that goes here. if(fFound) { //The addresses match; change the import section address. WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew, sizeof(pfnNew), NULL); return; // We did it; get out. } } //If we get to here, the function //is not in the caller's import section. }
PROC pfnOrig = GetProcAddress(GetModuleHandle("Kernel32"), "ExitProcess"); HMODULE hmodCaller = GetModuleHandle("DataBase.exe"); void ReplaceIATEntryInOneMod( "Kernel32.dll", // Module containing the function (ANSI) pfnOrig, // Address of function in callee MyExitProcess, // Address of new function to be called hmodCaller); // Handle of module that should call the new function
如果D a t a B a s e . e x e有一个输入节,那么I m a g e D i r e c t o r y E n t r y To D a t a就返回该输入节的地址,该地址是一个类型为P I M A G E _ I M P O RT _ D E S C R I P TO R的指针。现在我们必须查看该模块的输入节,找出包含我们想要修改的输入函数的D L L。在这个例子中,我们查找从“ K e r n e l 3 2 . d l l”输入的符号(这是传递给R e p l a c e I AT E n t r y I n O n e M o d函数的第一个参数)。f o r循环负责扫描D L L模块的名字。注意,模块的输入节中的所有字符串都是用A N S I(决不能用U n i c o d e)编写。这就是为什么要显式调用l s t r c m p i A而不是l s t r c m p i宏的原因。
如果该循环终止运行,但是没有找到对“ K e r n e l 3 2 . d l l”中的任何符号的引用,那么该函数就返回,并且仍然无所事事。如果模块的输入节确实引用了“ K e r n e l 3 2 . d l l”中的符号,那么将得到包含输入符号信息的I M A G E _ T H U N K _ D ATA结构的数组的地址。然后,重复引用来自“K e r n e l 3 2 . d l l”的所有输入符号,寻找与符号的当前地址相匹配的地址。在我们的例子中,我们寻找的是与E x i t P r o c e s s函数的地址相匹配的地址。
如果没有与我们寻找的地址相匹配的地址,那么这个方法决不能输入需要的的符号,而R e p l a c e I AT E n t r y I n O n e M o d 函数则返回。如果找到了一个匹配的地址,便调用Wr i t e P r o c e s s M e m o r y函数,以便改变替换函数的地址。使用Wr i t e P r o c e s s M e m o r y函数,而不是I n t e r l o c k e d E x c h a n g e P o i n t e r函数是因为Wr i t e P r o c e s s M e m o r y能够改变字节,而不管这些字节拥有什么页面保护属性。例如,如果页面拥有PA G E _ R E A D O N LY保护属性,那么I n t e r l o c k e dE x c h a n g e P o i n t e r函数将会引发访问违规,而Wr i t e P r o c e s s M e m o r y函数则能够处理页面保护属性的所有变更,并且仍然能够正常运行。
从现在起,当任何线程执行D a t a B a s e . e x e模块中调用E x i t P r o c e s s的代码时,就能够很容易得到K e r n e l 3 2 . d l l中的E x i t P r o c e s s函数的实地址,并在我们想要进行通常的E x i t P r o c e s s处理时调用它。
注意,R e p l a c e I AT E n t r y I n O n e M o d函数能够改变由单个模块中的代码进行的函数调用。但是,另一个D L L可能位于该地址空间中,而该D L L也可能调用E x i t P r o c e s s。如果D a t a B a s e . e x e之外的一个模块试图调用E x i t P r o c e s s,那么在调用K e r n e l 3 2 . d l l中的E x i t P r o c e s s时,它的调用将会成功。
如果想要捕获从所有模块对E x i t P r o c e s s进行的所有调用,必须为加载到进程的地址空间中的每个模块进行一次对R e p l a c e I AT E n t r y I n O n e M o d函数的调用。为此,我编写了另一个函数,称为R e p l a c e I AT E n t r y I n A l l M o d s。该函数仅仅使用To o l H e l p函数来枚举加载到进程的地址空间中的所有模块,然后为每个模块调用R e p l a c e I AT E n t r y I n O n e M o d,并为最后一个参数传递相应的模块句柄。
在少数几个地方可能发生一些问题。例如,如果在调用R e p l a c e I AT E n t r y I n A l l M o d s后,线程又调用L o a d L i b r a r y函数来加载新D L L,将会出现什么情况呢?在这种情况下,新加载的D L L可能调用没有挂接的E x i t P r o c e s s函数。为了解决这个问题,必须挂接L o a d L i b r a r y A、L o a d L i b r a r y W、L o a d L i b r a r y E x A和L o a d L i b r a r y E x W等函数,这样,就能够捕获这些函数的调用,并且为新加载的模块调用R e p l a c e I AT E n t r y I n O n e M o d。
最后一个问题与G e t P r o c A d d r e s s有关。比如说有一个线程执行下面的代码:
typedef int (WINAPI *PFNEXITPROCESS)(UINT uExitCode); PFNEXITPROCESS pfnExitProcess = (PFNEXITPROCESS)GetProcAddress( GetModuleHandle("Kernel32"), "ExitProcess"); pfnExitProcess(0);
下一节中展示的示例应用程序显示了如何进行A P I挂接,同时也解决了所有的L o a d L i b r a r y和G e t P r o c A d d r e s s函数的问题。
22.9.3 LastMsgBoxInfo示例应用程序
清单2 2 - 4中列出的L a s t M s g B o x I n f o应用程序(“22 LastMsgBoxInfo.exe”)展示了A P I挂接的方法。它挂接了对U s e r 3 2 . d l l中包含的所有M e s s a g e B o x函数的调用。若要挂接所有进程中的该函数,该应用程序使用Wi n d o w s挂接方法进行D L L的插入操作。该应用程序和D L L的源代码和资源文件均位于本书所附光盘上的22- LastMsgBoxInfo和22- LastMsgBoxInfoLib目录下。
当运行该应用程序时,将出现图2 2 - 6所示的对话框。
图22-6 运行L a s t M s g B o xIn f o时出现的对话框
这时,该应用程序进入等待状态。现在运行任何一个应用程序,使它显示一个消息框。为了测试的目的,我总是运行N o t e p a d,输入一些文字,然后设法关闭N o t e p a d,但是不保存输入的文字。这使得N o t e p a d显示图2 2 - 7所示的消息框。
当关闭这个消息框时,L a s t M s g B o x I n f o对话框将如图2 2 - 8所示。
图22-7 运行Notepad 时显示的消息框
图22-8 关闭Notepad 时显示的L a s t M s g B o x I n f o对话框
可以看到,L a s t M s g B o x I n f o应用程序能够知道其他进程是如何调用M e s s a g e B o x函数的。
显示和管理Last MessageBox Info对话框的代码是非常简单的。A P I挂接的设置正是全部工作的难点之所在。为了使A P I的挂接操作更加容易一些,我创建了一个CAPIHook C++类。这个类的定义是在A P I H o o k . h文件中,类的实现是在A P I H o o k . c p p文件中。这个类的使用是很方便的,因为它只有很少几个公有成员函数:一个构造函数,一个析构函数,还有一个返回原始函数的地址的函数。
若要挂接一个函数,只要像下面这样创建这个类的一个实例:
CAPIHook g_MessageBoxA("User32.dll", "MessageBoxA", (PROC) Hook_MessageBoxA, TRUE); CAPIHook g_MessageBoxW("User32.dll", "MessageBoxW", (PROC) Hook_MessageBoxW, TRUE);
我的C A P I H o o k类的构造函数只记住你决定要挂接的是什么A P I,并调用R e p l a c e I ATE n t r y I n A l l M o d s,以便进行实际的挂接操作。
另一个公有成员函数是析构函数。当一个C A P I H o o k对象超出作用域时,析构函数就调用R e p l a c e I AT E n t r y I n A l l M o d s,将符号的地址恢复成每个模块中它的原始地址,函数不再挂接。
第三个公有成员函数返回原始函数的地址。这个成员函数通常从替换函数内部进行调用,以便调用原始函数。下面是H o o k _ M e s s a g e B o x A函数中的代码:
int WINAPI Hook_MessageBoxA(HWND hWnd, PCSTR pszText, PCSTR pszCaption, UINT uType) { int nResult = ((PFNMESSAGEBOXA)(PROC) g_MessageBoxA) (hWnd, pszText, pszCaption, uType); SendLastMsgBoxInfo(FALSE, (PVOID) pszCaption, (PVOID) pszText, nResult); return(nResult); }
如果你使用这个C + +类,那么这就是挂接和撤消挂接输入函数的全部方法。如果你观察C A P I H o o k . c p p文件结尾处的代码,将会发现C + +类会自动建立C A P I H o o k对象的实例,以便捕获L o a d L i b r a r y A、L o a d L i b r a r y W、L o a d L i b r a r y E x A和L o a d L i b r a r y E x W。这样,C A P I H o o k类就能自动解决前面讲到的一些问题。
清单22-4 LastMsgBoxInfo示例应用程序