MFT和USN
- 缩写
- 前言
- 类型定义
- 属性类型定义
- MFT条目索引
- MFT条目头
- 属性头
- FILE_NAME值内容
- ATTRIBUTE_LIST属性值中记录的属性简要信息
- STANDARD_INFORMATION值内容
- 其它定义
- MFT
- MFT条目属性篇
- USN篇
缩写
MFT : 主文件表(Master File Table)
USN : 更新序列号(Update Sequence Number)
NTFS : NT文件系统(NewTechnology File System)
VCN : 虚拟簇号(Virtual Cluster Number)
LCN : 逻辑簇号(Logical Cluster Number)
前言
最近在实现一个类似Everything的本地搜索引擎,用到MFT和USN那一套东西,网上搜索到的代码,基本能用,但是确不全面,丢失了很多文件,且很多数据的提取不正确,经过一旦时间的研究,写了此文档。
类型定义
下面定义的这些结构体或者宏,来源于MSDN, 和网上能搜索到ntfs.h的定义有很大的区别,但对应结构基本是一致的,因为部分定义和windows库的定义有冲突,最好是使用命名空间包含,或者重命名对应类型定义, 其它用到的结构体定义在winioctl.h中,这里不做累述。
属性类型定义
typedef enum _ATTRIBUTE_TYPE_CODE { //此定义来源于MSDNSTANDARD_INFORMATION = 0x10, //常驻第一个位置ATTRIBUTE_LIST = 0x20, //如果有,则在第二个位置FILE_NAME = 0x30, //文件名OBJECT_ID = 0x40,VOLUME_NAME = 0x60,VOLUME_INFORMATION = 0x70,DATA = 0x80, //文件大小,目录没有该属性,文件一定有该属性INDEX_ROOT = 0x90,INDEX_ALLOCATION = 0xA0,BITMAP = 0xB0,REPARSE_POINT = 0xC0,ATTRIBUTE_END = 0xFFFFFFFF
}ATTRIBUTE_TYPE_CODE;
MFT条目索引
typedef struct _MFT_SEGMENT_REFERENCE {union{struct{ULONGLONG SegmentNumber;};struct{ULONG SegmentNumberLowPart; //MFT条目下标USHORT SegmentNumberHighPart; //没有用,都是0.USHORT SequenceNumber; //本条目复用次数,也没啥用};};
} MFT_SEGMENT_REFERENCE, * PMFT_SEGMENT_REFERENCE;
typedef MFT_SEGMENT_REFERENCE FILE_REFERENCE, * PFILE_REFERENCE;
MFT条目头
typedef struct _MULTI_SECTOR_HEADER { UCHAR Signature[4]; USHORT UpdateSequenceArrayOffset;USHORT UpdateSequenceArraySize;
} MULTI_SECTOR_HEADER, * PMULTI_SECTOR_HEADER;typedef struct _FILE_RECORD_SEGMENT_HEADER {MULTI_SECTOR_HEADER MultiSectorHeader;ULONGLONG Reserved1;USHORT SequenceNumber;USHORT Reserved2;USHORT FirstAttributeOffset;USHORT Flags;ULONG Reserved3[2];FILE_REFERENCE BaseFileRecordSegment;USHORT Reserved4;//UPDATE_SEQUENCE_ARRAY UpdateSequenceArray;
} FILE_RECORD_SEGMENT_HEADER, * PFILE_RECORD_SEGMENT_HEADER;typedef FILE_RECORD_SEGMENT_HEADER FILE_RECORD_HEADER, * PFILE_RECORD_HEADER;
属性头
typedef struct _ATTRIBUTE_RECORD_HEADER {ATTRIBUTE_TYPE_CODE TypeCode;ULONG RecordLength; UCHAR FormCode; UCHAR NameLength;USHORT NameOffset;USHORT Flags; //0x00 常驻 0x01 非常驻USHORT Instance;union {struct { //常驻属性, 值在偏移ValueOffset处ULONG ValueLength; USHORT ValueOffset;UCHAR Reserved[2];} Resident;struct { //非常驻属性,值在簇外VCN LowestVcn;VCN HighestVcn;USHORT MappingPairsOffset;UCHAR Reserved[6];LONGLONG AllocatedLength;LONGLONG FileSize;LONGLONG ValidDataLength;LONGLONG TotalAllocated;} Nonresident;} Form;
} ATTRIBUTE_RECORD_HEADER, * PATTRIBUTE_RECORD_HEADER;
FILE_NAME值内容
typedef struct _FILE_NAME_ATTRIBUTE {FILE_REFERENCE ParentDirectory;ULONGLONG CreationTime;ULONGLONG ChangeTime;ULONGLONG LastWriteTime; ULONGLONG LastAccessTime;ULONGLONG AllocatedSize; ULONGLONG DataSize; //文件大小,但不准确,不要获取此值ULONG FileAttributes; //此属性并非文件属性,只有高位记录了是否为目录ULONG AlignmentOrReserved;UCHAR FileNameLength;UCHAR Flags; //0x10或0位长名字, 0x20为短命字WCHAR FileName[1];
} FILE_NAME_ATTRIBUTE, * PFILE_NAME_ATTRIBUTE;
ATTRIBUTE_LIST属性值中记录的属性简要信息
typedef struct _ATTRIBUTE_LIST_ENTRY {ATTRIBUTE_TYPE_CODE AttributeTypeCode;USHORT RecordLength; //固定为32UCHAR AttributeNameLength;UCHAR AttributeNameOffset;VCN LowestVcn;MFT_SEGMENT_REFERENCE SegmentReference; //属性的详细信息记录所在的条目USHORT Reserved;WCHAR AttributeName[1]; //这玩意没用
} ATTRIBUTE_LIST_ENTRY, * PATTRIBUTE_LIST_ENTRY;
STANDARD_INFORMATION值内容
//MSDN上定义更简单,暂不清楚搜索的字段存储了啥。
typedef struct _ATTRIBUTE_STANDARD_INFORMATION {//MSDN定义为UCHAR Reserved[0x30]UCHAR Reserved[0x20];DWORD FileAttribute; //通过观察,这里记录了除目录属性以外的其它文件属性。UCHAR Reserved1[12];ULONG OwnerId;ULONG SecurityId;
} ATTRIBUTE_STANDARD_INFORMATION, *PATTRIBUTE_STANDARD_INFORMATION;
其它定义
typedef ULONGLONG VCN;
#define RESIDENT_FORM 0x00 //常驻
#define NONRESIDENT_FORM 0x01 //非常驻
MFT
MSDN两篇关于MFT的文档如下:
Master File Table (Local File Systems) - Win32 apps
Master File Table
下面我简单白话一下MFT是个啥玩意:
- MFT类似std::vector, 空间大小只增不减,唯一的区别在于删除文件时,只是标记对应条目空闲,不会对其它的条目做任何操作,std::vector是后面的条目向前偏移,其中MFT单个条目的大小是1024个字节。
- 通过FSCTL_GET_NTFS_FILE_RECODE可以获取MFT指定下标的条目内容,该操作永远返回一个有效的条目,借MSDN的举例是这样子的,如果条目1-9以及15是正在使用的条目, 10-14是空闲的条目,输入15,则返回15号条目内容,输入10-14,则返回9号条目内容,输入1-9,返回输出下标的条目内容。
- 单个条目由条目头和属性列表组成,条目头固定为56个字节,剩余空间,全部用于存储属性列表,如果剩余控件无法存储所有属性时,则会申请多个条目,且会增加ATTRIBUTE_LIST属性。
- 一个属性只会存储在单个条目中,且最终以 0xffffffff 结束。
- 如果单个属性内容无法存储于单个条目时,例如该属性的内容大于1024个字节时(其实应该更小,至少要减去条目头的56个字节),那么该属性会变成非常驻属性,属性的值内容会存储簇外空间,该非常驻属性的值内容存储的簇外空间的地址(有具体算法的)。
- 单个文件如果有多个条目,则会有ATTRIBUTE_LIST这项属性,该属性一定在根条目的第二条属性中,该属性有可能是非常驻(根条目存不下来,就会变成非常驻属性),该条目有序的存储除了当前属性以外,其它所有属性的简要信息(主要是记录了存储该属性的条目在哪儿,总共32个字节)参考上图的第二条属性,可以看出来0x80这条属性并不在该条目中。
- 条目分为3类: 空闲条目、根条目、扩展条目。其中空闲条目是指文件删除时被标记为空闲的条目,该条目随时可能被复用,根条目是指一个文件多个条目对象的第一个条目对象(位置不一定在最前),其它的条目则称其为扩展条目,空闲条目通过任何方法是获取不到的,参考条目头的定义:
typedef struct _MULTI_SECTOR_HEADER { UCHAR Signature[4]; USHORT UpdateSequenceArrayOffset;USHORT UpdateSequenceArraySize;
} MULTI_SECTOR_HEADER, * PMULTI_SECTOR_HEADER;typedef struct _FILE_RECORD_SEGMENT_HEADER {MULTI_SECTOR_HEADER MultiSectorHeader;ULONGLONG Reserved1;USHORT SequenceNumber;USHORT Reserved2;//第一条属性的偏移位置USHORT FirstAttributeOffset;//Flags&0x001: 有效条目, Flags&0x002: 文件索引名存在,//Flags&0x004: MSDN没找到定义,需要规避此类条目USHORT Flags;ULONG Reserved3[2];//BaseFileRecordSegment=0时:为根条目,否则为扩展条目FILE_REFERENCE BaseFileRecordSegment;USHORT Reserved4;//UPDATE_SEQUENCE_ARRAY UpdateSequenceArray;
} FILE_RECORD_SEGMENT_HEADER, * PFILE_RECORD_SEGMENT_HEADER;typedef FILE_RECORD_SEGMENT_HEADER FILE_RECORD_HEADER, * PFILE_RECORD_HEADER;
- 获取条目的内容方法如下:
NTFS_FILE_RECORD_INPUT_BUFFER input;
NTFS_FILE_RECORD_OUTPUT_BUFFER output;
input.FileReferenceNumber.LowPart = someindex;
bRet = ::DeviceIoControl(m_hVolume, FSCTL_GET_NTFS_FILE_RECORD, &input, sizeof(input),m_output, m_output_size, &dwLength, NULL);
if(bRet == FALSE)return; //Nerver be here
if (input.FileReferenceNumber.LowPart != output.FileReferenceNumber.LowPart)return; //输出的条目并非输入条目ID,表示输入条目指向的内容此时为空闲
//下面这一大块东西就是条目内容,条目内容囊括在1024个字节以内。
PFILE_RECORD_HEADER pHeader = (PFILE_RECORD_HEADER)m_output->FileRecordBuffer;
- 通过上诉方法可以看出一个问题,输入时仅仅指定了LowPart, 现在我们在回到MFT条目索引的定义,因为这个需要自定义,而winioctrl.h中用到的类型,有的定义为LARGER_INTERGER, 有的定义为ULONGLONG以及还有其他的,但都是16个字节,但只有下诉这个定义能弄清楚这16个字节到底存了一些啥。
typedef struct _MFT_SEGMENT_REFERENCE {union{struct{ULONGLONG SegmentNumber;};struct{//MFT条目下标ULONG SegmentNumberLowPart; //保留给LowPart,但一个磁盘的条目数不太可能大于2^32个,所以通常都是0.USHORT SegmentNumberHighPart; //本条目复用次数USHORT SequenceNumber; };};
} MFT_SEGMENT_REFERENCE, * PMFT_SEGMENT_REFERENCE;
typedef MFT_SEGMENT_REFERENCE FILE_REFERENCE, * PFILE_REFERENCE;
- 在第8条描述中提到如何获取指定条目的内容,那么遍历如何获取输入的下标了,简单的说就是如何遍历MFT? 遍历MFT有两种方案:
方案1, 计算条目总数,从后往前遍历, 为什么不能从前往后了?在第2条讲到FSCTL_GET_NTFS_FILE_RECODE操作获取的内容是向前提取的,废话少说,直接上代码:
m_output_size = sizeof(NTFS_FILE_RECORD_OUTPUT_BUFFER) + m_volume_data.BytesPerFileRecordSegment - 1;
m_output = (PNTFS_FILE_RECORD_OUTPUT_BUFFER)malloc(m_output_size);
NTFS_FILE_RECORD_INPUT_BUFFER input;
DWORD mft_count = m_volume_data.MftValidDataLength.QuadPart / m_volume_data.BytesPerFileRecordSegment;
BOOL bRet;
DWORD dwLength;
for (DWORD index = mft_count - 1; index >= 16; --index)
{input.FileReferenceNumber.LowPart = index;bRet = ::DeviceIoControl(m_hVolume, FSCTL_GET_NTFS_FILE_RECORD, &input, sizeof(input),m_output, m_output_size, &dwLength, NULL);if (bRet == FALSE){KLOG_ERROR << "enum master file table error!";break;}//下面这行代码很关键,可以跳过空闲条目片段。index = m_output->FileReferenceNumber.LowPart;PFILE_RECORD_HEADER pHeader = (PFILE_RECORD_HEADER)m_output->FileRecordBuffer;//0x001表示有效条目,一定有效,0x002: 文件索引啥的,0x004: 不知道什么鬼//总之要屏掉0x004的条目, MSDN并没有说明0x004。if (pHeader->Flags & 0x004)continue;//do something with pHeader//enummer(index, pHeader);
}
方案2,是MSDN推荐的遍历方案,调用操作FSCTL_ENUM_USN_DATA,代码如下:
m_output_size = sizeof(NTFS_FILE_RECORD_OUTPUT_BUFFER) + m_volume_data.BytesPerFileRecordSegment - 1;
m_output = (PNTFS_FILE_RECORD_OUTPUT_BUFFER)malloc(m_output_size);
MFT_ENUM_DATA mftdata;
mftdata.MinMajorVersion = 2;
mftdata.MaxMajorVersion = 2;
mftdata.LowUsn = 0;
mftdata.HighUsn = ujd.NextUsn;
mftdata.StartFileReferenceNumber = 0;const size_t bufferLength = sizeof(DWORDLONG) + 0x80000;
BYTE recvBuffer[bufferLength];while (true)
{BOOL result = ::DeviceIoControl(hVolume, FSCTL_ENUM_USN_DATA, &mftdata, sizeof(mftdata), recvBuffer, bufferLength, &dwLength, NULL);if (result == FALSE)break;PBYTE begin = ((PBYTE)recvBuffer + sizeof(USN));PBYTE end = ((PBYTE)recvBuffer + dwLength);mftdata.StartFileReferenceNumber = *(USN*)recvBuffer;while (begin < end){PUSN_RECORD record = (PUSN_RECORD)begin;//这里根据索引获取条目内容begin += record->RecordLength;}
}
对比上述这两个方案:
方案1, 遍历效率极快,但是获取到的数据有根条目、扩展条目,非阻塞,暂不清楚此方案还有没有其它隐藏的缺陷。
方案2, 遍历效率慢,获取到条目只有根条目,阻塞(打断点可致系统卡死),多一个数据来源USN_RECODE, 里面有很多数据,但都可以从条目中获取到,比较特别的是FileAttribute,这个属性从条目中获取比较麻烦, 在属性篇中会提到这一点。
MFT条目属性篇
在MFT篇中有提到了部分关于MFT的属性,MFT整个条目,除了56个字节的属性头以外,剩下的空间都用于存储属性,如果一个条目不够,则再申请一个或多个扩展条目用于存储其它的属性,并在在根条目中添加AttributeList属性,参考属性类型定义。这里主要讲一下这些属性:
FILE_NAME: 0x30, 常驻属性,1-n条, 参考FILE_NAME_ATTRIBUTE,一般是1-2条,分别存储长名字和短名字,部分存在n条,表示该文件存在重定向,如果你安装了git,你会发现git.exe重定向了50个文件,他们内容一致,路径不一致(ParentDirectory不一样或者FileName不一样)。
ATTRIBUTE_LIST : 0x20,常驻或者非常驻,0-1条,如果存储多个条目,一定有该属性,在根条目的第二个位置,否则没有该属性,该属性中列出了除了当前属性以外,其它所有所有属性(包括当前条目的属性)的简要信息,里面记录了对应的属性在哪个条目中,参考ATTRIBUTE_LIST_ENTRY。
DATA: 0x80, 常驻或者非常驻, 0-n条, 文件夹一定没有该属性,文件则至少有一条,为什么有多条暂时我也没搞明白,该属性存储的就是文件的内容,你可以创建一个文本,写入几个字符,查看该条目的这条属性,你会发现当前属性为常驻,如果你输入字符数>1024(界限是多少,没弄清楚),再次查看该条目的当前属性,你会发现属性变成非常驻属性,簇外存储仍然是文件的内容。
常驻和非常驻的区别在于属性值存储的位置,原因是因为这条属性无法完整存储在一个条目中,则会将
值存储到簇外,这里着重将一些文件的属性、文件的大小如何获取, 参考FILE_NAME_ATTRIBUTE的定义,你会发现文件的属性和文件的大小都在那儿,但事实上,都是错的,也并非完全错,其中的FileAttributes记录的值,除了低位记录了是否为目录,其它的不准确,其它的属性记录在ATTRIBUTE_STANDARD_INFORMATION;而该属性记录的文件大小,基本不靠谱,参考网上的代码,都是优先取值该属性,如果该属性记录的是0,则从DATA属性中提取,这是错误的,即便不是0,也可能是错的。正确的方法是直接从DATA中提取,如果没有DATA(目录没有该属性)就是0,文件是一定有该属性的,可能在扩展条目中,如果你没找到,一定代码写错了。
USN篇
NTFS系统,除了有主文件表(MFT)还有文件变更日志列表(USN), 里面存储了最近很多很多的变更日志,它的作用,参考MSDN吧, 这里主要是讲一下FCSTL_READ_USN_JOURNAL这个操作,输入的参数定义在winioctl.h, 如下所示:
HANDLE hDevice, //通过CreateFile获得文件/目录/设备的句柄
DWORD dwIoControlCode, //操作行为
LPVOID lpInBuffer, //输入参数的缓存地址
DWORD nInBufferSize, //输入参数的缓存大小
LPVOID lpOutBuffer, //输出参数的缓存地址
DWORD nOutBufferSize, //输出参数的缓存大小
LPDWORD lpBytesReturns, //输出参数的实际使用大小
LPOVERLAPPED lpOverlapped, //用于异步操作,可能是一个函数地址
限于篇幅,这里列出部分操作,所产生的的日志:
- 修改文件: 删除、增加或覆盖(任意操作,都会瞬时连续产生下面3条日志)
USN_REASON_DATA_TRUNCATION
USN_REASON_DATA_EXTEND|USN_REASON_DATA_TRUNCATION
USN_REASON_DATA_EXTEND|USN_REASON_DATA_TRUNCATION|USN_REASON_CLOSE
- 修改文件名称
USN_REASON_DATA_TRUNCATION
USN_REASON_DATA_EXTEND|USN_REASON_DATA_TRUNCATION
USN_REASON_DATA_EXTEND|USN_REASON_DATA_TRUNCATION|USN_REASON_CLOSE
- 删除文件(这个很奇怪,自己去思考,或者调试一下,就知道为什么呢)。
USN_REASON_FILE_CREATEUSN_REASON_DATA_EXTEND|USN_REASON_FILE_CREATEUSN_REASON_DATA_EXTEND|USN_REASON_FILE_CREATE|USN_REASON_CLOSEUSN_REASON_RENAME_OLD_NAMEUSN_REASON_RENAME_NEW_NAMEUSN_REASON_RENAME_NEW_NAME|USN_REASON_CLOSE
- 磁盘内剪切文件
USN_REASON_RENAME_OLD_NAME
USN_REASON_RENAME_NEW_NAME
USN_REASON_RENAME_NEW_NAME|USN_REASON_CLOSE
综上总结:
a. 文件的任何操作,都会产生1-n条记录,且瞬时产生。
b. 部分连续日志,是以USN_REASON_CLOSE。
c. 多个文件的连续日志是不会出现交叉的,尤其是已最后拼接USN_REASON_CLOSE结束的日志。
现在就方便理解那个几个参数:
a. - 该操作可以用于获取现有日志,同时可用于监听后续日志
b. - StartUsn传入下一条即将写入的日志ID,则表示监听新的日志,否则读取已产生并保留的日志。
c. - 因为单条日志小,且产生频率可能很快,因此日志是批量读取的,且写入到缓存的日志都是完整的,且缓存前8个字节,写入的下一条日志的ID,便于日志遍历。
d. - ReasonMask, 过滤日志,与ReturnOnlyOnClose是并行过滤日志的。
e. - ReturnOnlyOnClose, 过滤掉没有USN_RESON_CLOSE的日志。
f. -BytesToWaitFor, 限制写入的大小,如果写满了,即便时间没到,也会直接返回。
g. -TimeOut, 写入第一条日志后,开始计时,指定时间之后,不管写入多少都会直接返回。
监听新日志的代码如下:
下面附上一段监听日志的代码:
void listenUSN(USN startUsn, const USNMonitor& monitor){READ_USN_JOURNAL_DATA data;/** USN_REASON_RENAME_OLD_NAME : 会单独出现,只有从这个消息中可以获取旧名称* USN_REASON_FILE_CREATE : 用代码创建文件时,已经有记录,只要不关闭文件* 就不会收到USN_REASON_CLOSE,需要单独处理USN_REASON_FILE_CREATE* USN_REASON_CLOSE : 其它的修改,一般性会立即发送USN_REASON_CLOSE* 因此这里只处理这3个消息。*/data.ReasonMask = USN_REASON_CLOSE | USN_REASON_RENAME_OLD_NAME;//data.ReasonMask = 0xffffffff; data.MinMajorVersion = 2;data.MaxMajorVersion = 2;data.ReturnOnlyOnClose = FALSE;data.Timeout = -1000 * 10000;data.BytesToWaitFor = 4096;data.UsnJournalID = m_ujd.UsnJournalID;data.StartUsn = (startUsn == 0 ? m_ujd.NextUsn : startUsn);BYTE journalBuffer[8192 + sizeof(USN)];BOOL bRet;DWORD dwLength;NTFS_FILE_RECORD_INPUT_BUFFER input;while (m_bQuit == false){//看看有什么特殊的方法,直接修改特殊的文件,达到提前结束阻塞bRet = ::DeviceIoControl(m_hVolume, FSCTL_READ_USN_JOURNAL, &data, sizeof(data),journalBuffer, 8192 + sizeof(USN), &dwLength, NULL);if (bRet == FALSE){KLOG_ERROR << "monitor usn failed!";break;}data.StartUsn = m_lastUSN = *(USN*)journalBuffer;PBYTE begin = (PBYTE)(journalBuffer + sizeof(USN));PBYTE end = (PBYTE)(journalBuffer + dwLength);do{PUSN_RECORD recoder = (PUSN_RECORD)begin;monitor(recoder); //do something with recoderbegin = begin + recoder->RecordLength;} while (begin < end);}
}