第43章 内核6中的DLL注入

本文最后更新于:2022年5月27日 下午

第43章内核6中的DLL注入

本章将讲解在Windows OS Kernel 6 (Vista、7、8等)中实施DLL注入的方法。由于从Kernel 6开始采用了新的会话管理机制,这使得通过CreateRemoteThread() API注入DLL的旧方法对某些进程(服务进程)不再适用。本章将调试相关API,分析注入失败的原因,然后寻求解决之道。原有的DLL注入技术是通过调用CreateRemoteThread() API进行的,在Windows XP、2000中能够准确完成DLL注入操作。但Windows 7中该方法不太奏效,准确地说就是,在Windows 7中使用CreateRemoteThread() API无法完成对服务(Service)进程的DLL注入操作。原因在于,Windows7中的会话管理机制已经发生了变化。下面通过一个简单的练习示例再现DLL注入失败的情形,并分析失败原因,进而查找解决之策。

本示例文件在Windows7 32位系统中通过测试。

43.1 再现DLL注入失败

尝试将Dummy.dll文件注入Windows的系统进程时,会出现注入失败。本节中再现这种注入失败的情形(注入程序是之前用过的InjectDll.exe)。

43.1.1 源代码

先简单看一下相关源代码。

InjectDll.cpp

源代码中的核心部分是InjectDll()函数

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

if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )
{
_tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
return FALSE;
}

pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);

WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);

hMod = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");

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

代码43-1是典型的DLL注入代码,前面我们已经多次分析过,相信大家已经非常熟悉了(更多说明请参考第23章)。

Dummy.cpp

接下来查看Dummy.dll文件的源代码(dummy.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
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
TCHAR szPath[MAX_PATH] = {0,};
TCHAR szMsg[1024] = {0,};
TCHAR *p = NULL;

switch( fdwReason )
{
case DLL_PROCESS_ATTACH :
GetModuleFileName(NULL, szPath, MAX_PATH);
p = _tcsrchr(szPath, L'\\');
if( p != NULL )
{
_stprintf_s(szMsg, 1024 - sizeof(TCHAR),
L"Injected in %s(%d)",
p + 1, // Process Name
GetCurrentProcessId()); // PID
OutputDebugString(szMsg);
}

break;
}

return TRUE;
}

DllMain()函数代码非常简单,若dummy.dll被成功注入指定进程,就输出相关调试信息(进程名称、进程ID)。

43.1.2注入测试

首先运行Process Explorer工具,查看目标进程的PID (svhost.exe (属于会话0,PID为3300)、notepad.exe(属于会话1,PID为3964)),然后再使用InjectDll.exe分别向它们注入dummy.dll文件,如图43-1所示。

注入前先把lnjectDll.exe与dummy.dll文件复制到工作文件夹,然后运行InjectDll.exe命令实施注入,如图43-2所示。

dummy.dll文件成功注入notepad.exe进程(属于会话1,PID为3964),但向svchost.exe (属于会话0,PID为3300)注入时却发生了失败(error code=8)。在Process Explorer中搜索dummy.dll模块。

就像在图43-3中看到的一样,dummy.dll文件只成功注入notepad.exe进程(属于会话1,PID为3964)。

43.2原因分析

43.2.1 调试 #1

如图43-2所示,向svchost.exe进程(属于会话0,PID为3300)注入的过程中,调用CreateRemoteThread() API 函数时发生了失败,错误代码为8 (ERROR_NOT_ENOUGH_MEMORY)。下面使用OllyDbg工具调试InjectDll.exe文件。在Open对话框中选择InjectDll.exe文件,输入相应参数后单击“打开”按钮,如图43-4所示。

我们已经知道调用CreateRemoteThread() API时会发生错误,所以使用鼠标右键菜单中的Search for All intermodular calls菜单,直接在API的调用代码处设置断点,如图43-5所示。

InjectDll.exe进程中并未应用ASLR技术。

按F9运行程序,调试器将在断点处暂停,如图43-6所示。

然后按F8键(StepOver)执行调用指令,在OllyDbg的寄存器窗口中可以看到“LastErr=ERROR_NOT_ENOUGH_MEMORY(8)” 字样,如图43-7所示。

以上通过OllyDbg工具再现了注入失败的情形,但仍未能找到确切原因。只有直接调试kernel32!CreateRemoteThread() API才能准确把握失败原因。

43.2.2调试 #2

重新运行OllyDbg调试器,暂停在InjectDll.exe调用CreateRemoteThread()的代码处(参考图43-8)。

查看存储在栈中的CreateRemoteThread() API的参数,如图43-9所示

对上图中的重要参数说明如下:

① svchost.exe (PID: 3300)的进程句柄。

② kernel32!LoadLibraryA() API地址。

③ svchost.exe的进程内存中分配的缓冲区地址。

在图43-8中使用StepIn(F7)命令,进入kernel32!CreateRemoteThreacl()API,如图43-10所示。

kernelbase.dll 是从 Vista 开始新增的 DLL 文件,负责包装(wrapper) kernel32.dll。

继续按F7键运行到kernelbase!CreateRemoteThreadEx()调用前,查看栈中存储的参数,如图

43-11所示。

kernelbase! CreateRemoteThreadEx()的参数与kernel32!CreateRemoteThread()的参数几乎一样,

只多了 1 个 IpAttributeList 参数(Arg8)。继续进人 kernelbase!CreateRemoteThreadEx()代码

(StepInto(F7)),在代码窗口中向下拖动滚动条,可以看到调用ntdll!ZwCreateThreadEx() API的代码,如图43-12所示。

运行到ZwCreateThreadEx()调用前,查看栈中存储的参数,如图43-13所示。

从栈中可以看到,ZwCreateThreadEx()拥有很多个参数。比较图43-9与图43-13可以发现,重

要的参数①~③都被原样传递过来。继续跟踪进人ntdll!ZwCreateThreadEx()API,可以看到它最终

通过SYSENTER指令进人内核模式,无法继续用户模式调试。

实际上,kernelbase!CreateRemoteThreadEx()与ntdll!ZwCreateThreadEx()都是从Vista开始新增的API (XP之前的版本中不存在)。在XP操作系统中,kernel32!CreateRemoteThread()内部会直接调用ZwCreateThreadEx()函数。在Windows XP与Windows 7中调用kernel32!CreateRemoteThread()的流程分别如图43-14所示。

到此我们可以推测岀,DLL注入失败的原因在于系统中这些新增的API,正是它们导致了向运行在会话0中的服务进程注入DLL操作失败。

Ntdll!ZwCreateThreadEx()

由于kernelbase!CreateRemoteThreadEx()只是kernel32!CreateRemoteThread()的包装器(wrapper),所以问题的原因可能在ntdll!ZwCreateThreadEx()中。ntdll!ZwCreateThreadEx()是一个尚未公开的API, MSDN中查不到函数的定义,使用Google搜索查找。

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
typedef NTSTATUS (WINAPI *LPFUN_NtCreateThreadEx)
(
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN LPVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN LPTHREAD_START_ROUTINE lpStartAddress,
IN LPVOID lpParameter,
IN BOOL CreateSuspended,
IN ULONG StackZeroBits,
IN ULONG SizeOfStackCommit,
IN ULONG SizeOfStackReserve,
OUT LPVOID lpBytesBuffer
);

# This function is almost similar to CreateRemoteThread function except the last parameter which takes unknown buffer structure. Here is the definition of that buffer structure parameter...

struct NtCreateThreadExBuffer
{
ULONG Size;
ULONG Unknown1;
ULONG Unknown2;
PULONG Unknown3;
ULONG Unknown4;
ULONG Unknown5;
ULONG Unknown6;
PULONG Unknown7;
ULONG Unknown8;
};

通过Google搜索发现,在Windows Vista以后的OS中进行DLL注入操作时,直接调用ZwCreateThreadEx()而非CreateRemoteThread()就能成功注入DLL。从我的测试结果看,这样做非常成功,且不受所在会话的影响。

比较该方法中使用的参数与图43-13中的参数可以发现,它们的不同在于第七个参数(CreateSuspended)。直接调用ZwCreateThreadEx()成功注入DLL时,CreateSuspended参数值为FALSE(0),而在CreateRemoteThread() API内部调用ZwCreateThreadEx()时,该CreateSuspended参数值为TRUE(1)。这就是DLL注入失败的原因。

从Windows XP开始,CreateRemoteThread() API内部的实现算法采用了挂起模式,即先创建出线程,再使用“恢复运行”方法继续执行(CreateSuspended=1)。

43.3练习:使 CreateRemoteThread()正常工作

我们现在已经知道了DLL注入失败的原因,也知道了解决方法,下面使用调试器直接修改测试,使调用CreateRemoteThread() API能够成功完成注入操作。

43.3.1 方法 #1:修改 CreateSuspended 参数值

修改 ZwCreateThreadEx() API 的 CreateSuspended参数值,就可在 Windows 7中成功调用CreateRemoteThread()API。重启调试器,运行到图43-12中调用ntdll.ZwCreateThreadEx()函数的位置,然后将存储在栈中的CreateSuspended参数值由1修改为0,如图43-15所示。

接下来使用StepOver(F8)命令,运行到ZwCreateThreadEx()调用后,dummy.dll成功注入指定服务进程,如图43-16所示。

使用DebugView工具可以查看dummy.dll的DllMain()函数中输出的调试日志,如图43-17所示。

在Windows 7中修改CreateSuspended参数值,借助CreateRemoteThread()API将指定DLL文件成功注入svchost.exe服务进程。

43.3.2方法#2:操纵条件分支

进一步调试kernelbase!CreateRemoteThreadEx()函数可以发现更多内容。在图43-12中,直接使用StepOver(F8)命令,执行到调用ZwCreateThreadEx()函数(CreateSuspended=TRUE)后,第一个参数pThread Handle被赋值,如图43-18所示。

创建岀线程句柄就意味着线程正常创建,也就是说,调用CreateRemoteThread()的过程中成功创建了远程线程。

这是一个非常重要的发现。虽然远程线程已被成功创建,但它无法正常工作,原因可能是后面调用ntdlllZwResumeThread()API时发生了失败,或者干脆无法调用(由于线程是以挂起模式创建的,必须“恢复运行”才能正常执行)。继续跟踪,查看调用ZwResumeThread() API的部分。 图43-19中是kernelbase!CreateRemoteThread() API代码的结束部分。

从图43-19中可以发现,调用758EBD33地址处的ntdll!CsrClientCallServer() API后,其下的条件分支指令(CMP/JL)使ZwResumeThread() API未被调用而直接跳转到后面。在调试器中调用ntdll!CsrClientCallServer() API后,操纵下面的条件分支指令使ZwResumeThread()得以调用执行,从而将DLL文件成功注入指定进程。根据Intel IA-32 Reference可知,SF!=OF时JL指令就会执行发生跳转,如图43-20所示,使用鼠标双击S Flag修改其值。

继续执行后,检查DLL文件是否成功注入指定进程。

通过上面的调试,我们掌握了在Windows 7中调用kernel32!CreateRemoteThread() API向服务进程注入DLL文件时失败的原因。并通过操纵kernel32!CreateRemoteThread() API的参数与代码,成功地将DLL文件注入指定服务进程(该方法借助调试器实现,不便推广通用)。接下来,我们 要根据上面的方法编写一个新的InjectDll.exe程序,该程序具有较好的通用性,在Windows7与XP中都能顺利完成DLL注入。

所有源代码均使用MS Visual C++ 2010 Express Edition编写而成,并在Windows 7 &XPSP3中通过测试。

43.4 稍作整理

正式编写新的DLL注入程序前,先简单整理前面学过的内容。由于Windows 7的会话管理机制发生了变化,kernel32!CreateRemoteThread() API的内部实现代码也发生了变化,最终使借助CreateRemoteThread()进行DLL注入的技术在向Windows7的服务进程(会话0)注入DLL文件时无法正常发挥作用。从调试kernel32!CreateRemoteThread()的结果看,原因在于,在API内部创建远程线程时采用了挂起模式,若远程进程属于会话0,则不会“恢复运行”,而是直接返回错误。

创建远程线程时,先采用挂起模式创建,再“恢复运行”,这是从XP就开始使用的一种实现方法

在kernel32!CreateRemoteThread() API内部调用ntdll!ZwCreateThreadEx() API,操作它的参数,或者强制改变错误条件分支语句,就可以正常创建远程线程,并成功实现DLL文件注入。

43.5 lnjectDII_new.exe

从前面的学习中我们知道,在Windows 7中实施DLL注入时直接调用ntdll!ZwCreateThreadEx() API要比调用kernel32!CreateRemoteThread()好得多。下面以此为基础编写一个新的InjectDll_new.exe程序,使之能够在Windows Kernel 6 (Vista、7、8等)中顺利完成DLL注入。

43.5.1 lnjectDII_new.cpp

首先看新编写的InjectDll()函数。

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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#include "windows.h"
#include "stdio.h"
#include "tchar.h"

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

typedef DWORD (WINAPI *PFNTCREATETHREADEX)
(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
DWORD dw1,
DWORD dw2,
LPVOID Unknown
);

BOOL IsVistaOrLater()
{
OSVERSIONINFO osvi;

ZeroMemory(&osvi, sizeof(OSVERSIONINFO));
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);

GetVersionEx(&osvi);

if( osvi.dwMajorVersion >= 6 )
return TRUE;

return FALSE;
}

BOOL MyCreateRemoteThread(HANDLE hProcess, LPTHREAD_START_ROUTINE pThreadProc, LPVOID pRemoteBuf)
{
HANDLE hThread = NULL;
FARPROC pFunc = NULL;

if( IsVistaOrLater() ) // Vista, 7, Server2008
{
pFunc = GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtCreateThreadEx");
if( pFunc == NULL )
{
printf("MyCreateRemoteThread() : GetProcAddress(\"NtCreateThreadEx\") failed!!! [%d]\n",
GetLastError());
return FALSE;
}

((PFNTCREATETHREADEX)pFunc)(&hThread,
0x1FFFFF,
NULL,
hProcess,
pThreadProc,
pRemoteBuf,
FALSE,
NULL,
NULL,
NULL,
NULL);
if( hThread == NULL )
{
printf("MyCreateRemoteThread() : NtCreateThreadEx() failed!!! [%d]\n", GetLastError());
return FALSE;
}
}
else // 2000, XP, Server2003
{
hThread = CreateRemoteThread(hProcess,
NULL,
0,
pThreadProc,
pRemoteBuf,
0,
NULL);
if( hThread == NULL )
{
printf("MyCreateRemoteThread() : CreateRemoteThread() failed!!! [%d]\n", GetLastError());
return FALSE;
}
}

if( WAIT_FAILED == WaitForSingleObject(hThread, INFINITE) )
{
printf("MyCreateRemoteThread() : WaitForSingleObject() failed!!! [%d]\n", GetLastError());
return FALSE;
}

return TRUE;
}

BOOL InjectDll(DWORD dwPID, char *szDllName)
{
HANDLE hProcess = NULL;
LPVOID pRemoteBuf = NULL;
FARPROC pThreadProc = NULL;
DWORD dwBufSize = strlen(szDllName)+1;

if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )
{
printf("[ERROR] OpenProcess(%d) failed!!! [%d]\n",
dwPID, GetLastError());
return FALSE;
}

pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize,
MEM_COMMIT, PAGE_READWRITE);

WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllName,
dwBufSize, NULL);

pThreadProc = GetProcAddress(GetModuleHandle(L"kernel32.dll"),
"LoadLibraryA");

if( !MyCreateRemoteThread(hProcess, (LPTHREAD_START_ROUTINE)pThreadProc, pRemoteBuf) )
{
printf("[ERROR] MyCreateRemoteThread() failed!!!\n");
return FALSE;
}

VirtualFreeEx(hProcess, pRemoteBuf, 0, MEM_RELEASE);

CloseHandle(hProcess);

return TRUE;
}

int main(int argc, char *argv[])
{
// adjust privilege
SetPrivilege(SE_DEBUG_NAME, TRUE);

// InjectDll.exe <PID> <dll_path>
if( argc != 3 )
{
printf("usage : %s <PID> <dll_path>\n", argv[0]);
return 1;
}

if( !InjectDll((DWORD)atoi(argv[1]), argv[2]) )
{
printf("InjectDll() failed!!!\n");
return 1;
}

printf("InjectDll() succeeded!!!\n");

return 0;
}

InjectDll()函数中变动的部分是,函数内部并未直接调用kernel32!CreateRemoteThread(),而是调用了名为MyCreateRemoteThread()的用户函数。在MyCreateRemoteThread()函数内部先获取OS的版本,若为Vista以上版本,则调用ntdll!NtCreateThreadEx()函数;若为XP以下版本,则调用kernel32!CreateRemoteThread()。整个代码比较简单,很容易理解。

用户模式下,ntdll.dll 库中的 NtCreateThreadEx()与 ZwCreateThreadEx() API 其实是同一函数(二者起始地址是一样的)。而内核模式(ntoskml.exe)中,二者是不同的。请记住,用户模式下NtXXX()与ZwXXX()是一样的。

43.5.2注入练习

首先选择一个合适的服务进程(会话0)进行DLL文件注入练习,如图43-21所示。

然后运行InjectDll_new.exe命令,输入相关参数进行注入操作,如图43-22所示。

最后使用Process Explorer工具查看svchost.exe (PID: 2096),可以看到dummy.dll文件成功注入,如图43-23所示。

这样就可以在Windows Kernel 6 (Vista、7、8等)中向服务进程(会话0)顺利注入指定DLL文件。

ntdll!NtCreateThreadEx() API是一个尚未公开的API,所以微软不建议直接调用它,否则将导致系统稳定性失去保障。就我的测试结果来看,调用它之后工作非常正常,但微软可能在以后某个时候修改它。在某个项目中使用该方法时,一定要注意这一点。


第43章 内核6中的DLL注入
https://m0ck1ng-b1rd.github.io/1999/04/03/逆向工程核心原理/第43章 内核6中的DLL注入/
作者
何语灵
发布于
1999年4月3日
许可协议