NTFS 解析器库
一个C++库,用于帮助解析NTFS卷、文件记录和属性。
引言
这是一个用于帮助解析NTFS卷、文件记录和属性的库。读者应具备NTFS和C++编程的深入知识。
我不会在这里介绍NTFS概念,因为介绍要么内容庞杂,要么空洞无物。请在此搜索关于NTFS的最佳文档。
作为一个操作系统爱好者,我对自己对文件系统的了解甚少感到羞愧。每次阅读与操作系统相关的书籍时,我总是在“文件系统”章节感到困惑。内容要么过于简洁,不足以深入理解,要么过于枯燥,令人难以继续阅读。于是我决定编写一些短小的代码来了解我的硬盘上发生了什么。我选择了NTFS,因为它是我电脑上的文件系统,而且几乎每个人都说它是一个好的设计,至少不是一个坏的设计。
起初,这非常痛苦,因为可用的文档非常少。微软并没有公开其所谓的“新一代文件系统”。只能在网上找到零散的信息。经过几天的研究收集到的文档,我头脑中的迷雾逐渐散去。经过一些成功的测试,我认为可以编写一个库来方便NTFS解析,同时也加深我的理解。
Windows NT试图构建一个面向对象的操作系统。起初,我在选择使用C++类还是传统的C过程来完成任务时犹豫不决。作为操作系统的重要组成部分,它应该高效、紧凑,并具有可伸缩性和可管理性。操作系统内核必须用C编写。但我正在编写一个用户模式库,在仔细研究了NTFS数据结构后,我决定使用C++类来封装它们。
NTFS是一个先进的日志文件系统,能够满足从家用PC到数据服务器的需求。我尚未实现其所有功能。以下部分尚未支持:
- 日志记录
- 安全
- 加密和压缩
- 其他一些高级功能
演示项目
1. ntfsundel
其目的是搜索和恢复已删除的文件。
这似乎是一项艰巨的任务,但通过使用这个库,我花不到一个小时就实现了它,大部分时间都花在了调整对话框界面上。当然,这更像一个简单的测试程序,而不是商业产品。我没有检查已释放的簇是否已被其他文件修改(这也是商业工具分析大卷时花费大量时间的原因之一)。
2. ntfsdump
转储文件的前16K。由于该库直接从磁盘扇区读取数据,我们可以绕过操作系统保护,查看通常无法访问的文件,例如位于“Windows\System32\config”目录下的文件。
3. ntfsdir
列出子文件和目录。
4. ntfsattr
列出文件或目录的属性。
源代码
1. 源文件
源代码包含五个.h文件。在编写C++代码时,我更倾向于直接在头文件中编写,因为这大大简化了部署,而且看起来也很酷。只需包含.h文件即可完成所有工作,无需将.cpp文件添加到项目中。该库是您自己源代码的一部分,未引用的库源代码会被编译器默默丢弃。当然,用这种方式实现一个大型系统会很困难,当类之间相互引用时。我不知道微软ATL是如何实现这一目标的。
1. NTFS.h
在您的源代码中包含此文件。不需要其他包含。
2. NTFS_DataType.h
NTFS通用数据结构和数据类型定义。没有类,只有结构。
3. NTFS_Common.h
特定于此库的NTFS数据结构和数据类型定义。以及一个单一的列表实现CSList
,用于帮助管理相同类型的对象。
4. NTFS_FileRecord.h
NTFS卷和文件记录类定义和实现。
5. NTFS_Attribute.h
NTFS属性类和辅助类定义和实现。
2. 编码
我作为嵌入式系统设计师已经有大约十年的经验,习惯于有限的系统资源并挖掘硬件的全部潜力(试想一下在8位CPU上以2MIPS运行、只有256字节RAM的情况下实现一个IP堆栈)。如今的PC,RAM和CPU速度已不再是问题,但我仍然保持编写紧凑、尽可能高效和快速运行的代码的习惯。
为了实现这个目标,该库中的许多数据缓冲区在不同对象之间共享。为了完成不同的任务,玩弄指针是必须的,尽管危险。C++通过引入构造函数、析构函数以及复制构造函数来帮助我们管理内存,但这还不够。否则,就不会有所谓的“智能指针”,它只是C++风格的指针技巧(当然,如果你不够“聪明”,它会导致难以发现的“智能”错误)。
我正试图使这个库比简单的测试更有用。源代码和演示项目是在VC6.0 SP6中开发的,也可以在VC10.0中编译。二进制文件已在Windows XP SP3和Windows7中测试。我添加了许多跟踪消息,将在Visual Studio的输出窗口中显示,以帮助调试。该库兼容Unicode,可以编译成ANSI或Unicode二进制文件。定义_UNICODE
以进行Unicode构建。就像NT内核一样,NTFS使用Unicode存储文件名。因此,Unicode构建的运行速度将比ANSI构建快。所有已通过或返回的指针和引用,如果不应被目标修改,都被标记为“const
”。如果我们试图修改这些缓冲区或对象,编译器会发出警告(但我经常通过类型转换为非const指针来违反自己的规则)。我还添加了验证代码,以防止坏参数和错误数据。处理磁盘卷时,你越小心越好。
该库频繁读取磁盘扇区。因此,我将维护一些缓冲区来加快数据访问。尽管操作系统已经通过磁盘缓存帮助了我们,用户模式缓冲区将是额外的优势。
由于它直接访问磁盘扇区,您必须拥有管理员权限才能运行演示项目。在Windows7中,仅获得管理员权限还不够;需要提升的特权。您必须是“Administrator”用户或获得提升的特权才能成功打开卷。此库以只读模式访问磁盘;它**应该**是安全的,并且不会损坏您的磁盘卷。请自行承担使用风险。
NTFS卷和文件记录类
1. CNTFSVolume
此类封装了一个单独的NTFS卷。
volume
是卷名;例如:'C'、'D'。这是唯一的构造函数。它执行以下操作:
用户应在构造函数后立即调用此函数来验证一切是否正常。如果此函数返回FALSE
,则不应进行任何其他处理。
返回此卷中的文件记录数。它不是所有当前文件和目录的总和,因为已删除的文件可能仍占用记录槽。
磁盘物理扇区的大小(字节)。通常为512。从BPB获取。
单个文件记录的大小(字节)。通常为1024。从BPB获取。
索引块的大小(字节)。通常为4096。从BPB获取。
$MFT元文件的相对起始地址。从BPB获取。
返回值:成功时为TRUE
。当attrType
不是有效属性类型时为FALSE
。
安装一个卷范围回调函数,以便在找到特定属性时被调用。可用于在属性流被处理之前查看其原始流。
移除所有卷范围回调函数。
CNTFSVolume(_TCHAR volume)
- 以只读模式打开卷,并获取一个句柄以直接访问磁盘的物理扇区。
- 读取BPB,进行一些验证,并存储所需信息。
- 解析NTFS元文件$Volume,读取并验证NTFS版本。
- 解析NTFS元文件$MFT,获取其$DATA属性以定位$MFT中其他文件记录。NTFS试图通过保留$MFT后的某些缓冲区来保持文件记录的连续性。但在我的八年旧笔记本电脑上,$MFT在系统卷中被分割成三个部分。
BOOL IsVolumeOK() const
ULONGLONG GetRecordsCount() const
DWORD GetSectorSize() const
DWORD GetFileRecordSize() const
DWORD GetIndexBlockSize() const
ULONGLONG GetMFTAddr() const
BOOL InstallAttrRawCB(DWORD attrType, ATTR_RAW_CALLBACK cb)
attrType
:属性类型。cb
:回调函数。
void ClearAttrRawCB()
2. CFileRecord
解析单个文件记录。这是最重要的类。NTFS将几乎所有东西都视为文件,甚至包括引导扇区。
volume
表示此文件记录所属的卷。
fileRef
是要解析的文件的文件引用。
返回值:成功时为TRUE
。否则为FALSE
。当此函数失败时,不应进行任何进一步的处理。
此函数从磁盘读取文件记录,然后验证并修补更新序列号。用户可以逐个解析任意数量的文件。先前解析的数据将被释放。
解析文件记录的选定属性(由SetAttrMask()
例程选择)。这是库中最大且最耗时的例程。所有选定的属性都将被解析为相应的C++对象,并按类型插入单独的列表中。
返回值:成功时为TRUE
。当attrType
无效时为FALSE
。
安装一个文件记录范围回调函数,以便在找到特定属性时被调用。可用于在属性流被处理之前查看其原始流。
当ParseAttrs()
找到一个属性时,它会首先在CFileRecord
中查找已安装的回调函数并调用它。如果找不到,它将继续查找此文件记录所属的CNTFSVolume
对象中安装的回调函数。
移除所有文件记录范围回调函数。
mask
包含要解析的属性。在NTFS_Common.h中定义为MASK_???
。
用户可以选择要解析的属性,并丢弃不需要的属性以节省时间和RAM。例如,如果您只想获取文件大小和时间戳,则无需花费时间解析$DATA
属性。$STANDARD_INFORMATION
和$ATTRIBUTE_LIST
将始终被解析,无论它们是否被选中,但$ATTRIBUTE_LIST
中不需要的属性将被丢弃。
此函数应在ParseAttrs()
之前调用。
此例程遍历文件记录的所有已解析属性,并同步调用用户定义的函数,向用户提供属性的已解析C++对象。
此例程应在ParseAttrs()
之后调用。
查找包含类型“attrType
”的第一个属性。如果找不到类型为“attrType
”的属性,则返回NULL
。调用后,内部索引将移至第一个元素。
此例程应在ParseAttrs()
之后调用。
查找包含类型“attrType
”的下一个属性。如果找不到更多类型为“attrType
”的属性,则返回NULL
。调用后,内部索引将移至下一个。
此例程应在FindFirstAttr()
之后调用。
CAttrBase *ab = FindFirstAttr(ATTR_TYPE_FILENAME)
while (ab)
{
// process ab here
ab = FindNextAttr(ATTR_TYPE_FILENAME);
}
MFC的CFileFind
类设计得非常糟糕且容易出错,所以我没有遵循它的风格。
返回值
一个文件记录可能具有多个文件名($FILE_NAME
属性)。将返回第一个Win32名称。
以字节为单位获取文件大小。从$FILE_NAME
属性获取。
获取文件的最后修改时间、创建时间和最后访问时间。时间已转换为系统中设置的时区。从$STANDARD_INFORMATION
属性获取。
遍历文件记录(目录文件)中所有子条目,并同步调用用户定义的函数,向用户提供由CIndexEntry
类封装的所有子条目。在枚举子文件和目录时很有用。必须已解析$INDEX_ROOT
和$INEX_ALLOCATION
属性(请参阅SetAttrMask()
)。
返回值:找到时为TRUE
,否则为FALSE
。
它用于查找子文件或目录。必须已解析$INDEX_ROOT
和$INEX_ALLOCATION
属性(请参阅SetAttrMask()
)。
name
是文件数据流的名称。未命名流时为NULL
。
通过名称查找特定的数据流。NTFS文件可能具有多个数据流($DATA
属性)。文件内容始终位于未命名流中。必须已解析$DATA
属性(请参阅SetAttrMask()
)。
检查此文件记录是否已被删除。
检查此文件记录是否为目录。
检查它是只读文件。从$STANDARD_INFORMATION
属性获取。
检查它是隐藏文件。从$STANDARD_INFORMATION
属性获取。
检查它是系统文件。从$STANDARD_INFORMATION
属性获取。
检查它是压缩文件。从$STANDARD_INFORMATION
属性获取。
检查它是加密文件。从$STANDARD_INFORMATION
属性获取。
检查它是稀疏文件。从$STANDARD_INFORMATION
属性获取。
CFileRecord(const CNTFSVolume *volume)
BOOL ParseFileRecord(ULONGLONG fileRef)
BOOL ParseAttrs()
BOOL InstallAttrRawCB(DWORD attrType, ATTR_RAW_CALLBACK cb)
attrType
:属性类型。cb
:回调函数。
void ClearAttrRawCB()
void SetAttrMask(DWORD mask)
void TraverseAttrs(ATTRS_CALLBACK attrCallBack, void *context)
attrCallBack
:用户定义的函数。context
:要传递给回调函数的上下文。
const CAttrBase* FindFirstAttr(DWORD attrType) const
const CAttrBase* FindNextAttr(DWORD attrType) const
int GetFileName(_TCHAR *buf, DWORD bufLen) const
buf
:用于保存返回的文件名的缓冲区。bufLen
:缓冲区大小(字符数,不是字节数!)。
- 大于0:文件名的长度(字符数)。
- 等于0:此文件未命名。
- 小于0:缓冲区大小小于文件名的所需大小,负值表示所需的缓冲区大小。例如,返回值为-20表示您需要一个至少20个字符大小的缓冲区。
ULONGLONG GetFileSize() const
void GetFileTime(FILETIME *writeTm, FILETIME *createTm = NULL, FILETIME *accessTm = NULL) const
void TraverseSubEntries(SUBENTRY_CALLBACK seCallBack) const
const BOOL FindSubEntry(const _TCHAR *fileName, CIndexEntry &ieFound) const
fileName
:要查找的子文件名。ieFound
:找到的CIndexEntry
对象。
const CAttrBase* FindStream(_TCHAR *name = NULL)
BOOL IsDeleted() const
BOOL IsDirectory() const
BOOL IsReadOnly() const
BOOL IsHidden() const
BOOL IsSystem() const
BOOL IsCompressed() const
BOOL IsEncrypted() const
BOOL IsSparse() const
NTFS属性类
Attributes Class
$STANDARD_INFORMATION CAttr_StdInfo
$ATTRIBUTE_LIST CAttr_AttrList<TYPE_RESIENT>
$FILE_NAME CAttr_FileName
$VOLUME_NAME CAttr_VolName
$VOLUME_INFORMATION CAttr_VolInfo
$DATA CAttr_Data<TYPE_RESIDENT>
$INDEX_ROOT CAttr_IndexRoot
$INDEX_ALLOCATION CAttr_IndexAlloc
$BITMAP CAttr_Bitmap<TYPE_RESIENT>
NTFS属性分为驻留属性(CAttrResident
)和非驻留属性(CAttrNonResident
)。驻留和非驻留属性共享一个通用头(CAttrBase
)。所有属性类都派生自CAttrResident
或CAttrNonResident
,它们又派生自CAttrBase
。某些属性,如$DATA
和$ATTRIBUTE_LIST
,可以是驻留或非驻留的;这些类使用模板参数作为它们的基类。
1. CAttrBase
所有属性类的基类。
allocSize
是数据分配的大小(字节)。如果您不想要,请将此参数留空。
返回值:数据的实际大小(字节)。
获取此属性数据的大小(字节)。它被声明为纯虚函数。派生类CAttrResident
和CAttrNonResident
将实际实现此函数。感谢C++引入的多态性,通过此函数和下面的ReadData()
函数,驻留和非驻留属性可以使用相同的接口访问其数据,尽管它们差异很大。
返回值:成功时为TRUE
,否则为FALSE
。
将属性数据读取到缓冲区中。
__inline const ATTR_HEADER_COMMON* GetAttrHeader() const
__inline DWORD GetAttrType() const
__inline DWORD GetAttrTotalSize() const
__inline BOOL IsNonResident() const
__inline WORD GetAttrFlags() const
int GetAttrName(char *buf, DWORD bufLen) const
int GetAttrName(wchar_t *buf, DWORD bufLen) const
获取属性名称。返回值遵循与CFileRecord::GetFileName()
相同的规则。
__inline BOOL IsUnNamed() const
检查此属性是否未命名。
CAttrBase(const ATTR_HEADER_COMMON *ahc, const CFileRecord *fr)
ahc
:指向属性头缓冲区。fr
:拥有此属性的文件记录。
virtual __inline ULONGLONG GetDataSize(ULONGLONG *allocSize = NULL) const = 0
virtual BOOL ReadData(const ULONGLONG &offset, void *bufv, DWORD bufLen, DWORD *actural) const = 0
offset
:读取指针相对于开头的起始地址(字节)。bufv
:用户提供的用于接收数据的缓冲区。bufLen
:用户提供的缓冲区大小(字节)。actural
:实际读取的数据大小。抱歉拼写错误。现在微软Word告诉我了,但我太懒了,无法在我的源代码中查找和替换所有错误。我建议微软在Visual Studio中添加拼写检查,以帮助我们这些非英语母语的人,呵呵。
- 其他导出例程
2. CAttrResident
所有驻留属性类的基类。
实现驻留属性特有的虚拟函数GetDataSize()
和ReadData()
。
3. CAttrNonResident
所有非驻留属性类的基类。实现虚拟函数GetDataSize()
和ReadData()
,这些函数是为非驻留属性设计的。它比CAttrResident
的实现要复杂得多,因为它应该解析数据运行并构建一个列表来保存信息。我认为NTFS数据运行不是一个好的设计,因为节省的磁盘空间无法弥补浪费的解析时间。
4. CAttr_StdInfo
实现$STANDARD_INFORMATION
属性。派生自CAttrResident
。导出函数:
void GetFileTime(FILETIME *writeTm,
FILETIME *createTm = NULL, FILETIME *accessTm = NULL) const
__inline DWORD GetFilePermission() const
__inline BOOL IsReadOnly() const
__inline BOOL IsHidden() const
__inline BOOL IsSystem() const
__inline BOOL IsCompressed() const
__inline BOOL IsEncrypted() const
__inline BOOL IsSparse() const
5. CAttr_FileName
实现$FILE_NAME
属性。派生自CAttrResident
和CFileName
辅助类。
所有有用的函数都位于CFileName
基类中,该类将在后面介绍。位于$FILE_NAME
属性中的文件权限和时间仅在文件名更改时更新,因此从CFileName
派生的相关函数在CAttr_FileName
中被声明为“private
”,以防止用户获取错误的信息。$STANDARD_INFORMATION
和索引条目保存更新的文件权限和时间戳。
6. CAttr_VolInfo
实现$VOLUME_INFORMATION
属性。派生自CAttrResident
。导出函数:
__inline WORD GetVersion()
返回NTFS卷版本。高字节保存主版本,低字节保存次版本。在Windows XP和Windows7中,NTFS版本是3.1,Windows 2000是3.0,Windows NT是1.2。版本低于3.0的NTFS卷不受此库支持。
7. CAttr_VolName
实现$VOLUME_NAME
属性。派生自CAttrResident
。
导出函数:
__inline int GetName(wchar_t *buf, DWORD len) const
__inline int GetName(char *buf, DWORD len) const
获取Unicode或ANSI卷名。返回值遵循与CFileRecord::GetFileName()
相同的规则。
8. CAttr_Data
实现$DATA
属性。派生自模板类,该模板类是CAttrResident
或CAttrNonResident
。
GetDataSize()
和ReadData()
是从模板基类派生的。我们在处理$DATA
属性时只需要这两个函数。
9. CAttr_IndexRoot
实现$INDEX_ROOT
属性。派生自CAttrResident
和CIndexEntryList
辅助类。所有有用的函数都位于CIndexEntryList
持有的CIndexEntry
对象中,该对象将在后面介绍。
10. CAttr_IndexAlloc
实现$INDEX_ALLOCATION
属性。派生自CAttrNonResident
。
11. CAttr_Bitmap
实现$BITMAP
属性。派生自模板类,该模板类是CAttrResident
或CAttrNonResident
。
12. CAttr_AttrList
实现$ATTRIBUTE_LIST
属性。派生自模板类,该模板类是CAttrResident
或CAttrNonResident
。
这是最复杂的属性处理,因为它涉及文件记录和所有其他属性。但实现简洁,代码量少。
用户无需关心此属性;所有解析的子属性都将被插入到父文件记录的属性列表中,就好像它们直接包含在同一个文件记录中一样。
助手类
1. CFileName
此类帮助CAttr_FileName
和CIndexEntry
处理文件名相关信息。
导出函数:
int Compare(const wchar_t *fn) const
int Compare(const char *fn) const
将文件名与输入字符串进行比较。如果匹配则返回0,如果文件名小于输入字符串则返回负值,否则返回正值。此例程用于在索引根和索引分配构建的B+树中搜索特定文件。
__inline ULONGLONG GetFileSize() const
__inline DWORD GetFilePermission() const
__inline BOOL IsReadOnly() const
__inline BOOL IsHidden() const
__inline BOOL IsSystem() const
__inline BOOL IsDirectory() const
__inline BOOL IsCompressed() const
__inline BOOL IsEncrypted() const
__inline BOOL IsSparse() const
int GetFileName(char *buf, DWORD bufLen) const
int GetFileName(wchar_t *buf, DWORD bufLen) const
获取Unicode或ANSI文件名。返回值遵循与CFileRecord::GetFileName()
相同的规则。
__inline BOOL HasName() const
检查它是否包含文件名或未命名。
__inline BOOL IsWin32Name() const
无法放入DOS 8.3格式的文件名将具有DOS别名。例如,Win32名称“C:\Program files”将具有DOS兼容文件名“C:\Progra~1”。使用此函数检查它是否包含合法的Win32名称。
void GetFileTime(FILETIME *writeTm, FILETIME *createTm = NULL,
FILETIME *accessTm = NULL) const
2. CIndexEntry
此类封装了单个文件名索引条目。它派生自CFileName
,并且所有CFileName
导出函数都可以直接使用。
导出函数:
__inline ULONGLONG GetFileReference() const
获取此索引条目的文件引用。
__inline BOOL IsSubNodePtr() const
检查索引条目是否指向子节点。这些条目将不同的索引块链接成一个B+树。
__inline ULONGLONG GetSubNodeVCN() const
使用此函数定位子节点索引块。
3. CIndexBlock
此类有助于将单个索引块解析为CIndexEntry
列表。