延迟导入目录是否应该包含虚拟地址?

逆向工程 pe32
2021-07-05 09:52:16

REKO反编译器尝试加载我期待在一个特定的二进制的PE延迟进口目录崩溃。对于 32 位可执行文件,PE 规范指出该目录由一系列记录组成,其中偏移量 4 包含:

[the] 要加载的 DLL 名称的 RVA。该名称位于图像的只读数据部分 ( szName)

当我使用 dumpbin 查看图像时,我看到 PE 标头

      185000 [     2C6] RVA [size] of Delay Import 

.didata部分的原始数据是:

00585000: 00 00 00 00 90 51 58 00 00 00 00 00 A0 50 58 00  .....QX......PX.
00585010: B4 50 58 00 C8 50 58 00 DC 50 58 00 00 00 00 00  �PX.�PX.�PX..... 
(etc)

请注意,在 00585004 处,该szName字段在我看来是虚拟地址 (00585190) 而不是RVA(本来应该是 00185190)。尽管如此,dumpbin 还是设法将其解释为:

USER32.DLL
          00000000 Characteristics
          00000000 Address of HMODULE
          005850A0 Import Address Table
          005850B4 Import Name Table
          005850C8 Bound Import Name Table
          005850DC Unload Import Name Table
                 0 time date stamp

它跟随 00585190 来查找字符串USER32.DLL

那么延迟导入目录中的条目应该如何解释呢?PE 加载程序是否应该首先尝试将该szName字段作为 RVA读取,并且仅当它发现它不是有效的 RVA 时才尝试将其作为 VA 读取?

请注意,对于在地址 0x0040000 处加载的小型 EXE 文件,有效 RVA 的范围将为 ,[0x00000000..MAX_RVA)而有效虚拟地址的范围将为[0x00400000..MAX_RVA + 0x00400000],因此理论上可以通过查看 RVA 和 VA 的数值来区分它们。但是一旦二进制大小超过 0x00400000 字节(4194304 字节),这些范围就会重叠,您就无法再区分它们了。

更新:有趣的是,许多 PE 查看器和编辑器在此二进制文件上崩溃或脱轨。Dumpbin、IDA 和——最重要的——Windows 加载程序不会崩溃。想知道他们使用什么算法来避免死在这个二进制文件上?

1个回答

系统加载器不会处理延迟导入,因此程序员可以将任何类型的数据放入其中,只要他们准备好处理它。按照惯例(主要是因为 Visual C++ 这样做了),预计延迟导入使用与“正常”导入相同的格式,但由于操作系统不强制执行,因此不是必需的,特定程序可以使用自己的格式或把任何垃圾放进去。

IIRC szName 的问题来自延迟导入 (VC 6.0) 的第一次实现,它错误地使用了完整地址而不是 RVA。这可以在 Visual C++ ( delayhlp.cpp)附带的延迟导入助手的源代码中看到

// For our own internal use, we convert to the old
// format for convenience.
//
struct InternalImgDelayDescr {
    DWORD           grAttrs;        // attributes
    LPCSTR          szName;         // pointer to dll name
    HMODULE *       phmod;          // address of module handle
    PImgThunkData   pIAT;           // address of the IAT
    PCImgThunkData  pINT;           // address of the INT
    PCImgThunkData  pBoundIAT;      // address of the optional bound IAT
    PCImgThunkData  pUnloadIAT;     // address of optional copy of original IAT
    DWORD           dwTimeStamp;    // 0 if not bound,
                                    // O.W. date/time stamp of DLL bound to (Old BIND)
    };

(注意它是如何说“旧格式”的,大多数字段,包括szName,都是完整的指针而不是 RVA)。

MSDN 文章Changes in the DLL Delayed Loading Helper Function 自 Visual C++ 6.0 中也提到了这个问题

由于延迟描述符(delayimp.h 中的 ImgDelayDescr)中的指针已从绝对地址 (VAs) 更改为相对地址 (RVA) 以在 32 位和 64 位程序中按预期工作,您需要将它们转换回指针。引入了一个新函数:PFromRva,位于 delayhlp.cpp 中。您可以在描述符中的每个字段上使用此函数将它们转换回 32 位或 64 位指针。默认的延迟加载辅助函数仍然是一个很好的模板,可以用作示例。

如果打开上面提到的header,可以看到这个定义:

enum DLAttr {                   // Delay Load Attributes
    dlattrRva = 0x1,                // RVAs are used instead of pointers
                                    // Having this set indicates a VC7.0
                                    // and above delay load descriptor.
    };

这就是 IDA 检测延迟导入的正确格式(在您的情况下是绝对地址)并且可以在不崩溃的情况下处理它们的方式。