先来介绍几个重要的结构。Freeldr虽然是个引导程序,严格来讲不属于操作系统的一部分,但它也有一些类似Windows系统中的数据结构。
LOADER_PARAMETER_BLOCK这个可以看作Freeldr的PEB,结构也和PEB非常相似。
/include/reactos/arc/arc.h
- typedef struct _LOADER_PARAMETER_BLOCK
- {
- LIST_ENTRY LoadOrderListHead;
- LIST_ENTRY MemoryDescriptorListHead;
- LIST_ENTRY BootDriverListHead;
- ......
- } LOADER_PARAMETER_BLOCK, *PLOADER_PARAMETER_BLOCK;
这里我们只看前三个域。和PEB中一样,前三个元素是三个表头,Freeldr每加载一个模块就会生成一个LDR_DATA_TABLE_ENTRY结构,并将这个结构连入这三个链表。
LoadOrder是按照夹在顺序排序的,Memory按照内存位置排序,BootDriver按照分区号排序。在Freeldr中只使用了LoadOrderListHead。
LDR_DATA_TABLE_ENTRY的结构定义在include/ndk/Ldrtypes.h中
- typedef struct _LDR_DATA_TABLE_ENTRY
- {
- LIST_ENTRY InLoadOrderLinks; // 用来连入LoadOrderListHead等的表头
- LIST_ENTRY InMemoryOrderModuleList;
- LIST_ENTRY InInitializationOrderModuleList;
- PVOID DllBase; // 模块基地址
- PVOID EntryPoint; // 入口点
- ULONG SizeOfImage; // 内存镜像大小
- UNICODE_STRING FullDllName; // 文件全路径
- UNICODE_STRING BaseDllName; // 模块名, 不一定和全路径的文件名相同
- ULONG Flags; // 一些标志
- USHORT LoadCount; // 引用计数
- USHORT TlsIndex;
- union
- {
- LIST_ENTRY HashLinks;
- struct
- {
- PVOID SectionPointer;
- ULONG CheckSum; // 校验和
- };
- };
- union
- {
- ULONG TimeDateStamp;
- PVOID LoadedImports;
- };
- PVOID EntryPointActivationContext;
- PVOID PatchInformation;
- } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
我做过注释的是Freeldr实际用到的域。注意这里BaseDllName中记录的模块名不一定和模块的实际文件名相同。许多BaseDllName是硬编码的只具有参考意义。
上一节读的WinLdrLoadImage函数,只是把文件从硬盘中读出来按照PE格式对其,并没有涉及上面所说的结构。所以Freeldr中WinLdrLoadImage往往都是和WinLdrAllocateDataTableEntry函数配合使用的。WinLdrLoadImage负责加载镜像,WinLdrAllocateDataTableEntry负责生成、初始化对应的LDR_DATA_TABLE_ENTRY结构。
- BOOLEAN
- WinLdrAllocateDataTableEntry(IN OUT PLOADER_PARAMETER_BLOCK WinLdrBlock,
- IN PCCH BaseDllName,
- IN PCCH FullDllName,
- IN PVOID BasePA,
- OUT PLDR_DATA_TABLE_ENTRY *NewEntry)
- {
- PVOID BaseVA = PaToVa(BasePA);
- ......
- /* 申请LDR_DATA_TABLE_ENTRY结构 */
- DataTableEntry = (PLDR_DATA_TABLE_ENTRY)MmHeapAlloc(sizeof(LDR_DATA_TABLE_ENTRY));
- if (DataTableEntry == NULL)
- return FALSE;
- RtlZeroMemory(DataTableEntry, sizeof(LDR_DATA_TABLE_ENTRY));
- /* 获得NT头指针 */
- NtHeaders = RtlImageNtHeader(BasePA);
- /* 初始化DllBase、SizeOfImage、EntryPoint、CheckSum */
- DataTableEntry->DllBase = BaseVA;
- DataTableEntry->SizeOfImage = NtHeaders->OptionalHeader.SizeOfImage;
- DataTableEntry->EntryPoint = RVA(BaseVA, NtHeaders->OptionalHeader.AddressOfEntryPoint);
- DataTableEntry->SectionPointer = 0;
- DataTableEntry->CheckSum = NtHeaders->OptionalHeader.CheckSum;
- /* 把Ansi字符转换为UNICODE_STRING存入BaseDllName和FullDllName中。这个转换只是简单的逐字节复制,所以如果路径中有中文会出错的。 */
- Length = (USHORT)(strlen(BaseDllName) * sizeof(WCHAR));
- Buffer = (PWSTR)MmHeapAlloc(Length);
- if (Buffer == NULL)
- {
- MmHeapFree(DataTableEntry);
- return FALSE;
- }
- RtlZeroMemory(Buffer, Length);
- DataTableEntry->BaseDllName.Length = Length;
- DataTableEntry->BaseDllName.MaximumLength = Length;
- DataTableEntry->BaseDllName.Buffer = PaToVa(Buffer);
- while (*BaseDllName != 0)
- {
- *Buffer++ = *BaseDllName++;
- }
- Length = (USHORT)(strlen(FullDllName) * sizeof(WCHAR));
- Buffer = (PWSTR)MmHeapAlloc(Length);
- if (Buffer == NULL)
- {
- MmHeapFree(DataTableEntry);
- return FALSE;
- }
- RtlZeroMemory(Buffer, Length);
- DataTableEntry->FullDllName.Length = Length;
- DataTableEntry->FullDllName.MaximumLength = Length;
- DataTableEntry->FullDllName.Buffer = PaToVa(Buffer);
- while (*FullDllName != 0)
- {
- *Buffer++ = *FullDllName++;
- }
- /* 初始化Flag和LoadCount */
- DataTableEntry->Flags = LDRP_ENTRY_PROCESSED;
- DataTableEntry->LoadCount = 1;
- /* 把DTE连入传入的LOADER_PARAMETER_BLOCK */
- InsertTailList(&WinLdrBlock->LoadOrderListHead, &DataTableEntry->InLoadOrderLinks);
- /* 返回DTE */
- *NewEntry = DataTableEntry;
- return TRUE;
- }
函数使用MmHeapAlloc从堆中申请LDR_DATA_TABLE_ENTRY结构,按照传入的BaseDllName、FullDllName和NT头初始化。之后把LDR_DATA_TABLE_ENTRY连接入PLOADER_PARAMETER_BLOCK->InLoadOrderLinks。由于LDR_DATA_TABLE_ENTRY中的BaseDllName和FullDllName是UNICODE_STRING,所以需要对ANSI字符进行转化。这里只是逐字节的把ANSI字符拷贝到WCHAR里,所以如果有中文字符转化将会出错。
在freeldr/freeldr/disk/scsiport.c的LoadBootDeviceDriver中
有这样一段代码
- MachDiskGetBootPath(NtBootDdPath, sizeof(NtBootDdPath));
- strcat(NtBootDdPath, "//NTBOOTDD.SYS");
- /* Load file */
- Status = WinLdrLoadImage(NtBootDdPath, LoaderBootDriver, &ImageBase);
- if (!Status)
- {
- /* That's OK. File simply doesn't exist */
- return ESUCCESS;
- }
- /* Fix imports */
- Status = WinLdrAllocateDataTableEntry(&LoaderBlock, "ntbootdd.sys",
- "NTBOOTDD.SYS", ImageBase, &BootDdDTE);
- if (!Status)
- return EIO;
- Status = WinLdrScanImportDescriptorTable(&LoaderBlock, "", BootDdDTE);
- if (!Status)
- return EIO;
使用WinLdrLoadImage加载NTBOOTDD.SYS镜像,之后马上调用WinLdrAllocateDataTableEntry生成对应的LDR_DATA_TABLE_ENTRY。
到这里一切都顺利,但是好像忘了什么。导入表还没有处理,如果我们的模块导入了其他模块的函数怎么办?
没错,这就是上面代码中的WinLdrScanImportDescriptorTable的作用。
这个函数有三个参数
IN OUT PLOADER_PARAMETER_BLOCK WinLdrBlock
IN PCCH DirectoryPath
IN PLDR_DATA_TABLE_ENTRY ScanDTE
WinLdrBlock是DLL所在进程的控制块,在这里也就是FreeLdr对应的PEB。
DirectoryPath是根目录或磁盘的Arc路径,如果为空则使用引导盘根目录。
ScanDTE是需要处理导入表的模块的LDR_DATA_TABLE_ENTRY结构指针。
WinLdrImportDescriptorTable(freeldr/freeldr/peloader.c)
- BOOLEAN
- WinLdrScanImportDescriptorTable(IN OUT PLOADER_PARAMETER_BLOCK WinLdrBlock,
- IN PCCH DirectoryPath,
- IN PLDR_DATA_TABLE_ENTRY ScanDTE)
- {
- /* 从PE头中获得导入表地址 */
- ImportTable = (PIMAGE_IMPORT_DESCRIPTOR)RtlImageDirectoryEntryToData(VaToPa(ScanDTE->DllBase),
- TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ImportTableSize);
- /* If image doesn't have any import directory - just return success */
- if (ImportTable == NULL)
- return TRUE;
- /* 每个ImportTable表项循环一次 */
- for (;(ImportTable->Name != 0) && (ImportTable->FirstThunk != 0);ImportTable++)
- {
- /* 获得导入的模块名称 */
- ImportName = (PCH)VaToPa(RVA(ScanDTE->DllBase, ImportTable->Name));
- /* 有时导入表会包含模块本身,直接跳过 */
- if (WinLdrpCompareDllName(ImportName, &ScanDTE->BaseDllName))
- continue;
- /* 如果ImportName代表的模块没有被载入, 加载该模块 */
- if (!WinLdrCheckForLoadedDll(WinLdrBlock, ImportName, &DataTableEntry))
- {
- /* 加载ImportName代表的模块 */
- Status = WinLdrpLoadAndScanReferencedDll(WinLdrBlock,
- DirectoryPath,
- ImportName,
- &DataTableEntry);
- if (!Status)
- return Status;
- }
- /* 改写导入表中的函数地址*/
- Status = WinLdrpScanImportAddressTable(
- WinLdrBlock,
- DataTableEntry->DllBase,
- ScanDTE->DllBase,
- (PIMAGE_THUNK_DATA)RVA(ScanDTE->DllBase, ImportTable->FirstThunk));
- if (!Status)
- {
- return Status;
- }
- }
- return TRUE;
- }
这个函数的错用是为ScanDTE所代表的模块处理导入表。包括加载导入的DLL和改写IAT。
RtlImageDirectoryEntryToData函数可以通过镜像基地址找到其包含的导入表的地址,它在文件lib/rtl/image.c中。
获得导入表后,遍历每一个表项。表项里面包含了需要加载的DLL名称Name,因为有可能DLL中会包含自身的引用,所以使用WinLdrpCompareName函数将Name和自身进行对比,如果相同直接忽略这个表项(18行)。
之后使用WinLdrCheckForLoadedDll检查WinLdrBlock中是否已经加载了ImportName所代表的模块,如果已经加载则将模块对应的LDR_DATA_TABLE_ENTRY指针赋值给DataTableEntry参数(21行)。
如果模块还没有被加载,则使用WinLdrpLoadAndScanReferencedDll函数进行加载(24 - 29行)。这个函数里面包含对WinLdrScanImportDescriptorTable的引用,形成了一个递归。
到了32行时模块的状态是,所有引入的DLL已经被成功加载进内存,并且已经处理好了引用模块的IAT等信息。但是ScanDTE模块中对应的IAT还没有被改写。于是调用WinLdrpScanImportAddressTable填写IAT。
WinLdrpLoadAndScanReferencedDll加载导入DLL,WinLdrpScanImportAddressTable填写IAT都比较复杂,我们先看简单的。
21行使用WinLdrCheckForLoadedDll检测ImportName所代表的DLL是否已经被加载如内存。
WinLdrpScanImportDescriptorTable -> WinLdrCheckForLoadedDll(freeldr/freeldr/windows/peloader.c)
- BOOLEAN WinLdrCheckForLoadedDll(IN OUT PLOADER_PARAMETER_BLOCK WinLdrBlock,
- IN PCH DllName,
- OUT PLDR_DATA_TABLE_ENTRY *LoadedEntry)
- {
- PLDR_DATA_TABLE_ENTRY DataTableEntry;
- LIST_ENTRY *ModuleEntry;
- /* 遍历LOADER_PARAMETER_BLOCK中的LoadOrderListHead表。这里面存储的是已经加载的DLL的LDR_DATA_TABLE_ENTRY结构 */
- ModuleEntry = WinLdrBlock->LoadOrderListHead.Flink;
- while (ModuleEntry != &WinLdrBlock->LoadOrderListHead)
- {
- /* 获得LDR_DATA_TABLE_ENTRY指针 */
- DataTableEntry = CONTAINING_RECORD(ModuleEntry,
- LDR_DATA_TABLE_ENTRY,
- InLoadOrderLinks);
-
- /* 比较DllName和DTE中记录的名字是否一样 */
- if (WinLdrpCompareDllName(DllName, &DataTableEntry->BaseDllName))
- {
- /* 如果一样说明该DLL已经加载,增加DTE中的引用计数后返回 */
- *LoadedEntry = DataTableEntry;
- DataTableEntry->LoadCount++;
- return TRUE;
- }
- /* 下一个表项 */
- ModuleEntry = ModuleEntry->Flink;
- }
- /* 没找到 */
- return FALSE;
- }
这个函数会遍历LOADER_PARAMETER_BLOCK的LoadOrderListHead链。上面我们已经知道这个链中存放了所有已经加载的DLL的LDR_DATA_TABLE_ENTRY结构,通过比较名字就可以知道DllName代表的模块是否已经被加载。
如果已经加载则把对应的DTE的引用计数加1,没找到返回false。
如果WinLdrpScanImportDescriptorTable发现ImportName所指的模块没有被加载,就会使用WinLdrpLoadAndScanReferencedDll加载该模块(24行)。
WinLdrpScanImportDescriptorTable -> WinLdrpLoadAndScanReferencedDll(freeldr/freeldr/windows/peloader.c)
- BOOLEAN WinLdrpLoadAndScanReferencedDll(PLOADER_PARAMETER_BLOCK WinLdrBlock,
- PCCH DirectoryPath,
- PCH ImportName,
- PLDR_DATA_TABLE_ENTRY *DataTableEntry)
- {
- CHAR FullDllName[256];
- BOOLEAN Status;
- PVOID BasePA;
- /* 使用DirectoryPath和ImportName组合出文件全路径 */
- strcpy(FullDllName, DirectoryPath);
- strcat(FullDllName, ImportName);
- /* 使用WinLdrLoadImage加载DLL至内存 */
- Status = WinLdrLoadImage(FullDllName, LoaderHalCode, &BasePA);
- if (!Status)
- return Status;
- /* 为DLL生成DTE */
- Status = WinLdrAllocateDataTableEntry(WinLdrBlock,
- ImportName,
- FullDllName,
- BasePA,
- DataTableEntry);
- if (!Status)
- return Status;
- /* 处理DLL的导入表,在这里形成递归 */
- Status = WinLdrScanImportDescriptorTable(WinLdrBlock, DirectoryPath, *DataTableEntry);
- if (!Status)
- return Status;
- return TRUE;
- }
首先函数使用传入的目录DirectoryPath(这个是Arc路径)和文件名ImportName组合出文件路径。之后使用以前说过的WinLdrLoadImage将DLL加载入内存,并且使用WinLdrAllocateDataTableEntry生成对应的DTE结构。
之后我们发现它又调用了WinLdrScanImportDescriptorTable函数,来处理刚刚加载的DLL。为了处理某个DLL的导入表
WinLdrpScanImportDescriptorTable调用了
WinLdrpLoadAndScanReferencedDll
,而
WinLdrpLoadAndScanReferencedDll又为了处理其它DLL的导入表调用了
WinLdrpScanImportDescriptorTable。这里形成了递归。注意即便有A.DLL导入B.DLL,B.DLL又导入A.DLL
这里也不会出现死循环,因为A导入B时处理得是A的导入表和B的导出表,B导入A时处理的是B的导入表和A的导出表。而到导出在WinLdrLoadImage后就已经可以正常使用了,不需要单独的初始化,希望我表达的足够清楚 :)
现在我们回到
WinLdrImportDescriptorTable的24行,
WinLdrpLoadAndScanReferencedDll函数完成后ImportName代表的DLL就已经加载完毕,包括DLL的导入表也已经处理好了。
下面我们需要进行最后一步了,处理IAT,把IAT中的函数地址改写为ImportName
导出表中的函数。这个操作由32行的WinLdrpScanImportTableAddress函数完成。这个函数比较复杂,留到下一节吧 :)