从文章https://www.joachim-bauch.de/tutorials/loading-a-dll-from-memory/学习并记录
虽然是2014年的文章,但是很多技术都很值得学习
原理:
先将DLL写入临时文件,然后导入。当程序终止时,临时文件将被删除。
windows可执行文件-PE格式
DOS header DOS stub |
---|
PE header |
Section header |
Section 1 |
Section 2 |
… |
Section n |
这些结构都可以在winnt.h
中找到
DOS header / stub header
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 header
PE 标头包含有关可执行文件内不同部分的信息,这些部分用于存储代码和数据或定义从其他库的导入或此库提供的导出
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
FileHeader描述了文件的_物理_格式,即内容、符号信息等:
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;
OptionalHeader包含有关库的_逻辑_格式的信息,包括所需的操作系统版本、内存要求和入口点:
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
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;
DataDirectory包含 16 个 ( IMAGE_NUMBEROF_DIRECTORY_ENTRIES ) 条目,定义库的逻辑组件:
Index 指数 | Description 描述 |
---|---|
0 | Exported functions 导出函数 |
1 | Imported functions 导入函数 |
2 | Resources 资源 |
3 | Exception informations 异常信息 |
4 | Security informations 安全信息 |
5 | Base relocation table 基地搬迁表 |
6 | Debug informations 调试信息 |
7 | Architecture specific data架构特定数据 |
8 | Global pointer 全局指针 |
9 | Thread local storage 线程本地存储 |
10 | Load configuration 负载配置 |
11 | Bound imports 绑定进口 |
12 | Import address table 导入地址表 |
13 | Delay load imports 延迟加载导入 |
14 | COM runtime descriptor COM运行时描述符 |
为了导入 DLL,我们只需要描述导入的条目和基本重定位表。为了提供对导出函数的访问,需要导出条目。
Section header
节头存储在PE头中的OptionalHeader结构之后。微软提供了宏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;
节可以包含代码、数据、重定位信息、资源、导出或导入定义等。
Load Library
为了模拟PE加载器,我们必须首先了解将文件加载到内存并准备结构以便从其他程序调用它们需要哪些步骤
当发出 API 调用LoadLibrary时,Windows 基本上执行以下任务:
-
打开给定的文件并检查 DOS 和 PE 标头 -
尝试在PEHeader.OptionalHeader.ImageBase位置分配PEHeader.OptionalHeader.SizeOfImage字节的内存块 -
解析节标题并将节复制到其地址。每个节的目标地址(相对于分配的内存块的基址)存储在IMAGE_SECTION_HEADER结构的VirtualAddress属性中 -
如果分配的内存块与ImageBase不同,则必须调整代码和/或数据部分中的各种引用。这称为基地址重定位 -
库所需的导入必须通过加载相应的库来解决 -
必须根据部分的特性来保护不同部分的存储区域。某些部分被标记为_可丢弃_,因此此时可以安全地释放。这些部分通常包含仅在导入期间需要的临时数据,例如基本重定位的信息 -
现在库已完全加载。必须通过使用标志DLL_PROCESS_ATTACH调用入口点来通知它
关于上面步骤的详细解释:
分配内存
库所需的所有内存都必须使用VirtualAlloc保留/分配,因为 Windows 提供了保护这些内存块的功能。这是限制对内存的访问所必需的,例如阻止对代码或常量数据的写访问
OptionalHeader 结构定义了库所需的内存块的大小。如果可能的话,必须保留在ImageBase指定的地址处:
memory = VirtualAlloc((LPVOID)(PEHeader->OptionalHeader.ImageBase),
PEHeader->OptionalHeader.SizeOfImage,
MEM_RESERVE,
PAGE_READWRITE);
如果保留的内存与ImageBase中给出的地址不同,则必须执行如下所述的基址重定位
复制节sections
一旦保留了内存,就可以将文件内容复制到系统中。必须评估节头才能确定文件中的位置和内存中的目标区域
在复制数据之前,必须提交内存块:
dest = VirtualAlloc(baseAddress + section->VirtualAddress,
section->SizeOfRawData,
MEM_COMMIT,
PAGE_READWRITE);
文件中没有数据的部分(例如所使用变量的数据部分)的SizeOfRawData为0 ,因此您可以使用OptionalHeader 的SizeOfInitializedData或SizeOfUninitializedData 。必须根据该部分的特性中设置的位标志IMAGE_SCN_CNT_INITIALIZED_DATA和IMAGE_SCN_CNT_UNINITIALIZED_DATA选择哪一个
基址重定位
库的代码/数据部分中的所有内存地址都是相对于OptionalHeader中ImageBase定义的地址存储的。如果无法将库导入到此内存地址,则必须调整引用=>_重新定位_。文件格式通过在基本重定位表中存储有关所有这些引用的信息来帮助实现此目的,该表可以在OptionalHeader 中的DataDirectory 的目录条目5 中找到
这个表由一系列这样的结构组成
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION;
它包含(SizeOfBlock – IMAGE_SIZEOF_BASE_RELOCATION) / 2 个条目,每个条目 16 位。高 4 位定义重定位类型,低 12 位定义相对于VirtualAddress 的偏移量
DLL 中唯一使用的类型是
IMAGE_REL_BASED_ABSOLUTE
不进行搬迁操作。用于填充
IMAGE_REL_BASED_HIGHLOW
将ImageBase和分配的内存块之间的增量添加到在偏移处找到的 32 位。
导入相应库
OptionalHeader 中 DataDirectory 的目录条目 1 指定要从中导入符号的库列表。该列表中的每个条目定义如下:
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;
名称条目描述了库名称(例如KERNEL32.DLL )的 NULL 终止字符串的偏移量。 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);
之后我们可以像使用任何普通库一样使用导出的函数
导出函数
如果要访问库导出的函数,则需要找到符号的入口点,即要调用的函数的名称
OptionalHeader 中 DataDirectory 的目录项 0 包含有关导出函数的信息。它的定义如下:
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定义的数组,直到找到所需的名称
现在,您可以通过计算AddressOfFunctions数组的第 n 个元素来使用序数来读取地址
释放库
要释放自定义加载的库,请执行以下步骤
调用入口点以通知库已分离:
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);
知识星球
原文始发于微信公众号(CatalyzeSec):内存加载DLL学习
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论