150 lines
4.5 KiB
JavaScript
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)}`;
|
|
}
|