mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ce9776a19 | ||
|
|
5b2bb57247 | ||
|
|
3fc27b45cd | ||
|
|
b9db66256a | ||
|
|
331f2dd827 | ||
|
|
2a2fdfc0db | ||
|
|
5fad84347a | ||
|
|
027aea38a2 | ||
|
|
f584290933 |
No files matched your search
@@ -876,6 +876,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
JsonArray blocks = new JsonArray();
|
||||
JsonArray skipped = new JsonArray();
|
||||
JsonArray notLoaded = new JsonArray();
|
||||
JsonArray overlays = new JsonArray();
|
||||
List<ByteSearchBlock> eligible = new ArrayList<>();
|
||||
for (MemoryBlock block : ordered) {
|
||||
monitor.checkCancelled();
|
||||
@@ -883,10 +884,16 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
Address high = block.getEnd();
|
||||
Address bound = start == null ? end : start;
|
||||
if (bound != null && !bound.getAddressSpace().equals(low.getAddressSpace())) continue;
|
||||
if (bound == null && (!block.isLoaded() || block.isOverlay())) {
|
||||
// Without bounds, search loaded non-overlay memory. Unloaded file
|
||||
// parts and loaded overlays stay reachable through explicit bounds.
|
||||
if (bound == null && !block.isLoaded()) {
|
||||
notLoaded.add(byteSearchCoverage(block, low, high));
|
||||
continue;
|
||||
}
|
||||
if (bound == null && block.isOverlay()) {
|
||||
overlays.add(byteSearchCoverage(block, low, high));
|
||||
continue;
|
||||
}
|
||||
if (start != null && low.compareTo(start) < 0) low = start;
|
||||
if (end != null && high.compareTo(end) > 0) high = end;
|
||||
if (low.compareTo(high) > 0) continue;
|
||||
@@ -960,6 +967,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
searched.addProperty("bytes", covered);
|
||||
searched.add("skipped_uninitialized", skipped);
|
||||
searched.add("skipped_not_loaded", notLoaded);
|
||||
searched.add("skipped_overlays", overlays);
|
||||
JsonObject result = new JsonObject();
|
||||
result.add("matches", matches);
|
||||
result.add("searched", searched);
|
||||
|
||||
+11
-4
@@ -335,10 +335,17 @@ must be constrained.
|
||||
|
||||
Optional `start` and `end` addresses are inclusive and select one address space.
|
||||
A match must end at or before `end`, so `end` set inside a match excludes it.
|
||||
Without bounds, loaded memory blocks are eligible, in Ghidra address order.
|
||||
Blocks that are not loaded (file parts such as `.symtab`) and overlay blocks are
|
||||
listed in `searched.skipped_not_loaded` and are not searched; pass `start` or
|
||||
`end` in their address space to search them.
|
||||
Without bounds, loaded non-overlay memory blocks are eligible, in Ghidra address
|
||||
order, including across adjacent initialized blocks. Blocks that are not loaded
|
||||
(file parts such as `.symtab`) are listed in `searched.skipped_not_loaded`;
|
||||
loaded overlay blocks are listed separately in `searched.skipped_overlays`.
|
||||
Neither is searched by default; pass `start` or `end` in its address space to
|
||||
search it. A block that is both unloaded and an overlay is listed once under
|
||||
`skipped_not_loaded`.
|
||||
The separate overlay field changes the byte-search result contract and the
|
||||
Ghidra analysis profile. Refresh cached MCP schemas; snapshots from the older
|
||||
profile are not replayed. Previously exported Evidence remains importable as
|
||||
historical data with its original result shape.
|
||||
Overlapping matches are included, including across adjacent eligible initialized
|
||||
blocks in the same address space. Gaps and excluded or uninitialized blocks break
|
||||
a search run. Each match reports its address, starting segment, unique source file
|
||||
|
||||
@@ -531,7 +531,7 @@ the entry changes without `provider_id`, preservation of a session annotation
|
||||
and the target/provider receipt after a rejected open, unchanged source bytes,
|
||||
and owned cleanup. Six-byte x86 and x86_64 controls also verify supported compiler
|
||||
specifications, independent mappings, unchanged bytes, and decompilation of a
|
||||
known return value. AArch64 remains covered by deterministic tests rather than
|
||||
known return value. On a disposable Ghidra project, MCP `search_bytes` also checks a manually created loaded `overlay_test` block: an unbounded search lists it in `skipped_overlays` and not in `skipped_not_loaded`, and bounds in that overlay space find the pattern. This lane does not build an overlapping-load ELF, does not call CLI `search-bytes`, and does not page an overlay search across a snapshot reopen. AArch64 remains covered by deterministic tests rather than
|
||||
this real-provider lane. See [raw binary analysis](raw-binary.md).
|
||||
|
||||
`npm run verify:ghidra:address-context` runs on Linux with a caller-installed
|
||||
|
||||
@@ -760,7 +760,12 @@ async function verifyCrossBlockProject(target, targetSha256) {
|
||||
!all.matches.some((match) => match.address.startsWith("overlay_test:")),
|
||||
);
|
||||
assert.ok(
|
||||
all.searched.skipped_not_loaded.some(
|
||||
!all.searched.skipped_not_loaded.some(
|
||||
(block) => block.segment === "overlay_test",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
all.searched.skipped_overlays.some(
|
||||
(block) => block.segment === "overlay_test",
|
||||
),
|
||||
);
|
||||
|
||||
@@ -51,15 +51,26 @@ export const registerCoreNativeCommands = (
|
||||
): void => {
|
||||
registerAnnotationCommand(cli, logger, runDirectAnalysis);
|
||||
cli.command(CLI_COMMANDS.searchBytes, {
|
||||
description: "Find masked byte patterns in initialized provider memory",
|
||||
description:
|
||||
"Find masked bytes; unbounded searches skip unloaded blocks and overlays",
|
||||
args: z.object({
|
||||
path: localPathStringSchema.describe("Local executable path"),
|
||||
pattern: bytePatternFields.pattern,
|
||||
}),
|
||||
options: z.object({
|
||||
mask: bytePatternFields.mask,
|
||||
start: z.string().optional().describe("Inclusive start address"),
|
||||
end: z.string().optional().describe("Inclusive end address"),
|
||||
start: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Inclusive start address; use an overlay address space to search it",
|
||||
),
|
||||
end: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Inclusive end address; use an overlay address space to search it",
|
||||
),
|
||||
cursor: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -11,7 +11,7 @@ export const HOPPER_MEMORY_TOOL_DEFINITIONS = [
|
||||
{
|
||||
name: "search_bytes",
|
||||
description:
|
||||
"Find masked byte signatures in initialized analyzed memory. Hex byte pairs and ?? wildcards are supported; an optional equal-length hex mask selects bits. Bounds are inclusive. Matches are ordered by provider address and may span adjacent eligible initialized blocks; gaps and excluded blocks end a contiguous range. A match’s segment and file offset describe its starting address. Coverage describes this page; reuse the same query with next_cursor to continue. Uninitialized blocks are listed separately. Ghidra only; Hopper is unavailable.",
|
||||
"Find masked byte signatures in initialized analyzed memory. Hex byte pairs and ?? wildcards are supported; an optional equal-length hex mask selects bits. Bounds are inclusive. Without bounds, unloaded blocks and loaded overlays are skipped and listed separately; select an overlay address space with start or end to search it. Matches are ordered by provider address and may span adjacent eligible initialized blocks; gaps and excluded blocks end a contiguous range. A match’s segment and file offset describe its starting address. Coverage describes this page; reuse the same query with next_cursor to continue. Uninitialized blocks are listed separately. Ghidra only; Hopper is unavailable.",
|
||||
inputSchema: z
|
||||
.strictObject({
|
||||
...bytePatternFields,
|
||||
|
||||
@@ -22,6 +22,7 @@ it.each(["0x20", "overlay:0x10", "invalid"])(
|
||||
bytes: 16,
|
||||
skipped_uninitialized: [],
|
||||
skipped_not_loaded: [],
|
||||
skipped_overlays: [],
|
||||
},
|
||||
complete: true,
|
||||
next_cursor: null,
|
||||
@@ -52,6 +53,7 @@ it.each(["", "overlay:"])(
|
||||
bytes: 16,
|
||||
skipped_uninitialized: [],
|
||||
skipped_not_loaded: [],
|
||||
skipped_overlays: [],
|
||||
},
|
||||
complete: true,
|
||||
next_cursor: null,
|
||||
|
||||
@@ -165,9 +165,22 @@ export const byteSearchResultSchema = z
|
||||
blocks: z.array(coverageBlock),
|
||||
bytes: z.int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
||||
skipped_uninitialized: z.array(coverageBlock),
|
||||
skipped_not_loaded: z.array(coverageBlock),
|
||||
skipped_not_loaded: z
|
||||
.array(coverageBlock)
|
||||
.describe(
|
||||
"Unloaded blocks skipped by an unbounded search, including unloaded overlays.",
|
||||
),
|
||||
skipped_overlays: z
|
||||
.array(coverageBlock)
|
||||
.describe(
|
||||
"Loaded overlay blocks skipped by an unbounded search; select their address space with a bound.",
|
||||
),
|
||||
}),
|
||||
complete: z.boolean(),
|
||||
complete: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"No further matches remain in the eligible selection; skipped blocks were not searched.",
|
||||
),
|
||||
next_cursor: z.string().min(1).nullable(),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
|
||||
@@ -70,6 +70,7 @@ const sharedAnalysisSemantics = {
|
||||
instruction_flow_evidence: "decoded-return-pcode-v1",
|
||||
instruction_status: "unlisted-address-without-decode-v1",
|
||||
instruction_references: "exact-address-incoming-without-synthetic-entry-v3",
|
||||
byte_search_coverage: "separate-loaded-overlays-v1",
|
||||
function_boundary_observations: "ghidra-function-boundary-limitations-v4",
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -367,6 +367,14 @@ const byteSearch = (space = "") => ({
|
||||
bytes: 16,
|
||||
},
|
||||
],
|
||||
skipped_overlays: [
|
||||
{
|
||||
segment: "overlay",
|
||||
start: `${space}0x0040`,
|
||||
end: `${space}0x004f`,
|
||||
bytes: 16,
|
||||
},
|
||||
],
|
||||
},
|
||||
complete: true,
|
||||
next_cursor: null as string | null,
|
||||
@@ -392,6 +400,8 @@ it.each([
|
||||
["uninitialized end", ["searched", "skipped_uninitialized", 0, "end"]],
|
||||
["unloaded start", ["searched", "skipped_not_loaded", 0, "start"]],
|
||||
["unloaded end", ["searched", "skipped_not_loaded", 0, "end"]],
|
||||
["overlay start", ["searched", "skipped_overlays", 0, "start"]],
|
||||
["overlay end", ["searched", "skipped_overlays", 0, "end"]],
|
||||
] as const)(
|
||||
"rejects noncanonical %s at its owning address field",
|
||||
(_label, path) => {
|
||||
|
||||
@@ -445,6 +445,7 @@ const resultSchemas = {
|
||||
blocks: z.array(canonicalByteCoverage),
|
||||
skipped_uninitialized: z.array(canonicalByteCoverage),
|
||||
skipped_not_loaded: z.array(canonicalByteCoverage),
|
||||
skipped_overlays: z.array(canonicalByteCoverage),
|
||||
}),
|
||||
}),
|
||||
inspect_native_load_image: nativeLoadImageObservationSchema,
|
||||
|
||||
@@ -138,7 +138,7 @@ export const limitationsFor = (
|
||||
case "search_bytes":
|
||||
return [
|
||||
...common,
|
||||
"Searches contiguous eligible initialized loader-adjusted memory in provider address order, including across adjacent blocks. Bounds and page coverage ends are inclusive. Uninitialized blocks are listed separately and break search runs. Overlapping matches are included. A cursor resumes the same target, profile and query. Cancellation discards the active search and returns no successful partial page.",
|
||||
"Searches contiguous eligible initialized loader-adjusted memory in provider address order, including across adjacent blocks. Bounds and page coverage ends are inclusive. Unbounded searches separately list skipped unloaded blocks and loaded overlays; use an overlay address-space bound to search it. Uninitialized blocks are listed separately and break search runs. Overlapping matches are included. A cursor resumes the same target, profile and query. Cancellation discards the active search and returns no successful partial page.",
|
||||
];
|
||||
case "address_to_file_offset":
|
||||
return [
|
||||
|
||||
@@ -11,9 +11,9 @@ import { GhidraProvider } from "./GhidraProvider.js";
|
||||
// bit-field base-type revisions that prevent reuse of snapshots with earlier
|
||||
// results.
|
||||
const RAW_DIGESTS = {
|
||||
"6502": "6f904539c00521b04f76a3bfd1ca88275e080b5751f9e2305de66c20d0bd2fa0",
|
||||
"6502": "9f1632d3209c7cb3b17be1de28311aedf3a1b23d66ac45ac0006a4cd9a956ac1",
|
||||
"x86-disabled-analyzer":
|
||||
"5a534d959baf34abdafaea64ae9fcb9126553802d42ffa5e4bc0426de5f0af8c",
|
||||
"ab9dd0676e0e647654b70c4dca25e1efeae3867c52623af0cd4a842f3f7aab68",
|
||||
} as const;
|
||||
|
||||
const configFor = (environment: NodeJS.ProcessEnv) => {
|
||||
@@ -50,6 +50,7 @@ describe("raw-binary analysis profile digest", () => {
|
||||
"source-mappings-entry-context-relocation-symbol-v3",
|
||||
instruction_status: "unlisted-address-without-decode-v1",
|
||||
byte_search_boundary: "contiguous-eligible-initialized-blocks-v1",
|
||||
byte_search_coverage: "separate-loaded-overlays-v1",
|
||||
data_type_observations: "bit-field-base-and-lookup-v1",
|
||||
analyzer_preset: "ghidra-default",
|
||||
});
|
||||
|
||||
@@ -307,6 +307,11 @@ describe("Ghidra relationship coverage replay", () => {
|
||||
|
||||
describe("Ghidra snapshot profile admission", () => {
|
||||
it.each([
|
||||
{
|
||||
parameter: "byte_search_coverage",
|
||||
current: "separate-loaded-overlays-v1",
|
||||
legacy: undefined,
|
||||
},
|
||||
{
|
||||
parameter: "data_type_observations",
|
||||
current: "bit-field-base-and-lookup-v1",
|
||||
|
||||
@@ -75,6 +75,14 @@ it("retains byte-search coverage and rejects malformed pages before creating Evi
|
||||
skipped_not_loaded: [
|
||||
{ segment: ".symtab", start: "0x0", end: "0x17f", bytes: 384 },
|
||||
],
|
||||
skipped_overlays: [
|
||||
{
|
||||
segment: "overlay",
|
||||
start: "overlay:0x401000",
|
||||
end: "overlay:0x40100f",
|
||||
bytes: 16,
|
||||
},
|
||||
],
|
||||
},
|
||||
complete: false,
|
||||
next_cursor: "continuation",
|
||||
@@ -116,6 +124,27 @@ it("retains byte-search coverage and rejects malformed pages before creating Evi
|
||||
expect(parseMcpToolError(malformed)).toMatchObject({
|
||||
error: { code: "unreadable_output" },
|
||||
});
|
||||
output = jsonValueSchema.parse({
|
||||
...page,
|
||||
searched: {
|
||||
...page.searched,
|
||||
skipped_overlays: [
|
||||
{
|
||||
segment: "overlay",
|
||||
start: "overlay:0x401000",
|
||||
end: "overlay:0x40100f",
|
||||
bytes: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const invalidOverlay = await harness.mcp.callTool({
|
||||
name: "search_bytes",
|
||||
arguments: { pattern: "48 ??" },
|
||||
});
|
||||
expect(parseMcpToolError(invalidOverlay)).toMatchObject({
|
||||
error: { code: "unreadable_output" },
|
||||
});
|
||||
for (const block of [
|
||||
{ segment: ".text", start: "0x1000", end: "0x1009", bytes: 1 },
|
||||
{ segment: ".text", start: "0x1009", end: "0x1000", bytes: 1 },
|
||||
|
||||
@@ -101,6 +101,7 @@ describe("official operation arguments", () => {
|
||||
bytes: 0,
|
||||
skipped_uninitialized: [],
|
||||
skipped_not_loaded: [],
|
||||
skipped_overlays: [],
|
||||
},
|
||||
complete: true,
|
||||
next_cursor: null,
|
||||
|
||||
Vendored
+1
@@ -636,6 +636,7 @@ const inventoryResultFor = (method, params) => {
|
||||
},
|
||||
],
|
||||
skipped_not_loaded: [],
|
||||
skipped_overlays: [],
|
||||
},
|
||||
complete: !paged,
|
||||
next_cursor: paged ? "fixture-second-page" : null,
|
||||
|
||||
Reference in new issue
Block a user