diff --git a/Aaru.Filesystems/NTFS/Mount.cs b/Aaru.Filesystems/NTFS/Mount.cs index f5676fb80..3e8a915e4 100644 --- a/Aaru.Filesystems/NTFS/Mount.cs +++ b/Aaru.Filesystems/NTFS/Mount.cs @@ -216,6 +216,11 @@ public sealed partial class NTFS return ErrorNumber.InvalidArgument; } + // Parse $Secure (MFT record #9) to load centralized security descriptors (NTFS 3.0+) + _securityDescriptors = new Dictionary(); + + if(majorVer >= 3) LoadSecureDescriptors(); + // Initialize caches _rootDirectoryCache = new Dictionary(); @@ -270,6 +275,7 @@ public sealed partial class NTFS if(!_mounted) return ErrorNumber.AccessDenied; _rootDirectoryCache?.Clear(); + _securityDescriptors?.Clear(); _mounted = false; return ErrorNumber.NoError; @@ -346,4 +352,116 @@ public sealed partial class NTFS offset += (int)attrLength; } } + + /// + /// Loads security descriptors from the $Secure system file's $SDS named data stream. + /// Populates with a mapping from security_id to raw descriptor bytes. + /// + void LoadSecureDescriptors() + { + ErrorNumber errno = ReadMftRecord((uint)SystemFileNumber.Secure, out byte[] secureRecord); + + if(errno != ErrorNumber.NoError) + { + AaruLogging.Debug(MODULE_NAME, "Error reading $Secure MFT record: {0}", errno); + + return; + } + + MftRecord secureHeader = Marshal.ByteArrayToStructureLittleEndian(secureRecord); + + if(secureHeader.magic != NtfsRecordMagic.File) return; + + // Read the $SDS stream data (may be non-resident) + byte[] sdsData = null; + + // Try assembling non-resident data runs for $SDS + ErrorNumber runErrno = AssembleNonResidentRuns((uint)SystemFileNumber.Secure, + AttributeType.Data, + "$SDS", + out List<(long offset, long length)> allDataRuns, + out long sdsDataSize, + out _, + out _, + out _); + + if(runErrno == ErrorNumber.NoError && allDataRuns.Count > 0 && sdsDataSize > 0) + { + byte[] readBuf = Array.Empty(); + errno = ReadNonResidentData(allDataRuns, sdsDataSize, ref readBuf); + + if(errno == ErrorNumber.NoError) sdsData = readBuf; + } + else + { + // Resident $SDS (unlikely but handle it) + ErrorNumber findErrno = FindAttributes(secureRecord, + secureHeader, + (uint)SystemFileNumber.Secure, + AttributeType.Data, + "$SDS", + out List sdsResults); + + if(findErrno == ErrorNumber.NoError && sdsResults.Count > 0) + { + FoundAttribute attr = sdsResults[0]; + byte nonRes = attr.RecordData[attr.Offset + 8]; + + if(nonRes == 0) + { + var valueOffset = BitConverter.ToUInt16(attr.RecordData, attr.Offset + 0x14); + var valueLength = BitConverter.ToUInt32(attr.RecordData, attr.Offset + 0x10); + + int valueStart = attr.Offset + valueOffset; + + if(valueStart + valueLength <= attr.RecordData.Length && valueLength > 0) + { + sdsData = new byte[valueLength]; + Array.Copy(attr.RecordData, valueStart, sdsData, 0, valueLength); + } + } + } + } + + if(sdsData == null || sdsData.Length == 0) + { + AaruLogging.Debug(MODULE_NAME, "Could not read $Secure::$SDS data stream"); + + return; + } + + // Parse SDS entries: each has a 20-byte SdsEntryHeader followed by the security descriptor + int sdsHeaderSize = Marshal.SizeOf(); + var pos = 0; + + while(pos + sdsHeaderSize <= sdsData.Length) + { + SdsEntryHeader entryHeader = + Marshal.ByteArrayToStructureLittleEndian(sdsData, pos, sdsHeaderSize); + + // End marker or invalid entry + if(entryHeader.size < sdsHeaderSize || entryHeader.security_id == 0) break; + + // Validate bounds + if(pos + entryHeader.size > sdsData.Length) break; + + int sdSize = (int)entryHeader.size - sdsHeaderSize; + + if(sdSize > 0 && !_securityDescriptors.ContainsKey(entryHeader.security_id)) + { + var sd = new byte[sdSize]; + Array.Copy(sdsData, pos + sdsHeaderSize, sd, 0, sdSize); + _securityDescriptors[entryHeader.security_id] = sd; + } + + // Entries are 16-byte aligned + var totalSize = (int)(entryHeader.size + 15 & ~15u); + + if(totalSize == 0) break; + + pos += totalSize; + } + + AaruLogging.Debug(MODULE_NAME, "Loaded {0} security descriptors from $Secure", _securityDescriptors.Count); + } } \ No newline at end of file diff --git a/Aaru.Filesystems/NTFS/NTFS.cs b/Aaru.Filesystems/NTFS/NTFS.cs index 599e0d246..c39a70416 100644 --- a/Aaru.Filesystems/NTFS/NTFS.cs +++ b/Aaru.Filesystems/NTFS/NTFS.cs @@ -57,6 +57,7 @@ public sealed partial class NTFS : IReadOnlyFilesystem Partition _partition; Dictionary _rootDirectoryCache; uint _sectorsPerCluster; + Dictionary _securityDescriptors; FileSystemInfo _statfs; /// diff --git a/Aaru.Filesystems/NTFS/Structs.cs b/Aaru.Filesystems/NTFS/Structs.cs index 27e9c51bb..504c5c518 100644 --- a/Aaru.Filesystems/NTFS/Structs.cs +++ b/Aaru.Filesystems/NTFS/Structs.cs @@ -827,6 +827,24 @@ public sealed partial class NTFS #endregion +#region Nested type: SdsEntryHeader + + /// Security Descriptor Stream ($SDS) entry header in $Secure, 20 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + readonly struct SdsEntryHeader + { + /// 0x000, Hash of the security descriptor + public readonly uint hash; + /// 0x004, Security identifier (unique per volume) + public readonly uint security_id; + /// 0x008, Offset of this entry within the $SDS stream + public readonly ulong offset; + /// 0x010, Total size of this entry including header, 16-byte aligned + public readonly uint size; + } + +#endregion + #region Nested type: LogClientRecord /// LogFile client record diff --git a/Aaru.Filesystems/NTFS/Xattrs.cs b/Aaru.Filesystems/NTFS/Xattrs.cs index d8c433e13..1e3b53bd1 100644 --- a/Aaru.Filesystems/NTFS/Xattrs.cs +++ b/Aaru.Filesystems/NTFS/Xattrs.cs @@ -71,6 +71,9 @@ public sealed partial class NTFS xattrs = []; + var foundInlineSd = false; + uint securityId = 0; + // Find all attributes across base + extension records ErrorNumber findErrno = FindAllAttributes(mftRecordNumber, out List attrs); @@ -89,7 +92,33 @@ public sealed partial class NTFS { var valueLength = BitConverter.ToUInt32(attr.RecordData, attr.Offset + 0x10); - if(valueLength > 0) xattrs.Add(NT_ACL); + if(valueLength > 0) + { + foundInlineSd = true; + xattrs.Add(NT_ACL); + } + + break; + } + + // Track security_id from $STANDARD_INFORMATION for $Secure lookup + case AttributeType.StandardInformation when nonResident == 0: + { + var valueOffset = BitConverter.ToUInt16(attr.RecordData, attr.Offset + 0x14); + var valueLength = BitConverter.ToUInt32(attr.RecordData, attr.Offset + 0x10); + + int valueStart = attr.Offset + valueOffset; + + if(valueLength >= (uint)Marshal.SizeOf() && + valueStart + Marshal.SizeOf() <= attr.RecordData.Length) + { + StandardInformationV3 stdInfo = + Marshal.ByteArrayToStructureLittleEndian(attr.RecordData, + valueStart, + Marshal.SizeOf()); + + securityId = stdInfo.security_id; + } break; } @@ -128,6 +157,9 @@ public sealed partial class NTFS } } + // If no inline $SECURITY_DESCRIPTOR was found, check $Secure by security_id + if(!foundInlineSd && securityId != 0 && _securityDescriptors.ContainsKey(securityId)) xattrs.Add(NT_ACL); + return ErrorNumber.NoError; } @@ -186,7 +218,10 @@ public sealed partial class NTFS return ReadAlternateDataStream(recordData, header, mftRecordNumber, xattr, ref buf); } - /// Reads the raw security descriptor ($SECURITY_DESCRIPTOR attribute) from an MFT record. + /// + /// Reads the security descriptor for a file, first trying the inline $SECURITY_DESCRIPTOR attribute, + /// then falling back to $Secure via the security_id in $STANDARD_INFORMATION. + /// /// Raw MFT record data. /// Parsed MFT record header. /// MFT record number (for attribute list traversal). @@ -194,6 +229,7 @@ public sealed partial class NTFS /// Error number indicating success or failure. ErrorNumber ReadSecurityDescriptor(byte[] recordData, in MftRecord header, uint mftRecordNumber, ref byte[] buf) { + // Try inline $SECURITY_DESCRIPTOR attribute first ErrorNumber findErrno = FindAttributes(recordData, header, mftRecordNumber, @@ -201,22 +237,62 @@ public sealed partial class NTFS null, out List results); - if(findErrno != ErrorNumber.NoError || results.Count == 0) return ErrorNumber.NoSuchExtendedAttribute; + if(findErrno == ErrorNumber.NoError && results.Count > 0) + { + FoundAttribute attr = results[0]; + byte nonResident = attr.RecordData[attr.Offset + 8]; - FoundAttribute attr = results[0]; - byte nonResident = attr.RecordData[attr.Offset + 8]; + if(nonResident == 0) + { + var valueOffset = BitConverter.ToUInt16(attr.RecordData, attr.Offset + 0x14); + var valueLength = BitConverter.ToUInt32(attr.RecordData, attr.Offset + 0x10); - if(nonResident != 0) return ErrorNumber.NoSuchExtendedAttribute; + int valueStart = attr.Offset + valueOffset; - var valueOffset = BitConverter.ToUInt16(attr.RecordData, attr.Offset + 0x14); - var valueLength = BitConverter.ToUInt32(attr.RecordData, attr.Offset + 0x10); + if(valueStart + valueLength <= attr.RecordData.Length && valueLength > 0) + { + buf = new byte[valueLength]; + Array.Copy(attr.RecordData, valueStart, buf, 0, valueLength); - int valueStart = attr.Offset + valueOffset; + return ErrorNumber.NoError; + } + } + } - if(valueStart + valueLength > attr.RecordData.Length) return ErrorNumber.NoSuchExtendedAttribute; + // Fall back to $Secure via security_id from $STANDARD_INFORMATION + findErrno = FindAttributes(recordData, + header, + mftRecordNumber, + AttributeType.StandardInformation, + null, + out List stdInfoResults); - buf = new byte[valueLength]; - Array.Copy(attr.RecordData, valueStart, buf, 0, valueLength); + if(findErrno != ErrorNumber.NoError || stdInfoResults.Count == 0) return ErrorNumber.NoSuchExtendedAttribute; + + FoundAttribute stdAttr = stdInfoResults[0]; + byte stdNonRes = stdAttr.RecordData[stdAttr.Offset + 8]; + + if(stdNonRes != 0) return ErrorNumber.NoSuchExtendedAttribute; + + var stdValueOffset = BitConverter.ToUInt16(stdAttr.RecordData, stdAttr.Offset + 0x14); + var stdValueLength = BitConverter.ToUInt32(stdAttr.RecordData, stdAttr.Offset + 0x10); + + int stdValueStart = stdAttr.Offset + stdValueOffset; + + if(stdValueLength < (uint)Marshal.SizeOf() || + stdValueStart + Marshal.SizeOf() > stdAttr.RecordData.Length) + return ErrorNumber.NoSuchExtendedAttribute; + + StandardInformationV3 stdInfo = + Marshal.ByteArrayToStructureLittleEndian(stdAttr.RecordData, + stdValueStart, + Marshal.SizeOf()); + + if(stdInfo.security_id == 0 || !_securityDescriptors.TryGetValue(stdInfo.security_id, out byte[] sd)) + return ErrorNumber.NoSuchExtendedAttribute; + + buf = new byte[sd.Length]; + Array.Copy(sd, 0, buf, 0, sd.Length); return ErrorNumber.NoError; }