Files
WPinternals/DiscUtils/Ntfs/NtfsFileSystemChecker.cs
T
2018-10-25 22:35:49 +02:00

619 lines
24 KiB
C#

//
// Copyright (c) 2008-2011, Kenneth Bell
//
// 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.
//
namespace DiscUtils.Ntfs
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.Serialization;
using System.Text;
/// <summary>
/// Class that checks NTFS file system integrity.
/// </summary>
/// <remarks>Poor relation of chkdsk/fsck.</remarks>
public sealed class NtfsFileSystemChecker : DiscFileSystemChecker
{
private Stream _target;
private NtfsContext _context;
private TextWriter _report;
private ReportLevels _reportLevels;
private ReportLevels _levelsDetected;
private ReportLevels _levelsConsideredFail = ReportLevels.Errors;
/// <summary>
/// Initializes a new instance of the NtfsFileSystemChecker class.
/// </summary>
/// <param name="diskData">The file system to check.</param>
public NtfsFileSystemChecker(Stream diskData)
{
SnapshotStream protectiveStream = new SnapshotStream(diskData, Ownership.None);
protectiveStream.Snapshot();
protectiveStream.Freeze();
_target = protectiveStream;
}
/// <summary>
/// Checks the integrity of an NTFS file system held in a stream.
/// </summary>
/// <param name="reportOutput">A report on issues found.</param>
/// <param name="levels">The amount of detail to report.</param>
/// <returns><c>true</c> if the file system appears valid, else <c>false</c>.</returns>
public override bool Check(TextWriter reportOutput, ReportLevels levels)
{
_context = new NtfsContext();
_context.RawStream = _target;
_context.Options = new NtfsOptions();
_report = reportOutput;
_reportLevels = levels;
_levelsDetected = ReportLevels.None;
try
{
DoCheck();
}
catch (AbortException ae)
{
ReportError("File system check aborted: " + ae.ToString());
return false;
}
catch (Exception e)
{
ReportError("File system check aborted with exception: " + e.ToString());
return false;
}
return (_levelsDetected & _levelsConsideredFail) == 0;
}
/// <summary>
/// Gets an object that can convert between clusters and files.
/// </summary>
/// <returns>The cluster map.</returns>
public ClusterMap BuildClusterMap()
{
_context = new NtfsContext();
_context.RawStream = _target;
_context.Options = new NtfsOptions();
_context.RawStream.Position = 0;
byte[] bytes = Utilities.ReadFully(_context.RawStream, 512);
_context.BiosParameterBlock = BiosParameterBlock.FromBytes(bytes, 0);
_context.Mft = new MasterFileTable(_context);
File mftFile = new File(_context, _context.Mft.GetBootstrapRecord());
_context.Mft.Initialize(mftFile);
return _context.Mft.GetClusterMap();
}
private static void Abort()
{
throw new AbortException();
}
private void DoCheck()
{
_context.RawStream.Position = 0;
byte[] bytes = Utilities.ReadFully(_context.RawStream, 512);
_context.BiosParameterBlock = BiosParameterBlock.FromBytes(bytes, 0);
//-----------------------------------------------------------------------
// MASTER FILE TABLE
//
// Bootstrap the Master File Table
_context.Mft = new MasterFileTable(_context);
File mftFile = new File(_context, _context.Mft.GetBootstrapRecord());
// Verify basic MFT records before initializing the Master File Table
PreVerifyMft(mftFile);
_context.Mft.Initialize(mftFile);
// Now the MFT is up and running, do more detailed analysis of it's contents - double-accounted clusters, etc
VerifyMft();
_context.Mft.Dump(_report, "INFO: ");
//-----------------------------------------------------------------------
// INDEXES
//
// Need UpperCase in order to verify some indexes (i.e. directories).
File ucFile = new File(_context, _context.Mft.GetRecord(MasterFileTable.UpCaseIndex, false));
_context.UpperCase = new UpperCase(ucFile);
SelfCheckIndexes();
//-----------------------------------------------------------------------
// DIRECTORIES
//
VerifyDirectories();
//-----------------------------------------------------------------------
// WELL KNOWN FILES
//
VerifyWellKnownFilesExist();
//-----------------------------------------------------------------------
// OBJECT IDS
//
VerifyObjectIds();
//-----------------------------------------------------------------------
// FINISHED
//
// Temporary...
using (NtfsFileSystem fs = new NtfsFileSystem(_context.RawStream))
{
if ((_reportLevels & ReportLevels.Information) != 0)
{
ReportDump(fs);
}
}
}
private void VerifyWellKnownFilesExist()
{
Directory rootDir = new Directory(_context, _context.Mft.GetRecord(MasterFileTable.RootDirIndex, false));
DirectoryEntry extendDirEntry = rootDir.GetEntryByName("$Extend");
if (extendDirEntry == null)
{
ReportError("$Extend does not exist in root directory");
Abort();
}
Directory extendDir = new Directory(_context, _context.Mft.GetRecord(extendDirEntry.Reference));
DirectoryEntry objIdDirEntry = extendDir.GetEntryByName("$ObjId");
if (objIdDirEntry == null)
{
ReportError("$ObjId does not exist in $Extend directory");
Abort();
}
// Stash ObjectIds
_context.ObjectIds = new ObjectIds(new File(_context, _context.Mft.GetRecord(objIdDirEntry.Reference)));
DirectoryEntry sysVolInfDirEntry = rootDir.GetEntryByName("System Volume Information");
if (sysVolInfDirEntry == null)
{
ReportError("'System Volume Information' does not exist in root directory");
Abort();
}
////Directory sysVolInfDir = new Directory(_context, _context.Mft.GetRecord(sysVolInfDirEntry.Reference));
}
private void VerifyObjectIds()
{
foreach (FileRecord fr in _context.Mft.Records)
{
if (fr.BaseFile.Value != 0)
{
File f = new File(_context, fr);
foreach (var stream in f.AllStreams)
{
if (stream.AttributeType == AttributeType.ObjectId)
{
ObjectId objId = stream.GetContent<ObjectId>();
ObjectIdRecord objIdRec;
if (!_context.ObjectIds.TryGetValue(objId.Id, out objIdRec))
{
ReportError("ObjectId {0} for file {1} is not indexed", objId.Id, f.BestName);
}
else if (objIdRec.MftReference != f.MftReference)
{
ReportError("ObjectId {0} for file {1} points to {2}", objId.Id, f.BestName, objIdRec.MftReference);
}
}
}
}
}
foreach (var objIdRec in _context.ObjectIds.All)
{
if (_context.Mft.GetRecord(objIdRec.Value.MftReference) == null)
{
ReportError("ObjectId {0} refers to non-existant file {1}", objIdRec.Key, objIdRec.Value.MftReference);
}
}
}
private void VerifyDirectories()
{
foreach (FileRecord fr in _context.Mft.Records)
{
if (fr.BaseFile.Value != 0)
{
continue;
}
File f = new File(_context, fr);
foreach (var stream in f.AllStreams)
{
if (stream.AttributeType == AttributeType.IndexRoot && stream.Name == "$I30")
{
IndexView<FileNameRecord, FileRecordReference> dir = new IndexView<FileNameRecord, FileRecordReference>(f.GetIndex("$I30"));
foreach (var entry in dir.Entries)
{
FileRecord refFile = _context.Mft.GetRecord(entry.Value);
// Make sure each referenced file actually exists...
if (refFile == null)
{
ReportError("Directory {0} references non-existent file {1}", f, entry.Key);
}
File referencedFile = new File(_context, refFile);
StandardInformation si = referencedFile.StandardInformation;
if (si.CreationTime != entry.Key.CreationTime || si.MftChangedTime != entry.Key.MftChangedTime
|| si.ModificationTime != entry.Key.ModificationTime)
{
ReportInfo("Directory entry {0} in {1} is out of date", entry.Key, f);
}
}
}
}
}
}
private void SelfCheckIndexes()
{
foreach (FileRecord fr in _context.Mft.Records)
{
File f = new File(_context, fr);
foreach (var stream in f.AllStreams)
{
if (stream.AttributeType == AttributeType.IndexRoot)
{
SelfCheckIndex(f, stream.Name);
}
}
}
}
private void SelfCheckIndex(File file, string name)
{
ReportInfo("About to self-check index {0} in file {1} (MFT:{2})", name, file.BestName, file.IndexInMft);
IndexRoot root = file.GetStream(AttributeType.IndexRoot, name).GetContent<IndexRoot>();
byte[] rootBuffer;
using (Stream s = file.OpenStream(AttributeType.IndexRoot, name, FileAccess.Read))
{
rootBuffer = Utilities.ReadFully(s, (int)s.Length);
}
Bitmap indexBitmap = null;
if (file.GetStream(AttributeType.Bitmap, name) != null)
{
indexBitmap = new Bitmap(file.OpenStream(AttributeType.Bitmap, name, FileAccess.Read), long.MaxValue);
}
if (!SelfCheckIndexNode(rootBuffer, IndexRoot.HeaderOffset, indexBitmap, root, file.BestName, name))
{
ReportError("Index {0} in file {1} (MFT:{2}) has corrupt IndexRoot attribute", name, file.BestName, file.IndexInMft);
}
else
{
ReportInfo("Self-check of index {0} in file {1} (MFT:{2}) complete", name, file.BestName, file.IndexInMft);
}
}
private bool SelfCheckIndexNode(byte[] buffer, int offset, Bitmap bitmap, IndexRoot root, string fileName, string indexName)
{
bool ok = true;
IndexHeader header = new IndexHeader(buffer, offset);
IndexEntry lastEntry = null;
IComparer<byte[]> collator = root.GetCollator(_context.UpperCase);
int pos = (int)header.OffsetToFirstEntry;
while (pos < header.TotalSizeOfEntries)
{
IndexEntry entry = new IndexEntry(indexName == "$I30");
entry.Read(buffer, offset + pos);
pos += entry.Size;
if ((entry.Flags & IndexEntryFlags.Node) != 0)
{
long bitmapIdx = entry.ChildrenVirtualCluster / Utilities.Ceil(root.IndexAllocationSize, _context.BiosParameterBlock.SectorsPerCluster * _context.BiosParameterBlock.BytesPerSector);
if (!bitmap.IsPresent(bitmapIdx))
{
ReportError("Index entry {0} is non-leaf, but child vcn {1} is not in bitmap at index {2}", Index.EntryAsString(entry, fileName, indexName), entry.ChildrenVirtualCluster, bitmapIdx);
}
}
if ((entry.Flags & IndexEntryFlags.End) != 0)
{
if (pos != header.TotalSizeOfEntries)
{
ReportError("Found END index entry {0}, but not at end of node", Index.EntryAsString(entry, fileName, indexName));
ok = false;
}
}
if (lastEntry != null && collator.Compare(lastEntry.KeyBuffer, entry.KeyBuffer) >= 0)
{
ReportError("Found entries out of order {0} was before {1}", Index.EntryAsString(lastEntry, fileName, indexName), Index.EntryAsString(entry, fileName, indexName));
ok = false;
}
lastEntry = entry;
}
return ok;
}
private void PreVerifyMft(File file)
{
int recordLength = _context.BiosParameterBlock.MftRecordSize;
int bytesPerSector = _context.BiosParameterBlock.BytesPerSector;
// Check out the MFT's clusters
foreach (var range in file.GetAttribute(AttributeType.Data, null).GetClusters())
{
if (!VerifyClusterRange(range))
{
ReportError("Corrupt cluster range in MFT data attribute {0}", range.ToString());
Abort();
}
}
foreach (var range in file.GetAttribute(AttributeType.Bitmap, null).GetClusters())
{
if (!VerifyClusterRange(range))
{
ReportError("Corrupt cluster range in MFT bitmap attribute {0}", range.ToString());
Abort();
}
}
using (Stream mftStream = file.OpenStream(AttributeType.Data, null, FileAccess.Read))
using (Stream bitmapStream = file.OpenStream(AttributeType.Bitmap, null, FileAccess.Read))
{
Bitmap bitmap = new Bitmap(bitmapStream, long.MaxValue);
long index = 0;
while (mftStream.Position < mftStream.Length)
{
byte[] recordData = Utilities.ReadFully(mftStream, recordLength);
string magic = Utilities.BytesToString(recordData, 0, 4);
if (magic != "FILE")
{
if (bitmap.IsPresent(index))
{
ReportError("Invalid MFT record magic at index {0} - was ({2},{3},{4},{5}) \"{1}\"", index, magic.Trim('\0'), (int)magic[0], (int)magic[1], (int)magic[2], (int)magic[3]);
}
}
else
{
if (!VerifyMftRecord(recordData, bitmap.IsPresent(index), bytesPerSector))
{
ReportError("Invalid MFT record at index {0}", index);
StringBuilder bldr = new StringBuilder();
for (int i = 0; i < recordData.Length; ++i)
{
bldr.Append(string.Format(CultureInfo.InvariantCulture, " {0:X2}", recordData[i]));
}
ReportInfo("MFT record binary data for index {0}:{1}", index, bldr.ToString());
}
}
index++;
}
}
}
private void VerifyMft()
{
// Cluster allocation check - check for double allocations
Dictionary<long, string> clusterMap = new Dictionary<long, string>();
foreach (FileRecord fr in _context.Mft.Records)
{
if ((fr.Flags & FileRecordFlags.InUse) != 0)
{
File f = new File(_context, fr);
foreach (NtfsAttribute attr in f.AllAttributes)
{
string attrKey = fr.MasterFileTableIndex + ":" + attr.Id;
foreach (var range in attr.GetClusters())
{
if (!VerifyClusterRange(range))
{
ReportError("Attribute {0} contains bad cluster range {1}", attrKey, range);
}
for (long cluster = range.Offset; cluster < range.Offset + range.Count; ++cluster)
{
string existingKey;
if (clusterMap.TryGetValue(cluster, out existingKey))
{
ReportError("Two attributes referencing cluster {0} (0x{0:X16}) - {1} and {2} (as MftIndex:AttrId)", cluster, existingKey, attrKey);
}
}
}
}
}
}
}
private bool VerifyMftRecord(byte[] recordData, bool presentInBitmap, int bytesPerSector)
{
bool ok = true;
//
// Verify the attributes seem OK...
//
byte[] tempBuffer = new byte[recordData.Length];
Array.Copy(recordData, tempBuffer, tempBuffer.Length);
GenericFixupRecord genericRecord = new GenericFixupRecord(bytesPerSector);
genericRecord.FromBytes(tempBuffer, 0);
int pos = Utilities.ToUInt16LittleEndian(genericRecord.Content, 0x14);
while (Utilities.ToUInt32LittleEndian(genericRecord.Content, pos) != 0xFFFFFFFF)
{
int attrLen;
try
{
AttributeRecord ar = AttributeRecord.FromBytes(genericRecord.Content, pos, out attrLen);
if (attrLen != ar.Size)
{
ReportError("Attribute size is different to calculated size. AttrId={0}", ar.AttributeId);
ok = false;
}
if (ar.IsNonResident)
{
NonResidentAttributeRecord nrr = (NonResidentAttributeRecord)ar;
if (nrr.DataRuns.Count > 0)
{
long totalVcn = 0;
foreach (var run in nrr.DataRuns)
{
totalVcn += run.RunLength;
}
if (totalVcn != nrr.LastVcn - nrr.StartVcn + 1)
{
ReportError("Declared VCNs doesn't match data runs. AttrId={0}", ar.AttributeId);
ok = false;
}
}
}
}
catch
{
ReportError("Failure parsing attribute at pos={0}", pos);
return false;
}
pos += attrLen;
}
//
// Now consider record as a whole
//
FileRecord record = new FileRecord(bytesPerSector);
record.FromBytes(recordData, 0);
bool inUse = (record.Flags & FileRecordFlags.InUse) != 0;
if (inUse != presentInBitmap)
{
ReportError("MFT bitmap and record in-use flag don't agree. Mft={0}, Record={1}", presentInBitmap ? "InUse" : "Free", inUse ? "InUse" : "Free");
ok = false;
}
if (record.Size != record.RealSize)
{
ReportError("MFT record real size is different to calculated size. Stored in MFT={0}, Calculated={1}", record.RealSize, record.Size);
ok = false;
}
if (Utilities.ToUInt32LittleEndian(recordData, (int)record.RealSize - 8) != uint.MaxValue)
{
ReportError("MFT record is not correctly terminated with 0xFFFFFFFF");
ok = false;
}
return ok;
}
private bool VerifyClusterRange(Range<long, long> range)
{
bool ok = true;
if (range.Offset < 0)
{
ReportError("Invalid cluster range {0} - negative start", range);
ok = false;
}
if (range.Count <= 0)
{
ReportError("Invalid cluster range {0} - negative/zero count", range);
ok = false;
}
if ((range.Offset + range.Count) * _context.BiosParameterBlock.BytesPerCluster > _context.RawStream.Length)
{
ReportError("Invalid cluster range {0} - beyond end of disk", range);
ok = false;
}
return ok;
}
private void ReportDump(IDiagnosticTraceable toDump)
{
_levelsDetected |= ReportLevels.Information;
if ((_reportLevels & ReportLevels.Information) != 0)
{
toDump.Dump(_report, "INFO: ");
}
}
private void ReportInfo(string str, params object[] args)
{
_levelsDetected |= ReportLevels.Information;
if ((_reportLevels & ReportLevels.Information) != 0)
{
_report.WriteLine("INFO: " + str, args);
}
}
private void ReportError(string str, params object[] args)
{
_levelsDetected |= ReportLevels.Errors;
if ((_reportLevels & ReportLevels.Errors) != 0)
{
_report.WriteLine("ERROR: " + str, args);
}
}
[Serializable]
private sealed class AbortException : InvalidFileSystemException
{
public AbortException()
: base()
{
}
private AbortException(SerializationInfo info, StreamingContext ctxt)
: base(info, ctxt)
{
}
}
}
}