diff --git a/BinaryObjectScanner/Data/StaticChecks.cs b/BinaryObjectScanner/Data/StaticChecks.cs index 2368ef25..406119db 100644 --- a/BinaryObjectScanner/Data/StaticChecks.cs +++ b/BinaryObjectScanner/Data/StaticChecks.cs @@ -23,7 +23,7 @@ namespace BinaryObjectScanner.Data } /// - /// Cache for all IISOCheck types + /// Cache for all IDiskImageCheck types /// public static IDiskImageCheck[] ISO9660CheckClasses { @@ -33,7 +33,7 @@ namespace BinaryObjectScanner.Data return iso9660CheckClasses; } } - + /// /// Cache for all IExecutableCheck types /// @@ -103,12 +103,11 @@ namespace BinaryObjectScanner.Data /// private static IContentCheck[]? contentCheckClasses; - /// /// Cache for all IISOCheck types /// private static IDiskImageCheck[]? iso9660CheckClasses; - + /// /// Cache for all IExecutableCheck types /// @@ -169,7 +168,7 @@ namespace BinaryObjectScanner.Data if (assemblyTypes.Length == 0) return []; - // Loop through all types + // Loop through all types List classTypes = []; foreach (Type? type in assemblyTypes) { diff --git a/BinaryObjectScanner/FileType/DiskImage.cs b/BinaryObjectScanner/FileType/DiskImage.cs index 9f701b4a..a03b3988 100644 --- a/BinaryObjectScanner/FileType/DiskImage.cs +++ b/BinaryObjectScanner/FileType/DiskImage.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.IO; using BinaryObjectScanner.Data; using BinaryObjectScanner.Interfaces; using SabreTools.IO.Extensions; @@ -20,13 +18,13 @@ namespace BinaryObjectScanner.FileType #region Check Runners /// - /// Handle a single file based on all ISO check implementations + /// Handle a single file based on all disk image check implementations /// - /// Name of the source file of the ISO, for tracking + /// Name of the source file of the disk image, for tracking /// Set of checks to use /// True to include debug data, false otherwise /// Set of protections in file, empty on error - protected IDictionary RunISOChecks(string file, U[] checks, bool includeDebug) + protected IDictionary RunDiskImageChecks(string file, U[] checks, bool includeDebug) where U : IDiskImageCheck { // Create the output dictionary diff --git a/BinaryObjectScanner/FileType/ISO9660.cs b/BinaryObjectScanner/FileType/ISO9660.cs index 1160da73..9e16bbf9 100644 --- a/BinaryObjectScanner/FileType/ISO9660.cs +++ b/BinaryObjectScanner/FileType/ISO9660.cs @@ -25,7 +25,7 @@ namespace BinaryObjectScanner.FileType // Standard checks var subProtections - = RunISOChecks(file, StaticChecks.ISO9660CheckClasses, includeDebug); + = RunDiskImageChecks(file, StaticChecks.ISO9660CheckClasses, includeDebug); protections.Append(file, subProtections.Values); // If there are no protections @@ -43,51 +43,51 @@ namespace BinaryObjectScanner.FileType } /// - /// Checks whether the sequence of bytes is pure data (as in, not empty, not text, just high-entropy data) + /// Checks whether the sequence of bytes is pure data (as in, not empty, + /// not text, just high-entropy data) /// public static bool IsPureData(byte[] bytes) { // Check if there are three 0x00s in a row. Two seems like pushing it - byte[] containedZeroes = {0x00, 0x00, 0x00}; int index = 0; - for (int i = 0; i < bytes.Length; ++i) + for (int i = 0; i < bytes.Length; ++i) { - if (bytes[i] == containedZeroes[index]) + if (bytes[i] == 0x00) { - if (++index >= containedZeroes.Length) - return false; + if (++index >= 3) + return false; } else { - index = 0; + index = 0; } } - + // Checks if there are strings in the data // TODO: is this too dangerous, or too faulty? // Currently-found worst cases: // "Y:1BY:1BC" in Redump ID 23339 var strings = bytes.ReadStringsWithEncoding(charLimit: 7, Encoding.ASCII); - Regex rgx = new Regex("[^a-zA-Z0-9 -'!,.]"); + var rgx = new Regex("[^a-zA-Z0-9 -'!,.]"); foreach (string str in strings) { if (rgx.Replace(str, "").Length > 7) return false; } - + return true; } - - // TODO: can these 2 "noteworthy" functions be cached? + /// /// Checks whether the Application Use data is "noteworthy" enough to be worth checking for protection. /// + /// TODO: can these 2 "noteworthy" functions be cached? public static bool NoteworthyApplicationUse(PrimaryVolumeDescriptor pvd) { var applicationUse = pvd.ApplicationUse; if (Array.TrueForAll(applicationUse, b => b == 0x00)) return false; - + int offset = 0; string? potentialAppUseString = applicationUse.ReadNullTerminatedAnsiString(ref offset); if (potentialAppUseString != null && potentialAppUseString.Length > 0) // Some image authoring programs add a starting string to AU data @@ -100,20 +100,21 @@ namespace BinaryObjectScanner.FileType return false; else if (Array.TrueForAll(Encoding.ASCII.GetBytes(potentialAppUseString), b => b == 0x20)) return false; + // TODO: Unhandled "norb" mastering that puts stuff everywhere, inconsistently. See RID 103641 // More things will have to go here as more disc authoring softwares are found that do this. // Redump ID 24478 has a bunch of 0x20 with norb in the middle, some discs have 0x20 that ends in a "/" // character. If these are found to be causing issues they can be added. } - + offset = 141; potentialAppUseString = applicationUse.ReadNullTerminatedAnsiString(ref offset); - if (potentialAppUseString == "CD-XA001") - return false; - + if (potentialAppUseString == "CD-XA001") + return false; + return true; } - + /// /// Checks whether the Reserved 653 Bytes are "noteworthy" enough to be worth checking for protection. /// @@ -123,8 +124,9 @@ namespace BinaryObjectScanner.FileType var noteworthyReserved653Bytes = true; if (Array.TrueForAll(reserved653Bytes, b => b == 0x00)) noteworthyReserved653Bytes = false; + // Unsure if more will be needed return noteworthyReserved653Bytes; } } -} \ No newline at end of file +} diff --git a/BinaryObjectScanner/Interfaces/IDiskImageCheck.cs b/BinaryObjectScanner/Interfaces/IDiskImageCheck.cs index 6488a4ee..c0d0f532 100644 --- a/BinaryObjectScanner/Interfaces/IDiskImageCheck.cs +++ b/BinaryObjectScanner/Interfaces/IDiskImageCheck.cs @@ -11,9 +11,9 @@ namespace BinaryObjectScanner.Interfaces /// Check a path for protections based on file contents /// /// File to check for protection indicators - /// + /// Disk image representing the read-in file /// True to include debug data, false otherwise /// String containing any protections found in the file string? CheckDiskImage(string file, T diskImage, bool includeDebug); } -} \ No newline at end of file +} diff --git a/BinaryObjectScanner/Protection/AlphaROM.cs b/BinaryObjectScanner/Protection/AlphaROM.cs index df20d4ae..43c44047 100644 --- a/BinaryObjectScanner/Protection/AlphaROM.cs +++ b/BinaryObjectScanner/Protection/AlphaROM.cs @@ -10,7 +10,7 @@ namespace BinaryObjectScanner.Protection /// /// Alpha-ROM is a form of copy protection created by SETTEC. It is known to make use of twin sectors as well as region locking. /// Later forms of Alpha-ROM appear to be digital only, and it's currently unsure what forms of protection the digital only version includes, except that it does make use of region locking. - /// It seems that Alpha-ROM was used in Visual Novels using certain game engines, most notably RealLive and Siglus (https://forums.fuwanovel.net/topic/20927-cannot-crack-siglus-engine-with-alpharom/). + /// It seems that Alpha-ROM was used in Visual Novels using certain game engines, most notably RealLive and Siglus (https://forums.fuwanovel.net/topic/20927-cannot-crack-siglus-engine-with-alpharom/). /// Not every Siglus engine game uses Alpha-ROM (Source: https://sample9.dmm.co.jp/digital/pcgame/vsat_0263/vsat_0263t.zip {Official trial mirror}). /// Not every RealLive engine game uses Alpha-ROM (Source: IA item "Kanon_Standard_Edition_Japan"). /// Alpha-ROM also seems to have made use of something called "Alpha-DPS" for non-executable data files (http://www.gonsuke.co.jp/protect.html). @@ -43,12 +43,57 @@ namespace BinaryObjectScanner.Protection // - SETTEC0000SETTEC1111 // - SOFTWARE\SETTEC // TODO: Are there version numbers? - public class AlphaROM : IExecutableCheck, IDiskImageCheck + public class AlphaROM : IDiskImageCheck, IExecutableCheck { + /// + public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) + { + // Checks can be made even easier once UDF support exists, as most (although not all, some early discs like + // redump ID 124111 have no UDF partition) discs have "Settec" slathered over every field UDF lets them. + + if (diskImage.VolumeDescriptorSet.Length == 0) + return null; + if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) + return null; + + // Alpharom disc check #1: disc has varying (but observed to at least always be larger than 14) length + // string made up of numbers and capital letters. + // TODO: triple-check that length is never below 14 + int offset = 0; + var applicationIdentifierString = pvd.ApplicationIdentifier.ReadNullTerminatedAnsiString(ref offset)?.Trim(); + if (applicationIdentifierString == null || applicationIdentifierString.Length < 14) + return null; + + if (!Regex.IsMatch(applicationIdentifierString, "^[A-Z0-9]*$")) + return null; + + // Alpharom disc check #2: disc has publisher identifier filled with varying amount of data (26-50 bytes + // have been observed) followed by spaces. There's a decent chance this is just a Japanese text string, but + // UTF, Shift-JIS, and EUC-JP all fail to display anything but garbage. + + var publisherIdentifier = pvd.PublisherIdentifier; + int firstSpace = Array.FindIndex(publisherIdentifier, b => b == 0x20); + if (firstSpace <= 10 || firstSpace >= 120) + return null; + + var publisherData = new byte[firstSpace]; + var publisherSpaces = new byte[publisherData.Length - firstSpace]; + Array.Copy(publisherIdentifier, 0, publisherData, 0, firstSpace); + Array.Copy(publisherIdentifier, firstSpace, publisherSpaces, 0, publisherData.Length - firstSpace); + + if (!Array.TrueForAll(publisherSpaces, b => b == 0x20)) + return null; + + if (!FileType.ISO9660.IsPureData(publisherData)) + return null; + + return "AlphaROM"; + } + /// public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug) { - // TODO: Add support for detecting Alpha-ROM found in older games made with the RealLive engine. + // TODO: Add support for detecting Alpha-ROM found in older games made with the RealLive engine. // TODO: Add version detection for Alpha-ROM. // Get the .data/DATA section strings, if they exist @@ -88,51 +133,5 @@ namespace BinaryObjectScanner.Protection return null; } - - /// - public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) - { - // Checks can be made even easier once UDF support exists, as most (although not all, some early discs like - // redump ID 124111 have no UDF partition) discs have "Settec" slathered over every field UDF lets them. - - if (diskImage.VolumeDescriptorSet.Length == 0) - return null; - - if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) - return null; - - // Alpharom disc check #1: disc has varying (but observed to at least always be larger than 14) length - // string made up of numbers and capital letters. - // TODO: triple-check that length is never below 14 - int offset = 0; - var applicationIdentifierString = pvd.ApplicationIdentifier.ReadNullTerminatedAnsiString(ref offset)?.Trim(); - if (applicationIdentifierString == null || applicationIdentifierString.Length < 14) - return null; - - if (!Regex.IsMatch(applicationIdentifierString, "^[A-Z0-9]*$")) - return null; - - // Alpharom disc check #2: disc has publisher identifier filled with varying amount of data (26-50 bytes - // have been observed) followed by spaces. There's a decent chance this is just a Japanese text string, but - // UTF, Shift-JIS, and EUC-JP all fail to display anything but garbage. - - var publisherIdentifier = pvd.PublisherIdentifier; - int firstSpace = Array.FindIndex(publisherIdentifier, b => b == 0x20); - if (firstSpace <= 10 || firstSpace >= 120) - return null; - - var publisherData = new byte[firstSpace]; - var publisherSpaces = new byte[publisherData.Length - firstSpace]; - Array.Copy(publisherIdentifier, 0, publisherData, 0, firstSpace); - Array.Copy(publisherIdentifier, firstSpace, publisherSpaces, 0, publisherData.Length - firstSpace); - - if (!Array.TrueForAll(publisherSpaces, b => b == 0x20)) - return null; - - if (!FileType.ISO9660.IsPureData(publisherData)) - return null; - - return "AlphaROM"; - } } } diff --git a/BinaryObjectScanner/Protection/CopyLok.cs b/BinaryObjectScanner/Protection/CopyLok.cs index ce0c6d43..d62e83da 100644 --- a/BinaryObjectScanner/Protection/CopyLok.cs +++ b/BinaryObjectScanner/Protection/CopyLok.cs @@ -11,19 +11,99 @@ namespace BinaryObjectScanner.Protection /// More specifically, it may have been created a spin-off division known as "Panlok" (https://cdmediaworld.com/hardware/cdrom/cd_protections_copylok.shtml). /// Though it may also be that "PanLok" was an alternate name for the program itself (https://gamecopyworld.com/games/pc_generic_copylok.shtml). /// There was a PanLok website, but it's unknown what content it had hosted (https://web.archive.org/web/20041215075727/http://www.panlok.com/). - /// + /// /// CopyLok made use of bad sectors, using an average of 720-750 per disc, and appears to have been a form of ring protection (http://forum.redump.org/topic/29842/issues-dumping-pc-disc-with-code-lock-copy-protection/). /// At least one disc with CopyLok appears to contain an excerpt of the poem "Jabberwocky" by Lewis Carroll in the raw sector data (http://forum.redump.org/post/54050/#p54050). /// According to the Readme for poxylok (https://gf.wiretarget.com/copylok.htm), some version of Gangsters 2 may have this protection. - /// + /// /// Previous versions of BinaryObjectScanner incorrectly reported this DRM as "CodeLock / CodeLok / CopyLok". It was later discovered that due to the similar names, two entirely different DRM were erroneously lumped together. /// "CodeLock" (in this case actually referring to "Code-Lock") is an entirely separate form of DRM, with the existing check now getting used separately. /// Also not to be confused with https://en.wikipedia.org/wiki/Rob_Northen_copylock. - /// + /// /// COPYLOK trademark: https://www.trademarkelite.com/europe/trademark/trademark-detail/000618512/COPYLOK. /// - public class CopyLok : IExecutableCheck, IDiskImageCheck + public class CopyLok : IDiskImageCheck, IExecutableCheck { + /// + public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) + { + if (diskImage.VolumeDescriptorSet.Length == 0) + return null; + if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) + return null; + + if (!FileType.ISO9660.NoteworthyApplicationUse(pvd)) + return null; + + if (FileType.ISO9660.NoteworthyReserved653Bytes(pvd)) + return null; + + int offset = 0; + + #region Read Application Use + + var applicationUse = pvd.ApplicationUse; + uint constantValueOne = applicationUse.ReadUInt32LittleEndian(ref offset); + ushort smallSizeBytes = applicationUse.ReadUInt16LittleEndian(ref offset); + ushort constantValueTwo = applicationUse.ReadUInt16LittleEndian(ref offset); + uint finalSectionOneBytes = applicationUse.ReadUInt32LittleEndian(ref offset); + byte zeroByte = applicationUse.ReadByte(ref offset); + ushort earlyCopyLokBytesOne = applicationUse.ReadUInt16LittleEndian(ref offset); + ushort pairBytesOne = applicationUse.ReadUInt16LittleEndian(ref offset); + uint oneValueBytes = applicationUse.ReadUInt32LittleEndian(ref offset); + uint earlyCopyLokBytesTwo = applicationUse.ReadUInt32LittleEndian(ref offset); + uint pairBytesTwo = applicationUse.ReadUInt32LittleEndian(ref offset); + var endingZeroBytes = applicationUse.ReadBytes(ref offset, 483); + + #endregion + + #region Main Checks + + // Early return if the rest of the AU data isn't 0x00 + if (!Array.TrueForAll(endingZeroBytes, b => b == 0x00)) + return null; + + // Check first currently-observed constant value + if (constantValueOne != 0x4ED38AE1) + return null; + + // Check for early variant copylok + if (earlyCopyLokBytesOne == 0x00) + { + // Redump ID 35908, 56433, 44526 + if (pairBytesOne == 0 && oneValueBytes == 0 && earlyCopyLokBytesTwo == 0 && pairBytesTwo == 0) + return "CopyLok / CodeLok (Early, ~1850 errors)"; + + return "CopyLok / CodeLok - Unknown variant, please report to us on GitHub!"; + } + + // Check remaining currently-observed constant values + if (constantValueTwo != 0x4ED3 || zeroByte != 0x00 || earlyCopyLokBytesOne != 0x0C76 || oneValueBytes != 0x00000001) + return "CopyLok / CodeLok - Unknown variant, please report to us on GitHub!"; + + // Always 0xD1AD, except in Redump ID 71985 (the only sample) where it's 0x6999 + // Update number be more accurate if more samples are acquired. + if (smallSizeBytes < 0xADD1) + return "CopyLok / CodeLok (Less errors, ~255)"; + + if (pairBytesOne == 0x9425) + { + // Redump ID 37860, 37881, 38239, 100685, 108375 + if (pairBytesTwo != 0x00000000) + return "CopyLok / CodeLok (Pair errors, ~1500)"; + + return "CopyLok / CodeLok - Unknown variant, please report to us on GitHub!"; + } + + // Redump ID 31557, 44210, 49087, 72183, 31675 + if (pairBytesOne == 0xF3ED && pairBytesTwo == 0x00000000) + return "CopyLok / CodeLok (Solo errors, ~775)"; + + #endregion + + return "CopyLok / CodeLok - Unknown variant, please report to us on GitHub!"; + } + /// public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug) { @@ -36,91 +116,5 @@ namespace BinaryObjectScanner.Protection return null; } - - /// - public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) - { - #region Initial Checks - - if (diskImage.VolumeDescriptorSet.Length == 0) - return null; - - if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) - return null; - - - if (!FileType.ISO9660.NoteworthyApplicationUse(pvd)) - return null; - - if (FileType.ISO9660.NoteworthyReserved653Bytes(pvd)) - return null; - - #endregion - - int offset = 0; - - #region Read Application Use - - var applicationUse = pvd.ApplicationUse; - uint constantValueOne = applicationUse.ReadUInt32LittleEndian(ref offset); - ushort smallSizeBytes = applicationUse.ReadUInt16LittleEndian(ref offset); - ushort constantValueTwo = applicationUse.ReadUInt16LittleEndian(ref offset); - uint finalSectionOneBytes = applicationUse.ReadUInt32LittleEndian(ref offset); - byte zeroByte = applicationUse.ReadByte(ref offset); - ushort earlyCopyLokBytesOne = applicationUse.ReadUInt16LittleEndian(ref offset); - ushort pairBytesOne = applicationUse.ReadUInt16LittleEndian(ref offset); - uint oneValueBytes = applicationUse.ReadUInt32LittleEndian(ref offset); - uint earlyCopyLokBytesTwo = applicationUse.ReadUInt32LittleEndian(ref offset); - uint pairBytesTwo = applicationUse.ReadUInt32LittleEndian(ref offset); - var endingZeroBytes = applicationUse.ReadBytes(ref offset, 483); - - #endregion - - #region Main Checks - - // Early return if the rest of the AU data isn't 0x00 - if (!Array.TrueForAll(endingZeroBytes, b => b == 0x00)) - return null; - - // Check first currently-observed constant value - if (constantValueOne != 0x4ED38AE1) - return null; - - // Check for early variant copylok - if (earlyCopyLokBytesOne == 0x00) - { - // Redump ID 35908, 56433, 44526 - if (pairBytesOne == 0 && oneValueBytes == 0 && earlyCopyLokBytesTwo == 0 && pairBytesTwo == 0) - return "CopyLok / CodeLok (Early, ~1850 errors)"; - - return "CopyLok / CodeLok - Unknown variant, please report to us on GitHub!"; - } - - // Check remaining currently-observed constant values - if (constantValueTwo != 0x4ED3 || zeroByte != 0x00 || earlyCopyLokBytesOne != 0x0C76 || oneValueBytes != 0x00000001) - return "CopyLok / CodeLok - Unknown variant, please report to us on GitHub!"; - - // Always 0xD1AD, except in Redump ID 71985 (the only sample) where it's 0x6999 - // Update number be more accurate if more samples are acquired. - if (smallSizeBytes < 0xADD1) - return "CopyLok / CodeLok (Less errors, ~255)"; - - if (pairBytesOne == 0x9425) - { - // Redump ID 37860, 37881, 38239, 100685, 108375 - if (pairBytesTwo != 0x00000000) - return "CopyLok / CodeLok (Pair errors, ~1500)"; - - return "CopyLok / CodeLok - Unknown variant, please report to us on GitHub!"; - } - - // Redump ID 31557, 44210, 49087, 72183, 31675 - if (pairBytesOne == 0xF3ED && pairBytesTwo == 0x00000000) - return "CopyLok / CodeLok (Solo errors, ~775)"; - - #endregion - - return "CopyLok / CodeLok - Unknown variant, please report to us on GitHub!"; - } } } diff --git a/BinaryObjectScanner/Protection/LaserLok.cs b/BinaryObjectScanner/Protection/LaserLok.cs index c96ddbe6..1b7fc61d 100644 --- a/BinaryObjectScanner/Protection/LaserLok.cs +++ b/BinaryObjectScanner/Protection/LaserLok.cs @@ -11,8 +11,44 @@ using SabreTools.Serialization.Wrappers; namespace BinaryObjectScanner.Protection { - public class LaserLok : IExecutableCheck, IPathCheck, IDiskImageCheck + public class LaserLok : IDiskImageCheck, IExecutableCheck, IPathCheck { + /// + public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) + { + if (diskImage.VolumeDescriptorSet.Length == 0) + return null; + if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) + return null; + + if (FileType.ISO9660.NoteworthyApplicationUse(pvd)) + return null; //TODO: this might be too unsafe until more App Use strings are known + + if (!FileType.ISO9660.NoteworthyReserved653Bytes(pvd)) + return null; + + var reserved653Bytes = pvd.Reserved653Bytes; + int firstNonZero = Array.FindIndex(reserved653Bytes, b => b != 0); + if (firstNonZero < 0) + return null; + + string? finalString = reserved653Bytes.ReadNullTerminatedAnsiString(ref firstNonZero); + if (finalString == null) + return null; + + // Redump ID 113120 + if (finalString.StartsWith("MLSLaserlock")) + return "LaserLock"; + + // Redump ID 38308, 113341 + if (finalString.StartsWith("LaserlockECL")) + return "LaserLock Marathon"; + + // Some discs such as 128068, and also more normal ones, don't seem to have any identifying data. + // TODO: list some normal ones + return null; + } + /// public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug) { @@ -151,48 +187,6 @@ namespace BinaryObjectScanner.Protection return MatchUtil.GetFirstMatch(path, matchers, any: true); } - /// - public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) - { - #region Initial Checks - - if (diskImage.VolumeDescriptorSet.Length == 0) - return null; - - if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) - return null; - - - if (FileType.ISO9660.NoteworthyApplicationUse(pvd)) - return null; //TODO: this might be too unsafe until more App Use strings are known - - if (!FileType.ISO9660.NoteworthyReserved653Bytes(pvd)) - return null; - - #endregion - - var reserved653Bytes = pvd.Reserved653Bytes; - int firstNonZero = Array.FindIndex(reserved653Bytes, b => b != 0); - if (firstNonZero < 0) - return null; - - string? finalString = reserved653Bytes.ReadNullTerminatedAnsiString(ref firstNonZero); - if (finalString == null) - return null; - - // Redump ID 113120 - if (finalString.StartsWith("MLSLaserlock")) - return "LaserLock"; - - // Redump ID 38308, 113341 - if (finalString.StartsWith("LaserlockECL")) - return "LaserLock Marathon"; - - // Some discs such as 128068, and also more normal ones, don't seem to have any identifying data. - // TODO: list some normal ones - return null; - } - private static string GetBuild(byte[]? sectionContent, bool versionTwo) { if (sectionContent == null) diff --git a/BinaryObjectScanner/Protection/Macrovision.cs b/BinaryObjectScanner/Protection/Macrovision.cs index cea646ed..3d35ae25 100644 --- a/BinaryObjectScanner/Protection/Macrovision.cs +++ b/BinaryObjectScanner/Protection/Macrovision.cs @@ -13,12 +13,78 @@ namespace BinaryObjectScanner.Protection /// /// Macrovision was a company that specialized in various forms of DRM. They had an extensive product line, their most infamous product (within this context) being SafeDisc. /// Due to there being a significant amount of backend tech being shared between various protections, a separate class is needed for generic Macrovision detections. - /// + /// /// Macrovision Corporation CD-ROM Unauthorized Copying Study: https://web.archive.org/web/20011005161810/http://www.macrovision.com/solutions/software/cdrom/images/Games_CD-ROM_Study.PDF /// List of trademarks associated with Marovision: https://tmsearch.uspto.gov/bin/showfield?f=toc&state=4804%3Au8wykd.5.1&p_search=searchss&p_L=50&BackReference=&p_plural=yes&p_s_PARA1=&p_tagrepl%7E%3A=PARA1%24LD&expr=PARA1+AND+PARA2&p_s_PARA2=macrovision&p_tagrepl%7E%3A=PARA2%24ALL&p_op_ALL=AND&a_default=search&a_search=Submit+Query&a_search=Submit+Query /// - public partial class Macrovision : IExecutableCheck, IExecutableCheck, IPathCheck, IDiskImageCheck + public partial class Macrovision : IDiskImageCheck, IExecutableCheck, IExecutableCheck, IPathCheck { + /// + public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) + { + if (diskImage.VolumeDescriptorSet.Length == 0) + return null; + if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) + return null; + + if (!FileType.ISO9660.NoteworthyApplicationUse(pvd)) + return null; + + // Early SafeDisc actually doesn't cross into reserved bytes. Regardless, SafeDisc CD is easy enough to + // identify for obvious other reasons, so there's not much point in potentially running into false positives. + + if (!FileType.ISO9660.NoteworthyReserved653Bytes(pvd)) + return null; + + var applicationUse = pvd.ApplicationUse; + var reserved653Bytes = pvd.Reserved653Bytes; + + #region Read Application Use + + int offset = 0; + var appUseZeroBytes = applicationUse.ReadBytes(ref offset, 256); + var appUseDataBytesOne = applicationUse.ReadBytes(ref offset, 128); + offset += 64; // Some extra values get added here over time. Check is good enough, easier to skip this. + ushort appUseUshort = applicationUse.ReadUInt16LittleEndian(ref offset); + var appUseDataBytesTwo = applicationUse.ReadBytes(ref offset, 20); + uint appUseUint = applicationUse.ReadUInt32LittleEndian(ref offset); + var appUseDataBytesThree = applicationUse.ReadBytes(ref offset, 38); + + #endregion + + #region Read Reserved 653 Bytes + + offset = 0; + // Somewhat arbitrary, but going further than 11 seems to exclude some discs. + var reservedDataBytes = reserved653Bytes.ReadBytes(ref offset, 10); + offset = 132; // TODO: Does it ever go further than this? + var reservedZeroBytes = reserved653Bytes.ReadBytes(ref offset, 521); + + #endregion + + // The first 256 bytes of application use, and the last 521 bytes of reserved data, should all be 0x00. + // It's possible reserved might need to be shortened a bit, but a need for that has not been observed yet. + if (!Array.TrueForAll(appUseZeroBytes, b => b == 0x00) || !Array.TrueForAll(reservedZeroBytes, b => b == 0x00)) + return null; + + // All of these sections should be pure data + if (!FileType.ISO9660.IsPureData(appUseDataBytesOne) + || !FileType.ISO9660.IsPureData(appUseDataBytesTwo) + || !FileType.ISO9660.IsPureData(appUseDataBytesThree) + || !FileType.ISO9660.IsPureData(reservedDataBytes)) + { + return null; + } + + // appUseFirstUint has only ever been observed as 0xBB, but no need to be this strict yet. Can be checked + // if it's found that it's needed to, and always viable. appUseSecondUint varies more, but is still always + // under 0xFF so far. + if (appUseUshort > 0xFF || appUseUint > 0xFF) + return null; + + return "SafeDisc"; + } + /// public string? CheckExecutable(string file, NewExecutable exe, bool includeDebug) { @@ -244,77 +310,6 @@ namespace BinaryObjectScanner.Protection return null; } - /// - public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) - { - #region Initial Checks - - if (diskImage.VolumeDescriptorSet.Length == 0) - return null; - - if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) - return null; - - - if (!FileType.ISO9660.NoteworthyApplicationUse(pvd)) - return null; - - // Early SafeDisc actually doesn't cross into reserved bytes. Regardless, SafeDisc CD is easy enough to - // identify for obvious other reasons, so there's not much point in potentially running into false positives. - - if (!FileType.ISO9660.NoteworthyReserved653Bytes(pvd)) - return null; - - #endregion - - var applicationUse = pvd.ApplicationUse; - var reserved653Bytes = pvd.Reserved653Bytes; - - #region Read Application Use - - int offset = 0; - var appUseZeroBytes = applicationUse.ReadBytes(ref offset, 256); - var appUseDataBytesOne = applicationUse.ReadBytes(ref offset, 128); - offset += 64; // Some extra values get added here over time. Check is good enough, easier to skip this. - ushort appUseUshort = applicationUse.ReadUInt16LittleEndian(ref offset); - var appUseDataBytesTwo = applicationUse.ReadBytes(ref offset, 20); - uint appUseUint = applicationUse.ReadUInt32LittleEndian(ref offset); - var appUseDataBytesThree = applicationUse.ReadBytes(ref offset, 38); - - #endregion - - offset = 0; - - #region Read Reserved 653 Bytes - - // Somewhat arbitrary, but going further than 11 seems to exclude some discs. - var reservedDataBytes = reserved653Bytes.ReadBytes(ref offset, 10); - offset = 132; // TODO: Does it ever go further than this? - var reservedZeroBytes = reserved653Bytes.ReadBytes(ref offset, 521); - - #endregion - - // The first 256 bytes of application use, and the last 521 bytes of reserved data, should all be 0x00. - // It's possible reserved might need to be shortened a bit, but a need for that has not been observed yet. - if (!Array.TrueForAll(appUseZeroBytes, b => b == 0x00) || !Array.TrueForAll(reservedZeroBytes, b => b == 0x00)) - return null; - - // All of these sections should be pure data - if (!FileType.ISO9660.IsPureData(appUseDataBytesOne) - || !FileType.ISO9660.IsPureData(appUseDataBytesTwo) - || !FileType.ISO9660.IsPureData(appUseDataBytesThree) - || !FileType.ISO9660.IsPureData(reservedDataBytes)) - return null; - - // appUseFirstUint has only ever been observed as 0xBB, but no need to be this strict yet. Can be checked - // if it's found that it's needed to, and always viable. appUseSecondUint varies more, but is still always - // under 0xFF so far. - if (appUseUshort > 0xFF || appUseUint > 0xFF) - return null; - - return "SafeDisc"; - } - /// internal static List MacrovisionCheckDirectoryPath(string path, List? files) { @@ -349,7 +344,7 @@ namespace BinaryObjectScanner.Protection // One filesize is known to overlap with both SafeDisc and CDS-300, and so is detected separately here. return firstMatchedString.FileSize() switch { - // Found in Redump entries 37832 and 66005. + // Found in Redump entries 37832 and 66005. 20 => "SafeDisc 1.00.025-1.41.001", // Found in Redump entries 30555 and 58573. @@ -461,7 +456,7 @@ namespace BinaryObjectScanner.Protection private static string GetSecDrvExecutableVersion(PortableExecutable exe) { // Different versions of this driver correspond to different SafeDisc versions. - // TODO: Check if earlier versions of this driver contain the version string in a less obvious place. + // TODO: Check if earlier versions of this driver contain the version string in a less obvious place. var version = exe.FileVersion; if (!string.IsNullOrEmpty(version)) { @@ -672,13 +667,13 @@ namespace BinaryObjectScanner.Protection // Found in Redump entries 21154, 37982, and 108632. or "1.20.000" - // Found in Redump entries 17024/37920. + // Found in Redump entries 17024/37920. or "1.20.001" // Found in Redump entries 28708, 31526, 43321, 55080, and 75501. or "1.30.010" - // Found in Redump entries 9617 and 49552. + // Found in Redump entries 9617 and 49552. or "1.35.000" // Found in Redump entries 2595 and 30121. diff --git a/BinaryObjectScanner/Protection/ProtectDISC.cs b/BinaryObjectScanner/Protection/ProtectDISC.cs index a6a591cf..be46b1ec 100644 --- a/BinaryObjectScanner/Protection/ProtectDISC.cs +++ b/BinaryObjectScanner/Protection/ProtectDISC.cs @@ -13,8 +13,46 @@ namespace BinaryObjectScanner.Protection { // This protection was called VOB ProtectCD / ProtectDVD in versions prior to 6 // ProtectDISC 9/10 checks for the presence of CSS on the disc to run, but don't encrypt any sectors or check for keys. Confirmed in Redump entries 78367 and 110095. - public class ProtectDISC : IExecutableCheck, IPathCheck, IDiskImageCheck + public class ProtectDISC : IDiskImageCheck, IExecutableCheck, IPathCheck { + /// + public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) + { + // If false positives occur on ProtectDiSC for some reason, there's a bit in the reserved bytes that + // can be checked. Not bothering since this doesn't work for ProtectCD/DVD 6.x discs, which use otherwise + // the same check anyways. + + if (diskImage.VolumeDescriptorSet.Length == 0) + return null; + if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) + return null; + + int offset = 0; + var copyrightString = pvd.CopyrightFileIdentifier.ReadNullTerminatedAnsiString(ref offset); + if (copyrightString == null || copyrightString.Length < 19) + return null; + + copyrightString = copyrightString.Substring(0, 19); // Redump ID 15896 has a trailing space + + // Stores some kind of serial in the copyright string, format 0000-XXXX-XXXX-XXXX where it can be numbers or + // capital letters. + + if (!Regex.IsMatch(copyrightString, "[0]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}")) + return null; + + offset = 0; + + // Starting with sometime around 7.5, ProtectDiSC includes a version number string here. Examples include + // 7.5.0.61324 and 9.0.1119. ProtectDiSC versioning is very confusing, so this is not the "actual" version + // number and should not be printed. + // Previous versions just have spaces here, so it doesn't need to be validated beyond that. + var abstractIdentifierString = pvd.AbstractFileIdentifier.ReadNullTerminatedAnsiString(ref offset); + if (abstractIdentifierString == null || abstractIdentifierString.Trim().Length == 0) + return "ProtectDiSC 6-Early 7.x"; + + return "ProtectDiSC Mid-7.x+"; + } + /// public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug) { @@ -135,45 +173,6 @@ namespace BinaryObjectScanner.Protection return MatchUtil.GetFirstMatch(path, matchers, any: true); } - /// - public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) - { - // If false positives occur on ProtectDiSC for some reason, there's a bit in the reserved bytes that - // can be checked. Not bothering since this doesn't work for ProtectCD/DVD 6.x discs, which use otherwise - // the same check anyways. - - if (diskImage.VolumeDescriptorSet.Length == 0) - return null; - - if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) - return null; - - int offset = 0; - var copyrightString = pvd.CopyrightFileIdentifier.ReadNullTerminatedAnsiString(ref offset); - if (copyrightString == null || copyrightString.Length < 19) - return null; - - copyrightString = copyrightString.Substring(0, 19); // Redump ID 15896 has a trailing space - - // Stores some kind of serial in the copyright string, format 0000-XXXX-XXXX-XXXX where it can be numbers or - // capital letters. - - if (!Regex.IsMatch(copyrightString, "[0]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}")) - return null; - - offset = 0; - - // Starting with sometime around 7.5, ProtectDiSC includes a version number string here. Examples include - // 7.5.0.61324 and 9.0.1119. ProtectDiSC versioning is very confusing, so this is not the "actual" version - // number and should not be printed. - // Previous versions just have spaces here, so it doesn't need to be validated beyond that. - var abstractIdentifierString = pvd.AbstractFileIdentifier.ReadNullTerminatedAnsiString(ref offset); - if (abstractIdentifierString == null || abstractIdentifierString.Trim().Length == 0) - return "ProtectDiSC 6-Early 7.x"; - - return "ProtectDiSC Mid-7.x+"; - } - private static string GetOldVersion(string matchedString) { // Remove unnecessary parts diff --git a/BinaryObjectScanner/Protection/SecuROM.cs b/BinaryObjectScanner/Protection/SecuROM.cs index 952d29f7..001beef2 100644 --- a/BinaryObjectScanner/Protection/SecuROM.cs +++ b/BinaryObjectScanner/Protection/SecuROM.cs @@ -13,7 +13,7 @@ namespace BinaryObjectScanner.Protection { // TODO: Investigate SecuROM for Macintosh // TODO: Think of a way to detect dfe - public class SecuROM : IExecutableCheck, IPathCheck, IDiskImageCheck + public class SecuROM : IDiskImageCheck, IExecutableCheck, IPathCheck { /// /// Matches hash of the Release Control-encrypted executable to known hashes @@ -24,7 +24,7 @@ namespace BinaryObjectScanner.Protection {"C6DFF6B08EE126893840E107FD4EC9F6", "Alice - Madness Returns (USA)+(Europe)"}, {"D7703D32B72185358D58448B235BD55E", "Arcania - Gothic 4 (Australia)"}, // International version (English, French, Italian, German, Spanish) // Arcania - Gothic 4 - Polish(/Hungarian?) - known to most likely exist. Likely matches support site exe. - {"83CD6225899C08422F860095962287A5", "Arcania - Gothic 4 - Russian (not in redump yet)"}, + {"83CD6225899C08422F860095962287A5", "Arcania - Gothic 4 - Russian (not in redump yet)"}, // Arcania - Gothic 4 - Chinese - known to most likely exist. Likely matches support site exe. {"FAF6DD75DDB335101CB77A714793DC28", "Batman - Arkham City - Game of the Year Edition (UK)"}, {"77999579EE4378BDFAC9438CC9CDB44E", "Batman - Arkham City (USA)+(Europe)"}, @@ -103,6 +103,141 @@ namespace BinaryObjectScanner.Protection {48093043, "deadspace_f.aec"}, }; + /// + public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) + { + + if (diskImage.VolumeDescriptorSet.Length == 0) + return null; + if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) + return null; + + // Application Use is too inconsistent to include or exclude + + // There needs to be noteworthy data in the reserved 653 bytes + if (!FileType.ISO9660.NoteworthyReserved653Bytes(pvd)) + return null; + + var applicationUse = pvd.ApplicationUse; + var reserved653Bytes = pvd.Reserved653Bytes; + + #region Read Application Use + + var offset = 0; + + // Either there's nothing of note, or it's empty other than a 4-byte value at the start. + if (FileType.ISO9660.NoteworthyApplicationUse(pvd)) + { + uint appUseUint = applicationUse.ReadUInt32LittleEndian(ref offset); + var appUseZeroBytes = applicationUse.ReadBytes(ref offset, 508); + + if (appUseUint == 0 || !Array.TrueForAll(appUseZeroBytes, b => b == 0x00)) + return null; + } + + #endregion + + #region Read Reserved 653 Bytes + + offset = 0; + + var reservedZeroBytesOne = reserved653Bytes.ReadBytes(ref offset, 489); + uint reservedHundredValue = reserved653Bytes.ReadUInt32LittleEndian(ref offset); + var reserveDataBytesOne = reserved653Bytes.ReadBytes(ref offset, 80); + var reservedZeroBytesTwo = reserved653Bytes.ReadBytes(ref offset, 12); + uint reservedUintOne = reserved653Bytes.ReadUInt32LittleEndian(ref offset); + uint reservedUintTwoLow = reserved653Bytes.ReadUInt32LittleEndian(ref offset); // Low value + var reservedZeroBytesThree = reserved653Bytes.ReadBytes(ref offset, 4); + uint reservedUintThree = reserved653Bytes.ReadUInt32LittleEndian(ref offset); + var reservedZeroBytesFour = reserved653Bytes.ReadBytes(ref offset, 12); + uint reservedUintFour = reserved653Bytes.ReadUInt32LittleEndian(ref offset); + uint reservedOneValue = reserved653Bytes.ReadUInt32LittleEndian(ref offset); + var reservedZeroBytesFive = reserved653Bytes.ReadBytes(ref offset, 4); + var reservedDataBytesTwo = reserved653Bytes.ReadBytes(ref offset, 12); + byte reservedLowByteValueOne = reserved653Bytes.ReadByteValue(ref offset); + byte reservedLowByteValueTwo = reserved653Bytes.ReadByteValue(ref offset); + byte reservedLowByteValueThree = reserved653Bytes.ReadByteValue(ref offset); + byte reservedLowByteValueFour = reserved653Bytes.ReadByteValue(ref offset); + var reservedDataBytesThree = reserved653Bytes.ReadBytes(ref offset, 12); + + #endregion + + // True for all discs + if (!Array.TrueForAll(reservedZeroBytesOne, b => b == 0x00) + || !Array.TrueForAll(reservedZeroBytesTwo, b => b == 0x00) + || !Array.TrueForAll(reservedZeroBytesThree, b => b == 0x00) + || !Array.TrueForAll(reservedZeroBytesFour, b => b == 0x00) + || !Array.TrueForAll(reservedZeroBytesFive, b => b == 0x00)) + { + return null; + } + + #region Early SecuROM Checks + + // This duplicates a lot of code. This region is like this because it's still possible to detect early vers, + // but it should be easy to remove this section if it turns out this leads to conflicts or false positives + if (Array.TrueForAll(reserveDataBytesOne, b => b == 0x00) + && Array.TrueForAll(reservedDataBytesTwo, b => b == 0x00) + && reservedHundredValue == 0 && reservedOneValue == 0 + && reservedUintOne == 0 && reservedUintTwoLow == 0 && reservedUintThree == 0 && reservedUintFour == 0 + && reservedLowByteValueOne == 0 && reservedLowByteValueTwo == 0 && reservedLowByteValueThree == 0) + { + if (FileType.ISO9660.IsPureData(reservedDataBytesThree)) + { + if (reservedLowByteValueFour == 0) + return "SecuROM 3.x-4.6x"; + else if (reservedLowByteValueFour < 0x20) + return "SecuROM 4.7x-4.8x"; + else + return null; + } + + offset = 0; + var earlyFirstFourBytes = reservedDataBytesThree.ReadBytes(ref offset, 4); + var earlyLastEightBytes = reservedDataBytesThree.ReadBytes(ref offset, 8); + + if (Array.TrueForAll(earlyFirstFourBytes, b => b == 0x00) && FileType.ISO9660.IsPureData(earlyLastEightBytes)) + return "SecuROM 2.x-3.x"; + } + + #endregion + + // If this uint32 is 100, the next 80 bytes should be data. Otherwise, both should only ever be zero. + + switch (reservedHundredValue) + { + case 0: + if (!Array.TrueForAll(reserveDataBytesOne, b => b == 0x00)) + return null; + break; + case 100: + if (!FileType.ISO9660.IsPureData(reserveDataBytesOne)) + return null; + break; + default: + return null; + } + + //If you go back to early 4.0 CDs, only the above can be guaranteed to pass. CDs can already be identified via normal + //dumping, though, and (as well as most later CDs) should always pass these remaining checks. + if (reservedUintOne < 0xFFFF || reservedUintTwoLow > 0xFFFF || reservedUintThree < 0xFFFF || reservedUintFour < 0xFFFF) + return null; + + if (reservedOneValue != 1) + return null; + + if (reservedLowByteValueOne > 0x20 || reservedLowByteValueTwo > 0x20 || reservedLowByteValueThree > 0x20 || + reservedLowByteValueFour > 0x20) + return null; + + // TODO: RID 127715 fails this because the first 8 bytes of reservedDataBytesTwo happen to be "afsCafsC" + if (!FileType.ISO9660.IsPureData(reservedDataBytesTwo) + || !FileType.ISO9660.IsPureData(reservedDataBytesThree)) + return null; + + return "SecuROM 4.8x+"; + } + /// public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug) { @@ -254,142 +389,6 @@ namespace BinaryObjectScanner.Protection return MatchUtil.GetFirstMatch(path, matchers, any: true); } - /// - public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) - { - #region Initial Checks - - if (diskImage.VolumeDescriptorSet.Length == 0) - return null; - - if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) - return null; - - // Application Use is too inconsistent to include or exclude - - // There needs to be noteworthy data in the reserved 653 bytes - if (!FileType.ISO9660.NoteworthyReserved653Bytes(pvd)) - return null; - - #endregion - - var applicationUse = pvd.ApplicationUse; - var reserved653Bytes = pvd.Reserved653Bytes; - - #region Read Application Use - - var offset = 0; - - // Either there's nothing of note, or it's empty other than a 4-byte value at the start. - if (FileType.ISO9660.NoteworthyApplicationUse(pvd)) - { - uint appUseUint = applicationUse.ReadUInt32LittleEndian(ref offset); - var appUseZeroBytes = applicationUse.ReadBytes(ref offset, 508); - - if (appUseUint == 0 || !Array.TrueForAll(appUseZeroBytes, b => b == 0x00)) - return null; - } - - #endregion - - offset = 0; - - #region Read Reserved 653 Bytes - - var reservedZeroBytesOne = reserved653Bytes.ReadBytes(ref offset, 489); - uint reservedHundredValue = reserved653Bytes.ReadUInt32LittleEndian(ref offset); - var reserveDataBytesOne = reserved653Bytes.ReadBytes(ref offset, 80); - var reservedZeroBytesTwo = reserved653Bytes.ReadBytes(ref offset, 12); - uint reservedUintOne = reserved653Bytes.ReadUInt32LittleEndian(ref offset); - uint reservedUintTwoLow = reserved653Bytes.ReadUInt32LittleEndian(ref offset); // Low value - var reservedZeroBytesThree = reserved653Bytes.ReadBytes(ref offset, 4); - uint reservedUintThree = reserved653Bytes.ReadUInt32LittleEndian(ref offset); - var reservedZeroBytesFour = reserved653Bytes.ReadBytes(ref offset, 12); - uint reservedUintFour = reserved653Bytes.ReadUInt32LittleEndian(ref offset); - uint reservedOneValue = reserved653Bytes.ReadUInt32LittleEndian(ref offset); - var reservedZeroBytesFive = reserved653Bytes.ReadBytes(ref offset, 4); - var reservedDataBytesTwo = reserved653Bytes.ReadBytes(ref offset, 12); - byte reservedLowByteValueOne = reserved653Bytes.ReadByteValue(ref offset); - byte reservedLowByteValueTwo = reserved653Bytes.ReadByteValue(ref offset); - byte reservedLowByteValueThree = reserved653Bytes.ReadByteValue(ref offset); - byte reservedLowByteValueFour = reserved653Bytes.ReadByteValue(ref offset); - var reservedDataBytesThree = reserved653Bytes.ReadBytes(ref offset, 12); - - #endregion - - // True for all discs - if (!Array.TrueForAll(reservedZeroBytesOne, b => b == 0x00) - || !Array.TrueForAll(reservedZeroBytesTwo, b => b == 0x00) - || !Array.TrueForAll(reservedZeroBytesThree, b => b == 0x00) - || !Array.TrueForAll(reservedZeroBytesFour, b => b == 0x00) - || !Array.TrueForAll(reservedZeroBytesFive, b => b == 0x00)) - return null; - - #region Early SecuROM Checks - - // This duplicates a lot of code. This region is like this because it's still possible to detect early vers, - // but it should be easy to remove this section if it turns out this leads to conflicts or false positives - if (Array.TrueForAll(reserveDataBytesOne, b => b == 0x00) - && Array.TrueForAll(reservedDataBytesTwo, b => b == 0x00) - && reservedHundredValue == 0 && reservedOneValue == 0 - && reservedUintOne == 0 && reservedUintTwoLow == 0 && reservedUintThree == 0 && reservedUintFour == 0 - && reservedLowByteValueOne == 0 && reservedLowByteValueTwo == 0 && reservedLowByteValueThree == 0) - { - offset = 0; - - if (FileType.ISO9660.IsPureData(reservedDataBytesThree)) - if ( reservedLowByteValueFour == 0) - return "SecuROM 3.x-4.6x"; - else if (reservedLowByteValueFour < 0x20) - return "SecuROM 4.7x-4.8x"; - else - return null; - - var earlyFirstFourBytes = reservedDataBytesThree.ReadBytes(ref offset, 4); - var earlyLastEightBytes = reservedDataBytesThree.ReadBytes(ref offset, 8); - - if (Array.TrueForAll(earlyFirstFourBytes, b => b == 0x00) && FileType.ISO9660.IsPureData(earlyLastEightBytes)) - return "SecuROM 2.x-3.x"; - } - - #endregion - - // If this uint32 is 100, the next 80 bytes should be data. Otherwise, both should only ever be zero. - - switch(reservedHundredValue) - { - case 0: - if (!Array.TrueForAll(reserveDataBytesOne, b => b == 0x00)) - return null; - break; - case 100: - if (!FileType.ISO9660.IsPureData(reserveDataBytesOne)) - return null; - break; - default: - return null; - } - - //If you go back to early 4.0 CDs, only the above can be guaranteed to pass. CDs can already be identified via normal - //dumping, though, and (as well as most later CDs) should always pass these remaining checks. - if (reservedUintOne < 0xFFFF || reservedUintTwoLow > 0xFFFF || reservedUintThree < 0xFFFF || reservedUintFour < 0xFFFF) - return null; - - if (reservedOneValue != 1) - return null; - - if (reservedLowByteValueOne > 0x20 || reservedLowByteValueTwo > 0x20 || reservedLowByteValueThree > 0x20 || - reservedLowByteValueFour > 0x20) - return null; - - // TODO: RID 127715 fails this because the first 8 bytes of reservedDataBytesTwo happen to be "afsCafsC" - if (!FileType.ISO9660.IsPureData(reservedDataBytesTwo) || - !FileType.ISO9660.IsPureData(reservedDataBytesThree)) - return null; - - return "SecuROM 4.8x+"; - } - /// /// Try to get the SecuROM v4 version from the overlay, if possible /// @@ -430,7 +429,7 @@ namespace BinaryObjectScanner.Protection return null; // Format the version - string version = $"{addD.Version}.{addD.Build}"; + string version = $"{addD.Version}.{addD.Build}"; if (!char.IsNumber(version[0])) return "(very old, v3 or less)"; @@ -556,7 +555,7 @@ namespace BinaryObjectScanner.Protection /// private static string? CheckMatroschkaPackage(SecuROMMatroschkaPackage package, bool includeDebug) { - // Check for all 0x00 required, as at least one known non-RC matroschka has the field, just empty. + // Check for all 0x00 required, as at least one known non-RC matroschka has the field, just empty. if (package.KeyHexString == null || package.KeyHexString.Trim('\0').Length == 0) return "SecuROM Matroschka Package"; @@ -626,11 +625,11 @@ namespace BinaryObjectScanner.Protection // Fallback for PA if none of the above occur, in the case of companies that used their own modified PA // variants. PiD refers to this as "SecuROM Modified PA Module". - // Found in Redump entries 111997 (paul.dll) and 56373+56374 (AurParticleSystem.dll). The developers of + // Found in Redump entries 111997 (paul.dll) and 56373+56374 (AurParticleSystem.dll). The developers of // both, Softstar and Aurogon respectively(?), seem to have some connection, and use similar-looking - // modified PA. It probably has its own name like EA's GAM, but I don't currently know what that would be. + // modified PA. It probably has its own name like EA's GAM, but I don't currently know what that would be. // Regardless, even if these are given their own named variant later, this check should remain in order to - // catch other modified PA variants (this would have also caught EA GAM, for example) and to match PiD's + // catch other modified PA variants (this would have also caught EA GAM, for example) and to match PiD's // detection abilities. name = exe.ExportNameTable?.Strings?[0]; diff --git a/BinaryObjectScanner/Protection/StarForce.cs b/BinaryObjectScanner/Protection/StarForce.cs index 557581ae..1ec17df9 100644 --- a/BinaryObjectScanner/Protection/StarForce.cs +++ b/BinaryObjectScanner/Protection/StarForce.cs @@ -10,10 +10,91 @@ using SabreTools.Serialization.Wrappers; namespace BinaryObjectScanner.Protection { - public class StarForce : IExecutableCheck, IPathCheck, IDiskImageCheck + public class StarForce : IDiskImageCheck, IExecutableCheck, IPathCheck { + /// + /// If a StarForce Keyless hash is known to not fit the format, but is in some way a one-off. + /// Key is the SF Keyless Key, value is redump ID + /// + private static readonly Dictionary UnusualStarforceKeylessKeys = new() + { + {"FYFYILOVEYOU", 60270}, + }; + + /// + public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) + { + if (diskImage.VolumeDescriptorSet.Length == 0) + return null; + if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) + return null; + + // Starforce Keyless check #1: the reserved 653 bytes start with a 32-bit LE number that's slightly less + // than the length of the volume size space. The difference varies, it's usually around 10. Check 500 to be + // safe. The rest of the data is all 0x00. + if (FileType.ISO9660.NoteworthyApplicationUse(pvd)) + return null; + + if (!FileType.ISO9660.NoteworthyReserved653Bytes(pvd)) + return null; + + int offset = 0; + + var reserved653Bytes = pvd.Reserved653Bytes; + uint initialValue = reserved653Bytes.ReadUInt32LittleEndian(ref offset); + var zeroBytes = reserved653Bytes.ReadBytes(ref offset, 649); + + // It's unfortunately not known to be possible to detect many non-keyless StarForce discs, so some will slip + // through here. + if (initialValue > pvd.VolumeSpaceSize || initialValue + 500 < pvd.VolumeSpaceSize || !Array.TrueForAll(zeroBytes, b => b == 0x00)) + return null; + + offset = 0; + + // StarForce Keyless check #2: the key is stored in the Data Preparer identifier. + + // It turns out that some (i.e. Redump ID 60266, 72531, 87181, 91734, 106732, 105356, 74578, 78200) + // non-keyless StarForce discs still have this value here? This check may need to be disabled, but it + // seems to avoid any false positives in practice so far. + var dataPreparerIdentiferString = pvd.DataPreparerIdentifier.ReadNullTerminatedAnsiString(ref offset)?.Trim(); + if (dataPreparerIdentiferString == null || dataPreparerIdentiferString.Length == 0) + return "StarForce"; + + // It is returning the key, as it tells you what set of DPM your disc corresponds to, and it would also + // help show why a disc might be an alt of another disc (there are at least a decent amount of StarForce + // Keyless alts that would amtch otherwise). Unclear if this is desired by the users of BOS or those + // affected by it. + + // Thus far, the StarForce Keyless key is always made up of a number of characters, all either capital letters or + // numbers, sometimes with dashes in between. Thus far, 4 formats have been observed: + // XXXXXXXXXXXXXXXXXXXXXXXXX (25 characters) + // XXXXX-XXXXX-XXXXX-XXXXX-XXXXX (25 characters, plus 4 dashes seperating 5 groups of 5) + // XXXXXXXXXXXXXXXXXXXXXXXXXXXX (28 characters) + // XXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX (28 characters, with 4 dashes) + if (Regex.IsMatch(dataPreparerIdentiferString, "^[A-Z0-9]{25}$") + || Regex.IsMatch(dataPreparerIdentiferString, "^[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$") + || Regex.IsMatch(dataPreparerIdentiferString, "^[A-Z0-9]{28}$") + || Regex.IsMatch(dataPreparerIdentiferString, "^[A-Z0-9]{4}-[A-Z0-9]{6}-[A-Z0-9]{6}-[A-Z0-9]{6}-[A-Z0-9]{6}$")) + { + return $"StarForce Keyless - {dataPreparerIdentiferString}"; + } + + // Redump ID 60270 is a unique case, there could possibly be more. + if (UnusualStarforceKeylessKeys.ContainsKey(dataPreparerIdentiferString)) + return $"StarForce Keyless - {dataPreparerIdentiferString}"; + + // In case any variants were missed. + if (Regex.IsMatch(dataPreparerIdentiferString, "^[A-Z0-9-]*$")) + return $"StarForce Keyless - {dataPreparerIdentiferString} - Unknown variant, please report to us on GitHub!"; + + // 34206 reaches this because it's not keyless, and has "WinISO software" as the DPI string. However, since + // it has lowercase letters and spaces, it's caught here. It is genuinely StarForce, so it's not a false + // positive. + return "StarForce"; + } + // TODO: Bring up to par with PiD. - // Known issues: + // Known issues: // "Game.exe" not detected (Redump entry 96137). // "HR.exe" Themida not detected, doesn't detect "[Builder]" (Is that the default StarForce?) (Redump entry 94805). // "ChromeEngine3.dll" and "SGP4.dll" not detected, doesn't detect "[FL Disc]" (Redump entry 93098). @@ -58,9 +139,9 @@ namespace BinaryObjectScanner.Protection name = exe.TradeName; - // FrontLine ProActive (digital activation), samples: - // https://dbox.tools/titles/pc/46450FA4/ - // https://dbox.tools/titles/pc/4F430FA0/ + // FrontLine ProActive (digital activation), samples: + // https://dbox.tools/titles/pc/46450FA4/ + // https://dbox.tools/titles/pc/4F430FA0/ // https://dbox.tools/titles/pc/53450FA1/ name = exe.TradeName; if (name.OptionalContains("FL ProActive")) @@ -162,85 +243,5 @@ namespace BinaryObjectScanner.Protection // TODO: Determine if there are any file name checks that aren't too generic to use on their own. return null; } - - /// - public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) - { - if (diskImage.VolumeDescriptorSet.Length == 0) - return null; - - if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) - return null; - - // Starforce Keyless check #1: the reserved 653 bytes start with a 32-bit LE number that's slightly less - // than the length of the volume size space. The difference varies, it's usually around 10. Check 500 to be - // safe. The rest of the data is all 0x00. - if (FileType.ISO9660.NoteworthyApplicationUse(pvd)) - return null; - - if (!FileType.ISO9660.NoteworthyReserved653Bytes(pvd)) - return null; - - int offset = 0; - - var reserved653Bytes = pvd.Reserved653Bytes; - uint initialValue = reserved653Bytes.ReadUInt32LittleEndian(ref offset); - var zeroBytes = reserved653Bytes.ReadBytes(ref offset, 649); - - // It's unfortunately not known to be possible to detect many non-keyless StarForce discs, so some will slip - // through here. - if (initialValue > pvd.VolumeSpaceSize || initialValue + 500 < pvd.VolumeSpaceSize || !Array.TrueForAll(zeroBytes, b => b == 0x00)) - return null; - - offset = 0; - - // StarForce Keyless check #2: the key is stored in the Data Preparer identifier. - - // It turns out that some (i.e. Redump ID 60266, 72531, 87181, 91734, 106732, 105356, 74578, 78200) - // non-keyless StarForce discs still have this value here? This check may need to be disabled, but it - // seems to avoid any false positives in practice so far. - var dataPreparerIdentiferString = pvd.DataPreparerIdentifier.ReadNullTerminatedAnsiString(ref offset)?.Trim(); - if (dataPreparerIdentiferString == null || dataPreparerIdentiferString.Length == 0) - return "StarForce"; - - // It is returning the key, as it tells you what set of DPM your disc corresponds to, and it would also - // help show why a disc might be an alt of another disc (there are at least a decent amount of StarForce - // Keyless alts that would amtch otherwise). Unclear if this is desired by the users of BOS or those - // affected by it. - - // Thus far, the StarForce Keyless key is always made up of a number of characters, all either capital letters or - // numbers, sometimes with dashes in between. Thus far, 4 formats have been observed: - // XXXXXXXXXXXXXXXXXXXXXXXXX (25 characters) - // XXXXX-XXXXX-XXXXX-XXXXX-XXXXX (25 characters, plus 4 dashes seperating 5 groups of 5) - // XXXXXXXXXXXXXXXXXXXXXXXXXXXX (28 characters) - // XXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX (28 characters, with 4 dashes) - if (Regex.IsMatch(dataPreparerIdentiferString, "^[A-Z0-9]{25}$") - || Regex.IsMatch(dataPreparerIdentiferString, "^[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$") - || Regex.IsMatch(dataPreparerIdentiferString, "^[A-Z0-9]{28}$") - || Regex.IsMatch(dataPreparerIdentiferString, "^[A-Z0-9]{4}-[A-Z0-9]{6}-[A-Z0-9]{6}-[A-Z0-9]{6}-[A-Z0-9]{6}$")) - return $"StarForce Keyless - {dataPreparerIdentiferString}"; - - // Redump ID 60270 is a unique case, there could possibly be more. - if (UnusualStarforceKeylessKeys.ContainsKey(dataPreparerIdentiferString)) - return $"StarForce Keyless - {dataPreparerIdentiferString}"; - - // In case any variants were missed. - if (Regex.IsMatch(dataPreparerIdentiferString, "^[A-Z0-9-]*$")) - return $"StarForce Keyless - {dataPreparerIdentiferString} - Unknown variant, please report to us on GitHub!"; - - // 34206 reaches this because it's not keyless, and has "WinISO software" as the DPI string. However, since - // it has lowercase letters and spaces, it's caught here. It is genuinely StarForce, so it's not a false - // positive. - return $"StarForce"; - } - - /// - /// If a StarForce Keyless hash is known to not fit the format, but is in some way a one-off. - /// Key is the SF Keyless Key, value is redump ID - /// - private static readonly Dictionary UnusualStarforceKeylessKeys = new() - { - {"FYFYILOVEYOU", 60270}, - }; } } diff --git a/BinaryObjectScanner/Protection/Tages.cs b/BinaryObjectScanner/Protection/Tages.cs index 9231d2f4..b6d1081e 100644 --- a/BinaryObjectScanner/Protection/Tages.cs +++ b/BinaryObjectScanner/Protection/Tages.cs @@ -10,8 +10,48 @@ using SabreTools.Serialization.Wrappers; namespace BinaryObjectScanner.Protection { - public class TAGES : IExecutableCheck, IPathCheck, IDiskImageCheck + public class TAGES : IDiskImageCheck, IExecutableCheck, IPathCheck { + /// + public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) + { + if (diskImage.VolumeDescriptorSet.Length == 0) + return null; + if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) + return null; + + // There needs to be noteworthy application use data. + if (!FileType.ISO9660.NoteworthyApplicationUse(pvd)) + return null; + + // There should not be noteworthy data in the reserved 653 bytes + if (FileType.ISO9660.NoteworthyReserved653Bytes(pvd)) + return null; + + var applicationUse = pvd.ApplicationUse; + + // Non-early tages either has all 512 bytes of the AU data full of data, or only the last 128. + if (FileType.ISO9660.IsPureData(applicationUse)) + return "TAGES"; + + int offset = 0; + var optionalZeroBytes = applicationUse.ReadBytes(ref offset, 384); + var tagesBytes = applicationUse.ReadBytes(ref offset, 128); + if (Array.TrueForAll(optionalZeroBytes, b => b == 0x00) && FileType.ISO9660.IsPureData(tagesBytes)) + return "TAGES"; + + // Early tages has a 4-byte value at the beginning of the AU data and nothing else. + // Redump ID 8776, 21321, 35932 + offset = 0; + uint earlyTagesBytes = applicationUse.ReadUInt32LittleEndian(ref offset); + var zeroBytes = applicationUse.ReadBytes(ref offset, 508); + if (Array.TrueForAll(zeroBytes, b => b == 0x00) && earlyTagesBytes != 0) + return "TAGES (Early)"; + + // The original releases of Moto Racer 3 (31578, 34669) are so early they have seemingly nothing identifiable. + return null; + } + /// public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug) { @@ -213,51 +253,6 @@ namespace BinaryObjectScanner.Protection return MatchUtil.GetFirstMatch(path, matchers, any: true); } - /// - public string? CheckDiskImage(string file, ISO9660 diskImage, bool includeDebug) - { - #region Initial Checks - - if (diskImage.VolumeDescriptorSet.Length == 0) - return null; - - if (diskImage.VolumeDescriptorSet[0] is not PrimaryVolumeDescriptor pvd) - return null; - - // There needs to be noteworthy application use data. - if (!FileType.ISO9660.NoteworthyApplicationUse(pvd)) - return null; - - // There should not be noteworthy data in the reserved 653 bytes - if (FileType.ISO9660.NoteworthyReserved653Bytes(pvd)) - return null; - - #endregion - - var applicationUse = pvd.ApplicationUse; - - // Non-early tages either has all 512 bytes of the AU data full of data, or only the last 128. - if (FileType.ISO9660.IsPureData(applicationUse)) - return "TAGES"; - - int offset = 0; - var optionalZeroBytes = applicationUse.ReadBytes(ref offset, 384); - var tagesBytes = applicationUse.ReadBytes(ref offset, 128); - if (Array.TrueForAll(optionalZeroBytes, b => b == 0x00) && FileType.ISO9660.IsPureData(tagesBytes)) - return "TAGES"; - - // Early tages has a 4-byte value at the beginning of the AU data and nothing else. - // Redump ID 8776, 21321, 35932 - offset = 0; - uint earlyTagesBytes = applicationUse.ReadUInt32LittleEndian(ref offset); - var zeroBytes = applicationUse.ReadBytes(ref offset, 508); - if (Array.TrueForAll(zeroBytes, b => b == 0x00) && earlyTagesBytes != 0) - return "TAGES (Early)"; - - // The original releases of Moto Racer 3 (31578, 34669) are so early they have seemingly nothing identifiable. - return null; - } - private static string GetVersion(PortableExecutable exe) { // Check the internal versions diff --git a/BinaryObjectScanner/Scanner.cs b/BinaryObjectScanner/Scanner.cs index 02419c39..f72738ff 100644 --- a/BinaryObjectScanner/Scanner.cs +++ b/BinaryObjectScanner/Scanner.cs @@ -5,7 +5,6 @@ using BinaryObjectScanner.Data; using BinaryObjectScanner.Interfaces; using SabreTools.IO.Extensions; using SabreTools.Serialization; -using SabreTools.Serialization.Interfaces; using SabreTools.Serialization.Wrappers; namespace BinaryObjectScanner