完善 Windows 运维封装与 NTFS 索引解析

- 新增自启动幂等配置、统一错误语义、进程等待和进程树终止能力
- 增强服务生命周期管理,支持等待状态、重启、幂等创建和配置更新
- 新增 NTFS 卷索引、文件 ID 解析、文件遍历、USN 变更监听和 bookmark 持久化
- 修复 NTFS boot sector、fragment、MFT、USN 解析边界和路径重建问题
- 补充权限、进程、服务、NTFS 解析和工作流回归测试
- 增加 Windows 测试脚本和管理员 NTFS smoke 验证脚本
- 升级 Go 兼容版本到 1.18,并更新 stario、win32api 及相关间接依赖
This commit is contained in:
2026-06-09 15:59:31 +08:00
parent feb1a21da8
commit 7e6cc73106
31 changed files with 4937 additions and 981 deletions
+279 -150
View File
@@ -1,14 +1,15 @@
/*
Package mft provides functions to parse records and their attributes in an NTFS Master File Table ("MFT" for short).
Package mft provides functions to parse records and their attributes in an NTFS Master File Table ("MFT" for short).
Basic usage
# Basic usage
First parse a record using mft.ParseRecord(), which parses the record header and the attribute headers. Then parse
each attribute's data individually using the various mft.Parse...() functions.
// Error handling left out for brevity
record, err := mft.ParseRecord()
attrs, err := record.FindAttributes(mft.AttributeTypeFileName)
fileName, err := mft.ParseFileName(attrs[0])
First parse a record using mft.ParseRecord(), which parses the record header and the attribute headers. Then parse
each attribute's data individually using the various mft.Parse...() functions.
// Error handling left out for brevity
record, err := mft.ParseRecord()
attrs, err := record.FindAttributes(mft.AttributeTypeFileName)
fileName, err := mft.ParseFileName(attrs[0])
*/
package mft
@@ -26,7 +27,42 @@ var (
fileSignature = []byte{0x46, 0x49, 0x4c, 0x45}
)
const maxInt = int64(^uint(0) >> 1)
const (
maxInt = int64(^uint(0) >> 1)
minRecordHeaderLength = 42
minAttributeDataLength = 22
minAttributeListHeader = 8
minAttributeTypeLength = 4
dataRunTerminatorLength = 1
)
type recordHeader struct {
signature []byte
fileReference FileReference
baseRecordReference FileReference
logFileSequence uint64
hardLinkCount int
flags RecordFlag
actualSize uint32
allocatedSize uint32
nextAttributeID int
firstAttributeOffset int
}
type attributeHeader struct {
attrType AttributeType
resident bool
name string
flags AttributeFlags
attributeID int
payloadOffset int
}
type attributePayload struct {
allocatedSize uint64
actualSize uint64
data []byte
}
// A Record represents an MFT entry, excluding all technical data (such as "offset to first attribute"). The Attributes
// list only contains the attribute headers and raw data; the attribute data has to be parsed separately. When this is a
@@ -48,51 +84,68 @@ type Record struct {
// ParseRecord parses bytes into a Record after applying fixup. The data is assumed to be in Little Endian order. Only
// the attribute headers are parsed, not the actual attribute data.
func ParseRecord(b []byte) (Record, error) {
if len(b) < 42 {
return Record{}, fmt.Errorf("record data length should be at least 42 but is %d", len(b))
}
sig := b[:4]
if bytes.Compare(sig, fileSignature) != 0 {
return Record{}, fmt.Errorf("unknown record signature: %# x", sig)
}
b = binutil.Duplicate(b)
r := binutil.NewLittleEndianReader(b)
baseRecordRef, err := ParseFileReference(r.Read(0x20, 8))
header, data, err := parseRecordHeader(b)
if err != nil {
return Record{}, fmt.Errorf("unable to parse base record reference: %v", err)
return Record{}, err
}
firstAttributeOffset := int(r.Uint16(0x14))
if firstAttributeOffset < 0 || firstAttributeOffset >= len(b) {
return Record{}, fmt.Errorf("invalid first attribute offset %d (data length: %d)", firstAttributeOffset, len(b))
}
updateSequenceOffset := int(r.Uint16(0x04))
updateSequenceSize := int(r.Uint16(0x06))
b, err = applyFixUp(b, updateSequenceOffset, updateSequenceSize)
if err != nil {
return Record{}, fmt.Errorf("unable to apply fixup: %v", err)
}
attributes, err := ParseAttributes(b[firstAttributeOffset:])
attributes, err := ParseAttributes(data[header.firstAttributeOffset:])
if err != nil {
return Record{}, err
}
return Record{
Signature: binutil.Duplicate(sig),
FileReference: FileReference{RecordNumber: uint64(r.Uint32(0x2C)), SequenceNumber: r.Uint16(0x10)},
BaseRecordReference: baseRecordRef,
LogFileSequenceNumber: r.Uint64(0x08),
HardLinkCount: int(r.Uint16(0x12)),
Flags: RecordFlag(r.Uint16(0x16)),
ActualSize: r.Uint32(0x18),
AllocatedSize: r.Uint32(0x1C),
NextAttributeId: int(r.Uint16(0x28)),
Signature: header.signature,
FileReference: header.fileReference,
BaseRecordReference: header.baseRecordReference,
LogFileSequenceNumber: header.logFileSequence,
HardLinkCount: header.hardLinkCount,
Flags: header.flags,
ActualSize: header.actualSize,
AllocatedSize: header.allocatedSize,
NextAttributeId: header.nextAttributeID,
Attributes: attributes,
}, nil
}
func parseRecordHeader(b []byte) (recordHeader, []byte, error) {
if len(b) < minRecordHeaderLength {
return recordHeader{}, nil, fmt.Errorf("record data length should be at least %d but is %d", minRecordHeaderLength, len(b))
}
if !bytes.Equal(b[:4], fileSignature) {
return recordHeader{}, nil, fmt.Errorf("unknown record signature: %# x", b[:4])
}
data := binutil.Duplicate(b)
r := binutil.NewLittleEndianReader(data)
baseRecordRef, err := ParseFileReference(r.Read(0x20, 8))
if err != nil {
return recordHeader{}, nil, fmt.Errorf("unable to parse base record reference: %v", err)
}
firstAttributeOffset := int(r.Uint16(0x14))
if firstAttributeOffset < 0 || firstAttributeOffset >= len(data) {
return recordHeader{}, nil, fmt.Errorf("invalid first attribute offset %d (data length: %d)", firstAttributeOffset, len(data))
}
if _, err := applyFixUp(data, int(r.Uint16(0x04)), int(r.Uint16(0x06))); err != nil {
return recordHeader{}, nil, fmt.Errorf("unable to apply fixup: %v", err)
}
return recordHeader{
signature: binutil.Duplicate(data[:4]),
fileReference: FileReference{RecordNumber: uint64(r.Uint32(0x2C)), SequenceNumber: r.Uint16(0x10)},
baseRecordReference: baseRecordRef,
logFileSequence: r.Uint64(0x08),
hardLinkCount: int(r.Uint16(0x12)),
flags: RecordFlag(r.Uint16(0x16)),
actualSize: r.Uint32(0x18),
allocatedSize: r.Uint32(0x1C),
nextAttributeID: int(r.Uint16(0x28)),
firstAttributeOffset: firstAttributeOffset,
}, data, nil
}
// A FileReference represents a reference to an MFT record. Since the FileReference in a Record is only 4 bytes, the
// RecordNumber will probably not exceed 32 bits.
type FileReference struct {
@@ -102,10 +155,8 @@ type FileReference struct {
func (f FileReference) ToUint64() uint64 {
origin := make([]byte, 8)
binary.LittleEndian.PutUint16(origin, f.SequenceNumber)
origin[6] = origin[0]
origin[7] = origin[1]
binary.LittleEndian.PutUint32(origin, uint32(f.RecordNumber))
binary.LittleEndian.PutUint64(origin, f.RecordNumber)
binary.LittleEndian.PutUint16(origin[6:], f.SequenceNumber)
return binary.LittleEndian.Uint64(origin)
}
@@ -117,7 +168,7 @@ func ParseFileReference(b []byte) (FileReference, error) {
}
return FileReference{
RecordNumber: binary.LittleEndian.Uint64(padTo(b[:6], 8)),
RecordNumber: binary.LittleEndian.Uint64(padToUnsigned(b[:6], 8)),
SequenceNumber: binary.LittleEndian.Uint16(b[6:]),
}, nil
}
@@ -139,19 +190,45 @@ func (f *RecordFlag) Is(c RecordFlag) bool {
}
func applyFixUp(b []byte, offset int, length int) ([]byte, error) {
if offset < 0 {
return nil, fmt.Errorf("update sequence offset %d is negative", offset)
}
if length < 2 {
return nil, fmt.Errorf("update sequence length %d is too small", length)
}
updateSequenceLength := length * 2
if offset > len(b) || updateSequenceLength > len(b)-offset {
return nil, fmt.Errorf("update sequence range [%d:%d] exceeds record length %d", offset, offset+updateSequenceLength, len(b))
}
r := binutil.NewLittleEndianReader(b)
updateSequence := r.Read(offset, length*2) // length is in pairs, not bytes
updateSequence := r.Read(offset, updateSequenceLength) // length is in pairs, not bytes
updateSequenceNumber := updateSequence[:2]
updateSequenceArray := updateSequence[2:]
if len(updateSequenceArray) == 0 || len(updateSequenceArray)%2 != 0 {
return nil, fmt.Errorf("invalid update sequence array length %d", len(updateSequenceArray))
}
sectorCount := len(updateSequenceArray) / 2
if sectorCount == 0 {
return nil, fmt.Errorf("update sequence does not contain any sector entries")
}
if len(b)%sectorCount != 0 {
return nil, fmt.Errorf("record length %d is not divisible by sector count %d", len(b), sectorCount)
}
sectorSize := len(b) / sectorCount
if sectorSize < 2 {
return nil, fmt.Errorf("invalid sector size %d", sectorSize)
}
for i := 1; i <= sectorCount; i++ {
offset := sectorSize*i - 2
if bytes.Compare(updateSequenceNumber, b[offset:offset+2]) != 0 {
return nil, fmt.Errorf("update sequence mismatch at pos %d", offset)
sectorOffset := sectorSize*i - 2
if sectorOffset < 0 || sectorOffset+2 > len(b) {
return nil, fmt.Errorf("invalid sector offset %d for record length %d", sectorOffset, len(b))
}
if !bytes.Equal(updateSequenceNumber, b[sectorOffset:sectorOffset+2]) {
return nil, fmt.Errorf("update sequence mismatch at pos %d", sectorOffset)
}
}
@@ -237,99 +314,129 @@ func ParseAttributes(b []byte) ([]Attribute, error) {
}
attributes := make([]Attribute, 0)
for len(b) > 0 {
if len(b) < 4 {
return nil, fmt.Errorf("attribute header data should be at least 4 bytes but is %d", len(b))
recordData, remaining, done, err := nextAttributeRecordData(b)
if err != nil {
return nil, err
}
r := binutil.NewLittleEndianReader(b)
attrType := r.Uint32(0)
if attrType == uint32(AttributeTypeTerminator) {
if done {
break
}
if len(b) < 8 {
return nil, fmt.Errorf("cannot read attribute header record length, data should be at least 8 bytes but is %d", len(b))
}
uRecordLength := r.Uint32(0x04)
if int64(uRecordLength) > maxInt {
return nil, fmt.Errorf("record length %d overflows maximum int value %d", uRecordLength, maxInt)
}
recordLength := int(uRecordLength)
if recordLength <= 0 {
return nil, fmt.Errorf("cannot handle attribute with zero or negative record length %d", recordLength)
}
if recordLength > len(b) {
return nil, fmt.Errorf("attribute record length %d exceeds data length %d", recordLength, len(b))
}
recordData := r.Read(0, recordLength)
attribute, err := ParseAttribute(recordData)
if err != nil {
return nil, err
}
attributes = append(attributes, attribute)
b = r.ReadFrom(recordLength)
b = remaining
}
return attributes, nil
}
func nextAttributeRecordData(b []byte) (recordData []byte, remaining []byte, done bool, err error) {
if len(b) < minAttributeTypeLength {
return nil, nil, false, fmt.Errorf("attribute header data should be at least %d bytes but is %d", minAttributeTypeLength, len(b))
}
r := binutil.NewLittleEndianReader(b)
if AttributeType(r.Uint32(0)) == AttributeTypeTerminator {
return nil, nil, true, nil
}
if len(b) < minAttributeListHeader {
return nil, nil, false, fmt.Errorf("cannot read attribute header record length, data should be at least %d bytes but is %d", minAttributeListHeader, len(b))
}
uRecordLength := r.Uint32(0x04)
if int64(uRecordLength) > maxInt {
return nil, nil, false, fmt.Errorf("record length %d overflows maximum int value %d", uRecordLength, maxInt)
}
recordLength := int(uRecordLength)
if recordLength <= 0 {
return nil, nil, false, fmt.Errorf("cannot handle attribute with zero or negative record length %d", recordLength)
}
if recordLength > len(b) {
return nil, nil, false, fmt.Errorf("attribute record length %d exceeds data length %d", recordLength, len(b))
}
return r.Read(0, recordLength), r.ReadFrom(recordLength), false, nil
}
// ParseAttribute parses bytes into an Attribute. The data is assumed to be in Little Endian order. Only the attribute
// headers are parsed, not the actual attribute data.
func ParseAttribute(b []byte) (Attribute, error) {
if len(b) < 22 {
return Attribute{}, fmt.Errorf("attribute data should be at least 22 bytes but is %d", len(b))
if len(b) < minAttributeDataLength {
return Attribute{}, fmt.Errorf("attribute data should be at least %d bytes but is %d", minAttributeDataLength, len(b))
}
r := binutil.NewLittleEndianReader(b)
nameLength := r.Byte(0x09)
nameOffset := r.Uint16(0x0A)
name := ""
if nameLength != 0 {
nameBytes := r.Read(int(nameOffset), int(nameLength)*2)
name = utf16.DecodeString(nameBytes, binary.LittleEndian)
header, err := parseAttributeHeader(r, b)
if err != nil {
return Attribute{}, err
}
resident := r.Byte(0x08) == 0x00
var attributeData []byte
actualSize := uint64(0)
allocatedSize := uint64(0)
if resident {
dataOffset := int(r.Uint16(0x14))
uDataLength := r.Uint32(0x10)
if int64(uDataLength) > maxInt {
return Attribute{}, fmt.Errorf("attribute data length %d overflows maximum int value %d", uDataLength, maxInt)
}
dataLength := int(uDataLength)
expectedDataLength := dataOffset + dataLength
if len(b) < expectedDataLength {
return Attribute{}, fmt.Errorf("expected attribute data length to be at least %d but is %d", expectedDataLength, len(b))
}
attributeData = r.Read(dataOffset, dataLength)
} else {
dataOffset := int(r.Uint16(0x20))
if len(b) < dataOffset {
return Attribute{}, fmt.Errorf("expected attribute data length to be at least %d but is %d", dataOffset, len(b))
}
allocatedSize = r.Uint64(0x28)
actualSize = r.Uint64(0x30)
attributeData = r.ReadFrom(int(dataOffset))
payload, err := parseAttributePayload(r, b, header)
if err != nil {
return Attribute{}, err
}
return Attribute{
Type: AttributeType(r.Uint32(0)),
Resident: resident,
Name: name,
Flags: AttributeFlags(r.Uint16(0x0C)),
AttributeId: int(r.Uint16(0x0E)),
AllocatedSize: allocatedSize,
ActualSize: actualSize,
Data: binutil.Duplicate(attributeData),
Type: header.attrType,
Resident: header.resident,
Name: header.name,
Flags: header.flags,
AttributeId: header.attributeID,
AllocatedSize: payload.allocatedSize,
ActualSize: payload.actualSize,
Data: binutil.Duplicate(payload.data),
}, nil
}
func parseAttributeHeader(r *binutil.BinReader, b []byte) (attributeHeader, error) {
nameLength := int(r.Byte(0x09))
nameOffset := int(r.Uint16(0x0A))
name := ""
if nameLength != 0 {
nameEnd := nameOffset + nameLength*2
if len(b) < nameEnd {
return attributeHeader{}, fmt.Errorf("expected attribute name length to be at least %d but is %d", nameEnd, len(b))
}
name = utf16.DecodeString(r.Read(nameOffset, nameLength*2), binary.LittleEndian)
}
resident := r.Byte(0x08) == 0x00
payloadOffset := int(r.Uint16(0x20))
if resident {
payloadOffset = int(r.Uint16(0x14))
}
return attributeHeader{
attrType: AttributeType(r.Uint32(0)),
resident: resident,
name: name,
flags: AttributeFlags(r.Uint16(0x0C)),
attributeID: int(r.Uint16(0x0E)),
payloadOffset: payloadOffset,
}, nil
}
func parseAttributePayload(r *binutil.BinReader, b []byte, header attributeHeader) (attributePayload, error) {
if header.resident {
uDataLength := r.Uint32(0x10)
if int64(uDataLength) > maxInt {
return attributePayload{}, fmt.Errorf("attribute data length %d overflows maximum int value %d", uDataLength, maxInt)
}
dataLength := int(uDataLength)
expectedDataLength := header.payloadOffset + dataLength
if len(b) < expectedDataLength {
return attributePayload{}, fmt.Errorf("expected attribute data length to be at least %d but is %d", expectedDataLength, len(b))
}
return attributePayload{data: r.Read(header.payloadOffset, dataLength)}, nil
}
if len(b) < header.payloadOffset {
return attributePayload{}, fmt.Errorf("expected attribute data length to be at least %d but is %d", header.payloadOffset, len(b))
}
return attributePayload{
allocatedSize: r.Uint64(0x28),
actualSize: r.Uint64(0x30),
data: r.ReadFrom(header.payloadOffset),
}, nil
}
@@ -350,38 +457,45 @@ func ParseDataRuns(b []byte) ([]DataRun, error) {
runs := make([]DataRun, 0)
for len(b) > 0 {
r := binutil.NewLittleEndianReader(b)
header := r.Byte(0)
if header == 0 {
run, consumed, done, err := parseDataRun(b)
if err != nil {
return nil, err
}
if done {
break
}
lengthLength := int(header &^ 0xF0)
offsetLength := int(header >> 4)
dataRunDataLength := offsetLength + lengthLength
headerAndDataLength := dataRunDataLength + 1
if len(b) < headerAndDataLength {
return nil, fmt.Errorf("expected at least %d bytes of datarun data but is %d", headerAndDataLength, len(b))
}
dataRunData := r.Reader(1, dataRunDataLength)
lengthBytes := dataRunData.Read(0, lengthLength)
dataLength := binary.LittleEndian.Uint64(padTo(lengthBytes, 8))
offsetBytes := dataRunData.Read(lengthLength, offsetLength)
dataOffset := int64(binary.LittleEndian.Uint64(padTo(offsetBytes, 8)))
runs = append(runs, DataRun{OffsetCluster: dataOffset, LengthInClusters: dataLength})
b = r.ReadFrom(headerAndDataLength)
runs = append(runs, run)
b = b[consumed:]
}
return runs, nil
}
func parseDataRun(b []byte) (DataRun, int, bool, error) {
r := binutil.NewLittleEndianReader(b)
header := r.Byte(0)
if header == 0 {
return DataRun{}, dataRunTerminatorLength, true, nil
}
lengthLength := int(header &^ 0xF0)
offsetLength := int(header >> 4)
dataRunDataLength := offsetLength + lengthLength
headerAndDataLength := dataRunDataLength + dataRunTerminatorLength
if len(b) < headerAndDataLength {
return DataRun{}, 0, false, fmt.Errorf("expected at least %d bytes of datarun data but is %d", headerAndDataLength, len(b))
}
dataRunData := r.Reader(1, dataRunDataLength)
lengthBytes := dataRunData.Read(0, lengthLength)
offsetBytes := dataRunData.Read(lengthLength, offsetLength)
return DataRun{
OffsetCluster: int64(binary.LittleEndian.Uint64(padToSigned(offsetBytes, 8))),
LengthInClusters: binary.LittleEndian.Uint64(padToUnsigned(lengthBytes, 8)),
}, headerAndDataLength, false, nil
}
// DataRunsToFragments transform a list of DataRuns with relative offsets and lengths specified in cluster into a list
// of fragment.Fragment elements with absolute offsets and lengths specified in bytes (for example for use in a
// fragment.Reader). Note that data will probably not align to a cluster exactly so there could be some padding at the
@@ -401,7 +515,7 @@ func DataRunsToFragments(runs []DataRun, bytesPerCluster int) []fragment.Fragmen
return frags
}
func padTo(data []byte, length int) []byte {
func padToUnsigned(data []byte, length int) []byte {
if len(data) > length {
return data
}
@@ -413,7 +527,22 @@ func padTo(data []byte, length int) []byte {
return result
}
copy(result, data)
if data[len(data)-1]&0b10000000 == 0b10000000 {
return result
}
func padToSigned(data []byte, length int) []byte {
if len(data) > length {
return data
}
if len(data) == length {
return data
}
result := make([]byte, length)
if len(data) == 0 {
return result
}
copy(result, data)
if data[len(data)-1]&0x80 != 0 {
for i := len(data); i < length; i++ {
result[i] = 0xFF
}