本文描述了一种动态链接库(DLL)如何从内存中加载,而不首先将其存储在硬盘上的技术。
默认的Windows API函数加载外部库到程序(LoadLibrary,LoadLibraryEx)与文件系统上的文件进行通信工作,因此不可能从内存中加载dll。但有时,我们需要确切的功能,也就是不让文件落地磁盘,从而减小被杀毒软件检测风险。所以有了内存动态加载文件的思路出现,比较典型的就是反射DLL技术,本地内存DLL反射等技术,本文要讲的是内存加载DLL技术。
内存加载DLL技术和反射DLL技术的利用方式相近,这两种技术都是从内存中加载dll,因为文件不落地磁盘,所以杀毒软件检测有一定难度。
在大多数文章中,我们看到的介绍都是比较简单的,直接给出代码,其原理解释也是比较少的,所以我们要自己定制功能就需要大量的修改代码。目前来讲,内存加载DLL技术比反射DLL技术应用少,主要是反射DLL需要的loader大小和代码量比内存加载DLL的少; 反射DLL主要加载代码在DLL中,而内存加载DLL的主要代码在loader中,所以造成了内存加载DLL的应用不如反射DLL的广,但是这样并不影响我们过杀软的检测和其功能的隐蔽性。
内存加载DLL其实并不算神秘,也不是什么新技术,技术也是十几年前的,但是关注这方面的人很少。内存加载DLL可以说就是在本地重新写了一个PE装载器,把DLL加载进内存读取运行,仅此而已,但是它过查杀效果是很好的。
一般地,导入加载DLL,我们需要重定位和修复DLL的导入导出表,找到我们需要调用的函数地址,加载它。
详细的加载步骤如下:
打开给定的文件并检查DOS和PE头文件。
尝试分配一个字节的内存块在peheader.optionalheader.imagebase位置上。
解析section headers 和复制sections到它们的地址。每一段的section分配到内存块的相对地址,存储在image_section_header结构的virtualaddress属性中。
如果分配的内存块不同于ImageBase的基址。代码或数据段中的各种引用必须进行调整。这就是地址重定位。
必须通过加载相应的库来解决库所需的导入。
不同部分的内存区域必须根据节的特性进行保护。有些部分被标记为可以丢弃,因此可以安全地释放。在这一点上,这些部分通常包含在导入期间需要的临时数据,用于地址重定位的信息。
现在,这个库已完全加载。它必须被通知通过dll_process_attach调用的入口点。
首先,我们要把完整的DLL数据加载进内存,然后调用函数 MemoryLoadLibrary()来进行重定位操作
HMEMORYMODULE MemoryLoadLibrary(const void *data)
{
return MemoryLoadLibraryEx(data, _LoadLibrary, _GetProcAddress, _FreeLibrary, NULL);
}
MemoryLoadLibraryEx()函数返回加载后的数据和导出函数地址等。
现在,我们已经找到了相关DLL的数据,我们在得到DLL的句柄后,利用函数MemoryGetProcAddress()得到DLL的导出函数,进而把程序控制权交给DLL。
FARPROC MemoryGetProcAddress(HMEMORYMODULE module, LPCSTR name)
{
unsigned char *codeBase = ((PMEMORYMODULE)module)->codeBase;
int idx=-1;
DWORD i, *nameRef;
WORD *ordinal;
PIMAGE_EXPORT_DIRECTORY exports;
PIMAGE_DATA_DIRECTORY directory = GET_HEADER_DICTIONARY((PMEMORYMODULE)module, IMAGE_DIRECTORY_ENTRY_EXPORT);
if (directory->Size == 0) {
// no export table found
SetLastError(ERROR_PROC_NOT_FOUND);
return NULL;
}
exports = (PIMAGE_EXPORT_DIRECTORY) (codeBase + directory->VirtualAddress);
if (exports->NumberOfNames == 0 || exports->NumberOfFunctions == 0) {
// DLL doesn't export anything
SetLastError(ERROR_PROC_NOT_FOUND);
return NULL;
}
// search function name in list of exported names
nameRef = (DWORD *) (codeBase + exports->AddressOfNames);
ordinal = (WORD *) (codeBase + exports->AddressOfNameOrdinals);
for (i=0; i<exports->NumberOfNames; i++, nameRef++, ordinal++) {
if (_stricmp(name, (const char *) (codeBase + (*nameRef))) == 0) {
idx = *ordinal;
break;
}
}
if (idx == -1) {
// exported symbol not found
SetLastError(ERROR_PROC_NOT_FOUND);
return NULL;
}
if ((DWORD)idx > exports->NumberOfFunctions) {
// name <-> ordinal number don't match
SetLastError(ERROR_PROC_NOT_FOUND);
return NULL;
}
// AddressOfFunctions contains the RVAs to the "real" functions
return (FARPROC) (codeBase + (*(DWORD *) (codeBase + exports->AddressOfFunctions + (idx*4))));
}
在执行完DLL后,对资源进行释放MemoryFreeLibrary()。
void MemoryFreeLibrary(HMEMORYMODULE mod)
{
int i;
PMEMORYMODULE module = (PMEMORYMODULE)mod;
if (module != NULL) {
if (module->initialized != 0) {
// notify library about detaching from process
DllEntryProc DllEntry = (DllEntryProc) (module->codeBase + module->headers->OptionalHeader.AddressOfEntryPoint);
(*DllEntry)((HINSTANCE)module->codeBase, DLL_PROCESS_DETACH, 0);
module->initialized = 0;
}
if (module->modules != NULL) {
// free previously opened libraries
for (i=0; i<module->numModules; i++) {
if (module->modules[i] != NULL) {
module->freeLibrary(module->modules[i], module->userdata);
}
}
free(module->modules);
}
if (module->codeBase != NULL) {
// release memory of library
VirtualFree(module->codeBase, 0, MEM_RELEASE);
}
HeapFree(GetProcessHeap(), 0, module);
}
}
由于篇幅问题,不可能把代码全部贴出来讲解,内存加载DLL的大概就是这个样子的。
- 首先,我们读取DLL数据到内存中
- 利用第三方函数进行DLL的重定位操作
- 拿到内存中DLL的相关数据,比如:当前内存中DLL的地址,导出函数,DLL资源等
- _GetProcAddress()拿到DLL导出函数地址
- 直接调用导出函数
- 释放DLL资源
详细的代码请参考地址:http://github.com/fancycode/memorymodule
详细的原理请参考地址:https://www.joachim-bauch.de/tutorials/loading-a-dll-from-memory/
编写一个DLL,没错,就这么简单
extern "C" {
SAMPLEDLL_API int addNumbers(int a, int b)
{
return a + b;
}
}
把DLL加载进内存并用DLLloader调用addNumbers函数
执行结果
我们可以用此加载方法做一个云端木马。
我们在VPS上搭建一个Web服务器,在上面用一个页面放一个DLL文件的数据,然后读取数据并加载进内存。
我们也可以把它用于DLL劫持利用,分解型后门的组合式调用等等。
当然,我们也可以做一个RAT工具,内存加载DLL的具体应用如下
我们利用工具生成内存加载Loader程序
另一端进行监听,当Loader连接过来时,我们发送需要调用的DLL程序过去,进而控制被攻击机器
被攻击机器
过杀软效果
我们可以看到,在没有进行任何免杀过程中,只有一款杀软报毒,过杀软效果还是很好的,杀毒软件检测截图
http://r.virscan.org/report/a27eb8603abaedf2278d059b11aa8540