|
| 1 | +# Export Decompiled Code for _HoldStrong Namespace (Recursive, one file per function) |
| 2 | +# Ghidra 11.4.1 - Python 3 (Jython bridge via GhidraScript) |
| 3 | +# |
| 4 | +# Output layout: |
| 5 | +# Given function _HoldStrong::A::B(), the decompiled code is written to: |
| 6 | +# <OUTPUT_ROOT>/_HoldStrong/A/B.cpp |
| 7 | +# |
| 8 | +# Usage: |
| 9 | +# 1. Open your binary in Ghidra and run auto-analysis. |
| 10 | +# 2. Go to Window > Script Manager > Run Script, select this file. |
| 11 | +# 3. Files are written under OUTPUT_ROOT (default: a folder named after the |
| 12 | +# program, placed next to this script). |
| 13 | +# |
| 14 | +# @author Claude |
| 15 | +# @category _OPENSHC.TOOLS.DECOMPILATION |
| 16 | +# @keybinding |
| 17 | +# @menupath |
| 18 | +# @toolbar |
| 19 | +# @runtime PyGhidra |
| 20 | + |
| 21 | +import typing |
| 22 | +if typing.TYPE_CHECKING: |
| 23 | + from ghidra.ghidra_builtins import * # type: ignore |
| 24 | + |
| 25 | +import os |
| 26 | +import re |
| 27 | +from ghidra.app.decompiler import DecompInterface, DecompileOptions |
| 28 | +from ghidra.util.task import ConsoleTaskMonitor |
| 29 | +from ghidra.program.model.symbol import SymbolType |
| 30 | + |
| 31 | + |
| 32 | + |
| 33 | +TARGET_NAMESPACE = "_HoldStrong" # Root namespace to search (case-sensitive) |
| 34 | +OUTPUT_ROOT = None # None <script_dir>/<program_name>_decompiled/ |
| 35 | +TIMEOUT_SECONDS = 60 # Per-function decompile timeout |
| 36 | +INCLUDE_SIGNATURE = True # Prepend the function signature as a comment |
| 37 | +RECURSIVE = True # Descend into child namespaces |
| 38 | + |
| 39 | +currentProgram = getCurrentProgram() |
| 40 | + |
| 41 | + |
| 42 | +def get_output_root(): |
| 43 | + """Return (and create if needed) the root output directory.""" |
| 44 | + if OUTPUT_ROOT: |
| 45 | + root = OUTPUT_ROOT |
| 46 | + else: |
| 47 | + #prog_name = re.sub(r'[^\w\-.]', '_', currentProgram.getName()) |
| 48 | + script_dir = os.path.dirname(os.path.abspath( |
| 49 | + getScriptName() if hasattr(__builtins__, 'getScriptName') else __file__)) |
| 50 | + root = os.path.join(script_dir, "decompilation") |
| 51 | + if not os.path.isdir(root): |
| 52 | + os.makedirs(root) |
| 53 | + return root |
| 54 | + |
| 55 | + |
| 56 | +def sanitize_path_component(name): |
| 57 | + """ |
| 58 | + Make a single namespace / function name safe for use as a file-system |
| 59 | + component. Replaces characters that are illegal on Windows/Linux/macOS. |
| 60 | + Operator names like 'operator+=' become 'operator_plus_eq_' etc. |
| 61 | + """ |
| 62 | + replacements = [ |
| 63 | + ('::', '__'), |
| 64 | + ('<', '_lt_'), ('>', '_gt_'), |
| 65 | + ('*', '_star_'), ('/', '_div_'), ('%', '_mod_'), |
| 66 | + ('+', '_plus_'), ('-', '_minus_'), |
| 67 | + ('&', '_amp_'), ('|', '_pipe_'), ('^', '_xor_'), ('~', '_tilde_'), |
| 68 | + ('!', '_not_'), ('=', '_eq_'), |
| 69 | + ('(', '_'), (')', '_'), (',', '_'), (' ', '_'), |
| 70 | + ('[', '_'), (']', '_'), |
| 71 | + ] |
| 72 | + for old, new in replacements: |
| 73 | + name = name.replace(old, new) |
| 74 | + name = re.sub(r'[^\w\-.]', '_', name) |
| 75 | + name = re.sub(r'_+', '_', name).strip('_') |
| 76 | + return name or '_unnamed_' |
| 77 | + |
| 78 | + |
| 79 | +def ns_path_to_file_path(output_root, ns_qualified_name, func_name): |
| 80 | + """ |
| 81 | + Convert a fully-qualified namespace path and function name into a file path. |
| 82 | +
|
| 83 | + Example: |
| 84 | + ns_qualified_name = "_HoldStrong::A" |
| 85 | + func_name = "B" |
| 86 | + <output_root>/_HoldStrong/A/B.cpp |
| 87 | + """ |
| 88 | + # Split on '::' to get each namespace component |
| 89 | + parts = [p for p in ns_qualified_name.split('::') if p] |
| 90 | + if parts[0] == "_HoldStrong": |
| 91 | + parts[0] = "OpenSHC" |
| 92 | + parts.append(func_name) # function name becomes the filename |
| 93 | + safe_parts = [sanitize_path_component(p) for p in parts] |
| 94 | + dir_parts = safe_parts[:-1] |
| 95 | + file_part = safe_parts[-1] + '.cpp' |
| 96 | + out_dir = os.path.join(output_root, *dir_parts) if dir_parts else output_root |
| 97 | + if not os.path.isdir(out_dir): |
| 98 | + os.makedirs(out_dir) |
| 99 | + return os.path.join(out_dir, file_part) |
| 100 | + |
| 101 | + |
| 102 | +def init_decompiler(): |
| 103 | + """Initialise and open a DecompInterface for the current program.""" |
| 104 | + iface = DecompInterface() |
| 105 | + opts = DecompileOptions() |
| 106 | + iface.setOptions(opts) |
| 107 | + iface.openProgram(currentProgram) |
| 108 | + return iface |
| 109 | + |
| 110 | + |
| 111 | +def decompile_function(func, iface, monitor): |
| 112 | + """Return (code_str, None) on success or (None, error_str) on failure.""" |
| 113 | + result = iface.decompileFunction(func, TIMEOUT_SECONDS, monitor) |
| 114 | + if result is None or not result.decompileCompleted(): |
| 115 | + err = result.getErrorMessage() if result else "unknown error" |
| 116 | + return None, "/* ERROR decompiling {}: {} */\n".format(func.getName(), err) |
| 117 | + markup = result.getDecompiledFunction().getC() # .getCCodeMarkup() |
| 118 | + if markup is None: |
| 119 | + return None, "/* ERROR: no C markup for {} */\n".format(func.getName()) |
| 120 | + return str(markup).replace("_HoldStrong", "OpenSHC"), None |
| 121 | + |
| 122 | + |
| 123 | +def collect_functions_in_namespace(ns): |
| 124 | + """ |
| 125 | + Recursively collect every Function symbol inside *ns* and its descendants. |
| 126 | +
|
| 127 | + Returns a list of (ns_qualified_name, Function) tuples where |
| 128 | + ns_qualified_name is the namespace the function belongs to (NOT including |
| 129 | + the function name itself), e.g. "_HoldStrong::A". |
| 130 | + """ |
| 131 | + results = [] |
| 132 | + sym_table = currentProgram.getSymbolTable() |
| 133 | + ns_name = ns.getName(True) # fully-qualified, e.g. "_HoldStrong::A" |
| 134 | + |
| 135 | + children = sym_table.getChildren(ns.getSymbol()) |
| 136 | + while children.hasNext(): |
| 137 | + sym = children.next() |
| 138 | + sym_type = sym.getSymbolType() |
| 139 | + |
| 140 | + if sym_type == SymbolType.FUNCTION: |
| 141 | + func = getFunctionAt(sym.getAddress()) |
| 142 | + if func is not None: |
| 143 | + results.append((ns_name, func)) |
| 144 | + |
| 145 | + elif RECURSIVE and sym_type in (SymbolType.NAMESPACE, SymbolType.CLASS): |
| 146 | + child_ns = sym.getObject() |
| 147 | + if child_ns is not None: |
| 148 | + results.extend(collect_functions_in_namespace(child_ns)) |
| 149 | + |
| 150 | + return results |
| 151 | + |
| 152 | + |
| 153 | +def find_root_namespace(name): |
| 154 | + """ |
| 155 | + Find the first namespace (or class) whose simple name matches *name*. |
| 156 | + Returns the Namespace object, or None if not found. |
| 157 | + """ |
| 158 | + sym_table = currentProgram.getSymbolTable() |
| 159 | + it = sym_table.getSymbols(name) |
| 160 | + while it.hasNext(): |
| 161 | + sym = it.next() |
| 162 | + if sym.getSymbolType() in (SymbolType.NAMESPACE, SymbolType.CLASS): |
| 163 | + return sym.getObject() |
| 164 | + return None |
| 165 | + |
| 166 | + |
| 167 | + |
| 168 | +def run(): |
| 169 | + monitor = ConsoleTaskMonitor() |
| 170 | + |
| 171 | + print("[*] Searching for namespace: {}".format(TARGET_NAMESPACE)) |
| 172 | + root_ns = find_root_namespace(TARGET_NAMESPACE) |
| 173 | + if root_ns is None: |
| 174 | + msg = "Namespace '{}' not found in program.".format(TARGET_NAMESPACE) |
| 175 | + print("[!] " + msg) |
| 176 | + popup(msg) |
| 177 | + return |
| 178 | + |
| 179 | + print("[*] Found namespace: {}".format(root_ns.getName(True))) |
| 180 | + print("[*] Collecting functions{}...".format( |
| 181 | + " recursively" if RECURSIVE else "")) |
| 182 | + |
| 183 | + entries = collect_functions_in_namespace(root_ns) |
| 184 | + if not entries: |
| 185 | + msg = "No functions found under '{}'.".format(TARGET_NAMESPACE) |
| 186 | + print("[!] " + msg) |
| 187 | + popup(msg) |
| 188 | + return |
| 189 | + |
| 190 | + print("[*] Found {} function(s). Decompiling...".format(len(entries))) |
| 191 | + |
| 192 | + iface = init_decompiler() |
| 193 | + output_root = get_output_root() |
| 194 | + error_count = 0 |
| 195 | + written = [] |
| 196 | + |
| 197 | + for idx, (ns_qualified, func) in enumerate(entries, 1): |
| 198 | + addr = func.getEntryPoint() |
| 199 | + func_name = func.getName() |
| 200 | + out_path = ns_path_to_file_path(output_root, ns_qualified, func_name) |
| 201 | + |
| 202 | + print(" [{}/{}] {}::{} @{}".format( |
| 203 | + idx, len(entries), ns_qualified, func_name, addr)) |
| 204 | + print(" -> {}".format(out_path)) |
| 205 | + |
| 206 | + code, err = decompile_function(func, iface, monitor) |
| 207 | + |
| 208 | + with open(out_path, 'w', encoding='UTF-8') as fh: |
| 209 | + fh.write("// {}\n".format("=" * 76)) |
| 210 | + fh.write("// Program : {}\n".format(currentProgram.getName())) |
| 211 | + fh.write("// Namespace : {}\n".format(ns_qualified)) |
| 212 | + fh.write("// Function : {}\n".format(func_name)) |
| 213 | + fh.write("// Address : {}\n".format(addr)) |
| 214 | + if INCLUDE_SIGNATURE: |
| 215 | + fh.write("// Signature : {}\n".format(func.getSignature())) |
| 216 | + fh.write("// {}\n\n".format("=" * 76)) |
| 217 | + |
| 218 | + if err is not None: |
| 219 | + fh.write(err + "\n") |
| 220 | + error_count += 1 |
| 221 | + print(" [!] " + err.strip()) |
| 222 | + else: |
| 223 | + if not code: |
| 224 | + raise Exception("impossible situation") |
| 225 | + fh.write(code) |
| 226 | + fh.write("\n") |
| 227 | + |
| 228 | + written.append(out_path) |
| 229 | + |
| 230 | + iface.dispose() |
| 231 | + |
| 232 | + summary = ( |
| 233 | + "[+] Done. {} file(s) written, {} error(s).\n" |
| 234 | + " Output root: {}" |
| 235 | + ).format(len(written), error_count, output_root) |
| 236 | + print(summary) |
| 237 | + popup(summary) |
| 238 | + |
| 239 | +if __name__ == "__main__": |
| 240 | + # Ghidra calls run() automatically when the script is executed. |
| 241 | + run() |
0 commit comments