7e6cc73106
- 新增自启动幂等配置、统一错误语义、进程等待和进程树终止能力 - 增强服务生命周期管理,支持等待状态、重启、幂等创建和配置更新 - 新增 NTFS 卷索引、文件 ID 解析、文件遍历、USN 变更监听和 bookmark 持久化 - 修复 NTFS boot sector、fragment、MFT、USN 解析边界和路径重建问题 - 补充权限、进程、服务、NTFS 解析和工作流回归测试 - 增加 Windows 测试脚本和管理员 NTFS smoke 验证脚本 - 升级 Go 兼容版本到 1.18,并更新 stario、win32api 及相关间接依赖
431 lines
16 KiB
Go
431 lines
16 KiB
Go
package mft
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"time"
|
|
|
|
"b612.me/wincmd/ntfs/binutil"
|
|
"b612.me/wincmd/ntfs/utf16"
|
|
)
|
|
|
|
const (
|
|
minStandardInformationLength = 48
|
|
minFileNameLength = 66
|
|
minAttributeListEntryLength = 26
|
|
minIndexRootLength = 32
|
|
minIndexEntryLength = 13
|
|
indexRootHeaderLength = 16
|
|
indexRootEntryOffset = 0x20
|
|
)
|
|
|
|
// 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) < minStandardInformationLength {
|
|
return StandardInformation{}, fmt.Errorf("expected at least %d bytes but got %d", minStandardInformationLength, len(b))
|
|
}
|
|
|
|
r := binutil.NewLittleEndianReader(b)
|
|
ownerId, securityId, quotaCharged, updateSequenceNumber := parseStandardInformationTail(r, len(b))
|
|
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
|
|
}
|
|
|
|
func parseStandardInformationTail(r *binutil.BinReader, length int) (ownerID uint32, securityID uint32, quotaCharged uint64, updateSequenceNumber uint64) {
|
|
if length >= 0x30+4 {
|
|
ownerID = r.Uint32(0x30)
|
|
}
|
|
if length >= 0x34+4 {
|
|
securityID = r.Uint32(0x34)
|
|
}
|
|
if length >= 0x38+8 {
|
|
quotaCharged = r.Uint64(0x38)
|
|
}
|
|
if length >= 0x40+8 {
|
|
updateSequenceNumber = r.Uint64(0x40)
|
|
}
|
|
return ownerID, securityID, quotaCharged, updateSequenceNumber
|
|
}
|
|
|
|
// 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 = 0x0800
|
|
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) < minFileNameLength {
|
|
return FileName{}, fmt.Errorf("expected at least %d bytes but got %d", minFileNameLength, len(b))
|
|
}
|
|
|
|
fileNameLength := int(b[0x40 : 0x40+1][0]) * 2
|
|
minExpectedSize := minFileNameLength + 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) < minAttributeListEntryLength {
|
|
return []AttributeListEntry{}, fmt.Errorf("expected at least %d bytes but got %d", minAttributeListEntryLength, len(b))
|
|
}
|
|
|
|
entries := make([]AttributeListEntry, 0)
|
|
|
|
for len(b) > 0 {
|
|
entry, entryLength, err := parseAttributeListEntry(b)
|
|
if err != nil {
|
|
return entries, err
|
|
}
|
|
entries = append(entries, entry)
|
|
b = b[entryLength:]
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
func parseAttributeListEntry(b []byte) (AttributeListEntry, int, error) {
|
|
if len(b) < minAttributeListEntryLength {
|
|
return AttributeListEntry{}, 0, fmt.Errorf("expected at least %d bytes but got %d", minAttributeListEntryLength, len(b))
|
|
}
|
|
|
|
r := binutil.NewLittleEndianReader(b)
|
|
entryLength := int(r.Uint16(0x04))
|
|
if entryLength < minAttributeListEntryLength {
|
|
return AttributeListEntry{}, 0, fmt.Errorf("attribute list entry length %d is smaller than minimum %d", entryLength, minAttributeListEntryLength)
|
|
}
|
|
if len(b) < entryLength {
|
|
return AttributeListEntry{}, 0, fmt.Errorf("expected at least %d bytes remaining for AttributeList entry but is %d", entryLength, len(b))
|
|
}
|
|
|
|
name, err := parseAttributeListEntryName(r, b, entryLength)
|
|
if err != nil {
|
|
return AttributeListEntry{}, 0, err
|
|
}
|
|
baseRef, err := ParseFileReference(r.Read(0x10, 8))
|
|
if err != nil {
|
|
return AttributeListEntry{}, 0, fmt.Errorf("unable to parse base record reference: %v", err)
|
|
}
|
|
|
|
return AttributeListEntry{
|
|
Type: AttributeType(r.Uint32(0)),
|
|
Name: name,
|
|
StartingVCN: r.Uint64(0x08),
|
|
BaseRecordReference: baseRef,
|
|
AttributeId: r.Uint16(0x18),
|
|
}, entryLength, nil
|
|
}
|
|
|
|
func parseAttributeListEntryName(r *binutil.BinReader, b []byte, entryLength int) (string, error) {
|
|
nameLength := int(r.Byte(0x06))
|
|
if nameLength == 0 {
|
|
return "", nil
|
|
}
|
|
|
|
nameOffset := int(r.Byte(0x07))
|
|
nameEnd := nameOffset + nameLength*2
|
|
if nameEnd > entryLength || nameEnd > len(b) {
|
|
return "", fmt.Errorf("attribute list entry name exceeds entry boundary: offset=%d length=%d entry=%d", nameOffset, nameLength*2, entryLength)
|
|
}
|
|
return utf16.DecodeString(r.Read(nameOffset, nameLength*2), binary.LittleEndian), 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) {
|
|
header, entryData, err := parseIndexRootHeader(b)
|
|
if err != nil {
|
|
return IndexRoot{}, err
|
|
}
|
|
entries := []IndexEntry{}
|
|
if len(entryData) > 0 {
|
|
parsed, err := parseIndexEntries(entryData)
|
|
if err != nil {
|
|
return IndexRoot{}, fmt.Errorf("error parsing index entries: %v", err)
|
|
}
|
|
entries = parsed
|
|
}
|
|
|
|
return IndexRoot{
|
|
AttributeType: header.AttributeType,
|
|
CollationType: header.CollationType,
|
|
BytesPerRecord: header.BytesPerRecord,
|
|
ClustersPerRecord: header.ClustersPerRecord,
|
|
Flags: header.Flags,
|
|
Entries: entries,
|
|
}, nil
|
|
}
|
|
|
|
func parseIndexRootHeader(b []byte) (IndexRoot, []byte, error) {
|
|
if len(b) < minIndexRootLength {
|
|
return IndexRoot{}, nil, fmt.Errorf("expected at least %d bytes but got %d", minIndexRootLength, len(b))
|
|
}
|
|
r := binutil.NewLittleEndianReader(b)
|
|
attributeType := AttributeType(r.Uint32(0x00))
|
|
if attributeType != AttributeTypeFileName {
|
|
return IndexRoot{}, nil, 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{}, nil, fmt.Errorf("index root size %d overflows maximum int value %d", uTotalSize, maxInt)
|
|
}
|
|
totalSize := int(uTotalSize)
|
|
expectedSize := totalSize + indexRootHeaderLength
|
|
if len(b) < expectedSize {
|
|
return IndexRoot{}, nil, fmt.Errorf("expected %d bytes in $INDEX_ROOT but is %d", expectedSize, len(b))
|
|
}
|
|
entryData := []byte{}
|
|
if totalSize >= indexRootHeaderLength {
|
|
entryData = r.Read(indexRootEntryOffset, totalSize-indexRootHeaderLength)
|
|
}
|
|
return IndexRoot{
|
|
AttributeType: attributeType,
|
|
CollationType: CollationType(r.Uint32(0x04)),
|
|
BytesPerRecord: r.Uint32(0x08),
|
|
ClustersPerRecord: r.Uint32(0x0C),
|
|
Flags: r.Uint32(0x1C),
|
|
}, entryData, nil
|
|
}
|
|
|
|
func parseIndexEntries(b []byte) ([]IndexEntry, error) {
|
|
if len(b) < minIndexEntryLength {
|
|
return []IndexEntry{}, fmt.Errorf("expected at least %d bytes but got %d", minIndexEntryLength, len(b))
|
|
}
|
|
entries := make([]IndexEntry, 0)
|
|
for len(b) > 0 {
|
|
entry, entryLength, err := parseIndexEntry(b)
|
|
if err != nil {
|
|
return entries, err
|
|
}
|
|
entries = append(entries, entry)
|
|
b = b[entryLength:]
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
func parseIndexEntry(b []byte) (IndexEntry, int, error) {
|
|
if len(b) < minIndexEntryLength {
|
|
return IndexEntry{}, 0, fmt.Errorf("expected at least %d bytes but got %d", minIndexEntryLength, len(b))
|
|
}
|
|
|
|
r := binutil.NewLittleEndianReader(b)
|
|
entryLength := int(r.Uint16(0x08))
|
|
if entryLength < minIndexEntryLength {
|
|
return IndexEntry{}, 0, fmt.Errorf("index entry length %d is smaller than minimum %d", entryLength, minIndexEntryLength)
|
|
}
|
|
if len(b) < entryLength {
|
|
return IndexEntry{}, 0, fmt.Errorf("index entry length indicates %d bytes but got %d", entryLength, len(b))
|
|
}
|
|
|
|
flags := r.Uint32(0x0C)
|
|
contentLength := int(r.Uint16(0x0A))
|
|
fileName, err := parseIndexEntryFileName(r, b, entryLength, contentLength, flags)
|
|
if err != nil {
|
|
return IndexEntry{}, 0, err
|
|
}
|
|
subNodeVcn, err := parseIndexEntrySubNodeVCN(r, entryLength, flags)
|
|
if err != nil {
|
|
return IndexEntry{}, 0, err
|
|
}
|
|
fileReference, err := ParseFileReference(r.Read(0x00, 8))
|
|
if err != nil {
|
|
return IndexEntry{}, 0, fmt.Errorf("unable to file reference: %v", err)
|
|
}
|
|
|
|
return IndexEntry{
|
|
FileReference: fileReference,
|
|
Flags: flags,
|
|
FileName: fileName,
|
|
SubNodeVCN: subNodeVcn,
|
|
}, entryLength, nil
|
|
}
|
|
|
|
func parseIndexEntryFileName(r *binutil.BinReader, b []byte, entryLength int, contentLength int, flags uint32) (FileName, error) {
|
|
isLastEntryInNode := flags&0b10 != 0
|
|
if contentLength == 0 || isLastEntryInNode {
|
|
return FileName{}, nil
|
|
}
|
|
|
|
contentEnd := 0x10 + contentLength
|
|
if contentEnd > entryLength || contentEnd > len(b) {
|
|
return FileName{}, fmt.Errorf("index entry content exceeds entry boundary: content=%d entry=%d", contentLength, entryLength)
|
|
}
|
|
fileName, err := ParseFileName(r.Read(0x10, contentLength))
|
|
if err != nil {
|
|
return FileName{}, fmt.Errorf("error parsing $FILE_NAME record in index entry: %v", err)
|
|
}
|
|
return fileName, nil
|
|
}
|
|
|
|
func parseIndexEntrySubNodeVCN(r *binutil.BinReader, entryLength int, flags uint32) (uint64, error) {
|
|
pointsToSubNode := flags&0b1 != 0
|
|
if !pointsToSubNode {
|
|
return 0, nil
|
|
}
|
|
if entryLength < 8 {
|
|
return 0, fmt.Errorf("index entry length %d is too small for sub-node VCN", entryLength)
|
|
}
|
|
return r.Uint64(entryLength - 8), 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 {
|
|
const ticksPerSecond = uint64(10000000)
|
|
const unixOffsetSeconds = int64(-11644473600)
|
|
|
|
seconds := int64(timeValue / ticksPerSecond)
|
|
nanoseconds := int64(timeValue%ticksPerSecond) * 100
|
|
return time.Unix(unixOffsetSeconds+seconds, nanoseconds).UTC()
|
|
}
|