From 4a07a5851ca7b2eaab2cdc41ec1e75803f52ccc9 Mon Sep 17 00:00:00 2001 From: Ladislav Zezula Date: Tue, 22 Sep 2020 17:02:57 +0200 Subject: Added support for NP_Protect protector (SC2 Maps) --- src/SBaseCommon.cpp | 1 + src/SBaseFileTable.cpp | 85 +++++++++++++++++++++++++++++++++++++++++++++--- src/SFileOpenArchive.cpp | 82 ++++++++++++++++++++++++++-------------------- src/SFileReadFile.cpp | 3 +- src/StormCommon.h | 13 +++++++- 5 files changed, 140 insertions(+), 44 deletions(-) (limited to 'src') diff --git a/src/SBaseCommon.cpp b/src/SBaseCommon.cpp index fc80dee..a116625 100644 --- a/src/SBaseCommon.cpp +++ b/src/SBaseCommon.cpp @@ -945,6 +945,7 @@ void * LoadMpqTable( // On archives v 1.0, 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(ha->pHeader->wFormatVersion == MPQ_FORMAT_VERSION_1) { // Cut the table size diff --git a/src/SBaseFileTable.cpp b/src/SBaseFileTable.cpp index 7c9a857..c93920c 100644 --- a/src/SBaseFileTable.cpp +++ b/src/SBaseFileTable.cpp @@ -216,6 +216,28 @@ void SetBits( //----------------------------------------------------------------------------- // Support for MPQ header +static bool VerifyTablePosition64( + ULONGLONG MpqOffset, // Position of the MPQ header + ULONGLONG TableOffset, // Position of the MPQ table, relative to MPQ header + ULONGLONG TableSize, // Size of the MPQ table, in bytes + ULONGLONG FileSize) // Size of the entire file, in bytes +{ + // Verify overflows + if((MpqOffset + TableOffset) < MpqOffset) + return false; + if((MpqOffset + TableOffset + TableSize) < MpqOffset) + return false; + + // Verify sizes + if(TableOffset >= FileSize || TableSize >= FileSize) + return false; + if((MpqOffset + TableOffset) >= FileSize) + return false; + if((MpqOffset + TableOffset + TableSize) >= FileSize) + return false; + return true; +} + static ULONGLONG DetermineArchiveSize_V1( TMPQArchive * ha, TMPQHeader * pHeader, @@ -288,6 +310,38 @@ static ULONGLONG DetermineArchiveSize_V2( return (EndOfMpq - MpqOffset); } +static ULONGLONG DetermineArchiveSize_V4( + TMPQHeader * pHeader, + ULONGLONG /* MpqOffset */, + ULONGLONG /* FileSize */) +{ + ULONGLONG ArchiveSize = 0; + ULONGLONG EndOfTable; + + // This could only be called for MPQs version 4 + assert(pHeader->wFormatVersion == MPQ_FORMAT_VERSION_4); + + // Determine the archive size as the greatest of all valid values + EndOfTable = pHeader->BetTablePos64 + pHeader->BetTableSize64; + if(EndOfTable > ArchiveSize) + ArchiveSize = EndOfTable; + + EndOfTable = pHeader->dwHashTablePos + pHeader->dwHashTableSize * sizeof(TMPQHash); + if(EndOfTable > ArchiveSize) + ArchiveSize = EndOfTable; + + EndOfTable = pHeader->dwBlockTablePos + pHeader->dwBlockTableSize * sizeof(TMPQBlock); + if(EndOfTable > ArchiveSize) + ArchiveSize = EndOfTable; + + EndOfTable = pHeader->HetTablePos64 + pHeader->HetTableSize64; + if(EndOfTable > ArchiveSize) + ArchiveSize = EndOfTable; + + // Return the calculated archive size + return ArchiveSize; +} + ULONGLONG FileOffsetFromMpqOffset(TMPQArchive * ha, ULONGLONG MpqOffset) { if(ha->pHeader->wFormatVersion == MPQ_FORMAT_VERSION_1) @@ -339,7 +393,7 @@ int ConvertMpqHeaderToFormat4( ULONGLONG MpqOffset, ULONGLONG FileSize, DWORD dwFlags, - bool bIsWarcraft3Map) + MTYPE MapType) { TMPQHeader * pHeader = (TMPQHeader *)ha->HeaderData; ULONGLONG BlockTablePos64 = 0; @@ -351,8 +405,10 @@ int ConvertMpqHeaderToFormat4( // If version 1.0 is forced, then the format version is forced to be 1.0 // Reason: Storm.dll in Warcraft III ignores format version value - if((dwFlags & MPQ_OPEN_FORCE_MPQ_V1) || bIsWarcraft3Map) + if((dwFlags & MPQ_OPEN_FORCE_MPQ_V1) || (MapType == MapTypeWarcraft3)) wFormatVersion = MPQ_FORMAT_VERSION_1; + if(MapType == MapTypeStarcraft2) + wFormatVersion = MPQ_FORMAT_VERSION_4; // Format-specific fixes switch(wFormatVersion) @@ -558,6 +614,24 @@ int ConvertMpqHeaderToFormat4( // If MD5 doesn't match, we ignore this offset if(!VerifyDataBlockHash(pHeader, MPQ_HEADER_SIZE_V4 - MD5_DIGEST_SIZE, pHeader->MD5_MpqHeader)) return ERROR_FAKE_MPQ_HEADER; + if(!VerifyTablePosition64(MpqOffset, pHeader->HiBlockTablePos64, pHeader->HiBlockTableSize64, FileSize)) + return ERROR_FAKE_MPQ_HEADER; + + // Check for malformed MPQs + if(pHeader->wFormatVersion != MPQ_FORMAT_VERSION_4 || (ha->MpqPos + pHeader->ArchiveSize64) != FileSize || (ha->MpqPos + pHeader->HiBlockTablePos64) >= FileSize) + { + pHeader->wFormatVersion = MPQ_FORMAT_VERSION_4; + pHeader->dwHeaderSize = MPQ_HEADER_SIZE_V4; + ha->dwFlags |= MPQ_FLAG_MALFORMED; + } + + // Recalculate archive size + if(ha->dwFlags & MPQ_FLAG_MALFORMED) + { + // Calculate the archive size + pHeader->ArchiveSize64 = DetermineArchiveSize_V4(pHeader, MpqOffset, FileSize); + pHeader->dwArchiveSize = (DWORD)pHeader->ArchiveSize64; + } // Calculate the block table position BlockTablePos64 = MpqOffset + MAKE_OFFSET64(pHeader->wBlockTablePosHi, pHeader->dwBlockTablePos); @@ -1327,7 +1401,7 @@ static TMPQHetTable * TranslateHetTable(TMPQHetHeader * pHetHeader) if(pHetHeader->ExtHdr.dwDataSize >= (sizeof(TMPQHetHeader) - sizeof(TMPQExtHeader))) { // Verify the size of the table in the header - if(pHetHeader->dwTableSize == pHetHeader->ExtHdr.dwDataSize) + if(pHetHeader->ExtHdr.dwDataSize >= pHetHeader->dwTableSize) { // The size of the HET table must be sum of header, hash and index table size assert((sizeof(TMPQHetHeader) - sizeof(TMPQExtHeader) + pHetHeader->dwTotalCount + pHetHeader->dwIndexTableSize) == pHetHeader->dwTableSize); @@ -1597,10 +1671,11 @@ static TMPQBetTable * TranslateBetTable( if(pBetHeader->ExtHdr.dwDataSize >= (sizeof(TMPQBetHeader) - sizeof(TMPQExtHeader))) { // Verify the size of the table in the header - if(pBetHeader->dwTableSize == pBetHeader->ExtHdr.dwDataSize) + if(pBetHeader->ExtHdr.dwDataSize >= pBetHeader->dwTableSize) { // The number of entries in the BET table must be the same like number of entries in the block table - assert(pBetHeader->dwEntryCount == ha->pHeader->dwBlockTableSize); + // Note: Ignored if there is no block table + //assert(pBetHeader->dwEntryCount == ha->pHeader->dwBlockTableSize); assert(pBetHeader->dwEntryCount <= ha->dwMaxFileCount); // The number of entries in the BET table must be the same like number of entries in the HET table diff --git a/src/SFileOpenArchive.cpp b/src/SFileOpenArchive.cpp index 7755eaf..2020e56 100644 --- a/src/SFileOpenArchive.cpp +++ b/src/SFileOpenArchive.cpp @@ -19,39 +19,53 @@ #define HEADER_SEARCH_BUFFER_SIZE 0x1000 -/*****************************************************************************/ -/* Local functions */ -/*****************************************************************************/ +//----------------------------------------------------------------------------- +// Local functions -static bool IsAviFile(DWORD * HeaderData) +static MTYPE CheckMapType(LPCTSTR szFileName, LPBYTE pbHeaderBuffer, size_t cbHeaderBuffer) { - DWORD DwordValue0 = BSWAP_INT32_UNSIGNED(HeaderData[0]); - DWORD DwordValue2 = BSWAP_INT32_UNSIGNED(HeaderData[2]); - DWORD DwordValue3 = BSWAP_INT32_UNSIGNED(HeaderData[3]); + LPDWORD HeaderInt32 = (LPDWORD)pbHeaderBuffer; + LPCTSTR szExtension; - // Test for 'RIFF', 'AVI ' or 'LIST' - return (DwordValue0 == 0x46464952 && DwordValue2 == 0x20495641 && DwordValue3 == 0x5453494C); -} + // Don't do any checks if there is not at least 16 bytes + if(cbHeaderBuffer > 0x10) + { + DWORD DwordValue0 = BSWAP_INT32_UNSIGNED(HeaderInt32[0]); + DWORD DwordValue1 = BSWAP_INT32_UNSIGNED(HeaderInt32[1]); + DWORD DwordValue2 = BSWAP_INT32_UNSIGNED(HeaderInt32[2]); + DWORD DwordValue3 = BSWAP_INT32_UNSIGNED(HeaderInt32[3]); -static bool IsWarcraft3Map(DWORD * HeaderData) -{ - DWORD DwordValue0 = BSWAP_INT32_UNSIGNED(HeaderData[0]); - DWORD DwordValue1 = BSWAP_INT32_UNSIGNED(HeaderData[1]); + // Test for AVI files (Warcraft III cinematics) - 'RIFF', 'AVI ' or 'LIST' + if(DwordValue0 == 0x46464952 && DwordValue2 == 0x20495641 && DwordValue3 == 0x5453494C) + return MapTypeAviFile; - return (DwordValue0 == 0x57334D48 && DwordValue1 == 0x00000000); -} + // Check for Starcraft II maps + if((szExtension = _tcsrchr(szFileName, _T('.'))) != NULL) + { + // The "NP_Protect" protector places fake Warcraft III header + // into the Starcraft II maps, whilst SC2 maps have no other header but MPQ v4 + if(!_tcsicmp(szExtension, _T(".s2ma")) || !_tcsicmp(szExtension, _T(".SC2Map")) || !_tcsicmp(szExtension, _T(".SC2Mod"))) + { + return MapTypeStarcraft2; + } + } + + // Check for Warcraft III maps + if(DwordValue0 == 0x57334D48 && DwordValue1 == 0x00000000) + return MapTypeWarcraft3; + } -static bool IsDllFile(LPBYTE pbHeaderBuffer, size_t cbBytesAvailable) -{ // MIX files are DLL files that contain MPQ in overlay. - // Only Warcraft III is able to load them, so we consider them MPQs v 1.0 - if(cbBytesAvailable > 0x200 && pbHeaderBuffer[0] == 'M' && pbHeaderBuffer[1] == 'Z') + // Only Warcraft III is able to load them, so we consider them Warcraft III maps + if(cbHeaderBuffer > 0x200 && pbHeaderBuffer[0] == 'M' && pbHeaderBuffer[1] == 'Z') { - DWORD e_lfanew = *(LPDWORD)(pbHeaderBuffer + 0x3C); - if(0 < e_lfanew && e_lfanew < 0x10000) - return true; + // Check the value of IMAGE_DOS_HEADER::e_lfanew at offset 0x3C + if(0 < HeaderInt32[0x0F] && HeaderInt32[0x0F] < 0x10000) + return MapTypeWarcraft3; } - return false; + + // No special map type recognized + return MapTypeNotRecognized; } static TMPQUserData * IsValidMpqUserData(ULONGLONG ByteOffset, ULONGLONG FileSize, void * pvUserData) @@ -171,7 +185,7 @@ bool WINAPI SFileOpenArchive( ULONGLONG FileSize = 0; // Size of the file LPBYTE pbHeaderBuffer = NULL; // Buffer for searching MPQ header DWORD dwStreamFlags = (dwFlags & STREAM_FLAGS_MASK); - bool bIsWarcraft3Map = false; + MTYPE MapType = MapTypeNotRecognized; int nError = ERROR_SUCCESS; // Verify the parameters @@ -264,19 +278,15 @@ bool WINAPI SFileOpenArchive( break; } - // There are AVI files from Warcraft III with 'MPQ' extension. + // Check whether the file is AVI file or a Warcraft III/Starcraft II map if(SearchOffset == 0) { - if(IsAviFile((DWORD *)pbHeaderBuffer)) + // Do nothing if the file is an AVI file + if((MapType = CheckMapType(szMpqName, pbHeaderBuffer, dwBytesAvailable)) == MapTypeAviFile) { nError = ERROR_AVI_FILE; break; } - - if (IsWarcraft3Map((DWORD *)pbHeaderBuffer)) - bIsWarcraft3Map = true; - if (IsDllFile(pbHeaderBuffer, dwBytesAvailable)) - bIsWarcraft3Map = true; } // Search the header buffer @@ -288,7 +298,7 @@ bool WINAPI SFileOpenArchive( // If there is the MPQ user data, process it // Note that Warcraft III does not check for user data, which is abused by many map protectors dwHeaderID = BSWAP_INT32_UNSIGNED(ha->HeaderData[0]); - if(bIsWarcraft3Map == false && (dwFlags & MPQ_OPEN_FORCE_MPQ_V1) == 0) + if(MapType == MapTypeNotRecognized && (dwFlags & MPQ_OPEN_FORCE_MPQ_V1) == 0) { if(ha->pUserData == NULL && dwHeaderID == ID_MPQ_USERDATA) { @@ -316,7 +326,7 @@ bool WINAPI SFileOpenArchive( if(dwHeaderID == ID_MPQ && dwHeaderSize >= MPQ_HEADER_SIZE_V1) { // Now convert the header to version 4 - nError = ConvertMpqHeaderToFormat4(ha, SearchOffset, FileSize, dwFlags, bIsWarcraft3Map); + nError = ConvertMpqHeaderToFormat4(ha, SearchOffset, FileSize, dwFlags, MapType); if(nError != ERROR_FAKE_MPQ_HEADER) { bSearchComplete = true; @@ -325,7 +335,7 @@ bool WINAPI SFileOpenArchive( } // Check for MPK archives (Longwu Online - MPQ fork) - if(bIsWarcraft3Map == false && dwHeaderID == ID_MPK) + if(MapType == MapTypeNotRecognized && dwHeaderID == ID_MPK) { // Now convert the MPK header to MPQ Header version 4 nError = ConvertMpkHeaderToFormat4(ha, FileSize, dwFlags); @@ -399,7 +409,7 @@ bool WINAPI SFileOpenArchive( ha->dwFlags |= MPQ_FLAG_LISTFILE_FORCE; // Remember whether whis is a map for Warcraft III - if(bIsWarcraft3Map) + if(MapType == MapTypeWarcraft3) ha->dwFlags |= MPQ_FLAG_WAR3_MAP; // Set the size of file sector diff --git a/src/SFileReadFile.cpp b/src/SFileReadFile.cpp index a6b3bc4..a693e70 100644 --- a/src/SFileReadFile.cpp +++ b/src/SFileReadFile.cpp @@ -344,11 +344,10 @@ static int ReadMpqFileSingleUnit(TMPQFile * hf, void * pvBuffer, DWORD dwFilePos // Give the number of bytes read *pdwBytesRead = dwToRead; - return ERROR_SUCCESS; } // An error, sorry - return ERROR_CAN_NOT_COMPLETE; + return nError; } static int ReadMpkFileSingleUnit(TMPQFile * hf, void * pvBuffer, DWORD dwFilePos, DWORD dwToRead, LPDWORD pdwBytesRead) diff --git a/src/StormCommon.h b/src/StormCommon.h index 95b3320..2d51d92 100644 --- a/src/StormCommon.h +++ b/src/StormCommon.h @@ -70,6 +70,17 @@ // Macro for building 64-bit file offset from two 32-bit #define MAKE_OFFSET64(hi, lo) (((ULONGLONG)hi << 32) | (ULONGLONG)lo) +//----------------------------------------------------------------------------- +// MTYPE definition - specifies what kind of MPQ is the map type + +typedef enum _MTYPE +{ + MapTypeNotRecognized, // The file does not seems to be a map + MapTypeAviFile, // The file is actually an AVI file (Warcraft III cinematics) + MapTypeWarcraft3, // The file is a Warcraft III map + MapTypeStarcraft2 // The file is a Starcraft II map +} MTYPE, *PMTYPE; + //----------------------------------------------------------------------------- // MPQ signature information @@ -227,7 +238,7 @@ TMPQFile * IsValidFileHandle(HANDLE hFile); ULONGLONG FileOffsetFromMpqOffset(TMPQArchive * ha, ULONGLONG MpqOffset); ULONGLONG CalculateRawSectorOffset(TMPQFile * hf, DWORD dwSectorOffset); -int ConvertMpqHeaderToFormat4(TMPQArchive * ha, ULONGLONG MpqOffset, ULONGLONG FileSize, DWORD dwFlags, bool bIsWarcraft3Map); +int ConvertMpqHeaderToFormat4(TMPQArchive * ha, ULONGLONG MpqOffset, ULONGLONG FileSize, DWORD dwFlags, MTYPE MapType); bool IsValidHashEntry(TMPQArchive * ha, TMPQHash * pHash); -- cgit v1.2.3