using System; using System.Collections.Generic; using System.IO; using System.Text; using SabreTools.Data.Extensions; using SabreTools.Data.Models.COFF; using SabreTools.Data.Models.PortableExecutable; using SabreTools.Data.Models.PortableExecutable.Resource.Entries; using SabreTools.IO.Extensions; using SabreTools.Matching; using SabreTools.Numerics.Extensions; using SabreTools.Text.Extensions; #pragma warning disable IDE0330 // Use 'System.Threading.Lock' namespace SabreTools.Wrappers { public partial class PortableExecutable : WrapperBase { #region Descriptive Properties /// public override string DescriptionString => "Portable Executable (PE)"; #endregion #region Extension Properties /// public FileHeader COFFFileHeader => Model.FileHeader; /// /// Dictionary of debug data /// public Dictionary DebugData { get { lock (_debugDataLock) { // Use the cached data if possible if (field.Count != 0) return field; // If we have no resource table, just return if (DebugDirectoryTable is null || DebugDirectoryTable.Length == 0) return field; // Otherwise, build and return the cached dictionary field = ParseDebugTable(); return field; } } } = []; /// public Data.Models.PortableExecutable.DebugData.Entry[]? DebugDirectoryTable => Model.DebugTable?.DebugDirectoryTable; /// /// Entry point data, if it exists /// /// Caches up to 128 bytes public byte[] EntryPointData { get { lock (_entryPointDataLock) { // If we already have cached data, just use that immediately if (field is not null) return field; // If we have no entry point int entryPointAddress = (int)OptionalHeader.AddressOfEntryPoint.ConvertVirtualAddress(SectionTable); if (entryPointAddress == 0) { field = []; return field; } // If the entry point matches with the start of a section, use that int entryPointSection = FindEntryPointSectionIndex(); if (entryPointSection >= 0 && OptionalHeader.AddressOfEntryPoint == SectionTable[entryPointSection].VirtualAddress) { field = GetSectionData(entryPointSection) ?? []; return field; } // Read the first 128 bytes of the entry point field = ReadRangeFromSource(entryPointAddress, length: 128) ?? []; return field; } } } = null; /// public Data.Models.PortableExecutable.Export.AddressTableEntry[]? ExportTable => Model.ExportAddressTable; /// public Data.Models.PortableExecutable.Export.DirectoryTable? ExportDirectoryTable => Model.ExportDirectoryTable; /// public Data.Models.PortableExecutable.Export.NamePointerTable? ExportNamePointerTable => Model.NamePointerTable; /// public Data.Models.PortableExecutable.Export.NameTable? ExportNameTable => Model.ExportNameTable; /// public Data.Models.PortableExecutable.Export.OrdinalTable? ExportOrdinalTable => Model.OrdinalTable; /// /// Header padding data, if it exists /// public byte[] HeaderPaddingData { get { lock (_headerPaddingDataLock) { // If we already have cached data, just use that immediately if (field is not null) return field; // TODO: Don't scan the known header data as well // Populate the raw header padding data based on the source uint headerStartAddress = Stub.Header.NewExeHeaderAddr; uint firstSectionAddress = uint.MaxValue; foreach (var section in SectionTable) { if (section.PointerToRawData == 0) continue; if (section.PointerToRawData < headerStartAddress) continue; if (section.PointerToRawData < firstSectionAddress) firstSectionAddress = section.PointerToRawData; } // Check if the header length is more than 0 before reading data int headerLength = (int)(firstSectionAddress - headerStartAddress); if (headerLength <= 0) { field = []; return field; } field = ReadRangeFromSource((int)headerStartAddress, headerLength) ?? []; return field; } } } = null; /// /// Header padding strings, if they exist /// public List HeaderPaddingStrings { get { lock (_headerPaddingStringsLock) { // If we already have cached data, just use that immediately if (field is not null) return field; // Get the header padding data, if possible byte[] headerPaddingData = HeaderPaddingData; if (headerPaddingData.Length == 0) { field = []; return field; } // Otherwise, cache and return the strings field = headerPaddingData.ReadStringsFrom(charLimit: 3) ?? []; return field; } } } = null; /// public Dictionary? ImportAddressTables => Model.ImportAddressTables; /// public Data.Models.PortableExecutable.Import.DirectoryTableEntry[]? ImportDirectoryTable => Model.ImportDirectoryTable; /// public Data.Models.PortableExecutable.Import.HintNameTableEntry[]? ImportHintNameTable => Model.HintNameTable; /// public Dictionary? ImportLookupTables => Model.ImportLookupTables; /// /// SecuROM Matroschka package wrapper, if it exists /// public SecuROMMatroschkaPackage? MatroschkaPackage { get { lock (_matroschkaPackageLock) { // Use the cached data if possible if (field is not null) return field; // Check to see if creation has already been attempted if (_matroschkaPackageFailed) return null; // Get the available source length, if possible var dataLength = Length; if (dataLength == -1) { _matroschkaPackageFailed = true; return null; } // Find the matrosch or rcpacker section SectionHeader? section = null; foreach (var searchedSection in SectionTable) { string sectionName = Encoding.ASCII.GetString(searchedSection.Name).TrimEnd('\0'); if (sectionName != "matrosch" && sectionName != "rcpacker") continue; section = searchedSection; break; } // Otherwise, it could not be found if (section is null) { _matroschkaPackageFailed = true; return null; } // Get the offset long offset = section.VirtualAddress.ConvertVirtualAddress(SectionTable); if (offset < 0 || offset >= Length) { _matroschkaPackageFailed = true; return null; } // Read the section into a local array var sectionLength = (int)section.SizeOfRawData; var sectionData = ReadRangeFromSource(offset, sectionLength); if (sectionData.Length == 0) { _matroschkaPackageFailed = true; return null; } // Parse the package field = SecuROMMatroschkaPackage.Create(sectionData, 0); if (field?.Entries is null) _matroschkaPackageFailed = true; return field; } } } = null; /// /// InstallShield Executable wrapper, if it exists /// public InstallShieldExecutable? ISEXE { get { lock (_installshieldExecutableLock) { // Use the cached data if possible if (field is not null) return field; // Check to see if creation has already been attempted if (_installshieldExecutableFailed) return null; // Get the available source length, if possible var dataLength = Length; if (dataLength == -1) { _installshieldExecutableFailed = true; return null; } // Check if there's a valid OverlayAddress if (OverlayAddress < 0 || OverlayAddress > dataLength) { _installshieldExecutableFailed = true; return null; } // Parse the package lock (_dataSourceLock) { _dataSource.SeekIfPossible(OverlayAddress, SeekOrigin.Begin); field = InstallShieldExecutable.Create(_dataSource); } if (field?.Entries.Length == 0) { _installshieldExecutableFailed = true; return null; } return field; } } } = null; /// public Data.Models.PortableExecutable.OptionalHeader OptionalHeader => Model.OptionalHeader; /// /// Address of the overlay, if it exists /// /// public long OverlayAddress { get { lock (_overlayAddressLock) { // Use the cached data if possible if (field >= 0) return field; // Get the available source length, if possible long dataLength = Length; if (dataLength == -1) { field = -1; return field; } // If we have certificate data, use that as the end if (OptionalHeader.CertificateTable is not null) { long certificateTableAddress = OptionalHeader.CertificateTable.VirtualAddress; if (certificateTableAddress != 0 && certificateTableAddress < dataLength) dataLength = certificateTableAddress; } // Start from the first section with a valid raw data size and add all section sizes // TODO: Handle cases where sections are overlapping var firstSection = Array.Find(SectionTable, s => s.SizeOfRawData != 0); long endOfSectionData = firstSection?.PointerToRawData ?? 0; Array.ForEach(SectionTable, s => endOfSectionData += s.SizeOfRawData); // If we didn't find the end of section data if (endOfSectionData <= 0) endOfSectionData = -1; // If the section data is followed by the end of the data if (endOfSectionData >= dataLength) endOfSectionData = -1; // Cache and return the position field = endOfSectionData; return field; } } } = -1; /// /// Overlay data, if it exists /// /// Caches up to 0x10000 bytes /// public byte[] OverlayData { get { lock (_overlayDataLock) { // Use the cached data if possible if (field is not null) return field; // Get the available source length, if possible long dataLength = Length; if (dataLength == -1) { field = []; return field; } // If we have certificate data, use that as the end if (OptionalHeader.CertificateTable is not null) { long certificateTableAddress = OptionalHeader.CertificateTable.VirtualAddress; if (certificateTableAddress != 0 && certificateTableAddress < dataLength) dataLength = certificateTableAddress; } // Get the overlay address and size if possible long endOfSectionData = OverlayAddress; long overlaySize = OverlaySize; // If we didn't find the address or size if (endOfSectionData <= 0 || overlaySize <= 0) { field = []; return field; } // If we're at the end of the file, cache an empty byte array if (endOfSectionData >= dataLength) { field = []; return field; } // Otherwise, cache and return the data overlaySize = Math.Min(overlaySize, 0x10000); field = ReadRangeFromSource(endOfSectionData, (int)overlaySize) ?? []; return field; } } } = null; /// /// Size of the overlay data, if it exists /// /// public long OverlaySize { get { lock (_overlaySizeLock) { // Use the cached data if possible if (field >= 0) return field; // Get the available source length, if possible long dataLength = Length; if (dataLength == -1) { field = 0; return field; } // If we have certificate data, use that as the end if (OptionalHeader.CertificateTable is not null) { long certificateTableAddress = OptionalHeader.CertificateTable.VirtualAddress; if (certificateTableAddress != 0 && certificateTableAddress < dataLength) dataLength = certificateTableAddress; } // Get the overlay address if possible long endOfSectionData = OverlayAddress; // If we didn't find the end of section data if (endOfSectionData <= 0) { field = 0; return field; } // If we're at the end of the file, cache an empty byte array if (endOfSectionData >= dataLength) { field = 0; return field; } // Otherwise, cache and return the length field = dataLength - endOfSectionData; return field; } } } = -1; /// /// Overlay strings, if they exist /// public List OverlayStrings { get { lock (_overlayStringsLock) { // Use the cached data if possible if (field is not null) return field; // Get the overlay data, if possible var overlayData = OverlayData; if (overlayData.Length == 0) { field = []; return field; } // Otherwise, cache and return the strings field = overlayData.ReadStringsFrom(charLimit: 3) ?? []; return field; } } } = null; /// public Data.Models.PortableExecutable.Resource.DirectoryTable? ResourceDirectoryTable => Model.ResourceDirectoryTable; /// /// Sanitized section names /// public string[] SectionNames { get { lock (_sectionNamesLock) { // Use the cached data if possible if (field is not null) return field; // Otherwise, build and return the cached array field = new string[SectionTable.Length]; for (int i = 0; i < field.Length; i++) { // TODO: Handle long section names with leading `/` var section = SectionTable[i]; string sectionNameString = Encoding.UTF8.GetString(section.Name).TrimEnd('\0'); field[i] = sectionNameString; } return field; } } } = null; /// public SectionHeader[] SectionTable => Model.SectionTable; /// /// Data after the section table, if it exists /// public byte[] SectionTableTrailerData { get { lock (_sectionTableTrailerDataLock) { // If we already have cached data, just use that immediately if (field is not null) return field; // Get the offset from the end of the section table long endOfSectionTable = Stub.Header.NewExeHeaderAddr + 24 // Signature size + file header size + COFFFileHeader.SizeOfOptionalHeader + (COFFFileHeader.NumberOfSections * 40); // Size of a section header // Assume the extra data aligns to 512-byte segments int alignment = (int)(OptionalHeader?.FileAlignment ?? 0x200); int trailerDataSize = alignment - (int)(endOfSectionTable % alignment); // Cache and return the section table trailer data, even if null field = ReadRangeFromSource(endOfSectionTable, trailerDataSize); return field; } } } = null; /// public Data.Models.MSDOS.Executable Stub => Model.Stub; /// /// Stub executable data, if it exists /// public byte[] StubExecutableData { get { lock (_stubExecutableDataLock) { // If we already have cached data, just use that immediately if (field is not null) return field; // Populate the raw stub executable data based on the source int endOfStubHeader = 0x40; int lengthOfStubExecutableData = (int)Stub.Header.NewExeHeaderAddr - endOfStubHeader; field = ReadRangeFromSource(endOfStubHeader, lengthOfStubExecutableData); // Cache and return the stub executable data, even if null return field; } } } = null; /// /// Dictionary of resource data /// public Dictionary ResourceData { get { lock (_resourceDataLock) { // Use the cached data if possible if (_resourceData.Count != 0) return _resourceData; // If we have no resource table, just return if (OptionalHeader.ResourceTable is null || OptionalHeader.ResourceTable.VirtualAddress == 0 || ResourceDirectoryTable is null) { return _resourceData; } // Otherwise, build and return the cached dictionary ParseResourceDirectoryTable(ResourceDirectoryTable, types: []); return _resourceData; } } } /// /// Wise section wrapper, if it exists /// public WiseSectionHeader? WiseSection { get { lock (_wiseSectionHeaderLock) { // If we already have cached data, just use that immediately if (field is not null) return field; // If the header will not be found due to missing section data if (_wiseSectionHeaderMissing) return null; // Find the .WISE section SectionHeader? wiseSection = null; foreach (var section in SectionTable) { string sectionName = Encoding.ASCII.GetString(section.Name).TrimEnd('\0'); if (sectionName != ".WISE") continue; wiseSection = section; break; } // If the section cannot be found if (wiseSection is null) { _wiseSectionHeaderMissing = true; return null; } // Get the physical offset of the section long offset = wiseSection.VirtualAddress.ConvertVirtualAddress(SectionTable); if (offset < 0 || offset >= Length) { _wiseSectionHeaderMissing = true; return null; } // Read the section into a local array int sectionLength = (int)wiseSection.VirtualSize; byte[] sectionData = ReadRangeFromSource(offset, sectionLength); if (sectionData.Length == 0) { _wiseSectionHeaderMissing = true; return null; } // Parse the section header field = WiseSectionHeader.Create(sectionData, 0); if (field is null) _wiseSectionHeaderMissing = true; return field; } } } = null; #region Version Information /// /// "Build GUID" /// public string? BuildGuid { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("BuildGuid"); return field; } } = null; /// /// "Build signature" /// public string? BuildSignature { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("BuildSignature"); return field; } } = null; /// /// Additional information that should be displayed for diagnostic purposes. /// public string? Comments { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("Comments"); return field; } } = null; /// /// Company that produced the file—for example, "Microsoft Corporation" or /// "Standard Microsystems Corporation, Inc." This string is required. /// public string? CompanyName { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("CompanyName"); return field; } } = null; /// /// "Debug version" /// public string? DebugVersion { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("DebugVersion"); return field; } } = null; /// /// File description to be presented to users. This string may be displayed in a /// list box when the user is choosing files to install—for example, "Keyboard /// Driver for AT-Style Keyboards". This string is required. /// public string? FileDescription { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("FileDescription"); return field; } } = null; /// /// Version number of the file—for example, "3.10" or "5.00.RC2". This string /// is required. /// public string? FileVersion { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("FileVersion"); return field; } } = null; /// /// Internal name of the file, if one exists—for example, a module name if the /// file is a dynamic-link library. If the file has no internal name, this /// string should be the original filename, without extension. This string is required. /// public string? InternalName { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("InternalName"); return field; } } = null; /// /// Copyright notices that apply to the file. This should include the full text of /// all notices, legal symbols, copyright dates, and so on. This string is optional. /// public string? LegalCopyright { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("LegalCopyright"); return field; } } = null; /// /// Trademarks and registered trademarks that apply to the file. This should include /// the full text of all notices, legal symbols, trademark numbers, and so on. This /// string is optional. /// public string? LegalTrademarks { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("LegalTrademarks"); return field; } } = null; /// /// Original name of the file, not including a path. This information enables an /// application to determine whether a file has been renamed by a user. The format of /// the name depends on the file system for which the file was created. This string /// is required. /// public string? OriginalFilename { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("OriginalFilename"); return field; } } = null; /// /// Information about a private version of the file—for example, "Built by TESTER1 on /// \TESTBED". This string should be present only if VS_FF_PRIVATEBUILD is specified in /// the fileflags parameter of the root block. /// public string? PrivateBuild { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("PrivateBuild"); return field; } } = null; /// /// "Product GUID" /// public string? ProductGuid { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("ProductGuid"); return field; } } = null; /// /// Name of the product with which the file is distributed. This string is required. /// public string? ProductName { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("ProductName"); return field; } } = null; /// /// Version of the product with which the file is distributed—for example, "3.10" or /// "5.00.RC2". This string is required. /// public string? ProductVersion { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("ProductVersion"); return field; } } = null; /// /// Text that specifies how this version of the file differs from the standard /// version—for example, "Private build for TESTER1 solving mouse problems on M250 and /// M250E computers". This string should be present only if VS_FF_SPECIALBUILD is /// specified in the fileflags parameter of the root block. /// public string? SpecialBuild { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("SpecialBuild") ?? GetVersionInfoString("Special Build"); return field; } } = null; /// /// "Trade name" /// public string? TradeName { get { // Use the cached data if possible if (field is not null) return field; field = GetVersionInfoString("TradeName"); return field; } } = null; /// /// Get the internal version as reported by the resources /// /// Version string, null on error /// The internal version is either the file version, product version, or assembly version, in that order public string? GetInternalVersion() { string? version = FileVersion; if (!string.IsNullOrEmpty(version)) return version!.Replace(", ", "."); version = ProductVersion; if (!string.IsNullOrEmpty(version)) return version!.Replace(", ", "."); version = AssemblyVersion; if (!string.IsNullOrEmpty(version)) return version; return null; } #endregion #region Manifest Information /// /// Description as derived from the assembly manifest /// public string? AssemblyDescription { get { var manifest = GetAssemblyManifest(); return manifest? .Description? .Value; } } /// /// Name as derived from the assembly manifest /// /// /// If there are multiple identities included in the manifest, /// this will only retrieve the value from the first that doesn't /// have a null or empty name. /// public string? AssemblyName { get { var manifest = GetAssemblyManifest(); var identities = manifest?.AssemblyIdentities ?? []; var nameIdentity = Array.Find(identities, ai => !string.IsNullOrEmpty(ai?.Name)); return nameIdentity?.Name; } } /// /// Version as derived from the assembly manifest /// /// /// If there are multiple identities included in the manifest, /// this will only retrieve the value from the first that doesn't /// have a null or empty version. /// public string? AssemblyVersion { get { var manifest = GetAssemblyManifest(); var identities = manifest?.AssemblyIdentities ?? []; var versionIdentity = Array.Find(identities, ai => !string.IsNullOrEmpty(ai?.Version)); return versionIdentity?.Version; } } #endregion #endregion #region Instance Variables /// /// Lock object for /// private readonly object _debugDataLock = new(); /// /// Lock object for /// private readonly object _entryPointDataLock = new(); /// /// Lock object for /// private readonly object _headerPaddingDataLock = new(); /// /// Lock object for /// private readonly object _headerPaddingStringsLock = new(); /// /// Lock object for /// private readonly object _installshieldExecutableLock = new(); /// /// Cached attempt at creation for /// private bool _installshieldExecutableFailed = false; /// /// Lock object for /// private readonly object _matroschkaPackageLock = new(); /// /// Cached attempt at creation for /// private bool _matroschkaPackageFailed = false; /// /// Lock object for /// private readonly object _overlayAddressLock = new(); /// /// Lock object for /// private readonly object _overlayDataLock = new(); /// /// Lock object for /// private readonly object _overlaySizeLock = new(); /// /// Lock object for /// private readonly object _overlayStringsLock = new(); /// /// Cached resource data /// private readonly Dictionary _resourceData = []; /// /// Lock object for /// private readonly object _resourceDataLock = new(); /// /// Lock object for /// private readonly object _sectionNamesLock = new(); /// /// Cached raw section data /// private byte[][]? _sectionData = null; /// /// Cached found string data in sections /// private List?[]? _sectionStringData = null; /// /// Lock object for /// private readonly object _sectionStringDataLock = new(); /// /// Lock object for /// private readonly object _sectionTableTrailerDataLock = new(); /// /// Lock object for /// private readonly object _stubExecutableDataLock = new(); /// /// Cached raw table data /// private readonly byte[][] _tableData = new byte[16][]; /// /// Cached found string data in tables /// private readonly List?[] _tableStringData = new List?[16]; /// /// Lock object for /// private readonly object _wiseSectionHeaderLock = new(); /// /// Indicates if cannot be found /// private bool _wiseSectionHeaderMissing = false; #region Version Information /// /// Cached version info data /// private VersionInfo? _versionInfo = null; #endregion #region Manifest Information /// /// Cached assembly manifest data /// private AssemblyManifest? _assemblyManifest = null; #endregion #endregion #region Constructors /// public PortableExecutable(Executable model, byte[] data) : base(model, data) { } /// public PortableExecutable(Executable model, byte[] data, int offset) : base(model, data, offset) { } /// public PortableExecutable(Executable model, byte[] data, int offset, int length) : base(model, data, offset, length) { } /// public PortableExecutable(Executable model, Stream data) : base(model, data) { } /// public PortableExecutable(Executable model, Stream data, long offset) : base(model, data, offset) { } /// public PortableExecutable(Executable model, Stream data, long offset, long length) : base(model, data, offset, length) { } #endregion #region Static Constructors /// /// Create a PE executable from a byte array and offset /// /// Byte array representing the executable /// Offset within the array to parse /// A PE executable wrapper on success, null on failure public static PortableExecutable? Create(byte[]? data, int offset) { // If the data is invalid if (data is null || data.Length == 0) return null; // If the offset is out of bounds if (offset < 0 || offset >= data.Length) return null; // Create a memory stream and use that var dataStream = new MemoryStream(data, offset, data.Length - offset); return Create(dataStream); } /// /// Create a PE executable from a Stream /// /// Stream representing the executable /// A PE executable wrapper on success, null on failure public static PortableExecutable? Create(Stream? data) { // If the data is invalid if (data is null || !data.CanRead) return null; try { // Cache the current offset long currentOffset = data.Position; var model = new Serialization.Readers.PortableExecutable().Deserialize(data); if (model is null) return null; return new PortableExecutable(model, data, currentOffset); } catch { return null; } } #endregion #region Data // TODO: Cache all certificate objects /// /// Get the version info string associated with a key, if possible /// /// Case-insensitive key to find in the version info /// String representing the data, null on error /// /// This code does not take into account the locale and will find and return /// the first available value. This may not actually matter for version info, /// but it is worth mentioning. /// public string? GetVersionInfoString(string key) { // If we have an invalid key, we can't do anything if (string.IsNullOrEmpty(key)) return null; // Ensure the resource table has been parsed if (ResourceData is null) return null; // If we don't have string version info in this executable var stringTable = _versionInfo?.StringFileInfo?.Children; if (stringTable is null || stringTable.Length == 0) return null; // Try to find a key that matches StringData? match = null; foreach (var st in stringTable) { if (st.Length == 0) continue; // Return the match if found match = Array.Find(st.Children, sd => key.Equals(sd.Key, StringComparison.OrdinalIgnoreCase)); if (match is not null) return match.Value?.TrimEnd('\0'); } return null; } /// /// Get the assembly manifest, if possible /// /// Assembly manifest object, null on error private AssemblyManifest? GetAssemblyManifest() { // Use the cached data if possible if (_assemblyManifest is not null) return _assemblyManifest; // Cache the resource data for easier reading var resourceData = ResourceData; if (resourceData.Count == 0) return null; // Return the now-cached assembly manifest return _assemblyManifest; } #endregion #region Debug Data /// /// Find CodeView debug data by path /// /// Partial path to check for /// List of matching debug data public List FindCodeViewDebugTableByPath(string path) { // Cache the debug data for easier reading var debugData = DebugData; if (debugData.Count == 0) return []; var debugFound = new List(); foreach (var data in debugData.Values) { if (data is null) continue; if (data is Data.Models.PortableExecutable.DebugData.NB10ProgramDatabase n) { if (n.PdbFileName is null || !n.PdbFileName.Contains(path)) continue; debugFound.Add(n); } else if (data is Data.Models.PortableExecutable.DebugData.RSDSProgramDatabase r) { if (r.PathAndFileName is null || !r.PathAndFileName.Contains(path)) continue; debugFound.Add(r); } } return debugFound; } /// /// Find unparsed debug data by string value /// /// String value to check for /// List of matching debug data public List FindGenericDebugTableByValue(string value) { // Cache the debug data for easier reading var debugData = DebugData; if (debugData.Count == 0) return []; var table = new List(); foreach (var data in debugData.Values) { if (data is null) continue; if (data is not byte[] b || b is null) continue; try { string? arrayAsASCII = Encoding.ASCII.GetString(b); if (arrayAsASCII.Contains(value)) { table.Add(b); continue; } } catch { } try { string? arrayAsUTF8 = Encoding.UTF8.GetString(b); if (arrayAsUTF8.Contains(value)) { table.Add(b); continue; } } catch { } try { string? arrayAsUnicode = Encoding.Unicode.GetString(b); if (arrayAsUnicode.Contains(value)) { table.Add(b); continue; } } catch { } } return table; } #endregion #region Debug Parsing /// /// Parse the debug directory table information /// private Dictionary ParseDebugTable() { // If there is no debug table if (DebugDirectoryTable is null || DebugDirectoryTable.Length == 0) return []; // Create a new debug table Dictionary debugData = []; // Loop through all debug table entries for (int i = 0; i < DebugDirectoryTable.Length; i++) { var entry = DebugDirectoryTable[i]; uint address = entry.PointerToRawData; uint size = entry.SizeOfData; // Read the entry data until we have the end of the stream byte[]? entryData; try { entryData = ReadRangeFromSource((int)address, (int)size); if (entryData.Length < 4) continue; } catch (EndOfStreamException) { return debugData; } // If we have CodeView debug data, try to parse it if (entry.DebugType == DebugType.IMAGE_DEBUG_TYPE_CODEVIEW) { // Read the signature int offset = 0; uint signature = entryData.ReadUInt32LittleEndian(ref offset); // Reset the offset offset = 0; // NB10 if (signature == 0x3031424E) { var nb10ProgramDatabase = Serialization.Readers.PortableExecutable.ParseNB10ProgramDatabase(entryData, ref offset); if (nb10ProgramDatabase is not null) { debugData[i] = nb10ProgramDatabase; continue; } } // RSDS else if (signature == 0x53445352) { var rsdsProgramDatabase = Serialization.Readers.PortableExecutable.ParseRSDSProgramDatabase(entryData, ref offset); if (rsdsProgramDatabase is not null) { debugData[i] = rsdsProgramDatabase; continue; } } } else { debugData[i] = entryData; } } return debugData; } #endregion #region Resource Data /// /// Find dialog box resources by title /// /// Dialog box title to check for /// List of matching resources public List FindDialogByTitle(string title) { // Cache the resource data for easier reading var resourceData = ResourceData; if (resourceData.Count == 0) return []; var resources = new List(); foreach (var resource in resourceData.Values) { if (resource is null) continue; if (resource is not DialogBoxResource dbr || dbr is null) continue; if (dbr.DialogTemplate?.TitleResource?.Contains(title) ?? false) resources.Add(dbr); else if (dbr.ExtendedDialogTemplate?.TitleResource?.Contains(title) ?? false) resources.Add(dbr); } return resources; } /// /// Find dialog box resources by contained item title /// /// Dialog box item title to check for /// List of matching resources public List FindDialogBoxByItemTitle(string title) { // Cache the resource data for easier reading var resourceData = ResourceData; if (resourceData.Count == 0) return []; var resources = new List(); foreach (var resource in resourceData.Values) { if (resource is null) continue; if (resource is not DialogBoxResource dbr || dbr is null) continue; if (dbr.DialogItemTemplates is not null) { var templates = Array.FindAll(dbr.DialogItemTemplates, dit => dit?.TitleResource is not null); if (Array.FindIndex(templates, dit => dit?.TitleResource?.Contains(title) == true) > -1) resources.Add(dbr); } else if (dbr.ExtendedDialogItemTemplates is not null) { var templates = Array.FindAll(dbr.ExtendedDialogItemTemplates, edit => edit?.TitleResource is not null); if (Array.FindIndex(templates, edit => edit?.TitleResource?.Contains(title) == true) > -1) resources.Add(dbr); } } return resources; } /// /// Find string table resources by contained string entry /// /// String entry to check for /// List of matching resources public List FindStringTableByEntry(string entry) { // Cache the resource data for easier reading var resourceData = ResourceData; if (resourceData.Count == 0) return []; var stringTables = new List(); foreach (var resource in resourceData.Values) { if (resource is null) continue; if (resource is not StringTableResource st || st is null) continue; foreach (string? s in st.Data.Values) { #if NETFRAMEWORK || NETSTANDARD if (s is null || !s.Contains(entry)) #else if (s is null || !s.Contains(entry, StringComparison.OrdinalIgnoreCase)) #endif continue; stringTables.Add(st); break; } } return stringTables; } /// /// Find unparsed resources by type name /// /// Type name to check for /// List of matching resources public List FindResourceByNamedType(string typeName) { // Cache the resource data for easier reading var resourceData = ResourceData; if (resourceData.Count == 0) return []; var resources = new List(); foreach (var kvp in resourceData) { if (!kvp.Key.Contains(typeName)) continue; if (kvp.Value is null || kvp.Value is not GenericResourceEntry b || b is null) continue; resources.Add(b.Data); } return resources; } /// /// Find unparsed resources by string value /// /// String value to check for /// List of matching resources public List FindGenericResource(string value) { // Cache the resource data for easier reading var resourceData = ResourceData; if (resourceData.Count == 0) return []; var resources = new List(); foreach (var resource in resourceData.Values) { if (resource is null) continue; if (resource is not GenericResourceEntry b || b is null) continue; try { string? arrayAsASCII = Encoding.ASCII.GetString(b!.Data); if (arrayAsASCII.Contains(value)) { resources.Add(b.Data); continue; } } catch { } try { string? arrayAsUTF8 = Encoding.UTF8.GetString(b!.Data); if (arrayAsUTF8.Contains(value)) { resources.Add(b.Data); continue; } } catch { } try { string? arrayAsUnicode = Encoding.Unicode.GetString(b!.Data); if (arrayAsUnicode.Contains(value)) { resources.Add(b.Data); continue; } } catch { } } return resources; } /// /// Find the location of a Wise overlay header, if it exists /// /// Offset to the overlay header on success, -1 otherwise public long FindWiseOverlayHeader() { // Get the overlay offset long overlayOffset = OverlayAddress; lock (_dataSourceLock) { // Attempt to get the overlay header if (overlayOffset >= 0 && overlayOffset < Length) { _dataSource.SeekIfPossible(overlayOffset, SeekOrigin.Begin); var header = WiseOverlayHeader.Create(_dataSource); if (header is not null) return overlayOffset; } // Check section data foreach (var section in SectionTable) { string sectionName = Encoding.ASCII.GetString(section.Name).TrimEnd('\0'); long sectionOffset = section.VirtualAddress.ConvertVirtualAddress(SectionTable); _dataSource.SeekIfPossible(sectionOffset, SeekOrigin.Begin); var header = WiseOverlayHeader.Create(_dataSource); if (header is not null) return sectionOffset; // Check after the resource table if (sectionName == ".rsrc") { // Data immediately following long afterResourceOffset = sectionOffset + section.SizeOfRawData; _dataSource.SeekIfPossible(afterResourceOffset, SeekOrigin.Begin); header = WiseOverlayHeader.Create(_dataSource); if (header is not null) return afterResourceOffset; // Data following padding data _dataSource.SeekIfPossible(afterResourceOffset, SeekOrigin.Begin); _ = _dataSource.ReadNullTerminatedAnsiString(); afterResourceOffset = _dataSource.Position; header = WiseOverlayHeader.Create(_dataSource); if (header is not null) return afterResourceOffset; } } } // If there are no resources if (OptionalHeader.ResourceTable is null) return -1; // Cache the resource data for easier reading var resourceData = ResourceData; if (resourceData.Count == 0) return -1; // Get the resources that have an executable signature bool exeResources = false; foreach (var kvp in resourceData) { if (kvp.Value is null || kvp.Value is not GenericResourceEntry ba) continue; if (!ba.Data.StartsWith(Data.Models.MSDOS.Constants.SignatureBytes)) continue; exeResources = true; break; } // If there are no executable resources if (!exeResources) return -1; // Get the raw resource table offset long resourceTableOffset = OptionalHeader.ResourceTable.VirtualAddress.ConvertVirtualAddress(SectionTable); if (resourceTableOffset <= 0) return -1; lock (_dataSourceLock) { // Search the resource table data for the offset long resourceOffset = -1; _dataSource.SeekIfPossible(resourceTableOffset, SeekOrigin.Begin); while (_dataSource.Position < resourceTableOffset + OptionalHeader.ResourceTable.Size && _dataSource.Position < _dataSource.Length) { ushort possibleSignature = _dataSource.ReadUInt16LittleEndian(); if (possibleSignature == Data.Models.MSDOS.Constants.SignatureUInt16) { resourceOffset = _dataSource.Position - 2; break; } _dataSource.SeekIfPossible(-1, SeekOrigin.Current); } // If there was no valid offset, somehow if (resourceOffset == -1) return -1; // Parse the executable and recurse _dataSource.SeekIfPossible(resourceOffset, SeekOrigin.Begin); var resourceExe = WrapperFactory.CreateExecutableWrapper(_dataSource); if (resourceExe is not PortableExecutable resourcePex) return -1; return resourcePex.FindWiseOverlayHeader(); } } #endregion #region Resource Parsing /// /// Parse the resource directory table information /// private void ParseResourceDirectoryTable(Data.Models.PortableExecutable.Resource.DirectoryTable table, List types) { for (int i = 0; i < table.Entries.Length; i++) { var entry = table.Entries[i]; var newTypes = new List(types); if (entry.Name?.UnicodeString is not null) newTypes.Add(Encoding.Unicode.GetString(entry.Name.UnicodeString)); else newTypes.Add(entry.IntegerID); ParseResourceDirectoryEntry(entry, newTypes); } } /// /// Parse the name resource directory entry information /// private void ParseResourceDirectoryEntry(Data.Models.PortableExecutable.Resource.DirectoryEntry entry, List types) { if (entry.DataEntry is not null) ParseResourceDataEntry(entry.DataEntry, types); else if (entry.Subdirectory is not null) ParseResourceDirectoryTable(entry.Subdirectory, types); } /// /// Parse the resource data entry information /// /// /// When caching the version information and assembly manifest, this code assumes that there is only one of each /// of those resources in the entire exectuable. This means that only the last found version or manifest will /// ever be cached. /// private void ParseResourceDataEntry(Data.Models.PortableExecutable.Resource.DataEntry entry, List types) { // Create the key and value objects string key = string.Join(", ", Array.ConvertAll([.. types], t => t.ToString())); ResourceDataType? value = Serialization.Readers.PortableExecutable.ParseGenericResourceEntry(entry.Data); // If we have a known resource type if (types.Count > 0 && types[0] is uint resourceType) { try { switch ((ResourceType)resourceType) { case ResourceType.RT_CURSOR: // TODO: Implement specific parsing break; case ResourceType.RT_BITMAP: case ResourceType.RT_NEWBITMAP: // TODO: Implement specific parsing break; case ResourceType.RT_ICON: // TODO: Implement specific parsing break; case ResourceType.RT_MENU: case ResourceType.RT_NEWMENU: value = Serialization.Readers.PortableExecutable.ParseMenuResource(entry.Data); break; case ResourceType.RT_DIALOG: case ResourceType.RT_NEWDIALOG: value = Serialization.Readers.PortableExecutable.ParseDialogBoxResource(entry.Data); break; case ResourceType.RT_STRING: value = Serialization.Readers.PortableExecutable.ParseStringTableResource(entry.Data); break; case ResourceType.RT_FONTDIR: // TODO: Implement specific parsing break; case ResourceType.RT_FONT: // TODO: Implement specific parsing break; case ResourceType.RT_ACCELERATOR: value = Serialization.Readers.PortableExecutable.ParseAcceleratorTable(entry.Data); break; case ResourceType.RT_RCDATA: // TODO: Implement specific parsing break; case ResourceType.RT_MESSAGETABLE: value = Serialization.Readers.PortableExecutable.ParseMessageResourceData(entry.Data); break; case ResourceType.RT_GROUP_CURSOR: // TODO: Implement specific parsing break; case ResourceType.RT_GROUP_ICON: // TODO: Implement specific parsing break; case ResourceType.RT_VERSION: _versionInfo = Serialization.Readers.PortableExecutable.ParseVersionInfo(entry.Data); value = _versionInfo; break; case ResourceType.RT_DLGINCLUDE: // TODO: Implement specific parsing break; case ResourceType.RT_PLUGPLAY: // TODO: Implement specific parsing break; case ResourceType.RT_VXD: // TODO: Implement specific parsing break; case ResourceType.RT_ANICURSOR: // TODO: Implement specific parsing break; case ResourceType.RT_ANIICON: // TODO: Implement specific parsing break; case ResourceType.RT_HTML: // TODO: Implement specific parsing break; case ResourceType.RT_MANIFEST: _assemblyManifest = Serialization.Readers.PortableExecutable.ParseAssemblyManifest(entry.Data); value = _assemblyManifest; break; // Bitflag, ignore case ResourceType.RT_NEWRESOURCE: break; // Error state, ignore case ResourceType.RT_ERROR: // TODO: Implement specific parsing break; default: // TODO: Implement specific parsing break; } } catch { // Fall back on byte array data for malformed items value = Serialization.Readers.PortableExecutable.ParseGenericResourceEntry(entry.Data); } } // If we have a custom resource type else if (types.Count > 0 && types[0] is string) { value = Serialization.Readers.PortableExecutable.ParseGenericResourceEntry(entry.Data); } // Add the key and value to the cache _resourceData[key] = value; } #endregion #region Sections /// /// Determine if a section is contained within the section table /// /// Name of the section to check for /// True to enable exact matching of names, false for starts-with /// True if the section is in the executable, false otherwise public bool ContainsSection(string? sectionName, bool exact = false) { // If no section name is provided if (sectionName is null) return false; // Get all section names first if (SectionNames.Length == 0) return false; // If we're checking exactly, return only exact matches if (exact) return Array.FindIndex(SectionNames, n => n.Equals(sectionName)) > -1; // Otherwise, check if section name starts with the value else return Array.FindIndex(SectionNames, n => n.StartsWith(sectionName)) > -1; } /// /// Get the section index corresponding to the entry point, if possible /// /// Section index on success, null on error public int FindEntryPointSectionIndex() { // If we don't have an entry point if (OptionalHeader.AddressOfEntryPoint.ConvertVirtualAddress(SectionTable) == 0) return -1; // Otherwise, find the section it exists within return OptionalHeader.AddressOfEntryPoint.ContainingSectionIndex(SectionTable); } /// /// Get the first section based on name, if possible /// /// Name of the section to check for /// True to enable exact matching of names, false for starts-with /// Section data on success, null on error public SectionHeader? GetFirstSection(string? name, bool exact = false) { // If we have no sections if (SectionNames.Length == 0 || SectionTable.Length == 0) return null; // If the section doesn't exist if (!ContainsSection(name, exact)) return null; // Get the first index of the section int index = Array.IndexOf(SectionNames, name); if (index == -1) return null; // Return the section return SectionTable[index]; } /// /// Get the last section based on name, if possible /// /// Name of the section to check for /// True to enable exact matching of names, false for starts-with /// Section data on success, null on error public SectionHeader? GetLastSection(string? name, bool exact = false) { // If we have no sections if (SectionNames.Length == 0 || SectionTable.Length == 0) return null; // If the section doesn't exist if (!ContainsSection(name, exact)) return null; // Get the last index of the section int index = Array.LastIndexOf(SectionNames, name); if (index == -1) return null; // Return the section return SectionTable[index]; } /// /// Get the section based on index, if possible /// /// Index of the section to check for /// Section data on success, null on error public SectionHeader? GetSection(int index) { // If we have no sections if (SectionTable.Length == 0) return null; // If the section doesn't exist if (index < 0 || index >= SectionTable.Length) return null; // Return the section return SectionTable[index]; } /// /// Get the first section data based on name, if possible /// /// Name of the section to check for /// True to enable exact matching of names, false for starts-with /// Section data on success, null on error public byte[]? GetFirstSectionData(string? name, bool exact = false) { // If we have no sections if (SectionNames.Length == 0 || SectionTable.Length == 0) return null; // If the section doesn't exist if (!ContainsSection(name, exact)) return null; // Get the first index of the section int index = Array.IndexOf(SectionNames, name); return GetSectionData(index); } /// /// Get the last section data based on name, if possible /// /// Name of the section to check for /// True to enable exact matching of names, false for starts-with /// Section data on success, null on error public byte[]? GetLastSectionData(string? name, bool exact = false) { // If we have no sections if (SectionNames.Length == 0 || SectionTable.Length == 0) return null; // If the section doesn't exist if (!ContainsSection(name, exact)) return null; // Get the last index of the section int index = Array.LastIndexOf(SectionNames, name); return GetSectionData(index); } /// /// Get the section data based on index, if possible /// /// Index of the section to check for /// Section data on success, null on error public byte[]? GetSectionData(int index) { // If we have no sections if (SectionNames.Length == 0 || SectionTable.Length == 0) return null; // If the section doesn't exist if (index < 0 || index >= SectionTable.Length) return null; // Get the section data from the table var section = SectionTable[index]; uint address = section.VirtualAddress.ConvertVirtualAddress(SectionTable); if (address == 0) return null; // Set the section size uint size = section.SizeOfRawData; // Create the section data array if we have to _sectionData ??= new byte[SectionNames.Length][]; // If we already have cached data, just use that immediately if (_sectionData[index] is not null && _sectionData[index].Length > 0) return _sectionData[index]; // Populate the raw section data based on the source var sectionData = ReadRangeFromSource((int)address, (int)size); // Cache and return the section data _sectionData[index] = sectionData; return sectionData; } /// /// Get the first section strings based on name, if possible /// /// Name of the section to check for /// True to enable exact matching of names, false for starts-with /// Section strings on success, null on error public List? GetFirstSectionStrings(string? name, bool exact = false) { // If we have no sections if (SectionNames.Length == 0 || SectionTable.Length == 0) return null; // If the section doesn't exist if (!ContainsSection(name, exact)) return null; // Get the first index of the section int index = Array.IndexOf(SectionNames, name); return GetSectionStrings(index); } /// /// Get the last section strings based on name, if possible /// /// Name of the section to check for /// True to enable exact matching of names, false for starts-with /// Section strings on success, null on error public List? GetLastSectionStrings(string? name, bool exact = false) { // If we have no sections if (SectionNames.Length == 0 || SectionTable.Length == 0) return null; // If the section doesn't exist if (!ContainsSection(name, exact)) return null; // Get the last index of the section int index = Array.LastIndexOf(SectionNames, name); return GetSectionStrings(index); } /// /// Get the section strings based on index, if possible /// /// Index of the section to check for /// Section strings on success, null on error public List? GetSectionStrings(int index) { // If we have no sections if (SectionNames.Length == 0 || SectionTable.Length == 0) return null; // If the section doesn't exist if (index < 0 || index >= SectionTable.Length) return null; lock (_sectionStringDataLock) { // Create the section string array if we have to _sectionStringData ??= new List?[SectionNames.Length]; // If we already have cached data, just use that immediately if (_sectionStringData[index] is not null) return _sectionStringData[index]; // Get the section data, if possible byte[]? sectionData = GetSectionData(index); if (sectionData is null || sectionData.Length == 0) { _sectionStringData[index] = []; return _sectionStringData[index]; } // Otherwise, cache and return the strings _sectionStringData[index] = sectionData.ReadStringsFrom(charLimit: 3) ?? []; return _sectionStringData[index]; } } #endregion #region Tables /// /// Get the table based on index, if possible /// /// Index of the table to check for /// Table on success, null on error public DataDirectory? GetTable(int index) { // If the table doesn't exist if (index < 0 || index > 16) return null; return index switch { 1 => OptionalHeader.ExportTable, 2 => OptionalHeader.ImportTable, 3 => OptionalHeader.ResourceTable, 4 => OptionalHeader.ExceptionTable, 5 => OptionalHeader.CertificateTable, 6 => OptionalHeader.BaseRelocationTable, 7 => OptionalHeader.Debug, 8 => null, // Architecture Table 9 => OptionalHeader.GlobalPtr, 10 => OptionalHeader.ThreadLocalStorageTable, 11 => OptionalHeader.LoadConfigTable, 12 => OptionalHeader.BoundImport, 13 => OptionalHeader.ImportAddressTable, 14 => OptionalHeader.DelayImportDescriptor, 15 => OptionalHeader.CLRRuntimeHeader, 16 => null, // Reserved // Should never be possible _ => null, }; } /// /// Get the table data based on index, if possible /// /// Index of the table to check for /// Table data on success, null on error public byte[]? GetTableData(int index) { // If the table doesn't exist if (index < 0 || index > 16) return null; // If we already have cached data, just use that immediately if (_tableData[index] is not null && _tableData[index].Length > 0) return _tableData[index]; // Get the table from the optional header var table = GetTable(index); // Get the virtual address and size from the entries uint virtualAddress = table?.VirtualAddress ?? 0; uint size = table?.Size ?? 0; // Get the physical address from the virtual one uint address = virtualAddress.ConvertVirtualAddress(SectionTable); if (address == 0 || size == 0) return null; // Populate the raw table data based on the source var tableData = ReadRangeFromSource((int)address, (int)size); // Cache and return the table data _tableData[index] = tableData; return tableData; } /// /// Get the table strings based on index, if possible /// /// Index of the table to check for /// Table strings on success, null on error public List? GetTableStrings(int index) { // If the table doesn't exist if (index < 0 || index > 16) return null; // If we already have cached data, just use that immediately if (_tableStringData[index] is not null) return _tableStringData[index]; // Get the table data, if possible byte[]? tableData = GetTableData(index); if (tableData is null || tableData.Length == 0) { _tableStringData[index] = []; return _tableStringData[index]; } // Otherwise, cache and return the strings _tableStringData[index] = tableData.ReadStringsFrom(charLimit: 5) ?? []; return _tableStringData[index]; } #endregion } }