diff --git a/src/domain/peInspection.ts b/src/domain/peInspection.ts new file mode 100644 index 000000000..cd7193aa6 --- /dev/null +++ b/src/domain/peInspection.ts @@ -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; + +/** 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; + +/** 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; + +/** 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; + +/** 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; + +/** TLS callback entry. */ +export const peTlsCallbackSchema = z.strictObject({ + callback_rva: z.number().int().nonnegative(), + index: z.number().int().nonnegative(), +}); +export type PeTlsCallback = z.infer; + +/** Architecture/machine type. */ +export const peMachineTypeSchema = z.enum([ + "x86", + "x64", + "arm", + "arm64", + "ia64", + "unknown", +]); +export type PeMachineType = z.infer; + +/** 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; + +/** 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 { + 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; +} diff --git a/tests/peInspection.test.ts b/tests/peInspection.test.ts new file mode 100644 index 000000000..ca4612c80 --- /dev/null +++ b/tests/peInspection.test.ts @@ -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); + }); +});