本文描述了一种动态链接库(DLL)如何从内存中加载,而不首先将其存储在硬盘上的技术。

概述

默认的Windows API函数加载外部库到程序(LoadLibrary,LoadLibraryEx)与文件系统上的文件进行通信工作,因此不可能从内存中加载dll。但有时,我们需要确切的功能,也就是不让文件落地磁盘,从而减小被杀毒软件检测风险。所以有了内存动态加载文件的思路出现,比较典型的就是反射DLL技术,本地内存DLL反射等技术,本文要讲的是内存加载DLL技术。

0x00 简介

内存加载DLL技术和反射DLL技术的利用方式相近,这两种技术都是从内存中加载dll,因为文件不落地磁盘,所以杀毒软件检测有一定难度。

在大多数文章中,我们看到的介绍都是比较简单的,直接给出代码,其原理解释也是比较少的,所以我们要自己定制功能就需要大量的修改代码。目前来讲,内存加载DLL技术比反射DLL技术应用少,主要是反射DLL需要的loader大小和代码量比内存加载DLL的少; 反射DLL主要加载代码在DLL中,而内存加载DLL的主要代码在loader中,所以造成了内存加载DLL的应用不如反射DLL的广,但是这样并不影响我们过杀软的检测和其功能的隐蔽性。

0x01 技术实现

内存加载DLL其实并不算神秘,技术也是十几年前的,但是关注这方面的人很少。内存加载DLL可以说就是在本地重新写了一个PE装载器,把DLL加载进内存读取运行,仅此而已,但是它过查杀效果是很好的。

一般地,导入加载DLL,我们需要重定位和修复DLL的导入导出表,找到我们需要调用的函数地址,加载它。

详细的加载步骤如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 打开给定的文件并检查DOS和PE头文件。

2. 尝试分配一个字节的内存块在peheader.optionalheader.imagebase位置上。

3. 解析section headers 和复制sections到它们的地址。每一段的section分配到内存块的相对地址,存储在image_section_header结构的virtualaddress属性中。

4. 如果分配的内存块不同于ImageBase的基址。代码或数据段中的各种引用必须进行调整。这就是地址重定位。

5. 必须通过加载相应的库来解决库所需的导入。

6. 不同部分的内存区域必须根据节的特性进行保护。有些部分被标记为可以丢弃,因此可以安全地释放。在这一点上,这些部分通常包含在导入期间需要的临时数据,用于地址重定位的信息。

7. 现在,这个库已完全加载。它必须被通知通过dll_process_attach调用的入口点。

首先,我们要把完整的DLL数据加载进内存,然后调用函数 MemoryLoadLibrary()来进行重定位操作

1
2
3
4
HMEMORYMODULE MemoryLoadLibrary(const void *data)
{
return MemoryLoadLibraryEx(data, _LoadLibrary, _GetProcAddress, _FreeLibrary, NULL);
}

MemoryLoadLibraryEx()函数返回加载后的数据和导出函数地址等。

现在,我们已经找到了相关DLL的数据,我们在得到DLL的句柄后,利用函数MemoryGetProcAddress()得到DLL的导出函数,进而把程序控制权交给DLL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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的大概就是这个样子的。

  1. 首先,我们读取DLL数据到内存中
  2. 利用第三方函数进行DLL的重定位操作
  3. 拿到内存中DLL的相关数据,比如:当前内存中DLL的地址,导出函数,DLL资源等
  4. _GetProcAddress()拿到DLL导出函数地址
  5. 直接调用导出函数
  6. 释放DLL资源

详细的代码请参考地址:http://github.com/fancycode/memorymodule

详细的原理请参考地址:https://www.joachim-bauch.de/tutorials/loading-a-dll-from-memory/

也可以看我翻译的这篇:http://imosin.com/2017/11/21/MemoryLoadDll/

0x02 加载测试

编写一个DLL,没错,就这么简单

1
2
3
4
5
6
7
extern "C" {

SAMPLEDLL_API int addNumbers(int a, int b)
{
return a + b;
}
}

把DLL加载进内存并用DLLloader调用addNumbers函数

执行结果

p4

0x03 实际应用

我们可以用此加载方法做一个云端木马。
我们在VPS上搭建一个Web服务器,在上面用一个页面放一个DLL文件的数据,然后读取数据并加载进内存。

我们也可以把它用于DLL劫持利用,分解型后门的组合式调用等等。

当然,我们也可以做一个RAT工具,内存加载DLL的具体应用如下
我们利用工具生成内存加载Loader程序

p1

另一端进行监听,当Loader连接过来时,我们发送需要调用的DLL程序过去,进而控制被攻击机器

p2

相关模块已经集成在了Purelove框架中

下载地址:https://github.com/hucmosin/purelove