mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-07-08 18:16:24 +00:00
[ntfs] Add support for $Secure.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user