Initial ISO Protection Detection (#389)

* Initial

* Laserlock in

* This is a better way to read the string

* That array copy wasn't needed either

* Use static filetype method, rename filetype.iso

* Initial Codelok ISO scanning

* Comments with redump IDs

* Add redump examples to laserlock

* Change for testing

* Small comment

* TAGES

* halfway through safedisc

* Safedisc done

* Fix 1

* Major oversights in puredata fixed

* Finish SecuROM

* ProtectDiSC done

* Alpharom done

* Finish StarForce, initial PR review ready

* Wait, that would be really bad

* One more for the road.

* Small finding

* Small fix for finding

* Notes on finding

* Several minor fixes, decisions

* what do you MEAN it returns true if there are no elements in the array

* Future todo

* Update packages

* Rebase

* Fix runisochecks

* First round of fixes

* Second round of fixes

* Tests attempt 1

* Make checks work

* Individual test attempt 1

* Final tests

---------

Co-authored-by: Matt Nadareski <mnadareski@outlook.com>
This commit is contained in:
HeroponRikiBestest
2025-11-07 07:22:28 -05:00
committed by GitHub
parent 9f5b292035
commit a281e8325e
22 changed files with 925 additions and 9 deletions

View File

@@ -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);
}
}
}

View File

@@ -18,5 +18,18 @@ namespace BinaryObjectScanner.Test.Protection
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);
}
}
}

View File

@@ -18,5 +18,18 @@ namespace BinaryObjectScanner.Test.Protection
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);
}
}
}

View File

@@ -40,5 +40,18 @@ namespace BinaryObjectScanner.Test.Protection
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);
}
}
}

View File

@@ -53,5 +53,18 @@ namespace BinaryObjectScanner.Test.Protection
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);
}
}
}

View File

@@ -40,5 +40,18 @@ namespace BinaryObjectScanner.Test.Protection
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);
}
}
}

View File

@@ -40,5 +40,18 @@ namespace BinaryObjectScanner.Test.Protection
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);
}
}
}

View File

@@ -40,5 +40,18 @@ namespace BinaryObjectScanner.Test.Protection
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);
}
}
}

View File

@@ -40,5 +40,18 @@ namespace BinaryObjectScanner.Test.Protection
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);
}
}
}

View File

@@ -22,6 +22,18 @@ namespace BinaryObjectScanner.Data
}
}
/// <summary>
/// Cache for all IISOCheck<ISO> types
/// </summary>
public static IDiskImageCheck<ISO9660>[] ISO9660CheckClasses
{
get
{
iso9660CheckClasses ??= InitCheckClasses<IDiskImageCheck<ISO9660>>();
return iso9660CheckClasses;
}
}
/// <summary>
/// Cache for all IExecutableCheck<LinearExecutable> types
/// </summary>
@@ -91,6 +103,12 @@ namespace BinaryObjectScanner.Data
/// </summary>
private static IContentCheck[]? contentCheckClasses;
/// <summary>
/// Cache for all IISOCheck<ISO9660> types
/// </summary>
private static IDiskImageCheck<ISO9660>[]? iso9660CheckClasses;
/// <summary>
/// Cache for all IExecutableCheck<LinearExecutable> types
/// </summary>

View File

@@ -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
{
/// <summary>
/// Disk image file
/// </summary>
public abstract class DiskImage<T> : DetectableBase<T>
where T : WrapperBase
{
/// <inheritdoc/>
public DiskImage(T wrapper) : base(wrapper) { }
#region Check Runners
/// <summary>
/// Handle a single file based on all ISO check implementations
/// </summary>
/// <param name="file">Name of the source file of the ISO, for tracking</param>
/// <param name="checks">Set of checks to use</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>Set of protections in file, empty on error</returns>
protected IDictionary<U, string> RunISOChecks<U>(string file, U[] checks, bool includeDebug)
where U : IDiskImageCheck<T>
{
// Create the output dictionary
var protections = new CheckDictionary<U>();
// 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
}
}

View File

@@ -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
{
/// <summary>
/// ISO9660
/// </summary>
public class ISO9660 : DiskImage<SabreTools.Serialization.Wrappers.ISO9660>
{
/// <inheritdoc/>
public ISO9660(SabreTools.Serialization.Wrappers.ISO9660 wrapper) : base(wrapper) { }
/// <inheritdoc/>
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<string>();
foreach (string key in protections.Keys)
{
protectionList.AddRange(protections[key]);
}
return string.Join(";", [.. protectionList]);
}
/// <summary>
/// Checks whether the sequence of bytes is pure data (as in, not empty, not text, just high-entropy data)
/// </summary>
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?
/// <summary>
/// Checks whether the Application Use data is "noteworthy" enough to be worth checking for protection.
/// </summary>
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;
}
/// <summary>
/// Checks whether the Reserved 653 Bytes are "noteworthy" enough to be worth checking for protection.
/// </summary>
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;
}
}
}

View File

@@ -0,0 +1,19 @@
using SabreTools.Serialization.Wrappers;
namespace BinaryObjectScanner.Interfaces
{
/// <summary>
/// Check a disk image for protection
/// </summary>
public interface IDiskImageCheck<T> where T : WrapperBase
{
/// <summary>
/// Check a path for protections based on file contents
/// </summary>
/// <param name="file">File to check for protection indicators</param>
/// <param name="diskImage"></param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>String containing any protections found in the file</returns>
string? CheckDiskImage(string file, T diskImage, bool includeDebug);
}
}

View File

@@ -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<PortableExecutable>
public class AlphaROM : IExecutableCheck<PortableExecutable>, IDiskImageCheck<ISO9660>
{
/// <inheritdoc/>
public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug)
@@ -84,5 +88,51 @@ namespace BinaryObjectScanner.Protection
return null;
}
/// <inheritdoc/>
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";
}
}
}

View File

@@ -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.
/// </summary>
public class CopyLok : IExecutableCheck<PortableExecutable>
public class CopyLok : IExecutableCheck<PortableExecutable>, IDiskImageCheck<ISO9660>
{
/// <inheritdoc/>
public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug)
@@ -34,5 +36,91 @@ namespace BinaryObjectScanner.Protection
return null;
}
/// <inheritdoc/>
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!";
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
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 @@ using SabreTools.Serialization.Wrappers;
namespace BinaryObjectScanner.Protection
{
public class LaserLok : IExecutableCheck<PortableExecutable>, IPathCheck
public class LaserLok : IExecutableCheck<PortableExecutable>, IPathCheck, IDiskImageCheck<ISO9660>
{
/// <inheritdoc/>
public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug)
@@ -150,6 +151,48 @@ namespace BinaryObjectScanner.Protection
return MatchUtil.GetFirstMatch(path, matchers, any: true);
}
/// <inheritdoc/>
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)

View File

@@ -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
/// </summary>
public partial class Macrovision : IExecutableCheck<NewExecutable>, IExecutableCheck<PortableExecutable>, IPathCheck
public partial class Macrovision : IExecutableCheck<NewExecutable>, IExecutableCheck<PortableExecutable>, IPathCheck, IDiskImageCheck<ISO9660>
{
/// <inheritdoc/>
public string? CheckExecutable(string file, NewExecutable exe, bool includeDebug)
@@ -243,6 +244,77 @@ namespace BinaryObjectScanner.Protection
return null;
}
/// <inheritdoc/>
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";
}
/// <inheritdoc cref="IPathCheck.CheckDirectoryPath(string, List{string})"/>
internal static List<string> MacrovisionCheckDirectoryPath(string path, List<string>? files)
{

View File

@@ -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<PortableExecutable>, IPathCheck
public class ProtectDISC : IExecutableCheck<PortableExecutable>, IPathCheck, IDiskImageCheck<ISO9660>
{
/// <inheritdoc/>
public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug)
@@ -133,6 +135,45 @@ namespace BinaryObjectScanner.Protection
return MatchUtil.GetFirstMatch(path, matchers, any: true);
}
/// <inheritdoc/>
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

View File

@@ -2,6 +2,7 @@ using System;
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<PortableExecutable>, IPathCheck
public class SecuROM : IExecutableCheck<PortableExecutable>, IPathCheck, IDiskImageCheck<ISO9660>
{
/// <summary>
/// Matches hash of the Release Control-encrypted executable to known hashes
@@ -253,6 +254,142 @@ namespace BinaryObjectScanner.Protection
return MatchUtil.GetFirstMatch(path, matchers, any: true);
}
/// <inheritdoc/>
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+";
}
/// <summary>
/// Try to get the SecuROM v4 version from the overlay, if possible
/// </summary>

View File

@@ -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 @@ using SabreTools.Serialization.Wrappers;
namespace BinaryObjectScanner.Protection
{
public class StarForce : IExecutableCheck<PortableExecutable>, IPathCheck
public class StarForce : IExecutableCheck<PortableExecutable>, IPathCheck, IDiskImageCheck<ISO9660>
{
// TODO: Bring up to par with PiD.
// Known issues:
@@ -160,5 +162,85 @@ namespace BinaryObjectScanner.Protection
// TODO: Determine if there are any file name checks that aren't too generic to use on their own.
return null;
}
/// <inheritdoc/>
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";
}
/// <summary>
/// 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
/// </summary>
private static readonly Dictionary<string, uint> UnusualStarforceKeylessKeys = new()
{
{"FYFYILOVEYOU", 60270},
};
}
}

View File

@@ -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 @@ using SabreTools.Serialization.Wrappers;
namespace BinaryObjectScanner.Protection
{
public class TAGES : IExecutableCheck<PortableExecutable>, IPathCheck
public class TAGES : IExecutableCheck<PortableExecutable>, IPathCheck, IDiskImageCheck<ISO9660>
{
/// <inheritdoc/>
public string? CheckExecutable(string file, PortableExecutable exe, bool includeDebug)
@@ -212,6 +213,51 @@ namespace BinaryObjectScanner.Protection
return MatchUtil.GetFirstMatch(path, matchers, any: true);
}
/// <inheritdoc/>
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

View File

@@ -478,6 +478,7 @@ namespace BinaryObjectScanner
{
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);