diff --git a/src/app/debug/dll-viewer/page-client.tsx b/src/app/debug/dll-viewer/page-client.tsx new file mode 100644 index 000000000..57ec26edc --- /dev/null +++ b/src/app/debug/dll-viewer/page-client.tsx @@ -0,0 +1,570 @@ +"use client"; + +import { ChangeEvent, useCallback, useMemo, useState } from "react"; + +import { + parsePortableExecutable, + type PeDataDirectory, + type PeExport, + type PeImportLibrary, + type PeSection, + type PortableExecutableView, +} from "@/client/dll-viewer/pe-parser"; +import { toErrorMessage } from "@/client/utils/diagnostics"; +import { useTranslation } from "@/i18n"; + +type ParseStage = "idle" | "parsing" | "ready" | "error"; + +function formatBytes(value: number): string { + if (value < 1024) return `${value} B`; + if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; + return `${(value / (1024 * 1024)).toFixed(2)} MB`; +} + +function hex(value: number): string { + return `0x${value.toString(16).toUpperCase()}`; +} + +function truncateJson(value: string): string { + const maxLength = 60_000; + if (value.length <= maxLength) return value; + return `${value.slice(0, maxLength)}\n...`; +} + +function FieldGrid({ fields }: { fields: Array<[string, string]> }) { + return ( +
+ {fields.map(([label, value]) => ( +
+
{label}
+
{value}
+
+ ))} +
+ ); +} + +function FlagList({ values }: { values: string[] }) { + if (values.length === 0) return -; + return ( +
+ {values.map((value) => ( + {value} + ))} +
+ ); +} + +function SectionsTable({ labels, sections }: { labels: ReturnType; sections: PeSection[] }) { + return ( +
+

{labels.dllViewerSections}

+
+ + + + + + + + + + + + + {sections.map((section) => ( + + + + + + + + + ))} + +
{labels.dllViewerName}{labels.dllViewerVirtualAddress}{labels.dllViewerVirtualSize}{labels.dllViewerRawPointer}{labels.dllViewerRawSize}{labels.dllViewerCharacteristics}
{section.name}{hex(section.virtualAddress)}{formatBytes(section.virtualSize)}{hex(section.rawPointer)}{formatBytes(section.rawSize)} + +
+
+
+ ); +} + +function DirectoriesTable({ + directories, + labels, +}: { + directories: PeDataDirectory[]; + labels: ReturnType; +}) { + return ( +
+

{labels.dllViewerDataDirectories}

+
+ + + + + + + + + + {directories.map((directory) => ( + + + + + + ))} + +
{labels.dllViewerName}{labels.dllViewerRva}{labels.dllViewerSize}
{directory.name}{hex(directory.rva)}{formatBytes(directory.size)}
+
+
+ ); +} + +function ExportsTable({ labels, exports }: { labels: ReturnType; exports: PeExport[] }) { + const visibleExports = exports.slice(0, 240); + return ( + <> + {exports.length > visibleExports.length ? ( +

{labels.dllViewerShowingFirstExports.replace("{count}", String(visibleExports.length))}

+ ) : null} +
+ + + + + + + + + + + {visibleExports.map((exported) => ( + + + + + + + ))} + +
{labels.dllViewerName}{labels.dllViewerOrdinal}{labels.dllViewerRva}{labels.dllViewerForwardedTo}
{exported.name}{exported.ordinal}{hex(exported.rva)}{exported.forwardedTo ?? "-"}
+
+ + ); +} + +function ImportsView({ imports, labels }: { imports: PeImportLibrary[]; labels: ReturnType }) { + if (imports.length === 0) { + return

{labels.dllViewerNoImports}

; + } + + return ( +
+ {imports.map((library) => { + const visibleFunctions = library.functions.slice(0, 80); + return ( +
+ + {library.name} + {labels.dllViewerFunctions.replace("{count}", String(library.functions.length))} + +
+ {visibleFunctions.map((imported) => ( + + {imported.name} + + ))} +
+ {library.functions.length > visibleFunctions.length ? ( +

{labels.dllViewerShowingFirstImports.replace("{count}", String(visibleFunctions.length))}

+ ) : null} +
+ ); + })} +
+ ); +} + +function useDllViewerLabels() { + const { t } = useTranslation(); + return t.debug; +} + +function ParsedDllView({ parsed }: { parsed: PortableExecutableView }) { + const labels = useDllViewerLabels(); + const summaryFields = useMemo>( + () => [ + [labels.dllViewerFile, `${parsed.fileName} · ${formatBytes(parsed.fileSize)}`], + [labels.dllViewerFormat, parsed.optionalHeader.format], + [labels.dllViewerMachine, parsed.coffHeader.machine], + [labels.dllViewerSubsystem, parsed.optionalHeader.subsystem], + [labels.dllViewerEntryPoint, hex(parsed.optionalHeader.entryPoint)], + [labels.dllViewerImageBase, parsed.optionalHeader.imageBase], + [labels.dllViewerTimestamp, parsed.coffHeader.timestamp], + [labels.dllViewerPeOffset, hex(parsed.dosHeader.peHeaderOffset)], + [labels.dllViewerSectionAlignment, formatBytes(parsed.optionalHeader.sectionAlignment)], + [labels.dllViewerFileAlignment, formatBytes(parsed.optionalHeader.fileAlignment)], + [labels.dllViewerSizeOfImage, formatBytes(parsed.optionalHeader.sizeOfImage)], + [labels.dllViewerChecksum, hex(parsed.optionalHeader.checksum)], + ], + [labels, parsed], + ); + + return ( +
+
+

{labels.dllViewerSummary}

+ +
+ {labels.dllViewerCharacteristics} + +
+
+ +
+

{labels.dllViewerExports}

+ {parsed.exports ? ( + <> + + + + ) : ( +

{labels.dllViewerNoExports}

+ )} +
+ +
+

{labels.dllViewerImports}

+ +
+ + + + + {parsed.warnings.length > 0 ? ( +
+

{labels.dllViewerWarnings}

+
    + {parsed.warnings.map((warning) => ( +
  • {warning}
  • + ))} +
+
+ ) : null} + +
+
+ {labels.dllViewerRawJson} +
{truncateJson(JSON.stringify(parsed, null, 2))}
+
+
+
+ ); +} + +export function DllViewerPageClient() { + const labels = useDllViewerLabels(); + const [stage, setStage] = useState("idle"); + const [parsed, setParsed] = useState(null); + const [error, setError] = useState(null); + + const statusLabel = { + idle: labels.dllViewerStatusIdle, + parsing: labels.dllViewerStatusParsing, + ready: labels.dllViewerStatusReady, + error: labels.dllViewerStatusError, + }[stage]; + + const handleFile = useCallback( + async (event: ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + setStage("parsing"); + setError(null); + setParsed(null); + + try { + const buffer = await file.arrayBuffer(); + setParsed(parsePortableExecutable(buffer, file.name)); + setStage("ready"); + } catch (parseError) { + setError(toErrorMessage(parseError)); + setStage("error"); + } + }, + [], + ); + + return ( +
+
+
+

{labels.dllViewerTitle}

+

{labels.dllViewerDescription}

+
+ +
+ +
+ {labels.dllViewerStatus} + {statusLabel} +
+ + {error ? ( +
+

{labels.dllViewerError}

+

{error}

+
+ ) : null} + + {parsed ? :

{labels.dllViewerNoResult}

} + + +
+ ); +} diff --git a/src/app/debug/dll-viewer/page.tsx b/src/app/debug/dll-viewer/page.tsx new file mode 100644 index 000000000..fe6e647ae --- /dev/null +++ b/src/app/debug/dll-viewer/page.tsx @@ -0,0 +1,11 @@ +import { Suspense } from "react"; + +import { DllViewerPageClient } from "./page-client"; + +export default function DllViewerPage() { + return ( + + + + ); +} diff --git a/src/client/dll-viewer/__tests__/pe-parser.test.ts b/src/client/dll-viewer/__tests__/pe-parser.test.ts new file mode 100644 index 000000000..e2e9ba94c --- /dev/null +++ b/src/client/dll-viewer/__tests__/pe-parser.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from "vitest"; + +import { parsePortableExecutable } from "../pe-parser"; + +function writeAscii(bytes: Uint8Array, offset: number, value: string): void { + for (let index = 0; index < value.length; index += 1) { + bytes[offset + index] = value.charCodeAt(index); + } +} + +function buildPeFixture(): ArrayBuffer { + const buffer = new ArrayBuffer(1024); + const bytes = new Uint8Array(buffer); + const view = new DataView(buffer); + const peOffset = 0x80; + const coffOffset = peOffset + 4; + const optionalOffset = coffOffset + 20; + const sectionTableOffset = optionalOffset + 0xf0; + + writeAscii(bytes, 0, "MZ"); + view.setUint32(0x3c, peOffset, true); + writeAscii(bytes, peOffset, "PE\0\0"); + view.setUint16(coffOffset, 0x8664, true); + view.setUint16(coffOffset + 2, 1, true); + view.setUint16(coffOffset + 16, 0xf0, true); + view.setUint16(coffOffset + 18, 0x2022, true); + + view.setUint16(optionalOffset, 0x20b, true); + view.setUint32(optionalOffset + 16, 0x1010, true); + view.setUint32(optionalOffset + 24, 0x400000, true); + view.setUint32(optionalOffset + 32, 0x1000, true); + view.setUint32(optionalOffset + 36, 0x200, true); + view.setUint32(optionalOffset + 56, 0x3000, true); + view.setUint32(optionalOffset + 60, 0x200, true); + view.setUint16(optionalOffset + 68, 3, true); + view.setUint16(optionalOffset + 70, 0x0140, true); + view.setUint32(optionalOffset + 108, 16, true); + view.setUint32(optionalOffset + 112, 0x1100, true); + view.setUint32(optionalOffset + 116, 0x60, true); + view.setUint32(optionalOffset + 120, 0x1180, true); + view.setUint32(optionalOffset + 124, 0x80, true); + + writeAscii(bytes, sectionTableOffset, ".rdata"); + view.setUint32(sectionTableOffset + 8, 0x1000, true); + view.setUint32(sectionTableOffset + 12, 0x1000, true); + view.setUint32(sectionTableOffset + 16, 0x200, true); + view.setUint32(sectionTableOffset + 20, 0x200, true); + view.setUint32(sectionTableOffset + 36, 0x40000040, true); + + const exportOffset = 0x300; + view.setUint32(exportOffset + 12, 0x1140, true); + view.setUint32(exportOffset + 16, 1, true); + view.setUint32(exportOffset + 20, 1, true); + view.setUint32(exportOffset + 24, 1, true); + view.setUint32(exportOffset + 28, 0x1150, true); + view.setUint32(exportOffset + 32, 0x1154, true); + view.setUint32(exportOffset + 36, 0x1158, true); + writeAscii(bytes, 0x340, "fixture.dll\0"); + view.setUint32(0x350, 0x1010, true); + view.setUint32(0x354, 0x1160, true); + view.setUint16(0x358, 0, true); + writeAscii(bytes, 0x360, "FixtureExport\0"); + + const importOffset = 0x380; + view.setUint32(importOffset, 0x11d0, true); + view.setUint32(importOffset + 12, 0x11c0, true); + view.setUint32(importOffset + 16, 0x11e0, true); + writeAscii(bytes, 0x3c0, "KERNEL32.dll\0"); + view.setUint32(0x3d0, 0x11f0, true); + view.setUint32(0x3d4, 0, true); + view.setUint16(0x3f0, 7, true); + writeAscii(bytes, 0x3f2, "CreateFileW\0"); + + return buffer; +} + +describe("parsePortableExecutable", () => { + it("parses PE headers, sections, exports, and imports", () => { + const parsed = parsePortableExecutable(buildPeFixture(), "fixture.dll"); + + expect(parsed.coffHeader.machine).toBe("x86-64"); + expect(parsed.coffHeader.characteristics).toContain("DLL"); + expect(parsed.optionalHeader.format).toBe("PE32+"); + expect(parsed.optionalHeader.subsystem).toBe("Windows Console"); + expect(parsed.sections[0]).toMatchObject({ name: ".rdata", virtualAddress: 0x1000 }); + expect(parsed.exports?.dllName).toBe("fixture.dll"); + expect(parsed.exports?.namedExports[0]).toMatchObject({ name: "FixtureExport", ordinal: 1, rva: 0x1010 }); + expect(parsed.imports[0]).toMatchObject({ name: "KERNEL32.dll" }); + expect(parsed.imports[0].functions[0]).toMatchObject({ name: "CreateFileW", hint: 7 }); + }); + + it("rejects non-PE input", () => { + expect(() => parsePortableExecutable(new Uint8Array([1, 2, 3]).buffer, "raw.bin")).toThrow(/missing MZ/); + }); +}); diff --git a/src/client/dll-viewer/pe-parser.ts b/src/client/dll-viewer/pe-parser.ts new file mode 100644 index 000000000..d730947c0 --- /dev/null +++ b/src/client/dll-viewer/pe-parser.ts @@ -0,0 +1,476 @@ +export type PeDataDirectory = { + name: string; + rva: number; + size: number; +}; + +export type PeSection = { + name: string; + virtualAddress: number; + virtualSize: number; + rawPointer: number; + rawSize: number; + characteristics: string[]; +}; + +export type PeExport = { + name: string; + ordinal: number; + rva: number; + forwardedTo?: string; +}; + +export type PeImport = { + name: string; + hint?: number; + ordinal?: number; +}; + +export type PeImportLibrary = { + name: string; + functions: PeImport[]; +}; + +export type PortableExecutableView = { + fileName: string; + fileSize: number; + dosHeader: { + peHeaderOffset: number; + stubText: string; + }; + coffHeader: { + machine: string; + machineValue: number; + sectionCount: number; + timestamp: string; + characteristics: string[]; + }; + optionalHeader: { + format: string; + entryPoint: number; + imageBase: string; + subsystem: string; + sectionAlignment: number; + fileAlignment: number; + sizeOfImage: number; + checksum: number; + dllCharacteristics: string[]; + }; + dataDirectories: PeDataDirectory[]; + sections: PeSection[]; + exports: { + dllName: string; + ordinalBase: number; + functionCount: number; + namedExports: PeExport[]; + } | null; + imports: PeImportLibrary[]; + warnings: string[]; +}; + +const DATA_DIRECTORY_NAMES = [ + "Export Table", + "Import Table", + "Resource Table", + "Exception Table", + "Certificate Table", + "Base Relocation Table", + "Debug", + "Architecture", + "Global Ptr", + "TLS Table", + "Load Config Table", + "Bound Import", + "Import Address Table", + "Delay Import Descriptor", + "CLR Runtime Header", + "Reserved", +]; + +const MACHINE_NAMES = new Map([ + [0x014c, "x86"], + [0x01c0, "ARM"], + [0x01c4, "ARMv7"], + [0x0200, "Intel Itanium"], + [0x8664, "x86-64"], + [0xaa64, "ARM64"], +]); + +const SUBSYSTEM_NAMES = new Map([ + [1, "Native"], + [2, "Windows GUI"], + [3, "Windows Console"], + [5, "OS/2 Console"], + [7, "POSIX Console"], + [9, "Windows CE GUI"], + [10, "EFI Application"], + [11, "EFI Boot Service Driver"], + [12, "EFI Runtime Driver"], + [13, "EFI ROM"], + [14, "Xbox"], + [16, "Windows Boot Application"], +]); + +const COFF_CHARACTERISTICS: Array<[number, string]> = [ + [0x0002, "Executable"], + [0x0020, "Large address aware"], + [0x0100, "32-bit machine"], + [0x0200, "Debug symbols stripped"], + [0x2000, "DLL"], + [0x4000, "Uniprocessor only"], +]; + +const DLL_CHARACTERISTICS: Array<[number, string]> = [ + [0x0020, "High entropy VA"], + [0x0040, "Dynamic base"], + [0x0080, "Force integrity"], + [0x0100, "NX compatible"], + [0x0200, "No isolation"], + [0x0400, "No SEH"], + [0x0800, "No bind"], + [0x1000, "AppContainer"], + [0x2000, "WDM driver"], + [0x4000, "Guard CF"], + [0x8000, "Terminal Server aware"], +]; + +const SECTION_CHARACTERISTICS: Array<[number, string]> = [ + [0x00000020, "Code"], + [0x00000040, "Initialized data"], + [0x00000080, "Uninitialized data"], + [0x02000000, "Discardable"], + [0x04000000, "Not cached"], + [0x08000000, "Not paged"], + [0x10000000, "Shared"], + [0x20000000, "Execute"], + [0x40000000, "Read"], + [0x80000000, "Write"], +]; + +function flagsFrom(value: number, definitions: Array<[number, string]>): string[] { + return definitions.filter(([flag]) => (value & flag) !== 0).map(([, label]) => label); +} + +function hex(value: number): string { + return `0x${value.toString(16).toUpperCase()}`; +} + +class PeReader { + readonly data: DataView; + readonly bytes: Uint8Array; + readonly warnings: string[] = []; + + constructor(buffer: ArrayBuffer) { + this.bytes = new Uint8Array(buffer); + this.data = new DataView(buffer); + } + + has(offset: number, length: number): boolean { + return Number.isInteger(offset) && offset >= 0 && offset + length <= this.data.byteLength; + } + + u16(offset: number): number { + if (!this.has(offset, 2)) throw new Error(`PE read out of bounds at ${hex(offset)}`); + return this.data.getUint16(offset, true); + } + + u32(offset: number): number { + if (!this.has(offset, 4)) throw new Error(`PE read out of bounds at ${hex(offset)}`); + return this.data.getUint32(offset, true); + } + + u64(offset: number): bigint { + const low = BigInt(this.u32(offset)); + const high = BigInt(this.u32(offset + 4)); + return (high << 32n) | low; + } + + ascii(offset: number, length: number): string { + if (!this.has(offset, length)) return ""; + let result = ""; + for (let index = 0; index < length; index += 1) { + const byte = this.bytes[offset + index]; + if (byte === 0) break; + if (byte >= 32 && byte <= 126) result += String.fromCharCode(byte); + } + return result; + } + + cString(offset: number, maxLength = 4096): string { + if (!this.has(offset, 1)) return ""; + let result = ""; + for (let index = offset; index < this.bytes.length && index < offset + maxLength; index += 1) { + const byte = this.bytes[index]; + if (byte === 0) break; + if (byte >= 32 && byte <= 126) result += String.fromCharCode(byte); + } + return result; + } +} + +function rvaToOffset(rva: number, sections: PeSection[], sizeOfHeaders: number): number | null { + if (rva > 0 && rva < sizeOfHeaders) return rva; + + for (const section of sections) { + const sectionSize = Math.max(section.virtualSize, section.rawSize); + if (rva >= section.virtualAddress && rva < section.virtualAddress + sectionSize) { + return section.rawPointer + (rva - section.virtualAddress); + } + } + + return null; +} + +function readTimestamp(seconds: number): string { + if (seconds === 0) return "0"; + return new Date(seconds * 1000).toISOString(); +} + +function readDataDirectories(reader: PeReader, start: number, count: number): PeDataDirectory[] { + const directories: PeDataDirectory[] = []; + const safeCount = Math.min(count, DATA_DIRECTORY_NAMES.length); + + for (let index = 0; index < safeCount; index += 1) { + const offset = start + index * 8; + if (!reader.has(offset, 8)) break; + + const rva = reader.u32(offset); + const size = reader.u32(offset + 4); + if (rva !== 0 || size !== 0) { + directories.push({ name: DATA_DIRECTORY_NAMES[index], rva, size }); + } + } + + return directories; +} + +function readSections(reader: PeReader, sectionTableOffset: number, count: number): PeSection[] { + const sections: PeSection[] = []; + + for (let index = 0; index < count; index += 1) { + const offset = sectionTableOffset + index * 40; + if (!reader.has(offset, 40)) { + reader.warnings.push(`Section table truncated before section ${index + 1}.`); + break; + } + + sections.push({ + name: reader.ascii(offset, 8) || `section_${index + 1}`, + virtualSize: reader.u32(offset + 8), + virtualAddress: reader.u32(offset + 12), + rawSize: reader.u32(offset + 16), + rawPointer: reader.u32(offset + 20), + characteristics: flagsFrom(reader.u32(offset + 36), SECTION_CHARACTERISTICS), + }); + } + + return sections; +} + +function findDirectory(directories: PeDataDirectory[], name: string): PeDataDirectory | undefined { + return directories.find((directory) => directory.name === name && directory.rva !== 0 && directory.size !== 0); +} + +function readExports( + reader: PeReader, + directories: PeDataDirectory[], + sections: PeSection[], + sizeOfHeaders: number, +): PortableExecutableView["exports"] { + const directory = findDirectory(directories, "Export Table"); + if (!directory) return null; + + const directoryOffset = rvaToOffset(directory.rva, sections, sizeOfHeaders); + if (directoryOffset == null || !reader.has(directoryOffset, 40)) { + reader.warnings.push("Export table points outside the mapped sections."); + return null; + } + + const dllNameOffset = rvaToOffset(reader.u32(directoryOffset + 12), sections, sizeOfHeaders); + const ordinalBase = reader.u32(directoryOffset + 16); + const functionCount = reader.u32(directoryOffset + 20); + const nameCount = reader.u32(directoryOffset + 24); + const functionsOffset = rvaToOffset(reader.u32(directoryOffset + 28), sections, sizeOfHeaders); + const namesOffset = rvaToOffset(reader.u32(directoryOffset + 32), sections, sizeOfHeaders); + const ordinalsOffset = rvaToOffset(reader.u32(directoryOffset + 36), sections, sizeOfHeaders); + const namedExports: PeExport[] = []; + + if (functionsOffset == null || namesOffset == null || ordinalsOffset == null) { + reader.warnings.push("Export name arrays point outside the mapped sections."); + } else { + const safeNameCount = Math.min(nameCount, 5000); + for (let index = 0; index < safeNameCount; index += 1) { + const nameRva = reader.u32(namesOffset + index * 4); + const nameOffset = rvaToOffset(nameRva, sections, sizeOfHeaders); + const ordinalIndex = reader.u16(ordinalsOffset + index * 2); + const functionRva = reader.u32(functionsOffset + ordinalIndex * 4); + const forwardedTo = + functionRva >= directory.rva && functionRva < directory.rva + directory.size + ? reader.cString(rvaToOffset(functionRva, sections, sizeOfHeaders) ?? -1) + : undefined; + + namedExports.push({ + name: nameOffset == null ? `(name at ${hex(nameRva)})` : reader.cString(nameOffset), + ordinal: ordinalBase + ordinalIndex, + rva: functionRva, + ...(forwardedTo ? { forwardedTo } : {}), + }); + } + + if (nameCount > safeNameCount) { + reader.warnings.push(`Export list truncated to ${safeNameCount} names.`); + } + } + + return { + dllName: dllNameOffset == null ? "" : reader.cString(dllNameOffset), + ordinalBase, + functionCount, + namedExports, + }; +} + +function readImports( + reader: PeReader, + directories: PeDataDirectory[], + sections: PeSection[], + sizeOfHeaders: number, + isPe32Plus: boolean, +): PeImportLibrary[] { + const directory = findDirectory(directories, "Import Table"); + if (!directory) return []; + + const importOffset = rvaToOffset(directory.rva, sections, sizeOfHeaders); + if (importOffset == null) { + reader.warnings.push("Import table points outside the mapped sections."); + return []; + } + + const libraries: PeImportLibrary[] = []; + for (let descriptor = 0; descriptor < 256; descriptor += 1) { + const offset = importOffset + descriptor * 20; + if (!reader.has(offset, 20)) break; + + const originalFirstThunk = reader.u32(offset); + const nameRva = reader.u32(offset + 12); + const firstThunk = reader.u32(offset + 16); + if (originalFirstThunk === 0 && nameRva === 0 && firstThunk === 0) break; + + const nameOffset = rvaToOffset(nameRva, sections, sizeOfHeaders); + const thunkOffset = rvaToOffset(originalFirstThunk || firstThunk, sections, sizeOfHeaders); + const functions: PeImport[] = []; + + if (thunkOffset == null) { + reader.warnings.push(`Import thunk table for descriptor ${descriptor + 1} points outside mapped sections.`); + } else { + const thunkSize = isPe32Plus ? 8 : 4; + const ordinalMask = isPe32Plus ? 0x8000000000000000n : 0x80000000n; + const valueMask = isPe32Plus ? 0x7fffffffffffffffn : 0x7fffffffn; + + for (let index = 0; index < 2000; index += 1) { + const thunkEntryOffset = thunkOffset + index * thunkSize; + if (!reader.has(thunkEntryOffset, thunkSize)) break; + + const thunkValue = isPe32Plus ? reader.u64(thunkEntryOffset) : BigInt(reader.u32(thunkEntryOffset)); + if (thunkValue === 0n) break; + + if ((thunkValue & ordinalMask) !== 0n) { + functions.push({ name: `#${Number(thunkValue & 0xffffn)}`, ordinal: Number(thunkValue & 0xffffn) }); + continue; + } + + const hintNameRva = Number(thunkValue & valueMask); + const hintNameOffset = rvaToOffset(hintNameRva, sections, sizeOfHeaders); + if (hintNameOffset == null || !reader.has(hintNameOffset, 2)) { + functions.push({ name: `(import at ${hex(hintNameRva)})` }); + continue; + } + + functions.push({ + hint: reader.u16(hintNameOffset), + name: reader.cString(hintNameOffset + 2), + }); + } + } + + libraries.push({ + name: nameOffset == null ? `(library at ${hex(nameRva)})` : reader.cString(nameOffset), + functions, + }); + } + + return libraries; +} + +export function parsePortableExecutable(buffer: ArrayBuffer, fileName: string): PortableExecutableView { + const reader = new PeReader(buffer); + + if (reader.ascii(0, 2) !== "MZ") { + throw new Error("Not a PE file: missing MZ DOS header."); + } + + const peHeaderOffset = reader.u32(0x3c); + if (reader.ascii(peHeaderOffset, 4) !== "PE") { + throw new Error("Not a PE file: missing PE signature."); + } + + const coffOffset = peHeaderOffset + 4; + const machineValue = reader.u16(coffOffset); + const sectionCount = reader.u16(coffOffset + 2); + const timestamp = reader.u32(coffOffset + 4); + const optionalHeaderSize = reader.u16(coffOffset + 16); + const coffCharacteristics = reader.u16(coffOffset + 18); + const optionalOffset = coffOffset + 20; + const optionalMagic = reader.u16(optionalOffset); + const isPe32Plus = optionalMagic === 0x20b; + + if (optionalMagic !== 0x10b && optionalMagic !== 0x20b) { + throw new Error(`Unsupported PE optional header magic ${hex(optionalMagic)}.`); + } + + const entryPoint = reader.u32(optionalOffset + 16); + const imageBase = isPe32Plus ? reader.u64(optionalOffset + 24).toString() : String(reader.u32(optionalOffset + 28)); + const sectionAlignment = reader.u32(optionalOffset + 32); + const fileAlignment = reader.u32(optionalOffset + 36); + const sizeOfImage = reader.u32(optionalOffset + 56); + const sizeOfHeaders = reader.u32(optionalOffset + 60); + const checksum = reader.u32(optionalOffset + 64); + const subsystemValue = reader.u16(optionalOffset + 68); + const dllCharacteristicsValue = reader.u16(optionalOffset + 70); + const directoryCountOffset = optionalOffset + (isPe32Plus ? 108 : 92); + const dataDirectoryOffset = optionalOffset + (isPe32Plus ? 112 : 96); + const dataDirectories = readDataDirectories(reader, dataDirectoryOffset, reader.u32(directoryCountOffset)); + const sections = readSections(reader, optionalOffset + optionalHeaderSize, sectionCount); + + return { + fileName, + fileSize: buffer.byteLength, + dosHeader: { + peHeaderOffset, + stubText: reader.ascii(0x40, Math.max(0, peHeaderOffset - 0x40)), + }, + coffHeader: { + machine: MACHINE_NAMES.get(machineValue) ?? `Unknown (${hex(machineValue)})`, + machineValue, + sectionCount, + timestamp: readTimestamp(timestamp), + characteristics: flagsFrom(coffCharacteristics, COFF_CHARACTERISTICS), + }, + optionalHeader: { + format: isPe32Plus ? "PE32+" : "PE32", + entryPoint, + imageBase, + subsystem: SUBSYSTEM_NAMES.get(subsystemValue) ?? `Unknown (${subsystemValue})`, + sectionAlignment, + fileAlignment, + sizeOfImage, + checksum, + dllCharacteristics: flagsFrom(dllCharacteristicsValue, DLL_CHARACTERISTICS), + }, + dataDirectories, + sections, + exports: readExports(reader, dataDirectories, sections, sizeOfHeaders), + imports: readImports(reader, dataDirectories, sections, sizeOfHeaders, isPe32Plus), + warnings: reader.warnings, + }; +} diff --git a/src/i18n/locales/en-tail.ts b/src/i18n/locales/en-tail.ts index 5c32457ec..215c0be65 100644 --- a/src/i18n/locales/en-tail.ts +++ b/src/i18n/locales/en-tail.ts @@ -140,6 +140,53 @@ export const enTail: TailTranslationDictionarySections = { officeWasmPocShowingFirstRows: "Showing the first 40 rows and 18 columns.", officeWasmPocShapes: "shapes", officeWasmPocTextRuns: "text blocks", + dllViewerTitle: "DLL Viewer", + dllViewerDescription: "Upload a Windows DLL, EXE, SYS or OCX file to inspect its PE headers, sections, imports, and exports in-browser.", + dllViewerSelectFile: "Select binary", + dllViewerStatus: "Status", + dllViewerStatusIdle: "Idle", + dllViewerStatusParsing: "Parsing", + dllViewerStatusReady: "Ready", + dllViewerStatusError: "Error", + dllViewerError: "Parse error", + dllViewerNoResult: "No DLL parsed yet.", + dllViewerSummary: "Summary", + dllViewerFile: "File", + dllViewerFormat: "Format", + dllViewerMachine: "Machine", + dllViewerSubsystem: "Subsystem", + dllViewerEntryPoint: "Entry point", + dllViewerImageBase: "Image base", + dllViewerTimestamp: "Timestamp", + dllViewerPeOffset: "PE header offset", + dllViewerSectionAlignment: "Section alignment", + dllViewerFileAlignment: "File alignment", + dllViewerSizeOfImage: "Image size", + dllViewerChecksum: "Checksum", + dllViewerCharacteristics: "Characteristics", + dllViewerExports: "Exports", + dllViewerImports: "Imports", + dllViewerSections: "Sections", + dllViewerDataDirectories: "Data directories", + dllViewerWarnings: "Warnings", + dllViewerRawJson: "Raw JSON", + dllViewerDllName: "DLL name", + dllViewerOrdinalBase: "Ordinal base", + dllViewerFunctionCount: "Function count", + dllViewerNoExports: "No export table found.", + dllViewerNoImports: "No import table found.", + dllViewerShowingFirstExports: "Showing the first {count} exports.", + dllViewerShowingFirstImports: "Showing the first {count} imports.", + dllViewerFunctions: "{count} functions", + dllViewerName: "Name", + dllViewerVirtualAddress: "Virtual address", + dllViewerVirtualSize: "Virtual size", + dllViewerRawPointer: "Raw pointer", + dllViewerRawSize: "Raw size", + dllViewerRva: "RVA", + dllViewerSize: "Size", + dllViewerOrdinal: "Ordinal", + dllViewerForwardedTo: "Forwarded to", }, // UI components diff --git a/src/i18n/locales/zh-tail.ts b/src/i18n/locales/zh-tail.ts index 13e4eddd1..1879cc08a 100644 --- a/src/i18n/locales/zh-tail.ts +++ b/src/i18n/locales/zh-tail.ts @@ -137,6 +137,53 @@ export const zhTail: TailTranslationDictionarySections = { officeWasmPocShowingFirstRows: "显示前 40 行、18 列。", officeWasmPocShapes: "个形状", officeWasmPocTextRuns: "个文本块", + dllViewerTitle: "DLL 查看器", + dllViewerDescription: "上传 Windows DLL、EXE、SYS 或 OCX 文件,在浏览器内查看 PE 头、区段、导入表和导出表。", + dllViewerSelectFile: "选择二进制文件", + dllViewerStatus: "状态", + dllViewerStatusIdle: "空闲", + dllViewerStatusParsing: "解析中", + dllViewerStatusReady: "已完成", + dllViewerStatusError: "错误", + dllViewerError: "解析错误", + dllViewerNoResult: "尚未解析 DLL。", + dllViewerSummary: "摘要", + dllViewerFile: "文件", + dllViewerFormat: "格式", + dllViewerMachine: "机器类型", + dllViewerSubsystem: "子系统", + dllViewerEntryPoint: "入口点", + dllViewerImageBase: "镜像基址", + dllViewerTimestamp: "时间戳", + dllViewerPeOffset: "PE 头偏移", + dllViewerSectionAlignment: "区段对齐", + dllViewerFileAlignment: "文件对齐", + dllViewerSizeOfImage: "镜像大小", + dllViewerChecksum: "校验和", + dllViewerCharacteristics: "特征", + dllViewerExports: "导出表", + dllViewerImports: "导入表", + dllViewerSections: "区段", + dllViewerDataDirectories: "数据目录", + dllViewerWarnings: "警告", + dllViewerRawJson: "原始 JSON", + dllViewerDllName: "DLL 名称", + dllViewerOrdinalBase: "序号基数", + dllViewerFunctionCount: "函数数量", + dllViewerNoExports: "未找到导出表。", + dllViewerNoImports: "未找到导入表。", + dllViewerShowingFirstExports: "显示前 {count} 个导出。", + dllViewerShowingFirstImports: "显示前 {count} 个导入。", + dllViewerFunctions: "{count} 个函数", + dllViewerName: "名称", + dllViewerVirtualAddress: "虚拟地址", + dllViewerVirtualSize: "虚拟大小", + dllViewerRawPointer: "原始偏移", + dllViewerRawSize: "原始大小", + dllViewerRva: "RVA", + dllViewerSize: "大小", + dllViewerOrdinal: "序号", + dllViewerForwardedTo: "转发到", }, ui: { diff --git a/src/i18n/types-tail.ts b/src/i18n/types-tail.ts index b0992fc00..986fd8f36 100644 --- a/src/i18n/types-tail.ts +++ b/src/i18n/types-tail.ts @@ -138,6 +138,53 @@ export interface TailTranslationDictionarySections { officeWasmPocShowingFirstRows: string; officeWasmPocShapes: string; officeWasmPocTextRuns: string; + dllViewerTitle: string; + dllViewerDescription: string; + dllViewerSelectFile: string; + dllViewerStatus: string; + dllViewerStatusIdle: string; + dllViewerStatusParsing: string; + dllViewerStatusReady: string; + dllViewerStatusError: string; + dllViewerError: string; + dllViewerNoResult: string; + dllViewerSummary: string; + dllViewerFile: string; + dllViewerFormat: string; + dllViewerMachine: string; + dllViewerSubsystem: string; + dllViewerEntryPoint: string; + dllViewerImageBase: string; + dllViewerTimestamp: string; + dllViewerPeOffset: string; + dllViewerSectionAlignment: string; + dllViewerFileAlignment: string; + dllViewerSizeOfImage: string; + dllViewerChecksum: string; + dllViewerCharacteristics: string; + dllViewerExports: string; + dllViewerImports: string; + dllViewerSections: string; + dllViewerDataDirectories: string; + dllViewerWarnings: string; + dllViewerRawJson: string; + dllViewerDllName: string; + dllViewerOrdinalBase: string; + dllViewerFunctionCount: string; + dllViewerNoExports: string; + dllViewerNoImports: string; + dllViewerShowingFirstExports: string; + dllViewerShowingFirstImports: string; + dllViewerFunctions: string; + dllViewerName: string; + dllViewerVirtualAddress: string; + dllViewerVirtualSize: string; + dllViewerRawPointer: string; + dllViewerRawSize: string; + dllViewerRva: string; + dllViewerSize: string; + dllViewerOrdinal: string; + dllViewerForwardedTo: string; }; // UI components