fix(ghidra): normalize Mach-O imported symbol names

This commit is contained in:
morluto 2026-10-11 09:54:07 +08:00
parent 49e6f00ed0
commit f88c99021b
5 changed files with 55 additions and 18 deletions

View File

@ -33,7 +33,7 @@ public final class ReaGhidraNoReturnFix extends GhidraScript {
for (Function f : fm.getExternalFunctions()) {
monitor.checkCancelled();
// Do not apply import-name assumptions to local functions or to arbitrary externals.
if (!f.hasNoReturn() || !RETURNING_IMPORTS.contains(f.getName())) continue;
if (!f.hasNoReturn() || !RETURNING_IMPORTS.contains(externalAbiName(f))) continue;
f.setNoReturn(false);
enqueueReturning(f, cleared, pending);
}
@ -98,6 +98,14 @@ public final class ReaGhidraNoReturnFix extends GhidraScript {
}
}
private String externalAbiName(Function function) {
String name = function.getName();
// Mach-O stores C-linkage symbols with one object-format underscore prefix.
if (currentProgram.getExecutableFormat().contains("Mach-O") && name.startsWith("_"))
return name.substring(1);
return name;
}
private boolean clearDecodedReturn(Function function, Listing listing) throws Exception {
if (!function.hasNoReturn() || function.isThunk()) return false;
for (Instruction instruction : listing.getInstructions(function.getBody(), true)) {

View File

@ -23,8 +23,16 @@ 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 procedureName = (name) =>
process.platform === "darwin" ? `_${name}` : name;
assert.ok(
process.platform === "linux" || process.platform === "darwin",
"This regression lane requires Linux or macOS",
);
assert.ok(
process.arch === "x64" || process.arch === "arm64",
"This regression lane requires x86-64 or arm64",
);
const config = parseConfig(process.env);
if (!config.ok) throw config.error;
const installation = inspectGhidraInstallation({
@ -43,9 +51,22 @@ let cleaned = true;
let report;
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
try {
const targetPath = join(workspace, "noreturn.so");
const targetPath = join(
workspace,
process.platform === "darwin" ? "noreturn.dylib" : "noreturn.so",
);
const targetOptions =
process.platform === "darwin"
? [
"-dynamiclib",
"-undefined",
"dynamic_lookup",
"-U_FORTIFY_SOURCE",
"-D_FORTIFY_SOURCE=0",
]
: ["-shared"];
await exec(process.env.REA_CC ?? "cc", [
"-shared",
...targetOptions,
"-fPIC",
"-O0",
"-fno-builtin",
@ -154,7 +175,7 @@ try {
"analyze_function",
]) {
const result = await client.execute(operation, {
procedure: "rea_abort",
procedure: procedureName("rea_abort"),
});
if (!result.ok) throw result.error;
const warnings = result.value.limitations.filter((value) =>
@ -169,7 +190,7 @@ try {
observations.push({ operation, warnings });
}
const healthy = await client.execute("procedure_pseudo_code", {
procedure: "rea_tls",
procedure: procedureName("rea_tls"),
});
if (!healthy.ok) throw healthy.error;
assert.ok(
@ -192,7 +213,7 @@ try {
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.",
"Real Ghidra ELF or Mach-O 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 });

View File

@ -15,12 +15,13 @@ binaries and Ghidra projects are never committed.
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
real ELF or Mach-O 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 or macOS x64/arm64. On Linux, use 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.

View File

@ -21,7 +21,7 @@ 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. */
/** Injects reported analysis flags into imported ELF/Mach-O functions; never executes target code. */
public class ReaNoReturnProbe extends GhidraScript {
private final JsonArray checks = new JsonArray();
private final Map<Function, Address> originalEnds = new LinkedHashMap<>();
@ -32,15 +32,21 @@ public class ReaNoReturnProbe extends GhidraScript {
checks.add(name);
}
private boolean matchesSymbol(Function function, String name) {
String observed = function.getName();
// Mach-O symbol names use a leading underscore for C-linkage names.
return observed.equals(name) || observed.equals("_" + name);
}
private Function function(String name) {
for (Function f : currentProgram.getFunctionManager().getFunctions(true))
if (!f.isThunk() && f.getName().equals(name)) return f;
if (!f.isThunk() && matchesSymbol(f, 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;
if (matchesSymbol(f, name)) return f;
throw new IllegalStateException("Missing imported function " + name);
}
@ -151,7 +157,7 @@ public class ReaNoReturnProbe extends GhidraScript {
pseudocode.setAccessible(true);
JsonObject query = new JsonObject();
query.addProperty("document", currentProgram.getName());
query.addProperty("procedure", "rea_tls");
query.addProperty("procedure", function("rea_tls").getName());
var field = bridge.getClass().getDeclaredField("decompiler");
field.setAccessible(true);
try {

View File

@ -3,6 +3,7 @@
#include <errno.h>
extern void *__tls_get_addr(void *);
extern int *__errno_location(void);
extern void rea_unknown_stop(void);
extern void _Unwind_Resume(void *) __attribute__((noreturn));
volatile unsigned long rea_after_call;