mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(pe): bound forwarders to export directory
This commit is contained in:
1 parent
50822e1eff
commit
1fe92bff8e
3 files changed
+90
-3
No files matched your search
@@ -108,6 +108,47 @@ describe("PE export name coverage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("PE forwarder directory bounds", () => {
|
||||
it.each([32, 64] as const)(
|
||||
"requires the complete PE%d forwarder string to remain inside the export directory",
|
||||
(bits) => {
|
||||
const bytes = peExportImage({
|
||||
bits,
|
||||
functions: [{ rva: 0x11fc }, { rva: 0x2000 }],
|
||||
names: [{ name: "Kept", index: 1 }],
|
||||
});
|
||||
// The string starts in .edata's declared export range but its NUL is
|
||||
// outside it. The adjacent section bytes remain mapped and readable.
|
||||
bytes.write("DLL.Target\0", 0x5fc, "latin1");
|
||||
const result = parse(bytes);
|
||||
|
||||
expect(result.exports).toMatchObject([{ name: "Kept", ordinal: 2 }]);
|
||||
expect(result.issues).toMatchObject([
|
||||
{ code: "malformed_entry", location: "0x11fc" },
|
||||
]);
|
||||
expect(result.coverage).toMatchObject({
|
||||
status: "partial",
|
||||
examined_entries: 2,
|
||||
exports: 1,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([32, 64] as const)(
|
||||
"accepts a PE%d forwarder whose NUL is the final export-directory byte",
|
||||
(bits) => {
|
||||
const bytes = peExportImage({ bits, functions: [{ rva: 0x11f5 }] });
|
||||
bytes.write("DLL.Target\0", 0x5f5, "latin1");
|
||||
|
||||
expect(parse(bytes)).toMatchObject({
|
||||
exports: [{ name: null, ordinal: 1, forwarder: "DLL.Target" }],
|
||||
coverage: { status: "complete", exports: 1 },
|
||||
issues: [],
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("PE export directories", () => {
|
||||
it.each([32, 64] as const)(
|
||||
"reports named, aliased, ordinal-only and forwarded PE%d exports",
|
||||
|
||||
@@ -132,12 +132,15 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
||||
const mappings = imageFileMappings(image);
|
||||
const map = (rva: number, size: number): number =>
|
||||
mappedFileOffset(mappings, base + BigInt(rva), size, image.zero_fills);
|
||||
const cStringBytes = (rva: number): Buffer => {
|
||||
const cStringBytes = (
|
||||
rva: number,
|
||||
maximumBytes = MAXIMUM_NAME_BYTES,
|
||||
): Buffer => {
|
||||
const value = readMappedCString(
|
||||
bytes,
|
||||
image,
|
||||
base + BigInt(rva),
|
||||
MAXIMUM_NAME_BYTES,
|
||||
maximumBytes,
|
||||
);
|
||||
if (value.length === 0)
|
||||
throw new BinaryFormatFailure(
|
||||
@@ -216,7 +219,12 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
||||
try {
|
||||
target = {
|
||||
address: null,
|
||||
forwarder: cStringBytes(rva).toString("latin1"),
|
||||
// PE forwarder strings, including their NUL, must fit within the
|
||||
// export data directory. Ordinary export names have no such bound.
|
||||
forwarder: cStringBytes(
|
||||
rva,
|
||||
Math.min(MAXIMUM_NAME_BYTES, forwarderEnd - rva - 1),
|
||||
).toString("latin1"),
|
||||
};
|
||||
} catch (cause) {
|
||||
issues.push({
|
||||
|
||||
@@ -13,6 +13,44 @@ import {
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
describe("native export stable filesystem boundary", () => {
|
||||
it("retains source identity and usable exports when a forwarder leaves its declared directory", async () => {
|
||||
const root = await createTestTempDirectory("rea-native-export-forwarder-");
|
||||
const path = join(root, "truncated-forwarder.dll");
|
||||
const bytes = peExportImage({
|
||||
functions: [{ rva: 0x11fc }, { rva: 0x2000 }],
|
||||
names: [{ name: "Kept", index: 1 }],
|
||||
});
|
||||
bytes.write("DLL.Target\0", 0x5fc, "latin1");
|
||||
await writeFile(path, bytes);
|
||||
const service = createNativeExportsService();
|
||||
try {
|
||||
const result = await service.inspect({ path });
|
||||
if (!result.ok) throw new Error(JSON.stringify(result.error));
|
||||
const evidence = parseEvidence(result.value);
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
expect(evidence.subject).toMatchObject({
|
||||
local_path: path,
|
||||
digest: { sha256 },
|
||||
});
|
||||
expect(
|
||||
nativeExportsSchema.parse(evidence.normalized_result),
|
||||
).toMatchObject({
|
||||
artifact: { path, sha256, bytes: bytes.length },
|
||||
exports: [{ name: "Kept", ordinal: 2 }],
|
||||
issues: [
|
||||
{
|
||||
code: "malformed_entry",
|
||||
location: "0x11fc",
|
||||
message: expect.stringContaining("Forwarder of ordinal 1"),
|
||||
},
|
||||
],
|
||||
coverage: { status: "partial", examined_entries: 2, exports: 1 },
|
||||
});
|
||||
} finally {
|
||||
await service.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("bounds retained PE aliases before interpreting their target", async () => {
|
||||
const root = await createTestTempDirectory("rea-native-export-aliases-");
|
||||
const service = createNativeExportsService();
|
||||
|
||||
Reference in new issue
Block a user