第24章 DLL卸载

本文最后更新于: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
// EjectDll.exe

#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;

// Get the snapshot of the system
pe.dwSize = sizeof( PROCESSENTRY32 );
hSnapShot = CreateToolhelp32Snapshot( TH32CS_SNAPALL, NULL );

// find process
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, // lookup privilege on local system
lpszPrivilege, // privilege to lookup
&luid) ) // receives LUID of privilege
{
_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;

// Enable the privilege or disable all privileges.
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;

// dwPID = notepad 进程 ID
// 使用TH32CS_SNAPM0DULE参数,获取加载到notepad进程的DLL名称
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;

// find process
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);

// change privilege
if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
return 1;

// eject dll
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
// handle to loaded library module
);

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章


第24章 DLL卸载
https://m0ck1ng-b1rd.github.io/1999/02/22/逆向工程核心原理/第24章 DLL卸载/
作者
何语灵
发布于
1999年2月22日
许可协议