feat(pe): add broader PE/COFF/PDB static inspection
Expand PE/COFF/PDB inspection to cover imports, exports, resources, manifests, signatures, debug identifiers, sections, relocations, TLS callbacks, and architecture metadata (#362). - peInspection.ts: domain schemas for PE manifests with sections, imports, exports, resources, debug entries, TLS callbacks, and relocations. Parsers for machine types, section characteristics, image characteristics, and debug directory types. - Public contracts do not expose macOS-only vocabulary. - Unsupported capabilities are reported explicitly. Closes #362
This commit is contained in:
parent
a5e355a3d5
commit
94e6600faf
|
|
@ -0,0 +1,225 @@
|
|||
import { z } from "zod";
|
||||
|
||||
/** PE/COFF section characteristics flags. */
|
||||
export const sectionCharacteristicsSchema = z.object({
|
||||
/** Section contains executable code. */
|
||||
has_code: z.boolean().default(false),
|
||||
/** Section contains initialized data. */
|
||||
has_initialized_data: z.boolean().default(false),
|
||||
/** Section contains uninitialized data. */
|
||||
has_uninitialized_data: z.boolean().default(false),
|
||||
/** Section can be read. */
|
||||
readable: z.boolean().default(false),
|
||||
/** Section can be written. */
|
||||
writable: z.boolean().default(false),
|
||||
/** Section can be executed. */
|
||||
executable: z.boolean().default(false),
|
||||
});
|
||||
|
||||
/** PE/COFF section header. */
|
||||
export const peSectionSchema = z.strictObject({
|
||||
name: z.string().min(1),
|
||||
virtual_size: z.number().int().nonnegative(),
|
||||
virtual_address: z.number().int().nonnegative(),
|
||||
raw_size: z.number().int().nonnegative(),
|
||||
raw_offset: z.number().int().nonnegative(),
|
||||
characteristics: sectionCharacteristicsSchema,
|
||||
});
|
||||
export type PeSection = z.infer<typeof peSectionSchema>;
|
||||
|
||||
/** PE import entry. */
|
||||
export const peImportSchema = z.strictObject({
|
||||
dll: z.string().min(1),
|
||||
function_name: z.string().min(1),
|
||||
ordinal: z.number().int().nullable(),
|
||||
is_delay_import: z.boolean().default(false),
|
||||
});
|
||||
export type PeImport = z.infer<typeof peImportSchema>;
|
||||
|
||||
/** PE export entry. */
|
||||
export const peExportSchema = z.strictObject({
|
||||
name: z.string().min(1),
|
||||
ordinal: z.number().int().nonnegative(),
|
||||
rva: z.number().int().nonnegative(),
|
||||
forwarder: z.string().nullable(),
|
||||
});
|
||||
export type PeExport = z.infer<typeof peExportSchema>;
|
||||
|
||||
/** PE/COFF resource entry. */
|
||||
export const peResourceSchema = z.strictObject({
|
||||
type: z.string().min(1),
|
||||
name: z.string().nullable(),
|
||||
language: z.number().int().nullable(),
|
||||
rva: z.number().int().nonnegative(),
|
||||
size: z.number().int().nonnegative(),
|
||||
});
|
||||
export type PeResource = z.infer<typeof peResourceSchema>;
|
||||
|
||||
/** PE debug directory entry. */
|
||||
export const peDebugEntrySchema = z.strictObject({
|
||||
type: z.string().min(1),
|
||||
rva: z.number().int().nonnegative(),
|
||||
size: z.number().int().nonnegative(),
|
||||
/** PDB GUID if this is a CodeView entry. */
|
||||
pdb_guid: z.string().nullable(),
|
||||
/** PDB age if this is a CodeView entry. */
|
||||
pdb_age: z.number().int().nullable(),
|
||||
/** PDB path if this is a CodeView entry. */
|
||||
pdb_path: z.string().nullable(),
|
||||
});
|
||||
export type PeDebugEntry = z.infer<typeof peDebugEntrySchema>;
|
||||
|
||||
/** TLS callback entry. */
|
||||
export const peTlsCallbackSchema = z.strictObject({
|
||||
callback_rva: z.number().int().nonnegative(),
|
||||
index: z.number().int().nonnegative(),
|
||||
});
|
||||
export type PeTlsCallback = z.infer<typeof peTlsCallbackSchema>;
|
||||
|
||||
/** Architecture/machine type. */
|
||||
export const peMachineTypeSchema = z.enum([
|
||||
"x86",
|
||||
"x64",
|
||||
"arm",
|
||||
"arm64",
|
||||
"ia64",
|
||||
"unknown",
|
||||
]);
|
||||
export type PeMachineType = z.infer<typeof peMachineTypeSchema>;
|
||||
|
||||
/** PE/COFF manifest for static inspection. */
|
||||
export const peManifestSchema = z.strictObject({
|
||||
/** DOS header magic. */
|
||||
dos_magic: z.literal(0x5a4d),
|
||||
/** PE signature. */
|
||||
pe_signature: z.literal(0x00004550),
|
||||
/** Machine type. */
|
||||
machine: peMachineTypeSchema,
|
||||
/** Number of sections. */
|
||||
number_of_sections: z.number().int().positive(),
|
||||
/** Timestamp from the COFF header. */
|
||||
timestamp: z.number().int().nullable(),
|
||||
/** PE characteristics flags. */
|
||||
is_dll: z.boolean().default(false),
|
||||
is_executable: z.boolean().default(false),
|
||||
is_console: z.boolean().default(true),
|
||||
/** Entry point RVA. */
|
||||
entry_point: z.number().int().nonnegative(),
|
||||
/** Image base. */
|
||||
image_base: z.number().int().nonnegative(),
|
||||
/** Section alignment. */
|
||||
section_alignment: z.number().int().positive(),
|
||||
/** Subsystem version. */
|
||||
subsystem_version: z.string().nullable(),
|
||||
/** Sections. */
|
||||
sections: z.array(peSectionSchema).min(0).max(96),
|
||||
/** Imports. */
|
||||
imports: z.array(peImportSchema).default([]),
|
||||
/** Exports. */
|
||||
exports: z.array(peExportSchema).default([]),
|
||||
/** Resources. */
|
||||
resources: z.array(peResourceSchema).default([]),
|
||||
/** Debug directory entries. */
|
||||
debug_entries: z.array(peDebugEntrySchema).default([]),
|
||||
/** TLS callbacks. */
|
||||
tls_callbacks: z.array(peTlsCallbackSchema).default([]),
|
||||
/** Relocations count (not all entries). */
|
||||
relocations_count: z.number().int().nonnegative().default(0),
|
||||
/** Image file characteristics. */
|
||||
image_characteristics: z.array(z.string()).default([]),
|
||||
});
|
||||
export type PeManifest = z.infer<typeof peManifestSchema>;
|
||||
|
||||
/** Parse machine type from COFF machine field. */
|
||||
export function parseMachineType(machine: number): PeMachineType {
|
||||
switch (machine) {
|
||||
case 0x014c:
|
||||
return "x86";
|
||||
case 0x8664:
|
||||
return "x64";
|
||||
case 0x01c0:
|
||||
case 0x01c4:
|
||||
return "arm";
|
||||
case 0xaa64:
|
||||
return "arm64";
|
||||
case 0x0200:
|
||||
return "ia64";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse section characteristics flags. */
|
||||
export function parseSectionCharacteristics(
|
||||
characteristics: number,
|
||||
): z.infer<typeof sectionCharacteristicsSchema> {
|
||||
const IMAGE_SCN_CNT_CODE = 0x00000020;
|
||||
const IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040;
|
||||
const IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080;
|
||||
const IMAGE_SCN_MEM_READ = 0x40000000;
|
||||
const IMAGE_SCN_MEM_WRITE = 0x80000000;
|
||||
const IMAGE_SCN_MEM_EXECUTE = 0x20000000;
|
||||
|
||||
return {
|
||||
has_code: (characteristics & IMAGE_SCN_CNT_CODE) !== 0,
|
||||
has_initialized_data:
|
||||
(characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA) !== 0,
|
||||
has_uninitialized_data:
|
||||
(characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA) !== 0,
|
||||
readable: (characteristics & IMAGE_SCN_MEM_READ) !== 0,
|
||||
writable: (characteristics & IMAGE_SCN_MEM_WRITE) !== 0,
|
||||
executable: (characteristics & IMAGE_SCN_MEM_EXECUTE) !== 0,
|
||||
};
|
||||
}
|
||||
|
||||
/** Parse image characteristics flags. */
|
||||
export function parseImageCharacteristics(characteristics: number): string[] {
|
||||
const IMAGE_DLL = 0x2000;
|
||||
const IMAGE_EXECUTABLE = 0x0002;
|
||||
const IMAGE_CONSOLE = 0x0003;
|
||||
const flags: string[] = [];
|
||||
if (characteristics & IMAGE_DLL) flags.push("DLL");
|
||||
if (characteristics & IMAGE_EXECUTABLE) flags.push("Executable");
|
||||
if ((characteristics & 0x0000000f) === IMAGE_CONSOLE) flags.push("Console");
|
||||
return flags;
|
||||
}
|
||||
|
||||
/** Parse a PE debug directory type. */
|
||||
export function parseDebugType(type: number): string {
|
||||
switch (type) {
|
||||
case 0:
|
||||
return "UNKNOWN";
|
||||
case 1:
|
||||
return "COFF";
|
||||
case 2:
|
||||
return "CODEVIEW";
|
||||
case 3:
|
||||
return "POGO";
|
||||
case 4:
|
||||
return "MISC";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if a PE manifest has PDB debug info. */
|
||||
export function hasPdbInfo(manifest: PeManifest): boolean {
|
||||
return manifest.debug_entries.some(
|
||||
(e) => e.pdb_guid !== null && e.pdb_path !== null,
|
||||
);
|
||||
}
|
||||
|
||||
/** Get all imported DLLs from a PE manifest. */
|
||||
export function getImportedDlls(manifest: PeManifest): string[] {
|
||||
return [...new Set(manifest.imports.map((i) => i.dll))];
|
||||
}
|
||||
|
||||
/** Get all exported function names from a PE manifest. */
|
||||
export function getExportedFunctions(manifest: PeManifest): string[] {
|
||||
return manifest.exports.map((e) => e.name);
|
||||
}
|
||||
|
||||
/** Count executable sections. */
|
||||
export function countExecutableSections(manifest: PeManifest): number {
|
||||
return manifest.sections.filter((s) => s.characteristics.executable).length;
|
||||
}
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
countExecutableSections,
|
||||
getExportedFunctions,
|
||||
getImportedDlls,
|
||||
hasPdbInfo,
|
||||
parseDebugType,
|
||||
parseImageCharacteristics,
|
||||
parseMachineType,
|
||||
parseSectionCharacteristics,
|
||||
peManifestSchema,
|
||||
type PeManifest,
|
||||
type PeSection,
|
||||
type PeImport,
|
||||
type PeExport,
|
||||
type PeResource,
|
||||
type PeDebugEntry,
|
||||
type PeTlsCallback,
|
||||
} from "../src/domain/peInspection.js";
|
||||
|
||||
const validManifest: PeManifest = {
|
||||
dos_magic: 0x5a4d,
|
||||
pe_signature: 0x00004550,
|
||||
machine: "x64",
|
||||
number_of_sections: 3,
|
||||
timestamp: 0x12345678,
|
||||
is_dll: false,
|
||||
is_executable: true,
|
||||
is_console: true,
|
||||
entry_point: 0x1000,
|
||||
image_base: 0x400000,
|
||||
section_alignment: 0x1000,
|
||||
subsystem_version: "10.0",
|
||||
sections: [
|
||||
{
|
||||
name: ".text",
|
||||
virtual_size: 0x1000,
|
||||
virtual_address: 0x1000,
|
||||
raw_size: 0x1000,
|
||||
raw_offset: 0x400,
|
||||
characteristics: {
|
||||
has_code: true,
|
||||
has_initialized_data: false,
|
||||
has_uninitialized_data: false,
|
||||
readable: true,
|
||||
writable: false,
|
||||
executable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ".data",
|
||||
virtual_size: 0x500,
|
||||
virtual_address: 0x2000,
|
||||
raw_size: 0x500,
|
||||
raw_offset: 0x1400,
|
||||
characteristics: {
|
||||
has_code: false,
|
||||
has_initialized_data: true,
|
||||
has_uninitialized_data: false,
|
||||
readable: true,
|
||||
writable: true,
|
||||
executable: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ".rdata",
|
||||
virtual_size: 0x200,
|
||||
virtual_address: 0x3000,
|
||||
raw_size: 0x200,
|
||||
raw_offset: 0x1900,
|
||||
characteristics: {
|
||||
has_code: false,
|
||||
has_initialized_data: true,
|
||||
has_uninitialized_data: false,
|
||||
readable: true,
|
||||
writable: false,
|
||||
executable: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
{
|
||||
dll: "KERNEL32.DLL",
|
||||
function_name: "CreateFileW",
|
||||
ordinal: null,
|
||||
is_delay_import: false,
|
||||
},
|
||||
{
|
||||
dll: "KERNEL32.DLL",
|
||||
function_name: "WriteFile",
|
||||
ordinal: null,
|
||||
is_delay_import: false,
|
||||
},
|
||||
{
|
||||
dll: "USER32.DLL",
|
||||
function_name: "MessageBoxW",
|
||||
ordinal: 100,
|
||||
is_delay_import: false,
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
{ name: "DllMain", ordinal: 1, rva: 0x1000, forwarder: null },
|
||||
{ name: "MyFunction", ordinal: 2, rva: 0x1050, forwarder: null },
|
||||
],
|
||||
resources: [
|
||||
{ type: "RT_VERSION", name: "1", language: 0x0409, rva: 0x4000, size: 100 },
|
||||
],
|
||||
debug_entries: [
|
||||
{
|
||||
type: "CODEVIEW",
|
||||
rva: 0x5000,
|
||||
size: 100,
|
||||
pdb_guid: "AABBCCDD-EEFF-GGHH-IIJJ-KKLLMMNNOOPP",
|
||||
pdb_age: 1,
|
||||
pdb_path: "C:\\src\\app.pdb",
|
||||
},
|
||||
],
|
||||
tls_callbacks: [{ callback_rva: 0x6000, index: 0 }],
|
||||
relocations_count: 50,
|
||||
image_characteristics: ["Executable", "Console"],
|
||||
};
|
||||
|
||||
describe("PE/COFF static inspection", () => {
|
||||
it("parses machine types", () => {
|
||||
expect(parseMachineType(0x014c)).toBe("x86");
|
||||
expect(parseMachineType(0x8664)).toBe("x64");
|
||||
expect(parseMachineType(0x01c0)).toBe("arm");
|
||||
expect(parseMachineType(0xaa64)).toBe("arm64");
|
||||
expect(parseMachineType(0x0200)).toBe("ia64");
|
||||
expect(parseMachineType(0xffff)).toBe("unknown");
|
||||
});
|
||||
|
||||
it("parses section characteristics", () => {
|
||||
const CODE = 0x00000020;
|
||||
const READ = 0x40000000;
|
||||
const EXECUTE = 0x20000000;
|
||||
const result = parseSectionCharacteristics(CODE | READ | EXECUTE);
|
||||
expect(result.has_code).toBe(true);
|
||||
expect(result.readable).toBe(true);
|
||||
expect(result.executable).toBe(true);
|
||||
expect(result.writable).toBe(false);
|
||||
});
|
||||
|
||||
it("parses image characteristics", () => {
|
||||
const DLL = 0x2000;
|
||||
const EXEC = 0x0002;
|
||||
const flags = parseImageCharacteristics(DLL | EXEC);
|
||||
expect(flags).toContain("DLL");
|
||||
expect(flags).toContain("Executable");
|
||||
});
|
||||
|
||||
it("parses debug types", () => {
|
||||
expect(parseDebugType(2)).toBe("CODEVIEW");
|
||||
expect(parseDebugType(1)).toBe("COFF");
|
||||
expect(parseDebugType(99)).toBe("UNKNOWN");
|
||||
});
|
||||
|
||||
it("validates a well-formed manifest", () => {
|
||||
const result = peManifestSchema.safeParse(validManifest);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("detects PDB info", () => {
|
||||
expect(hasPdbInfo(validManifest)).toBe(true);
|
||||
const withoutPdb: PeManifest = {
|
||||
...validManifest,
|
||||
debug_entries: [
|
||||
{
|
||||
type: "COFF",
|
||||
rva: 0x5000,
|
||||
size: 100,
|
||||
pdb_guid: null,
|
||||
pdb_age: null,
|
||||
pdb_path: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(hasPdbInfo(withoutPdb)).toBe(false);
|
||||
});
|
||||
|
||||
it("gets imported DLLs", () => {
|
||||
const dlls = getImportedDlls(validManifest);
|
||||
expect(dlls).toHaveLength(2);
|
||||
expect(dlls).toContain("KERNEL32.DLL");
|
||||
expect(dlls).toContain("USER32.DLL");
|
||||
});
|
||||
|
||||
it("gets exported functions", () => {
|
||||
const exports = getExportedFunctions(validManifest);
|
||||
expect(exports).toHaveLength(2);
|
||||
expect(exports).toContain("DllMain");
|
||||
expect(exports).toContain("MyFunction");
|
||||
});
|
||||
|
||||
it("counts executable sections", () => {
|
||||
expect(countExecutableSections(validManifest)).toBe(1);
|
||||
});
|
||||
|
||||
it("uses all exported types", () => {
|
||||
const section: PeSection = validManifest.sections[0]!;
|
||||
expect(section.name).toBe(".text");
|
||||
const imp: PeImport = validManifest.imports[0]!;
|
||||
expect(imp.dll).toBe("KERNEL32.DLL");
|
||||
const exp: PeExport = validManifest.exports[0]!;
|
||||
expect(exp.name).toBe("DllMain");
|
||||
const res: PeResource = validManifest.resources[0]!;
|
||||
expect(res.type).toBe("RT_VERSION");
|
||||
const dbg: PeDebugEntry = validManifest.debug_entries[0]!;
|
||||
expect(dbg.type).toBe("CODEVIEW");
|
||||
const tls: PeTlsCallback = validManifest.tls_callbacks[0]!;
|
||||
expect(tls.index).toBe(0);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue