diff --git a/.claude/skills/ghidra-cli/SKILL.md b/.claude/skills/ghidra-cli/SKILL.md index 35c26e5..8052353 100644 --- a/.claude/skills/ghidra-cli/SKILL.md +++ b/.claude/skills/ghidra-cli/SKILL.md @@ -100,11 +100,19 @@ ghidra function decompile TARGET [QUERY_OPTS] ghidra function disasm TARGET [QUERY_OPTS] ghidra function calls TARGET [QUERY_OPTS] # outgoing calls ghidra function xrefs TARGET [QUERY_OPTS] # incoming references -ghidra function rename OLD NEW [--project P] [--program PROG] -ghidra function create ADDRESS [NAME] [--project P] [--program PROG] +ghidra function set-signature TARGET 'SIGNATURE' # aliases: set-sig, signature +ghidra function create ADDRESS [NAME] ghidra function delete TARGET [QUERY_OPTS] ``` +`set-signature` parses a C-style function prototype and applies it. Supports `__thiscall`, +`__cdecl`, `__stdcall`, `__fastcall` calling conventions. The function is also renamed to match +the signature. Does NOT handle namespaces — use `symbol rename` for that. + +```bash +ghidra function set-signature 0x401000 'void __thiscall Update(float dt, int flags)' +``` + ### Top-level Shortcuts ```bash @@ -124,9 +132,20 @@ ghidra strings refs STRING [QUERY_OPTS] # xrefs to string ```bash ghidra symbol list [QUERY_OPTS] # aliases: sym, symbols ghidra symbol get NAME [QUERY_OPTS] -ghidra symbol create ADDRESS NAME [--project P] [--program PROG] +ghidra symbol create ADDRESS NAME ghidra symbol delete NAME [QUERY_OPTS] -ghidra symbol rename OLD NEW [--project P] [--program PROG] +ghidra symbol rename TARGET NEW_NAME [--namespace NS] +``` + +`symbol rename` accepts an address or name as TARGET. NEW_NAME supports `Namespace::Name` +syntax to set the namespace. Use `--namespace NS` as an alternative, or `--namespace ''` to +move to the global namespace. + +```bash +ghidra symbol rename 0x401000 MyClass::MyMethod # rename + set namespace +ghidra symbol rename 0x401000 MyMethod --namespace MyClass # same result +ghidra symbol rename 0x401000 MyMethod --namespace '' # move to global +ghidra symbol rename 0x401000 NewName # rename only, keep namespace ``` ### Memory Operations @@ -155,6 +174,25 @@ ghidra type list [QUERY_OPTS] # alias: types ghidra type get NAME [QUERY_OPTS] ghidra type create DEFINITION [--project P] [--program PROG] ghidra type apply ADDRESS TYPE_NAME [--project P] [--program PROG] +ghidra type import-c 'C_CODE' [--category PATH] [--project P] [--program PROG] # aliases: import, parse-c +``` + +`import-c` parses C type definitions (structs, unions, enums, typedefs including function +pointers) and imports them into the program's data type manager. Supports bitfields and struct +inheritance (`: Parent`). Existing types with the same name are overwritten. + +Use `--category` to organize types into Ghidra data type categories instead of the root `/`. + +```bash +# Import types to root +ghidra type import-c 'struct Vec3 { float x; float y; float z; };' + +# Import into a category (ideal for vtables, class definitions) +ghidra type import-c --category /CTimer \ + 'struct CTimer; + typedef void (*CTimer_dtor)(CTimer *this, short flags); + struct CTimer_vtable { void *rtti0; void *rtti1; CTimer_dtor dtor; }; + struct CTimer { CTimer_vtable *vtable; int state; float timer; };' ``` ### Comment Operations @@ -382,7 +420,13 @@ ghidra graph callers suspicious_func --depth 3 --project analysis ghidra x-ref to 0x401000 --project analysis ghidra function disasm 0x401000 --project analysis -# 5. Patch +# 5. Type recovery +ghidra type import-c --category /MyClass 'struct MyClass { void* vtable; int state; float timer; };' +ghidra symbol rename 0x401000 MyClass::Update # apply namespace +ghidra function set-signature 0x401000 'void __thiscall Update(float dt, int flags)' +ghidra decompile 0x401000 # verify improved output + +# 6. Patch ghidra patch nop 0x401234 --count 3 --project analysis ghidra patch export -o patched.exe --project analysis ``` diff --git a/README.md b/README.md index a8f10d2..139d976 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ A high-performance Rust CLI for automating Ghidra reverse engineering tasks, des - **Auto-start bridge** - Import/analyze commands automatically start the bridge - **Fast queries** - Sub-second response times with Ghidra kept in memory - **Comprehensive analysis** - Functions, symbols, types, strings, cross-references +- **Type recovery** - Create data types with C syntax (structs, enums, typedefs) +- **Function prototypes** - Adjust function signatures and calling conventions +- **Symbol management** - Rename symbols and organize namespaces - **Binary patching** - Modify bytes, NOP instructions, export patches - **Call graphs** - Generate caller/callee graphs, export to DOT format - **Search capabilities** - Find strings, bytes, functions, crypto patterns @@ -99,15 +102,20 @@ ghidra function list # List all functions ghidra function list --filter "size > 100" # Filter by size ghidra decompile # Decompile function ghidra disasm
--instructions 20 # Disassemble instructions +ghidra function set-signature 'void __thiscall Update(float dt, int flags)' # Set function prototype ``` ### Symbols & Types ```bash -ghidra symbol list # List symbols -ghidra symbol create # Create symbol -ghidra symbol rename # Rename symbol -ghidra type list # List data types -ghidra type get # Get type details +ghidra symbol list # List symbols +ghidra symbol create # Create symbol +ghidra symbol rename # Rename symbol (address or name) +ghidra symbol rename Ns::Name # Rename + set namespace +ghidra symbol rename Name --namespace '' # Move to global namespace +ghidra type list # List data types +ghidra type get # Get type details +ghidra type import-c 'struct Vec3 { float x; float y; float z; };' +ghidra type import-c --category /Player 'struct Player { Vec3 pos; int hp; };' ``` ### Cross-References @@ -251,8 +259,12 @@ Example workflow with an AI agent: 2. `ghidra find interesting` - AI analyzes suspicious patterns 3. `ghidra decompile ` - AI examines specific functions 4. `ghidra x-ref to ` - AI traces data flow -5. `ghidra patch nop ` - AI patches anti-debug code -6. `ghidra patch export -o patched.bin` - Export patched binary +5. `ghidra type import-c --category /MyClass 'struct MyClass { ... };'` - AI defines recovered types +6. `ghidra symbol rename MyClass::Method` - AI assigns names and namespaces +7. `ghidra function set-signature 'void __thiscall Method(int arg)'` - AI applies function prototypes +8. `ghidra decompile ` - AI iterates on decompilation output +9. `ghidra patch nop ` - AI patches anti-debug code +10. `ghidra patch export -o patched.bin` - Export patched binary ## Troubleshooting diff --git a/src/cli.rs b/src/cli.rs index 551f610..3d6ae01 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -304,6 +304,9 @@ pub enum FunctionCommands { XRefs(FunctionGetArgs), /// Rename function Rename(RenameArgs), + /// Update function signature + #[command(alias = "set-sig", alias = "signature")] + SetSignature(SetSignatureArgs), /// Create function Create(CreateFunctionArgs), /// Delete function @@ -333,8 +336,25 @@ impl FunctionGetArgs { #[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct RenameArgs { - pub old_name: String, + /// Symbol address or name + pub target: String, + /// New name ('Name' or 'Namespace::Name') pub new_name: String, + /// Explicit namespace (overrides '::' parsing; empty string = global) + #[arg(long)] + pub namespace: Option, + #[arg(long)] + pub program: Option, + #[arg(long)] + pub project: Option, +} + +#[derive(Args, Clone, Serialize, Deserialize, Debug)] +pub struct SetSignatureArgs { + /// Function address or name + pub target: String, + /// C-style function signature (e.g. 'void foo(int a, float b)') + pub signature: String, #[arg(long)] pub program: Option, #[arg(long)] @@ -479,6 +499,9 @@ pub enum TypeCommands { Create(CreateTypeArgs), /// Apply type to address Apply(ApplyTypeArgs), + /// Import C type definitions + #[command(alias = "import", alias = "parse-c")] + ImportC(ImportCArgs), } #[derive(Args, Clone, Serialize, Deserialize, Debug)] @@ -507,6 +530,19 @@ pub struct ApplyTypeArgs { pub project: Option, } +#[derive(Args, Clone, Serialize, Deserialize, Debug)] +pub struct ImportCArgs { + /// C code containing type definitions + pub code: String, + /// Category path to store types in + #[arg(long)] + pub category: Option, + #[arg(long)] + pub program: Option, + #[arg(long)] + pub project: Option, +} + #[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum CommentCommands { /// List all comments diff --git a/src/ghidra/scripts/GhidraCliBridge.java b/src/ghidra/scripts/GhidraCliBridge.java index 3669ad4..db5c8a8 100644 --- a/src/ghidra/scripts/GhidraCliBridge.java +++ b/src/ghidra/scripts/GhidraCliBridge.java @@ -25,6 +25,10 @@ import ghidra.program.model.mem.Memory; import ghidra.program.model.mem.MemoryBlock; import ghidra.program.model.symbol.*; +import ghidra.app.util.cparser.C.CParser; +import ghidra.app.util.cparser.C.ParseException; +import ghidra.app.cmd.function.ApplyFunctionSignatureCmd; +import ghidra.app.cmd.function.FunctionRenameOption; import ghidra.util.task.ConsoleTaskMonitor; import ghidra.util.task.TaskMonitor; @@ -188,11 +192,6 @@ private JsonObject dispatchCommand(String command, JsonObject args) { switch (command) { case "ping": return handlePing(); case "program_info": return handleProgramInfo(); - case "list_functions": return handleListFunctions(args); - case "get_function": return handleGetFunction(args); - case "rename_function": return handleRenameFunction(args); - case "create_function": return handleCreateFunction(args); - case "delete_function": return handleDeleteFunction(args); case "decompile": return handleDecompile(args); case "list_strings": return handleListStrings(args); case "list_imports": return handleListImports(); @@ -220,11 +219,18 @@ private JsonObject dispatchCommand(String command, JsonObject args) { case "symbol_create": return handleSymbolCreate(args); case "symbol_delete": return handleSymbolDelete(args); case "symbol_rename": return handleSymbolRename(args); + // Function commands + case "function_list": return handleFunctionList(args); + case "function_get": return handleFunctionGet(args); + case "function_create": return handleFunctionCreate(args); + case "function_delete": return handleFunctionDelete(args); + case "function_set_signature": return handleFunctionSetSignature(args); // Type commands case "type_list": return handleTypeList(args); case "type_get": return handleTypeGet(args); case "type_create": return handleTypeCreate(args); case "type_apply": return handleTypeApply(args); + case "type_import_c": return handleTypeImportC(args); // Comment commands case "comment_list": return handleCommentList(args); case "comment_get": return handleCommentGet(args); @@ -424,7 +430,7 @@ private JsonObject handleProgramInfo() { return result; } - private JsonObject handleListFunctions(JsonObject args) { + private JsonObject handleFunctionList(JsonObject args) { if (currentProgram == null) { return errorResult("No program loaded"); } @@ -581,7 +587,7 @@ private int levenshteinDistance(String a, String b) { return dp[n][m]; } - private JsonObject handleGetFunction(JsonObject args) { + private JsonObject handleFunctionGet(JsonObject args) { if (currentProgram == null) return errorResult("No program loaded"); String target = getArgString(args, "address"); @@ -601,43 +607,7 @@ private JsonObject handleGetFunction(JsonObject args) { return functionToJson(func); } - private JsonObject handleRenameFunction(JsonObject args) { - if (currentProgram == null) return errorResult("No program loaded"); - - String oldTarget = getArgString(args, "old_name"); - String newName = getArgString(args, "new_name"); - if (oldTarget == null || newName == null || oldTarget.isEmpty() || newName.isEmpty()) { - return errorResult("old_name and new_name required"); - } - - try { - Function func = findFunctionByNameOrAddress(oldTarget); - if (func == null) { - return errorResult(buildFunctionTargetHint(oldTarget)); - } - - int txId = currentProgram.startTransaction("Rename function"); - try { - String oldName = func.getName(); - func.setName(newName, SourceType.USER_DEFINED); - currentProgram.endTransaction(txId, true); - - JsonObject result = new JsonObject(); - result.addProperty("status", "renamed"); - result.addProperty("old_name", oldName); - result.addProperty("new_name", newName); - result.addProperty("address", func.getEntryPoint().toString()); - return result; - } catch (Exception e) { - currentProgram.endTransaction(txId, false); - throw e; - } - } catch (Exception e) { - return errorResult("Failed to rename function: " + e.getMessage()); - } - } - - private JsonObject handleCreateFunction(JsonObject args) { + private JsonObject handleFunctionCreate(JsonObject args) { if (currentProgram == null) return errorResult("No program loaded"); String target = getArgString(args, "address"); @@ -684,7 +654,7 @@ private JsonObject handleCreateFunction(JsonObject args) { } } - private JsonObject handleDeleteFunction(JsonObject args) { + private JsonObject handleFunctionDelete(JsonObject args) { if (currentProgram == null) return errorResult("No program loaded"); String target = getArgString(args, "address"); @@ -1825,26 +1795,17 @@ private JsonObject handleSymbolCreate(JsonObject args) { private JsonObject handleSymbolDelete(JsonObject args) { if (currentProgram == null) return errorResult("No program loaded"); - String name = getArgString(args, "name"); - if (name == null) return errorResult("Symbol name required"); + String target = getArgString(args, "target"); + if (target == null) return errorResult("Symbol target required"); try { - SymbolTable symbolTable = currentProgram.getSymbolTable(); - SymbolIterator syms = symbolTable.getSymbols(name); - List toDelete = new ArrayList<>(); - while (syms.hasNext()) { - toDelete.add(syms.next()); - } - - if (toDelete.isEmpty()) { - return errorResult("Symbol not found: " + name); - } + Symbol symbol = findUniqueSymbolByNameOrAddress(target); + Address symbolAddr = symbol.getAddress(); + String deletedName = symbol.getName(true); int txId = currentProgram.startTransaction("Delete symbol"); try { - for (Symbol s : toDelete) { - s.delete(); - } + symbol.delete(); currentProgram.endTransaction(txId, true); } catch (Exception e) { currentProgram.endTransaction(txId, false); @@ -1853,8 +1814,15 @@ private JsonObject handleSymbolDelete(JsonObject args) { JsonObject result = new JsonObject(); result.addProperty("status", "deleted"); - result.addProperty("name", name); + result.addProperty("name", deletedName); + if (symbolAddr != null) { + result.addProperty("address", symbolAddr.toString()); + } else { + result.add("address", JsonNull.INSTANCE); + } return result; + } catch (IllegalArgumentException e) { + return errorResult(e.getMessage()); } catch (Exception e) { return errorResult("Failed to delete symbol: " + e.getMessage()); } @@ -1863,28 +1831,53 @@ private JsonObject handleSymbolDelete(JsonObject args) { private JsonObject handleSymbolRename(JsonObject args) { if (currentProgram == null) return errorResult("No program loaded"); - String oldName = getArgString(args, "old_name"); + String target = getArgString(args, "target"); String newName = getArgString(args, "new_name"); - if (oldName == null || newName == null) { - return errorResult("old_name and new_name required"); + if (target == null) return errorResult("Symbol target required"); + if (newName == null || newName.trim().isEmpty()) { + return errorResult("New name required"); } - try { - SymbolTable symbolTable = currentProgram.getSymbolTable(); - SymbolIterator syms = symbolTable.getSymbols(oldName); - List toRename = new ArrayList<>(); - while (syms.hasNext()) { - toRename.add(syms.next()); - } + // Check for explicit --namespace arg (may be null = not provided, or "" = global) + String namespaceArg = getArgString(args, "namespace"); - if (toRename.isEmpty()) { - return errorResult("Symbol not found: " + oldName); + try { + Symbol symbol = findUniqueSymbolByNameOrAddress(target); + Address symbolAddr = symbol.getAddress(); + if (symbolAddr == null) { + return errorResult("Symbol has no address: " + target); + } + + String oldName = symbol.getName(true); // include namespace + String simpleName = newName; + String namespacePath = null; + + // Determine namespace: --namespace flag takes precedence over :: parsing + if (namespaceArg != null) { + // Explicit namespace provided (empty string = global namespace) + namespacePath = namespaceArg; + // If new_name also has ::, extract just the simple name + int lastColon = newName.lastIndexOf("::"); + if (lastColon >= 0) { + simpleName = newName.substring(lastColon + 2); + } + } else { + // Parse :: from new_name + int lastColon = newName.lastIndexOf("::"); + if (lastColon >= 0) { + namespacePath = newName.substring(0, lastColon); + simpleName = newName.substring(lastColon + 2); + } } int txId = currentProgram.startTransaction("Rename symbol"); try { - for (Symbol s : toRename) { - s.setName(newName, SourceType.USER_DEFINED); + if (namespacePath != null) { + Namespace ns = createNamespaceHierarchy(namespacePath); + symbol.setNameAndNamespace( + simpleName, ns, SourceType.USER_DEFINED); + } else { + symbol.setName(simpleName, SourceType.USER_DEFINED); } currentProgram.endTransaction(txId, true); } catch (Exception e) { @@ -1894,14 +1887,46 @@ private JsonObject handleSymbolRename(JsonObject args) { JsonObject result = new JsonObject(); result.addProperty("status", "renamed"); + result.addProperty("address", symbolAddr.toString()); result.addProperty("old_name", oldName); - result.addProperty("new_name", newName); + result.addProperty("new_name", symbol.getName(true)); + result.addProperty("namespace", symbol.getParentNamespace().getName(true)); + result.addProperty("type", symbol.getSymbolType().toString()); return result; + } catch (IllegalArgumentException e) { + return errorResult(e.getMessage()); } catch (Exception e) { - return errorResult("Failed to rename symbol: " + e.getMessage()); + return errorResult("Failed to rename: " + e.getMessage()); } } + /** + * Create a namespace hierarchy from a "::" separated path. + * E.g., "A" creates a single namespace, + * "A::B::C" creates A -> B -> C hierarchy. + * Empty/null path returns the global namespace. + */ + private Namespace createNamespaceHierarchy(String path) throws Exception { + if (path == null || path.isEmpty()) { + return currentProgram.getGlobalNamespace(); + } + SymbolTable symbolTable = currentProgram.getSymbolTable(); + String[] parts = path.split("::"); + Namespace parent = currentProgram.getGlobalNamespace(); + + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + Namespace existing = symbolTable.getNamespace(part, parent); + if (existing != null) { + parent = existing; + } else { + parent = symbolTable.createNameSpace(parent, part, SourceType.USER_DEFINED); + } + } + return parent; + } + // --- Type Handlers --- private JsonObject handleTypeList(JsonObject args) { @@ -2075,6 +2100,263 @@ private JsonObject handleTypeApply(JsonObject args) { } } + // --- Import C Types Handler --- + + private JsonObject handleTypeImportC(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String code = getArgString(args, "code"); + if (code == null || code.trim().isEmpty()) { + return errorResult("C code required"); + } + + String categoryPath = getArgString(args, "category"); + + try { + DataTypeManager dtm = currentProgram.getDataTypeManager(); + + // Ensure trailing semicolon for CParser + String processedCode = code.trim(); + if (!processedCode.endsWith(";")) { + processedCode += ";"; + } + + int txId = currentProgram.startTransaction("Import C types"); + try { + // Parse into program DTM + CParser parser = new CParser(dtm, true, + new DataTypeManager[] { dtm }); + parser.parse(processedCode); + String parseMessages = parser.getParseMessages(); + + // Collect all explicitly defined type names from the parser + Set definedNames = new HashSet<>(); + definedNames.addAll(parser.getComposites().keySet()); + definedNames.addAll(parser.getEnums().keySet()); + definedNames.addAll(parser.getTypes().keySet()); + definedNames.addAll(parser.getFunctions().keySet()); + Set parsedTypes = new HashSet<>(); + parsedTypes.addAll(parser.getComposites().values()); + parsedTypes.addAll(parser.getEnums().values()); + parsedTypes.addAll(parser.getTypes().values()); + parsedTypes.addAll(parser.getFunctions().values()); + + // If --category specified, move defined types to target category + CategoryPath lookupPath = CategoryPath.ROOT; + if (categoryPath != null) { + String normalizedPath = categoryPath.startsWith("/") + ? categoryPath : "/" + categoryPath; + CategoryPath targetPath = new CategoryPath(normalizedPath); + Category targetCat = dtm.createCategory(targetPath); + + // Move only datatypes touched by this parse operation. + for (DataType dt : parsedTypes) { + if (!isUserFacingDataType(dt)) continue; + if (dt.getCategoryPath().equals(targetPath)) continue; + targetCat.moveDataType(dt, DataTypeConflictHandler.REPLACE_HANDLER); + } + lookupPath = targetPath; + } + + currentProgram.endTransaction(txId, true); + + // Build response with all imported types + JsonArray typesArray = new JsonArray(); + for (String name : definedNames) { + DataType best = findBestParsedDataType(name, parsedTypes, lookupPath); + if (best == null) { + best = findBestDataType(dtm, name, lookupPath); + } + if (best != null) { + JsonObject typeInfo = new JsonObject(); + typeInfo.addProperty("name", best.getName()); + typeInfo.addProperty("path", best.getPathName()); + typeInfo.addProperty("size", best.getLength()); + typeInfo.addProperty("category", + best.getCategoryPath().toString()); + typesArray.add(typeInfo); + } + } + + JsonObject response = new JsonObject(); + response.addProperty("status", "imported"); + response.add("types", typesArray); + if (parseMessages != null && !parseMessages.trim().isEmpty()) { + response.addProperty("messages", parseMessages.trim()); + } + return response; + + } catch (Exception e) { + currentProgram.endTransaction(txId, false); + throw e; + } + + } catch (ParseException pe) { + return errorResult("C parse error: " + pe.getMessage()); + } catch (Exception e) { + return errorResult("Failed to import C types: " + e.getMessage()); + } + } + + // --- Function Set Signature Handler --- + + private JsonObject handleFunctionSetSignature(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String addrStr = getArgString(args, "address"); + String signature = getArgString(args, "signature"); + if (addrStr == null) return errorResult("Function address required"); + if (signature == null || signature.trim().isEmpty()) { + return errorResult("Signature string required"); + } + + try { + // Step 1: Resolve the function + Address addr = resolveAddress(addrStr); + if (addr == null) return errorResult("Cannot resolve address: " + addrStr); + + FunctionManager fm = currentProgram.getFunctionManager(); + Function func = fm.getFunctionAt(addr); + if (func == null) { + func = fm.getFunctionContaining(addr); + } + if (func == null) return errorResult("No function at address: " + addrStr); + + // Step 2: Pre-process the signature + // CParser (C.jj:1194-1204) defines tokens for __cdecl, __stdcall, __fastcall, + // __vectorcall, __rustcall, __pascal — but NOT __thiscall. + // We mirror applyFunctionQualifiers() (C.jj:654-696): strip __thiscall from + // the string and apply it separately via setCallingConvention(). + String processedSig = signature.trim(); + boolean isThiscall = false; + if (processedSig.contains("__thiscall")) { + isThiscall = true; + processedSig = processedSig.replaceAll("\\b__thiscall\\b", "").replaceAll("\\s+", " ").trim(); + } + + // Ensure trailing semicolon for CParser + if (!processedSig.endsWith(";")) { + processedSig += ";"; + } + + // Step 3: Parse with CParser + DataTypeManager dtm = currentProgram.getDataTypeManager(); + CParser parser = new CParser(dtm, false, new DataTypeManager[] { dtm }); + DataType parsed = parser.parse(processedSig); + + if (!(parsed instanceof FunctionDefinition)) { + return errorResult("Could not parse as function signature: " + signature); + } + FunctionDefinition funcDef = (FunctionDefinition) parsed; + + // Step 4: Apply within a transaction + int txId = currentProgram.startTransaction("Set function signature"); + try { + ApplyFunctionSignatureCmd cmd = new ApplyFunctionSignatureCmd( + func.getEntryPoint(), + funcDef, + SourceType.USER_DEFINED, + false, // don't preserve calling convention + false, // applyEmptyComposites + DataTypeConflictHandler.REPLACE_HANDLER, + FunctionRenameOption.RENAME + ); + boolean applied = cmd.applyTo(currentProgram); + if (!applied) { + currentProgram.endTransaction(txId, false); + return errorResult("Failed to apply signature: " + cmd.getStatusMsg()); + } + + // Re-fetch function after signature change + func = fm.getFunctionAt(func.getEntryPoint()); + + // Set __thiscall calling convention (mirrors C.jj applyFunctionQualifiers()) + if (isThiscall && func != null) { + func.setCallingConvention("__thiscall"); + } + + currentProgram.endTransaction(txId, true); + } catch (Exception e) { + currentProgram.endTransaction(txId, false); + throw e; + } + + // Build response + func = fm.getFunctionAt(addr); + JsonObject result = new JsonObject(); + result.addProperty("status", "updated"); + result.addProperty("address", addr.toString()); + if (func != null) { + result.addProperty("name", func.getName()); + result.addProperty("namespace", func.getParentNamespace().getName(true)); + result.addProperty("calling_convention", func.getCallingConventionName()); + result.addProperty("signature", func.getPrototypeString(false, false)); + result.addProperty("parameter_count", func.getParameterCount()); + } + return result; + + } catch (ParseException pe) { + return errorResult("Signature parse error: " + pe.getMessage()); + } catch (Exception e) { + return errorResult("Failed to set signature: " + e.getMessage()); + } + } + + /** + * Find all user-facing data types matching a name, excluding internal + * /functions/ category types (FunctionDefinition internals). + */ + private List findUserDataTypes(DataTypeManager dtm, String name) { + List all = new ArrayList<>(); + dtm.findDataTypes(name, all); + List result = new ArrayList<>(); + for (DataType dt : all) { + if (!dt.getName().equals(name)) continue; + if (!isUserFacingDataType(dt)) continue; + result.add(dt); + } + return result; + } + + private boolean isUserFacingDataType(DataType dt) { + if (dt == null) return false; + return !dt.getCategoryPath().toString().equals("/functions"); + } + + private DataType findBestParsedDataType(String name, Set parsed, + CategoryPath preferred) { + DataType best = null; + for (DataType dt : parsed) { + if (!isUserFacingDataType(dt)) continue; + if (!dt.getName().equals(name)) continue; + if (dt.getCategoryPath().equals(preferred)) { + return dt; + } + if (best == null) { + best = dt; + } + } + return best; + } + + /** + * Find the best user-facing data type for a name, preferring the given + * category path. + */ + private DataType findBestDataType(DataTypeManager dtm, String name, + CategoryPath preferred) { + DataType best = null; + for (DataType dt : findUserDataTypes(dtm, name)) { + if (dt.getCategoryPath().equals(preferred)) { + return dt; // exact match + } + if (best == null) { + best = dt; + } + } + return best; + } + // --- Comment Handlers --- private int resolveCommentType(String typeStr) { @@ -2308,6 +2590,45 @@ private JsonObject handleGraphCalls(JsonObject args) { return result; } + private Symbol findUniqueSymbolByNameOrAddress(String target) { + if (currentProgram == null || target == null || target.isEmpty()) { + throw new IllegalArgumentException("Symbol target required"); + } + + SymbolTable symbolTable = currentProgram.getSymbolTable(); + + // Prefer exact name matches so non-primary labels are renamed correctly. + List namedMatches = new ArrayList<>(); + SymbolIterator syms = symbolTable.getSymbols(target); + while (syms.hasNext()) { + Symbol s = syms.next(); + Address symAddr = s.getAddress(); + if (symAddr != null && !symAddr.isExternalAddress()) { + namedMatches.add(s); + } + } + + if (namedMatches.size() == 1) { + return namedMatches.get(0); + } + if (namedMatches.size() > 1) { + throw new IllegalArgumentException( + "Ambiguous symbol target '" + target + "' (" + namedMatches.size() + + " matches). Use an address." + ); + } + + Address addr = resolveAddress(target); + if (addr == null) { + throw new IllegalArgumentException("Symbol not found: " + target); + } + Symbol symbol = symbolTable.getPrimarySymbol(addr); + if (symbol == null) { + throw new IllegalArgumentException("No symbol at address: " + target); + } + return symbol; + } + private Function findFunctionByNameOrAddress(String nameOrAddr) { if (currentProgram == null || nameOrAddr == null || nameOrAddr.isEmpty()) { return null; diff --git a/src/ipc/client.rs b/src/ipc/client.rs index 654a0c2..d91e21a 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -110,7 +110,7 @@ impl BridgeClient { filter: Option, ) -> Result { self.send_command( - "list_functions", + "function_list", Some(json!({"limit": limit, "filter": filter})), ) } @@ -199,14 +199,19 @@ impl BridgeClient { ) } - pub fn symbol_delete(&self, name: &str) -> Result { - self.send_command("symbol_delete", Some(json!({"name": name}))) + pub fn symbol_delete(&self, target: &str) -> Result { + self.send_command("symbol_delete", Some(json!({"target": target}))) } - pub fn symbol_rename(&self, old_name: &str, new_name: &str) -> Result { + pub fn symbol_rename( + &self, + target: &str, + new_name: &str, + namespace: Option<&str>, + ) -> Result { self.send_command( "symbol_rename", - Some(json!({"old_name": old_name, "new_name": new_name})), + Some(json!({"target": target, "new_name": new_name, "namespace": namespace})), ) } @@ -229,6 +234,24 @@ impl BridgeClient { ) } + pub fn type_import_c(&self, code: &str, category: Option<&str>) -> Result { + self.send_command( + "type_import_c", + Some(json!({"code": code, "category": category})), + ) + } + + pub fn function_set_signature( + &self, + target: &str, + signature: &str, + ) -> Result { + self.send_command( + "function_set_signature", + Some(json!({"address": target, "signature": signature})), + ) + } + pub fn comment_list(&self, limit: Option, filter: Option<&str>) -> Result { self.send_command("comment_list", Some(json!({"limit": limit, "filter": filter}))) } diff --git a/src/main.rs b/src/main.rs index 5dfe5b2..31d4d9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -145,6 +145,7 @@ fn extract_project_from_command(command: &Commands) -> Option { cli::FunctionCommands::Calls(args) => args.options.project.clone(), cli::FunctionCommands::XRefs(args) => args.options.project.clone(), cli::FunctionCommands::Rename(args) => args.project.clone(), + cli::FunctionCommands::SetSignature(args) => args.project.clone(), cli::FunctionCommands::Create(args) => args.project.clone(), cli::FunctionCommands::Delete(args) => args.options.project.clone(), }, @@ -203,6 +204,7 @@ fn extract_project_from_command(command: &Commands) -> Option { cli::TypeCommands::Get(args) => args.options.project.clone(), cli::TypeCommands::Create(args) => args.project.clone(), cli::TypeCommands::Apply(args) => args.project.clone(), + cli::TypeCommands::ImportC(args) => args.project.clone(), }, Commands::Patch(cmd) => match cmd { cli::PatchCommands::Bytes(args) => args.project.clone(), @@ -249,6 +251,7 @@ fn extract_program_from_command(command: &Commands) -> Option { cli::FunctionCommands::Calls(args) => args.options.program.clone(), cli::FunctionCommands::XRefs(args) => args.options.program.clone(), cli::FunctionCommands::Rename(args) => args.program.clone(), + cli::FunctionCommands::SetSignature(args) => args.program.clone(), cli::FunctionCommands::Create(args) => args.program.clone(), cli::FunctionCommands::Delete(args) => args.options.program.clone(), }, @@ -307,6 +310,7 @@ fn extract_program_from_command(command: &Commands) -> Option { cli::TypeCommands::Get(args) => args.options.program.clone(), cli::TypeCommands::Create(args) => args.program.clone(), cli::TypeCommands::Apply(args) => args.program.clone(), + cli::TypeCommands::ImportC(args) => args.program.clone(), }, Commands::Patch(cmd) => match cmd { cli::PatchCommands::Bytes(args) => args.program.clone(), @@ -640,29 +644,30 @@ fn execute_via_bridge( } FunctionCommands::Get(args) => { client.send_command( - "get_function", + "function_get", Some(json!({"address": args.resolved_target()})), ) } FunctionCommands::Disasm(args) => client.disasm(args.resolved_target(), None), FunctionCommands::Calls(args) => client.find_calls(args.resolved_target()), FunctionCommands::XRefs(args) => client.xrefs_to(args.resolved_target().to_string()), - FunctionCommands::Rename(args) => client.send_command( - "rename_function", - Some(json!({ - "old_name": args.old_name, - "new_name": args.new_name, - })), + FunctionCommands::Rename(args) => client.symbol_rename( + &args.target, + &args.new_name, + args.namespace.as_deref(), ), + FunctionCommands::SetSignature(args) => { + client.function_set_signature(&args.target, &args.signature) + } FunctionCommands::Create(args) => client.send_command( - "create_function", + "function_create", Some(json!({ "address": args.address, "name": args.name, })), ), FunctionCommands::Delete(args) => client.send_command( - "delete_function", + "function_delete", Some(json!({ "address": args.resolved_target(), })), @@ -754,7 +759,11 @@ fn execute_via_bridge( SymbolCommands::Create(args) => client.symbol_create(&args.address, &args.name), SymbolCommands::Delete(args) => client.symbol_delete(&args.name), SymbolCommands::Rename(args) => { - client.symbol_rename(&args.old_name, &args.new_name) + client.symbol_rename( + &args.target, + &args.new_name, + args.namespace.as_deref(), + ) } } } @@ -765,6 +774,9 @@ fn execute_via_bridge( TypeCommands::Get(args) => client.type_get(&args.name), TypeCommands::Create(args) => client.type_create(&args.definition), TypeCommands::Apply(args) => client.type_apply(&args.address, &args.type_name), + TypeCommands::ImportC(args) => { + client.type_import_c(&args.code, args.category.as_deref()) + } } } Commands::Comment(cmd) => { diff --git a/tests/symbol_tests.rs b/tests/symbol_tests.rs index 61970f3..07e48f7 100644 --- a/tests/symbol_tests.rs +++ b/tests/symbol_tests.rs @@ -3,6 +3,7 @@ use predicates::prelude::*; use serial_test::serial; use std::sync::OnceLock; +use std::time::{SystemTime, UNIX_EPOCH}; #[macro_use] mod common; @@ -22,6 +23,13 @@ fn harness() -> &'static DaemonTestHarness { }) } +fn unique_suffix() -> u128 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time went backwards") + .as_nanos() +} + #[test] #[serial] fn test_symbol_list() { @@ -134,6 +142,67 @@ fn test_symbol_rename() { .stdout(predicate::str::contains(&*new_name)); } +#[test] +#[serial] +fn test_symbol_rename_non_primary_label_by_name() { + require_ghidra!(); + let harness = harness(); + + let addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main"); + let suffix = unique_suffix(); + let old_name = format!("rename_label_old_{}", suffix); + let new_name = format!("rename_label_new_{}", suffix); + + assert_cmd::cargo::cargo_bin_cmd!("ghidra") + .arg("symbol") + .arg("create") + .arg(&addr) + .arg(&old_name) + .arg("--project") + .arg(TEST_PROJECT) + .arg("--program") + .arg(TEST_PROGRAM) + .assert() + .success(); + + assert_cmd::cargo::cargo_bin_cmd!("ghidra") + .arg("symbol") + .arg("rename") + .arg(&old_name) + .arg(&new_name) + .arg("--project") + .arg(TEST_PROJECT) + .arg("--program") + .arg(TEST_PROGRAM) + .assert() + .success(); + + // The label should have been renamed, not left in place while + // the primary symbol at the same address was renamed. + assert_cmd::cargo::cargo_bin_cmd!("ghidra") + .arg("symbol") + .arg("get") + .arg(&old_name) + .arg("--project") + .arg(TEST_PROJECT) + .arg("--program") + .arg(TEST_PROGRAM) + .assert() + .failure(); + + assert_cmd::cargo::cargo_bin_cmd!("ghidra") + .arg("symbol") + .arg("get") + .arg(&new_name) + .arg("--project") + .arg(TEST_PROJECT) + .arg("--program") + .arg(TEST_PROGRAM) + .assert() + .success() + .stdout(predicate::str::contains(&*new_name)); +} + #[test] #[serial] fn test_symbol_get_nonexistent() { diff --git a/tests/type_tests.rs b/tests/type_tests.rs index 0c3c99f..957695d 100644 --- a/tests/type_tests.rs +++ b/tests/type_tests.rs @@ -2,11 +2,13 @@ use predicates::prelude::*; use serial_test::serial; +use std::collections::HashSet; use std::sync::OnceLock; +use std::time::{SystemTime, UNIX_EPOCH}; #[macro_use] mod common; -use common::{ensure_test_project, get_function_address, DaemonTestHarness}; +use common::{ensure_test_project, get_function_address, ghidra, DaemonTestHarness}; const TEST_PROJECT: &str = "ci-test"; const TEST_PROGRAM: &str = "sample_binary"; @@ -20,6 +22,13 @@ fn harness() -> &'static DaemonTestHarness { }) } +fn unique_suffix() -> u128 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time went backwards") + .as_nanos() +} + #[test] #[serial] fn test_type_list() { @@ -135,3 +144,69 @@ fn test_type_get_nonexistent() { .assert() .failure(); } + +#[test] +#[serial] +fn test_type_import_c_category_keeps_existing_same_named_types() { + require_ghidra!(); + let harness = harness(); + + let suffix = unique_suffix(); + let type_name = format!("CatIsoType_{}", suffix); + let category_a = format!("/cat_a_{}", suffix); + let category_b = format!("/cat_b_{}", suffix); + let def_a = format!("struct {} {{ int a; }};", type_name); + let def_b = format!("struct {} {{ int b; }};", type_name); + + ghidra(harness) + .arg("type") + .arg("import-c") + .arg("--category") + .arg(&category_a) + .arg(&def_a) + .with_project(TEST_PROJECT, TEST_PROGRAM) + .run() + .assert_success(); + + ghidra(harness) + .arg("type") + .arg("import-c") + .arg("--category") + .arg(&category_b) + .arg(&def_b) + .with_project(TEST_PROJECT, TEST_PROGRAM) + .run() + .assert_success(); + + let list_result = ghidra(harness) + .arg("type") + .arg("list") + .arg("--filter") + .arg(&type_name) + .with_project(TEST_PROJECT, TEST_PROGRAM) + .json_format() + .run(); + + list_result.assert_success(); + let listed_types: Vec = list_result.json(); + + let categories: HashSet = listed_types + .iter() + .filter(|item| item.get("name").and_then(|v| v.as_str()) == Some(type_name.as_str())) + .filter_map(|item| item.get("category").and_then(|v| v.as_str())) + .map(|s| s.to_string()) + .collect(); + + assert!( + categories.contains(&category_a), + "Expected {} to remain after second import. Seen categories: {:?}", + category_a, + categories + ); + assert!( + categories.contains(&category_b), + "Expected {} after second import. Seen categories: {:?}", + category_b, + categories + ); +}