从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
问题原因
- 地址计算错误:x86-64的call指令使用的是相对偏移,当前代码用
ImageBase + 偏移值计算目标地址是错误的,正确逻辑应为当前call指令地址 + 相对偏移。 - 跳转桩的存在: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
相关产品推荐
相关产品推荐

