Created
March 19, 2026 14:31
-
-
Save pierrehpezier/a37984b91c2054032e856faf2700d278 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright (c) 2026 Nextron Systems | |
| # Author: Pierre-Henri Pezier | |
| import idaapi | |
| import idautils | |
| import idc | |
| def _touches_rax(ea): | |
| """Check if instruction at ea writes to rax/eax/ax/al/ah.""" | |
| if idc.get_operand_type(ea, 0) == idc.o_reg and idc.get_operand_value(ea, 0) == 0: | |
| mnem = idc.print_insn_mnem(ea) | |
| if mnem not in ("cmp", "test", "push"): | |
| return True | |
| return False | |
| def detect_indirect_calls_in_func(func): | |
| """Scan a single function for the mov rax,imm / add rax,mem / call rax pattern.""" | |
| insns = list(idautils.FuncItems(func.start_ea)) | |
| results = [] | |
| for i, head in enumerate(insns): | |
| mnem = idc.print_insn_mnem(head) | |
| # Step 1: mov rax, <immediate> | |
| if mnem != "mov": | |
| continue | |
| if idc.get_operand_type(head, 0) != idc.o_reg: | |
| continue | |
| if idc.get_operand_value(head, 0) != 0: | |
| continue | |
| if idc.get_operand_type(head, 1) != idc.o_imm: | |
| continue | |
| imm_val = idc.get_operand_value(head, 1) & 0xFFFFFFFFFFFFFFFF | |
| mov_ea = head | |
| # Step 2: add rax, <mem> | |
| if i + 1 >= len(insns): | |
| continue | |
| add_ea = insns[i + 1] | |
| if idc.print_insn_mnem(add_ea) != "add": | |
| continue | |
| if idc.get_operand_type(add_ea, 0) != idc.o_reg: | |
| continue | |
| if idc.get_operand_value(add_ea, 0) != 0: | |
| continue | |
| add_op1_type = idc.get_operand_type(add_ea, 1) | |
| if add_op1_type not in (idc.o_mem, idc.o_displ): | |
| continue | |
| offset_addr = idc.get_operand_value(add_ea, 1) | |
| offset_val = idaapi.get_qword(offset_addr) | |
| # Step 3: scan up to 10 instructions for call rax, bail if rax is written | |
| found_call = False | |
| call_ea = None | |
| for j in range(i + 2, min(i + 2 + 10, len(insns))): | |
| scan_ea = insns[j] | |
| scan_mnem = idc.print_insn_mnem(scan_ea) | |
| scan_disasm = idc.GetDisasm(scan_ea) | |
| if scan_mnem == "call" and "rax" in scan_disasm: | |
| found_call = True | |
| call_ea = scan_ea | |
| break | |
| if _touches_rax(scan_ea): | |
| break | |
| if not found_call: | |
| continue | |
| resolved = (imm_val + offset_val) & 0xFFFFFFFFFFFFFFFF | |
| resolved_name = idc.get_func_name(resolved) or idc.get_name(resolved) or f"sub_{resolved:x}" | |
| results.append({ | |
| "mov_ea": mov_ea, | |
| "call_ea": call_ea, | |
| "resolved": resolved, | |
| "resolved_name": resolved_name, | |
| "imm": imm_val, | |
| "offset_addr": offset_addr, | |
| "offset_val": offset_val, | |
| }) | |
| return results | |
| def crawl_all(): | |
| """Crawl every function in the binary, detect patterns, and add comments.""" | |
| total = 0 | |
| for func_ea in idautils.Functions(): | |
| func = idaapi.get_func(func_ea) | |
| if not func: | |
| continue | |
| hits = detect_indirect_calls_in_func(func) | |
| if not hits: | |
| continue | |
| func_name = idc.get_func_name(func.start_ea) | |
| for hit in hits: | |
| resolved = hit["resolved"] | |
| resolved_name = hit["resolved_name"] | |
| call_ea = hit["call_ea"] | |
| imm = hit["imm"] | |
| offset_val = hit["offset_val"] | |
| offset_addr = hit["offset_addr"] | |
| offset_name = idc.get_name(offset_addr) or f"{offset_addr:#x}" | |
| comment = resolved_name | |
| idc.set_cmt(call_ea, comment, 0) | |
| total += 1 | |
| print(f" {func_name} | {call_ea:#x}: call rax ; {comment}") | |
| print(f"\n{'='*50}") | |
| print(f"Done. Commented {total} indirect call(s) across all functions.") | |
| print(f"{'='*50}") | |
| if __name__ == "__main__": | |
| crawl_all() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment