diff --git a/SabreTools.IO.Test/SabreTools.IO.Test.csproj b/SabreTools.IO.Test/SabreTools.IO.Test.csproj index 3fb950c..e4e6717 100644 --- a/SabreTools.IO.Test/SabreTools.IO.Test.csproj +++ b/SabreTools.IO.Test/SabreTools.IO.Test.csproj @@ -26,7 +26,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/SabreTools.IO/Compression/Deflate/InflateWrapper.cs b/SabreTools.IO/Compression/Deflate/InflateWrapper.cs index b3ade8a..8c049d6 100644 --- a/SabreTools.IO/Compression/Deflate/InflateWrapper.cs +++ b/SabreTools.IO/Compression/Deflate/InflateWrapper.cs @@ -301,13 +301,13 @@ namespace SabreTools.IO.Compression.Deflate header.FileName = Encoding.ASCII.GetString(filenameBytes); } + + // Parsing extras is skipped here, unlike in Serialization if (header.ExtraFieldLength > 0 && data.Position + header.ExtraFieldLength <= data.Length) { byte[] extraBytes = data.ReadBytes(header.ExtraFieldLength); if (extraBytes.Length != header.ExtraFieldLength) return null; - - header.ExtraField = extraBytes; } return header; diff --git a/SabreTools.IO/Encryption/MoPaQDecrypter.cs b/SabreTools.IO/Encryption/MoPaQDecrypter.cs new file mode 100644 index 0000000..6c411d0 --- /dev/null +++ b/SabreTools.IO/Encryption/MoPaQDecrypter.cs @@ -0,0 +1,172 @@ +using System; +using System.IO; +using SabreTools.Hashing; +using SabreTools.Matching; +using static SabreTools.Models.MoPaQ.Constants; + +namespace SabreTools.IO.Encryption +{ + /// + /// Handler for decrypting MoPaQ block and table data + /// + public class MoPaQDecrypter + { + #region Private Instance Variables + + /// + /// Buffer for encryption and decryption + /// + private readonly uint[] _stormBuffer = new uint[STORM_BUFFER_SIZE]; + + #endregion + + public MoPaQDecrypter() + { + PrepareCryptTable(); + } + + /// + /// Prepare the encryption table + /// + private void PrepareCryptTable() + { + uint seed = 0x00100001; + for (uint index1 = 0; index1 < 0x100; index1++) + { + for (uint index2 = index1, i = 0; i < 5; i++, index2 += 0x100) + { + seed = (seed * 125 + 3) % 0x2AAAAB; + uint temp1 = (seed & 0xFFFF) << 0x10; + + seed = (seed * 125 + 3) % 0x2AAAAB; + uint temp2 = (seed & 0xFFFF); + + _stormBuffer[index2] = (temp1 | temp2); + } + } + } + + /// + /// Load a table block by optionally decompressing and + /// decrypting before returning the data. + /// + /// Stream to parse + /// Data offset to parse + /// Optional MD5 hash for validation + /// Size of the table in the file + /// Expected size of the table + /// Encryption key to use + /// Output represening the real table size + /// Byte array representing the processed table + public byte[]? LoadTable(Stream data, + long offset, + byte[]? expectedHash, + uint compressedSize, + uint tableSize, + uint key, + out long realTableSize) + { + byte[]? tableData; + byte[]? readBytes; + long bytesToRead = tableSize; + + // Allocate the MPQ table + tableData = readBytes = new byte[tableSize]; + + // Check if the MPQ table is compressed + if (compressedSize != 0 && compressedSize < tableSize) + { + // Allocate temporary buffer for holding compressed data + readBytes = new byte[compressedSize]; + bytesToRead = compressedSize; + } + + // Get the file offset from which we will read the table + // Note: According to Storm.dll from Warcraft III (version 2002), + // if the hash table position is 0xFFFFFFFF, no SetFilePointer call is done + // and the table is loaded from the current file offset + if (offset == 0xFFFFFFFF) + offset = data.Position; + + // Is the sector table within the file? + if (offset >= data.Length) + { + realTableSize = 0; + return null; + } + + // The hash table and block table can go beyond EOF. + // Storm.dll reads as much as possible, then fills the missing part with zeros. + // Abused by Spazzler map protector which sets hash table size to 0x00100000 + // Abused by NP_Protect in MPQs v4 as well + if ((offset + bytesToRead) > data.Length) + bytesToRead = (uint)(data.Length - offset); + + // Give the caller information that the table was cut + realTableSize = bytesToRead; + + // If everything succeeded, read the raw table from the MPQ + data.Seek(offset, SeekOrigin.Begin); + _ = data.Read(readBytes, 0, (int)bytesToRead); + + // Verify the MD5 of the table, if present + byte[]? actualHash = HashTool.GetByteArrayHashArray(readBytes, HashType.MD5); + if (expectedHash != null && actualHash != null && !actualHash.EqualsExactly(expectedHash)) + { + Console.WriteLine("Table is corrupt!"); + return null; + } + + // First of all, decrypt the table + if (key != 0) + tableData = DecryptBlock(readBytes, bytesToRead, key); + + // If the table is compressed, decompress it + if (compressedSize != 0 && compressedSize < tableSize) + { + Console.WriteLine("Table is compressed, it will not read properly!"); + return null; + + // TODO: Handle decompression + // int cbOutBuffer = (int)tableSize; + // int cbInBuffer = (int)compressedSize; + + // if (!SCompDecompress2(readBytes, &cbOutBuffer, tableData, cbInBuffer)) + // errorCode = SErrGetLastError(); + + // tableData = readBytes; + } + + // Return the MPQ table + return tableData; + } + + /// + /// Decrypt a single block of data + /// + public unsafe byte[] DecryptBlock(byte[] block, long length, uint key) + { + uint seed = 0xEEEEEEEE; + + uint[] castBlock = new uint[length >> 2]; + Buffer.BlockCopy(block, 0, castBlock, 0, (int)length); + int castBlockPtr = 0; + + // Round to uints + length >>= 2; + + while (length-- > 0) + { + seed += _stormBuffer[MPQ_HASH_KEY2_MIX + (key & 0xFF)]; + uint ch = castBlock[castBlockPtr] ^ (key + seed); + + key = ((~key << 0x15) + 0x11111111) | (key >> 0x0B); + seed = ch + seed + (seed << 5) + 3; + castBlock[castBlockPtr++] = ch; + } + + Buffer.BlockCopy(castBlock, 0, block, 0, block.Length >> 2); + return block; + } + } +} diff --git a/SabreTools.IO/SabreTools.IO.csproj b/SabreTools.IO/SabreTools.IO.csproj index 268c2ba..5824eec 100644 --- a/SabreTools.IO/SabreTools.IO.csproj +++ b/SabreTools.IO/SabreTools.IO.csproj @@ -30,7 +30,7 @@ - + diff --git a/SabreTools.IO/Streams/ViewStream.cs b/SabreTools.IO/Streams/ViewStream.cs index 2ae9469..f792d9a 100644 --- a/SabreTools.IO/Streams/ViewStream.cs +++ b/SabreTools.IO/Streams/ViewStream.cs @@ -135,7 +135,7 @@ namespace SabreTools.IO.Streams _source.Seek(_initialPosition, SeekOrigin.Begin); } - /// + /// /// Construct a new ViewStream from a byte array /// public ViewStream(byte[] data, long offset) @@ -170,6 +170,26 @@ namespace SabreTools.IO.Streams #endregion + #region Data + + /// + /// Check if a data segment is valid in the data source + /// + /// Position in the source + /// Length of the data to check + /// True if the positional data is valid, false otherwise + public bool SegmentValid(long offset, long count) + { + if (offset < 0 || offset > Length) + return false; + if (count < 0 || offset + count > Length) + return false; + + return true; + } + + #endregion + #region Stream Implementations /// @@ -231,4 +251,4 @@ namespace SabreTools.IO.Streams #endregion } -} \ No newline at end of file +}