261 lines
8.0 KiB
TypeScript
261 lines
8.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { open, readFile, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { it } from "vitest";
|
|
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
|
import { readMipsElfMetadata } from "../../../src/application/MipsElfMetadata.js";
|
|
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
|
|
import {
|
|
ghidraMipsProfileParameters,
|
|
ghidraMipsUnsupportedReason,
|
|
} from "../../../src/ghidra/GhidraMipsProfile.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
const relOffset = 4096;
|
|
const fixture = (little: boolean, count = 2): Buffer => {
|
|
const bytes = Buffer.alloc(relOffset + count * 8 + 16);
|
|
bytes.set([0x7f, 0x45, 0x4c, 0x46, 1, little ? 1 : 2, 1]);
|
|
const u16 = (value: number, offset: number) =>
|
|
little
|
|
? bytes.writeUInt16LE(value, offset)
|
|
: bytes.writeUInt16BE(value, offset);
|
|
const u32 = (value: number, offset: number) =>
|
|
little
|
|
? bytes.writeUInt32LE(value, offset)
|
|
: bytes.writeUInt32BE(value, offset);
|
|
u16(3, 16);
|
|
u16(8, 18);
|
|
u32(1, 20);
|
|
u32(52, 28);
|
|
u32(0x74001005, 36);
|
|
u16(52, 40);
|
|
u16(32, 42);
|
|
u16(2, 44);
|
|
u32(1, 52);
|
|
u32(bytes.length, 68);
|
|
u32(bytes.length, 72);
|
|
u32(2, 84);
|
|
u32(256, 88);
|
|
u32(256, 92);
|
|
u32(32, 100);
|
|
u32(32, 104);
|
|
u32(17, 256);
|
|
u32(relOffset, 260);
|
|
u32(18, 264);
|
|
u32(count * 8, 268);
|
|
u32(19, 272);
|
|
u32(8, 276);
|
|
for (let i = 1; i < count; i += 1) {
|
|
u32(512, relOffset + 8 * i);
|
|
u32(3, relOffset + 8 * i + 4);
|
|
}
|
|
return bytes;
|
|
};
|
|
const resolve = async (bytes: Buffer) => {
|
|
const directory = await createTestTempDirectory("rea-mips-shared-test-");
|
|
const path = join(directory, "shared.elf");
|
|
await writeFile(path, bytes);
|
|
const result = await parseBinaryTarget(path);
|
|
assert.ok(result.ok);
|
|
assert.equal(result.value.kind, "executable");
|
|
assert.equal(result.value.architecture, "mips");
|
|
if (
|
|
result.value.kind !== "executable" ||
|
|
result.value.architecture !== "mips"
|
|
)
|
|
throw new Error("expected MIPS ELF");
|
|
assert.equal(
|
|
result.value.sha256,
|
|
createHash("sha256").update(bytes).digest("hex"),
|
|
);
|
|
assert.deepEqual(await readFile(path), bytes);
|
|
return result.value;
|
|
};
|
|
|
|
for (const little of [true, false]) {
|
|
it(`retains complete dynamic declarations beyond the initial probe (${little ? "LE" : "BE"})`, async () => {
|
|
const target = await resolve(fixture(little));
|
|
assert.equal(target.mips.abiFlags, null);
|
|
assert.deepEqual(target.mips.sharedLoad, {
|
|
status: "complete",
|
|
relocationCount: 2,
|
|
relocationTypes: [0, 3],
|
|
symbolRelativeCount: 0,
|
|
});
|
|
assert.equal(ghidraMipsUnsupportedReason(target), null);
|
|
assert.deepEqual(ghidraMipsProfileParameters(target).mips_elf, {
|
|
elf_class: 32,
|
|
byte_order: little ? "little" : "big",
|
|
type: 3,
|
|
flags: 0x74001005,
|
|
abi_flags: null,
|
|
shared_load: {
|
|
status: "complete",
|
|
relocation_count: 2,
|
|
relocation_types: [0, 3],
|
|
symbol_relative_count: 0,
|
|
},
|
|
});
|
|
});
|
|
it(`retains and refuses symbol-bearing REL32 without repairing the mode tag (${little ? "LE" : "BE"})`, async () => {
|
|
const bytes = fixture(little);
|
|
if (little) bytes.writeUInt32LE(0x103, relOffset + 12);
|
|
else bytes.writeUInt32BE(0x103, relOffset + 12);
|
|
const target = await resolve(bytes);
|
|
assert.equal(target.mips.sharedLoad?.status, "complete");
|
|
assert.match(
|
|
ghidraMipsUnsupportedReason(target) ?? "",
|
|
/Symbol-bearing R_MIPS_REL32/u,
|
|
);
|
|
});
|
|
}
|
|
|
|
it("examines relocation records beyond a read chunk before deciding completeness", async () => {
|
|
const count = 8194;
|
|
const bytes = fixture(true, count);
|
|
bytes.writeUInt32LE(0x103, relOffset + (count - 1) * 8 + 4);
|
|
const target = await resolve(bytes);
|
|
assert.deepEqual(target.mips.sharedLoad, {
|
|
status: "complete",
|
|
relocationCount: count,
|
|
relocationTypes: [0, 3],
|
|
symbolRelativeCount: 1,
|
|
});
|
|
assert.match(ghidraMipsUnsupportedReason(target) ?? "", /mode tag/u);
|
|
});
|
|
|
|
it("does not treat uncaptured ABI declaration headers as an inspected absence", async () => {
|
|
const bytes = fixture(true);
|
|
bytes.writeUInt32LE(bytes.length + 1, 32);
|
|
bytes.writeUInt16LE(40, 46);
|
|
bytes.writeUInt16LE(1, 48);
|
|
const target = await resolve(bytes);
|
|
assert.equal(target.mips.abiFlags, null);
|
|
assert.equal(target.mips.sharedLoad?.status, "unavailable");
|
|
assert.match(
|
|
ghidraMipsUnsupportedReason(target) ?? "",
|
|
/unavailable headers remain unknown/u,
|
|
);
|
|
});
|
|
|
|
const malformed: [string, (bytes: Buffer) => void, RegExp][] = [
|
|
[
|
|
"overlapping load ranges",
|
|
(b) => {
|
|
b.writeUInt16LE(3, 44);
|
|
b.writeUInt32LE(1, 116);
|
|
b.writeUInt32LE(256, 124);
|
|
b.writeUInt32LE(16, 132);
|
|
b.writeUInt32LE(16, 136);
|
|
},
|
|
/overlapping MIPS PT_LOAD/u,
|
|
],
|
|
[
|
|
"inconsistent dynamic address",
|
|
(b) => b.writeUInt32LE(0x10000, 92),
|
|
/matching file-backed PT_LOAD/u,
|
|
],
|
|
["missing dynamic table", (b) => b.writeUInt32LE(0, 84), /no PT_DYNAMIC/u],
|
|
["repeated dynamic tables", (b) => b.writeUInt32LE(2, 52), /duplicate/u],
|
|
[
|
|
"declared dynamic tail outside file",
|
|
(b) => b.writeUInt32LE(b.length, 100),
|
|
/beyond the file/u,
|
|
],
|
|
[
|
|
"unavailable load bytes",
|
|
(b) => b.writeUInt32LE(b.length + 1, 68),
|
|
/PT_LOAD range/u,
|
|
],
|
|
["missing terminator", (b) => b.writeUInt32LE(1, 280), /DT_NULL/u],
|
|
[
|
|
"repeated scalar tags",
|
|
(b) => b.writeUInt32LE(17, 272),
|
|
/duplicate MIPS dynamic tag/u,
|
|
],
|
|
["invalid REL entry size", (b) => b.writeUInt32LE(16, 276), /DT_RELENT/u],
|
|
["truncated REL record", (b) => b.writeUInt32LE(17, 268), /DT_RELENT/u],
|
|
[
|
|
"unmapped REL table",
|
|
(b) => b.writeUInt32LE(0x10000, 260),
|
|
/PT_LOAD mapping/u,
|
|
],
|
|
[
|
|
"unmapped REL destination",
|
|
(b) => b.writeUInt32LE(0x10000, relOffset + 8),
|
|
/PT_LOAD memory range/u,
|
|
],
|
|
[
|
|
"RELA declaration",
|
|
(b) => {
|
|
b.writeUInt32LE(7, 272);
|
|
b.writeUInt32LE(4096, 276);
|
|
},
|
|
/RELA or PLT/u,
|
|
],
|
|
];
|
|
it.each(malformed)(
|
|
"preserves useful partial header facts for %s",
|
|
async (_name, mutate, reason) => {
|
|
const bytes = fixture(true);
|
|
mutate(bytes);
|
|
const target = await resolve(bytes);
|
|
assert.equal(target.mips.flags, 0x74001005);
|
|
assert.equal(target.mips.abiFlags, null);
|
|
assert.equal(target.mips.sharedLoad?.status, "unavailable");
|
|
assert.match(ghidraMipsUnsupportedReason(target) ?? "", reason);
|
|
},
|
|
);
|
|
|
|
it("retains unverified relocation type instead of claiming every examined record is supported", async () => {
|
|
const bytes = fixture(true);
|
|
bytes.writeUInt32LE(5, relOffset + 12);
|
|
const target = await resolve(bytes);
|
|
assert.deepEqual(target.mips.sharedLoad, {
|
|
status: "complete",
|
|
relocationCount: 2,
|
|
relocationTypes: [0, 5],
|
|
symbolRelativeCount: 0,
|
|
});
|
|
assert.match(
|
|
ghidraMipsUnsupportedReason(target) ?? "",
|
|
/other dynamic relocation types/u,
|
|
);
|
|
});
|
|
|
|
it("does not equate complete dynamic REL coverage with coverage of another section relocation table", async () => {
|
|
const bytes = fixture(true);
|
|
bytes.writeUInt32LE(1024, 32);
|
|
bytes.writeUInt16LE(40, 46);
|
|
bytes.writeUInt16LE(1, 48);
|
|
bytes.writeUInt32LE(9, 1028);
|
|
bytes.writeUInt32LE(2048, 1040);
|
|
bytes.writeUInt32LE(8, 1044);
|
|
bytes.writeUInt32LE(8, 1060);
|
|
const target = await resolve(bytes);
|
|
assert.equal(target.mips.sharedLoad?.status, "unavailable");
|
|
assert.match(
|
|
ghidraMipsUnsupportedReason(target) ?? "",
|
|
/section relocation declarations/u,
|
|
);
|
|
});
|
|
|
|
it("preserves the caller cancellation error during a bounded relocation scan", async () => {
|
|
const target = await resolve(fixture(true, 8194));
|
|
const handle = await open(target.path, "r");
|
|
const cancellation = new AnalysisCancelledError("open_binary");
|
|
let checks = 0;
|
|
try {
|
|
await assert.rejects(
|
|
readMipsElfMetadata(handle, target.mips, () => {
|
|
checks += 1;
|
|
if (checks === 100) throw cancellation;
|
|
}),
|
|
(error) => error === cancellation,
|
|
);
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
});
|