Everything内部实现基本原理之MFT和USN Journal

   相信很多朋友都使用过Everything这款文件搜索神器,当我第一次使用时,我不禁感慨于它的速度之快。同时它不仅仅只是显示当前的搜索结果,当你在打开Everything搜索界面并同时修改文件时,它也可以一并监测出来并显示到界面上,这样高的效率不经让我发出疑问,到底是如何实现的。经过一段时间的学习之后,也算是掌握了一些皮毛,可以使用C++配合一些WinAPI来初步实现一些功能,下面来一起分享一下。

    首先,Everything是只支持NTFS文件系统,因为该软件的核心功能就是通过检索NTFS文件系统的MFT(Master File Table)主文件表和USN Journal监督机制来实现。

    我们可以将核心功能分为三块:

1.文件数据检索功能

2.文件快速查找功能

3.文件变化实时监测功能

在介绍完NTFS文件系统的一些相关知识后,我们会逐一讲解这三个功能

一.NTFS文件系统的基本架构

  这是一块内存条,由引导扇区$Boot开始,NTFS文件系统一共由16个“元文件”构成,它们是在分区格式化时写入到硬盘的隐藏文件(以”$”开头),也是NTFS文件系统的系统信息。我们由于篇幅限制,只关心$MFT即可,因为MFT中存储了所有文件/目录的元数据(时间戳,大小,属性等),如果有了解PE文件结构的朋友们也可以理解为MFT就类似于PE文件中的映射表,每40个字节会用于描述后面节的信息,还有节的偏移。MFT中存储的都是元数据,真正的数据都存储在后面的用户数据中。了解到MFT的基本结构后,我们不难发现,只要我们通过枚举获取MFT的信息之后,再进行索引编号,就可以构建起对应的数据库来存储整个磁盘中的文件信息(因为无需关心文件的实际位置)。

二.USN Journal的结构

  在NTFS磁盘文件系统中,当发生文件/目录的变更的时候,就会向一个系统维护的日志中追加纪录,包括文件名,变化的时间,类型(文件夹的创建,删除等)等元数据,但是实际变化的数据却并不会记录。该日志文件会不断记录改变信息,每一条日志都有一个64bit的标识,即USN(Update Sequence Number ),需要注意的是,这个USN是自增的(不一定是按照1,2,3,4的连续顺序来的,但肯定是按照大小来的),号码越小,时间发生的越早。所以在Windows操作系统中,USN是LONG LONG类型,就是代表编号,而USN Journal就是记录变化信息的一个结构。

三.Everything的基本实现原理

 1.数据索引库的建立:遍历所有的MFT内容,实现在不遍历文件系统就能获取当前磁盘中的所有文件的名称和路径。将所有的数据文件信息存储到数据库中,通过哈希表建立高效索引

 2.搜索功能的实现:通过字符串匹配算法,正则表达式,模糊搜索来进行数据的检索,和位置的快速跳转

 3.监控日志的变化:监控jJournal日志文件实现对文件修改的监控,如果发生变化,只需要进行部分的更正

  接下来:我将使用Windows编程,来初步实现一下1和3功能(2涉及到的算法问题我还没有研究明白,就先不乱说了)。先叠个甲:本人没有接触过专业的文件系统操作和专业的相关代码训练编写,完全是出自于兴趣,自己研究了一番,不保证代码的完美和简洁,当然,也不保证代码一定能在你的电脑上跑起来,有问题我们评论区里讨论。

  注意事项:

①本程序都是在Viusal Studio2019中编写,运行之前请务必打开UAC权限,有些函数不开启此权限会编译失败(编译器->清单工具->UAC)

②代码中会牵扯到一些内核的知识,限于篇幅不会一一解释,如果有问题我们可以评论区讨论

四.使用WinAPI实现基本的功能

  Ⅰ.建立基本的文件数据库(使用单字,多字符集)

1.获得驱动盘的句柄

DiskVolumeHandle = CreateFileA(
        DiskVolumeName,
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY,  //设置为只读模式
        NULL);
    if (DiskVolumeHandle == INVALID_HANDLE_VALUE)
    {
        goto Exit;
    }

2.通过句柄,控制码与内核交互获得数据

USNJournalData = VirtualAlloc(NULL, sizeof(USN_JOURNAL_DATA), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    ZeroMemory(USNJournalData, sizeof(USN_JOURNAL_DATA));
    //Ring3层向Ring0层发起控制请求
    IsOk = DeviceIoControl(DiskVolumeHandle,
        FSCTL_QUERY_USN_JOURNAL,      //使用的是查询
        NULL, 0, USNJournalData,
        sizeof(USN_JOURNAL_DATA),
        &ReturnLength,
        NULL);            //不使用异步模型,请求不完成直接卡死
    if (IsOk == FALSE)
    {
        goto Exit;
    }

 MftEnumData.StartFileReferenceNumber = 0;       //枚举起始的文件引用号(MFT 条目号),一般从0开始
    MftEnumData.LowUsn = 0;                         //最小 USN 编号,表示只返回大于等于该 USN 的记录。
    MftEnumData.HighUsn = ((PUSN_JOURNAL_DATA)USNJournalData)->NextUsn;  //最大 USN 编号,表示只返回小于等于该 USN 的记录,此处是表示下一个要分配的USN编号

3.解析数据,将数据存储入结构体(这里使用微软官方的Map进行存储)

 while (DeviceIoControl(DiskVolumeHandle, FSCTL_ENUM_USN_DATA, &MftEnumData,
        sizeof(MFT_ENUM_DATA_V0), USNDataBuffer, 0x8000, &ReturnLength, NULL) != FALSE)
    {
        DWORD USNRecordLength = ReturnLength - sizeof(USN);
        //去除掉数据的USN编号,直接开始分析数据
        USNRecordData = (PUSN_RECORD)(((PCHAR)USNDataBuffer) + sizeof(USN));
        //第五步:将所有的数据都存储进树中,使用键值对进行索引搜索
        while (USNRecordLength > 0)
        {
            const int FileNameLength = USNRecordData->FileNameLength;
            char FileNameData[MAX_PATH] = { 0 };
            WideCharToMultiByte(CP_OEMCP, NULL, USNRecordData->FileName, FileNameLength / 2, FileNameData, FileNameLength, NULL, FALSE);
            //构造MAP节点,放入MAP
            USNNode.ParentReferenceNumber = USNRecordData->ParentFileReferenceNumber;
            USNNode.Usn = USNRecordData->Usn;
            memcpy(USNNode.FileNameData, FileNameData, MAX_PATH);
            memcpy(&USNNode.FileAttributes, &USNRecordData->FileAttributes, MAX_PATH);
            //注意:索引是文件的引用号,也就是说不是从0开始写
            __USNFileMap[USNRecordData->FileReferenceNumber] = USNNode;
            // 获取下一个记录,通过偏移
            DWORD RecordLength = USNRecordData->RecordLength;
            USNRecordLength -= RecordLength;
            USNRecordData = (PUSN_RECORD)(((PCHAR)USNRecordData) + RecordLength);
        }
        MftEnumData.StartFileReferenceNumber = *(USN*)&USNDataBuffer;
    }

注意点:代码中直接使用官方的数据结构Map进行存储数据,而且没有使用数据库存储数据,直接将数据存储在堆中,使用全局变量进行文件信息的存储

Ⅱ.实现USN Journal的监控

1.根据文件修改的标志来创建修改函数

void ChangeFileMap(DWORD FileAttribute, ULONG64 FileReferenceNumber, DWORD Reason, ULONG64 ParentReferenceNumber, PWCHAR FileNameData, DWORD FileNameLength)
{
    CHAR v1[MAX_PATH] = { 0 };
    //将FileNameData转化为单字符集
    WideCharToMultiByte(CP_ACP, 0, FileNameData, FileNameLength, v1, MAX_PATH, 0, 0);
    memcpy(v1, FileNameData, MAX_PATH);
    USN_NODE USNNode = { 0 };
    USNNode.ParentReferenceNumber = ParentReferenceNumber;
    memcpy(USNNode.FileNameData, v1, MAX_PATH);
    //FileAttribute是文件属性的常量(类似于OA)
    //FILE_ATTRIBUTE_DIRECTORY表示这是一个目录
    BOOL IsDirctory = FileAttribute & FILE_ATTRIBUTE_DIRECTORY;
    //创建文件,直接插入即可
    //用于检查有没有对应的属性
    if ((USN_REASON_FILE_CREATE & Reason) && (USN_REASON_CLOSE & Reason))
    {
        // 文件创建,插入Map
        __USNFileMap[FileReferenceNumber] = USNNode;
    }
    //重命名文件或目录
    else if ((USN_REASON_RENAME_NEW_NAME & Reason) && (Reason & USN_REASON_CLOSE))
    {
        //根据索引改名字
        __USNFileMap[FileReferenceNumber] = USNNode;
    }
    //文件或目录被重命名
    else if (USN_REASON_RENAME_OLD_NAME & Reason)
    {
        if (USN_REASON_FILE_CREATE & Reason)
        {
            // 过滤重复情况
        }
        //更改原先文件的名字
        else
        {
            if (IsDirctory)
                __USNFileMap[FileReferenceNumber] = USNNode;
            else
                __USNFileMap.erase(FileReferenceNumber);
        }
    }
    //文件或目录被删除。
    else if ((Reason & USN_REASON_FILE_DELETE) && (USN_REASON_CLOSE & Reason))
    {
        if (USN_REASON_FILE_CREATE & Reason)
        {
            // 过滤重复情况
        }
        else
        {
            //重命名目录 仅根目录变化
            __USNFileMap.erase(FileReferenceNumber);
        }
    }
}

2.根据USN的变化来创建监控线程

DWORD MonitorProcedure(char VolumeValue)
{
    const DWORD MonitorUSNReason = USN_REASON_FILE_CREATE | USN_REASON_FILE_DELETE | USN_REASON_RENAME_OLD_NAME | USN_REASON_RENAME_NEW_NAME;
    BOOL IsOk = FALSE;
    DWORD ReturnLength = 0;
    // 打开磁盘
    CHAR   DiskVolumeName[] = "\\\\.\\A:";
    DiskVolumeName[4] = VolumeValue;

    HANDLE DiskVolumeHandle = CreateFileA(DiskVolumeName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL);
    if (DiskVolumeHandle == INVALID_HANDLE_VALUE)
    {
        return -1;
    }
    // 得到USN_DATA得到上一次遍历的最后结果
    USN_JOURNAL_DATA_V0  USNJournalData = { 0 };
    IsOk = DeviceIoControl(DiskVolumeHandle,
        FSCTL_QUERY_USN_JOURNAL, nullptr, 0, &USNJournalData, sizeof(USN_JOURNAL_DATA_V0), &ReturnLength,
        NULL);
    if (!IsOk)
    {
        return -1;
    }
    // 构造监视结构体 - 关键是监视类型 
    READ_USN_JOURNAL_DATA_V0 ReadUSNJournalData = { 0 };
    ReadUSNJournalData.BytesToWaitFor = 0;
    ReadUSNJournalData.ReasonMask = MonitorUSNReason;
    ReadUSNJournalData.ReturnOnlyOnClose = 0;
    ReadUSNJournalData.StartUsn = USNJournalData.NextUsn;
    ReadUSNJournalData.Timeout = 0;
    ReadUSNJournalData.UsnJournalID = USNJournalData.UsnJournalID;

    BYTE BufferData[USN_PAGE_SIZE] = { 0 };
    PUSN_RECORD_V2 USNRecordData = NULL;
    DWORD USNRecordLength = 0;
    while (true)
    {
        IsOk = DeviceIoControl(DiskVolumeHandle, FSCTL_READ_USN_JOURNAL, &ReadUSNJournalData, sizeof(READ_USN_JOURNAL_DATA_V0),
            BufferData, USN_PAGE_SIZE, &ReturnLength, NULL);
        if (!IsOk)
        {
            return -1;
        }
        // 返回数据结构: USN + n个USN_RECORD
        if (ReturnLength < sizeof(USN))
        {
            continue;
        }
        USNRecordLength = ReturnLength - sizeof(USN);
        USNRecordData = (PUSN_RECORD)(BufferData + sizeof(USN));
        while (USNRecordLength > 0)
        {
            //修改数据结构
            ChangeFileMap(USNRecordData->FileAttributes, USNRecordData->FileReferenceNumber,
                USNRecordData->Reason, USNRecordData->ParentFileReferenceNumber,
                PWCHAR(USNRecordData->FileName), USNRecordData->FileNameLength);
            USNRecordLength -= USNRecordData->RecordLength;
            USNRecordData = (PUSN_RECORD)(((PBYTE)USNRecordData) + USNRecordData->RecordLength);
        }
        ReadUSNJournalData.StartUsn = *(USN*)BufferData;
    }
    return 0;
}

五.总结

  通过上述代码可以基本实现磁盘文件信息的读取,但是距离实现Everything还差了十万八千里,文中也有很多细节问题没有谈到,比如使用文件引用号代替Map的键问题,WinAPI中的结构体USN_JOURNAL_DATA_V0,MFT_ENUM_DATA_V0等,部分与内核交互的函数DeviceIoControl等都没有细说,如果各位有兴趣也可以自行查阅资料或者在评论区交流看法都可。

  本文也就是大条粗俗介绍了一下通过MFT和USN Journal来实现磁盘文件信息的查询和监控功能,使用的方法肯定不是最好的,但是具体的功能还是实现到了。有问题的话咱们可以在评论区进行交流学习🤭

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值