rea/scripts/lib/ghidra-windows-byte-search.mjs

150 lines
4.5 KiB
JavaScript

import assert from "node:assert/strict";
export const WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE = Object.freeze({
pattern: "a0 ?? c0",
mask: "f0 ff f0",
startFileOffset: 0x380,
endFileOffset: 0x392,
markerOffsets: Object.freeze([0x380, 0x390]),
});
/** Put two masked, wildcarded signatures in loaded, source-owned PE bytes. */
export function writeWindowsGhidraByteSearchFixture(bytes) {
bytes.set([0xa1, 0xb2, 0xc3], 0x380);
bytes.set([0xa4, 0xb5, 0xc6], 0x390);
}
/** Verify real Ghidra search pages against the immutable source fixture. */
export async function verifyWindowsGhidraByteSearch({
invoke,
sourceBytes,
architecture,
sourceOwnedFixture = true,
fallbackAddress,
fallbackFileOffset,
runCliPage,
}) {
const imageBase = architecture === "x86" ? 0x400000n : 0x140000000n;
const query = sourceOwnedFixture
? {
pattern: WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE.pattern,
mask: WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE.mask,
start: addressAtFileOffset(
imageBase,
WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE.startFileOffset,
),
end: addressAtFileOffset(
imageBase,
WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE.endFileOffset,
),
limit: 1,
}
: {
pattern: sourceBytes
.subarray(fallbackFileOffset, fallbackFileOffset + 1)
.toString("hex"),
start: fallbackAddress,
end: fallbackAddress,
limit: 1,
};
if (!sourceOwnedFixture) {
const result = await invoke("search_bytes", query);
assert.equal(result.complete, true);
assert.equal(result.next_cursor, null);
assert.deepEqual(
result.matches.map(({ address, file_offset }) => ({
address,
file_offset,
})),
[{ address: fallbackAddress, file_offset: fallbackFileOffset }],
);
assert.ok(result.searched.bytes > 0);
return { bounded_source_match: true };
}
const { startFileOffset, endFileOffset } = WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE;
const expected = [];
for (
let fileOffset = startFileOffset;
fileOffset + 2 <= endFileOffset;
fileOffset += 1
) {
const first = sourceBytes[fileOffset];
const last = sourceBytes[fileOffset + 2];
if (first === undefined || last === undefined) break;
if ((first & 0xf0) === 0xa0 && (last & 0xf0) === 0xc0)
expected.push({
address: addressAtFileOffset(imageBase, fileOffset),
file_offset: fileOffset,
});
}
assert.deepEqual(
expected.map(({ file_offset }) => file_offset),
[...WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE.markerOffsets],
"The source-owned byte oracle must retain both distinct signatures",
);
const first = await invoke("search_bytes", query);
assert.deepEqual(
first.matches.map(({ address, file_offset }) => ({ address, file_offset })),
[expected[0]],
);
assert.equal(first.complete, false);
assert.equal(typeof first.next_cursor, "string");
assert.ok(first.searched.bytes > 0);
if (runCliPage !== undefined) {
const cli = await runCliPage(query);
assert.deepEqual(cli.matches, first.matches);
assert.deepEqual(cli.searched, first.searched);
assert.equal(cli.complete, first.complete);
}
const second = await invoke("search_bytes", {
...query,
cursor: first.next_cursor,
});
assert.deepEqual(
second.matches.map(({ address, file_offset }) => ({
address,
file_offset,
})),
[expected[1]],
);
assert.equal(second.complete, true);
assert.equal(second.next_cursor, null);
assert.ok(second.searched.bytes > 0);
const covered = new Set();
for (const page of [first, second]) {
assert.equal(
page.searched.bytes,
page.searched.blocks.reduce((sum, block) => sum + block.bytes, 0),
);
for (const block of page.searched.blocks) {
const start = BigInt(block.start);
const end = BigInt(block.end);
assert.ok(start >= BigInt(query.start) && end <= BigInt(query.end));
for (let offset = start; offset <= end; offset += 1n)
covered.add(offset.toString());
}
}
assert.equal(
covered.size,
endFileOffset - startFileOffset + 1,
"Paged byte-search coverage must account for the complete bounded source range",
);
return {
bounded: true,
wildcardAndMask: true,
matches: expected,
continuation: true,
examinedBytes: covered.size,
cliMcpParity: runCliPage !== undefined,
};
}
function addressAtFileOffset(imageBase, fileOffset) {
return `0x${(imageBase + 0x1000n + BigInt(fileOffset - 0x200)).toString(16)}`;
}