using System.Collections.Generic; using System.IO; using System.Text; using BinaryObjectScanner.Models.InstallShieldCabinet; using BinaryObjectScanner.Utilities; using static BinaryObjectScanner.Models.InstallShieldCabinet.Constants; namespace BinaryObjectScanner.Builders { // TODO: Add multi-cabinet reading public class InstallShieldCabinet { #region Byte Data /// /// Parse a byte array into a InstallShield Cabinet file /// /// Byte array to parse /// Offset into the byte array /// Filled cabinet on success, null on error public static Cabinet ParseCabinet(byte[] data, int offset) { // If the data is invalid if (data == null) return null; // If the offset is out of bounds if (offset < 0 || offset >= data.Length) return null; // Create a memory stream and parse that MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); return ParseCabinet(dataStream); } #endregion #region Stream Data /// /// Parse a Stream into a InstallShield Cabinet file /// /// Stream to parse /// Filled cabinet on success, null on error public static Cabinet ParseCabinet(Stream data) { // If the data is invalid if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) return null; // If the offset is out of bounds if (data.Position < 0 || data.Position >= data.Length) return null; // Cache the current offset int initialOffset = (int)data.Position; // Create a new cabinet to fill var cabinet = new Cabinet(); #region Common Header // Try to parse the cabinet header var commonHeader = ParseCommonHeader(data); if (commonHeader == null) return null; // Set the cabinet header cabinet.CommonHeader = commonHeader; #endregion #region Volume Header // Try to parse the volume header var volumeHeader = ParseVolumeHeader(data, GetMajorVersion(commonHeader)); if (volumeHeader == null) return null; // Set the volume header cabinet.VolumeHeader = volumeHeader; #endregion #region Descriptor // Get the descriptor offset uint descriptorOffset = commonHeader.DescriptorOffset; if (descriptorOffset < 0 || descriptorOffset >= data.Length) return null; // Seek to the descriptor data.Seek(descriptorOffset, SeekOrigin.Begin); // Try to parse the descriptor var descriptor = ParseDescriptor(data); if (descriptor == null) return null; // Set the descriptor cabinet.Descriptor = descriptor; #endregion #region File Descriptor Offsets // Get the file table offset uint fileTableOffset = commonHeader.DescriptorOffset + descriptor.FileTableOffset; if (fileTableOffset < 0 || fileTableOffset >= data.Length) return null; // Seek to the file table data.Seek(fileTableOffset, SeekOrigin.Begin); // Get the number of file table items uint fileTableItems; if (GetMajorVersion(commonHeader) <= 5) fileTableItems = descriptor.DirectoryCount + descriptor.FileCount; else fileTableItems = descriptor.DirectoryCount; // Create and fill the file table cabinet.FileDescriptorOffsets = new uint[fileTableItems]; for (int i = 0; i < cabinet.FileDescriptorOffsets.Length; i++) { cabinet.FileDescriptorOffsets[i] = data.ReadUInt32(); } #endregion #region Directory Descriptors // Create and fill the directory descriptors cabinet.DirectoryNames = new string[descriptor.DirectoryCount]; for (int i = 0; i < descriptor.DirectoryCount; i++) { // Get the directory descriptor offset uint offset = descriptorOffset + descriptor.FileTableOffset + cabinet.FileDescriptorOffsets[i]; // If we have an invalid offset if (offset < 0 || offset >= data.Length) continue; // Seek to the file descriptor offset data.Seek(offset, SeekOrigin.Begin); // Create and add the file descriptor string directoryName = ParseDirectoryName(data, GetMajorVersion(commonHeader)); cabinet.DirectoryNames[i] = directoryName; } #endregion #region File Descriptors // Create and fill the file descriptors cabinet.FileDescriptors = new FileDescriptor[descriptor.FileCount]; for (int i = 0; i < descriptor.FileCount; i++) { // Get the file descriptor offset uint offset; if (GetMajorVersion(commonHeader) <= 5) { offset = descriptorOffset + descriptor.FileTableOffset + cabinet.FileDescriptorOffsets[descriptor.DirectoryCount + i]; } else { offset = descriptorOffset + descriptor.FileTableOffset + descriptor.FileTableOffset2 + (uint)(i * 0x57); } // If we have an invalid offset if (offset < 0 || offset >= data.Length) continue; // Seek to the file descriptor offset data.Seek(offset, SeekOrigin.Begin); // Create and add the file descriptor FileDescriptor fileDescriptor = ParseFileDescriptor(data, GetMajorVersion(commonHeader), descriptorOffset + descriptor.FileTableOffset); cabinet.FileDescriptors[i] = fileDescriptor; } #endregion #region File Group Offsets // Create and fill the file group offsets cabinet.FileGroupOffsets = new Dictionary(); for (int i = 0; i < descriptor.FileGroupOffsets.Length; i++) { // Get the file group offset uint offset = descriptor.FileGroupOffsets[i]; if (offset == 0) continue; // Adjust the file group offset offset += commonHeader.DescriptorOffset; if (offset < 0 || offset >= data.Length) continue; // Seek to the file group offset data.Seek(offset, SeekOrigin.Begin); // Create and add the offset OffsetList offsetList = ParseOffsetList(data, GetMajorVersion(commonHeader), descriptorOffset); cabinet.FileGroupOffsets[descriptor.FileGroupOffsets[i]] = offsetList; // If we have a nonzero next offset uint nextOffset = offsetList.NextOffset; while (nextOffset != 0) { // Get the next offset to read uint internalOffset = nextOffset + commonHeader.DescriptorOffset; // Seek to the file group offset data.Seek(internalOffset, SeekOrigin.Begin); // Create and add the offset offsetList = ParseOffsetList(data, GetMajorVersion(commonHeader), descriptorOffset); cabinet.FileGroupOffsets[nextOffset] = offsetList; // Set the next offset nextOffset = offsetList.NextOffset; } } #endregion #region File Groups // Create the file groups array cabinet.FileGroups = new FileGroup[cabinet.FileGroupOffsets.Count]; // Create and fill the file groups int fileGroupId = 0; foreach (var kvp in cabinet.FileGroupOffsets) { // Get the offset OffsetList list = kvp.Value; if (list == null) { fileGroupId++; continue; } // If we have an invalid offset if (list.DescriptorOffset <= 0) { fileGroupId++; continue; } /// Seek to the file group data.Seek(list.DescriptorOffset + descriptorOffset, SeekOrigin.Begin); // Try to parse the file group var fileGroup = ParseFileGroup(data, GetMajorVersion(commonHeader), descriptorOffset); if (fileGroup == null) return null; // Add the file group cabinet.FileGroups[fileGroupId++] = fileGroup; } #endregion #region Component Offsets // Create and fill the component offsets cabinet.ComponentOffsets = new Dictionary(); for (int i = 0; i < descriptor.ComponentOffsets.Length; i++) { // Get the component offset uint offset = descriptor.ComponentOffsets[i]; if (offset == 0) continue; // Adjust the component offset offset += commonHeader.DescriptorOffset; if (offset < 0 || offset >= data.Length) continue; // Seek to the component offset data.Seek(offset, SeekOrigin.Begin); // Create and add the offset OffsetList offsetList = ParseOffsetList(data, GetMajorVersion(commonHeader), descriptorOffset); cabinet.ComponentOffsets[descriptor.ComponentOffsets[i]] = offsetList; // If we have a nonzero next offset uint nextOffset = offsetList.NextOffset; while (nextOffset != 0) { // Get the next offset to read uint internalOffset = nextOffset + commonHeader.DescriptorOffset; // Seek to the file group offset data.Seek(internalOffset, SeekOrigin.Begin); // Create and add the offset offsetList = ParseOffsetList(data, GetMajorVersion(commonHeader), descriptorOffset); cabinet.ComponentOffsets[nextOffset] = offsetList; // Set the next offset nextOffset = offsetList.NextOffset; } } #endregion #region Components // Create the components array cabinet.Components = new Component[cabinet.ComponentOffsets.Count]; // Create and fill the components int componentId = 0; foreach (KeyValuePair kvp in cabinet.ComponentOffsets) { // Get the offset OffsetList list = kvp.Value; if (list == null) { componentId++; continue; } // If we have an invalid offset if (list.DescriptorOffset <= 0) { componentId++; continue; } // Seek to the component data.Seek(list.DescriptorOffset + descriptorOffset, SeekOrigin.Begin); // Try to parse the component var component = ParseComponent(data, GetMajorVersion(commonHeader), descriptorOffset); if (component == null) return null; // Add the component cabinet.Components[componentId++] = component; } #endregion // TODO: Parse setup types return cabinet; } /// /// Parse a Stream into a common header /// /// Stream to parse /// Filled common header on success, null on error private static CommonHeader ParseCommonHeader(Stream data) { CommonHeader commonHeader = new CommonHeader(); byte[] signature = data.ReadBytes(4); commonHeader.Signature = Encoding.ASCII.GetString(signature); if (commonHeader.Signature != SignatureString) return null; commonHeader.Version = data.ReadUInt32(); commonHeader.VolumeInfo = data.ReadUInt32(); commonHeader.DescriptorOffset = data.ReadUInt32(); commonHeader.DescriptorSize = data.ReadUInt32(); return commonHeader; } /// /// Parse a Stream into a volume header /// /// Stream to parse /// Major version of the cabinet /// Filled volume header on success, null on error private static VolumeHeader ParseVolumeHeader(Stream data, int majorVersion) { VolumeHeader volumeHeader = new VolumeHeader(); // Read the descriptor based on version if (majorVersion <= 5) { volumeHeader.DataOffset = data.ReadUInt32(); _ = data.ReadBytes(0x04); // Skip 0x04 bytes, unknown data? volumeHeader.FirstFileIndex = data.ReadUInt32(); volumeHeader.LastFileIndex = data.ReadUInt32(); volumeHeader.FirstFileOffset = data.ReadUInt32(); volumeHeader.FirstFileSizeExpanded = data.ReadUInt32(); volumeHeader.FirstFileSizeCompressed = data.ReadUInt32(); volumeHeader.LastFileOffset = data.ReadUInt32(); volumeHeader.LastFileSizeExpanded = data.ReadUInt32(); volumeHeader.LastFileSizeCompressed = data.ReadUInt32(); } else { // TODO: Should standard and high values be combined? volumeHeader.DataOffset = data.ReadUInt32(); volumeHeader.DataOffsetHigh = data.ReadUInt32(); volumeHeader.FirstFileIndex = data.ReadUInt32(); volumeHeader.LastFileIndex = data.ReadUInt32(); volumeHeader.FirstFileOffset = data.ReadUInt32(); volumeHeader.FirstFileOffsetHigh = data.ReadUInt32(); volumeHeader.FirstFileSizeExpanded = data.ReadUInt32(); volumeHeader.FirstFileSizeExpandedHigh = data.ReadUInt32(); volumeHeader.FirstFileSizeCompressed = data.ReadUInt32(); volumeHeader.FirstFileSizeCompressedHigh = data.ReadUInt32(); volumeHeader.LastFileOffset = data.ReadUInt32(); volumeHeader.LastFileOffsetHigh = data.ReadUInt32(); volumeHeader.LastFileSizeExpanded = data.ReadUInt32(); volumeHeader.LastFileSizeExpandedHigh = data.ReadUInt32(); volumeHeader.LastFileSizeCompressed = data.ReadUInt32(); volumeHeader.LastFileSizeCompressedHigh = data.ReadUInt32(); } return volumeHeader; } /// /// Parse a Stream into a descriptor /// /// Stream to parse /// Filled descriptor on success, null on error private static Descriptor ParseDescriptor(Stream data) { Descriptor descriptor = new Descriptor(); descriptor.StringsOffset = data.ReadUInt32(); descriptor.Reserved0 = data.ReadBytes(4); descriptor.ComponentListOffset = data.ReadUInt32(); descriptor.FileTableOffset = data.ReadUInt32(); descriptor.Reserved1 = data.ReadBytes(4); descriptor.FileTableSize = data.ReadUInt32(); descriptor.FileTableSize2 = data.ReadUInt32(); descriptor.DirectoryCount = data.ReadUInt16(); descriptor.Reserved2 = data.ReadBytes(4); descriptor.Reserved3 = data.ReadBytes(2); descriptor.Reserved4 = data.ReadBytes(4); descriptor.FileCount = data.ReadUInt32(); descriptor.FileTableOffset2 = data.ReadUInt32(); descriptor.ComponentTableInfoCount = data.ReadUInt16(); descriptor.ComponentTableOffset = data.ReadUInt32(); descriptor.Reserved5 = data.ReadBytes(4); descriptor.Reserved6 = data.ReadBytes(4); descriptor.FileGroupOffsets = new uint[MAX_FILE_GROUP_COUNT]; for (int i = 0; i < descriptor.FileGroupOffsets.Length; i++) { descriptor.FileGroupOffsets[i] = data.ReadUInt32(); } descriptor.ComponentOffsets = new uint[MAX_COMPONENT_COUNT]; for (int i = 0; i < descriptor.ComponentOffsets.Length; i++) { descriptor.ComponentOffsets[i] = data.ReadUInt32(); } descriptor.SetupTypesOffset = data.ReadUInt32(); descriptor.SetupTableOffset = data.ReadUInt32(); descriptor.Reserved7 = data.ReadBytes(4); descriptor.Reserved8 = data.ReadBytes(4); return descriptor; } /// /// Parse a Stream into an offset list /// /// Stream to parse /// Major version of the cabinet /// Offset of the cabinet descriptor /// Filled offset list on success, null on error private static OffsetList ParseOffsetList(Stream data, int majorVersion, uint descriptorOffset) { OffsetList offsetList = new OffsetList(); offsetList.NameOffset = data.ReadUInt32(); offsetList.DescriptorOffset = data.ReadUInt32(); offsetList.NextOffset = data.ReadUInt32(); // Cache the current offset long currentOffset = data.Position; // Seek to the name offset data.Seek(offsetList.NameOffset + descriptorOffset, SeekOrigin.Begin); // Read the string if (majorVersion >= 17) offsetList.Name = data.ReadString(Encoding.Unicode); else offsetList.Name = data.ReadString(Encoding.ASCII); // Seek back to the correct offset data.Seek(currentOffset, SeekOrigin.Begin); return offsetList; } /// /// Parse a Stream into a file group /// /// Stream to parse /// Major version of the cabinet /// Offset of the cabinet descriptor /// Filled file group on success, null on error private static FileGroup ParseFileGroup(Stream data, int majorVersion, uint descriptorOffset) { FileGroup fileGroup = new FileGroup(); fileGroup.NameOffset = data.ReadUInt32(); fileGroup.ExpandedSize = data.ReadUInt32(); fileGroup.Reserved0 = data.ReadBytes(4); fileGroup.CompressedSize = data.ReadUInt32(); fileGroup.Reserved1 = data.ReadBytes(4); fileGroup.Reserved2 = data.ReadBytes(2); fileGroup.Attribute1 = data.ReadUInt16(); fileGroup.Attribute2 = data.ReadUInt16(); // TODO: Figure out what data lives in this area for V5 and below if (majorVersion <= 5) data.Seek(0x36, SeekOrigin.Current); fileGroup.FirstFile = data.ReadUInt32(); fileGroup.LastFile = data.ReadUInt32(); fileGroup.UnknownOffset = data.ReadUInt32(); fileGroup.Var4Offset = data.ReadUInt32(); fileGroup.Var1Offset = data.ReadUInt32(); fileGroup.HTTPLocationOffset = data.ReadUInt32(); fileGroup.FTPLocationOffset = data.ReadUInt32(); fileGroup.MiscOffset = data.ReadUInt32(); fileGroup.Var2Offset = data.ReadUInt32(); fileGroup.TargetDirectoryOffset = data.ReadUInt32(); fileGroup.Reserved3 = data.ReadBytes(2); fileGroup.Reserved4 = data.ReadBytes(2); fileGroup.Reserved5 = data.ReadBytes(2); fileGroup.Reserved6 = data.ReadBytes(2); fileGroup.Reserved7 = data.ReadBytes(2); // Cache the current position long currentPosition = data.Position; // Read the name, if possible if (fileGroup.NameOffset != 0) { // Seek to the name data.Seek(fileGroup.NameOffset + descriptorOffset, SeekOrigin.Begin); // Read the string if (majorVersion >= 17) fileGroup.Name = data.ReadString(Encoding.Unicode); else fileGroup.Name = data.ReadString(Encoding.ASCII); } // Seek back to the correct offset data.Seek(currentPosition, SeekOrigin.Begin); return fileGroup; } /// /// Parse a Stream into a component /// /// Stream to parse /// Major version of the cabinet /// Offset of the cabinet descriptor /// Filled component on success, null on error private static Component ParseComponent(Stream data, int majorVersion, uint descriptorOffset) { Component component = new Component(); component.IdentifierOffset = data.ReadUInt32(); component.DescriptorOffset = data.ReadUInt32(); component.DisplayNameOffset = data.ReadUInt32(); component.Reserved0 = data.ReadBytes(2); component.ReservedOffset0 = data.ReadUInt32(); component.ReservedOffset1 = data.ReadUInt32(); component.ComponentIndex = data.ReadUInt16(); component.NameOffset = data.ReadUInt32(); component.ReservedOffset2 = data.ReadUInt32(); component.ReservedOffset3 = data.ReadUInt32(); component.ReservedOffset4 = data.ReadUInt32(); component.Reserved1 = data.ReadBytes(32); component.CLSIDOffset = data.ReadUInt32(); component.Reserved2 = data.ReadBytes(28); component.Reserved3 = data.ReadBytes(majorVersion <= 5 ? 2 : 1); component.DependsCount = data.ReadUInt16(); component.DependsOffset = data.ReadUInt32(); component.FileGroupCount = data.ReadUInt16(); component.FileGroupNamesOffset = data.ReadUInt32(); component.X3Count = data.ReadUInt16(); component.X3Offset = data.ReadUInt32(); component.SubComponentsCount = data.ReadUInt16(); component.SubComponentsOffset = data.ReadUInt32(); component.NextComponentOffset = data.ReadUInt32(); component.ReservedOffset5 = data.ReadUInt32(); component.ReservedOffset6 = data.ReadUInt32(); component.ReservedOffset7 = data.ReadUInt32(); component.ReservedOffset8 = data.ReadUInt32(); // Cache the current position long currentPosition = data.Position; // Read the identifier, if possible if (component.IdentifierOffset != 0) { // Seek to the identifier data.Seek(component.IdentifierOffset + descriptorOffset, SeekOrigin.Begin); // Read the string if (majorVersion >= 17) component.Identifier = data.ReadString(Encoding.Unicode); else component.Identifier = data.ReadString(Encoding.ASCII); } // Read the display name, if possible if (component.DisplayNameOffset != 0) { // Seek to the name data.Seek(component.DisplayNameOffset + descriptorOffset, SeekOrigin.Begin); // Read the string if (majorVersion >= 17) component.DisplayName = data.ReadString(Encoding.Unicode); else component.DisplayName = data.ReadString(Encoding.ASCII); } // Read the name, if possible if (component.NameOffset != 0) { // Seek to the name data.Seek(component.NameOffset + descriptorOffset, SeekOrigin.Begin); // Read the string if (majorVersion >= 17) component.Name = data.ReadString(Encoding.Unicode); else component.Name = data.ReadString(Encoding.ASCII); } // Read the CLSID, if possible if (component.CLSIDOffset != 0) { // Seek to the CLSID data.Seek(component.CLSIDOffset + descriptorOffset, SeekOrigin.Begin); // Read the GUID component.CLSID = data.ReadGuid(); } // Read the file group names, if possible if (component.FileGroupCount != 0 && component.FileGroupNamesOffset != 0) { // Seek to the file group table offset data.Seek(component.FileGroupNamesOffset + descriptorOffset, SeekOrigin.Begin); // Read the file group names table component.FileGroupNames = new string[component.FileGroupCount]; for (int j = 0; j < component.FileGroupCount; j++) { // Get the name offset uint nameOffset = data.ReadUInt32(); // Cache the current offset long preNameOffset = data.Position; // Seek to the name offset data.Seek(nameOffset + descriptorOffset, SeekOrigin.Begin); if (majorVersion >= 17) component.FileGroupNames[j] = data.ReadString(Encoding.Unicode); else component.FileGroupNames[j] = data.ReadString(Encoding.ASCII); // Seek back to the original position data.Seek(preNameOffset, SeekOrigin.Begin); } } // Seek back to the correct offset data.Seek(currentPosition, SeekOrigin.Begin); return component; } /// /// Parse a Stream into a directory name /// /// Stream to parse /// Major version of the cabinet /// Filled directory name on success, null on error private static string ParseDirectoryName(Stream data, int majorVersion) { // Read the string if (majorVersion >= 17) return data.ReadString(Encoding.Unicode); else return data.ReadString(Encoding.ASCII); } /// /// Parse a Stream into a file descriptor /// /// Stream to parse /// Major version of the cabinet /// Offset of the cabinet descriptor /// Filled file descriptor on success, null on error private static FileDescriptor ParseFileDescriptor(Stream data, int majorVersion, uint descriptorOffset) { FileDescriptor fileDescriptor = new FileDescriptor(); // Read the descriptor based on version if (majorVersion <= 5) { fileDescriptor.Volume = 0xFFFF; // Set by the header index fileDescriptor.NameOffset = data.ReadUInt32(); fileDescriptor.DirectoryIndex = data.ReadUInt32(); fileDescriptor.Flags = (FileFlags)data.ReadUInt16(); fileDescriptor.ExpandedSize = data.ReadUInt32(); fileDescriptor.CompressedSize = data.ReadUInt32(); _ = data.ReadBytes(0x14); // Skip 0x14 bytes, unknown data? fileDescriptor.DataOffset = data.ReadUInt32(); if (majorVersion == 5) fileDescriptor.MD5 = data.ReadBytes(0x10); } else { fileDescriptor.Flags = (FileFlags)data.ReadUInt16(); fileDescriptor.ExpandedSize = data.ReadUInt64(); fileDescriptor.CompressedSize = data.ReadUInt64(); fileDescriptor.DataOffset = data.ReadUInt64(); fileDescriptor.MD5 = data.ReadBytes(0x10); _ = data.ReadBytes(0x10); // Skip 0x10 bytes, unknown data? fileDescriptor.NameOffset = data.ReadUInt32(); fileDescriptor.DirectoryIndex = data.ReadUInt16(); _ = data.ReadBytes(0x0C); // Skip 0x0C bytes, unknown data? fileDescriptor.LinkPrevious = data.ReadUInt32(); fileDescriptor.LinkNext = data.ReadUInt32(); fileDescriptor.LinkFlags = (LinkFlags)data.ReadByteValue(); fileDescriptor.Volume = data.ReadUInt16(); } // Cache the current position long currentPosition = data.Position; // Read the name, if possible if (fileDescriptor.NameOffset != 0) { // Seek to the name data.Seek(fileDescriptor.NameOffset + descriptorOffset, SeekOrigin.Begin); // Read the string if (majorVersion >= 17) fileDescriptor.Name = data.ReadString(Encoding.Unicode); else fileDescriptor.Name = data.ReadString(Encoding.ASCII); } // Seek back to the correct offset data.Seek(currentPosition, SeekOrigin.Begin); return fileDescriptor; } #endregion #region Helpers /// /// Get the major version of the cabinet /// /// This should live in the wrapper but is needed during parsing private static int GetMajorVersion(CommonHeader commonHeader) { uint majorVersion = commonHeader.Version; if (majorVersion >> 24 == 1) { majorVersion = (majorVersion >> 12) & 0x0F; } else if (majorVersion >> 24 == 2 || majorVersion >> 24 == 4) { majorVersion = majorVersion & 0xFFFF; if (majorVersion != 0) majorVersion /= 100; } return (int)majorVersion; } #endregion } }