Created
April 3, 2025 17:50
-
-
Save 19h/12ade37babcf8ee9498f42643a9f31d1 to your computer and use it in GitHub Desktop.
Recursive function decompiler / LLM helper plugin for IDA Pro 9.0 / 9.1
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
import idaapi | |
import idautils | |
import idc | |
import ida_hexrays | |
import ida_kernwin | |
def count_total_xrefs_to(ea): | |
"""Counts total code and data cross-references to a given address.""" | |
count = 0 | |
# Count code references | |
cref = idc.get_first_cref_to(ea) | |
while cref != idaapi.BADADDR: | |
count += 1 | |
cref = idc.get_next_cref_to(ea, cref) | |
# Count data references | |
dref = idc.get_first_dref_to(ea) | |
while dref != idaapi.BADADDR: | |
count += 1 | |
dref = idc.get_next_dref_to(ea, dref) | |
return count | |
def decompile_reachable_functions(): | |
""" | |
Performs a depth-first search starting from the current function to identify all | |
reachable functions in the call graph. Then decompiles each function and writes | |
the decompiled code to a text file, sorted by the total number of incoming | |
cross-references from anywhere in the binary, with prettified output including | |
detailed headers and separators. | |
""" | |
output_file = ida_kernwin.ask_file(True, "*.txt", "Select or create output file for decompiled functions") | |
if not output_file: | |
print("No output file selected.") | |
return | |
# Get the current effective address and the function containing it | |
ea = idaapi.get_screen_ea() | |
func = idaapi.get_func(ea) | |
if not func: | |
print("No function at the current address.") | |
return | |
# Initialize data structures | |
start_ea = func.start_ea | |
visited = set() # Set of reachable function start addresses | |
# Perform DFS to build the set of reachable functions | |
dfs(start_ea, visited) | |
# Convert visited set to a list | |
reachable_funcs = list(visited) | |
# Sort by the total number of cross-references to the function start | |
reachable_funcs.sort(key=lambda fea: count_total_xrefs_to(fea)) | |
try: | |
with open(output_file, "w") as f: | |
for fea in reachable_funcs: # Use fea (function effective address) for clarity | |
# Get function name or use address if unnamed | |
func_name = idc.get_func_name(fea) | |
# Calculate total incoming references for the header | |
total_incoming_xrefs = count_total_xrefs_to(fea) | |
# Create a detailed header using total xrefs | |
if func_name: | |
header = f"// Function: {func_name} at 0x{fea:X}, Total incoming xrefs: {total_incoming_xrefs}\n" | |
else: | |
header = f"// Function at 0x{fea:X}, Total incoming xrefs: {total_incoming_xrefs}\n" | |
# Write the header | |
f.write(header) | |
# Attempt to decompile the function | |
try: | |
cfunc = ida_hexrays.decompile(fea) | |
if cfunc: | |
decompiled_code = str(cfunc) | |
f.write(decompiled_code) | |
f.write("\n") | |
else: | |
f.write("// Decompilation failed.\n") | |
except Exception as e: | |
f.write(f"// Decompilation error: {str(e)}\n") | |
# Add a separator after each function | |
f.write("// -----------------------------\n\n") | |
print(f"[*] Decompiled functions written to {output_file}") | |
except Exception as e: | |
print(f"Error writing output file: {e}") | |
def dfs(func_ea, visited): | |
"""Recursively traverse the call graph starting from func_ea.""" | |
if func_ea in visited: | |
return | |
visited.add(func_ea) | |
func = idaapi.get_func(func_ea) | |
if not func: | |
return | |
# Iterate over the function's instructions using idc.next_head | |
current_ea = func.start_ea | |
while current_ea < func.end_ea: | |
# Ensure the current address is recognized as code by IDA | |
if idaapi.is_code(idaapi.get_flags(current_ea)): | |
if idaapi.is_call_insn(current_ea): | |
# Get the target address from the call instruction's operand | |
for i in range(idaapi.UA_MAXOP): | |
op_type = idc.get_operand_type(current_ea, i) | |
# Check if the operand is a code address (near or far) | |
if op_type == idc.o_near or op_type == idc.o_far: | |
ref = idc.get_operand_value(current_ea, i) | |
if ref != idaapi.BADADDR: # Ensure we got a valid address | |
called_func = idaapi.get_func(ref) | |
# Ensure the reference is the start of a function | |
if called_func and called_func.start_ea == ref: | |
callee_ea = ref | |
# Recurse to the called function (no callers dict needed) | |
dfs(callee_ea, visited) | |
# Assuming a call instruction targets only one function directly | |
break | |
# Move to the next instruction or data head | |
current_ea = idc.next_head(current_ea, func.end_ea) | |
if current_ea == idaapi.BADADDR: # Check if next_head failed or went past the end | |
break | |
class DecompilerPlugin(idaapi.plugin_t): | |
flags = idaapi.PLUGIN_KEEP | |
comment = "Decompiles all functions reachable from the current function." | |
help = ("This plugin performs a depth-first search from the current function, " | |
"identifies all reachable functions, and decompiles them to a text file.") | |
wanted_name = "Function Decompiler" | |
wanted_hotkey = "" | |
def init(self): | |
print("[*] Function Decompiler plugin initialized.") | |
return idaapi.PLUGIN_OK | |
def run(self, arg): | |
decompile_reachable_functions() | |
def term(self): | |
print("[*] Function Decompiler plugin terminated.") | |
def PLUGIN_ENTRY(): | |
return DecompilerPlugin() |
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
{ | |
"IDAMetadataDescriptorVersion": 1, | |
"plugin": { | |
"name": "comdump", | |
"entryPoint": "comdump.py" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment