逆向工程核心原理第二十三章
DLL注入
DLL注入指的是向运行中的其他进程强制插入特定的DLL文件。从技术细节来说,DLL注入命令其他进程自行调用LoadLibrary() API,加载用户指定的DLL文件。DLL注入与一般DLL加载的区别在于,加载的目标进程是其自身或其他进程。
从上图可以看到,myhack.dll已被强制插入notepad.exe进程(本来notepad.exe并不会加载myhack.dll)。加载到notepad.exe进程中的myhack.dll与已经被加载到notepad.exe进程中的DLL(kernel32.dll、user32.dll)一样,拥有访问notepad.exe进程内存的(正当的)权限,这样用户就可以做任何想做的事了(添加一些新功能,或者修复bug等)
DLL(Dynamic Linked Library,动态链接库):DLL被加载到进程后会自动运行DllMain()函数,用户可以把想执行的代码放到DllMain()函数,每当加载DLL时,添加的代码就会自然而然得到执行。利用该特性可修复程序Bug,或向程序添加新功能。
DLL注入的实现方法
- 向某个进程注入DLL时主要使用以下三种方法
- 创建远程线程(CreateRemoteThread() API)
- 使用注册表(AppInit_DLLs值)
- 消息钩取(SetWindowsHookEx() API)
CreateRemoteThread()
因为这里用的是64位win10,所以作者提供的32位InjectDll.exe和myhack.dll文件不能生效。需要使用作者提供的源码编译成64位的PE文件
把重新编译生成的InjectDll.exe和myhack.dll放在一个文件夹下,以管理员身份运行cmd(命令行)
运行notepad.exe,然后使用Process Explorer获取notepad.exe的进程的PID值
命令行中运行一下命令注入myhack.dll,注意myhack.dll的路径必须位绝对路径,其中第一个参数6940即位notepad.exe此时的pid值
查看DebugView,可以看到myhack.dll的日志输出
myhack.dll所在路径也静默下载了指定网站的index.html
Process View查看notepad.exe的DLL,可以看到,myhack.dll已经被加载到notepad.exe进程中
分析示例源代码
先分析一下myhack.dll源代码(myhack.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
HMODULE g_hMod = NULL;
DWORD WINAPI ThreadProc(LPVOID lParam)
{
TCHAR szPath[_MAX_PATH] = {0,};
if( !GetModuleFileName( g_hMod, szPath, MAX_PATH ) )
return FALSE;
TCHAR *p = _tcsrchr( szPath, '\\' );
if( !p )
return FALSE;
_tcscpy_s(p+1, _MAX_PATH, DEF_FILE_NAME);
URLDownloadToFile(NULL, DEF_URL, szPath, 0, NULL);
return 0;
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
HANDLE hThread = NULL;
g_hMod = (HMODULE)hinstDLL;
switch( fdwReason )
{
case DLL_PROCESS_ATTACH :
OutputDebugString(L"<myhack.dll> Injection!!!");
hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
CloseHandle(hThread);
break;
}
return TRUE;
}在DllMain()函数中可以看到,该DLL被加载(DLL_PROCESS_ATTACH)时,先输入一个调试字符串("<myhack.dll> Injection!!!"),然后创建线程调用函数(ThreadProc)。在ThreadProc()函数中通过调用urlmon!URLDownloadToFile() API来下载指定网站的index.html文件。向进程注入DLL后就会自动调用执行该DLL的DLLMain()函数。所以当my hack.dll注入notepad.exe进程后,最终会调用执行URLDownloadToFile() API
接下来查看InjectDll.exe的源码InjectDll.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
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 InjectDll(DWORD dwPID, LPCTSTR szDllPath)
{
HANDLE hProcess = NULL, hThread = NULL;
HMODULE hMod = NULL;
LPVOID pRemoteBuf = NULL;
DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc;
// #1.使用dwPID获取目标进程(notepad.exe)句柄
if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )
{
_tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
return FALSE;
}
// #2. 在目标程序(notepad.exe)内存中分配szDllName大小的内存
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);
// #3. 将myhack.dll路径写入分配的内存
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);
// #4. 获取LoadLibraryW() API的地址
hMod = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");
// #5. 在notepad.exe进程中运行线程
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}
int _tmain(int argc, TCHAR *argv[])
{
if( argc != 3)
{
_tprintf(L"USAGE : %s <pid> <dll_path>\n", argv[0]);
return 1;
}
// change privilege
if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
return 1;
// inject dll
if( InjectDll((DWORD)_tstol(argv[1]), argv[2]) )
_tprintf(L"InjectDll(\"%s\") success!!!\n", argv[2]);
else
_tprintf(L"InjectDll(\"%s\") failed!!!\n", argv[2]);
return 0;
}main()函数的主要功能是检查输入程序的参数,然后先调用SetPrivilege()函数获取权限,再调用InjectDll()函数。InjectDll()函数是用来实施DLL注入的核心函数,其功能是命令目标进程自行调用LoadLibrary("myhack.dll") API。下面分析InjectDll()函数
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)
,获取目标进程句柄,调用OpenProcess() API,借助程序运行时以参数形式传递过来的dwPID值,获取notepad.exe进程的句柄(PROCESS_ALL_ACCESS权限)。得到PROCESS_ALL_ACCESS权限后,就可以使用获取的句柄(hProcess)控制对应的进程(notepad.exe)pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);
,将要注入的DLL路径写入目标进程内存。需要把即将加载的DLL文件的路径(字符串)告知目标进程(notepad.exe)。因为任何内存空间都无法进行写入操作,故先使用VirtualAllocEx() API在目标进程(notepad.exe)的内存空间中分配一块缓冲区,且指定该缓冲区的大小为DLL文件路径字符串的长度(含Terminating NULL即'\0')即可WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL)
,使用WriteProcessMemory() API将DLL路径字符串("C:\xxxx\myhack.dll")写入分配所得缓冲区(pRemoteBuf)地址。WriteProcessMemory() API所写的内存空间也是hProcess句柄所指的目标进程(notepad.exe)的内存空间。这样,要注入的DLL文件的路径就被写入目标进程(notepad.exe)的内存空间/** * 获取LoadLibraryW() API地址,调用LoadLibrary() API前先要获取其地址 *(LoadLibraryW()是LoadLibrary()的Unicode字符串版本) * 这里需要注意的是,我们的目标是获取加载到notepad.exe进程的kernel32.dll的LoadLibraryW() API * 的起始地址 * 但下面的代码切是用来获取加载到InjectDll.exe进程的kernel32.dll的LoadLibraryW()的起始地址 * 之所以可以这样是因为,在Windows系统中,kernel32.dll在每个进程中的加载地址都是相同的 */ hMod = GetModuleHandle(L"kernel32.dll"); pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- `hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);`,在目标进程中运行远程线程(Remote Thread)。pThreadProc = notepad.exe进程内存中的LoadLibraryW() 地址,pRemoteBuf = notepad.exe 进程内存中的"C:\xxxx\myhack.dll"字符串地址。一切准备就绪后,最后向notepad.exe发送一个命令,让其调用LoadLibraryW() API函数加载指定的DLL即可,但是Windows并未直接提供执行这一命令的API。不过,可以另辟蹊径,使用CreateRemoteThread()这个API(在DLL注入是几乎总会用到)
- CreateRemoteThread() API函数原型如下
```c++
HANDLE CreateRemoteThread(
[in] HANDLE hProcess, // 目标进程
[in] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress, // 线程函数地址
[in] LPVOID lpParameter,// 线程参数地址
[in] DWORD dwCreationFlags,
[out] LPDWORD lpThreadId
);除了第一个参数hProcess外,其他参数与CreateThread()函数完全一样。hProcess参数是要执行目标线程的目标进程的句柄。lpStartAddress与lpParameter参数分别给出线程函数地址与线程参数地址。需要注意的是,这两个地址都应该在目标进程虚拟内存空间中
这里可以用LoadLibrary作为第四个参数的原因是LoadLibrary函数原型与线程函数原型一样,两者都接受一个4字节参数并返回一个4字节的值,LoadLibrary和ThreadProc函数比较如下
1
2
3
4
5
6
7DWORD WINAPI ThreadProc(
[in] LPVOID lpParameter
)
DWORD WINAPI LoadLibrary(
[in] LPCTSTR lpFileName
)CreateRemoteThread()函数的最主要功能就是驱使目标进程调用LoadLibrary()函数,进而加载指定的DLL文件
AppInit_DLLs
进行DLL注入的第二种方法是使用注册表。Windows操作系统的注册表中默认提供了AppInit_DLLs与LoadAppInit_DLLs两个注册表项,如下图所示
在注册表编辑器中,将要注入的DLL的路径字符串写入AppInit_DLLs项目,然后把LoadAppInit_DLLs的项目值设置为1。重启后,指定DLL会注入所有运行进程。该方法操作非常简单,但功能非常强大。
同上编译生成x64的myhack2.dll,然后修改注册表AppInit_DLLs和LoadAppInit_DLLs的值
保存后,重启电脑,用Process Explorer查找加载了myhack2.dll的进程,可以看到,myhack2.dll成功被注入进程
接下来分析myhack2.dll源码,如下所示
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// myhack2.cpp
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
TCHAR szCmd[MAX_PATH] = {0,};
TCHAR szPath[MAX_PATH] = {0,};
TCHAR *p = NULL;
STARTUPINFO si = {0,};
PROCESS_INFORMATION pi = {0,};
si.cb = sizeof(STARTUPINFO);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
switch( fdwReason )
{
case DLL_PROCESS_ATTACH :
if( !GetModuleFileName( NULL, szPath, MAX_PATH ) )
break;
if( !(p = _tcsrchr(szPath, '\\')) )
break;
if( _tcsicmp(p+1, DEF_DST_PROC) )
break;
wsprintf(szCmd, L"%s %s", DEF_CMD, DEF_ADDR);
if( !CreateProcess(NULL, (LPTSTR)(LPCTSTR)szCmd,
NULL, NULL, FALSE,
NORMAL_PRIORITY_CLASS,
NULL, NULL, &si, &pi) )
break;
if( pi.hProcess != NULL )
CloseHandle(pi.hProcess);
break;
}
return TRUE;
}myhack2.dll的源代码很简单,若当前加载自己的进程为"notepad.exe",则以使用IE打开连接指定网站。
AppInit_DLLs注册表键非常强大,通过它几乎可以向所有进程注入DLL文件。若被注入的DLL出现问题(Bug),则有可能导致Windows无法正常启动,所以修改AppInit_DLLs前务必彻查
SetWindowsHookEx()
- 注入DLL的第三个方法就是消息钩取,即用SetWindowsHookEx() API安装好消息"钩子",然后由OS将指定DLL(含有"钩子"过程)强制注入相应(带窗口的)进程。
逆向工程核心原理第二十四章
DLL卸载
- DLL卸载是将强制插入进程的DLL弹出的一种技术,其基本原理与用CreateRemoteThread API进行DLL注入的原理类似
DLL卸载的工作原理
- 前面利用CreateRemoteThread() API进行DLL注入的工作原理:驱使目标进程调用LoadLibrary() API;同样DLL卸载工作原理也与之类似:驱使目标进程调用FreeLibrary() API
- 也就是说,将FreeLibrary() API的地址传递给CreateRemoteThread()的lpStartAddress参数,并把要卸载的DLL的句柄传递给lpParameter参数
实现DLL卸载
首先分析一下EjectDll.exe源码,它用来从目标进程卸载指定的DLL文件(myhack.dll,已注入目标进程)
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
162// EjectDll.exe
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_SNAPMODULE 参数,获取加载到notepad.exe进程的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信息
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID);
使用CreateToolhelp32Snapshot() API可以获取加载到进程的模块(DLL)信息。将获取的hSnapshot句柄传递给Module32First()/Module32Next()函数后,即可设置与MODULEENTRY32结构体相关的模块信息。
1
2
3
4
5
6
7
8
9
10
11
12typedef 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文件名称,能够准确查找到相应模块的信息
DLL卸载练习
同样用vs将EjectDll.cpp编译生成64位的可执行文件来测试DLL卸载
将EjectDll.exe复制到之前InjectDll.exe和myhack.dll一个目录下
用Process Explorer查看notepad.exe的PID,然后使用InjectDll注入myhack.dll
可以看到myhack.dll已成功注入到notepad.exe的进程中
接下来卸载myhack.dll,使用命令行运行EjectDll.exe,然后用Process Explorer验证是否成功卸载