fix(ghidra): recover returning import callers

This commit is contained in:
You Wish
2026-10-11 09:59:27 +08:00
committed by morluto
parent af88184eee
commit 49e6f00ed0
8 changed files with 537 additions and 40 deletions
+66 -40
View File
@@ -3,20 +3,25 @@ import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.FlowType;
import ghidra.program.model.symbol.Reference;
import ghidra.program.model.pcode.PcodeOp;
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
* Undo wrong no-return flags from auto-analysis. A function flagged no-return that contains a
* real RET is an ordinary function (stream readers, cxa_guard_acquire, ...). Callers treated the
* call as a terminator and the decompiler truncated them. Clear the flag, then re-disassemble only
* real RET is an ordinary function (stream readers, cxa_guard_acquire, ...). Known returning
* libc/TLS imports have no body to inspect, so clear their flags by exact external ABI name.
* Callers treated the call as a terminator and the decompiler truncated them. Clear the flag, then re-disassemble only
* the call sites of the functions that were cleared and refit their owning functions in place.
*/
public final class ReaGhidraNoReturnFix extends GhidraScript {
private static final Set<String> RETURNING_IMPORTS = Set.of(
"malloc", "__tls_get_addr", "memcpy", "memset", "strlen", "__errno_location");
@Override
public void run() throws Exception {
// Windows P0 does not admit analysis-database mutation.
@@ -24,66 +29,87 @@ public final class ReaGhidraNoReturnFix extends GhidraScript {
FunctionManager fm = currentProgram.getFunctionManager();
Listing li = currentProgram.getListing();
Set<Address> cleared = new HashSet<>();
for (Function f : fm.getFunctions(true)) {
Deque<Address> pending = new ArrayDeque<>();
for (Function f : fm.getExternalFunctions()) {
monitor.checkCancelled();
if (!f.hasNoReturn() || f.isThunk()) continue;
for (Instruction ins : li.getInstructions(f.getBody(), true)) {
FlowType ft = ins.getFlowType();
boolean decodedReturn = false;
for (PcodeOp operation : ins.getPcode()) {
if (operation.getOpcode() == PcodeOp.RETURN) decodedReturn = true;
}
if (decodedReturn && !ft.isJump() && !ft.isCall()) {
f.setNoReturn(false);
cleared.add(f.getEntryPoint());
break;
}
}
// Do not apply import-name assumptions to local functions or to arbitrary externals.
if (!f.hasNoReturn() || !RETURNING_IMPORTS.contains(f.getName())) continue;
f.setNoReturn(false);
enqueueReturning(f, cleared, pending);
}
// A thunk inherits the flag of the function it forwards to.
for (Function f : fm.getFunctions(true)) {
monitor.checkCancelled();
if (!f.isThunk()) continue;
Function target = f.getThunkedFunction(true);
if (target != null && cleared.contains(target.getEntryPoint())) {
cleared.add(f.getEntryPoint());
if (clearDecodedReturn(f, li)) {
enqueueReturning(f, cleared, pending);
}
}
int redone = 0;
TreeSet<Address> owners = new TreeSet<>();
if (!cleared.isEmpty()) {
List<Address> sites = new ArrayList<>();
for (Instruction ins : li.getInstructions(true)) {
while (!pending.isEmpty()) {
Address target = pending.removeFirst();
TreeSet<Address> sites = new TreeSet<>();
// References include computed import calls after Ghidra resolves their destination.
// Collect before redisassembly mutates the reference table.
for (Reference reference : currentProgram.getReferenceManager().getReferencesTo(target)) {
monitor.checkCancelled();
FlowType ft = ins.getFlowType();
if (!(ft.isCall() && ft.isTerminal())) continue;
Address[] flows = ins.getFlows();
if (flows.length == 0 || !cleared.contains(flows[0])) continue;
// Do not redisassemble into an independently identified function.
Address next = ins.getMaxAddress().add(1);
if (fm.getFunctionAt(next) != null || !currentProgram.getMemory().contains(next)) continue;
sites.add(ins.getAddress());
sites.add(reference.getFromAddress());
}
for (Address s : sites) {
monitor.checkCancelled();
Instruction ins = li.getInstructionAt(s);
if (ins == null) continue;
FlowType ft = ins.getFlowType();
if (!(ft.isCall() && ft.isTerminal())) continue;
Address[] flows = ins.getFlows();
boolean callsTarget = false;
for (Address flow : flows) {
if (flow.equals(target)) callsTarget = true;
}
if (!callsTarget) continue;
// Do not redisassemble into an independently identified function.
Address next = ins.getMaxAddress().add(1);
if (fm.getFunctionAt(next) != null || !currentProgram.getMemory().contains(next)) continue;
Function owner = fm.getFunctionContaining(s);
if (owner != null && !owner.isThunk()) owners.add(owner.getEntryPoint());
clearListing(ins.getMinAddress(), ins.getMaxAddress());
disassemble(s);
redone++;
}
for (Address e : owners) {
monitor.checkCancelled();
Function f = fm.getFunctionAt(e);
// Refit the body in place so the name, signature and calling convention survive.
if (f != null) CreateFunctionCmd.fixupFunctionBody(currentProgram, f, monitor);
if (owner != null && !owner.isThunk()) {
CreateFunctionCmd.fixupFunctionBody(currentProgram, owner, monitor);
// Auto-analysis may have propagated the false flag to this truncated caller.
// Its RET becomes visible only now; repair its callers in the same pass.
if (clearDecodedReturn(owner, li)) enqueueReturning(owner, cleared, pending);
}
}
// No analyzeChanges here: on libpl2.so it adds about 70 s, which pushes startup past the 330 s deadline.
}
// No analyzeChanges here: on libpl2.so it adds about 70 s, which pushes startup past the 330 s deadline.
println(
"REA no-return fix: cleared=" + cleared.size() + " sites=" + redone + " refit=" + owners.size());
}
private void enqueueReturning(Function function, Set<Address> cleared, Deque<Address> pending) {
if (cleared.add(function.getEntryPoint())) pending.addLast(function.getEntryPoint());
// A thunk inherits the flag of the function it forwards to, including through other thunks.
Address[] thunks = function.getFunctionThunkAddresses(true);
if (thunks == null) return;
for (Address thunk : thunks) {
if (cleared.add(thunk)) pending.addLast(thunk);
}
}
private boolean clearDecodedReturn(Function function, Listing listing) throws Exception {
if (!function.hasNoReturn() || function.isThunk()) return false;
for (Instruction instruction : listing.getInstructions(function.getBody(), true)) {
monitor.checkCancelled();
FlowType flow = instruction.getFlowType();
if (flow.isJump() || flow.isCall()) continue;
for (PcodeOp operation : instruction.getPcode()) {
if (operation.getOpcode() != PcodeOp.RETURN) continue;
function.setNoReturn(false);
return true;
}
}
return false;
}
}
+10
View File
@@ -66,6 +66,16 @@ observations qualify the analysis; they do not prove that a no-return flag is
incorrect or recover excluded code. Analysis profiles version this observation
contract so older snapshots cannot replay unqualified results.
On non-Windows hosts, the ephemeral import repair clears no-return flags on the
known returning external ABI names `malloc`, `__tls_get_addr`, `memcpy`, `memset`,
`strlen`, and `__errno_location`. This relies on their expected ABI behavior;
external implementations are not inspected. The repair re-disassembles affected
terminal calls and refits their owning functions, preserving independent function
entries. If a recovered caller reveals a decoded return, its propagated no-return
flag and affected callers are repaired in the same pass. Unknown imports and
genuine no-return imports keep their flags. Analysis profiles version this repair
so snapshots produced before it are not reused. Windows P0 does not run it.
`npm run verify:ghidra` compiles the versioned C oracle into debug and stripped
host-native targets (x86-64/AArch64 ELF on Linux or Mach-O on macOS), plus a native
DWARF 4 type-layout object. It proves the admitted operations, external functions, resolved thunks, exports, stripped-name
+1
View File
@@ -8,6 +8,7 @@
"src/evaluation/AgentEvaluationFixtures.ts",
"src/application/RealHopperAssertions.ts",
"scripts/verify/termux/browser.mjs",
"scripts/verify-real-ghidra-noreturn.mjs",
"tests/processOwnershipRunner.ts"
],
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
+206
View File
@@ -0,0 +1,206 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { parseConfig } from "../dist/config/parseConfig.js";
import { GhidraClient } from "../dist/ghidra/GhidraClient.js";
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallationPolicy.js";
import { GhidraProvider } from "../dist/ghidra/GhidraProvider.js";
import { silentLogger } from "../dist/logger.js";
import { cleanupOwnedProcessGroup } from "../dist/process/ProcessOwnership.js";
import {
ProviderProcessSupervisor,
spawnOwnedProviderProcess,
} from "../dist/process/ProviderProcess.js";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const exec = promisify(execFile);
const run = createVerifierRun();
assert.equal(process.platform, "linux", "This regression lane requires Linux");
assert.equal(process.arch, "x64", "This regression lane requires x86-64");
const config = parseConfig(process.env);
if (!config.ok) throw config.error;
const installation = inspectGhidraInstallation({
environment: process.env,
installDir: config.value.ghidraInstallDir,
javaHome: config.value.ghidraJavaHome,
});
assert.equal(installation.status, "available", JSON.stringify(installation));
assert.equal(installation.providerVersion, SUPPORTED_GHIDRA_VERSION);
const fixtures = fileURLToPath(
new URL("../tests/conformance/ghidra", import.meta.url),
);
const bridge = fileURLToPath(new URL("../bridge/ghidra", import.meta.url));
const workspace = await mkdtemp(join(tmpdir(), "rea-ghidra-noreturn-"));
let cleaned = true;
let report;
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
try {
const targetPath = join(workspace, "noreturn.so");
await exec(process.env.REA_CC ?? "cc", [
"-shared",
"-fPIC",
"-O0",
"-fno-builtin",
join(fixtures, "no-return.c"),
"-o",
targetPath,
]);
const targetDigest = digest(await readFile(targetPath));
const sourceDigests = Object.fromEntries(
await Promise.all(
["ReaGhidraBridge.java", "ReaGhidraNoReturnFix.java"].map(
async (name) => [name, digest(await readFile(join(bridge, name)))],
),
),
);
const project = join(workspace, "project");
await mkdir(project);
const launch = await spawnOwnedProviderProcess({
command: installation.analyzeHeadlessPath,
arguments: [
project,
"NoReturn",
"-import",
targetPath,
"-readOnly",
"-deleteProject",
"-scriptPath",
`${fixtures};${bridge}`,
"-postScript",
join(fixtures, "ReaNoReturnProbe.java"),
bridge,
],
runId: run.run_id,
expectedCommand: null,
});
const supervisor = new ProviderProcessSupervisor({
...launch,
ownsProcessLifetime: true,
cleanup: () => cleanupOwnedProcessGroup(launch.ownership),
});
let probe;
try {
assert.ok(
await supervisor.waitForExit(120000),
"No-return probe timed out",
);
assert.ok(
await supervisor.waitForOutputClose(1000),
"No-return probe output did not close",
);
const snapshot = supervisor.snapshot();
const output = `${snapshot.stdout.text}\n${snapshot.stderr.text}`;
assert.equal(snapshot.exitCode, 0, output);
const reports = [
...output.matchAll(/REA_NO_RETURN_PROBE_JSON (\{[^\r\n]*\})/gu),
];
assert.equal(reports.length, 1, output);
probe = JSON.parse(reports[0][1]);
assert.equal(probe.status, "passed");
assert.equal(probe.ghidra_version, SUPPORTED_GHIDRA_VERSION);
assert.equal(probe.target_sha256, targetDigest);
for (const [name, expected] of Object.entries(sourceDigests))
assert.equal(probe[name], expected);
assert.ok(probe.checks.includes("independent_entry_preserved"));
assert.ok(probe.checks.includes("propagated_flag_repaired_rea_tls_outer"));
assert.ok(probe.checks.includes("recovered_pseudocode"));
} finally {
const stopped = await supervisor.stop();
cleaned = stopped.status === "verified-cleanup";
assert.ok(
cleaned,
`Owned cleanup incomplete; retained ${workspace}: ${JSON.stringify(stopped)}`,
);
}
// Exercise the actual Java server, authenticated transport, request queue and Evidence
// projection. This read-only TCP lane also runs where AF_UNIX is unavailable; it does
// not claim Windows authority or validation of the default Linux Unix-socket transport.
const provider = new GhidraProvider(
config.value,
silentLogger,
process.env,
undefined,
(options) =>
new GhidraClient({ ...options, transport: "authenticated-loopback-tcp" }),
);
const target = await parseBinaryTarget(targetPath);
if (!target.ok) throw target.error;
const profile = await provider.resolveAnalysisProfile(target.value);
if (!profile.ok) throw profile.error;
assert.ok(profile.value.profile);
assert.equal(
profile.value.profile.parameters.no_return_repair,
"returning-imports-decoded-return-v2",
);
const client = provider.createClient(target.value, profile.value.profile);
cleaned = false;
const observations = [];
const failures = [];
try {
for (const operation of [
"procedure_pseudo_code",
"procedure_assembly",
"procedure_info",
"read_function_instructions",
"analyze_function",
]) {
const result = await client.execute(operation, {
procedure: "rea_abort",
});
if (!result.ok) throw result.error;
const warnings = result.value.limitations.filter((value) =>
/terminal call/u.test(value),
);
assert.ok(
warnings.some(
(value) => /abort/u.test(value) && /fallthrough/u.test(value),
),
operation,
);
observations.push({ operation, warnings });
}
const healthy = await client.execute("procedure_pseudo_code", {
procedure: "rea_tls",
});
if (!healthy.ok) throw healthy.error;
assert.ok(
!healthy.value.limitations.some((value) => /terminal call/u.test(value)),
);
} catch (cause) {
failures.push(cause);
}
const closed = await client.close();
cleaned = closed.ok;
if (!closed.ok) failures.push(closed.error);
if (failures.length > 0)
throw new AggregateError(
failures,
"No-return Evidence verification failed",
);
assert.equal(digest(await readFile(targetPath)), targetDigest);
report = {
status: "passed",
probe,
observations,
scope:
"Real Ghidra ELF analysis with injected flags; real read-only TCP Evidence projection. No target execution; exact Amethyst artifact and Windows are unverified.",
};
} finally {
if (cleaned) await rm(workspace, { recursive: true, force: true });
}
console.log(
JSON.stringify({
...report,
cleanup: "complete",
verifier_run: await completeVerifierRun(run),
}),
);
+4
View File
@@ -84,6 +84,10 @@ export const resolveGhidraAnalysisProfile = (
location_resolution: "explicit-address-exact-entry-symbol-first-v3",
instruction_flow_evidence: "decoded-return-pcode-v1",
function_boundary_observations: "ghidra-terminal-call-limitations-v1",
no_return_repair:
installation.platform === "win32"
? "disabled-windows-p0"
: "returning-imports-decoded-return-v2",
process_launch:
installation.platform === "win32"
? "official-headless-script-v1"
+9
View File
@@ -14,6 +14,15 @@ binaries and Ghidra projects are never committed.
x86-64 PE, and x86-64 Mach-O targets from the same semantics. It preserves an
exported entry, direct and indirect calls, a volatile string reference, and a
multi-block branch across loaders.
- `no-return.c` and `ReaNoReturnProbe.java` inject incorrect import flags into a
real ELF analysis database, verify caller recovery, propagated flag repair and signature preservation,
and retain genuine/unknown no-return calls and independently identified entries.
Run `npm run build:cached`, then `node scripts/verify-real-ghidra-noreturn.mjs`
with the Ghidra/JDK environment below on Linux x64 and a procps-compatible process
table that exposes REA's current process for owned cleanup. It also checks named
limitations through the real read-only TCP bridge and provider Evidence
projection; default Unix-socket transport, Windows, and the reported Amethyst
artifact need separate verification. Fixture target code is never executed.
`scripts/verify-real-ghidra.mjs` uses `cc`, `clang`, and `lld-link` by default;
`REA_CC`, `REA_CLANG`, and `REA_LLD_LINK` can select alternate commands. The
@@ -0,0 +1,179 @@
//@category REA Verification
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import generic.jar.ResourceFile;
import ghidra.app.script.GhidraScript;
import ghidra.app.script.GhidraScriptUtil;
import ghidra.framework.Application;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.listing.FlowOverride;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Instruction;
import ghidra.program.model.symbol.SourceType;
import java.io.File;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** Injects the reported analysis flags into real imported ELF functions; never executes ELF code. */
public class ReaNoReturnProbe extends GhidraScript {
private final JsonArray checks = new JsonArray();
private final Map<Function, Address> originalEnds = new LinkedHashMap<>();
private final Map<Function, String> originalSignatures = new LinkedHashMap<>();
private void require(String name, boolean condition) {
if (!condition) throw new IllegalStateException("No-return assertion failed: " + name);
checks.add(name);
}
private Function function(String name) {
for (Function f : currentProgram.getFunctionManager().getFunctions(true))
if (!f.isThunk() && f.getName().equals(name)) return f;
throw new IllegalStateException("Missing fixture function " + name);
}
private Function external(String name) {
for (Function f : currentProgram.getFunctionManager().getExternalFunctions())
if (f.getName().equals(name)) return f;
throw new IllegalStateException("Missing imported function " + name);
}
private void truncate(Function caller, String callee) throws Exception {
truncate(caller, external(callee));
}
private void truncate(Function caller, Function callee) throws Exception {
for (Instruction ins : currentProgram.getListing().getInstructions(caller.getBody(), true)) {
if (!ins.getFlowType().isCall()) continue;
Address[] flows = ins.getFlows();
if (flows.length != 1) continue;
Function target = currentProgram.getFunctionManager().getFunctionAt(flows[0]);
if (target == null) continue;
Function root = target.isThunk() ? target.getThunkedFunction(true) : target;
if (root == null || !root.equals(callee)) continue;
originalEnds.put(caller, caller.getBody().getMaxAddress());
originalSignatures.put(caller, caller.getPrototypeString(false, true));
callee.setNoReturn(true);
ins.setFlowOverride(FlowOverride.CALL_RETURN);
Address end = ins.getMaxAddress();
clearListing(end.add(1), originalEnds.get(caller));
caller.setBody(new AddressSet(caller.getEntryPoint(), end));
require("truncated_" + callee.getName(), ins.getFlowType().isTerminal());
return;
}
throw new IllegalStateException("Missing fixture call to " + callee.getName());
}
private GhidraScript script(String name, File expected) throws Exception {
ResourceFile source = GhidraScriptUtil.findScriptByName(name);
if (source == null || !expected.equals(new File(source.getAbsolutePath()).getCanonicalFile()))
throw new IllegalStateException("Fixture resolved an unexpected script: " + name);
GhidraScript instance = GhidraScriptUtil.getProvider(source).getScriptInstance(source, errorWriter);
instance.set(getState(), getControls());
return instance;
}
@Override
public void run() throws Exception {
String[] args = getScriptArgs();
if (args.length != 1) throw new IllegalArgumentException("Expected exact bridge directory");
File directory = new File(args[0]).getCanonicalFile();
Map<String, String> imports = Map.of(
"rea_malloc", "malloc", "rea_tls", "__tls_get_addr",
"rea_memcpy", "memcpy", "rea_memset", "memset",
"rea_strlen", "strlen", "rea_errno", "__errno_location");
for (var item : imports.entrySet()) truncate(function(item.getKey()), item.getValue());
truncate(function("rea_tls_chain"), function("rea_tls"));
truncate(function("rea_tls_outer"), function("rea_tls_chain"));
function("rea_tls_outer").setNoReturn(true);
Function adjacentCaller = function("rea_malloc_adjacent");
truncate(adjacentCaller, "malloc");
Address adjacentEntry = adjacentCaller.getBody().getMaxAddress().add(1);
Function adjacent = currentProgram.getFunctionManager().createFunction(
"rea_independent_entry", adjacentEntry,
new AddressSet(adjacentEntry, originalEnds.get(adjacentCaller)), SourceType.USER_DEFINED);
AddressSet adjacentBody = new AddressSet(adjacent.getBody());
truncate(function("rea_unknown"), "rea_unknown_stop");
external("abort").setNoReturn(true);
external("_Unwind_Resume").setNoReturn(true);
Function localStop = function("rea_local_stop");
localStop.setParentNamespace(currentProgram.getSymbolTable().createNameSpace(
currentProgram.getGlobalNamespace(), "REA_LOCAL_CONTROL", SourceType.USER_DEFINED));
localStop.setName("malloc", SourceType.USER_DEFINED);
localStop.setNoReturn(true);
Function localReturn = function("rea_local_return");
localReturn.setNoReturn(true);
GhidraScript fix = script("ReaGhidraNoReturnFix.java", new File(directory, "ReaGhidraNoReturnFix.java"));
Method repair = fix.getClass().getMethod("run");
repair.invoke(fix);
for (String name : List.of("rea_tls", "rea_tls_chain", "rea_tls_outer")) {
Function caller = function(name);
require("propagated_flag_repaired_" + name, !caller.hasNoReturn());
require("propagated_body_recovered_" + name, caller.getBody().contains(originalEnds.get(caller)));
require("propagated_signature_preserved_" + name, caller.getPrototypeString(false, true).equals(originalSignatures.get(caller)));
}
for (var item : imports.entrySet()) {
Function caller = function(item.getKey());
require("returning_import_" + item.getValue(), !external(item.getValue()).hasNoReturn());
require("recovered_body_" + item.getValue(), caller.getBody().contains(originalEnds.get(caller)));
require("preserved_signature_" + item.getValue(), caller.getPrototypeString(false, true).equals(originalSignatures.get(caller)));
}
require("abort_preserved", external("abort").hasNoReturn());
require("unwind_preserved", external("_Unwind_Resume").hasNoReturn());
require("unknown_import_preserved", external("rea_unknown_stop").hasNoReturn());
require("local_name_not_whitelisted", localStop.hasNoReturn());
require("local_return_repaired", !localReturn.hasNoReturn());
require("independent_entry_preserved", adjacent.getBody().equals(adjacentBody) &&
!adjacentCaller.getBody().contains(adjacentEntry));
GhidraScript bridge = script("ReaGhidraBridge.java", new File(directory, "ReaGhidraBridge.java"));
Method warnings = bridge.getClass().getDeclaredMethod("noReturnCallLimitations", Iterable.class);
warnings.setAccessible(true);
Object reported = warnings.invoke(bridge, currentProgram.getListing().getInstructions(function("rea_unknown").getBody(), true));
require("unknown_boundary_named", reported instanceof List<?> values &&
values.stream().anyMatch(value -> value.toString().contains("rea_unknown_stop") && value.toString().contains("fallthrough")));
Object healthy = warnings.invoke(bridge, currentProgram.getListing().getInstructions(function("rea_tls").getBody(), true));
require("healthy_boundary_no_warning", healthy instanceof List<?> values && values.isEmpty());
Object guarded = warnings.invoke(bridge, currentProgram.getListing().getInstructions(adjacentCaller.getBody(), true));
require("unrepaired_boundary_named", guarded instanceof List<?> values &&
values.stream().anyMatch(value -> value.toString().contains("malloc")));
Method initialize = bridge.getClass().getDeclaredMethod("initializeDecompiler");
initialize.setAccessible(true);
initialize.invoke(bridge);
Method pseudocode = bridge.getClass().getDeclaredMethod("procedurePseudocode", JsonObject.class);
pseudocode.setAccessible(true);
JsonObject query = new JsonObject();
query.addProperty("document", currentProgram.getName());
query.addProperty("procedure", "rea_tls");
var field = bridge.getClass().getDeclaredField("decompiler");
field.setAccessible(true);
try {
Object output = pseudocode.invoke(bridge, query);
require("recovered_pseudocode", output instanceof JsonObject value &&
value.get("value").getAsString().contains("rea_after_call"));
} finally {
((ghidra.app.decompiler.DecompInterface) field.get(bridge)).dispose();
}
Map<Function, AddressSet> bodies = new LinkedHashMap<>();
for (var item : imports.entrySet()) bodies.put(function(item.getKey()), new AddressSet(function(item.getKey()).getBody()));
repair.invoke(fix);
require("idempotent_body_repair", bodies.entrySet().stream().allMatch(item -> item.getValue().equals(item.getKey().getBody())));
JsonObject report = new JsonObject();
report.addProperty("status", "passed");
report.addProperty("ghidra_version", Application.getApplicationVersion());
report.addProperty("target_sha256", currentProgram.getExecutableSHA256());
for (String name : List.of("ReaGhidraBridge.java", "ReaGhidraNoReturnFix.java")) {
byte[] bytes = Files.readAllBytes(new File(directory, name).toPath());
report.addProperty(name, HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)));
}
report.add("checks", checks);
println("REA_NO_RETURN_PROBE_JSON " + report);
}
}
+62
View File
@@ -0,0 +1,62 @@
#include <stdlib.h>
#include <string.h>
#include <errno.h>
extern void *__tls_get_addr(void *);
extern void rea_unknown_stop(void);
extern void _Unwind_Resume(void *) __attribute__((noreturn));
volatile unsigned long rea_after_call;
void *rea_malloc(size_t n) {
void *p = malloc(n);
rea_after_call++;
return p;
}
void *rea_malloc_adjacent(size_t n) {
void *p = malloc(n);
rea_after_call++;
return p;
}
__attribute__((visibility("hidden"))) void *rea_tls(void *p) {
void *value = __tls_get_addr(p);
rea_after_call++;
return value;
}
__attribute__((visibility("hidden"))) void *rea_tls_chain(void *p) {
void *value = rea_tls(p);
rea_after_call++;
return value;
}
void *rea_tls_outer(void *p) {
void *value = rea_tls_chain(p);
rea_after_call++;
return value;
}
void *rea_memcpy(void *out, const void *in, size_t n) {
void *value = memcpy(out, in, n);
rea_after_call++;
return value;
}
void *rea_memset(void *out, int c, size_t n) {
void *value = memset(out, c, n);
rea_after_call++;
return value;
}
size_t rea_strlen(const char *p) {
size_t value = strlen(p);
rea_after_call++;
return value;
}
int *rea_errno(void) {
int *p = __errno_location();
rea_after_call++;
return p;
}
void rea_unknown(void) {
rea_unknown_stop();
rea_after_call++;
}
void rea_abort(void) { abort(); }
void rea_unwind(void *p) { _Unwind_Resume(p); }
void rea_local_stop(void) { __builtin_trap(); }
int rea_local_return(void) { return 1658; }