Skip to content

Instantly share code, notes, and snippets.

@19h
Created April 3, 2025 17:50
Show Gist options
  • Save 19h/12ade37babcf8ee9498f42643a9f31d1 to your computer and use it in GitHub Desktop.
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
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()
{
"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