You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wincmd/ntfs/mft/attributes.go

344 lines
12 KiB
Go

package mft
import (
"encoding/binary"
"fmt"
"time"
"b612.me/wincmd/ntfs/binutil"
"b612.me/wincmd/ntfs/utf16"
)
var (
reallyStrangeEpoch = time.Date(1601, time.January, 1, 0, 0, 0, 0, time.UTC)
)
// StandardInformation represents the data contained in a $STANDARD_INFORMATION attribute.
type StandardInformation struct {
Creation time.Time
FileLastModified time.Time
MftLastModified time.Time
LastAccess time.Time
FileAttributes FileAttribute
MaximumNumberOfVersions uint32
VersionNumber uint32
ClassId uint32
OwnerId uint32
SecurityId uint32
QuotaCharged uint64
UpdateSequenceNumber uint64
}
// ParseStandardInformation parses the data of a $STANDARD_INFORMATION attribute's data (type
// AttributeTypeStandardInformation) into StandardInformation. Note that no additional correctness checks are done, so
// it's up to the caller to ensure the passed data actually represents a $STANDARD_INFORMATION attribute's data.
func ParseStandardInformation(b []byte) (StandardInformation, error) {
if len(b) < 48 {
return StandardInformation{}, fmt.Errorf("expected at least %d bytes but got %d", 48, len(b))
}
r := binutil.NewLittleEndianReader(b)
ownerId := uint32(0)
securityId := uint32(0)
quotaCharged := uint64(0)
updateSequenceNumber := uint64(0)
if len(b) >= 0x30+4 {
ownerId = r.Uint32(0x30)
}
if len(b) >= 0x34+4 {
securityId = r.Uint32(0x34)
}
if len(b) >= 0x38+8 {
quotaCharged = r.Uint64(0x38)
}
if len(b) >= 0x40+8 {
updateSequenceNumber = r.Uint64(0x40)
}
return StandardInformation{
Creation: ConvertFileTime(r.Uint64(0x00)),
FileLastModified: ConvertFileTime(r.Uint64(0x08)),
MftLastModified: ConvertFileTime(r.Uint64(0x10)),
LastAccess: ConvertFileTime(r.Uint64(0x18)),
FileAttributes: FileAttribute(r.Uint32(0x20)),
MaximumNumberOfVersions: r.Uint32(0x24),
VersionNumber: r.Uint32(0x28),
ClassId: r.Uint32(0x2C),
OwnerId: ownerId,
SecurityId: securityId,
QuotaCharged: quotaCharged,
UpdateSequenceNumber: updateSequenceNumber,
}, nil
}
// FileAttribute represents a bit mask of various file attributes.
type FileAttribute uint32
// Bit values for FileAttribute. For example, a normal, hidden file has value 0x0082.
const (
FileAttributeReadOnly FileAttribute = 0x0001
FileAttributeHidden FileAttribute = 0x0002
FileAttributeSystem FileAttribute = 0x0004
FileAttributeArchive FileAttribute = 0x0020
FileAttributeDevice FileAttribute = 0x0040
FileAttributeNormal FileAttribute = 0x0080
FileAttributeTemporary FileAttribute = 0x0100
FileAttributeSparseFile FileAttribute = 0x0200
FileAttributeReparsePoint FileAttribute = 0x0400
FileAttributeCompressed FileAttribute = 0x1000
FileAttributeOffline FileAttribute = 0x1000
FileAttributeNotContentIndexed FileAttribute = 0x2000
FileAttributeEncrypted FileAttribute = 0x4000
)
// Is checks if this FileAttribute's bit mask contains the specified attribute value.
func (a *FileAttribute) Is(c FileAttribute) bool {
return *a&c == c
}
// FileNameNamespace indicates the namespace of a $FILE_NAME attribute's file name.
type FileNameNamespace byte
const (
FileNameNamespacePosix FileNameNamespace = 0
FileNameNamespaceWin32 FileNameNamespace = 1
FileNameNamespaceDos FileNameNamespace = 2
FileNameNamespaceWin32Dos FileNameNamespace = 3
)
// FileName represents the data of a $FILE_NAME attribute. ParentFileReference points to the MFT record that is the
// parent (ie. containing directory of this file). The AllocatedSize and ActualSize may be zero, in which case the file
// size may be found in a $DATA attribute instead (it could also be the ActualSize is zero, while the AllocatedSize does
// contain a value).
type FileName struct {
ParentFileReference FileReference
Creation time.Time
FileLastModified time.Time
MftLastModified time.Time
LastAccess time.Time
AllocatedSize uint64
ActualSize uint64
Flags FileAttribute
ExtendedData uint32
Namespace FileNameNamespace
Name string
}
// ParseFileName parses the data of a $FILE_NAME attribute's data (type AttributeTypeFileName) into FileName. Note that
// no additional correctness checks are done, so it's up to the caller to ensure the passed data actually represents a
// $FILE_NAME attribute's data.
func ParseFileName(b []byte) (FileName, error) {
if len(b) < 66 {
return FileName{}, fmt.Errorf("expected at least %d bytes but got %d", 66, len(b))
}
fileNameLength := int(b[0x40 : 0x40+1][0]) * 2
minExpectedSize := 66 + fileNameLength
if len(b) < minExpectedSize {
return FileName{}, fmt.Errorf("expected at least %d bytes but got %d", minExpectedSize, len(b))
}
r := binutil.NewLittleEndianReader(b)
parentRef, err := ParseFileReference(r.Read(0x00, 8))
if err != nil {
return FileName{}, fmt.Errorf("unable to parse file reference: %v", err)
}
return FileName{
ParentFileReference: parentRef,
Creation: ConvertFileTime(r.Uint64(0x08)),
FileLastModified: ConvertFileTime(r.Uint64(0x10)),
MftLastModified: ConvertFileTime(r.Uint64(0x18)),
LastAccess: ConvertFileTime(r.Uint64(0x20)),
AllocatedSize: r.Uint64(0x28),
ActualSize: r.Uint64(0x30),
Flags: FileAttribute(r.Uint32(0x38)),
ExtendedData: r.Uint32(0x3c),
Namespace: FileNameNamespace(r.Byte(0x41)),
Name: utf16.DecodeString(r.Read(0x42, fileNameLength), binary.LittleEndian),
}, nil
}
// AttributeListEntry represents an entry in an $ATTRIBUTE_LIST attribute. The Type indicates the attribute type, while
// the BaseRecordReference indicates which MFT record the attribute is located in (ie. an "extension record", if it is
// not the same as the one where the $ATTRIBUTE_LIST is located).
type AttributeListEntry struct {
Type AttributeType
Name string
StartingVCN uint64
BaseRecordReference FileReference
AttributeId uint16
}
// ParseAttributeList parses the data of a $ATTRIBUTE_LIST attribute's data (type AttributeTypeAttributeList) into a
// list of AttributeListEntry. Note that no additional correctness checks are done, so it's up to the caller to ensure
// the passed data actually represents a $ATTRIBUTE_LIST attribute's data.
func ParseAttributeList(b []byte) ([]AttributeListEntry, error) {
if len(b) < 26 {
return []AttributeListEntry{}, fmt.Errorf("expected at least %d bytes but got %d", 26, len(b))
}
entries := make([]AttributeListEntry, 0)
for len(b) > 0 {
r := binutil.NewLittleEndianReader(b)
entryLength := int(r.Uint16(0x04))
if len(b) < entryLength {
return entries, fmt.Errorf("expected at least %d bytes remaining for AttributeList entry but is %d", entryLength, len(b))
}
nameLength := int(r.Byte(0x06))
name := ""
if nameLength != 0 {
nameOffset := int(r.Byte(0x07))
name = utf16.DecodeString(r.Read(nameOffset, nameLength*2), binary.LittleEndian)
}
baseRef, err := ParseFileReference(r.Read(0x10, 8))
if err != nil {
return entries, fmt.Errorf("unable to parse base record reference: %v", err)
}
entry := AttributeListEntry{
Type: AttributeType(r.Uint32(0)),
Name: name,
StartingVCN: r.Uint64(0x08),
BaseRecordReference: baseRef,
AttributeId: r.Uint16(0x18),
}
entries = append(entries, entry)
b = r.ReadFrom(entryLength)
}
return entries, nil
}
// CollationType indicates how the entries in an index should be ordered.
type CollationType uint32
const (
CollationTypeBinary CollationType = 0x00000000
CollationTypeFileName CollationType = 0x00000001
CollationTypeUnicodeString CollationType = 0x00000002
CollationTypeNtofsULong CollationType = 0x00000010
CollationTypeNtofsSid CollationType = 0x00000011
CollationTypeNtofsSecurityHash CollationType = 0x00000012
CollationTypeNtofsUlongs CollationType = 0x00000013
)
// IndexRoot represents the data (header and entries) of an $INDEX_ROOT attribute, which typically is the root of a
// directory's B+tree index containing file names of the directory (but could be use for other types of indices, too).
// The AttributeType is the type of attributes that are contained in the entries (currently only $FILE_NAME attributes
// are supported).
type IndexRoot struct {
AttributeType AttributeType
CollationType CollationType
BytesPerRecord uint32
ClustersPerRecord uint32
Flags uint32
Entries []IndexEntry
}
// IndexEntry represents an entry in an B+tree index. Currently only $FILE_NAME attribute entries are supported. The
// FileReference points to the MFT record of the indexed file.
type IndexEntry struct {
FileReference FileReference
Flags uint32
FileName FileName
SubNodeVCN uint64
}
// ParseIndexRoot parses the data of a $INDEX_ROOT attribute's data (type AttributeTypeIndexRoot) into
// IndexRoot. Note that no additional correctness checks are done, so it's up to the caller to ensure the passed data
// actually represents a $INDEX_ROOT attribute's data.
func ParseIndexRoot(b []byte) (IndexRoot, error) {
if len(b) < 32 {
return IndexRoot{}, fmt.Errorf("expected at least %d bytes but got %d", 32, len(b))
}
r := binutil.NewLittleEndianReader(b)
attributeType := AttributeType(r.Uint32(0x00))
if attributeType != AttributeTypeFileName {
return IndexRoot{}, fmt.Errorf("unable to handle attribute type %d (%s) in $INDEX_ROOT", attributeType, attributeType.Name())
}
uTotalSize := r.Uint32(0x14)
if int64(uTotalSize) > maxInt {
return IndexRoot{}, fmt.Errorf("index root size %d overflows maximum int value %d", uTotalSize, maxInt)
}
totalSize := int(uTotalSize)
expectedSize := totalSize + 16
if len(b) < expectedSize {
return IndexRoot{}, fmt.Errorf("expected %d bytes in $INDEX_ROOT but is %d", expectedSize, len(b))
}
entries := []IndexEntry{}
if totalSize >= 16 {
parsed, err := parseIndexEntries(r.Read(0x20, totalSize-16))
if err != nil {
return IndexRoot{}, fmt.Errorf("error parsing index entries: %v", err)
}
entries = parsed
}
return IndexRoot{
AttributeType: attributeType,
CollationType: CollationType(r.Uint32(0x04)),
BytesPerRecord: r.Uint32(0x08),
ClustersPerRecord: r.Uint32(0x0C),
Flags: r.Uint32(0x1C),
Entries: entries,
}, nil
}
func parseIndexEntries(b []byte) ([]IndexEntry, error) {
if len(b) < 13 {
return []IndexEntry{}, fmt.Errorf("expected at least %d bytes but got %d", 13, len(b))
}
entries := make([]IndexEntry, 0)
for len(b) > 0 {
r := binutil.NewLittleEndianReader(b)
entryLength := int(r.Uint16(0x08))
if len(b) < entryLength {
return entries, fmt.Errorf("index entry length indicates %d bytes but got %d", entryLength, len(b))
}
flags := r.Uint32(0x0C)
pointsToSubNode := flags&0b1 != 0
isLastEntryInNode := flags&0b10 != 0
contentLength := int(r.Uint16(0x0A))
fileName := FileName{}
if contentLength != 0 && !isLastEntryInNode {
parsedFileName, err := ParseFileName(r.Read(0x10, contentLength))
if err != nil {
return entries, fmt.Errorf("error parsing $FILE_NAME record in index entry: %v", err)
}
fileName = parsedFileName
}
subNodeVcn := uint64(0)
if pointsToSubNode {
subNodeVcn = r.Uint64(entryLength - 8)
}
fileReference, err := ParseFileReference(r.Read(0x00, 8))
if err != nil {
return entries, fmt.Errorf("unable to file reference: %v", err)
}
entry := IndexEntry{
FileReference: fileReference,
Flags: flags,
FileName: fileName,
SubNodeVCN: subNodeVcn,
}
entries = append(entries, entry)
b = r.ReadFrom(entryLength)
}
return entries, nil
}
// ConvertFileTime converts a Windows "file time" to a time.Time. A "file time" is a 64-bit value that represents the
// number of 100-nanosecond intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal Time
// (UTC). See also: https://docs.microsoft.com/en-us/windows/win32/sysinfo/file-times
func ConvertFileTime(timeValue uint64) time.Time {
dur := time.Duration(int64(timeValue))
r := time.Date(1601, time.January, 1, 0, 0, 0, 0, time.UTC)
for i := 0; i < 100; i++ {
r = r.Add(dur)
}
return r
}