[ntfs] Add support for $Secure.

This commit is contained in:
2026-03-01 17:14:31 +00:00
parent b9fda2e204
commit c968decb19
4 changed files with 225 additions and 12 deletions

View File

@@ -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<uint, byte[]>();
if(majorVer >= 3) LoadSecureDescriptors();
// Initialize caches
_rootDirectoryCache = new Dictionary<string, ulong>();
@@ -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;
}
}
/// <summary>
/// Loads security descriptors from the $Secure system file's $SDS named data stream.
/// Populates <see cref="_securityDescriptors" /> with a mapping from security_id to raw descriptor bytes.
/// </summary>
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<MftRecord>(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<byte>();
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<FoundAttribute> 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<SdsEntryHeader>();
var pos = 0;
while(pos + sdsHeaderSize <= sdsData.Length)
{
SdsEntryHeader entryHeader =
Marshal.ByteArrayToStructureLittleEndian<SdsEntryHeader>(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);
}
}

View File

@@ -57,6 +57,7 @@ public sealed partial class NTFS : IReadOnlyFilesystem
Partition _partition;
Dictionary<string, ulong> _rootDirectoryCache;
uint _sectorsPerCluster;
Dictionary<uint, byte[]> _securityDescriptors;
FileSystemInfo _statfs;
/// <inheritdoc />

View File

@@ -827,6 +827,24 @@ public sealed partial class NTFS
#endregion
#region Nested type: SdsEntryHeader
/// <summary>Security Descriptor Stream ($SDS) entry header in $Secure, 20 bytes</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
readonly struct SdsEntryHeader
{
/// <summary>0x000, Hash of the security descriptor</summary>
public readonly uint hash;
/// <summary>0x004, Security identifier (unique per volume)</summary>
public readonly uint security_id;
/// <summary>0x008, Offset of this entry within the $SDS stream</summary>
public readonly ulong offset;
/// <summary>0x010, Total size of this entry including header, 16-byte aligned</summary>
public readonly uint size;
}
#endregion
#region Nested type: LogClientRecord
/// <summary>LogFile client record</summary>

View File

@@ -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<FoundAttribute> 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<StandardInformationV3>() &&
valueStart + Marshal.SizeOf<StandardInformationV3>() <= attr.RecordData.Length)
{
StandardInformationV3 stdInfo =
Marshal.ByteArrayToStructureLittleEndian<StandardInformationV3>(attr.RecordData,
valueStart,
Marshal.SizeOf<StandardInformationV3>());
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);
}
/// <summary>Reads the raw security descriptor ($SECURITY_DESCRIPTOR attribute) from an MFT record.</summary>
/// <summary>
/// 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.
/// </summary>
/// <param name="recordData">Raw MFT record data.</param>
/// <param name="header">Parsed MFT record header.</param>
/// <param name="mftRecordNumber">MFT record number (for attribute list traversal).</param>
@@ -194,6 +229,7 @@ public sealed partial class NTFS
/// <returns>Error number indicating success or failure.</returns>
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<FoundAttribute> 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<FoundAttribute> 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<StandardInformationV3>() ||
stdValueStart + Marshal.SizeOf<StandardInformationV3>() > stdAttr.RecordData.Length)
return ErrorNumber.NoSuchExtendedAttribute;
StandardInformationV3 stdInfo =
Marshal.ByteArrayToStructureLittleEndian<StandardInformationV3>(stdAttr.RecordData,
stdValueStart,
Marshal.SizeOf<StandardInformationV3>());
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;
}