本文描述了一种动态链接库(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的导入导出表,找到我们需要调用的函数地址,加载它。

详细的加载步骤如下:

打开给定的文件并检查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的大概就是这个样子的。

  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/

0x02 加载测试

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

extern "C" {

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

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

执行结果

0x03 实际应用

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

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

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

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

被攻击机器

过杀软效果

我们可以看到,在没有进行任何免杀过程中,只有一款杀软报毒,过杀软效果还是很好的,杀毒软件检测截图

http://r.virscan.org/report/a27eb8603abaedf2278d059b11aa8540

源链接

Hacking more

...