0%
毅种循环

模块 28 - 进程注入 - DLL 注入

恶意软件开发课程 - 进程注入 - DLL 注入


模块 28 - 进程注入 - DLL 注入#

[!IMPORTANT] 本知识库声明

本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示实践心得

  1. 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
  2. 版权归属:原始知识产权归原作者/官方所有。
  3. 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程
  4. 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。

进程注入 - DLL 注入#

简介#

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

💡 初学者提示:本地注入 vs 远程注入

  • 本地注入:在当前进程中加载 DLL 或执行 Shellcode。你自己控制这个进程,权限通常不是问题。
  • 远程注入:强制另一个正在运行的进程(如 notepad.exe, explorer.exe)加载 DLL 或执行代码。
    • 隐蔽性更高:恶意代码隐藏在合法进程内部。
    • 权限要求更高:你需要有足够的权限打开目标进程(OpenProcess)。
    • 更复杂:涉及跨进程内存操作。

枚举进程#

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

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

CreateToolhelp32Snapshot#

代码片段首先使用具有 TH32CS_SNAPPROCESS 标志作为其第一个参数的 CreateToolhelp32Snapshot,它会在函数执行的那一刻获取系统上所有正在运行的进程的快照。

// 获取当前正在运行的进程的快照
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
c

PROCESSENTRY32 结构体#

拍摄快照后,使用 Process32First 获取快照中第一个进程的信息。对于快照中的所有其余进程,使用 Process32Next

微软的文档指出,Process32FirstProcess32Next 都需要传入一个 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;
c

Process32FirstProcess32Next 填充结构体后,可以使用点运算符从结构体中提取数据。例如,要提取 PID,请使用 PROCESSENTRY32.th32ProcessID

Process32First & Process32Next#

如前所述,Process32First 用于获取第一个进程的信息,而 Process32Next 用于使用 do-while 循环获取快照中所有其余进程的信息。正在搜索的进程名称 szProcessName 将与从填充的结构体 Proc.szExeFile 中提取的当前循环迭代中的进程名称进行比较。如果有匹配项,则保存进程 ID 并打开该进程的句柄。

📚 知识扩展:OpenProcess 权限

这里使用了 PROCESS_ALL_ACCESS,这意味着请求对目标进程的所有可能权限。

  • 优点:简单,确保后续所有操作(写内存、创建线程)都能成功。
  • 缺点:非常可疑,且容易失败(如果目标是受保护进程或高权限进程)。
  • 最佳实践:只请求所需的最低权限。对于 DLL 注入,通常只需:
    • PROCESS_VM_OPERATION (操作内存)
    • PROCESS_VM_WRITE (写内存)
    • PROCESS_CREATE_THREAD (创建线程)
    • PROCESS_QUERY_INFORMATION (查询信息)

进程枚举 - 代码#

微软的示例#

可以在这里查看另一个进程枚举示例。

区分大小写的进程名称#

上面的代码片段包含一个被忽略的缺陷,可能导致结果不准确。使用了 wcscmp 函数比较进程名称,但未考虑到大小写敏感性,这意味着 Process1.exeprocess1.exe 将被视为两个不同的进程。

下面的代码片段通过将 Proc.szExeFile 成员中的值转换为小写字符串,然后将其与 szProcessName 进行比较来修复此问题。因此,szProcessName 必须始终作为小写字符串传入。

DLL 注入#

已成功检索到目标进程的进程句柄。下一步是将 DLL 注入目标进程,这将需要使用几个之前使用过的 Windows API 和一些新的 API。

代码演练#

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

  1. 进程句柄 - 这是目标进程的 HANDLE,DLL 将被注入其中。
  2. DLL 名称 - 将被注入目标进程的 DLL 的完整路径。

查找 LoadLibraryW 地址#

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

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

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

📚 知识扩展:为什么 LoadLibraryW 地址在不同进程中相同?

在 Windows 中,核心系统 DLL(如 kernel32.dll, ntdll.dll)通常被加载到所有进程的相同虚拟地址

  • 这是为了优化系统内存和性能。
  • 这给我们带来了极大的便利:我们在当前进程中获取的 LoadLibraryW 地址,直接在远程进程中也是有效的!
  • ASLR(地址空间布局随机化)虽然会随机化 DLL 加载地址,但在也就是启动后,所有进程共享相同的系统 DLL 内存映像(Copy-on-Write),因此基址通常对齐一致(直至重启)。

分配内存#

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

// 在远程进程 hProcess 内分配大小为 dwSizeToWrite(即 dll 名称的大小)的内存。
// 内存保护为可读可写
pAddress = VirtualAllocEx(hProcess, NULL, dwSizeToWrite, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
c

写入分配的内存#

在远程进程中成功分配内存后,可以使用 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' 变量的指针
);
c

根据上面显示的 WriteProcessMemory 参数,将按如下方式调用它,将缓冲区(DllName)写入分配的地址(pAddress),该地址由之前调用的 VirtualAllocEx 函数返回。

// 正在写入的数据是 DLL 名称 'DllName',大小为 'dwSizeToWrite'
SIZE_T lpNumberOfBytesWritten = NULL;
WriteProcessMemory(hProcess, pAddress, DllName, dwSizeToWrite, &lpNumberOfBytesWritten)
c

通过新线程执行#

成功将 DLL 路径写入分配的缓冲区后,将使用 CreateRemoteThread 在远程进程中创建一个新线程。这就是 LoadLibraryW 地址变得必要的地方。pLoadLibraryW 被传递作为线程的起始地址,然后 pAddress(包含 DLL 名称)被传递作为 LoadLibraryW 调用的参数。这是通过将 pAddress 作为 CreateRemoteThreadlpParameter 参数传递来完成的。

CreateRemoteThread 的参数与前面解释的 CreateThread WinAPI 函数相同,除了额外的 HANDLE hProcess 参数,它表示要在其中创建线程的进程句柄。

// 线程入口将是 'pLoadLibraryW',即 LoadLibraryW 的地址
// DLL 的名称 pAddress 作为参数传递给 LoadLibrary
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, pLoadLibraryW, pAddress, NULL, NULL);
c

💡 初学者提示:CreateRemoteThread 魔法

这里的魔法在于利用了 LoadLibraryW 的函数签名与线程函数签名兼容:

  • 线程函数DWORD WINAPI ThreadProc(LPVOID lpParameter);
  • LoadLibraryWHMODULE LoadLibraryW(LPCWSTR lpLibFileName);

两者都接受一个指针参数并返回一个值。 我们告诉远程进程:“嘿,启动一个新线程,但不要执行普通函数,而是执行 LoadLibraryW,参数就是我刚写入你内存里的那个 DLL 路径。” 于是,远程进程就乖乖地加载了我们的 DLL!

DLL 注入 - 代码片段#

调试#

在本节中,使用 xdbg 调试器调试实现,以进一步了解底层发生的情况。

首先,运行 RemoteDllInjection.exe 并传递两个参数:目标进程和要注入到目标进程内的完整 DLL 路径。在本演示中,正在注入 notepad.exe

图片
图片

进程枚举成功工作。使用 Process Hacker 验证 Notepad 的 PID 确实是 20932

图片
图片

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

图片
图片

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

图片
图片

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

图片
图片

使用 Process Hacker 的模块选项卡验证 DLL 是否已成功注入。

图片
图片

前往 Process Hacker 中的线程选项卡,注意运行 LoadLibraryW 作为其入口函数的线程。

图片
图片


🎯 总结#

在本模块中,我们学习了:

  1. 进程枚举:使用 CreateToolhelp32Snapshot 查找目标进程 PID。
  2. 远程内存操作:使用 VirtualAllocExWriteProcessMemory 在其他进程中操作内存。
  3. 经典的 DLL 注入技术CreateRemoteThread + LoadLibraryW 的组合拳。
  4. 调试技巧:如何验证远程注入是否成功。

💡 关键要点

  • 远程注入需要 OpenProcess 及其正确的权限。
  • LoadLibrary 地址在所有进程中通常是相同的。
  • 这种经典注入技术非常喧闹,容易被 EDR 检测到(CreateRemoteThread 是重点监控对象)。

📚 下一步学习

下一个模块将介绍 Shellcode 注入,它的原理类似,但我们不再注入 DLL 文件,而是直接注入并执行 shellcode!