diff --git a/BinaryObjectScanner.Test/BinaryObjectScanner.Test.csproj b/BinaryObjectScanner.Test/BinaryObjectScanner.Test.csproj index b009ea13..f8de8ec1 100644 --- a/BinaryObjectScanner.Test/BinaryObjectScanner.Test.csproj +++ b/BinaryObjectScanner.Test/BinaryObjectScanner.Test.csproj @@ -16,10 +16,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/BinaryObjectScanner.Test/FileType/DiskImageTests.cs b/BinaryObjectScanner.Test/FileType/DiskImageTests.cs new file mode 100644 index 00000000..8fa8b956 --- /dev/null +++ b/BinaryObjectScanner.Test/FileType/DiskImageTests.cs @@ -0,0 +1,34 @@ +using System.IO; +using BinaryObjectScanner.FileType; +using Xunit; + +namespace BinaryObjectScanner.Test.FileType +{ + public class DiskImageTests + { + private static readonly SabreTools.Serialization.Wrappers.ISO9660 wrapper + = new(new SabreTools.Data.Models.ISO9660.Volume(), new MemoryStream(new byte[1024])); + + [Fact] + public void DetectFile_EmptyString_Null() + { + string file = string.Empty; + var detectable = new ISO9660(wrapper); + + string? actual = detectable.Detect(file, includeDebug: false); + Assert.Null(actual); + } + + [Fact] + public void DetectStream_EmptyStream_Empty() + { + Stream? stream = new MemoryStream(); + string file = string.Empty; + var detectable = new ISO9660(wrapper); + + string? actual = detectable.Detect(stream, file, includeDebug: false); + Assert.NotNull(actual); + Assert.Empty(actual); + } + } +} \ No newline at end of file diff --git a/BinaryObjectScanner.Test/Protection/AlphaROMTests.cs b/BinaryObjectScanner.Test/Protection/AlphaROMTests.cs index 09ebe17f..e43e04af 100644 --- a/BinaryObjectScanner.Test/Protection/AlphaROMTests.cs +++ b/BinaryObjectScanner.Test/Protection/AlphaROMTests.cs @@ -18,5 +18,18 @@ public void CheckPortableExecutableTest() string? actual = checker.CheckExecutable(file, exe, includeDebug: false); Assert.Null(actual); } + + [Fact] + public void CheckDiskImageTest() + { + string file = "filename"; + SabreTools.Data.Models.ISO9660.Volume model = new(); + Stream source = new MemoryStream(new byte[1024]); + SabreTools.Serialization.Wrappers.ISO9660 iso = new(model, source); + + var checker = new AlphaROM(); + string? actual = checker.CheckDiskImage(file, iso, includeDebug: false); + Assert.Null(actual); + } } } \ No newline at end of file diff --git a/BinaryObjectScanner.Test/Protection/CopyLokTests.cs b/BinaryObjectScanner.Test/Protection/CopyLokTests.cs index 1fd1cd9b..a1ed7165 100644 --- a/BinaryObjectScanner.Test/Protection/CopyLokTests.cs +++ b/BinaryObjectScanner.Test/Protection/CopyLokTests.cs @@ -18,5 +18,18 @@ public void CheckPortableExecutableTest() string? actual = checker.CheckExecutable(file, exe, includeDebug: false); Assert.Null(actual); } + + [Fact] + public void CheckDiskImageTest() + { + string file = "filename"; + SabreTools.Data.Models.ISO9660.Volume model = new(); + Stream source = new MemoryStream(new byte[1024]); + SabreTools.Serialization.Wrappers.ISO9660 iso = new(model, source); + + var checker = new CopyLok(); + string? actual = checker.CheckDiskImage(file, iso, includeDebug: false); + Assert.Null(actual); + } } } \ No newline at end of file diff --git a/BinaryObjectScanner.Test/Protection/LaserLokTests.cs b/BinaryObjectScanner.Test/Protection/LaserLokTests.cs index 2f83e0fe..e324b595 100644 --- a/BinaryObjectScanner.Test/Protection/LaserLokTests.cs +++ b/BinaryObjectScanner.Test/Protection/LaserLokTests.cs @@ -40,5 +40,18 @@ public void CheckFilePathTest() string? actual = checker.CheckFilePath(path); Assert.Null(actual); } + + [Fact] + public void CheckDiskImageTest() + { + string file = "filename"; + SabreTools.Data.Models.ISO9660.Volume model = new(); + Stream source = new MemoryStream(new byte[1024]); + SabreTools.Serialization.Wrappers.ISO9660 iso = new(model, source); + + var checker = new LaserLok(); + string? actual = checker.CheckDiskImage(file, iso, includeDebug: false); + Assert.Null(actual); + } } } \ No newline at end of file diff --git a/BinaryObjectScanner.Test/Protection/MacrovisionTests.cs b/BinaryObjectScanner.Test/Protection/MacrovisionTests.cs index 81f18d6d..5b9ab0ec 100644 --- a/BinaryObjectScanner.Test/Protection/MacrovisionTests.cs +++ b/BinaryObjectScanner.Test/Protection/MacrovisionTests.cs @@ -53,5 +53,18 @@ public void CheckFilePathTest() string? actual = checker.CheckFilePath(path); Assert.Null(actual); } + + [Fact] + public void CheckDiskImageTest() + { + string file = "filename"; + SabreTools.Data.Models.ISO9660.Volume model = new(); + Stream source = new MemoryStream(new byte[1024]); + SabreTools.Serialization.Wrappers.ISO9660 iso = new(model, source); + + var checker = new Macrovision(); + string? actual = checker.CheckDiskImage(file, iso, includeDebug: false); + Assert.Null(actual); + } } } \ No newline at end of file diff --git a/BinaryObjectScanner.Test/Protection/ProtectDiscTests.cs b/BinaryObjectScanner.Test/Protection/ProtectDiscTests.cs index acc140eb..31c2a5b6 100644 --- a/BinaryObjectScanner.Test/Protection/ProtectDiscTests.cs +++ b/BinaryObjectScanner.Test/Protection/ProtectDiscTests.cs @@ -40,5 +40,18 @@ public void CheckFilePathTest() string? actual = checker.CheckFilePath(path); Assert.Null(actual); } + + [Fact] + public void CheckDiskImageTest() + { + string file = "filename"; + SabreTools.Data.Models.ISO9660.Volume model = new(); + Stream source = new MemoryStream(new byte[1024]); + SabreTools.Serialization.Wrappers.ISO9660 iso = new(model, source); + + var checker = new ProtectDISC(); + string? actual = checker.CheckDiskImage(file, iso, includeDebug: false); + Assert.Null(actual); + } } } \ No newline at end of file diff --git a/BinaryObjectScanner.Test/Protection/SecuROMTests.cs b/BinaryObjectScanner.Test/Protection/SecuROMTests.cs index f2e3c716..834b4166 100644 --- a/BinaryObjectScanner.Test/Protection/SecuROMTests.cs +++ b/BinaryObjectScanner.Test/Protection/SecuROMTests.cs @@ -40,5 +40,18 @@ public void CheckFilePathTest() string? actual = checker.CheckFilePath(path); Assert.Null(actual); } + + [Fact] + public void CheckDiskImageTest() + { + string file = "filename"; + SabreTools.Data.Models.ISO9660.Volume model = new(); + Stream source = new MemoryStream(new byte[1024]); + SabreTools.Serialization.Wrappers.ISO9660 iso = new(model, source); + + var checker = new SecuROM(); + string? actual = checker.CheckDiskImage(file, iso, includeDebug: false); + Assert.Null(actual); + } } } \ No newline at end of file diff --git a/BinaryObjectScanner.Test/Protection/StarForceTests.cs b/BinaryObjectScanner.Test/Protection/StarForceTests.cs index 3f0afa4d..518071ae 100644 --- a/BinaryObjectScanner.Test/Protection/StarForceTests.cs +++ b/BinaryObjectScanner.Test/Protection/StarForceTests.cs @@ -40,5 +40,18 @@ public void CheckFilePathTest() string? actual = checker.CheckFilePath(path); Assert.Null(actual); } + + [Fact] + public void CheckDiskImageTest() + { + string file = "filename"; + SabreTools.Data.Models.ISO9660.Volume model = new(); + Stream source = new MemoryStream(new byte[1024]); + SabreTools.Serialization.Wrappers.ISO9660 iso = new(model, source); + + var checker = new StarForce(); + string? actual = checker.CheckDiskImage(file, iso, includeDebug: false); + Assert.Null(actual); + } } } \ No newline at end of file diff --git a/BinaryObjectScanner.Test/Protection/TAGESTests.cs b/BinaryObjectScanner.Test/Protection/TAGESTests.cs index b5866503..49c004a5 100644 --- a/BinaryObjectScanner.Test/Protection/TAGESTests.cs +++ b/BinaryObjectScanner.Test/Protection/TAGESTests.cs @@ -40,5 +40,18 @@ public void CheckFilePathTest() string? actual = checker.CheckFilePath(path); Assert.Null(actual); } + + [Fact] + public void CheckDiskImageTest() + { + string file = "filename"; + SabreTools.Data.Models.ISO9660.Volume model = new(); + Stream source = new MemoryStream(new byte[1024]); + SabreTools.Serialization.Wrappers.ISO9660 iso = new(model, source); + + var checker = new TAGES(); + string? actual = checker.CheckDiskImage(file, iso, includeDebug: false); + Assert.Null(actual); + } } } \ No newline at end of file diff --git a/BinaryObjectScanner/BinaryObjectScanner.csproj b/BinaryObjectScanner/BinaryObjectScanner.csproj index ae28bd85..b37dd513 100644 --- a/BinaryObjectScanner/BinaryObjectScanner.csproj +++ b/BinaryObjectScanner/BinaryObjectScanner.csproj @@ -49,9 +49,9 @@ - - - + + + \ No newline at end of file diff --git a/BinaryObjectScanner/Data/StaticChecks.cs b/BinaryObjectScanner/Data/StaticChecks.cs index 9b3e2f25..2368ef25 100644 --- a/BinaryObjectScanner/Data/StaticChecks.cs +++ b/BinaryObjectScanner/Data/StaticChecks.cs @@ -22,6 +22,18 @@ public static IContentCheck[] ContentCheckClasses } } + /// + /// Cache for all IISOCheck types + /// + public static IDiskImageCheck[] ISO9660CheckClasses + { + get + { + iso9660CheckClasses ??= InitCheckClasses>(); + return iso9660CheckClasses; + } + } + /// /// Cache for all IExecutableCheck types /// @@ -91,6 +103,12 @@ public static IExecutableCheck[] PortableExecutableCheckClas /// private static IContentCheck[]? contentCheckClasses; + + /// + /// Cache for all IISOCheck types + /// + private static IDiskImageCheck[]? iso9660CheckClasses; + /// /// Cache for all IExecutableCheck types /// diff --git a/BinaryObjectScanner/FileType/DiskImage.cs b/BinaryObjectScanner/FileType/DiskImage.cs new file mode 100644 index 00000000..9f701b4a --- /dev/null +++ b/BinaryObjectScanner/FileType/DiskImage.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.IO; +using BinaryObjectScanner.Data; +using BinaryObjectScanner.Interfaces; +using SabreTools.IO.Extensions; +using SabreTools.Serialization.Wrappers; + +namespace BinaryObjectScanner.FileType +{ + /// + /// Disk image file + /// + public abstract class DiskImage : DetectableBase + where T : WrapperBase + { + /// + public DiskImage(T wrapper) : base(wrapper) { } + + #region Check Runners + + /// + /// Handle a single file based on all ISO check implementations + /// + /// Name of the source file of the ISO, 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) + where U : IDiskImageCheck + { + // Create the output dictionary + var protections = new CheckDictionary(); + + // Iterate through all checks + checks.IterateWithAction(checkClass => + { + // Get the protection for the class, if possible + var protection = checkClass.CheckDiskImage(file, _wrapper, includeDebug); + if (string.IsNullOrEmpty(protection)) + return; + + protections.Append(checkClass, protection); + }); + + return protections; + } + + #endregion + } +} diff --git a/BinaryObjectScanner/FileType/Executable.cs b/BinaryObjectScanner/FileType/Executable.cs index f549c75a..ab2acb96 100644 --- a/BinaryObjectScanner/FileType/Executable.cs +++ b/BinaryObjectScanner/FileType/Executable.cs @@ -76,7 +76,6 @@ protected IDictionary RunContentChecks(string? file, Stre /// Name of the source file of the executable, for tracking /// Executable to scan /// Set of checks to use - /// Scanner for handling recursive protections /// True to include debug data, false otherwise /// Set of protections in file, empty on error protected IDictionary RunExecutableChecks(string file, T exe, U[] checks, bool includeDebug) diff --git a/BinaryObjectScanner/FileType/ISO9660.cs b/BinaryObjectScanner/FileType/ISO9660.cs new file mode 100644 index 00000000..1160da73 --- /dev/null +++ b/BinaryObjectScanner/FileType/ISO9660.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using BinaryObjectScanner.Data; +using SabreTools.Data.Models.ISO9660; +using SabreTools.IO.Extensions; + +namespace BinaryObjectScanner.FileType +{ + /// + /// ISO9660 + /// + public class ISO9660 : DiskImage + { + /// + public ISO9660(SabreTools.Serialization.Wrappers.ISO9660 wrapper) : base(wrapper) { } + + /// + public override string? Detect(Stream stream, string file, bool includeDebug) + { + // Create the output dictionary + var protections = new ProtectionDictionary(); + + // Standard checks + var subProtections + = RunISOChecks(file, StaticChecks.ISO9660CheckClasses, includeDebug); + protections.Append(file, subProtections.Values); + + // If there are no protections + if (protections.Count == 0) + return null; + + // Create the internal list + var protectionList = new List(); + foreach (string key in protections.Keys) + { + protectionList.AddRange(protections[key]); + } + + return string.Join(";", [.. protectionList]); + } + + /// + /// 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) + { + if (bytes[i] == containedZeroes[index]) + { + if (++index >= containedZeroes.Length) + return false; + } + else + { + 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 -'!,.]"); + 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. + /// + 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 + { + if (potentialAppUseString.StartsWith("ImgBurn")) + return false; + else if (potentialAppUseString.StartsWith("ULTRAISO")) + return false; + else if (potentialAppUseString.StartsWith("Rimage")) + 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; + + return true; + } + + /// + /// Checks whether the Reserved 653 Bytes are "noteworthy" enough to be worth checking for protection. + /// + public static bool NoteworthyReserved653Bytes(PrimaryVolumeDescriptor pvd) + { + var reserved653Bytes = pvd.Reserved653Bytes; + 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 new file mode 100644 index 00000000..6488a4ee --- /dev/null +++ b/BinaryObjectScanner/Interfaces/IDiskImageCheck.cs @@ -0,0 +1,19 @@ +using SabreTools.Serialization.Wrappers; + +namespace BinaryObjectScanner.Interfaces +{ + /// + /// Check a disk image for protection + /// + public interface IDiskImageCheck where T : WrapperBase + { + /// + /// Check a path for protections based on file contents + /// + /// File to check for protection indicators + /// + /// 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 deda8920..df20d4ae 100644 --- a/BinaryObjectScanner/Protection/AlphaROM.cs +++ b/BinaryObjectScanner/Protection/AlphaROM.cs @@ -1,4 +1,8 @@ -using BinaryObjectScanner.Interfaces; +using System; +using System.Text.RegularExpressions; +using BinaryObjectScanner.Interfaces; +using SabreTools.Data.Models.ISO9660; +using SabreTools.IO.Extensions; using SabreTools.Serialization.Wrappers; namespace BinaryObjectScanner.Protection @@ -39,7 +43,7 @@ namespace BinaryObjectScanner.Protection // - SETTEC0000SETTEC1111 // - SOFTWARE\SETTEC // TODO: Are there version numbers? - public class AlphaROM : IExecutableCheck + public class AlphaROM : IExecutableCheck, IDiskImageCheck { /// public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug) @@ -84,5 +88,51 @@ public class AlphaROM : IExecutableCheck 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 972e9871..ce0c6d43 100644 --- a/BinaryObjectScanner/Protection/CopyLok.cs +++ b/BinaryObjectScanner/Protection/CopyLok.cs @@ -1,5 +1,7 @@ using System; using BinaryObjectScanner.Interfaces; +using SabreTools.Data.Models.ISO9660; +using SabreTools.IO.Extensions; using SabreTools.Serialization.Wrappers; namespace BinaryObjectScanner.Protection @@ -20,7 +22,7 @@ namespace BinaryObjectScanner.Protection /// /// COPYLOK trademark: https://www.trademarkelite.com/europe/trademark/trademark-detail/000618512/COPYLOK. /// - public class CopyLok : IExecutableCheck + public class CopyLok : IExecutableCheck, IDiskImageCheck { /// public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug) @@ -34,5 +36,91 @@ public class CopyLok : IExecutableCheck 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 1b5ea35e..c96ddbe6 100644 --- a/BinaryObjectScanner/Protection/LaserLok.cs +++ b/BinaryObjectScanner/Protection/LaserLok.cs @@ -3,6 +3,7 @@ using System.IO; using System.Text; using BinaryObjectScanner.Interfaces; +using SabreTools.Data.Models.ISO9660; using SabreTools.IO; using SabreTools.IO.Extensions; using SabreTools.IO.Matching; @@ -10,7 +11,7 @@ namespace BinaryObjectScanner.Protection { - public class LaserLok : IExecutableCheck, IPathCheck + public class LaserLok : IExecutableCheck, IPathCheck, IDiskImageCheck { /// public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug) @@ -150,6 +151,48 @@ public List CheckDirectoryPath(string path, List? files) 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 175c34d6..cea646ed 100644 --- a/BinaryObjectScanner/Protection/Macrovision.cs +++ b/BinaryObjectScanner/Protection/Macrovision.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using BinaryObjectScanner.Interfaces; +using SabreTools.Data.Models.ISO9660; using SabreTools.IO; using SabreTools.IO.Extensions; using SabreTools.IO.Matching; @@ -16,7 +17,7 @@ namespace BinaryObjectScanner.Protection /// 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 + public partial class Macrovision : IExecutableCheck, IExecutableCheck, IPathCheck, IDiskImageCheck { /// public string? CheckExecutable(string file, NewExecutable exe, bool includeDebug) @@ -243,6 +244,77 @@ public List CheckDirectoryPath(string path, List? files) 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) { diff --git a/BinaryObjectScanner/Protection/ProtectDISC.cs b/BinaryObjectScanner/Protection/ProtectDISC.cs index 1f969a27..a6a591cf 100644 --- a/BinaryObjectScanner/Protection/ProtectDISC.cs +++ b/BinaryObjectScanner/Protection/ProtectDISC.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Text; +using System.Text.RegularExpressions; using BinaryObjectScanner.Interfaces; +using SabreTools.Data.Models.ISO9660; using SabreTools.IO; using SabreTools.IO.Extensions; using SabreTools.IO.Matching; @@ -11,7 +13,7 @@ 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 + public class ProtectDISC : IExecutableCheck, IPathCheck, IDiskImageCheck { /// public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug) @@ -133,6 +135,45 @@ public List CheckDirectoryPath(string path, List? files) 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 685f7bad..952d29f7 100644 --- a/BinaryObjectScanner/Protection/SecuROM.cs +++ b/BinaryObjectScanner/Protection/SecuROM.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; using BinaryObjectScanner.Interfaces; +using SabreTools.Data.Models.ISO9660; using SabreTools.IO; using SabreTools.IO.Extensions; using SabreTools.IO.Matching; @@ -12,7 +13,7 @@ namespace BinaryObjectScanner.Protection { // TODO: Investigate SecuROM for Macintosh // TODO: Think of a way to detect dfe - public class SecuROM : IExecutableCheck, IPathCheck + public class SecuROM : IExecutableCheck, IPathCheck, IDiskImageCheck { /// /// Matches hash of the Release Control-encrypted executable to known hashes @@ -253,6 +254,142 @@ public List CheckDirectoryPath(string path, List? files) 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 /// diff --git a/BinaryObjectScanner/Protection/StarForce.cs b/BinaryObjectScanner/Protection/StarForce.cs index f7a88654..557581ae 100644 --- a/BinaryObjectScanner/Protection/StarForce.cs +++ b/BinaryObjectScanner/Protection/StarForce.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; using BinaryObjectScanner.Interfaces; +using SabreTools.Data.Models.ISO9660; using SabreTools.IO; using SabreTools.IO.Extensions; using SabreTools.IO.Matching; @@ -8,7 +10,7 @@ namespace BinaryObjectScanner.Protection { - public class StarForce : IExecutableCheck, IPathCheck + public class StarForce : IExecutableCheck, IPathCheck, IDiskImageCheck { // TODO: Bring up to par with PiD. // Known issues: @@ -160,5 +162,85 @@ public List CheckDirectoryPath(string path, List? files) // 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 0ec99056..9231d2f4 100644 --- a/BinaryObjectScanner/Protection/Tages.cs +++ b/BinaryObjectScanner/Protection/Tages.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using BinaryObjectScanner.Interfaces; +using SabreTools.Data.Models.ISO9660; using SabreTools.IO; using SabreTools.IO.Extensions; using SabreTools.IO.Matching; @@ -9,7 +10,7 @@ namespace BinaryObjectScanner.Protection { - public class TAGES : IExecutableCheck, IPathCheck + public class TAGES : IExecutableCheck, IPathCheck, IDiskImageCheck { /// public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug) @@ -212,6 +213,51 @@ public List CheckDirectoryPath(string path, List? files) 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 137b7340..02419c39 100644 --- a/BinaryObjectScanner/Scanner.cs +++ b/BinaryObjectScanner/Scanner.cs @@ -478,6 +478,7 @@ private static List PerformPathCheck(IPathCheck impl, string? path, List { case AACSMediaKeyBlock obj: return new FileType.AACSMediaKeyBlock(obj); case BDPlusSVM obj: return new FileType.BDPlusSVM(obj); + case ISO9660 obj: return new FileType.ISO9660(obj); case LDSCRYPT obj: return new FileType.LDSCRYPT(obj); case LinearExecutable obj: return new FileType.LinearExecutable(obj); case MSDOS obj: return new FileType.MSDOS(obj); diff --git a/ProtectionScan/ProtectionScan.csproj b/ProtectionScan/ProtectionScan.csproj index c870b64d..3da7baee 100644 --- a/ProtectionScan/ProtectionScan.csproj +++ b/ProtectionScan/ProtectionScan.csproj @@ -33,7 +33,7 @@ - + \ No newline at end of file