mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 16:26:36 +00:00
* fix(call-path): keep computed-call targets out of complete callee coverage Ghidra reported complete callee coverage when a computed call had one ReferenceManager target, so trace_call_path, build_call_path and the function comparisons turned the static candidate into a definite negative. The bridge now keeps those targets as observed callees and marks callee coverage partial, naming the computed call sites. IDA callee reads cannot classify their call sites, so their callee coverage is partial too. The Ghidra call-coverage profile revision refuses older snapshots. A source-owned x86-64 bridge probe in the ghidra-bridge-sources job checks the slot, register and direct-call controls against real Ghidra. Fixes #8277 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): match the observed call coverage probe marker --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
155 lines
8.6 KiB
Java
155 lines
8.6 KiB
Java
//@category REA Verification
|
|
|
|
import com.google.gson.JsonArray;
|
|
import com.google.gson.JsonElement;
|
|
import com.google.gson.JsonNull;
|
|
import com.google.gson.JsonObject;
|
|
import ghidra.app.script.GhidraScript;
|
|
import ghidra.app.script.GhidraScriptUtil;
|
|
import ghidra.program.model.address.Address;
|
|
import ghidra.program.model.listing.Instruction;
|
|
import ghidra.program.model.symbol.RefType;
|
|
import ghidra.program.model.symbol.Reference;
|
|
import ghidra.program.model.symbol.SourceType;
|
|
import java.io.File;
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.lang.reflect.Method;
|
|
|
|
/**
|
|
* Real x86-64 decoding, functions and ReferenceManager state for calls through
|
|
* a writable slot, a register and a direct target. Only the slot call's
|
|
* recorded target is injected, as Ghidra's analysis records one for an
|
|
* initialized slot. The production bridge must keep that candidate while
|
|
* observed coverage stays complete and discovery names the unknown targets; the direct call is
|
|
* the complete control. The source-owned bytes are never executed.
|
|
*/
|
|
public class ReaComputedCallCoverageProbe extends GhidraScript {
|
|
private static final String COMPUTED_LIMITATION = "computed call site(s) (";
|
|
|
|
@Override
|
|
public void run() throws Exception {
|
|
String[] args = getScriptArgs();
|
|
require(args.length == 1, "Expected the exact production bridge directory");
|
|
var source = GhidraScriptUtil.findScriptByName("ReaGhidraBridge.java");
|
|
require(source != null && new File(source.getAbsolutePath()).getCanonicalFile().equals(
|
|
new File(args[0], "ReaGhidraBridge.java").getCanonicalFile()), "Production bridge source");
|
|
GhidraScript bridge = GhidraScriptUtil.getProvider(source).getScriptInstance(source, errorWriter);
|
|
bridge.set(getState(), getControls());
|
|
require(currentProgram.getLanguageID().getIdAsString().equals("x86:LE:64:default"),
|
|
"x86-64 fixture language");
|
|
Address base = currentProgram.getMemory().getMinAddress();
|
|
Address selected = base; // mov eax, 1; ret
|
|
Address alternate = base.add(0x08); // mov eax, 2; ret
|
|
Address slotCaller = base.add(0x10); // call qword ptr [rip + 0x2a] (slot at +0x40); ret
|
|
Address registerCaller = base.add(0x18); // call rax; ret
|
|
Address directCaller = base.add(0x20); // call selected; ret
|
|
for (Address entry : new Address[] { selected, alternate, slotCaller, registerCaller, directCaller }) {
|
|
if (getInstructionAt(entry) == null) disassemble(entry);
|
|
require(getInstructionAt(entry) != null, "Decoded entry " + entry);
|
|
require(createFunction(entry, null) != null, "Created function at " + entry);
|
|
}
|
|
Instruction slotCall = requireCall(slotCaller, true);
|
|
requireCall(registerCaller, true);
|
|
Instruction directCall = requireCall(directCaller, false);
|
|
currentProgram.getReferenceManager().addMemoryReference(
|
|
slotCall.getAddress(), selected, RefType.COMPUTED_CALL, SourceType.ANALYSIS, 0);
|
|
if (!hasCallReference(directCall))
|
|
currentProgram.getReferenceManager().addMemoryReference(
|
|
directCall.getAddress(), selected, RefType.UNCONDITIONAL_CALL, SourceType.ANALYSIS, 0);
|
|
|
|
Field space = bridge.getClass().getDeclaredField("sessionDefaultAddressSpace");
|
|
space.setAccessible(true);
|
|
space.set(null, base.getAddressSpace());
|
|
Method callees = bridge.getClass().getDeclaredMethod("procedureCalls", JsonObject.class, boolean.class);
|
|
callees.setAccessible(true);
|
|
String selectedAddress = canonical(selected);
|
|
|
|
JsonObject computed = callees(callees, bridge, slotCaller);
|
|
require(contains(computed.getAsJsonArray("value"), selectedAddress),
|
|
"The recorded slot target remains an observed callee");
|
|
require(!contains(computed.getAsJsonArray("value"), canonical(alternate)),
|
|
"No callee is invented for the alternate value");
|
|
JsonObject computedCoverage = computed.getAsJsonObject("relationship_coverage");
|
|
require(computedCoverage.get("status").getAsString().equals("complete"),
|
|
"The recorded computed target inventory is complete");
|
|
JsonObject calleeDiscovery = computedCoverage.getAsJsonObject("discovery").getAsJsonObject("callees");
|
|
require(calleeDiscovery.get("status").getAsString().equals("unknown"), "Other computed targets stay unknown");
|
|
require(calleeDiscovery.get("reason").getAsString().contains("1 " + COMPUTED_LIMITATION + canonical(slotCall.getAddress()) + ")"),
|
|
"Discovery uncertainty names the computed call site");
|
|
|
|
JsonObject targetless = callees(callees, bridge, registerCaller);
|
|
JsonObject targetlessCoverage = targetless.getAsJsonObject("relationship_coverage");
|
|
require(targetlessCoverage.get("status").getAsString().equals("partial"),
|
|
"A targetless computed call keeps partial coverage");
|
|
require(!limitationContains(targetlessCoverage, COMPUTED_LIMITATION),
|
|
"A targetless call is not reported as having recorded candidates");
|
|
|
|
JsonObject direct = callees(callees, bridge, directCaller);
|
|
JsonArray directValue = direct.getAsJsonArray("value");
|
|
require(directValue.size() == 1 && contains(directValue, selectedAddress), "Direct callee");
|
|
require(direct.getAsJsonObject("relationship_coverage").get("status").getAsString().equals("complete"),
|
|
"A direct call with its recorded destination stays complete");
|
|
for (Address target : new Address[] { selected, alternate }) {
|
|
JsonObject input = new JsonObject(); input.add("document", JsonNull.INSTANCE); input.addProperty("procedure", canonical(target));
|
|
JsonObject incoming = (JsonObject) callees.invoke(bridge, input, true);
|
|
JsonObject coverage = incoming.getAsJsonObject("relationship_coverage");
|
|
require(coverage.get("status").getAsString().equals("complete"), "Recorded incoming edges remain complete");
|
|
require(coverage.getAsJsonObject("discovery").getAsJsonObject("callers").get("status").getAsString().equals("unknown"), "Additional computed callers stay unknown");
|
|
if (target.equals(selected)) require(contains(incoming.getAsJsonArray("value"), canonical(directCaller)), "Known direct caller survives");
|
|
else require(incoming.getAsJsonArray("value").isEmpty(), "No alternate caller is invented");
|
|
}
|
|
println("REA_COMPUTED_CALL_COVERAGE_OK slot=observed-complete-discovery-unknown register=partial direct=complete callers=discovery-unknown");
|
|
}
|
|
|
|
private Instruction requireCall(Address entry, boolean computed) {
|
|
Instruction instruction = getInstructionAt(entry);
|
|
require(instruction != null && instruction.getFlowType().isCall() &&
|
|
instruction.getFlowType().isComputed() == computed,
|
|
(computed ? "Computed" : "Direct") + " call decoded at " + entry);
|
|
return instruction;
|
|
}
|
|
|
|
private static boolean hasCallReference(Instruction instruction) {
|
|
for (Reference reference : instruction.getReferencesFrom())
|
|
if (reference.getReferenceType().isCall()) return true;
|
|
return false;
|
|
}
|
|
|
|
private JsonObject callees(Method method, Object bridge, Address procedure) throws Exception {
|
|
JsonObject input = new JsonObject();
|
|
input.add("document", JsonNull.INSTANCE);
|
|
input.addProperty("procedure", canonical(procedure));
|
|
JsonObject result;
|
|
try {
|
|
result = (JsonObject) method.invoke(bridge, input, false);
|
|
} catch (InvocationTargetException failure) {
|
|
throw new IllegalStateException("procedure_callees failed at " + procedure, failure.getCause());
|
|
}
|
|
println("REA_COMPUTED_CALL_COVERAGE " + canonical(procedure) + " " + result);
|
|
return result;
|
|
}
|
|
|
|
private static String canonical(Address address) {
|
|
return "0x" + Long.toHexString(address.getOffset());
|
|
}
|
|
|
|
private static boolean contains(JsonArray values, String expected) {
|
|
for (JsonElement value : values)
|
|
if (value.getAsString().equals(expected)) return true;
|
|
return false;
|
|
}
|
|
|
|
private static boolean limitationContains(JsonObject coverage, String expected) {
|
|
JsonArray limitations = coverage.getAsJsonArray("limitations");
|
|
if (limitations == null) return false;
|
|
for (JsonElement limitation : limitations)
|
|
if (limitation.getAsString().contains(expected)) return true;
|
|
return false;
|
|
}
|
|
|
|
private static void require(boolean condition, String label) {
|
|
if (!condition) throw new IllegalStateException(label);
|
|
}
|
|
}
|