You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从Windows PE文件恢复导入函数失败:调用地址与IAT不匹配

问题:PE文件导入函数恢复失败,调用目标地址与IAT不匹配

我尝试从PE文件中恢复导入函数,逻辑是反汇编.text段指令,筛选call指令并获取目标地址,再和IAT(导入地址表)中的函数地址匹配。但所有调用目标地址都无法匹配IAT地址,且IAT地址始终高于调用目标地址(示例:调用目标0x140005de8,IAT地址0x140007300)。测试多个良性可执行文件都存在这个问题。

相关代码

import sys
import pefile
from capstone import *

def valid_pefile(file_path):
    """
     Validates the input file as a PE file.
     Args: the path to the PE file
     Returns: pe file or None if the file is not a PE file """
    try:
        pe = pefile.PE(file_path)
        return pe
    except pefile.PEFormatError as pe_err:
        print("[-] error while parsing file {}:\n\t{}".format(file_path,pe_err))
        return None

def get_imported_functions(pe,ins):
    """
    Extracts the imported functions from a PE file.
    Args: pe file and disassembled.
    Returns: A list of imported functions.
    """
    imported_calls = []
    for (address, size, mnemonic, op_str) in ins:
        if mnemonic == 'call' and op_str.startswith('0x'):
            # Get the virtual address of the call destination
            va = pe.OPTIONAL_HEADER.ImageBase + int(op_str, 16)
            # debug: print("Call destination address: " + str(hex(va)))
            # Check if the destination address matches an IAT entry
            for entry in pe.DIRECTORY_ENTRY_IMPORT:
                for imp in entry.imports:
                    # debug: print("IAT func address: " + str(hex(imp.address)))
                    if imp.address == va:
                        imported_calls.append({
                            'function_name': imp.name.decode(),
                            'call_address': hex(address),
                            'rva': hex(address - pe.OPTIONAL_HEADER.ImageBase)
                        })
                        break

    return imported_calls


def disassemble_code(code, offset=1000):
    """
    Disassembles x86-64 binary code section.
    Args: binary_data (bytes): The raw binary data to be disassembled.
    Returns: A list of disassembled instructions in tuple form.
    """
    try:
        # Initialize the Capstone disassembler
        md = Cs(CS_ARCH_X86, CS_MODE_64)

        # Disassemble the binary data
        instructions = md.disasm_lite(code, offset)
        return instructions
    except CsError as e:
        print(f"Disassembly error: {e}")
        return None



def get_text_section(pe):
    """
    Extracts the .text section from a PE file and prints its starting point and size.
    Args: file_path (str): The path to the PE file.
    Returns: contents of the .text section.
    """
    if pe is not None:
        text_section = next((section for section in pe.sections if section.Name.decode().strip('\x00') == '.text'), None)
        if text_section:
            text_data = text_section.get_data()
            text_start = text_section.VirtualAddress
            text_size = text_section.Misc_VirtualSize
            # debugging
            # print(f"The .text section starts at virtual address 0x{text_start:08X} and has a size of {text_size} bytes.")
            return text_data, text_start
        else:
            return None
    else:
        print("Error while parsing PE file: {}".format(pe))


if __name__ == '__main__':
    pe = valid_pefile(sys.argv[1])
    code, address = get_text_section(pe)
    ins = disassemble_code(code, address)
    calls = get_imported_functions(pe,ins)
    for call in calls:
        print(f"Function name: {call['function_name']}, Call address: {call['call_address']}, RVA: {call['rva']}")

示例输出

Call dest addresses     IAT func addresses
0x140005de8         0x140007300
0x140005de8         0x140007308
0x140005de8         0x140007310
0x140005de8         0x140007318
0x140005de8         0x140007320
0x140005d9c         0x140007328
0x140005de8         0x140007330
0x14000107c         0x140007338
0x140005de8         0x140007340
0x140005d9c         0x140007348
0x140005de8         0x140007350
0x14000107c         0x140007358
0x14000107c         0x140007360
0x140005d9c         0x140007368

问题原因

  1. 地址计算错误:x86-64的call指令使用的是相对偏移,当前代码用ImageBase + 偏移值计算目标地址是错误的,正确逻辑应为当前call指令地址 + 相对偏移。
  2. 跳转桩的存在:64位PE中,直接call导入函数的指令不会指向IAT地址,而是指向跳转桩(通常是jmp qword ptr [IAT地址])。你看到的call目标地址是跳转桩的地址,而非IAT中的实际函数入口,因此两者无法匹配。

修复后的代码

import sys
import pefile
from capstone import *

def valid_pefile(file_path):
    try:
        pe = pefile.PE(file_path)
        return pe
    except pefile.PEFormatError as pe_err:
        print("[-] error while parsing file {}:\n\t{}".format(file_path,pe_err))
        return None

def get_section_by_va(pe, va):
    # 根据虚拟地址找到对应的段
    for section in pe.sections:
        sec_start = pe.OPTIONAL_HEADER.ImageBase + section.VirtualAddress
        sec_end = sec_start + section.Misc_VirtualSize
        if sec_start <= va < sec_end:
            return section
    return None

def resolve_jump_stub(pe, stub_va):
    # 解析跳转桩,获取实际IAT地址
    section = get_section_by_va(pe, stub_va)
    if not section:
        return None
    
    # 计算桩在段内的偏移
    stub_offset = stub_va - (pe.OPTIONAL_HEADER.ImageBase + section.VirtualAddress)
    stub_data = section.get_data()[stub_offset:stub_offset+16]  # 取足够长的字节解析指令
    
    # 反汇编跳转桩指令
    md = Cs(CS_ARCH_X86, CS_MODE_64)
    md.detail = False
    for ins in md.disasm_lite(stub_data, stub_va):
        if ins[2] == 'jmp' and 'qword ptr [' in ins[3]:
            # 提取括号内的地址
            addr_str = ins[3].split('[')[1].split(']')[0]
            try:
                # 处理绝对地址或相对地址
                if addr_str.startswith('0x'):
                    iat_va = int(addr_str, 16)
                else:
                    # 相对地址的情况,计算实际VA
                    iat_va = ins[0] + int(addr_str, 16)
                return iat_va
            except:
                return None
    return None

def get_imported_functions(pe, ins):
    imported_calls = []
    # 先构建IAT地址到函数名的映射,提高匹配效率
    iat_map = {}
    for entry in pe.DIRECTORY_ENTRY_IMPORT:
        for imp in entry.imports:
            if imp.address != 0 and imp.name:
                iat_map[imp.address] = imp.name.decode()
    
    md = Cs(CS_ARCH_X86, CS_MODE_64)
    md.detail = True
    
    for (addr, size, mnemonic, op_str) in ins:
        if mnemonic == 'call':
            # 正确计算call目标VA:当前指令地址 + 相对偏移
            # 或者直接从Capstone的详细信息中获取目标地址
            code = pe.get_data(addr - pe.OPTIONAL_HEADER.ImageBase, size)
            for detail_ins in md.disasm(code, addr):
                if detail_ins.mnemonic == 'call':
                    if len(detail_ins.operands) == 1:
                        op = detail_ins.operands[0]
                        if op.type == CS_OP_IMM:
                            call_target_va = op.value.imm
                            # 检查是否是跳转桩
                            iat_va = resolve_jump_stub(pe, call_target_va)
                            if not iat_va:
                                iat_va = call_target_va
                            
                            # 匹配IAT映射
                            if iat_va in iat_map:
                                imported_calls.append({
                                    'function_name': iat_map[iat_va],
                                    'call_address': hex(addr),
                                    'rva': hex(addr - pe.OPTIONAL_HEADER.ImageBase),
                                    'iat_address': hex(iat_va)
                                })
                    break
    return imported_calls

def disassemble_code(code, offset=1000):
    try:
        md = Cs(CS_ARCH_X86, CS_MODE_64)
        instructions = md.disasm_lite(code, offset)
        return instructions
    except CsError as e:
        print(f"Disassembly error: {e}")
        return None

def get_text_section(pe):
    if pe is not None:
        text_section = next((section for section in pe.sections if section.Name.decode().strip('\x00') == '.text'), None)
        if text_section:
            text_data = text_section.get_data()
            text_start = text_section.VirtualAddress
            return text_data, text_start
        else:
            return None
    else:
        print("Error while parsing PE file: {}".format(pe))

if __name__ == '__main__':
    pe = valid_pefile(sys.argv[1])
    if pe:
        code, address = get_text_section(pe)
        ins = disassemble_code(code, address)
        calls = get_imported_functions(pe, ins)
        for call in calls:
            print(f"函数名: {call['function_name']}, 调用地址: {call['call_address']}, RVA: {call['rva']}, IAT地址: {call['iat_address']}")

内容的提问来源于stack exchange,提问作者Stephen O'Shaughnessy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 01:32:04