public ulong[] VerifyDataHashTree(bool rehash = false) { ulong dataBlockCount = XvdMath.OffsetToPageNumber((ulong)_io.Stream.Length - UserDataOffset); var invalidBlocks = new List <ulong>(); for (ulong i = 0; i < dataBlockCount; i++) { var hashEntryOffset = CalculateHashEntryOffsetForBlock(i, 0); _io.Stream.Position = (long)hashEntryOffset; byte[] oldhash = _io.Reader.ReadBytes((int)DataHashEntryLength); var dataToHashOffset = XvdMath.PageNumberToOffset(i) + UserDataOffset; _io.Stream.Position = (long)dataToHashOffset; byte[] data = _io.Reader.ReadBytes((int)PAGE_SIZE); byte[] hash = HashUtils.ComputeSha256(data); Array.Resize(ref hash, (int)DataHashEntryLength); if (hash.IsEqualTo(oldhash)) { continue; } invalidBlocks.Add(i); if (!rehash) { continue; } _io.Stream.Position = (long)hashEntryOffset; _io.Writer.Write(hash); } return(invalidBlocks.ToArray()); }
public bool VirtualToLogicalDriveOffset(ulong virtualOffset, out ulong logicalOffset) { logicalOffset = 0; if (virtualOffset >= Header.DriveSize) { throw new InvalidOperationException( $"Virtual offset 0x{virtualOffset:X} is outside drivedata length 0x{Header.DriveSize:X}"); } if (Header.Type > XvdType.Dynamic) { throw new NotSupportedException($"Xvd type {Header.Type} is unhandled"); } if (Header.Type == XvdType.Dynamic) { var dataStartOffset = virtualOffset + XvdMath.PageNumberToOffset(Header.NumberOfMetadataPages); var pageNumber = XvdMath.OffsetToPageNumber(dataStartOffset); var inBlockOffset = XvdMath.InBlockOffset(dataStartOffset); var firstDynamicPage = XvdMath.QueryFirstDynamicPage(Header.NumberOfMetadataPages); if (pageNumber >= firstDynamicPage) { var firstDynamicPageBytes = XvdMath.PageNumberToOffset(firstDynamicPage); var blockNumber = XvdMath.OffsetToBlockNumber(dataStartOffset - firstDynamicPageBytes); ulong allocatedBlock = ReadBat(blockNumber); if (allocatedBlock == INVALID_SECTOR) { return(false); } dataStartOffset = XvdMath.PageNumberToOffset(allocatedBlock) + inBlockOffset; pageNumber = XvdMath.OffsetToPageNumber(dataStartOffset); } var dataBackingBlockNum = XvdMath.ComputeDataBackingPageNumber(Header.Type, HashTreeLevels, HashTreePageCount, pageNumber); logicalOffset = XvdMath.PageNumberToOffset(dataBackingBlockNum); logicalOffset += XvdMath.InPageOffset(dataStartOffset); logicalOffset += XvdMath.PageNumberToOffset(Header.EmbeddedXvdPageCount); logicalOffset += Header.MutableDataLength; logicalOffset += XVD_HEADER_INCL_SIGNATURE_SIZE; logicalOffset += PAGE_SIZE; } else { // Xvd type fixed logicalOffset = virtualOffset; logicalOffset += XvdMath.PageNumberToOffset(Header.EmbeddedXvdPageCount); logicalOffset += Header.MutableDataLength; logicalOffset += XvdMath.PageNumberToOffset(Header.NumberOfMetadataPages); logicalOffset += XVD_HEADER_INCL_SIGNATURE_SIZE; logicalOffset += PAGE_SIZE; } return(true); }
public bool VerifyHashTree() { if (!IsDataIntegrityEnabled) { return(true); } _io.Stream.Position = (long)HashTreeOffset; byte[] hash = HashUtils.ComputeSha256(_io.Reader.ReadBytes((int)PAGE_SIZE)); if (!Header.TopHashBlockHash.IsEqualTo(hash)) { return(false); } if (HashTreeLevels == 1) { return(true); } var blocksPerLevel = 0xAA; ulong topHashTreeBlock = 0; uint hashTreeLevel = 1; while (hashTreeLevel < HashTreeLevels) { uint dataBlockNum = 0; if (Header.NumberOfHashedPages != 0) { while (dataBlockNum < Header.NumberOfHashedPages) { _io.Stream.Position = (long)CalculateHashEntryOffsetForBlock(dataBlockNum, hashTreeLevel - 1); byte[] blockHash = HashUtils.ComputeSha256(_io.Reader.ReadBytes((int)PAGE_SIZE)); Array.Resize(ref blockHash, (int)HASH_ENTRY_LENGTH); var upperHashBlockOffset = CalculateHashEntryOffsetForBlock(dataBlockNum, hashTreeLevel); topHashTreeBlock = XvdMath.OffsetToPageNumber(upperHashBlockOffset - HashTreeOffset); _io.Stream.Position = (long)upperHashBlockOffset; byte[] expectedHash = _io.Reader.ReadBytes((int)HASH_ENTRY_LENGTH); if (!expectedHash.IsEqualTo(blockHash)) { // wrong hash return(false); } dataBlockNum += (uint)blocksPerLevel; } } hashTreeLevel++; blocksPerLevel = blocksPerLevel * 0xAA; } if (topHashTreeBlock != 0) { Console.WriteLine(@"Top level hash page calculated to be at {0}, should be 0!", topHashTreeBlock); } return(true); }
internal bool RemoveData(ulong offset, ulong numPages) { var page = XvdMath.OffsetToPageNumber(offset); var length = numPages * PAGE_SIZE; _io.Stream.Position = (long)offset; if (!_io.DeleteBytes((long)length)) { return(false); } if (!IsXvcFile) { return(true); } if (XvcInfo.InitialPlayOffset > offset) { XvcInfo.InitialPlayOffset -= length; } if (XvcInfo.PreviewOffset > offset) { XvcInfo.PreviewOffset -= length; } for (int i = 0; i < RegionHeaders.Count; i++) { var region = RegionHeaders[i]; region.Hash = 0; // ??? if (offset >= region.Offset && region.Offset + region.Length > offset) { region.Length -= length; // offset is part of region, reduce length } else if (region.Offset > offset) { region.Offset -= length; // offset is before region, reduce offset } RegionHeaders[i] = region; // region is a copy instead of a reference due to it being a struct, so we have to replace the original data ourselves } for (int i = 0; i < UpdateSegments.Count; i++) { var segment = UpdateSegments[i]; if (segment.PageNum < page) { continue; } segment.PageNum -= (uint)numPages; UpdateSegments[i] = segment; } return(true); }
internal bool AddData(ulong offset, ulong numPages) { var page = XvdMath.OffsetToPageNumber(offset); var length = numPages * PAGE_SIZE; _io.Stream.Position = (long)offset; if (!_io.AddBytes((long)length)) { return(false); } if (!IsXvcFile) { return(true); } if (XvcInfo.InitialPlayOffset > offset) { XvcInfo.InitialPlayOffset += length; } if (XvcInfo.PreviewOffset > offset) { XvcInfo.PreviewOffset += length; } for (int i = 0; i < RegionHeaders.Count; i++) { var region = RegionHeaders[i]; region.Hash = 0; // ??? if (offset >= region.Offset && region.Offset + region.Length > offset) { region.Length += length; // offset is part of region, add to length } else if (region.Offset > offset) { region.Offset += length; // offset is before region, add to offset } RegionHeaders[i] = region; } for (int i = 0; i < UpdateSegments.Count; i++) { var segment = UpdateSegments[i]; if (segment.PageNum < page) { continue; } segment.PageNum += (uint)numPages; UpdateSegments[i] = segment; } return(true); }
internal bool CryptSectionXts(bool encrypt, byte[] key, uint headerId, ulong offset, ulong length) { var startPage = XvdMath.OffsetToPageNumber(offset - UserDataOffset); ulong numPages = XvdMath.BytesToPages(length); // Pre-read data unit numbers to minimize needing to seek around the file List <uint> dataUnits = null; if (IsDataIntegrityEnabled) { dataUnits = new List <uint>(); for (uint page = 0; page < numPages; page++) { // fetch dataUnit from hash table entry for this page // TODO: seems we'll have to insert dataUnit when re-adding hashtables... // last 4 bytes of hash entry = dataUnit _io.Stream.Position = (long)CalculateHashEntryOffsetForBlock(startPage + page, 0) + 0x14; dataUnits.Add(_io.Reader.ReadUInt32()); } } var tweakAesKey = new byte[0x10]; var dataAesKey = new byte[0x10]; var tweak = new byte[0x10]; // Split tweak- / Data AES key Array.Copy(key, tweakAesKey, 0x10); Array.Copy(key, 0x10, dataAesKey, 0, 0x10); // Copy VDUID and header Id as tweak var headerIdBytes = BitConverter.GetBytes(headerId); Array.Copy(Header.VDUID, 0, tweak, 0x8, 0x8); Array.Copy(headerIdBytes, 0, tweak, 0x4, 0x4); var cipher = new AesXtsTransform(tweak, dataAesKey, tweakAesKey, encrypt); // Perform crypto! _io.Stream.Position = (long)offset; for (uint page = 0; page < numPages; page++) { var transformedData = new byte[PAGE_SIZE]; var pageOffset = _io.Stream.Position; var origData = _io.Reader.ReadBytes((int)PAGE_SIZE); cipher.TransformDataUnit(origData, 0, origData.Length, transformedData, 0, dataUnits?[(int)page] ?? page); _io.Stream.Position = pageOffset; _io.Writer.Write(transformedData); } return(true); }
uint[] GetAllBATEntries() { var batEntryCount = XvdMath.BytesToBlocks(Header.DriveSize); uint[] BatEntries = new uint[batEntryCount]; for (ulong i = 0; i < batEntryCount; i++) { BatEntries[i] = ReadBat(i); } return(BatEntries); }
byte[] ReadDynamic(int count) { int positionInBuffer = 0; int bytesRemaining = count; byte[] destBuffer = new byte[count]; while (positionInBuffer < count) { byte[] data = new byte[0]; if (Position < StaticDataLength) { // Read a chunk from non-dynamic area, next iteration will read dynamic data int maxReadLength = (int)(StaticDataLength - Position); int length = bytesRemaining > maxReadLength ? maxReadLength : bytesRemaining; data = InternalRead(length); } else { // Lookup block allocation table for real data offset var targetVirtualOffset = (ulong)(Position - StaticDataLength); ulong blockNumber = XvdMath.OffsetToBlockNumber(targetVirtualOffset); long inBlockOffset = (long)XvdMath.InBlockOffset(targetVirtualOffset); int maxReadLength = (int)(XvdFile.BLOCK_SIZE - inBlockOffset); int length = bytesRemaining > maxReadLength ? maxReadLength : bytesRemaining; var targetPage = _xvdFile.ReadBat(blockNumber); if (targetPage == XvdFile.INVALID_SECTOR) { data = new byte[length]; // Advance stream position cause we are not actually reading data Position += length; } else { long targetPhysicalOffset = DynamicBaseOffset + (long)XvdMath.PageNumberToOffset(targetPage) + inBlockOffset; data = InternalReadAbsolute(targetPhysicalOffset, length); } } Array.Copy(data, 0, destBuffer, positionInBuffer, data.Length); positionInBuffer += data.Length; bytesRemaining -= data.Length; } return(destBuffer); }
public string ToString(bool formatted) { var b = new StringBuilder(); b.AppendLine("XvcUpdateSegment"); b.AppendLine(); string fmt = formatted ? " " : ""; b.AppendLineSpace(fmt + $"PageNum: 0x{PageNum:X} (@ 0x{XvdMath.PageNumberToOffset(PageNum)})"); b.AppendLineSpace(fmt + $"Hash: 0x{Hash:X}"); return(b.ToString()); }
ulong CalculateStaticDataLength() { if (Header.Type == XvdType.Dynamic) { var smallestPage = GetAllBATEntries().Min(); return(XvdMath.PageNumberToOffset(smallestPage) - XvdMath.PageNumberToOffset(Header.DynamicHeaderPageCount) - XvdMath.PageNumberToOffset(Header.XvcInfoPageCount)); } else if (Header.Type == XvdType.Fixed) { return(0); } else { throw new InvalidProgramException("Unsupported XvdType"); } }
public ulong CalculateHashEntryOffsetForBlock(ulong blockNum, uint hashLevel) { var hashBlock = XvdMath.CalculateHashBlockNumForBlockNum(Header.Type, HashTreeLevels, Header.NumberOfHashedPages, blockNum, hashLevel, out var entryNum); return(HashTreeOffset + XvdMath.PageNumberToOffset(hashBlock) + (entryNum * HASH_ENTRY_LENGTH)); }
public bool VerifyXvcHash(bool rehash = false) { if (!IsXvcFile) { return(true); } ulong hashTreeSize = HashTreePageCount * PAGE_SIZE; var ms = new MemoryStream(); var msIo = new IO(ms); msIo.Writer.WriteStruct(XvcInfo); // fix region headers to match pre-hashtable for (int i = 0; i < XvcInfo.RegionCount; i++) { var region = RegionHeaders[i]; region.Hash = 0; if (IsDataIntegrityEnabled) { if (HashTreeOffset >= region.Offset && region.Offset + region.Length > HashTreeOffset) { region.Length -= hashTreeSize; } else if (region.Offset > HashTreeOffset) { region.Offset -= hashTreeSize; } } msIo.Writer.WriteStruct(region); } for (int i = 0; i < XvcInfo.UpdateSegmentCount; i++) { var segment = UpdateSegments[i]; var hashTreeEnd = XvdMath.BytesToPages(HashTreeOffset) + HashTreePageCount; if (segment.PageNum >= hashTreeEnd) { segment.PageNum -= (uint)HashTreePageCount; } segment.Hash = 0; msIo.Writer.WriteStruct(segment); } if (RegionSpecifiers != null) { for (int i = 0; i < XvcInfo.RegionSpecifierCount; i++) { msIo.Writer.WriteStruct(RegionSpecifiers[i]); } } if (Header.XvcDataLength > msIo.Stream.Length) { msIo.Stream.SetLength(Header.XvcDataLength); } if (IsDataIntegrityEnabled) { // remove hash table offset from the special regions if (XvcInfo.InitialPlayOffset > HashTreeOffset) { msIo.Stream.Position = 0xD28; msIo.Writer.Write(XvcInfo.InitialPlayOffset - hashTreeSize); } if (XvcInfo.PreviewOffset > HashTreeOffset) { msIo.Stream.Position = 0xD40; msIo.Writer.Write(XvcInfo.PreviewOffset - hashTreeSize); } } byte[] xvcData = ms.ToArray(); msIo.Dispose(); byte[] hash = HashUtils.ComputeSha256(xvcData); bool isValid = Header.OriginalXvcDataHash.IsEqualTo(hash); if (rehash) { Header.OriginalXvcDataHash = hash; } return(isValid); //todo: investigate why this gets the correct hash for dev XVCs but fails for retail ones, might be to do with retail XVC data having a content ID that doesn't match with VDUID/UDUID }
public string ToString(bool formatted) { var b = new StringBuilder(); string fmt = formatted ? " " : ""; b.AppendLine("XvdMiscInfo:"); b.AppendLineSpace(fmt + $"Page Count: 0x{Header.NumberOfHashedPages:X}"); b.AppendLineSpace(fmt + $"Embedded XVD Offset: 0x{EmbeddedXvdOffset:X}"); b.AppendLineSpace(fmt + $"MDU Offset: 0x{MduOffset:X}"); b.AppendLineSpace(fmt + $"HashTree Offset: 0x{HashTreeOffset:X}"); b.AppendLineSpace(fmt + $"User Data Offset: 0x{UserDataOffset:X}"); b.AppendLineSpace(fmt + $"XVC Data Offset: 0x{XvcInfoOffset:X}"); b.AppendLineSpace(fmt + $"Dynamic Header Offset: 0x{DynamicHeaderOffset:X}"); b.AppendLineSpace(fmt + $"Drive Data Offset: 0x{DriveDataOffset:X}"); if (IsDataIntegrityEnabled) { b.AppendLineSpace(fmt + $"Hash Tree Page Count: 0x{HashTreePageCount:X}"); b.AppendLineSpace(fmt + $"Hash Tree Levels: 0x{HashTreeLevels:X}"); b.AppendLineSpace(fmt + $"Hash Tree Valid: {HashTreeValid}"); if (!DisableDataHashChecking) { b.AppendLineSpace(fmt + $"Data Hash Tree Valid: {DataHashTreeValid}"); } } if (IsXvcFile) { b.AppendLineSpace(fmt + $"XVC Data Hash Valid: {XvcDataHashValid}"); } b.AppendLine(); b.Append(Header.ToString(formatted)); if (IsXvcFile && XvcInfo.ContentID != null) { b.AppendLine(); bool xvcKeyFound = GetXvcKey(0, out var decryptKey); if (xvcKeyFound) { b.AppendLine($"Decrypt key for xvc keyslot 0: {decryptKey.ToHexString()}"); b.AppendLine("(key is wrong though until the obfuscation/encryption on it is figured out)"); b.AppendLine(); } b.AppendLine(XvcInfo.ToString(formatted)); } if (RegionHeaders != null) { for (int i = 0; i < RegionHeaders.Count; i++) { b.AppendLine(); string presenceInfo = ""; if (RegionPresenceInfo != null && RegionPresenceInfo.Count > i) { var presenceFlags = RegionPresenceInfo[i]; presenceInfo = " ("; presenceInfo += (presenceFlags.HasFlag(XvcRegionPresenceInfo.IsPresent) ? "present" : "not present") + ", "; presenceInfo += presenceFlags.HasFlag(XvcRegionPresenceInfo.IsAvailable) ? "available" : "unavailable"; if (((int)presenceFlags & 0xF0) != 0) { presenceInfo += $", on disc {(int)presenceFlags >> 4}"; } presenceInfo += ")"; } b.AppendLine($"Region {i}{presenceInfo}"); b.Append(RegionHeaders[i].ToString(formatted)); } } if (UpdateSegments != null && UpdateSegments.Count > 0) { // have to add segments to a seperate List so we can store the index of them... var segments = new List <Tuple <int, XvcUpdateSegment> >(); for (int i = 0; i < UpdateSegments.Count; i++) { if (UpdateSegments[i].Hash == 0) { break; } segments.Add(Tuple.Create(i, UpdateSegments[i])); } b.AppendLine(); b.AppendLine("Update Segments:"); b.AppendLine(); b.AppendLine(segments.ToStringTable( new[] { "Id", "PageNum (Offset)", "Hash" }, a => a.Item1, a => $"0x{a.Item2.PageNum:X} (0x{XvdMath.PageNumberToOffset(a.Item2.PageNum):X})", a => $"0x{a.Item2.Hash:X}")); } if (RegionSpecifiers != null && RegionSpecifiers.Count > 0) { // have to add specifiers to a seperate List so we can store the index of them... var specs = new List <Tuple <int, XvcRegionSpecifier> >(); for (int i = 0; i < RegionSpecifiers.Count; i++) { specs.Add(Tuple.Create(i, RegionSpecifiers[i])); } b.AppendLine(); b.AppendLine("Region Specifiers:"); b.AppendLine(); b.AppendLine(specs.ToStringTable( new[] { "Id", "RegionId", "Key", "Value" }, a => a.Item1, a => $"0x{a.Item2.RegionId:X}", a => a.Item2.Key, a => a.Item2.Value)); } if (!IsEncrypted) { b.AppendLine(); try { b.Append(Filesystem.ToString(formatted)); } catch (Exception e) { b.AppendLine($"Failed to get XvdFilesystem info, error: {e}"); } } else { b.AppendLine($"Cannot get XvdFilesystem from encrypted package"); } return(b.ToString()); }