进程注入技术是网络安全领域中攻击者的重要工具,它们可以让威胁行为者在合法进程中运行恶意代码,从而避开安全检测,或者通过在远程进程中设置钩子来操控其行为。在 Windows 系统中,这一主题已经被广泛研究并得到了深入理解。然而,在 Linux 系统中,尽管已有一些优秀的研究资源,但对于进程注入技术的认识仍显不足,尤其是与 Windows 系统相比。
本文将聚焦于“真正的进程注入”技术,即针对实时运行进程的注入方法,而非涉及修改磁盘上的二进制文件、利用特定环境变量或借助进程加载进程的方式。
文章中将探讨 Linux 操作系统中支持进程注入的核心功能,以及基于这些功能构建的各种注入技术,包括先前已描述的方法和新发现的变体。此外,我们还将提供检测和缓解这些技术的策略,以帮助防御者应对潜在的威胁。
在 Windows 和 Linux 之间,进程注入技术的实现方式有着显著差异。Windows 提供了多种接口和 API,使得攻击者能够轻松与远程进程交互并实现复杂的注入操作。而在 Linux 中,系统调用的种类相对有限,远程内存分配、内存保护修改以及线程创建等功能均无直接支持。正因如此,Linux 进程注入的流程更倾向于通过覆盖现有内存、执行代码并恢复原始状态的方式实现。本文将对这些差异进行详细分析,期望为读者提供深入的理解和实践指导。
在 Linux 中,与远程进程内存的交互仅限于三种主要方法:ptrace、procfs和process_vm_writev。以下各节对它们分别进行了简要介绍。
ptrace是用于调试远程进程的系统调用。启动进程能够检查和修改被调试进程的内存和寄存器。GDB 等调试器就是使用 ptrace 实现的,用于控制被调试进程。
ptrace 支持不同的操作,这些操作由ptrace请求代码指定— 一些值得注意的示例包括 PTRACE_ATTACH(附加到进程)、PTRACE_PEEKTEXT(从进程内存读取)和 PTRACE_GETREGS(检索进程寄存器)。
使用 ptrace 检索远程进程的寄存器的示例:
// Attach to the remote process
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
wait(NULL);
// Get registers state
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, pid, NULL, ®s);
procfs是一个特殊的伪文件系统,充当系统上运行进程的接口。它可以通过 /proc 目录访问。
每个进程都表示为一个目录,根据其 PID 命名。在此目录下,我们可以找到提供有关该进程的信息的文件。例如,cmdline文件包含进程命令行,environ文件包含进程环境变量,等等。
procfs 还为我们提供了与远程进程内存交互的能力。在每个进程目录中,我们都会找到mem文件,这是一个代表进程整个地址空间的特殊文件。访问给定偏移量的进程的 mem 文件相当于访问相同地址的进程内存。
在下图的示例中,我们使用 xxd 实用程序从进程 mem 文件中读取 100 个字节,从指定的偏移量开始。
如果我们使用 GDB 检查内存中的相同地址,我们会注意到内容是相同的
地图文件是另一个有趣的文件,可以在进程目录中找到。此文件包含有关进程地址空间中不同内存区域的信息,包括其地址范围和内存权限。
在接下来的部分中,我们将看到识别具有特定权限的内存区域的能力如何非常有用。
与远程进程内存交互的第三种方法是process_vm_writev系统调用。此系统调用允许将数据写入远程进程的地址空间。
process_vm_writev 接收指向本地缓冲区的指针,并将其内容复制到远程进程内的指定地址。下面显示了 process_vm_writev 的使用示例。
// Initialize local and remote iovec structs used to perform the syscall
struct iovec local[1];
struct iovec remote[1];
// Place our data in the local iovec
local[0].iov_base = data;
local[0].iov_len = data_len;
// Point the remote iovec to the address in the remote process
remote[0].iov_base = (void *)remote_address;
remote[0].iov_len = data_len;
// Write the local data to the remote address
process_vm_writev(pid, local, 1, remote, 1, 0);
现在我们了解了与其他进程交互的不同方法,让我们看看如何使用它们来执行代码注入。注入攻击的第一步是将我们的 shellcode 写入远程进程内存。正如我们所提到的,在 Linux 中没有直接的方法在远程进程中分配新内存。这意味着我们无法创建新的内存部分;我们必须利用目标进程的现有内存。
为了使代码能够运行,我们需要将其写入具有执行权限的内存区域。我们可以通过解析前面提到的 procfs 映射文件并识别具有执行 (x) 权限的内存区域来找到这样的区域。
我们可能会遇到两种类型的可执行区域:可写和不可写。以下部分将展示何时以及如何使用它们。
适用于:ptrace、procfs mem
理想情况下,我们希望确定一个具有写入和执行权限的内存区域,这将允许我们编写代码并执行它。实际上,大多数进程都不会有具有此类权限的区域,因为分配 WX 内存被认为是一种不好的做法。相反,我们通常会被限制为读取和执行权限。
有趣的是,事实证明,可以使用我们刚刚描述的两种方法(ptrace 和 procfs mem)来突破这一限制。这两种机制的实现方式都允许它们绕过内存权限并写入任何地址,甚至无需写入权限。
这意味着,无论写入权限如何,我们始终可以使用 ptrace 或 procfs mem 将我们的代码写入远程可执行内存区域。
要将我们的有效载荷写入远程进程,我们可以使用 POKETEXT 或 POKEDATA ptrace 请求 - 这些相同的请求允许将一个字的数据写入远程进程内存。通过反复调用它们,我们可以将整个有效载荷复制到目标进程内存中。
使用 ptrace POKETEXT 将我们的 payload 写入远程进程内存:
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
wait(NULL);
// write payload to remote address
for (size_t i = 0; i < payload_size; i += 8, payload++)
{
ptrace(PTRACE_POKETEXT, pid, address + i, *payload);
}
要使用 procfs 将有效载荷写入远程进程,我们只需将其写入正确偏移量的 mem 文件中。对 mem 文件所做的任何更改都会应用于进程内存。要执行这些操作,我们可以使用常规文件 API。
使用 procfs mem 文件将数据写入远程进程内存:
// Open the process mem file
FILE *file = fopen("/proc/<pid>/mem", "w");
// Set the file index to our required offset, representing the memory address
fseek(file, address, SEEK_SET);
// Write our payload to the mem file
fwrite(payload, sizeof(char), payload_size,