using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Xml; using BurnOutSharp.ExecutableType.Microsoft.PE; using BurnOutSharp.ExecutableType.Microsoft.PE.Entries; using BurnOutSharp.ExecutableType.Microsoft.PE.Sections; using BurnOutSharp.ExecutableType.Microsoft.PE.Tables; using BurnOutSharp.ExecutableType.Microsoft.Resources; namespace BurnOutSharp.Tools { internal static class Utilities { #region Dictionary Manipulation /// /// Append one result to a results dictionary /// /// Dictionary to append to /// Key to add information to /// String value to add public static void AppendToDictionary(ConcurrentDictionary> original, string key, string value) { // If the value is empty, don't add it if (string.IsNullOrWhiteSpace(value)) return; var values = new ConcurrentQueue(); values.Enqueue(value); AppendToDictionary(original, key, values); } /// /// Append one result to a results dictionary /// /// Dictionary to append to /// Key to add information to /// String value to add public static void AppendToDictionary(ConcurrentDictionary> original, string key, ConcurrentQueue values) { // If the dictionary is null, just return if (original == null) return; // Use a placeholder value if the key is null key = key ?? "NO FILENAME"; // Add the key if needed and then append the lists original.TryAdd(key, new ConcurrentQueue()); original[key].AddRange(values); } /// /// Append one results dictionary to another /// /// Dictionary to append to /// Dictionary to pull from public static void AppendToDictionary(ConcurrentDictionary> original, ConcurrentDictionary> addition) { // If either dictionary is missing, just return if (original == null || addition == null) return; // Loop through each of the addition keys and add accordingly foreach (string key in addition.Keys) { original.TryAdd(key, new ConcurrentQueue()); original[key].AddRange(addition[key]); } } /// /// Remove empty or null keys from a results dictionary /// /// Dictionary to clean public static void ClearEmptyKeys(ConcurrentDictionary> original) { // If the dictionary is missing, we can't do anything if (original == null) return; // Get a list of all of the keys var keys = original.Keys.ToList(); // Iterate and reset keys for (int i = 0; i < keys.Count; i++) { // Get the current key string key = keys[i]; // If the key is empty, remove it if (original[key] == null || !original[key].Any()) original.TryRemove(key, out _); } } /// /// Prepend a parent path from dictionary keys, if possible /// /// Dictionary to strip values from /// Path to strip from the keys public static void PrependToKeys(ConcurrentDictionary> original, string pathToPrepend) { // If the dictionary is missing, we can't do anything if (original == null) return; // Use a placeholder value if the path is null pathToPrepend = (pathToPrepend ?? "ARCHIVE").TrimEnd(Path.DirectorySeparatorChar); // Get a list of all of the keys var keys = original.Keys.ToList(); // Iterate and reset keys for (int i = 0; i < keys.Count; i++) { // Get the current key string currentKey = keys[i]; // Otherwise, get the new key name and transfer over string newKey = $"{pathToPrepend}{Path.DirectorySeparatorChar}{currentKey.Trim(Path.DirectorySeparatorChar)}"; original[newKey] = original[currentKey]; original.TryRemove(currentKey, out _); } } /// /// Strip a parent path from dictionary keys, if possible /// /// Dictionary to strip values from /// Path to strip from the keys public static void StripFromKeys(ConcurrentDictionary> original, string pathToStrip) { // If either is missing, we can't do anything if (original == null || string.IsNullOrEmpty(pathToStrip)) return; // Get a list of all of the keys var keys = original.Keys.ToList(); // Iterate and reset keys for (int i = 0; i < keys.Count; i++) { // Get the current key string currentKey = keys[i]; // If the key doesn't start with the path, don't touch it if (!currentKey.StartsWith(pathToStrip, StringComparison.OrdinalIgnoreCase)) continue; // Otherwise, get the new key name and transfer over string newKey = currentKey.Substring(pathToStrip.Length); original[newKey] = original[currentKey]; original.TryRemove(currentKey, out _); } } #endregion #region Concurrent Manipulation /// /// Add a range of values from one queue to another /// /// Queue to add data to /// Queue to get data from public static void AddRange(this ConcurrentQueue original, ConcurrentQueue values) { while (!values.IsEmpty) { if (!values.TryDequeue(out string value)) return; original.Enqueue(value); } } #endregion #region Executable Information /// /// Get the company name as reported by the filesystem /// /// PortableExecutable representing the file contents /// Company name string, null on error public static string GetCompanyName(PortableExecutable pex) => GetResourceString(pex, "CompanyName"); /// /// Get the file description as reported by the filesystem /// /// PortableExecutable representing the file contents /// Description string, null on error public static string GetFileDescription(PortableExecutable pex) => GetResourceString(pex, "FileDescription"); /// /// Get the file version as reported by the filesystem /// /// Byte array representing the file contents /// Version string, null on error public static string GetFileVersion(byte[] fileContent) { if (fileContent == null || !fileContent.Any()) return null; return GetFileVersion(new PortableExecutable(fileContent, 0)); } /// /// Get the file version as reported by the filesystem /// /// File to check for version /// Version string, null on error public static string GetFileVersion(string file) { var fvinfo = GetFileVersionInfo(file); if (fvinfo?.FileVersion == null) return string.Empty; if (fvinfo.FileVersion != "") return fvinfo.FileVersion.Replace(", ", "."); else return fvinfo.ProductVersion.Replace(", ", "."); } /// /// Get the file version as reported by the filesystem /// /// PortableExecutable representing the file contents /// Version string, null on error public static string GetFileVersion(PortableExecutable pex) { string version = GetResourceString(pex, "FileVersion"); if (!string.IsNullOrWhiteSpace(version)) return version.Replace(", ", "."); version = GetResourceString(pex, "ProductVersion"); if (!string.IsNullOrWhiteSpace(version)) return version.Replace(", ", "."); return null; } /// /// Wrapper for GetFileVersion for use in content matching /// /// File to check for version /// Byte array representing the file contents /// Last matched positions in the contents /// Version string, null on error public static string GetFileVersion(string file, byte[] fileContent, List positions) => GetFileVersion(fileContent); /// /// Wrapper for GetFileVersion for use in path matching /// /// File to check for version /// Full list of input paths /// Version string, null on error public static string GetFileVersion(string firstMatchedString, IEnumerable files) => GetFileVersion(firstMatchedString); /// /// Get the internal name as reported by the filesystem /// /// PortableExecutable representing the file contents /// Internal name string, null on error public static string GetInternalName(PortableExecutable pex) => GetResourceString(pex, "InternalName"); /// /// Get the legal copyright as reported by the filesystem /// /// PortableExecutable representing the file contents /// Legal copyright string, null on error public static string GetLegalCopyright(PortableExecutable pex) => GetResourceString(pex, "LegalCopyright"); /// /// Get the assembly version as determined by an embedded assembly manifest /// /// Byte array representing the file contents /// Version string, null on error public static string GetManifestDescription(PortableExecutable pex) { // If we don't have a PE executable, just return null var resourceSection = pex?.ResourceSection; if (resourceSection == null) return null; // Read in the manifest to a string string manifestString = FindAssemblyManifest(pex.ResourceSection); if (string.IsNullOrWhiteSpace(manifestString)) return null; // Try to read the XML in from the string try { // Try to read the assembly var assemblyNode = GetAssemblyNode(manifestString); if (assemblyNode == null) return null; // Return the content of the description node, if possible var descriptionNode = assemblyNode["description"]; if (descriptionNode == null) return null; return descriptionNode.InnerXml; } catch { return null; } } /// /// Get the assembly version as determined by an embedded assembly manifest /// /// PortableExecutable representing the file contents /// Version string, null on error public static string GetManifestVersion(PortableExecutable pex) { // If we don't have a PE executable, just return null var resourceSection = pex?.ResourceSection; if (resourceSection == null) return null; // Read in the manifest to a string string manifestString = FindAssemblyManifest(pex.ResourceSection); if (string.IsNullOrWhiteSpace(manifestString)) return null; // Try to read the XML in from the string try { // Try to read the assembly var assemblyNode = GetAssemblyNode(manifestString); if (assemblyNode == null) return null; // Try to read the assemblyIdentity var assemblyIdentityNode = assemblyNode["assemblyIdentity"]; if (assemblyIdentityNode == null) return null; // Return the version attribute, if possible return assemblyIdentityNode.GetAttribute("version"); } catch { return null; } } /// /// Get the original filename as reported by the filesystem /// /// PortableExecutable representing the file contents /// Original filename string, null on error public static string GetOriginalFileName(PortableExecutable pex) => GetResourceString(pex, "OriginalFileName"); /// /// Get the product name as reported by the filesystem /// /// PortableExecutable representing the file contents /// Product name string, null on error public static string GetProductName(PortableExecutable pex) => GetResourceString(pex, "ProductName"); /// /// Find resource data in a ResourceSection, if possible /// /// ResourceSection from the executable /// String to use if checking for data starting with a string /// String to use if checking for data contains a string /// String to use if checking for data ending with a string /// Full encoded resource data, null on error public static ResourceDataEntry FindResourceInSection(ResourceSection rs, string dataStart = null, string dataContains = null, string dataEnd = null) { if (rs == null) return null; return FindResourceInTable(rs.ResourceDirectoryTable, dataStart, dataContains, dataEnd); } /// /// Find resource data in a ResourceDirectoryTable, if possible /// /// ResourceDirectoryTable representing a layer /// String to use if checking for data starting with a string /// String to use if checking for data contains a string /// String to use if checking for data ending with a string /// Full encoded resource data, null on error private static ResourceDataEntry FindResourceInTable(ResourceDirectoryTable rdt, string dataStart, string dataContains, string dataEnd) { if (rdt == null) return null; try { foreach (var rdte in rdt.NamedEntries) { if (rdte.IsResourceDataEntry() && rdte.DataEntry != null) { if (dataStart != null && rdte.DataEntry.DataAsUTF8String.StartsWith(dataStart)) return rdte.DataEntry; else if (dataContains != null && rdte.DataEntry.DataAsUTF8String.Contains(dataContains)) return rdte.DataEntry; else if (dataEnd != null && rdte.DataEntry.DataAsUTF8String.EndsWith(dataStart)) return rdte.DataEntry; } else { var manifest = FindResourceInTable(rdte.Subdirectory, dataStart, dataContains, dataEnd); if (manifest != null) return manifest; } } foreach (var rdte in rdt.IdEntries) { if (rdte.IsResourceDataEntry() && rdte.DataEntry != null) { if (dataStart != null && rdte.DataEntry.DataAsUTF8String.StartsWith(dataStart)) return rdte.DataEntry; else if (dataContains != null && rdte.DataEntry.DataAsUTF8String.Contains(dataContains)) return rdte.DataEntry; else if (dataEnd != null && rdte.DataEntry.DataAsUTF8String.EndsWith(dataStart)) return rdte.DataEntry; } else { var manifest = FindResourceInTable(rdte.Subdirectory, dataStart, dataContains, dataEnd); if (manifest != null) return manifest; } } } catch { } return null; } /// /// Find the assembly manifest from a resource section, if possible /// /// ResourceSection from the executable /// Full assembly manifest, null on error private static string FindAssemblyManifest(ResourceSection rs) => FindResourceInSection(rs, dataContains: " /// Get the assembly identity node from an embedded manifest /// /// String representing the XML document /// Assembly identity node, if possible private static XmlElement GetAssemblyNode(string manifestString) { // An invalid string means we can't read it if (string.IsNullOrWhiteSpace(manifestString)) return null; try { // Load the XML string as a document var manifestDoc = new XmlDocument(); manifestDoc.LoadXml(manifestString); // If the XML has no children, it's invalid if (!manifestDoc.HasChildNodes) return null; // Try to read the assembly node return manifestDoc["assembly"]; } catch (Exception ex) { return null; } } /// /// Get the file version info object related to a path, if possible /// /// File to get information for /// FileVersionInfo object on success, null on error private static FileVersionInfo GetFileVersionInfo(string file) { if (file == null || !File.Exists(file)) return null; try { return FileVersionInfo.GetVersionInfo(file); } catch { return null; } } /// /// Get a resource string from the version info /// /// PortableExecutable representing the file contents /// Original filename string, null on error private static string GetResourceString(PortableExecutable pex, string key) { var resourceStrings = GetVersionInfo(pex)?.ChildrenStringFileInfo?.Children?.Children; if (resourceStrings == null) return null; var value = resourceStrings.FirstOrDefault(s => s.Key == key); if (!string.IsNullOrWhiteSpace(value?.Value)) return value.Value.Trim(' ', '\0'); return null; } /// /// Get the version info object related to file contents, if possible /// /// PortableExecutable representing the file contents /// VersionInfo object on success, null on error private static VersionInfo GetVersionInfo(PortableExecutable pex) { // If we don't have a PE executable, just return null var resourceSection = pex?.ResourceSection; if (resourceSection == null) return null; // Try to get the matching resource var resource = FindResourceInSection(resourceSection, dataContains: "V\0S\0_\0V\0E\0R\0S\0I\0O\0N\0_\0I\0N\0F\0O\0"); if (resource?.Data == null) return null; try { int index = 0; return VersionInfo.Deserialize(resource.Data, ref index); } catch (Exception ex) { // Console.WriteLine(ex); return null; } } #endregion } }