本文最后更新于:2022年5月19日 晚上
第24章DLL卸载
DLL卸载(DLL Ejection )是将强制插入进程的DLL弹出的一种技术,其基本工作原理与使用CreateRemoteThread API进行DLL注入的原理类似。
24.1 DLL卸载的工作原理
前面我们学习过使用CreateRemoteThread() API进行DLL注入的工作原理,概括如下:
驱使目标进程调用LoadLibrary() API
同样,DLL卸载工作原理也非常简单:
驱使目标进程调用FreeLibrary() API
也就是说,将FreeLibrary() API的地址传递给CreateRemoteThread()的lpStartAddress参数,并把要卸载的DLL的句柄传递给IpParameter参数。
每个Windows内核对象(Kernel Object)都拥有一个引用计数(Reference Count),代表对象被使用的次数。调用10次LoadLibrary("a.dll")
,a.dll的引用计数就变为10,卸载a.dll时同样需要调用10次Freelibrary()
(每调用一次LoadLibrary()
,引用计数会加1;而每调用一次Freelibrary()
,引用计数会减1)。因此,卸载DLL时要充分考虑好“引用计数”这个因素。
24.2实现DLL卸载
下面介绍的源代码使用Microsoft Visual C++ Express 2010编写而成,并在Windows XP/7 32位系统中通过测试。
首先分析一下EjectDll.exe程序,它用来从目标进程(notepad.exe)卸载指定的DLL文件(myhack.dll,已注入目标进程),程序源代码(EjectDll.cpp )如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
|
#include "windows.h" #include "tlhelp32.h" #include "tchar.h"
#define DEF_PROC_NAME (L"notepad.exe") #define DEF_DLL_NAME (L"myhack.dll")
DWORD FindProcessID(LPCTSTR szProcessName) { DWORD dwPID = 0xFFFFFFFF; HANDLE hSnapShot = INVALID_HANDLE_VALUE; PROCESSENTRY32 pe;
pe.dwSize = sizeof( PROCESSENTRY32 ); hSnapShot = CreateToolhelp32Snapshot( TH32CS_SNAPALL, NULL );
Process32First(hSnapShot, &pe); do { if(!_tcsicmp(szProcessName, (LPCTSTR)pe.szExeFile)) { dwPID = pe.th32ProcessID; break; } } while(Process32Next(hSnapShot, &pe));
CloseHandle(hSnapShot);
return dwPID; }
BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege) { TOKEN_PRIVILEGES tp; HANDLE hToken; LUID luid;
if( !OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken) ) { _tprintf(L"OpenProcessToken error: %u\n", GetLastError()); return FALSE; }
if( !LookupPrivilegeValue(NULL, lpszPrivilege, &luid) ) { _tprintf(L"LookupPrivilegeValue error: %u\n", GetLastError() ); return FALSE; }
tp.PrivilegeCount = 1; tp.Privileges[0].Luid = luid; if( bEnablePrivilege ) tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; else tp.Privileges[0].Attributes = 0;
if( !AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES) NULL, (PDWORD) NULL) ) { _tprintf(L"AdjustTokenPrivileges error: %u\n", GetLastError() ); return FALSE; }
if( GetLastError() == ERROR_NOT_ALL_ASSIGNED ) { _tprintf(L"The token does not have the specified privilege. \n"); return FALSE; }
return TRUE; }
BOOL EjectDll(DWORD dwPID, LPCTSTR szDllName) { BOOL bMore = FALSE, bFound = FALSE; HANDLE hSnapshot, hProcess, hThread; HMODULE hModule = NULL; MODULEENTRY32 me = { sizeof(me) }; LPTHREAD_START_ROUTINE pThreadProc;
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID);
bMore = Module32First(hSnapshot, &me); for( ; bMore ; bMore = Module32Next(hSnapshot, &me) ) { if( !_tcsicmp((LPCTSTR)me.szModule, szDllName) || !_tcsicmp((LPCTSTR)me.szExePath, szDllName) ) { bFound = TRUE; break; } }
if( !bFound ) { CloseHandle(hSnapshot); return FALSE; }
if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) ) { _tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError()); return FALSE; }
hModule = GetModuleHandle(L"kernel32.dll"); pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hModule, "FreeLibrary"); hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, me.modBaseAddr, 0, NULL); WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread); CloseHandle(hProcess); CloseHandle(hSnapshot);
return TRUE; }
int _tmain(int argc, TCHAR* argv[]) { DWORD dwPID = 0xFFFFFFFF; dwPID = FindProcessID(DEF_PROC_NAME); if( dwPID == 0xFFFFFFFF ) { _tprintf(L"There is no <%s> process!\n", DEF_PROC_NAME); return 1; }
_tprintf(L"PID of \"%s\" is %d\n", DEF_PROC_NAME, dwPID);
if( !SetPrivilege(SE_DEBUG_NAME, TRUE) ) return 1;
if( EjectDll(dwPID, DEF_DLL_NAME) ) _tprintf(L"EjectDll(%d, \"%s\") success!!!\n", dwPID, DEF_DLL_NAME); else _tprintf(L"EjectDll(%d, \"%s\") failed!!!\n", dwPID, DEF_DLL_NAME);
return 0; }
|
前面介绍过,卸载DLL的原理是驱使目标对象自己调用FreeLibrary() API,上述代码中的EjectDll()函数就是用来卸载DLL的。下面仔细分析一下EjectDll()函数。
24.2.1获取进程中加载的DLL信息
1
| hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID);
|
使用CreateToolhelp32Snapshot() API可以获取加载到进程的模块(DLL)信息。将获取的hSnapshot句柄传递给Module32First()/Module32Next()函数后,即可设置与MODULEENTRY32结构体相关的模块信息。代码24-2是MODULEENTRY32结构体的定义。
1 2 3 4 5 6 7 8 9 10 11 12
| typedef struct tagMODULEENTRY32 { DWORD dwSize; DWORD th32ModuleID; DWORD th32ProcessID; DWORD GlblcntUsage; DWORD ProccntUsage; BYTE *modBaseAddr; DWORD modBaseSize; HMODULE hModule; char szModule[MAX_MODULE_NAME32 + 1]; char szExePath[MAX_PATH]; } MODULEENTRY32;
|
szModule成员表示DLL的名称,modBaseAddr成员表示相应DLL被加载的地址(进程虚拟内存)。在EjectDll()函数的for循环中比较szModule与希望卸载的DLL文件名称,能够准确查找到相应模块的信息。
24.2.2获取目标进程的句柄
1
| hProcess=openProcess(PROCESS_ALL_ACCESS,FALSE,dwPID);
|
该语句使用进程ID来获取目标进程(notepad)的进程句柄(下面用获得的进程句柄调用CreateRemoteThread() API)。
24.2.3 获取 FreeLibrary() API 地址
1 2
| hModule=GetModuleHandle(L"kernel32.dll"); pThreadProc=(LPTHREAD_START_ROUTINE)GetProcAddress(hModule, "FreeLibrary");
|
若要驱使notepad进程自己调用FreeLibrary() API,需要先得到FreeLibrary()的地址。然而上述代码获取的不是加载到notepad.exe进程中的Kernel32!FreeLibrary地址,而是加载到EjectDll.exe进程中的Kemel32!FreeLibrary地址。如果理解了前面学过的有关DLL注入的内容,那么各位应该能猜出其中缘由——FreeLibrary地址在所有进程中都是相同的。
24.2.4在目标进程中运行线程
1
| hThread=CreateRemoteThread(hProcess,NULL,0,pThreadProc,me.modBaseAddr, 0,NULL);
|
pThreadProc参数是FreeLibrary() API的地址,me.modBaseAddr参数是要卸载的DLL的加载地址。将线程函数指定为FreeLibrary函数,并把DLL加载地址传递给线程参数,这样就在目标进程中成功调用了FreeLibrary() API ( CreateRemoteThread() API原意是在外部进程调用执行线程函数,只不过这里的线程函数换成了FreeLibrary()函数)。
1 2 3 4 5
| BOOL FreeLibrary( HMODULE hLibModule );
|
ThreadProc函数与FreeLibrary函数都只有1个参数,以上方法的灵感即源于此。
24.3 DLL卸载练习
本节一起做个练习,先将myhack.dll注入notepad.exe进程,随后再将其卸载。
24.3.1 复制文件及运行notepad.exe
首先,复制下面3个文件到工作文件夹(c:\work),如图24-1所示。
然后,运行notepad.exe并查看其PID,如图24-2所示。我的电脑环境中,notepad.exe的PID为2832。
24.3.2 注入 myhack.dll
打开命令行窗口( cmd.exe ),输入如下参数,将myhack.dll文件注入notepad.exe进程,如图24-3
所示。
可以在Process Explorer中看到myhack.dll注入成功,如图24.4所示。
24.3.3 卸载 myhack.dll
打开命令行窗口(cmd.exe),输入如下参数,将注入notepad.exe进程的myhack.dll文件卸载下
来,如图24-5所示。
请使用Process Explorer查看是否成功卸载。DLL卸载的基本原理与DLL注入的原理相同,理
解起来非常容易。请各位认真阅读上面的内容并亲自操作。
Q.使用FreeLibrary()卸载DLL的方法好像仅适用于使用CreateRemoteThread()注入的DLL 文件,有没有什么方法可以将加载的普通DLL文件卸载下来呢?
A.正如您所说,使用FreeLibrary()的方法仅适用于卸载自己强制注入的DLL文件。PE文件直接导入的DLL文件是无法在进程运行过程中卸载的。
参考
《逆向工程核心原理》 第24章