本文涉及地址与文件偏移换算参考PE文件可执行文件结构中地址映射的原理和计算方法PE 文件加载时Windows 加载器ntdll.dll 中的 LdrpInitializeProcess 等内部函数对导入表Import Table的处理是一个核心步骤称为导入解析Import Resolution或动态链接。如果这一步失败例如找不到依赖的 DLL 或缺少某个函数进程通常会终止并报错如 “Missing DLL” 或 “Entry Point Not Found”。以下是加载器操作导入表的详细流程一. 定位导入表加载器首先读取 PE 头的 导入数据目录Data Directory找到第一个条目IMAGE_DIRECTORY_ENTRY_IMPORT的RVA和所有条目的总大小导入数据目录的位置structIMAGE_NT_HEADERSNtHeaderstructIMAGE_OPTIONAL_HEADER64OptionalHeaderstructIMAGE_DATA_DIRECTORY_ARRAYDataDirArraystructIMAGE_DATA_DIRECTORYImport可以看到VirtualAddress28D4h属性名为VirtualAddress实际该地址表示的是RVA类地址大小为160右侧可看到010Editor自动计算了FOA文件偏移为1CD4定位文件中如下可以看到28D4h指向的是struct IMAGE_IMPORT_DESCRIPTOR ImportDescriptor[0]该结构体数组的第一个变量这里每一个IMAGE_IMPORT_DESCRIPTOR结构体大小为14h对应20这里一共有7个共占140但先前导入数据目录中看到的却是160。实际上在最后一个IMAGE_IMPORT_DESCRIPTOR还有20字节全0用来表示导入表结束二.遍历导入描述符按 DLL 分组加载器遍历 IMAGE_IMPORT_DESCRIPTOR 数组对每一个非零项执行以下操作A. 加载依赖的 DLL读取 descriptor 中的 Name 字段指向 DLL 文件名如 kernel32.dll调用内部加载逻辑类似于 LoadLibrary将该 DLL 加载到当前进程的地址空间中。如果 DLL 已加载则增加引用计数如多处需要导入一个DLL或DLL间相互依赖则此时为节省内存会增加该DLL的引用计数如果加载失败文件不存在、架构不匹配等进程启动失败。获取该 DLL 的基址Base AddressB. 解析导入函数按函数分组每个 DLL 对应两组重要的数组Thunk Arrays加载器需要同时处理它们原始导入名称表OriginalFirstThunk / Import Name Table, INT由 descriptor 的 OriginalFirstThunk (或 Characteristics) 字段指向。包含导入函数的原始信息函数名 RVA 或 序号 Ordinal。注意这个表在加载过程中通常是只读的用于查找函数信息。如果该字段为 0则回退使用 FirstThunk。这里关注OriginalFirstThunk和FirstThunk对应的文件偏移分别为1DF8h和1480h在文件中定位可以看到在PE未加载时对应位置的值都是0000000000002B46h是一样的一个地址所以当该字段为0时可回退使用FirstThunk如下导入地址表Import Address Table, IAT由 descriptor 的 FirstThunk 字段指向。这是最终存放函数实际内存地址的地方。代码中的 CALL [IAT_Entry] 指令将跳转到这里执行。如下OriginalFirstThunkOFT为29F8h转为文件偏移FOA10F8h定位文件偏移看到是一个8字节的地址2B46h因为这个PE为64位C. 逐个函数解析循环加载器并行遍历上述两个数组INT 和 IAT直到遇到全 0 的条目读取 thunk 值从 INT 中读取一个 IMAGE_THUNK_DATA判断导入方式按名称导入Import by Name如果 thunk 值的最高位为 0则该值是一个 RVA指向一个IMAGE_IMPORT_BY_NAME 结构。该结构包含一个 Hint提示值可选和一个 ASCII 字符串函数名如 CreateFileA。加载器在已加载 DLL 的导出表Export Table中通过二分查找或哈希匹配该函数名找到对应的函数 RVA按序号导入Import by Ordinal如果 thunk 值的最高位为 1则低 16/31 位表示函数的序号Ordinal加载器直接在 DLL 的导出表中通过序号索引找到函数 RVA在CFF工具中对照如下其中Qword表8字节大小该2B46的64位中的最高位为0故其为按名称导入故2B46h将指向一个IMAGE_IMPORT_BY_NAME 结构将这个2B46h转为文件偏移即1F46h查看其中前两个字节为hint之后为函数名字符串直至结束以00结束一个字符串所以加载器就按照这个函数名在加载的DLL中进行二分或哈希找到函数当处理完该DLL的第一个函数后需要处理第二个函数那么第二个函数如何解析呢答案是按函数名导入时若想获取第二个函数地址必须获得第二函数的函数名而第一个函数函数名在IMAGE_IMPORT_BY_NAME中由OriginalFirstThunkOFT定位该地址OFT指向的是INT中的第一个Thunk故要获取第二个函数要从INT中获取第二个Thunk。这里第二个Thunk即为第一个Thunk的后8字节这里是2B30hCFF中对照如下同理转为FOA文件偏移处得到函数名同理一直遍历下一个Thunk直至某一个Thunk全0这里此DLL的第一个Thunk在内共有5个Thunk对应5个函数之后便为全0在CFF中对照确实该DLL导入了5个函数3.计算绝对地址Function_Address DLL_Base_Address Function_RVA一个DLL也有其ImageBase这里以VCRUNTIME140.dll的导入的第一个函数__current_exception_context为例在x64dbg中的符号窗口中找到该函数的地址为7FFEB91911F0h4.填充 IAT将计算出的 Function_Address 写入到 IAT 对应的条目中覆盖原有的 RVA 或序号信息注意:这里写入的是IAT而非INTINT为只读的! 此时IAT 从“导入描述”变成了“可执行的函数指针表”具体为将获得的函数地址覆盖写入到IAT对应的Thunk处IAT的第一个Thunk由FirstThunk指向如下第一个Thunk的地址为2080hRVA该PE刚好加载在ImageBase上将该RVA加上ImageBase140000000h即可得到内存中的地址140002080h在x64dbg中查看对应内存处可看到对应的F0 11 19 B9 FE 7F 00 00端序转换后即为7FFEB91911F0h和x64dbg中符号窗口中找到该函数的地址相同。这便是函数地址写入的地方–IAT表该表不像INT表一样只读而是加载后会写入数据以记录各导入函数的地址
PE文件导入表解析(x64)
本文涉及地址与文件偏移换算参考PE文件可执行文件结构中地址映射的原理和计算方法PE 文件加载时Windows 加载器ntdll.dll 中的 LdrpInitializeProcess 等内部函数对导入表Import Table的处理是一个核心步骤称为导入解析Import Resolution或动态链接。如果这一步失败例如找不到依赖的 DLL 或缺少某个函数进程通常会终止并报错如 “Missing DLL” 或 “Entry Point Not Found”。以下是加载器操作导入表的详细流程一. 定位导入表加载器首先读取 PE 头的 导入数据目录Data Directory找到第一个条目IMAGE_DIRECTORY_ENTRY_IMPORT的RVA和所有条目的总大小导入数据目录的位置structIMAGE_NT_HEADERSNtHeaderstructIMAGE_OPTIONAL_HEADER64OptionalHeaderstructIMAGE_DATA_DIRECTORY_ARRAYDataDirArraystructIMAGE_DATA_DIRECTORYImport可以看到VirtualAddress28D4h属性名为VirtualAddress实际该地址表示的是RVA类地址大小为160右侧可看到010Editor自动计算了FOA文件偏移为1CD4定位文件中如下可以看到28D4h指向的是struct IMAGE_IMPORT_DESCRIPTOR ImportDescriptor[0]该结构体数组的第一个变量这里每一个IMAGE_IMPORT_DESCRIPTOR结构体大小为14h对应20这里一共有7个共占140但先前导入数据目录中看到的却是160。实际上在最后一个IMAGE_IMPORT_DESCRIPTOR还有20字节全0用来表示导入表结束二.遍历导入描述符按 DLL 分组加载器遍历 IMAGE_IMPORT_DESCRIPTOR 数组对每一个非零项执行以下操作A. 加载依赖的 DLL读取 descriptor 中的 Name 字段指向 DLL 文件名如 kernel32.dll调用内部加载逻辑类似于 LoadLibrary将该 DLL 加载到当前进程的地址空间中。如果 DLL 已加载则增加引用计数如多处需要导入一个DLL或DLL间相互依赖则此时为节省内存会增加该DLL的引用计数如果加载失败文件不存在、架构不匹配等进程启动失败。获取该 DLL 的基址Base AddressB. 解析导入函数按函数分组每个 DLL 对应两组重要的数组Thunk Arrays加载器需要同时处理它们原始导入名称表OriginalFirstThunk / Import Name Table, INT由 descriptor 的 OriginalFirstThunk (或 Characteristics) 字段指向。包含导入函数的原始信息函数名 RVA 或 序号 Ordinal。注意这个表在加载过程中通常是只读的用于查找函数信息。如果该字段为 0则回退使用 FirstThunk。这里关注OriginalFirstThunk和FirstThunk对应的文件偏移分别为1DF8h和1480h在文件中定位可以看到在PE未加载时对应位置的值都是0000000000002B46h是一样的一个地址所以当该字段为0时可回退使用FirstThunk如下导入地址表Import Address Table, IAT由 descriptor 的 FirstThunk 字段指向。这是最终存放函数实际内存地址的地方。代码中的 CALL [IAT_Entry] 指令将跳转到这里执行。如下OriginalFirstThunkOFT为29F8h转为文件偏移FOA10F8h定位文件偏移看到是一个8字节的地址2B46h因为这个PE为64位C. 逐个函数解析循环加载器并行遍历上述两个数组INT 和 IAT直到遇到全 0 的条目读取 thunk 值从 INT 中读取一个 IMAGE_THUNK_DATA判断导入方式按名称导入Import by Name如果 thunk 值的最高位为 0则该值是一个 RVA指向一个IMAGE_IMPORT_BY_NAME 结构。该结构包含一个 Hint提示值可选和一个 ASCII 字符串函数名如 CreateFileA。加载器在已加载 DLL 的导出表Export Table中通过二分查找或哈希匹配该函数名找到对应的函数 RVA按序号导入Import by Ordinal如果 thunk 值的最高位为 1则低 16/31 位表示函数的序号Ordinal加载器直接在 DLL 的导出表中通过序号索引找到函数 RVA在CFF工具中对照如下其中Qword表8字节大小该2B46的64位中的最高位为0故其为按名称导入故2B46h将指向一个IMAGE_IMPORT_BY_NAME 结构将这个2B46h转为文件偏移即1F46h查看其中前两个字节为hint之后为函数名字符串直至结束以00结束一个字符串所以加载器就按照这个函数名在加载的DLL中进行二分或哈希找到函数当处理完该DLL的第一个函数后需要处理第二个函数那么第二个函数如何解析呢答案是按函数名导入时若想获取第二个函数地址必须获得第二函数的函数名而第一个函数函数名在IMAGE_IMPORT_BY_NAME中由OriginalFirstThunkOFT定位该地址OFT指向的是INT中的第一个Thunk故要获取第二个函数要从INT中获取第二个Thunk。这里第二个Thunk即为第一个Thunk的后8字节这里是2B30hCFF中对照如下同理转为FOA文件偏移处得到函数名同理一直遍历下一个Thunk直至某一个Thunk全0这里此DLL的第一个Thunk在内共有5个Thunk对应5个函数之后便为全0在CFF中对照确实该DLL导入了5个函数3.计算绝对地址Function_Address DLL_Base_Address Function_RVA一个DLL也有其ImageBase这里以VCRUNTIME140.dll的导入的第一个函数__current_exception_context为例在x64dbg中的符号窗口中找到该函数的地址为7FFEB91911F0h4.填充 IAT将计算出的 Function_Address 写入到 IAT 对应的条目中覆盖原有的 RVA 或序号信息注意:这里写入的是IAT而非INTINT为只读的! 此时IAT 从“导入描述”变成了“可执行的函数指针表”具体为将获得的函数地址覆盖写入到IAT对应的Thunk处IAT的第一个Thunk由FirstThunk指向如下第一个Thunk的地址为2080hRVA该PE刚好加载在ImageBase上将该RVA加上ImageBase140000000h即可得到内存中的地址140002080h在x64dbg中查看对应内存处可看到对应的F0 11 19 B9 FE 7F 00 00端序转换后即为7FFEB91911F0h和x64dbg中符号窗口中找到该函数的地址相同。这便是函数地址写入的地方–IAT表该表不像INT表一样只读而是加载后会写入数据以记录各导入函数的地址