Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ dependencies {
implementation 'org.json:json:20250107'
implementation "com.google.guava:guava:33.2.0-jre"
implementation group: 'com.fifesoft', name: 'rsyntaxtextarea', version: '3.5.2'
implementation "ai.reveng:sdk:2.37.4"
implementation "ai.reveng:sdk:2.52.1"
testImplementation('junit:junit:4.13.1')
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.8.2")

Expand Down
6 changes: 2 additions & 4 deletions ghidra_scripts/RevEngExamplePostScript.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ai.reveng.toolkit.ghidra.core.services.api.AnalysisOptionsBuilder;
import ai.reveng.toolkit.ghidra.core.services.api.GhidraRevengService;
import ai.reveng.toolkit.ghidra.core.services.api.types.ApiInfo;
import ghidra.app.plugin.core.analysis.AutoAnalysisManager;
import ghidra.app.script.GhidraScript;

public class RevEngExamplePostScript extends GhidraScript {
Expand All @@ -12,9 +13,6 @@ protected void run() throws Exception {
ghidraRevengService.upload(currentProgram);

AnalysisOptionsBuilder options = AnalysisOptionsBuilder.forProgram(currentProgram);
var binID = ghidraRevengService.analyse(currentProgram, options, monitor);

// Wait for analysis to finish
ghidraRevengService.waitForFinishedAnalysis(monitor, binID, null, null);
var analyzedProgram = ghidraRevengService.analyse(currentProgram, options, monitor);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,10 @@
import ghidra.app.cmd.function.ApplyFunctionSignatureCmd;
import ghidra.app.cmd.label.RenameLabelCmd;
import ghidra.framework.cmd.Command;
import ghidra.framework.model.DomainObject;
import ghidra.program.model.data.DataTypeDependencyException;
import ghidra.program.model.data.FunctionDefinitionDataType;
import ghidra.program.model.listing.CircularDependencyException;
import ghidra.program.model.listing.Program;
import ghidra.program.model.symbol.Namespace;
import ghidra.program.model.symbol.SourceType;
import ghidra.util.Msg;
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.exception.InvalidInputException;
import org.jetbrains.annotations.NotNull;
Expand All @@ -21,31 +17,61 @@

import static ai.reveng.toolkit.ghidra.plugins.BinarySimilarityPlugin.REVENG_AI_NAMESPACE;

// We can't use Command<Program> because that breaks compatibility with Ghidra 11.0
public class ApplyMatchCmd implements Command {
/// The central command to apply a function match to a {@link Program}
/// It centralizes product design decisions about how to apply a match, like moving it to a namespace,
/// renaming it, applying the signature, etc.
/// It will apply the function signature if available, otherwise it will just rename the function
/// There are various considerations when ap
public class ApplyMatchCmd implements Command<Program> {

private final Program program;
private final GhidraRevengService.AnalysedProgram analyzedProgram;
private final GhidraFunctionMatchWithSignature match;
@Nullable private final GhidraRevengService service;
private final Boolean includeBinaryNameInNameSpace;

public ApplyMatchCmd(
@Nullable GhidraRevengService service,
@NotNull GhidraFunctionMatchWithSignature match) {
@NotNull GhidraRevengService.AnalysedProgram program,
@NotNull GhidraFunctionMatchWithSignature match,
Boolean includeBinaryNameInNameSpace

) {
super();
this.program = match.function().getProgram();
this.analyzedProgram = program;
this.match = match;
this.service = service;
this.includeBinaryNameInNameSpace = includeBinaryNameInNameSpace;
}

private boolean shouldApplyMatch() {
var func = match.function();
return func != null &&
// Do not override user-defined function names
func.getSymbol().getSource() != SourceType.USER_DEFINED &&
// Exclude thunks and external functions
!func.isThunk() &&
!func.isExternal() &&
// Only accept valid names (no spaces)
!match.functionMatch().nearest_neighbor_mangled_function_name().contains(" ") &&
!match.functionMatch().nearest_neighbor_function_name().contains(" ")
// Only rename if the function ID is known (boundaries matched)
&& analyzedProgram.getIDForFunction(func).map(id -> id.functionID() != match.functionMatch().origin_function_id()).orElse(false);
}

@Override
public boolean applyTo(DomainObject obj) {
public boolean applyTo(Program obj) {
// Check that this is the same program
if (obj != this.program) {
if (obj != this.analyzedProgram.program()) {
throw new IllegalArgumentException("This command can only be applied to the same program as the one provided in the constructor");
}
var libraryNamespace = getLibraryNameSpaceForName(match.functionMatch().nearest_neighbor_binary_name());
if (!shouldApplyMatch()) {
return false;
}

var nameSpace = includeBinaryNameInNameSpace ? getLibraryNameSpaceForName(match.functionMatch().nearest_neighbor_binary_name()): getRevEngAINameSpace();
var function = match.function();
try {
function.setParentNamespace(libraryNamespace);
function.setParentNamespace(nameSpace);
} catch (DuplicateNameException e) {
throw new RuntimeException(e);
} catch (InvalidInputException e) {
Expand All @@ -54,43 +80,36 @@ public boolean applyTo(DomainObject obj) {
throw new RuntimeException(e);
}

FunctionDefinitionDataType signature = null;
if (match.signature().isPresent()) {
try {
signature = GhidraRevengService.getFunctionSignature(match.signature().get());
} catch (DataTypeDependencyException e) {
Msg.showError(this, null,"Failed to create function signature",
"Failed to create signature for match function with type %s"
.formatted(match.signature().get().func_types().getSignature()),
e);
}
}
this.analyzedProgram.setMangledNameForFunction(function, match.functionMatch().nearest_neighbor_mangled_function_name());

var signature = match.signature();
if (signature != null) {
var cmd = new ApplyFunctionSignatureCmd(function.getEntryPoint(), signature, SourceType.USER_DEFINED);
cmd.applyTo(program);
cmd.applyTo(analyzedProgram.program());
}
else {
var renameCmd = new RenameLabelCmd(match.function().getSymbol(), match.functionMatch().name(), SourceType.USER_DEFINED);
renameCmd.applyTo(program);
renameCmd.applyTo(analyzedProgram.program());
}
// If we have a service then push the name. If not then it was explicitly not provided, i.e. the caller
// is responsible for pushing the names in batch
if (service != null) {
service.getApi().renameFunction(match.functionMatch().origin_function_id(), match.functionMatch().name());
service.getApi().renameFunction(match.functionMatch().origin_function_id(), match.functionMatch().nearest_neighbor_function_name());
}


return false;
return true;
}

public void applyWithTransaction() {
var program = this.analyzedProgram.program();
var tID = program.startTransaction("RevEng.AI: Apply Match");
var status = applyTo(program);
program.endTransaction(tID, status);
}

private Namespace getRevEngAINameSpace() {
var program = this.analyzedProgram.program();
Namespace revengMatchNamespace = null;
try {
revengMatchNamespace = program.getSymbolTable().getOrCreateNameSpace(
Expand All @@ -105,6 +124,7 @@ private Namespace getRevEngAINameSpace() {
}

private Namespace getLibraryNameSpaceForName(String name) {
var program = this.analyzedProgram.program();
Namespace libraryNamespace = null;
try {
libraryNamespace = program.getSymbolTable().getOrCreateNameSpace(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ai.reveng.toolkit.ghidra.binarysimilarity.cmds;

import ai.reveng.toolkit.ghidra.core.services.api.GhidraRevengService;
import ai.reveng.toolkit.ghidra.core.services.api.TypedApiInterface;
import ai.reveng.toolkit.ghidra.core.services.api.types.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
Expand All @@ -10,9 +11,7 @@
import java.time.Duration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.groupingBy;

Expand All @@ -23,12 +22,12 @@
*
*/
public class ComputeTypeInfoTask extends Task {
private final List<FunctionID> functions;
private final List<TypedApiInterface.FunctionID> functions;
private final GhidraRevengService service;
private final DataTypeAvailableCallback callback;

public ComputeTypeInfoTask(GhidraRevengService service,
List<FunctionID> functions,
List<TypedApiInterface.FunctionID> functions,
@Nullable DataTypeAvailableCallback callback) {
super("Computing Type Info", true, true, false);
this.service = service;
Expand All @@ -49,7 +48,7 @@ public void run(TaskMonitor monitor) throws CancelledException {
service.getApi().generateFunctionDataTypes(analysisID, functions.stream().map(FunctionDetails::functionId).toList());
});

Set<FunctionID> missing = new HashSet<>(functions);
Set<TypedApiInterface.FunctionID> missing = new HashSet<>(functions);

while (!missing.isEmpty()) {
try {
Expand All @@ -76,6 +75,6 @@ public void run(TaskMonitor monitor) throws CancelledException {
}

public interface DataTypeAvailableCallback {
void dataTypeAvailable(FunctionID functionID, FunctionDataTypeStatus dataTypeStatus);
void dataTypeAvailable(TypedApiInterface.FunctionID functionID, FunctionDataTypeStatus dataTypeStatus);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import ai.reveng.invoker.ApiException;
import ai.reveng.toolkit.ghidra.core.services.api.GhidraRevengService;
import ai.reveng.toolkit.ghidra.core.services.api.types.FunctionID;
import ai.reveng.toolkit.ghidra.core.services.api.TypedApiInterface;
import ai.reveng.toolkit.ghidra.core.services.logging.ReaiLoggingService;
import ai.reveng.toolkit.ghidra.plugins.ReaiPluginPackage;
import ai.reveng.toolkit.ghidra.core.services.api.types.AIDecompilationStatus;
Expand Down Expand Up @@ -53,10 +53,15 @@ public AIDecompilationdWindow(PluginTool tool, String owner) {
public void actionPerformed(ActionContext context) {
if (function != null) {
var service = tool.getService(GhidraRevengService.class);
var fID = service.getFunctionIDFor(function);
var analyzedProgram = service.getAnalysedProgram(function.getProgram());
if (analyzedProgram.isEmpty()) {
Msg.error(this, "Failed to send positive feedback: Program is not known to RevEng.AI");
return;
}
var fID = analyzedProgram.get().getIDForFunction(function);
fID.ifPresent(id -> {
try {
service.getApi().aiDecompRating(id, "POSITIVE", "");
service.getApi().aiDecompRating(id.functionID(), "POSITIVE", "");
} catch (ApiException e) {
// Fail silently because this is not a critical feature
Msg.error(this, "Failed to send positive feedback for function %s: %s".formatted(function.getName(), e.getMessage()));
Expand Down Expand Up @@ -89,10 +94,15 @@ public void actionPerformed(ActionContext context) {
if (!dialog.isCanceled()) {
if (function != null) {
var service = tool.getService(GhidraRevengService.class);
var fID = service.getFunctionIDFor(function);
var programWithID = service.getAnalysedProgram(function.getProgram());
if (programWithID.isEmpty()) {
Msg.error(this, "Failed to send negative feedback: Program is not known to RevEng.AI");
return;
}
var fID = programWithID.get().getIDForFunction(function);
fID.ifPresent(id -> {
try {
service.getApi().aiDecompRating(id, "NEGATIVE", dialog.getValue());
service.getApi().aiDecompRating(id.functionID(), "NEGATIVE", dialog.getValue());
} catch (ApiException e) {
// Fail silently because this is not a critical feature
Msg.error(this, "Failed to send negative feedback for function %s: %s".formatted(function.getName(), e.getMessage()));
Expand Down Expand Up @@ -164,11 +174,11 @@ private void clear() {
descriptionArea.setText("");
}

public void refresh(Function function) {
public void refresh(GhidraRevengService.FunctionWithID function) {
// Check if we know this function already
var cachedStatus = cache.get(function);
var cachedStatus = cache.get(function.function());
if (cachedStatus != null) {
setDisplayedValuesBasedOnStatus(function, cachedStatus);
setDisplayedValuesBasedOnStatus(function.function(), cachedStatus);
} else {
// TODO: Allow toggling auto decomp mode via local toggle action, for now do it always

Expand All @@ -184,6 +194,13 @@ public void refresh(Function function) {
}

public void locationChanged(ProgramLocation loc) {
var service = tool.getService(GhidraRevengService.class);
var analyzedProgram = service.getAnalysedProgram(loc.getProgram());
if (analyzedProgram.isEmpty()) {
clear();
return;
}

var functionMgr = loc.getProgram().getFunctionManager();
var newFuncLocation = functionMgr.getFunctionContaining(loc.getAddress());

Expand All @@ -193,9 +210,8 @@ public void locationChanged(ProgramLocation loc) {
}

function = newFuncLocation;
if (function != null && !function.isExternal() && !function.isThunk()) {
refresh(function);
}
var functionWithID = analyzedProgram.get().getIDForFunction(function);
functionWithID.ifPresent(this::refresh);
}


Expand Down Expand Up @@ -225,18 +241,17 @@ private boolean hasPendingDecompilations() {
class AIDecompTask extends Task {

private final GhidraRevengService service;
private final Function function;
private final GhidraRevengService.FunctionWithID functionWithID;

public AIDecompTask(PluginTool tool, Function function) {
public AIDecompTask(PluginTool tool, GhidraRevengService.FunctionWithID functionWithID) {
super("AI Decomp task", true, false, false);
service = tool.getService(GhidraRevengService.class);
this.function = function;
this.functionWithID = functionWithID;
}

@Override
public void run(TaskMonitor monitor) throws CancelledException {
var fID = service.getFunctionIDFor(function)
.orElseThrow(() -> new RuntimeException("Function has no associated FunctionID"));
var fID = functionWithID.functionID();
// Check if there is an existing process already, because the trigger API will fail with 400 if there is
if (service.getApi().pollAIDecompileStatus(fID).status().equals("uninitialised")) {
// Trigger the decompilation
Expand All @@ -247,7 +262,7 @@ public void run(TaskMonitor monitor) throws CancelledException {
}


private void waitForDecomp(FunctionID id, TaskMonitor monitor) throws CancelledException {
private void waitForDecomp(TypedApiInterface.FunctionID id, TaskMonitor monitor) throws CancelledException {
var logger = tool.getService(ReaiLoggingService.class);
var api = service.getApi();
AIDecompilationStatus lastDecompStatus = null;
Expand All @@ -256,9 +271,9 @@ private void waitForDecomp(FunctionID id, TaskMonitor monitor) throws CancelledE
if (lastDecompStatus == null || !Objects.equals(newStatus.status(), lastDecompStatus.status())) {
lastDecompStatus = newStatus;

newStatusForFunction(function, newStatus);
newStatusForFunction(functionWithID.function(), newStatus);
}
monitor.setMessage("Waiting for AI Decompilation for %s ... Current status: %s".formatted(function.getName(), lastDecompStatus.status()));
monitor.setMessage("Waiting for AI Decompilation for %s ... Current status: %s".formatted(functionWithID.function().getName(), lastDecompStatus.status()));
monitor.checkCancelled();
switch (newStatus.status()) {
case "pending":
Expand Down
Loading
Loading