mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
fix(ghidra): recover returning import callers
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
);
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
Reference in New Issue
Block a user