ReactOS-Freeldr镜像加载2

 先来介绍几个重要的结构。Freeldr虽然是个引导程序,严格来讲不属于操作系统的一部分,但它也有一些类似Windows系统中的数据结构。

LOADER_PARAMETER_BLOCK这个可以看作Freeldr的PEB,结构也和PEB非常相似。
/include/reactos/arc/arc.h
  1. typedef struct _LOADER_PARAMETER_BLOCK
  2. {
  3.     LIST_ENTRY LoadOrderListHead;
  4.     LIST_ENTRY MemoryDescriptorListHead;
  5.     LIST_ENTRY BootDriverListHead;
  6.     ......
  7. } 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中
  1. typedef struct _LDR_DATA_TABLE_ENTRY
  2. {
  3.     LIST_ENTRY InLoadOrderLinks;                      // 用来连入LoadOrderListHead等的表头
  4.     LIST_ENTRY InMemoryOrderModuleList;
  5.     LIST_ENTRY InInitializationOrderModuleList;
  6.     PVOID DllBase;                                    // 模块基地址
  7.     PVOID EntryPoint;                                 // 入口点
  8.     ULONG SizeOfImage;                                // 内存镜像大小
  9.     UNICODE_STRING FullDllName;                       // 文件全路径
  10.     UNICODE_STRING BaseDllName;                       // 模块名, 不一定和全路径的文件名相同
  11.     ULONG Flags;                                      // 一些标志
  12.     USHORT LoadCount;                                 // 引用计数
  13.     USHORT TlsIndex;
  14.     union
  15.     {
  16.         LIST_ENTRY HashLinks;
  17.         struct
  18.         {
  19.             PVOID SectionPointer;
  20.             ULONG CheckSum;                           // 校验和
  21.         };
  22.     };
  23.     union
  24.     {
  25.         ULONG TimeDateStamp;
  26.         PVOID LoadedImports;
  27.     };
  28.     PVOID EntryPointActivationContext;
  29.     PVOID PatchInformation;
  30. } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
我做过注释的是Freeldr实际用到的域。注意这里BaseDllName中记录的模块名不一定和模块的实际文件名相同。许多BaseDllName是硬编码的只具有参考意义。

上一节读的WinLdrLoadImage函数,只是把文件从硬盘中读出来按照PE格式对其,并没有涉及上面所说的结构。所以Freeldr中WinLdrLoadImage往往都是和WinLdrAllocateDataTableEntry函数配合使用的。WinLdrLoadImage负责加载镜像,WinLdrAllocateDataTableEntry负责生成、初始化对应的LDR_DATA_TABLE_ENTRY结构。

  1. BOOLEAN
  2. WinLdrAllocateDataTableEntry(IN OUT PLOADER_PARAMETER_BLOCK WinLdrBlock,
  3.                              IN PCCH BaseDllName,
  4.                              IN PCCH FullDllName,
  5.                              IN PVOID BasePA,
  6.                              OUT PLDR_DATA_TABLE_ENTRY *NewEntry)
  7. {
  8.     PVOID BaseVA = PaToVa(BasePA);
  9.     ......
  10.     /* 申请LDR_DATA_TABLE_ENTRY结构 */
  11.     DataTableEntry = (PLDR_DATA_TABLE_ENTRY)MmHeapAlloc(sizeof(LDR_DATA_TABLE_ENTRY));
  12.     if (DataTableEntry == NULL)
  13.         return FALSE;
  14.     RtlZeroMemory(DataTableEntry, sizeof(LDR_DATA_TABLE_ENTRY));
  15.     /* 获得NT头指针 */
  16.     NtHeaders = RtlImageNtHeader(BasePA);
  17.     /* 初始化DllBase、SizeOfImage、EntryPoint、CheckSum */
  18.     DataTableEntry->DllBase = BaseVA;
  19.     DataTableEntry->SizeOfImage = NtHeaders->OptionalHeader.SizeOfImage;
  20.     DataTableEntry->EntryPoint = RVA(BaseVA, NtHeaders->OptionalHeader.AddressOfEntryPoint);
  21.     DataTableEntry->SectionPointer = 0;
  22.     DataTableEntry->CheckSum = NtHeaders->OptionalHeader.CheckSum;
  23.     /* 把Ansi字符转换为UNICODE_STRING存入BaseDllName和FullDllName中。这个转换只是简单的逐字节复制,所以如果路径中有中文会出错的。 */
  24.     Length = (USHORT)(strlen(BaseDllName) * sizeof(WCHAR));
  25.     Buffer = (PWSTR)MmHeapAlloc(Length);
  26.     if (Buffer == NULL)
  27.     {
  28.         MmHeapFree(DataTableEntry);
  29.         return FALSE;
  30.     }
  31.     RtlZeroMemory(Buffer, Length);
  32.     DataTableEntry->BaseDllName.Length = Length;
  33.     DataTableEntry->BaseDllName.MaximumLength = Length;
  34.     DataTableEntry->BaseDllName.Buffer = PaToVa(Buffer);
  35.     while (*BaseDllName != 0)
  36.     {
  37.         *Buffer++ = *BaseDllName++;
  38.     }
  39.     Length = (USHORT)(strlen(FullDllName) * sizeof(WCHAR));
  40.     Buffer = (PWSTR)MmHeapAlloc(Length);
  41.     if (Buffer == NULL)
  42.     {
  43.         MmHeapFree(DataTableEntry);
  44.         return FALSE;
  45.     }
  46.     RtlZeroMemory(Buffer, Length);
  47.     DataTableEntry->FullDllName.Length = Length;
  48.     DataTableEntry->FullDllName.MaximumLength = Length;
  49.     DataTableEntry->FullDllName.Buffer = PaToVa(Buffer);
  50.     while (*FullDllName != 0)
  51.     {
  52.         *Buffer++ = *FullDllName++;
  53.     }
  54.     /* 初始化Flag和LoadCount */
  55.     DataTableEntry->Flags = LDRP_ENTRY_PROCESSED;
  56.     DataTableEntry->LoadCount = 1;
  57.     /* 把DTE连入传入的LOADER_PARAMETER_BLOCK */
  58.     InsertTailList(&WinLdrBlock->LoadOrderListHead, &DataTableEntry->InLoadOrderLinks);
  59.     /* 返回DTE */
  60.     *NewEntry = DataTableEntry;
  61.     return TRUE;
  62. }
函数使用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中 有这样一段代码

  1.     MachDiskGetBootPath(NtBootDdPath, sizeof(NtBootDdPath));
  2.     strcat(NtBootDdPath, "//NTBOOTDD.SYS");
  3.     /* Load file */
  4.     Status = WinLdrLoadImage(NtBootDdPath, LoaderBootDriver, &ImageBase);
  5.     if (!Status)
  6.     {
  7.         /* That's OK. File simply doesn't exist */
  8.         return ESUCCESS;
  9.     }
  10.     /* Fix imports */
  11.     Status = WinLdrAllocateDataTableEntry(&LoaderBlock, "ntbootdd.sys",
  12.         "NTBOOTDD.SYS", ImageBase, &BootDdDTE);
  13.     if (!Status)
  14.         return EIO;
  15.     Status = WinLdrScanImportDescriptorTable(&LoaderBlock, "", BootDdDTE);
  16.     if (!Status)
  17.         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)
  1. BOOLEAN
  2. WinLdrScanImportDescriptorTable(IN OUT PLOADER_PARAMETER_BLOCK WinLdrBlock,
  3.                                 IN PCCH DirectoryPath,
  4.                                 IN PLDR_DATA_TABLE_ENTRY ScanDTE)
  5. {
  6.     /* 从PE头中获得导入表地址 */
  7.     ImportTable = (PIMAGE_IMPORT_DESCRIPTOR)RtlImageDirectoryEntryToData(VaToPa(ScanDTE->DllBase),
  8.         TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ImportTableSize);
  9.     /* If image doesn't have any import directory - just return success */
  10.     if (ImportTable == NULL)
  11.         return TRUE;
  12.     /* 每个ImportTable表项循环一次 */
  13.     for (;(ImportTable->Name != 0) && (ImportTable->FirstThunk != 0);ImportTable++)
  14.     {
  15.         /* 获得导入的模块名称 */
  16.         ImportName = (PCH)VaToPa(RVA(ScanDTE->DllBase, ImportTable->Name));
  17.         /* 有时导入表会包含模块本身,直接跳过 */
  18.         if (WinLdrpCompareDllName(ImportName, &ScanDTE->BaseDllName))
  19.             continue;
  20.         /* 如果ImportName代表的模块没有被载入, 加载该模块 */
  21.         if (!WinLdrCheckForLoadedDll(WinLdrBlock, ImportName, &DataTableEntry))
  22.         {
  23.             /* 加载ImportName代表的模块 */
  24.             Status = WinLdrpLoadAndScanReferencedDll(WinLdrBlock,
  25.                 DirectoryPath,
  26.                 ImportName,
  27.                 &DataTableEntry);
  28.             if (!Status)
  29.                 return Status;
  30.         }
  31.         /* 改写导入表中的函数地址*/
  32.         Status = WinLdrpScanImportAddressTable(
  33.             WinLdrBlock,
  34.             DataTableEntry->DllBase,
  35.             ScanDTE->DllBase,
  36.             (PIMAGE_THUNK_DATA)RVA(ScanDTE->DllBase, ImportTable->FirstThunk));
  37.         if (!Status)
  38.         {
  39.             return Status;
  40.         }
  41.     }
  42.     return TRUE;
  43. }
这个函数的错用是为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)
  1. BOOLEAN WinLdrCheckForLoadedDll(IN OUT PLOADER_PARAMETER_BLOCK WinLdrBlock,
  2.                                 IN PCH DllName,
  3.                                 OUT PLDR_DATA_TABLE_ENTRY *LoadedEntry)
  4. {
  5.     PLDR_DATA_TABLE_ENTRY DataTableEntry;
  6.     LIST_ENTRY *ModuleEntry;
  7.     /* 遍历LOADER_PARAMETER_BLOCK中的LoadOrderListHead表。这里面存储的是已经加载的DLL的LDR_DATA_TABLE_ENTRY结构 */
  8.     ModuleEntry = WinLdrBlock->LoadOrderListHead.Flink;
  9.     while (ModuleEntry != &WinLdrBlock->LoadOrderListHead)
  10.     {
  11.         /* 获得LDR_DATA_TABLE_ENTRY指针 */
  12.         DataTableEntry = CONTAINING_RECORD(ModuleEntry,
  13.             LDR_DATA_TABLE_ENTRY,
  14.             InLoadOrderLinks);
  15.             
  16.         /* 比较DllName和DTE中记录的名字是否一样 */
  17.         if (WinLdrpCompareDllName(DllName, &DataTableEntry->BaseDllName))
  18.         {
  19.             /* 如果一样说明该DLL已经加载,增加DTE中的引用计数后返回 */
  20.             *LoadedEntry = DataTableEntry;
  21.             DataTableEntry->LoadCount++;
  22.             return TRUE;
  23.         }
  24.         /* 下一个表项 */
  25.         ModuleEntry = ModuleEntry->Flink;
  26.     }
  27.     /* 没找到 */
  28.     return FALSE;
  29. }
这个函数会遍历LOADER_PARAMETER_BLOCK的LoadOrderListHead链。上面我们已经知道这个链中存放了所有已经加载的DLL的LDR_DATA_TABLE_ENTRY结构,通过比较名字就可以知道DllName代表的模块是否已经被加载。
如果已经加载则把对应的DTE的引用计数加1,没找到返回false。

如果WinLdrpScanImportDescriptorTable发现ImportName所指的模块没有被加载,就会使用WinLdrpLoadAndScanReferencedDll加载该模块(24行)。
WinLdrpScanImportDescriptorTable -> WinLdrpLoadAndScanReferencedDll(freeldr/freeldr/windows/peloader.c)
  1. BOOLEAN WinLdrpLoadAndScanReferencedDll(PLOADER_PARAMETER_BLOCK WinLdrBlock,
  2.                                         PCCH DirectoryPath,
  3.                                         PCH ImportName,
  4.                                         PLDR_DATA_TABLE_ENTRY *DataTableEntry)
  5. {
  6.     CHAR FullDllName[256];
  7.     BOOLEAN Status;
  8.     PVOID BasePA;
  9.     /* 使用DirectoryPath和ImportName组合出文件全路径 */
  10.     strcpy(FullDllName, DirectoryPath);
  11.     strcat(FullDllName, ImportName);
  12.     /* 使用WinLdrLoadImage加载DLL至内存 */
  13.     Status = WinLdrLoadImage(FullDllName, LoaderHalCode, &BasePA);
  14.     if (!Status)
  15.         return Status;
  16.     /* 为DLL生成DTE */
  17.     Status = WinLdrAllocateDataTableEntry(WinLdrBlock,
  18.         ImportName,
  19.         FullDllName,
  20.         BasePA,
  21.         DataTableEntry);
  22.     if (!Status)
  23.         return Status;
  24.     /* 处理DLL的导入表,在这里形成递归 */
  25.     Status = WinLdrScanImportDescriptorTable(WinLdrBlock, DirectoryPath, *DataTableEntry);
  26.     if (!Status)
  27.         return Status;
  28.     return TRUE;
  29. }
首先函数使用传入的目录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函数完成。这个函数比较复杂,留到下一节吧 :)

你可能感兴趣的:(ReactOS代码精读)