diff --git a/Aaru.Decryption.csproj b/Aaru.Decryption.csproj index 72c6796..2eeeef4 100644 --- a/Aaru.Decryption.csproj +++ b/Aaru.Decryption.csproj @@ -33,8 +33,8 @@ CS1591;CS1574 - - + + $(Version)+{chash:8} @@ -43,9 +43,9 @@ - - - + + + diff --git a/DVD/CSS.cs b/DVD/CSS.cs index f85ecf5..3a28055 100644 --- a/DVD/CSS.cs +++ b/DVD/CSS.cs @@ -44,7 +44,12 @@ // libdvdcss (https://www.videolan.org/developers/libdvdcss.html) using System; +using System.Collections.Generic; using System.Linq; +using Aaru.CommonTypes; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; using Aaru.Decoders.DVD; namespace Aaru.Decryption.DVD; @@ -544,11 +549,11 @@ public class CSS } /// Takes an encrypted key and its crypto and returns the key decrypted. - /// + /// For disc keys, invert is 0x00. For title keys, invert if 0xff. /// The key used to encrypt the data. /// The encrypted data. /// The decrypted data. - public static void DecryptKey(byte invert, byte[] cryptoKey, byte[] encryptedKey, out byte[] decryptedKey) + static void DecryptKey(byte invert, byte[] cryptoKey, byte[] encryptedKey, out byte[] decryptedKey) { decryptedKey = new byte[5]; byte[] k = new byte[5]; @@ -590,8 +595,8 @@ public class CSS decryptedKey[0] = (byte)(k[0] ^ _cssTable1[decryptedKey[0]]); } - public static void DecryptTitleKey(byte invert, byte[] cryptoKey, byte[] encryptedKey, out byte[] decryptedKey) => - DecryptKey(invert, cryptoKey, encryptedKey, out decryptedKey); + public static void DecryptTitleKey(byte[] cryptoKey, byte[] encryptedKey, out byte[] decryptedKey) => + DecryptKey(0xff, cryptoKey, encryptedKey, out decryptedKey); /// Takes an bytearray of encrypted keys, decrypts them and returns the correctly decrypted key. /// Encrypted keys to try to decrypt. @@ -630,65 +635,115 @@ public class CSS /// Number of sectors in sectorData. /// Size of one sector. /// The decrypted sector. - public static byte[] DecryptSector(byte[] sectorData, byte[] cmiData, byte[] keyData, uint blocks = 1, + public static byte[] DecryptSector(byte[] sectorData, byte[] keyData, byte[]? cmiData, uint blocks = 1, uint blockSize = 2048) { - if(cmiData.All(cmi => (cmi & 0x80) >> 7 == 0) || - keyData.All(k => k == 0)) + // None of the sectors are encrypted + if((cmiData != null && cmiData.All(static cmi => (cmi & 0x80) >> 7 == 0)) || + keyData.All(static k => k == 0)) return sectorData; byte[] decryptedBuffer = new byte[sectorData.Length]; - for(uint j = 0; j < blocks; j++) + for(uint i = 0; i < blocks; i++) { - byte[] currentKey = keyData.Skip((int)(j * 5)).Take(5).ToArray(); - byte[] currentSector = sectorData.Skip((int)(j * blockSize)).Take((int)blockSize).ToArray(); + byte[] currentKey = keyData.Skip((int)(i * 5)).Take(5).ToArray(); + byte[] currentSector = sectorData.Skip((int)(i * blockSize)).Take((int)blockSize).ToArray(); - // If the CMI tells use the sector isn't encrypted or - // if the key is all zeroes or - // if the MPEG Packetized Elementary Stream scrambling control value tells us the packet is not scrambled - if((cmiData[j] & 0x80) >> 7 == 0 || - currentKey.All(k => k == 0) || - (currentSector[20] & 0x30) >> 4 == 0) + if(!IsEncrypted(cmiData?[i], currentKey, currentSector)) { - // Sector is not encrypted - Array.Copy(currentSector, 0, decryptedBuffer, (int)(j * blockSize), blockSize); + Array.Copy(currentSector, 0, decryptedBuffer, (int)(i * blockSize), blockSize); continue; } - uint lfsr1Lo = (uint)(currentKey[0] ^ currentSector[0x54]) | 0x100; - uint lfsr1Hi = (uint)currentKey[1] ^ currentSector[0x55]; - - uint lfsr0 = (uint)((currentKey[2] | (currentKey[3] << 8) | (currentKey[4] << 16)) ^ - (sectorData[0x56] | (sectorData[0x57] << 8) | (sectorData[0x58] << 16))); - - uint oLfsr1 = lfsr0 & 7; - lfsr0 = (lfsr0 * 2) + 8 - oLfsr1; - - uint combined = 0; - - for(uint i = 128; i < blockSize; i++) - { - oLfsr1 = (uint)(_cssTable2[lfsr1Hi] ^ _cssTable3[lfsr1Lo]); - lfsr1Hi = lfsr1Lo >> 1; - lfsr1Lo = ((lfsr1Lo & 1) << 8) ^ oLfsr1; - oLfsr1 = _cssTable5[oLfsr1]; - uint oLfsr0 = (((((((lfsr0 >> 3) ^ lfsr0) >> 1) ^ lfsr0) >> 8) ^ lfsr0) >> 5) & 0xff; - lfsr0 = (lfsr0 >> 8) | (oLfsr0 << 24); - lfsr0 = (lfsr0 << 8) | oLfsr0; - oLfsr0 = _cssTable4[oLfsr0]; - combined += oLfsr0 + oLfsr1; - currentSector[i] = (byte)(_cssTable1[currentSector[i]] ^ (combined & 0xff)); - combined >>= 8; - } - - Array.Copy(currentSector, 0, decryptedBuffer, (int)(j * blockSize), blockSize); + Array.Copy(UnscrambleSector(currentKey, currentSector), 0, decryptedBuffer, (int)(i * blockSize), + blockSize); } return decryptedBuffer; } + /// + /// Unscrambles a DVD sector with a title key. + /// + /// The title key. + /// The scrambled sector. + /// The unscrambled sector. + static byte[] UnscrambleSector(IReadOnlyList key, byte[] sector) + { + long lfsr1Lo = (key[0] ^ sector[0x54]) | 0x100; + long lfsr1Hi = key[1] ^ sector[0x55]; + + long lfsr0 = (key[2] | (key[3] << 8) | (key[4] << 16)) ^ + (sector[0x56] | (sector[0x57] << 8) | (sector[0x58] << 16)); + + long oLfsr1 = lfsr0 & 7; + lfsr0 = (lfsr0 * 2) + 8 - oLfsr1; + + long combined = 0; + + for(uint i = 0x80; i < 2048; i++) + { + oLfsr1 = _cssTable2[lfsr1Hi] ^ _cssTable3[lfsr1Lo]; + lfsr1Hi = lfsr1Lo >> 1; + lfsr1Lo = ((lfsr1Lo & 1) << 8) ^ oLfsr1; + oLfsr1 = _cssTable5[oLfsr1]; + long oLfsr0 = (((((((lfsr0 >> 3) ^ lfsr0) >> 1) ^ lfsr0) >> 8) ^ lfsr0) >> 5) & 0xff; + lfsr0 = (lfsr0 << 8) | oLfsr0; + oLfsr0 = _cssTable4[oLfsr0]; + combined += oLfsr0 + oLfsr1; + sector[i] = (byte)(_cssTable1[sector[i]] ^ (combined & 0xff)); + combined >>= 8; + } + + // Since we unscrambled the sector, we need to set the MPEG Packetized Elementary Stream + // scrambling control value to "not scrambled". + sector[20] ^= 1 << 4; + + return sector; + } + + /// + /// Analyzes data to try to figure out if the sector is encrypted, including + /// + /// If the packet is not an MPEG packet + /// If the CMI tells us the sector isn't encrypted + /// If the key is all zeroes + /// If the MPEG Packetized Elementary Stream scrambling control value tells us the packet is not scrambled + /// If if the packet is system_header, padding_stream or private_stream2 (cannot be encrypted according to libdvdcss) + /// + /// + /// The Copyright Management Information. + /// The title key. + /// The sector data. + /// True if encrypted + static bool IsEncrypted(byte? cmi, byte[]? key, IReadOnlyList sector) + { + // Only MPEG packets can be encrypted. + if(!MPEG.IsMpegPacket(sector)) + return false; + + // The CMI tells us the sector is not encrypted. + if(cmi != null && + (cmi & 0x80) >> 7 == 0) + return false; + + // We have the key but it's all zeroes, so sector is unencrypted. + if(key != null && + key.All(static k => k == 0)) + return false; + + // These packet types cannot be encrypted + if(sector[17] == (byte)MPEG.Mpeg2StreamId.SystemHeader || + sector[17] == (byte)MPEG.Mpeg2StreamId.PaddingStream || + sector[17] == (byte)MPEG.Mpeg2StreamId.PrivateStream2) + return false; + + // MPEG Packetized Elementary Stream scrambling control value + return (sector[20] & 0x30) >> 4 == 1; + } + /// Takes an RPC state from the drive and a CMI from a disc and checks if the regions are compatible. /// The RegionalPlaybackControlState from drive. /// The LeadInCopyright from disc. @@ -708,4 +763,263 @@ public class CSS ((rpc.RegionMask & 0x40) == (cmi.RegionInformation & 0x40) && (rpc.RegionMask & 0x40) != 0x40) || ((rpc.RegionMask & 0x80) == (cmi.RegionInformation & 0x80) && (rpc.RegionMask & 0x80) != 0x80); } + + /// + /// This tries to find a title key for a range of sectors by doing a brute force pattern search developed by + /// Ethan Hawke of DeCSSPlus. CSS encrypted sectors have parts of them that are unencrypted (byte 0x0 - 0x80). + /// We try to find a long pattern of repeated bytes just before the encryption starts. If we assume this + /// pattern continues into the encrypted part, we can force keys until one of them satisfies this condition. + /// + /// The sector to analyze. + /// The key found. + /// true if a key was found. + static bool AttackPattern(byte[] sector, out byte[] key) + { + uint bestPatternLength = 0; + uint bestPattern = 0; + key = new byte[5]; + + for(uint i = 2; i < 0x30; i++) + { + // Find the number of bytes that repeats in cycles. + for(uint j = i + 1; j < 0x80 && sector[0x7F - (j % i)] == sector[0x7F - j]; j++) + { + if(j <= bestPatternLength) + continue; + + bestPatternLength = j; + bestPattern = i; + } + } + + // If we found an adequate pattern. + if(bestPattern <= 0 || + bestPatternLength <= 3 || + bestPatternLength / bestPattern < 2) + return false; + + int offset = (int)(0x80 - ((bestPatternLength / bestPattern) * bestPattern)); + + int result = RecoverTitleKey(0, sector.Skip(0x80).Take(sector.Length - 0x80).ToArray(), + sector.Skip(offset).Take((int)(sector.Length - offset)).ToArray(), + sector.Skip(0x54).Take(5).ToArray(), out key); + + return result >= 0; + } + + /// + /// Takes a guessed plain text and a encrypted bytes and tries to recover the title key + /// from those. Attack developed by Frank Stevenson. + /// + /// Start position. + /// Buffer with encrypted bytes. + /// Buffer with decrypted bytes. + /// This sector's seed values. + /// The title key. + /// Positive values on success. + static int RecoverTitleKey(uint start, byte[] encryptedBytes, byte[] decryptedBytes, byte[] sectorSeed, + out byte[] key) + { + byte[] buffer = new byte[10]; + long oLfsr1; + long oLfsr0; + long iTry; + uint i; + int exit = -1; + key = new byte[5]; + + for(i = 0; i < 10; i++) + buffer[i] = (byte)(_cssTable1[encryptedBytes[i]] ^ decryptedBytes[i]); + + for(iTry = start; iTry < 0x10000; iTry++) + { + long lfsr1Lo = (iTry >> 8) | 0x100; + long lfsr1Hi = iTry & 0xff; + long lfsr0 = 0; + long combined = 0; + + // Iterate cipher 4 times to reconstruct LFSR2 + for(i = 0; i < 4; i++) + { + // Advance LFSR1 normally + oLfsr1 = _cssTable2[lfsr1Hi] ^ _cssTable3[lfsr1Lo]; + lfsr1Hi = lfsr1Lo >> 1; + lfsr1Lo = ((lfsr1Lo & 1) << 8) ^ oLfsr1; + oLfsr1 = _cssTable5[oLfsr1]; + oLfsr0 = buffer[i]; + + if(combined > 0) + oLfsr0 = (oLfsr0 + 0xff) & 0x0ff; + + if(oLfsr0 < oLfsr1) + oLfsr0 += 0x100; + + oLfsr0 -= oLfsr1; + combined += oLfsr0 + oLfsr1; + oLfsr0 = _cssTable4[oLfsr0]; + lfsr0 = (lfsr0 << 8) | oLfsr0; + combined >>= 8; + } + + long candidate = lfsr0; + + // Iterate 6 more times to validate candidate key + for(; i < 10; i++) + { + oLfsr1 = _cssTable2[lfsr1Hi] ^ _cssTable3[lfsr1Lo]; + lfsr1Hi = lfsr1Lo >> 1; + lfsr1Lo = ((lfsr1Lo & 1) << 8) ^ oLfsr1; + oLfsr1 = _cssTable5[oLfsr1]; + oLfsr0 = (((((((lfsr0 >> 3) ^ lfsr0) >> 1) ^ lfsr0) >> 8) ^ lfsr0) >> 5) & 0xff; + lfsr0 = (lfsr0 << 8) | oLfsr0; + oLfsr0 = _cssTable4[oLfsr0]; + combined += oLfsr0 + oLfsr1; + + if((combined & 0xff) != buffer[i]) + break; + + combined >>= 8; + } + + if(i != 10) + continue; + + lfsr0 = candidate; + + for(i = 0; i < 4; i++) + { + lfsr1Lo = lfsr0 & 0xff; + lfsr0 = (lfsr0 >> 8); + + for(uint j = 0; j < 256; j++) + { + lfsr0 = (lfsr0 & 0x1ffff) | (j << 17); + oLfsr0 = (((((((lfsr0 >> 3) ^ lfsr0) >> 1) ^ lfsr0) >> 8) ^ lfsr0) >> 5) & 0xff; + + if(oLfsr0 == lfsr1Lo) + break; + } + } + + oLfsr1 = (lfsr0 >> 1) - 4; + + for(combined = 0; combined < 8; combined++) + { + if(((oLfsr1 + combined) * 2) + 8 - ((oLfsr1 + combined) & 7) != lfsr0) + continue; + + key[0] = (byte)(iTry >> 8); + key[1] = (byte)(iTry & 0xFF); + key[2] = (byte)(((oLfsr1 + combined) >> 0) & 0xFF); + key[3] = (byte)(((oLfsr1 + combined) >> 8) & 0xFF); + key[4] = (byte)(((oLfsr1 + combined) >> 16) & 0xFF); + exit = (int)(iTry + 1); + } + } + + if(exit < 0) + return exit; + + key[0] ^= sectorSeed[0]; + key[1] ^= sectorSeed[1]; + key[2] ^= sectorSeed[2]; + key[3] ^= sectorSeed[3]; + key[4] ^= sectorSeed[4]; + + return exit; + } + + /// + /// Tries to find a title key by attacking CSS vulnerabilities. + /// + /// IOpticalMediaImage to find the title key in. + /// Sector index to begin search. + /// Amount of sectors to search before giving up. + /// The title key. + static byte[] FindTitleKey(IOpticalMediaImage input, ulong startSector, ulong sectorsToSearch = 20000) + { + byte[] titleKey = new byte[5]; + + for(ulong i = 0; i < sectorsToSearch; i++) + { + input.ReadSector(startSector + i, out byte[] sector); + + if(!IsEncrypted(null, null, sector)) + continue; + + if(AttackPattern(sector, out byte[] key)) + return key; + } + + return titleKey; + } + + /// + /// Generates title keys for all sectors in a track. + /// + /// IOpticalMediaImage to generate keys for. + /// List of Partition to analyze. + /// Total number of sectors for track. + /// + /// A byte array with keys for every sector in the track. One key is 5 bytes. + public static byte[] GenerateTitleKeys(IOpticalMediaImage input, List partitions, ulong trackSectors, + Type pluginType) + { + byte[] keys = new byte[trackSectors * 5]; + + foreach(Partition partition in partitions) + { + if(Activator.CreateInstance(pluginType) is not IReadOnlyFilesystem fs) + continue; + + if(!HasVideoTsFolder(input, fs, partition)) + continue; + + if(fs.Mount(input, partition, null, null, null) != ErrorNumber.NoError) + continue; + + if(fs.OpenDir("VIDEO_TS", out IDirNode node) == ErrorNumber.NoError) + { + while(fs.ReadDir(node, out string entry) == ErrorNumber.NoError && + entry is not null) + { + if(!entry.ToLower().EndsWith(".vob")) + continue; + + fs.Stat("VIDEO_TS" + "/" + entry, out FileEntryInfo stat); + + byte[] key = FindTitleKey(input, stat.Inode); + + for(long i = 0; i < stat.Blocks; i++) + key.CopyTo(keys, (long)(5 * (stat.Inode + (ulong)i))); + } + + fs.CloseDir(node); + } + + fs.Unmount(); + } + + return keys; + } + + /// + /// DVD video discs always have a VIDEO_TS folder. If it doesn't have one, it's not a DVD video. + /// + /// IOpticalMediaImage to check for VIDEO_TS folder in. + /// IReadOnlyFilesystem to check in. + /// Partition to check in. + /// true if VIDEO_TS folder was found. + static bool HasVideoTsFolder(IOpticalMediaImage input, IReadOnlyFilesystem fs, Partition partition) + { + ErrorNumber error = fs.Mount(input, partition, null, null, null); + + if(error != ErrorNumber.NoError) + return false; + + error = fs.Stat("VIDEO_TS", out FileEntryInfo stat); + fs.Unmount(); + + return error == ErrorNumber.NoError && stat.Attributes == FileAttributes.Directory; + } } \ No newline at end of file diff --git a/DVD/MPEG.cs b/DVD/MPEG.cs new file mode 100644 index 0000000..70df521 --- /dev/null +++ b/DVD/MPEG.cs @@ -0,0 +1,108 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : MPEG.cs +// Author(s) : Rebecca Wallander +// +// --[ Description ] ---------------------------------------------------------- +// +// Handles MPEG packets functionality. +// +// --[ License ] -------------------------------------------------------------- +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// ---------------------------------------------------------------------------- +// Copyright © 2023 Rebecca Wallander +// ****************************************************************************/ + +// http://www.mpucoder.com/DVD/vobov.html + +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Aaru.Decryption.DVD; + +public class MPEG +{ + static byte[] _mpeg2PackHeaderStartCode = + { + 0x0, 0x0, 0x1 + }; + + public enum Mpeg2StreamId : byte + { + ProgramEnd = 0xB9, PackHeader = 0xBA, SystemHeader = 0xBB, + ProgramStreamMap = 0xBC, PrivateStream1 = 0xBD, PaddingStream = 0xBE, + PrivateStream2 = 0xBF, EcmStream = 0xF0, EmmStream = 0xF1, + ItuTRecH222_0_or_IsoIec13818_1AnnexA_or_IsoIec13818_6DsmccStream = 0xF2, IsoIec13522Stream = 0xF3, + ItuTRecH222_1TypeA = 0xF4, ItuTRecH222_1TypeB = 0xF5, ItuTRecH222_1TypeC = 0xF6, + ItuTRecH222_1TypeD = 0xF7, ItuTRecH222_1TypeE = 0xF8, AncillaryStream = 0xF9, + Reserved1 = 0xFA, Reserved2 = 0xFB, Reserved3 = 0xFC, + Reserved4 = 0xFD, Reserved5 = 0xFE, ProgramStreamDirectory = 0xFF, + + // DVD Video can only hold 8 audio streams + MpegAudioStream1 = 0xC0, MpegAudioStream2 = 0xC1, MpegAudioStream3 = 0xC2, + MpegAudioStream4 = 0xC3, MpegAudioStream5 = 0xC4, MpegAudioStream6 = 0xC5, + MpegAudioStream7 = 0xC6, MpegAudioStream8 = 0xC7, MpegAudioStream9 = 0xC8, + MpegAudioStream10 = 0xC9, MpegAudioStream11 = 0xCA, MpegAudioStream12 = 0xCB, + MpegAudioStream13 = 0xCC, MpegAudioStream14 = 0xCD, MpegAudioStream15 = 0xCE, + MpegAudioStream16 = 0xCF, MpegAudioStream17 = 0xD0, MpegAudioStream18 = 0xD1, + MpegAudioStream19 = 0xD2, MpegAudioStream20 = 0xD3, MpegAudioStream21 = 0xD4, + MpegAudioStream22 = 0xD5, MpegAudioStream23 = 0xD6, MpegAudioStream24 = 0xD7, + MpegAudioStream25 = 0xD8, MpegAudioStream26 = 0xD9, MpegAudioStream27 = 0xDA, + MpegAudioStream28 = 0xDB, MpegAudioStream29 = 0xDC, MpegAudioStream30 = 0xDD, + MpegAudioStream31 = 0xDE, MpegAudioStream32 = 0xDF, + + // DVD Video can only hold 1 video stream + MpegVideStream1 = 0xE0, MpegVideStream2 = 0xE1, MpegVideStream3 = 0xE2, + MpegVideStream4 = 0xE3, MpegVideStream5 = 0xE4, MpegVideStream6 = 0xE5, + MpegVideStream7 = 0xE6, MpegVideStream8 = 0xE7, MpegVideStream9 = 0xE8, + MpegVideStream10 = 0xE9, MpegVideStream11 = 0xEA, MpegVideStream12 = 0xEB, + MpegVideStream13 = 0xEC, MpegVideStream14 = 0xED, MpegVideStream15 = 0xEE, + MpegVideStream16 = 0xEF + } + + public struct MpegHeader + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public byte[] startCode; + public byte packIdentifier; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] + public byte[] scrBlock; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public byte[] programMuxRateBlock; + byte packStuffingLengthBlock; + } + + public static bool ContainsMpegPackets(byte[] sectorData, uint blocks = 1, uint blockSize = 2048) + { + for(uint i = 0; i < blocks; i++) + if(IsMpegPacket(sectorData.Skip((int)(i * blockSize)))) + return true; + + return false; + } + + public static bool IsMpegPacket(IEnumerable sector) => + sector.Take(3).ToArray().SequenceEqual(_mpeg2PackHeaderStartCode); +} \ No newline at end of file