从内存中加载的DLL
本教程介绍如何从内存中加载一个动态链接库(DLL)。
概括
默认的Windows API函数加载到程序的外部库(调用LoadLibrary,LoadLibraryEx)只适用于文件系统上的文件。因此,它不可能从内存中加载DLL。但有时,你需要的正是这样的功能(例如,你不希望发布多个的文件或删除困难)。这个问题的常见解决方法是写的DLL到一个临时文件,并从那里调用。当程序终止,临时文件被删除。
在本教程中,我将首先描述,DLL文件的结构,并提出了一些代码,可以完全从内存中加载DLL - 不存储在磁盘上。
Windows可执行文件 - PE格式
大多数Windows二进制文件,它可以包含可执行代码(EXE,DLL,SYS)共享一个通用的文件格式,由以下几部分组成:
DOS header
DOS stub |
PE header |
Section header |
Section 1 |
Section 2 |
. . . |
Section n |
文件winnt.h中可以发现下面给出的所有结构。
DOS头/存根(stub)
DOS头只用于向后兼容。之前的DOS存根,通常只显示一条错误消息有关的文件不能在DOS模式下运行。
微软定义的DOS头如下:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
PE头
PE头中包含的信息。可执行文件的不同部分,用于存储代码和数据定义这个库提供的其他库或出口的进口。
它的定义如下:
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
文件头描述的物理格式的文件,即内容,关于符号的信息,等:
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
可选头包含的逻辑格式的库的信息,包括所需的操作系统版本,内存需求和切入口:
的typedef的结构_IMAGE_OPTIONAL_HEADER { / / / /标准领域。 / / WORD魔术; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; / / / / NT额外的字段。 / / DWORD基址; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD校验和; WORD子系统; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY使用DataDirectory [IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32,* PIMAGE_OPTIONAL_HEADER32;
数据目录包含16个的(IMAGE_NUMBEROF_DIRECTORY_ENTRIES)项定义的逻辑组件的库:
指数 | 描述 |
---|---|
0 | 导出的函数 |
1 | 进口功能 |
2 | 资源 |
3 | 异常信息 |
4 | 安全信息 |
5 | 基地搬迁表 |
6 | 调试信息 |
7 | 架构体系数据 |
8 | 全局指针 |
9 | 线程局部存储 |
10 | 加载配置 |
11 | 绑定进口 |
12 | 导入地址表 |
13 | 延迟加载进口 |
14 | COM运行时描述符 |
对于导入的DLL,我们只需要导入项描述和基地重定位表项。为了提供访问导出的函数,导出项是必需的
。
节头
节头后存储在PE头的可选头结构。微软提供宏观IMAGE_FIRST_SECTION, PE头的基础上得 到的起始地址。
其实,节头信息的文件中的每个部分的列表:
typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
一节可以包含代码,数据,重定位信息,资源,出口或入口的定义,等等。
加载库。
为了模拟PE加载器,我们必须先了解哪些步骤,都需要将文件加载到内存中,并准备结构,使他们能够从其他程序调用。
当我们调用LoadLibrary的,Windows主要做了以下任务:
- 打开指定的文件,并检查DOS和PE头。
- 尝试的PE头.可选头.映像大小字节分配一个内存块的位置PE头.可选头.映像基址。
- 解析部分的页眉和部分复制到自己的地址。VirtualAddress的属性的IMAGE_SECTION_HEADER结构的每个部分中,相对于分配的内存块的基础上,被存储在目的地地址。
- 如果分配的内存块不同于基址,各种参考文献中的代码和/或数据段必须被调整。这就是所谓的基本搬迁。
- 需要进口的库加载相应的库,必须解决。
- 必须被保护的存储器区域的不同的部分,根据部分的特性。的某些部分被标记为“ 舍弃,因此可以安全地释放在这一点上。这些部分通常包含临时数据,只需要在导入过程中,类似的信息为基地的搬迁。
- 现在的图书馆是完全加载。必须通过调用入口点使用标志DLL_PROCESS_ATTACH通知。
在下面的段落中,每个步骤进行说明。
分配内存
库所需的所有内存,必须保留/分配使用VirtualAlloc的,因为Windows提供的功能来保护这些内存块。这是必需的,如阻塞写的代码或常量数据的访问限制对内存的访问。
可选头结构定义为库所需的存储器块的大小。它必须被保留的地址所指定的映像基址如果可能的话:
memory = VirtualAlloc((LPVOID)(PEHeader->OptionalHeader.ImageBase), PEHeader->OptionalHeader.SizeOfImage, MEM_RESERVE, PAGE_READWRITE);
如果保留的内存的地址在基址不同(如下文所述),基地重定位必须做的。
复制部分
一旦存储器已经被保留,该文件的内容可以被复制到系统中。节标头必须得到评估,以确定在内存中的文件和目标区域的位置。
在复制数据的内存块必须得到承诺:
dest = VirtualAlloc(baseAddress + section->VirtualAddress, section->SizeOfRawData, MEM_COMMIT, PAGE_READWRITE);
没有文件中的数据(如所使用的变量的数据部分)部分的SizeOfRawData为0,所以您可以使用SizeOfInitializedData或可选头的SizeOfUninitializedData。选择哪一个取决于上的的位标志IMAGE_SCN_CNT_INITIALIZED_DATA和IMAGE_SCN_CNT_UNINITIALIZED_DATA节“的特点,可以被设置在。
基地重定位
所有的内存地址被存储在库中的代码/数据段的相对在可选头定义的地址由基址。如果该库不能被导入到这个内存地址,都必须进行调整=> 重新定位。帮助基地搬迁表,可以发现目录中的5项的数据目录在可选头所有这些引用在存储信息的文件格式。
此表包含此结构的一系列
typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; } IMAGE_BASE_RELOCATION;
它包含(SizeOfBlock - IMAGE_SIZEOF_BASE_RELOCATION)/ 2,每个项目的16位。高4位的低12位定义的重定位类型,定义偏移相对的虚拟地址。
唯一的类型,似乎是在DLL文件
- IMAGE_REL_BASED_ABSOLUTE
- 无操作重定位。用于填充。
- IMAGE_REL_BASED_HIGHLOW
- 在基址和分配的内存块添加增量的32位地址的偏移。
解决入口
1的DataDirectory在OptionalHeader的指定目录项的列表导入符号库。在该列表中的每个条目定义如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) }; DWORD TimeDateStamp; // 0 if not bound, // -1 if bound, and real date\time stamp // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) // O.W. date/time stamp of DLL bound to (Old BIND) DWORD ForwarderChain; // -1 if no forwarders DWORD Name; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) } IMAGE_IMPORT_DESCRIPTOR;
名称条目描述的偏移量,以NULL结尾的字符串库的名称(如KERNEL32.DLL)。将OriginalFirstThunk入口点从外部导入库的函数名的列表。FirstThunk项保存点得到填补进口的符号指针的地址的列表。
当我们解决了进口,我们在并行访问这两个表,导入第一个列表中定义的函数的名称和存储的指针符号在第二个列表:
nameRef = (DWORD *)(baseAddress + importDesc->OriginalFirstThunk); symbolRef = (DWORD *)(baseAddress + importDesc->FirstThunk); for (; *nameRef; nameRef++, symbolRef++) { PIMAGE_IMPORT_BY_NAME thunkData = (PIMAGE_IMPORT_BY_NAME)(codeBase + *nameRef); *symbolRef = (DWORD)GetProcAddress(handle, (LPCSTR)&thunkData->Name); if (*funcRef == 0) { handleImportError(); return; } }
保护内存
每一部分都有它的特点进入指定的权限标志。这些标记可以是一个或它们的组合
- IMAGE_SCN_MEM_EXECUTE
- 本节包含的数据,可以执行。
- IMAGE_SCN_MEM_READ
- 本节包含的数据是可读的。
- IMAGE_SCN_MEM_WRITE
- 本节包含的数据是可写的。
这些标志必须被映射到保护标志
- PAGE_NOACCESS
- PAGE_WRITECOPY
- PAGE_READONLY
- PAGE_READWRITE
- PAGE_EXECUTE
- PAGE_EXECUTE_WRITECOPY
- PAGE_EXECUTE_READ
- PAGE_EXECUTE_READWRITE
现在,该函数的VirtualProtect可以被用于限制对存储器的访问。如果程序试图访问它在未经授权的方式,将会抛出一个异常的Windows。
除了上述的部分标志,可以添加以下:
- IMAGE_SCN_MEM_DISCARDABLE
- 本节中的数据可以被释放后的进口。这通常是指定的重定位数据。
- IMAGE_SCN_MEM_NOT_CACHED
- 本节中的数据不能由Windows缓存。上面的保护标志位标志PAGE_NOCACHE的。
通知库
做的最后一件事是调用的DLL入口点(定义为AddressOfEntryPoint),通知与库连接到某个进程。
的入口点处的功能被定义为
typedef BOOL (WINAPI *DllEntryProc)(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved);
所以最后我们要执行的代码是
DllEntryProc entry = (DllEntryProc)(baseAddress + PEHeader->OptionalHeader.AddressOfEntryPoint); (*entry)((HINSTANCE)baseAddress, DLL_PROCESS_ATTACH, 0);
之后,我们可以使用任何正常的库导出的函数。
导出的函数
如果你想访问的库导出的函数,你需要找到切入点,以一个符号,即要调用的函数的名称。
0的DataDirectory在OptionalHeader的包含目录项有关导出的函数的信息。它的定义如下:
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; // RVA from base of image DWORD AddressOfNames; // RVA from base of image DWORD AddressOfNameOrdinals; // RVA from base of image } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
首先要做的事,是名映射的序号导出的符号的功能。所以,我走在定义的阵列的AddressOfNames和AddressOfNameOrdinals平行,直到你找到所需的名称。
现在你可以用序数读通过评估的第n个元素的AddressOfFunctions数组的地址。
释放库
要释放自定义加载的库,请执行下列步骤
- 调用入口点通知被分离的库:
DllEntryProc entry = (DllEntryProc)(baseAddress + PEHeader->OptionalHeader.AddressOfEntryPoint); (*entry)((HINSTANCE)baseAddress, DLL_PROCESS_ATTACH, 0);
- 免费使用的外部库,以解决进口。
- 释放分配的内存。
MemoryModule
MemoryModule是C库,可用于从内存中加载的DLL。
该接口是非常类似的标准方法加载的库:
typedef void *HMEMORYMODULE; HMEMORYMODULE MemoryLoadLibrary(const void *); FARPROC MemoryGetProcAddress(HMEMORYMODULE, const char *); void MemoryFreeLibrary(HMEMORYMODULE);
已知问题
- 所有的内存,并不受部分标志是得到承诺PAGE_READWRITE。我不知道这是否是正确的。
许可证
自从版本0.0.2中,MemoryModule库下发布的Mozilla公共许可证(MPL)。版本0.0.1已经发布的Unter较宽松通用公共许可证(LGPL)。
它提供的是不提供任何担保。您就可以使用您自己的风险。
版权
MemoryModule库和本教程是由约阿希姆·鲍赫版权所有(c)2004-2011。