From bbe6c3839e028e213adab6777efda28a5ca079b6 Mon Sep 17 00:00:00 2001 From: Orange20000922 <2140523341@qq.com> Date: Thu, 19 Feb 2026 20:05:51 +0800 Subject: [PATCH 1/7] =?UTF-8?q?1=E3=80=81=E6=9B=B4=E6=96=B0USN=E8=A7=A6?= =?UTF-8?q?=E5=8F=91MFT=E5=BF=AB=E7=85=A7=E5=92=8C=E5=86=85=E6=A0=B8IRP?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=E5=BF=AB=E7=85=A7=EF=BC=88=E5=AE=9E=E9=AA=8C?= =?UTF-8?q?=E6=80=A7=EF=BC=89=E5=8A=9F=E8=83=BD=202=E3=80=81=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=B8=80=E4=BA=9B=E5=B7=B2=E7=9F=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 10 +- Filerestore_CLI/Filerestore_CLI.vcxproj | 3 - .../Filerestore_CLI.vcxproj.filters | 5 - Filerestore_CLI/src/commands/cmd.cpp | 13 - .../src/fileRestore/KernelBridgeClient.cpp | 234 +++++ .../src/fileRestore/KernelBridgeClient.h | 93 ++ .../src/fileRestore/MFTSnapshotStore.cpp | 280 ++++++ .../src/fileRestore/MFTSnapshotStore.h | 113 +++ .../src/fileRestore/MonitorDaemon.cpp | 392 ++++++++ .../src/fileRestore/MonitorDaemon.h | 80 ++ .../src/fileRestore/UsnDeleteMonitor.cpp | 262 ++++++ .../src/fileRestore/UsnDeleteMonitor.h | 103 ++ Filerestore_sys/Filerestore_sys.sln | 35 + .../Filerestore_sys/Filerestore_sys.inf | 84 ++ .../Filerestore_sys/Filerestore_sys.vcxproj | 168 ++++ .../Filerestore_sys.vcxproj.filters | 48 + Filerestore_sys/Filerestore_sys/common.h | 86 ++ .../Filerestore_sys/communication.c | 271 ++++++ Filerestore_sys/Filerestore_sys/driver.c | 118 +++ Filerestore_sys/Filerestore_sys/driver.h | 59 ++ Filerestore_sys/Filerestore_sys/filter.c | 275 ++++++ .../Filerestore_sys/packages.config | 6 + dev_notes/kernel_solution.docx | Bin 0 -> 41390 bytes dev_notes/kernel_solution.md | 884 ++++++++++++++++++ dev_notes/monitor_daemon_implementation.md | 165 ++++ 25 files changed, 3761 insertions(+), 26 deletions(-) delete mode 100644 Filerestore_CLI/src/commands/cmd.cpp create mode 100644 Filerestore_CLI/src/fileRestore/KernelBridgeClient.cpp create mode 100644 Filerestore_CLI/src/fileRestore/KernelBridgeClient.h create mode 100644 Filerestore_CLI/src/fileRestore/MFTSnapshotStore.cpp create mode 100644 Filerestore_CLI/src/fileRestore/MFTSnapshotStore.h create mode 100644 Filerestore_CLI/src/fileRestore/MonitorDaemon.cpp create mode 100644 Filerestore_CLI/src/fileRestore/MonitorDaemon.h create mode 100644 Filerestore_CLI/src/fileRestore/UsnDeleteMonitor.cpp create mode 100644 Filerestore_CLI/src/fileRestore/UsnDeleteMonitor.h create mode 100644 Filerestore_sys/Filerestore_sys.sln create mode 100644 Filerestore_sys/Filerestore_sys/Filerestore_sys.inf create mode 100644 Filerestore_sys/Filerestore_sys/Filerestore_sys.vcxproj create mode 100644 Filerestore_sys/Filerestore_sys/Filerestore_sys.vcxproj.filters create mode 100644 Filerestore_sys/Filerestore_sys/common.h create mode 100644 Filerestore_sys/Filerestore_sys/communication.c create mode 100644 Filerestore_sys/Filerestore_sys/driver.c create mode 100644 Filerestore_sys/Filerestore_sys/driver.h create mode 100644 Filerestore_sys/Filerestore_sys/filter.c create mode 100644 Filerestore_sys/Filerestore_sys/packages.config create mode 100644 dev_notes/kernel_solution.docx create mode 100644 dev_notes/kernel_solution.md create mode 100644 dev_notes/monitor_daemon_implementation.md diff --git a/.gitignore b/.gitignore index d09c765..709f6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -33,10 +33,6 @@ x86/ *.VC.db-wal *.VC.opendb -# Build directories -# (已统一输出到解决方案根目录的 x64/) -x64/ - # NuGet packages (can be restored via nuget restore) packages/ *.nupkg @@ -109,4 +105,8 @@ release_packages/ Filerestore_CLI/deps/ftxui/.git/ Filerestore_CLI/deps/ftxui/build/ Filerestore_CLI/deps/ftxui/.cache/ -Filerestore_CLI/deps/ftxui/cmake-build-*/ \ No newline at end of file +Filerestore_CLI/deps/ftxui/cmake-build-*/ + +# Kernel driver build outputs +*.sys +*.cer \ No newline at end of file diff --git a/Filerestore_CLI/Filerestore_CLI.vcxproj b/Filerestore_CLI/Filerestore_CLI.vcxproj index 94def0a..1fddce0 100644 --- a/Filerestore_CLI/Filerestore_CLI.vcxproj +++ b/Filerestore_CLI/Filerestore_CLI.vcxproj @@ -169,7 +169,6 @@ - @@ -207,7 +206,6 @@ - @@ -259,7 +257,6 @@ - diff --git a/Filerestore_CLI/Filerestore_CLI.vcxproj.filters b/Filerestore_CLI/Filerestore_CLI.vcxproj.filters index f1999a2..776dbe2 100644 --- a/Filerestore_CLI/Filerestore_CLI.vcxproj.filters +++ b/Filerestore_CLI/Filerestore_CLI.vcxproj.filters @@ -101,9 +101,6 @@ 源文件\Core - - 源文件\Commands - 源文件\Commands @@ -192,7 +189,6 @@ 源文件\FileRestore\ML - 源文件\FileRestore @@ -357,7 +353,6 @@ 头文件\FileRestore\ML - 头文件\FileRestore diff --git a/Filerestore_CLI/src/commands/cmd.cpp b/Filerestore_CLI/src/commands/cmd.cpp deleted file mode 100644 index 29619f4..0000000 --- a/Filerestore_CLI/src/commands/cmd.cpp +++ /dev/null @@ -1,13 +0,0 @@ -// cmd.cpp - Command framework -// All command implementations have been moved to separate files: -// - SystemCommands.cpp -// - RestoreCommands.cpp -// - SearchCommands.cpp -// - DiagnosticCommands.cpp -// - CarveCommands.cpp -// - PEAnalysisCommands.cpp - -#include "cmd.h" - -// This file now only serves as a compilation unit for the command framework. -// Individual command implementations are in their respective category files. diff --git a/Filerestore_CLI/src/fileRestore/KernelBridgeClient.cpp b/Filerestore_CLI/src/fileRestore/KernelBridgeClient.cpp new file mode 100644 index 0000000..ecc9533 --- /dev/null +++ b/Filerestore_CLI/src/fileRestore/KernelBridgeClient.cpp @@ -0,0 +1,234 @@ +// ============================================================================ +// 内核桥接客户端 - 通过 minifilter 通信端口连接 FileRestoreMon 驱动 +// ============================================================================ + +#ifdef ENABLE_KERNEL_BRIDGE + +#include "KernelBridgeClient.h" +#include "MFTSnapshotStore.h" +#include "Logger.h" + +#pragma comment(lib, "fltlib.lib") + +using namespace std; + +KernelBridgeClient::KernelBridgeClient() + : hPort(INVALID_HANDLE_VALUE) {} + +KernelBridgeClient::~KernelBridgeClient() { + Disconnect(); +} + +// ========== 连接管理 ========== + +bool KernelBridgeClient::Connect() { + if (IsConnected()) return true; + + HRESULT hr = FilterConnectCommunicationPort( + FILERESTORE_PORT_NAME, + 0, // options + nullptr, // context + 0, // context size + nullptr, // security attributes + &hPort); + + if (FAILED(hr)) { + hPort = INVALID_HANDLE_VALUE; + if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) { + lastError = "驱动未加载: FileRestoreMon 通信端口不存在"; + } else if (hr == HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED)) { + lastError = "权限不足: 需要管理员权限连接驱动"; + } else { + lastError = "连接驱动失败, HRESULT: 0x" + + to_string(static_cast(hr)); + } + LOG_DEBUG_FMT("KernelBridgeClient: %s", lastError.c_str()); + return false; + } + + LOG_INFO("KernelBridgeClient: 已连接到 minifilter 驱动"); + return true; +} + +void KernelBridgeClient::Disconnect() { + if (hPort != INVALID_HANDLE_VALUE) { + CloseHandle(hPort); + hPort = INVALID_HANDLE_VALUE; + } +} + +// ========== 内部通信 ========== + +bool KernelBridgeClient::SendCommand(ULONG command, void* outBuffer, ULONG outSize, ULONG* bytesReturned) { + if (!IsConnected()) { + lastError = "未连接到驱动"; + return false; + } + + COMMAND_MESSAGE msg; + msg.Command = command; + + DWORD returned = 0; + HRESULT hr = FilterSendMessage( + hPort, + &msg, + sizeof(msg), + outBuffer, + outSize, + &returned); + + if (FAILED(hr)) { + lastError = "FilterSendMessage 失败, HRESULT: 0x" + + to_string(static_cast(hr)); + return false; + } + + if (bytesReturned) { + *bytesReturned = returned; + } + return true; +} + +// ========== 监控控制 ========== + +bool KernelBridgeClient::StartMonitor() { + return SendCommand(CMD_START_MONITOR, nullptr, 0, nullptr); +} + +bool KernelBridgeClient::StopMonitor() { + return SendCommand(CMD_STOP_MONITOR, nullptr, 0, nullptr); +} + +// ========== 通知接收 ========== + +bool KernelBridgeClient::PollNotifications(vector& notifications) { + notifications.clear(); + + // 分配接收缓冲区:能容纳 EVENTS_REPLY 头 + 若干 DELETE_NOTIFICATION + const ULONG BUFFER_SIZE = FIELD_OFFSET(EVENTS_REPLY, Events) + + 64 * sizeof(DELETE_NOTIFICATION); + vector buffer(BUFFER_SIZE, 0); + + ULONG bytesReturned = 0; + if (!SendCommand(CMD_GET_EVENTS, buffer.data(), BUFFER_SIZE, &bytesReturned)) { + return false; + } + + if (bytesReturned < FIELD_OFFSET(EVENTS_REPLY, Events)) { + return true; // 无数据 + } + + auto* reply = reinterpret_cast(buffer.data()); + if (reply->EventCount == 0) { + return true; + } + + // 校验返回数据大小 + ULONG expectedSize = FIELD_OFFSET(EVENTS_REPLY, Events) + + reply->EventCount * sizeof(DELETE_NOTIFICATION); + if (bytesReturned < expectedSize) { + lastError = "回复数据不完整"; + return false; + } + + notifications.reserve(reply->EventCount); + for (ULONG i = 0; i < reply->EventCount; i++) { + const DELETE_NOTIFICATION& src = reply->Events[i]; + DeleteNotification notif; + + notif.fileName = wstring(src.FileName, src.FileNameLength / sizeof(WCHAR)); + notif.fileSize = src.FileSize.QuadPart; + notif.allocationSize = src.AllocationSize.QuadPart; + notif.processId = src.ProcessId; + notif.deleteType = src.DeleteType; + + // LARGE_INTEGER → FILETIME(二进制布局相同) + memcpy(¬if.creationTime, &src.CreationTime, sizeof(FILETIME)); + memcpy(¬if.lastWriteTime, &src.LastWriteTime, sizeof(FILETIME)); + memcpy(¬if.deleteTime, &src.Timestamp, sizeof(FILETIME)); + + // LCN 映射 + notif.lcnMappings.reserve(src.LCNCount); + for (ULONG j = 0; j < src.LCNCount; j++) { + notif.lcnMappings.emplace_back( + src.LCNEntries[j].StartLCN, + src.LCNEntries[j].ClusterCount); + } + + notifications.push_back(move(notif)); + } + + return true; +} + +size_t KernelBridgeClient::FlushToSnapshotStore(MFTSnapshotStore& store) { + vector notifications; + if (!PollNotifications(notifications)) { + return 0; + } + + size_t count = 0; + for (const auto& notif : notifications) { + MFTSnapshot snapshot; + snapshot.recordNumber = 0; // 驱动未提供 MFT 记录号 + snapshot.sequenceNumber = 0; + snapshot.fileName = notif.fileName; + snapshot.fileSize = notif.fileSize; + snapshot.isResident = notif.lcnMappings.empty() && notif.fileSize > 0; + + // LCN 映射转换: (ULONGLONG, ULONG) → (ULONGLONG, ULONGLONG) + snapshot.dataRuns.reserve(notif.lcnMappings.size()); + for (const auto& [lcn, clusters] : notif.lcnMappings) { + snapshot.dataRuns.emplace_back(lcn, static_cast(clusters)); + } + + snapshot.deleteTime = notif.deleteTime; + GetSystemTimeAsFileTime(&snapshot.captureTime); + + store.AddSnapshot(snapshot); + count++; + } + + return count; +} + +// ========== 驱动信息 ========== + +string KernelBridgeClient::GetDriverVersion() { + if (!IsConnected()) return "未连接"; + + VERSION_REPLY reply = {}; + ULONG bytesReturned = 0; + + if (!SendCommand(CMD_GET_VERSION, &reply, sizeof(reply), &bytesReturned)) { + return "未知版本"; + } + + if (bytesReturned == 0) { + return "未知版本"; + } + + return string(reply.Version); +} + +bool KernelBridgeClient::GetStats(DriverStats& stats) { + STATS_REPLY reply = {}; + ULONG bytesReturned = 0; + + if (!SendCommand(CMD_GET_STATS, &reply, sizeof(reply), &bytesReturned)) { + return false; + } + + if (bytesReturned < sizeof(STATS_REPLY)) { + lastError = "统计数据不完整"; + return false; + } + + stats.totalEvents = reply.TotalEvents; + stats.pendingEvents = reply.PendingEvents; + stats.droppedEvents = reply.DroppedEvents; + stats.monitorActive = (reply.MonitorActive != 0); + return true; +} + +#endif // ENABLE_KERNEL_BRIDGE diff --git a/Filerestore_CLI/src/fileRestore/KernelBridgeClient.h b/Filerestore_CLI/src/fileRestore/KernelBridgeClient.h new file mode 100644 index 0000000..8acecc9 --- /dev/null +++ b/Filerestore_CLI/src/fileRestore/KernelBridgeClient.h @@ -0,0 +1,93 @@ +#pragma once + +// ============================================================================ +// 内核桥接客户端 - 通过 minifilter 通信端口连接 FileRestoreMon 驱动 +// +// 默认不启用。要启用,在项目属性中定义 ENABLE_KERNEL_BRIDGE 预处理器宏。 +// ============================================================================ + +#ifdef ENABLE_KERNEL_BRIDGE + +#include +#include +#include +#include + +// 共享定义(与内核驱动 common.h 一致) +#include "../../../../Filerestore_sys/Filerestore_sys/common.h" + +class MFTSnapshotStore; + +class KernelBridgeClient { +public: + KernelBridgeClient(); + ~KernelBridgeClient(); + + // ========== 连接管理 ========== + + // 通过 FilterConnectCommunicationPort 连接到 minifilter 驱动 + bool Connect(); + + // 断开连接 + void Disconnect(); + + // 是否已连接 + bool IsConnected() const { return hPort != INVALID_HANDLE_VALUE; } + + // ========== 监控控制 ========== + + // 启动文件删除监控 + bool StartMonitor(); + + // 停止文件删除监控 + bool StopMonitor(); + + // ========== 通知接收 ========== + + // 删除通知结构 + struct DeleteNotification { + std::wstring fileName; + ULONGLONG fileSize; + ULONGLONG allocationSize; + FILETIME creationTime; + FILETIME lastWriteTime; + FILETIME deleteTime; + DWORD processId; + DWORD deleteType; + std::vector> lcnMappings; // (startLCN, clusterCount) + }; + + // 从驱动获取待处理的删除通知 + bool PollNotifications(std::vector& notifications); + + // 将通知转换为快照并存入 MFTSnapshotStore + size_t FlushToSnapshotStore(MFTSnapshotStore& store); + + // ========== 驱动信息 ========== + + // 获取驱动版本 + std::string GetDriverVersion(); + + // 驱动统计信息 + struct DriverStats { + ULONG totalEvents; + ULONG pendingEvents; + ULONG droppedEvents; + bool monitorActive; + }; + + // 获取驱动统计 + bool GetStats(DriverStats& stats); + + // 获取最后的错误信息 + std::string GetLastError() const { return lastError; } + +private: + HANDLE hPort; // minifilter 通信端口句柄 + std::string lastError; + + // 向驱动发送命令并接收回复 + bool SendCommand(ULONG command, void* outBuffer, ULONG outSize, ULONG* bytesReturned); +}; + +#endif // ENABLE_KERNEL_BRIDGE diff --git a/Filerestore_CLI/src/fileRestore/MFTSnapshotStore.cpp b/Filerestore_CLI/src/fileRestore/MFTSnapshotStore.cpp new file mode 100644 index 0000000..0b4b3a8 --- /dev/null +++ b/Filerestore_CLI/src/fileRestore/MFTSnapshotStore.cpp @@ -0,0 +1,280 @@ +#include "MFTSnapshotStore.h" +#include "Logger.h" +#include + +using namespace std; + +// ============================================================================ +// 构造和析构 +// ============================================================================ +MFTSnapshotStore::MFTSnapshotStore() : driveLetter(0) {} +MFTSnapshotStore::~MFTSnapshotStore() {} + +string MFTSnapshotStore::GenerateStorePath(char drive) { + char tempPath[MAX_PATH]; + GetTempPathA(MAX_PATH, tempPath); + return string(tempPath) + "mft_snapshot_" + drive + ".dat"; +} + +// ============================================================================ +// 快照管理 +// ============================================================================ +void MFTSnapshotStore::AddSnapshot(const MFTSnapshot& snapshot) { + lock_guard lock(mtx); + snapshots[snapshot.recordNumber].push_back(snapshot); +} + +const MFTSnapshot* MFTSnapshotStore::FindByRecord(ULONGLONG recordNum, WORD seqNum) const { + lock_guard lock(mtx); + + auto it = snapshots.find(recordNum); + if (it == snapshots.end()) return nullptr; + + for (const auto& snap : it->second) { + if (snap.sequenceNumber == seqNum) { + return &snap; + } + } + return nullptr; +} + +const MFTSnapshot* MFTSnapshotStore::FindLatestByRecord(ULONGLONG recordNum) const { + lock_guard lock(mtx); + + auto it = snapshots.find(recordNum); + if (it == snapshots.end() || it->second.empty()) return nullptr; + + // 返回最后一个(最新添加的) + return &it->second.back(); +} + +vector MFTSnapshotStore::SearchByName(const wstring& pattern) const { + lock_guard lock(mtx); + vector results; + + wstring lowerPattern = pattern; + transform(lowerPattern.begin(), lowerPattern.end(), lowerPattern.begin(), ::towlower); + + for (const auto& [recordNum, snapList] : snapshots) { + for (const auto& snap : snapList) { + wstring lowerName = snap.fileName; + transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::towlower); + if (lowerName.find(lowerPattern) != wstring::npos) { + results.push_back(&snap); + } + } + } + return results; +} + +// ============================================================================ +// 持久化 +// ============================================================================ +void MFTSnapshotStore::SerializeSnapshot(ofstream& out, const MFTSnapshot& snap) { + out.write((char*)&snap.recordNumber, sizeof(ULONGLONG)); + out.write((char*)&snap.sequenceNumber, sizeof(WORD)); + out.write((char*)&snap.fileSize, sizeof(ULONGLONG)); + out.write((char*)&snap.captureTime, sizeof(FILETIME)); + out.write((char*)&snap.deleteTime, sizeof(FILETIME)); + out.write((char*)&snap.parentRecord, sizeof(ULONGLONG)); + + BYTE flags = snap.isResident ? 0x01 : 0x00; + out.write((char*)&flags, sizeof(BYTE)); + + // 文件名 + WORD nameLen = (WORD)snap.fileName.length(); + out.write((char*)&nameLen, sizeof(WORD)); + if (nameLen > 0) { + out.write((char*)snap.fileName.data(), nameLen * sizeof(WCHAR)); + } + + // Data Runs + DWORD runCount = (DWORD)snap.dataRuns.size(); + out.write((char*)&runCount, sizeof(DWORD)); + for (const auto& [lcn, count] : snap.dataRuns) { + out.write((char*)&lcn, sizeof(ULONGLONG)); + out.write((char*)&count, sizeof(ULONGLONG)); + } + + // 常驻数据 + DWORD resDataLen = (DWORD)snap.residentData.size(); + out.write((char*)&resDataLen, sizeof(DWORD)); + if (resDataLen > 0) { + out.write((char*)snap.residentData.data(), resDataLen); + } +} + +bool MFTSnapshotStore::DeserializeSnapshot(ifstream& in, MFTSnapshot& snap) { + in.read((char*)&snap.recordNumber, sizeof(ULONGLONG)); + in.read((char*)&snap.sequenceNumber, sizeof(WORD)); + in.read((char*)&snap.fileSize, sizeof(ULONGLONG)); + in.read((char*)&snap.captureTime, sizeof(FILETIME)); + in.read((char*)&snap.deleteTime, sizeof(FILETIME)); + in.read((char*)&snap.parentRecord, sizeof(ULONGLONG)); + + BYTE flags; + in.read((char*)&flags, sizeof(BYTE)); + snap.isResident = (flags & 0x01) != 0; + + // 文件名 + WORD nameLen; + in.read((char*)&nameLen, sizeof(WORD)); + if (nameLen > 0 && nameLen < 512) { + snap.fileName.resize(nameLen); + in.read((char*)snap.fileName.data(), nameLen * sizeof(WCHAR)); + } + + // Data Runs + DWORD runCount; + in.read((char*)&runCount, sizeof(DWORD)); + if (runCount > 0 && runCount < 10000) { + snap.dataRuns.resize(runCount); + for (DWORD j = 0; j < runCount; j++) { + in.read((char*)&snap.dataRuns[j].first, sizeof(ULONGLONG)); + in.read((char*)&snap.dataRuns[j].second, sizeof(ULONGLONG)); + } + } + + // 常驻数据 + DWORD resDataLen; + in.read((char*)&resDataLen, sizeof(DWORD)); + if (resDataLen > 0 && resDataLen < 4096) { // MFT 常驻数据最大 ~3.5KB + snap.residentData.resize(resDataLen); + in.read((char*)snap.residentData.data(), resDataLen); + } + + return in.good(); +} + +bool MFTSnapshotStore::SaveToFile(const string& path) { + lock_guard lock(mtx); + + ofstream out(path, ios::binary); + if (!out) { + LOG_ERROR("无法创建快照存储文件"); + return false; + } + + // 计算总快照数 + size_t totalCount = 0; + for (const auto& [recordNum, snapList] : snapshots) { + totalCount += snapList.size(); + } + + // 写入头 + MFTSnapshotHeader header; + header.magic = MFT_SNAPSHOT_MAGIC; + header.version = MFT_SNAPSHOT_VERSION; + header.snapshotCount = totalCount; + header.driveLetter = driveLetter; + memset(header.padding, 0, sizeof(header.padding)); + GetSystemTimeAsFileTime(&header.lastUpdateTime); + header.createTime = header.lastUpdateTime; + + out.write((char*)&header, sizeof(header)); + + // 写入快照 + for (const auto& [recordNum, snapList] : snapshots) { + for (const auto& snap : snapList) { + SerializeSnapshot(out, snap); + } + } + + out.close(); + LOG_INFO_FMT("快照存储已保存: %zu 个快照到 %s", totalCount, path.c_str()); + return true; +} + +bool MFTSnapshotStore::LoadFromFile(const string& path) { + lock_guard lock(mtx); + snapshots.clear(); + + ifstream in(path, ios::binary); + if (!in) { + return false; + } + + // 读取头 + MFTSnapshotHeader header; + in.read((char*)&header, sizeof(header)); + + if (header.magic != MFT_SNAPSHOT_MAGIC || header.version != MFT_SNAPSHOT_VERSION) { + LOG_ERROR("快照存储文件格式无效或版本不匹配"); + return false; + } + + driveLetter = header.driveLetter; + + // 读取快照 + for (ULONGLONG i = 0; i < header.snapshotCount; i++) { + MFTSnapshot snap; + if (!DeserializeSnapshot(in, snap)) { + LOG_ERROR("快照存储文件损坏"); + snapshots.clear(); + return false; + } + snapshots[snap.recordNumber].push_back(snap); + } + + in.close(); + LOG_INFO_FMT("快照存储已加载: %llu 个快照从 %s", header.snapshotCount, path.c_str()); + return true; +} + +// ============================================================================ +// 清理和统计 +// ============================================================================ +size_t MFTSnapshotStore::PurgeOlderThan(int days) { + lock_guard lock(mtx); + + FILETIME now; + GetSystemTimeAsFileTime(&now); + + ULARGE_INTEGER nowLI; + nowLI.LowPart = now.dwLowDateTime; + nowLI.HighPart = now.dwHighDateTime; + + // 100 纳秒 * 10^7 = 1 秒, * 86400 = 1 天 + ULONGLONG cutoff = nowLI.QuadPart - (ULONGLONG)days * 864000000000ULL; + + size_t purgedCount = 0; + + for (auto it = snapshots.begin(); it != snapshots.end(); ) { + auto& snapList = it->second; + + auto removeIt = remove_if(snapList.begin(), snapList.end(), + [cutoff, &purgedCount](const MFTSnapshot& snap) { + ULARGE_INTEGER snapTime; + snapTime.LowPart = snap.captureTime.dwLowDateTime; + snapTime.HighPart = snap.captureTime.dwHighDateTime; + if (snapTime.QuadPart < cutoff) { + purgedCount++; + return true; + } + return false; + }); + snapList.erase(removeIt, snapList.end()); + + if (snapList.empty()) { + it = snapshots.erase(it); + } else { + ++it; + } + } + + return purgedCount; +} + +size_t MFTSnapshotStore::GetCount() const { + lock_guard lock(mtx); + size_t count = 0; + for (const auto& [recordNum, snapList] : snapshots) { + count += snapList.size(); + } + return count; +} + +void MFTSnapshotStore::Clear() { + lock_guard lock(mtx); + snapshots.clear(); +} diff --git a/Filerestore_CLI/src/fileRestore/MFTSnapshotStore.h b/Filerestore_CLI/src/fileRestore/MFTSnapshotStore.h new file mode 100644 index 0000000..cc86d17 --- /dev/null +++ b/Filerestore_CLI/src/fileRestore/MFTSnapshotStore.h @@ -0,0 +1,113 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +// ============================================================================ +// MFT 快照魔术字节和版本 +// ============================================================================ +constexpr DWORD MFT_SNAPSHOT_MAGIC = 0x4D465453; // "MFTS" +constexpr DWORD MFT_SNAPSHOT_VERSION = 1; + +// ============================================================================ +// MFT 快照条目 - 记录文件删除时刻的完整 MFT 元数据 +// ============================================================================ +struct MFTSnapshot { + ULONGLONG recordNumber; // MFT 记录号 + WORD sequenceNumber; // 删除时的序列号 + wstring fileName; // 文件名 + ULONGLONG fileSize; // 文件大小 + vector> dataRuns; // 完整 LCN 映射 (LCN, clusterCount) + FILETIME captureTime; // 快照捕获时间 + FILETIME deleteTime; // 删除时间(来自 USN) + bool isResident; // 是否常驻数据 + vector residentData; // 常驻数据内容 + ULONGLONG parentRecord; // 父目录记录号 + + MFTSnapshot() : + recordNumber(0), sequenceNumber(0), fileSize(0), + isResident(false), parentRecord(0) { + ZeroMemory(&captureTime, sizeof(FILETIME)); + ZeroMemory(&deleteTime, sizeof(FILETIME)); + } +}; + +// ============================================================================ +// MFT 快照存储文件头 +// ============================================================================ +struct MFTSnapshotHeader { + DWORD magic; + DWORD version; + ULONGLONG snapshotCount; + char driveLetter; + char padding[7]; + FILETIME createTime; // 存储创建时间 + FILETIME lastUpdateTime; // 最后更新时间 +}; + +// ============================================================================ +// MFT 快照存储 - 线程安全的增量式快照管理 +// ============================================================================ +class MFTSnapshotStore { +public: + MFTSnapshotStore(); + ~MFTSnapshotStore(); + + // ========== 快照管理 ========== + + // 添加快照(线程安全) + void AddSnapshot(const MFTSnapshot& snapshot); + + // 按 MFT 记录号 + 序列号精确查询 + // 返回 nullptr 如果未找到 + const MFTSnapshot* FindByRecord(ULONGLONG recordNum, WORD seqNum) const; + + // 按 MFT 记录号查找最近的快照(不限序列号) + const MFTSnapshot* FindLatestByRecord(ULONGLONG recordNum) const; + + // 按文件名模糊查询 + vector SearchByName(const wstring& pattern) const; + + // ========== 持久化 ========== + + // 保存到文件 + bool SaveToFile(const string& path); + + // 从文件加载 + bool LoadFromFile(const string& path); + + // 生成默认存储路径 + static string GenerateStorePath(char drive); + + // ========== 清理和统计 ========== + + // 清理超过指定天数的快照,返回删除数量 + size_t PurgeOlderThan(int days); + + // 获取快照总数 + size_t GetCount() const; + + // 清空所有快照 + void Clear(); + + // ========== 驱动器信息 ========== + + void SetDriveLetter(char drive) { driveLetter = drive; } + char GetDriveLetter() const { return driveLetter; } + +private: + // 按记录号索引(key = recordNumber, value = 该记录的快照列表,按序列号排序) + unordered_map> snapshots; + mutable mutex mtx; + char driveLetter; + + // 序列化单条快照 + void SerializeSnapshot(ofstream& out, const MFTSnapshot& snap); + bool DeserializeSnapshot(ifstream& in, MFTSnapshot& snap); +}; diff --git a/Filerestore_CLI/src/fileRestore/MonitorDaemon.cpp b/Filerestore_CLI/src/fileRestore/MonitorDaemon.cpp new file mode 100644 index 0000000..341fa50 --- /dev/null +++ b/Filerestore_CLI/src/fileRestore/MonitorDaemon.cpp @@ -0,0 +1,392 @@ +#include "MonitorDaemon.h" +#include "UsnDeleteMonitor.h" +#include "MFTSnapshotStore.h" +#include "Logger.h" +#include + +using namespace std; + +// ============================================================================ +// 共享内存自旋锁(跨进程安全) +// ============================================================================ +static void SpinLockAcquire(volatile LONG* lock) { + while (InterlockedCompareExchange(lock, 1, 0) != 0) { + YieldProcessor(); + } +} + +static void SpinLockRelease(volatile LONG* lock) { + InterlockedExchange(lock, 0); +} + +// ============================================================================ +// 析构 +// ============================================================================ +MonitorDaemon::~MonitorDaemon() { + DetachSharedMemory(); +} + +// ============================================================================ +// 命名约定 +// ============================================================================ +wstring MonitorDaemon::GetMutexName(char d) { + return wstring(L"Global\\FileRestoreMonitor_") + (wchar_t)d; +} + +wstring MonitorDaemon::GetSharedMemName(char d) { + return wstring(L"Global\\FileRestoreMonitor_") + (wchar_t)d + L"_Mem"; +} + +wstring MonitorDaemon::GetStopEventName(char d) { + return wstring(L"Global\\FileRestoreMonitor_") + (wchar_t)d + L"_Stop"; +} + +// ============================================================================ +// IsDaemonRunning - 通过 Mutex 检测守护进程是否已在运行 +// ============================================================================ +bool MonitorDaemon::IsDaemonRunning(char drive) { + wstring mutexName = GetMutexName(drive); + HANDLE hMutex = OpenMutexW(SYNCHRONIZE, FALSE, mutexName.c_str()); + if (hMutex) { + CloseHandle(hMutex); + return true; + } + return false; +} + +// ============================================================================ +// GetDaemonPID - 从共享内存读取 PID +// ============================================================================ +DWORD MonitorDaemon::GetDaemonPID(char drive) { + if (AttachSharedMemory(drive)) { + MonitorSharedState state; + if (ReadState(state)) { + DetachSharedMemory(); + return state.pid; + } + DetachSharedMemory(); + } + return 0; +} + +// ============================================================================ +// StartDaemon - 启动独立守护进程 +// ============================================================================ +bool MonitorDaemon::StartDaemon(char drive) { + // 检查是否已运行 + if (IsDaemonRunning(drive)) { + return true; + } + + // 获取自身路径 + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(NULL, exePath, MAX_PATH); + + // 构造命令行: "" --monitor-daemon D + wstring cmdLine = wstring(L"\"") + exePath + L"\" --monitor-daemon " + (wchar_t)drive; + + STARTUPINFOW si = {}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi = {}; + + // CREATE_NO_WINDOW | DETACHED_PROCESS 确保无窗口后台运行 + BOOL ok = CreateProcessW( + NULL, + (LPWSTR)cmdLine.c_str(), + NULL, NULL, FALSE, + CREATE_NO_WINDOW | DETACHED_PROCESS, + NULL, NULL, + &si, &pi + ); + + if (!ok) { + LOG_ERROR_FMT("StartDaemon: CreateProcessW failed, error=%u", GetLastError()); + return false; + } + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + + // 轮询等待共享内存出现(最多 3 秒) + for (int i = 0; i < 30; i++) { + Sleep(100); + if (IsDaemonRunning(drive)) { + return true; + } + } + + LOG_ERROR("StartDaemon: Daemon did not start within timeout"); + return false; +} + +// ============================================================================ +// StopDaemon - 通知守护进程退出 +// ============================================================================ +bool MonitorDaemon::StopDaemon(char drive) { + if (!IsDaemonRunning(drive)) { + return true; // 已经停止 + } + + wstring eventName = GetStopEventName(drive); + HANDLE hEvent = OpenEventW(EVENT_MODIFY_STATE, FALSE, eventName.c_str()); + if (!hEvent) { + LOG_ERROR_FMT("StopDaemon: Cannot open stop event, error=%u", GetLastError()); + return false; + } + + SetEvent(hEvent); + CloseHandle(hEvent); + + // 轮询等待守护进程退出(最多 5 秒) + for (int i = 0; i < 50; i++) { + Sleep(100); + if (!IsDaemonRunning(drive)) { + return true; + } + } + + LOG_ERROR("StopDaemon: Daemon did not stop within timeout"); + return false; +} + +// ============================================================================ +// AttachSharedMemory / DetachSharedMemory / ReadState +// ============================================================================ +bool MonitorDaemon::AttachSharedMemory(char drive) { + if (sharedPtr_) return true; // 已附加 + + wstring memName = GetSharedMemName(drive); + hSharedMem_ = OpenFileMappingW(FILE_MAP_READ | FILE_MAP_WRITE, FALSE, memName.c_str()); + if (!hSharedMem_) { + return false; + } + + sharedPtr_ = (MonitorSharedState*)MapViewOfFile( + hSharedMem_, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, sizeof(MonitorSharedState)); + if (!sharedPtr_) { + CloseHandle(hSharedMem_); + hSharedMem_ = nullptr; + return false; + } + + return true; +} + +void MonitorDaemon::DetachSharedMemory() { + if (sharedPtr_) { + UnmapViewOfFile(sharedPtr_); + sharedPtr_ = nullptr; + } + if (hSharedMem_) { + CloseHandle(hSharedMem_); + hSharedMem_ = nullptr; + } +} + +bool MonitorDaemon::ReadState(MonitorSharedState& out) { + if (!sharedPtr_) return false; + if (sharedPtr_->magic != MONITOR_SHARED_MAGIC) return false; + if (sharedPtr_->version != MONITOR_SHARED_VERSION) return false; + + // 加锁拷贝,确保不会读到写了一半的事件 + SpinLockAcquire(&sharedPtr_->spinLock); + memcpy(&out, (const void*)sharedPtr_, sizeof(MonitorSharedState)); + SpinLockRelease(&sharedPtr_->spinLock); + + // 拷贝完成后释放锁,out 是本地副本,后续操作无需锁 + return true; +} + +// ============================================================================ +// 自启动注册表 +// ============================================================================ +static wstring GetAutoStartValueName(char drive) { + return wstring(L"FileRestoreMonitor_") + (wchar_t)drive; +} + +bool MonitorDaemon::InstallAutoStart(char drive) { + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(NULL, exePath, MAX_PATH); + wstring cmdLine = wstring(L"\"") + exePath + L"\" --monitor-daemon " + (wchar_t)drive; + wstring valueName = GetAutoStartValueName(drive); + + HKEY hKey; + LONG result = RegOpenKeyExW(HKEY_CURRENT_USER, + L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", + 0, KEY_SET_VALUE, &hKey); + if (result != ERROR_SUCCESS) { + LOG_ERROR_FMT("InstallAutoStart: RegOpenKeyExW failed, error=%ld", result); + return false; + } + + result = RegSetValueExW(hKey, valueName.c_str(), 0, REG_SZ, + (const BYTE*)cmdLine.c_str(), + (DWORD)((cmdLine.size() + 1) * sizeof(wchar_t))); + RegCloseKey(hKey); + + return result == ERROR_SUCCESS; +} + +bool MonitorDaemon::UninstallAutoStart(char drive) { + wstring valueName = GetAutoStartValueName(drive); + + HKEY hKey; + LONG result = RegOpenKeyExW(HKEY_CURRENT_USER, + L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", + 0, KEY_SET_VALUE, &hKey); + if (result != ERROR_SUCCESS) return false; + + result = RegDeleteValueW(hKey, valueName.c_str()); + RegCloseKey(hKey); + + return result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND; +} + +bool MonitorDaemon::IsAutoStartInstalled(char drive) { + wstring valueName = GetAutoStartValueName(drive); + + HKEY hKey; + LONG result = RegOpenKeyExW(HKEY_CURRENT_USER, + L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", + 0, KEY_QUERY_VALUE, &hKey); + if (result != ERROR_SUCCESS) return false; + + result = RegQueryValueExW(hKey, valueName.c_str(), NULL, NULL, NULL, NULL); + RegCloseKey(hKey); + + return result == ERROR_SUCCESS; +} + +// ============================================================================ +// RunDaemonMain - 守护进程主入口 +// ============================================================================ +int MonitorDaemon::RunDaemonMain(char drive) { + // 1. 创建单例 Mutex + wstring mutexName = GetMutexName(drive); + HANDLE hMutex = CreateMutexW(NULL, TRUE, mutexName.c_str()); + if (!hMutex || GetLastError() == ERROR_ALREADY_EXISTS) { + if (hMutex) CloseHandle(hMutex); + return 1; // 已有实例运行 + } + + // 2. 创建停止事件(手动复位) + wstring eventName = GetStopEventName(drive); + HANDLE hStopEvent = CreateEventW(NULL, TRUE, FALSE, eventName.c_str()); + if (!hStopEvent) { + ReleaseMutex(hMutex); + CloseHandle(hMutex); + return 1; + } + + // 3. 创建共享内存 + wstring memName = GetSharedMemName(drive); + HANDLE hMapping = CreateFileMappingW( + INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, + 0, sizeof(MonitorSharedState), memName.c_str()); + if (!hMapping) { + CloseHandle(hStopEvent); + ReleaseMutex(hMutex); + CloseHandle(hMutex); + return 1; + } + + MonitorSharedState* shared = (MonitorSharedState*)MapViewOfFile( + hMapping, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(MonitorSharedState)); + if (!shared) { + CloseHandle(hMapping); + CloseHandle(hStopEvent); + ReleaseMutex(hMutex); + CloseHandle(hMutex); + return 1; + } + + // 4. 初始化共享内存 + memset(shared, 0, sizeof(MonitorSharedState)); + shared->magic = MONITOR_SHARED_MAGIC; + shared->version = MONITOR_SHARED_VERSION; + shared->driveLetter = drive; + shared->pid = GetCurrentProcessId(); + GetSystemTimeAsFileTime(&shared->startTime); + shared->daemonRunning = 1; + shared->autoStartEnabled = IsAutoStartInstalled(drive) ? 1 : 0; + + // 5. 创建 UsnDeleteMonitor + UsnDeleteMonitor monitor(drive); + monitor.SetPollInterval(1000); + + shared->pollIntervalMs = monitor.GetPollInterval(); + + // 设置事件回调 - 写入环形缓冲区(加锁) + monitor.SetEventCallback([shared](ULONGLONG recordNum, ULONGLONG fileSize, + FILETIME deleteTime, const wstring& fileName, + bool captured) { + SpinLockAcquire(&shared->spinLock); + + LONG head = shared->recentEventHead; + LONG idx = head % MONITOR_RECENT_EVENT_MAX; + + MonitorRecentEvent& evt = shared->recentEvents[idx]; + evt.mftRecord = recordNum; + evt.fileSize = fileSize; + evt.deleteTime = deleteTime; + evt.captured = captured ? 1 : 0; + + // 安全拷贝文件名 + size_t copyLen = min(fileName.size(), (size_t)259); + memcpy(evt.fileName, fileName.c_str(), copyLen * sizeof(wchar_t)); + evt.fileName[copyLen] = L'\0'; + + shared->recentEventHead = head + 1; + LONG count = shared->recentEventCount + 1; + if (count > MONITOR_RECENT_EVENT_MAX) count = MONITOR_RECENT_EVENT_MAX; + shared->recentEventCount = count; + + SpinLockRelease(&shared->spinLock); + }); + + // 6. 捕获现有删除记录 + LOG_INFO_FMT("Daemon: Capturing existing deleted files on %c:...", drive); + size_t captured = monitor.CaptureExistingDeleted(24); + InterlockedExchange64(&shared->snapshotCount, (LONGLONG)monitor.GetSnapshotStore().GetCount()); + LOG_INFO_FMT("Daemon: Captured %zu existing snapshots", captured); + + // 7. 启动监控 + if (!monitor.Start()) { + LOG_ERROR("Daemon: Failed to start monitor"); + shared->daemonRunning = 0; + UnmapViewOfFile(shared); + CloseHandle(hMapping); + CloseHandle(hStopEvent); + ReleaseMutex(hMutex); + CloseHandle(hMutex); + return 1; + } + + LOG_INFO_FMT("Daemon: Monitor started on %c:, PID=%u", drive, GetCurrentProcessId()); + + // 8. 主循环 - 等待停止信号,定期更新统计 + while (WaitForSingleObject(hStopEvent, 500) == WAIT_TIMEOUT) { + // 更新统计到共享内存 + auto& stats = monitor.GetStats(); + InterlockedExchange64(&shared->totalEvents, (LONGLONG)stats.totalEvents.load()); + InterlockedExchange64(&shared->capturedCount, (LONGLONG)stats.capturedCount.load()); + InterlockedExchange64(&shared->missedCount, (LONGLONG)stats.missedCount.load()); + InterlockedExchange64(&shared->skippedCount, (LONGLONG)stats.skippedCount.load()); + InterlockedExchange64(&shared->snapshotCount, (LONGLONG)monitor.GetSnapshotStore().GetCount()); + GetSystemTimeAsFileTime(&shared->lastUpdate); + } + + // 9. 清理退出 + LOG_INFO("Daemon: Stop signal received, shutting down..."); + monitor.Stop(); + shared->daemonRunning = 0; + + UnmapViewOfFile(shared); + CloseHandle(hMapping); + CloseHandle(hStopEvent); + ReleaseMutex(hMutex); + CloseHandle(hMutex); + + LOG_INFO("Daemon: Exited cleanly"); + return 0; +} diff --git a/Filerestore_CLI/src/fileRestore/MonitorDaemon.h b/Filerestore_CLI/src/fileRestore/MonitorDaemon.h new file mode 100644 index 0000000..3ca5f24 --- /dev/null +++ b/Filerestore_CLI/src/fileRestore/MonitorDaemon.h @@ -0,0 +1,80 @@ +#pragma once +#include +#include + +// ============================================================================ +// 共享内存常量 +// ============================================================================ +constexpr DWORD MONITOR_SHARED_MAGIC = 0x46524D44; // 'FRMD' +constexpr DWORD MONITOR_SHARED_VERSION = 1; +constexpr int MONITOR_RECENT_EVENT_MAX = 16; + +// ============================================================================ +// 共享内存结构 - 固定大小 POD,无指针/STL +// ============================================================================ +#pragma pack(push, 8) + +struct MonitorRecentEvent { + ULONGLONG mftRecord; + ULONGLONG fileSize; + FILETIME deleteTime; + wchar_t fileName[260]; + BYTE captured; // 1=success, 0=failed + BYTE padding[7]; +}; + +struct MonitorSharedState { + DWORD magic, version; + char driveLetter; char pad1[3]; + DWORD pid; + FILETIME startTime, lastUpdate; + // 统计(Interlocked 更新) + volatile LONGLONG totalEvents, capturedCount, missedCount, skippedCount, snapshotCount; + DWORD pollIntervalMs; DWORD pad2; + // 环形缓冲区自旋锁 + volatile LONG spinLock; + // 环形缓冲区 + volatile LONG recentEventCount, recentEventHead; + MonitorRecentEvent recentEvents[MONITOR_RECENT_EVENT_MAX]; + // 标志 + volatile LONG daemonRunning, autoStartEnabled; +}; + +#pragma pack(pop) + +// ============================================================================ +// MonitorDaemon - 守护进程管理器 +// ============================================================================ +class MonitorDaemon { +public: + MonitorDaemon() = default; + ~MonitorDaemon(); + + // ========== 守护进程生命周期 ========== + bool StartDaemon(char drive); + bool StopDaemon(char drive); + bool IsDaemonRunning(char drive); + DWORD GetDaemonPID(char drive); + + // ========== 共享内存读取(CLI/TUI 侧)========== + bool AttachSharedMemory(char drive); + void DetachSharedMemory(); + bool ReadState(MonitorSharedState& out); + + // ========== 自启动注册表 ========== + static bool InstallAutoStart(char drive); + static bool UninstallAutoStart(char drive); + static bool IsAutoStartInstalled(char drive); + + // ========== 命名约定 ========== + static std::wstring GetMutexName(char d); + static std::wstring GetSharedMemName(char d); + static std::wstring GetStopEventName(char d); + + // ========== 守护进程入口(从 Main.cpp 调用)========== + static int RunDaemonMain(char drive); + +private: + HANDLE hSharedMem_ = nullptr; + MonitorSharedState* sharedPtr_ = nullptr; +}; diff --git a/Filerestore_CLI/src/fileRestore/UsnDeleteMonitor.cpp b/Filerestore_CLI/src/fileRestore/UsnDeleteMonitor.cpp new file mode 100644 index 0000000..28d22f5 --- /dev/null +++ b/Filerestore_CLI/src/fileRestore/UsnDeleteMonitor.cpp @@ -0,0 +1,262 @@ +#include "UsnDeleteMonitor.h" +#include "Logger.h" +#include + +using namespace std; + +// ============================================================================ +// 构造和析构 +// ============================================================================ +UsnDeleteMonitor::UsnDeleteMonitor(char driveLetter) + : driveLetter(driveLetter), running(false), + lastProcessedUsn(0), pollIntervalMs(1000), autoSaveIntervalSec(60) { + snapshotStore.SetDriveLetter(driveLetter); +} + +UsnDeleteMonitor::~UsnDeleteMonitor() { + Stop(); +} + +// ============================================================================ +// 一次性扫描:为现有 USN 删除记录捕获快照 +// ============================================================================ +size_t UsnDeleteMonitor::CaptureExistingDeleted(int maxTimeHours) { + MFTReader reader; + if (!reader.OpenVolume(driveLetter)) { + LOG_ERROR_FMT("CaptureExistingDeleted: 无法打开卷 %c:", driveLetter); + return 0; + } + reader.GetTotalMFTRecords(); + + MFTParser parser(&reader); + + UsnJournalReader usnReader; + if (!usnReader.Open(driveLetter)) { + LOG_ERROR_FMT("CaptureExistingDeleted: 无法打开 USN 日志 %c:", driveLetter); + return 0; + } + + int maxTimeSeconds = maxTimeHours * 3600; + auto deletedFiles = usnReader.ScanRecentlyDeletedFiles(maxTimeSeconds, 0); + + size_t captured = 0; + size_t missed = 0; + size_t skipped = 0; + + for (const auto& info : deletedFiles) { + // 跳过目录 + if (info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + skipped++; + continue; + } + + ULONGLONG recordNum = info.GetMftRecordNumber(); + WORD expectedSeq = info.GetExpectedSequence(); + + // 读取 MFT 记录 + vector recordData; + if (!reader.ReadMFT(recordNum, recordData)) { + missed++; + continue; + } + + // 提取文件数据信息 + auto dataInfo = parser.ExtractFileDataInfo(recordData.data(), recordData.size()); + if (!dataInfo) { + missed++; + continue; + } + + // 检查序列号:如果已经被复用(序列号递增),仍然尝试捕获 + // 因为 data runs 可能仍然指向原文件数据(尤其是刚删除的情况下) + WORD actualSeq = dataInfo->sequenceNumber; + + MFTSnapshot snapshot; + snapshot.recordNumber = recordNum; + snapshot.sequenceNumber = expectedSeq; // 使用 USN 中的序列号 + snapshot.fileName = info.FileName; + snapshot.fileSize = dataInfo->fileSize; + snapshot.dataRuns = dataInfo->dataRuns; + snapshot.isResident = dataInfo->isResident; + snapshot.residentData = dataInfo->residentData; + snapshot.parentRecord = info.GetParentMftRecordNumber(); + + // 时间戳 + GetSystemTimeAsFileTime(&snapshot.captureTime); + snapshot.deleteTime.dwLowDateTime = info.TimeStamp.LowPart; + snapshot.deleteTime.dwHighDateTime = info.TimeStamp.HighPart; + + snapshotStore.AddSnapshot(snapshot); + captured++; + } + + stats.capturedCount += captured; + stats.missedCount += missed; + stats.skippedCount += skipped; + stats.totalEvents += deletedFiles.size(); + + LOG_INFO_FMT("CaptureExistingDeleted: %zu 个删除记录, 捕获 %zu, 失败 %zu, 跳过 %zu", + deletedFiles.size(), captured, missed, skipped); + + return captured; +} + +// ============================================================================ +// 处理单个删除事件 +// ============================================================================ +bool UsnDeleteMonitor::HandleDeleteEvent(const UsnDeletedFileInfo& info) { + // 跳过目录 + if (info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + stats.skippedCount++; + return false; + } + + stats.totalEvents++; + + ULONGLONG recordNum = info.GetMftRecordNumber(); + + // 读取 MFT 记录(需要独立的 reader,因为在后台线程中) + MFTReader reader; + if (!reader.OpenVolume(driveLetter)) { + stats.missedCount++; + return false; + } + reader.GetTotalMFTRecords(); + + MFTParser parser(&reader); + + vector recordData; + if (!reader.ReadMFT(recordNum, recordData)) { + stats.missedCount++; + return false; + } + + auto dataInfo = parser.ExtractFileDataInfo(recordData.data(), recordData.size()); + if (!dataInfo) { + stats.missedCount++; + if (eventCallback) { + FILETIME ft; + ft.dwLowDateTime = info.TimeStamp.LowPart; + ft.dwHighDateTime = info.TimeStamp.HighPart; + eventCallback(recordNum, 0, ft, info.FileName, false); + } + return false; + } + + MFTSnapshot snapshot; + snapshot.recordNumber = recordNum; + snapshot.sequenceNumber = info.GetExpectedSequence(); + snapshot.fileName = info.FileName; + snapshot.fileSize = dataInfo->fileSize; + snapshot.dataRuns = dataInfo->dataRuns; + snapshot.isResident = dataInfo->isResident; + snapshot.residentData = dataInfo->residentData; + snapshot.parentRecord = info.GetParentMftRecordNumber(); + + GetSystemTimeAsFileTime(&snapshot.captureTime); + snapshot.deleteTime.dwLowDateTime = info.TimeStamp.LowPart; + snapshot.deleteTime.dwHighDateTime = info.TimeStamp.HighPart; + + snapshotStore.AddSnapshot(snapshot); + stats.capturedCount++; + + if (eventCallback) { + FILETIME ft; + ft.dwLowDateTime = info.TimeStamp.LowPart; + ft.dwHighDateTime = info.TimeStamp.HighPart; + eventCallback(recordNum, dataInfo->fileSize, ft, info.FileName, true); + } + + LOG_DEBUG_FMT("捕获快照: MFT#%llu %S (%.1fKB, %zu runs)", + recordNum, info.FileName.c_str(), + (double)dataInfo->fileSize / 1024.0, + dataInfo->dataRuns.size()); + + return true; +} + +// ============================================================================ +// 后台监控线程 +// ============================================================================ +void UsnDeleteMonitor::MonitorThread() { + LOG_INFO_FMT("USN 删除监控已启动: 驱动器 %c:, 轮询间隔 %ums", driveLetter, pollIntervalMs); + + UsnJournalReader usnReader; + if (!usnReader.Open(driveLetter)) { + LOG_ERROR_FMT("MonitorThread: 无法打开 USN 日志 %c:", driveLetter); + running = false; + return; + } + + // 获取当前 USN 位置作为起点 + UsnJournalStats journalStats; + if (usnReader.GetJournalStats(journalStats)) { + lastProcessedUsn = journalStats.NextUsn; + } + + DWORD lastSaveTime = GetTickCount(); + + while (running.load()) { + // 扫描新的删除事件 + // 使用短时间窗口(30秒)避免重复处理 + auto deleted = usnReader.ScanRecentlyDeletedFiles(30, 1000); + + for (const auto& info : deleted) { + // 只处理 USN 位置在上次处理之后的记录 + if (info.Usn > lastProcessedUsn) { + HandleDeleteEvent(info); + if (info.Usn > lastProcessedUsn) { + lastProcessedUsn = info.Usn; + } + } + } + + // 自动保存 + if (autoSaveIntervalSec > 0) { + DWORD now = GetTickCount(); + if (now - lastSaveTime >= autoSaveIntervalSec * 1000) { + string path = MFTSnapshotStore::GenerateStorePath(driveLetter); + snapshotStore.SaveToFile(path); + lastSaveTime = now; + } + } + + // 等待下一次轮询 + for (DWORD waited = 0; waited < pollIntervalMs && running.load(); waited += 100) { + Sleep(100); + } + } + + // 退出前保存 + string path = MFTSnapshotStore::GenerateStorePath(driveLetter); + snapshotStore.SaveToFile(path); + + LOG_INFO("USN 删除监控已停止"); +} + +// ============================================================================ +// 启动和停止 +// ============================================================================ +bool UsnDeleteMonitor::Start() { + if (running.load()) { + return true; // 已经在运行 + } + + // 尝试加载已有的快照 + string path = MFTSnapshotStore::GenerateStorePath(driveLetter); + snapshotStore.LoadFromFile(path); + + running = true; + monitorThread = thread(&UsnDeleteMonitor::MonitorThread, this); + + return true; +} + +void UsnDeleteMonitor::Stop() { + if (!running.load()) return; + + running = false; + if (monitorThread.joinable()) { + monitorThread.join(); + } +} diff --git a/Filerestore_CLI/src/fileRestore/UsnDeleteMonitor.h b/Filerestore_CLI/src/fileRestore/UsnDeleteMonitor.h new file mode 100644 index 0000000..b0d3693 --- /dev/null +++ b/Filerestore_CLI/src/fileRestore/UsnDeleteMonitor.h @@ -0,0 +1,103 @@ +#pragma once +#include +#include +#include +#include +#include +#include "UsnJournalReader.h" +#include "MFTReader.h" +#include "MFTParser.h" +#include "MFTSnapshotStore.h" + +using namespace std; + +// ============================================================================ +// USN 删除监控统计 +// ============================================================================ +struct UsnMonitorStats { + atomic totalEvents{0}; // 总检测到的删除事件 + atomic capturedCount{0}; // 成功捕获快照数 + atomic missedCount{0}; // MFT 已复用来不及捕获数 + atomic skippedCount{0}; // 跳过数(目录、系统文件等) +}; + +// ============================================================================ +// USN 删除监控器 - 后台轮询 USN Journal,捕获被删文件的 MFT 快照 +// ============================================================================ +class UsnDeleteMonitor { +public: + UsnDeleteMonitor(char driveLetter); + ~UsnDeleteMonitor(); + + // ========== 控制 ========== + + // 启动后台监控 + bool Start(); + + // 停止监控 + void Stop(); + + // 是否正在运行 + bool IsRunning() const { return running.load(); } + + // ========== 快照访问 ========== + + // 获取快照存储引用 + MFTSnapshotStore& GetSnapshotStore() { return snapshotStore; } + const MFTSnapshotStore& GetSnapshotStore() const { return snapshotStore; } + + // ========== 一次性扫描 ========== + + // 扫描 USN 日志中已有的删除记录并捕获快照(非后台,立即执行) + // maxTimeHours: 回溯时间(小时) + // 返回捕获的快照数 + size_t CaptureExistingDeleted(int maxTimeHours = 24); + + // ========== 统计 ========== + + const UsnMonitorStats& GetStats() const { return stats; } + + // ========== 配置 ========== + + // 轮询间隔(毫秒),默认 1000ms + void SetPollInterval(DWORD ms) { pollIntervalMs = ms; } + DWORD GetPollInterval() const { return pollIntervalMs; } + + // 自动保存间隔(秒),0 = 不自动保存,默认 60s + void SetAutoSaveInterval(DWORD seconds) { autoSaveIntervalSec = seconds; } + + // ========== 事件回调 ========== + + using EventCallback = function; + void SetEventCallback(EventCallback cb) { eventCallback = std::move(cb); } + +private: + // 后台轮询线程 + void MonitorThread(); + + // 处理单个删除事件:读取 MFT 记录,创建快照 + bool HandleDeleteEvent(const UsnDeletedFileInfo& info); + + // 组件 + char driveLetter; + MFTSnapshotStore snapshotStore; + + // 线程控制 + thread monitorThread; + atomic running; + + // USN 游标 + USN lastProcessedUsn; + + // 配置 + DWORD pollIntervalMs; + DWORD autoSaveIntervalSec; + + // 统计 + UsnMonitorStats stats; + + // 事件回调 + EventCallback eventCallback; +}; diff --git a/Filerestore_sys/Filerestore_sys.sln b/Filerestore_sys/Filerestore_sys.sln new file mode 100644 index 0000000..b08bbc5 --- /dev/null +++ b/Filerestore_sys/Filerestore_sys.sln @@ -0,0 +1,35 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36811.4 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Filerestore_sys", "Filerestore_sys\Filerestore_sys.vcxproj", "{4E97BA7A-9203-3699-D57F-5596CAD720A0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Debug|ARM64.Build.0 = Debug|ARM64 + {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Debug|x64.ActiveCfg = Debug|x64 + {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Debug|x64.Build.0 = Debug|x64 + {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Debug|x64.Deploy.0 = Debug|x64 + {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Release|ARM64.ActiveCfg = Release|ARM64 + {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Release|ARM64.Build.0 = Release|ARM64 + {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Release|ARM64.Deploy.0 = Release|ARM64 + {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Release|x64.ActiveCfg = Release|x64 + {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Release|x64.Build.0 = Release|x64 + {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Release|x64.Deploy.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C73C08B3-4928-41BE-980A-DC28CB001DD0} + EndGlobalSection +EndGlobal diff --git a/Filerestore_sys/Filerestore_sys/Filerestore_sys.inf b/Filerestore_sys/Filerestore_sys/Filerestore_sys.inf new file mode 100644 index 0000000..996f5d9 --- /dev/null +++ b/Filerestore_sys/Filerestore_sys/Filerestore_sys.inf @@ -0,0 +1,84 @@ +; +; Filerestore_sys.inf - Minifilter driver installation +; + +[Version] +Signature = "$WINDOWS NT$" +Class = "ActivityMonitor" +ClassGuid = {b86dff51-a31e-4bac-b3cf-e8cfe75c9fc2} +Provider = %ManufacturerName% +CatalogFile = Filerestore_sys.cat +DriverVer = 02/19/2026,1.0.0.0 +PnpLockdown = 1 + +[DestinationDirs] +DefaultDestDir = 12 ; %windir%\system32\drivers +FileRestoreMon.CopyFiles = 12 + +[SourceDisksNames] +1 = %DiskName%,,,"" + +[SourceDisksFiles] +Filerestore_sys.sys = 1,, + +;***************************************** +; DefaultInstall Section (non-PnP) +;***************************************** + +[DefaultInstall.NT$ARCH$] +OptionDesc = %ServiceDescription% +CopyFiles = FileRestoreMon.CopyFiles + +[DefaultInstall.NT$ARCH$.Services] +AddService = FileRestoreMon,,FileRestoreMon.Service + +[DefaultUninstall.NT$ARCH$] +LegacyUninstall = 1 +DelFiles = FileRestoreMon.CopyFiles + +[DefaultUninstall.NT$ARCH$.Services] +DelService = FileRestoreMon,0x200 ; SPSVCINST_STOPSERVICE + +;***************************************** +; File copy +;***************************************** + +[FileRestoreMon.CopyFiles] +Filerestore_sys.sys + +;***************************************** +; Service installation +;***************************************** + +[FileRestoreMon.Service] +DisplayName = %ServiceName% +Description = %ServiceDescription% +ServiceType = 2 ; SERVICE_FILE_SYSTEM_DRIVER +StartType = 3 ; SERVICE_DEMAND_START +ErrorControl = 1 ; SERVICE_ERROR_NORMAL +ServiceBinary = %12%\Filerestore_sys.sys +LoadOrderGroup = "FSFilter Activity Monitor" +AddReg = FileRestoreMon.AddRegistry + +;***************************************** +; Minifilter instance registration +;***************************************** + +[FileRestoreMon.AddRegistry] +HKR,"Instances","DefaultInstance",0x00000000,%DefaultInstance% +HKR,"Instances\"%Instance1.Name%,"Altitude",0x00000000,%Instance1.Altitude% +HKR,"Instances\"%Instance1.Name%,"Flags",0x00010001,%Instance1.Flags% + +;***************************************** +; Strings +;***************************************** + +[Strings] +ManufacturerName = "FileRestore" +ServiceName = "FileRestoreMon" +ServiceDescription = "FileRestore Monitor Minifilter Driver" +DiskName = "FileRestoreMon Installation Disk" +DefaultInstance = "FileRestoreMon Instance" +Instance1.Name = "FileRestoreMon Instance" +Instance1.Altitude = "370100" +Instance1.Flags = 0x0 ; automatic attachment diff --git a/Filerestore_sys/Filerestore_sys/Filerestore_sys.vcxproj b/Filerestore_sys/Filerestore_sys/Filerestore_sys.vcxproj new file mode 100644 index 0000000..f48cde3 --- /dev/null +++ b/Filerestore_sys/Filerestore_sys/Filerestore_sys.vcxproj @@ -0,0 +1,168 @@ + + + + + + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + {4E97BA7A-9203-3699-D57F-5596CAD720A0} + {1bc93793-694f-48fe-9372-81e2b05556fd} + v4.5 + 12.0 + Debug + x64 + Filerestore_sys + + + + Windows10 + true + WindowsKernelModeDriver10.0 + Driver + WDM + Universal + + + Windows10 + false + WindowsKernelModeDriver10.0 + Driver + WDM + Universal + + + Windows10 + true + WindowsKernelModeDriver10.0 + Driver + WDM + Universal + + + Windows10 + false + WindowsKernelModeDriver10.0 + Driver + WDM + Universal + + + + + + + + + + false + false + + + DbgengKernelDebugger + + + DbgengKernelDebugger + + + DbgengKernelDebugger + + + DbgengKernelDebugger + + + + sha256 + + + %(PreprocessorDefinitions) + %(AdditionalIncludeDirectories) + + + %(AdditionalDependencies);fltMgr.lib + + + + + sha256 + + + %(PreprocessorDefinitions) + %(AdditionalIncludeDirectories) + + + %(AdditionalDependencies);fltMgr.lib + + + + + sha256 + + + %(PreprocessorDefinitions) + %(AdditionalIncludeDirectories) + + + %(AdditionalDependencies);fltMgr.lib + + + + + sha256 + + + %(PreprocessorDefinitions) + %(AdditionalIncludeDirectories) + + + %(AdditionalDependencies);fltMgr.lib + + + + + + + + + + + + + + + + + + + + + + + + + + + 这台计算机上缺少此项目引用的 NuGet 程序包。使用“NuGet 程序包还原”可下载这些程序包。有关更多信息,请参见 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。 + + + + + + + \ No newline at end of file diff --git a/Filerestore_sys/Filerestore_sys/Filerestore_sys.vcxproj.filters b/Filerestore_sys/Filerestore_sys/Filerestore_sys.vcxproj.filters new file mode 100644 index 0000000..ee59303 --- /dev/null +++ b/Filerestore_sys/Filerestore_sys/Filerestore_sys.vcxproj.filters @@ -0,0 +1,48 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {8E41214B-6785-4CFE-B992-037D68949A14} + inf;inv;inx;mof;mc; + + + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + + + Driver Files + + + + + + diff --git a/Filerestore_sys/Filerestore_sys/common.h b/Filerestore_sys/Filerestore_sys/common.h new file mode 100644 index 0000000..ae3f61b --- /dev/null +++ b/Filerestore_sys/Filerestore_sys/common.h @@ -0,0 +1,86 @@ +/* + * common.h - Shared definitions between kernel driver and user-mode client + * + * This header is included by both the minifilter driver (kernel) and the + * user-mode application (Filerestore_CLI). Keep it pure C compatible. + */ + +#ifndef _FILERESTORE_COMMON_H_ +#define _FILERESTORE_COMMON_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +/* Communication port name */ +#define FILERESTORE_PORT_NAME L"\\FileRestoreMonPort" + +/* Command codes (user -> kernel) */ +#define CMD_START_MONITOR 1 +#define CMD_STOP_MONITOR 2 +#define CMD_GET_EVENTS 3 +#define CMD_GET_STATS 4 +#define CMD_GET_VERSION 5 + +/* Delete types */ +#define DELETE_TYPE_PERMANENT 0 +#define DELETE_TYPE_RECYCLE 1 + +/* Limits */ +#define MAX_LCN_ENTRIES 64 +#define MAX_FILE_PATH_CHARS 520 +#define DRIVER_VERSION_STRING "1.0.0" + +/* ===== Structures ===== */ + +/* LCN (Logical Cluster Number) entry for file extent mapping */ +typedef struct _LCN_ENTRY { + ULONGLONG StartLCN; + ULONG ClusterCount; + ULONG Reserved; +} LCN_ENTRY, *PLCN_ENTRY; + +/* Single delete event notification */ +typedef struct _DELETE_NOTIFICATION { + LARGE_INTEGER Timestamp; + ULONG ProcessId; + ULONG DeleteType; + LARGE_INTEGER FileSize; + LARGE_INTEGER AllocationSize; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastWriteTime; + ULONG LCNCount; + LCN_ENTRY LCNEntries[MAX_LCN_ENTRIES]; + USHORT FileNameLength; /* in bytes */ + WCHAR FileName[MAX_FILE_PATH_CHARS]; +} DELETE_NOTIFICATION, *PDELETE_NOTIFICATION; + +/* Command message (user -> kernel) */ +typedef struct _COMMAND_MESSAGE { + ULONG Command; +} COMMAND_MESSAGE, *PCOMMAND_MESSAGE; + +/* Events reply header (kernel -> user, for CMD_GET_EVENTS) */ +typedef struct _EVENTS_REPLY { + ULONG EventCount; + DELETE_NOTIFICATION Events[1]; /* variable-length array */ +} EVENTS_REPLY, *PEVENTS_REPLY; + +/* Statistics reply (kernel -> user, for CMD_GET_STATS) */ +typedef struct _STATS_REPLY { + ULONG TotalEvents; + ULONG PendingEvents; + ULONG DroppedEvents; + ULONG MonitorActive; +} STATS_REPLY, *PSTATS_REPLY; + +/* Version reply (kernel -> user, for CMD_GET_VERSION) */ +typedef struct _VERSION_REPLY { + CHAR Version[64]; +} VERSION_REPLY, *PVERSION_REPLY; + +#ifdef __cplusplus +} +#endif + +#endif /* _FILERESTORE_COMMON_H_ */ diff --git a/Filerestore_sys/Filerestore_sys/communication.c b/Filerestore_sys/Filerestore_sys/communication.c new file mode 100644 index 0000000..d9348e4 --- /dev/null +++ b/Filerestore_sys/Filerestore_sys/communication.c @@ -0,0 +1,271 @@ +/* + * communication.c - FltMgr communication port management and event buffering + */ + +#include "driver.h" + +/* Forward declarations for port callbacks */ +static NTSTATUS +ConnectNotifyCallback( + _In_ PFLT_PORT ClientPort, + _In_opt_ PVOID ServerPortCookie, + _In_reads_bytes_opt_(SizeOfContext) PVOID ConnectionContext, + _In_ ULONG SizeOfContext, + _Outptr_result_maybenull_ PVOID *ConnectionCookie + ); + +static VOID +DisconnectNotifyCallback( + _In_opt_ PVOID ConnectionCookie + ); + +static NTSTATUS +MessageNotifyCallback( + _In_opt_ PVOID PortCookie, + _In_reads_bytes_opt_(InputBufferLength) PVOID InputBuffer, + _In_ ULONG InputBufferLength, + _Out_writes_bytes_to_opt_(OutputBufferLength, *ReturnOutputBufferLength) PVOID OutputBuffer, + _In_ ULONG OutputBufferLength, + _Out_ PULONG ReturnOutputBufferLength + ); + +/* ===== SetupCommunicationPort ===== */ + +NTSTATUS +SetupCommunicationPort(VOID) +{ + NTSTATUS status; + UNICODE_STRING portName; + PSECURITY_DESCRIPTOR sd = NULL; + OBJECT_ATTRIBUTES oa; + + RtlInitUnicodeString(&portName, FILERESTORE_PORT_NAME); + + /* Build a security descriptor that only allows admin access */ + status = FltBuildDefaultSecurityDescriptor(&sd, FLT_PORT_ALL_ACCESS); + if (!NT_SUCCESS(status)) { + return status; + } + + InitializeObjectAttributes( + &oa, + &portName, + OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, + NULL, + sd + ); + + status = FltCreateCommunicationPort( + g_Context.FilterHandle, + &g_Context.ServerPort, + &oa, + NULL, /* ServerPortCookie */ + ConnectNotifyCallback, + DisconnectNotifyCallback, + MessageNotifyCallback, + 1 /* MaxConnections: single client */ + ); + + FltFreeSecurityDescriptor(sd); + return status; +} + +/* ===== ConnectNotifyCallback ===== */ + +static NTSTATUS +ConnectNotifyCallback( + _In_ PFLT_PORT ClientPort, + _In_opt_ PVOID ServerPortCookie, + _In_reads_bytes_opt_(SizeOfContext) PVOID ConnectionContext, + _In_ ULONG SizeOfContext, + _Outptr_result_maybenull_ PVOID *ConnectionCookie + ) +{ + UNREFERENCED_PARAMETER(ServerPortCookie); + UNREFERENCED_PARAMETER(ConnectionContext); + UNREFERENCED_PARAMETER(SizeOfContext); + + g_Context.ClientPort = ClientPort; + *ConnectionCookie = NULL; + return STATUS_SUCCESS; +} + +/* ===== DisconnectNotifyCallback ===== */ + +static VOID +DisconnectNotifyCallback( + _In_opt_ PVOID ConnectionCookie + ) +{ + UNREFERENCED_PARAMETER(ConnectionCookie); + + if (g_Context.ClientPort != NULL) { + FltCloseClientPort(g_Context.FilterHandle, &g_Context.ClientPort); + g_Context.ClientPort = NULL; + } + + /* Stop monitoring when client disconnects */ + InterlockedExchange(&g_Context.MonitorActive, 0); +} + +/* ===== MessageNotifyCallback ===== */ + +static NTSTATUS +MessageNotifyCallback( + _In_opt_ PVOID PortCookie, + _In_reads_bytes_opt_(InputBufferLength) PVOID InputBuffer, + _In_ ULONG InputBufferLength, + _Out_writes_bytes_to_opt_(OutputBufferLength, *ReturnOutputBufferLength) PVOID OutputBuffer, + _In_ ULONG OutputBufferLength, + _Out_ PULONG ReturnOutputBufferLength + ) +{ + PCOMMAND_MESSAGE cmdMsg; + KIRQL oldIrql; + LONG count; + LONG tail; + LONG i; + + UNREFERENCED_PARAMETER(PortCookie); + + *ReturnOutputBufferLength = 0; + + if (InputBuffer == NULL || InputBufferLength < sizeof(COMMAND_MESSAGE)) { + return STATUS_INVALID_PARAMETER; + } + + cmdMsg = (PCOMMAND_MESSAGE)InputBuffer; + + switch (cmdMsg->Command) { + + case CMD_START_MONITOR: + InterlockedExchange(&g_Context.MonitorActive, 1); + return STATUS_SUCCESS; + + case CMD_STOP_MONITOR: + InterlockedExchange(&g_Context.MonitorActive, 0); + return STATUS_SUCCESS; + + case CMD_GET_EVENTS: + { + PEVENTS_REPLY reply; + ULONG replySize; + + if (OutputBuffer == NULL || OutputBufferLength < sizeof(EVENTS_REPLY)) { + return STATUS_BUFFER_TOO_SMALL; + } + + reply = (PEVENTS_REPLY)OutputBuffer; + + KeAcquireSpinLock(&g_Context.BufferLock, &oldIrql); + + count = g_Context.EventCount; + if (count == 0) { + KeReleaseSpinLock(&g_Context.BufferLock, oldIrql); + reply->EventCount = 0; + *ReturnOutputBufferLength = FIELD_OFFSET(EVENTS_REPLY, Events); + return STATUS_SUCCESS; + } + + /* Calculate how many events fit in the output buffer */ + replySize = FIELD_OFFSET(EVENTS_REPLY, Events); + { + LONG maxEvents = (LONG)((OutputBufferLength - replySize) / sizeof(DELETE_NOTIFICATION)) + 1; + if (count > maxEvents) { + count = maxEvents; + } + } + + /* Copy events from ring buffer */ + tail = g_Context.EventTail; + for (i = 0; i < count; i++) { + RtlCopyMemory( + &reply->Events[i], + &g_Context.Events[tail], + sizeof(DELETE_NOTIFICATION) + ); + tail = (tail + 1) % MAX_PENDING_EVENTS; + } + + g_Context.EventTail = tail; + g_Context.EventCount -= count; + + KeReleaseSpinLock(&g_Context.BufferLock, oldIrql); + + reply->EventCount = (ULONG)count; + *ReturnOutputBufferLength = FIELD_OFFSET(EVENTS_REPLY, Events) + + (ULONG)count * sizeof(DELETE_NOTIFICATION); + return STATUS_SUCCESS; + } + + case CMD_GET_STATS: + { + PSTATS_REPLY statsReply; + + if (OutputBuffer == NULL || OutputBufferLength < sizeof(STATS_REPLY)) { + return STATUS_BUFFER_TOO_SMALL; + } + + statsReply = (PSTATS_REPLY)OutputBuffer; + statsReply->TotalEvents = (ULONG)g_Context.TotalEvents; + statsReply->DroppedEvents = (ULONG)g_Context.DroppedEvents; + statsReply->MonitorActive = (ULONG)g_Context.MonitorActive; + + KeAcquireSpinLock(&g_Context.BufferLock, &oldIrql); + statsReply->PendingEvents = (ULONG)g_Context.EventCount; + KeReleaseSpinLock(&g_Context.BufferLock, oldIrql); + + *ReturnOutputBufferLength = sizeof(STATS_REPLY); + return STATUS_SUCCESS; + } + + case CMD_GET_VERSION: + { + PVERSION_REPLY verReply; + + if (OutputBuffer == NULL || OutputBufferLength < sizeof(VERSION_REPLY)) { + return STATUS_BUFFER_TOO_SMALL; + } + + verReply = (PVERSION_REPLY)OutputBuffer; + RtlZeroMemory(verReply, sizeof(VERSION_REPLY)); + RtlStringCbCopyA(verReply->Version, sizeof(verReply->Version), DRIVER_VERSION_STRING); + + *ReturnOutputBufferLength = sizeof(VERSION_REPLY); + return STATUS_SUCCESS; + } + + default: + return STATUS_INVALID_PARAMETER; + } +} + +/* ===== BufferPushEvent ===== */ + +VOID +BufferPushEvent( + _In_ const DELETE_NOTIFICATION* Event + ) +{ + KIRQL oldIrql; + + KeAcquireSpinLock(&g_Context.BufferLock, &oldIrql); + + if (g_Context.EventCount >= MAX_PENDING_EVENTS) { + /* Buffer full - drop the oldest event by advancing tail */ + g_Context.EventTail = (g_Context.EventTail + 1) % MAX_PENDING_EVENTS; + g_Context.EventCount--; + InterlockedIncrement(&g_Context.DroppedEvents); + } + + RtlCopyMemory( + &g_Context.Events[g_Context.EventHead], + Event, + sizeof(DELETE_NOTIFICATION) + ); + + g_Context.EventHead = (g_Context.EventHead + 1) % MAX_PENDING_EVENTS; + g_Context.EventCount++; + + KeReleaseSpinLock(&g_Context.BufferLock, oldIrql); +} diff --git a/Filerestore_sys/Filerestore_sys/driver.c b/Filerestore_sys/Filerestore_sys/driver.c new file mode 100644 index 0000000..f91b65a --- /dev/null +++ b/Filerestore_sys/Filerestore_sys/driver.c @@ -0,0 +1,118 @@ +/* + * driver.c - DriverEntry, filter registration, unload + */ + +#include "driver.h" + +/* Global context - zero-initialized */ +GLOBAL_CONTEXT g_Context = { 0 }; + +/* Forward declarations */ +DRIVER_INITIALIZE DriverEntry; + +static NTSTATUS +FilterUnloadCallback( + _In_ FLT_FILTER_UNLOAD_FLAGS Flags + ); + +/* ===== Filter registration ===== */ + +static const FLT_OPERATION_REGISTRATION FilterCallbacks[] = { + { + IRP_MJ_SET_INFORMATION, + 0, + PreSetInformation, /* PreOperation */ + NULL /* PostOperation - not needed */ + }, + { IRP_MJ_OPERATION_END } +}; + +static const FLT_REGISTRATION FilterRegistration = { + sizeof(FLT_REGISTRATION), /* Size */ + FLT_REGISTRATION_VERSION, /* Version */ + 0, /* Flags */ + NULL, /* ContextRegistration */ + FilterCallbacks, /* OperationRegistration */ + FilterUnloadCallback, /* FilterUnloadCallback */ + NULL, /* InstanceSetupCallback */ + NULL, /* InstanceQueryTeardownCallback */ + NULL, /* InstanceTeardownStartCallback */ + NULL, /* InstanceTeardownCompleteCallback */ + NULL, /* GenerateFileNameCallback */ + NULL, /* NormalizeNameComponentCallback */ + NULL /* NormalizeContextCleanupCallback */ +}; + +/* ===== DriverEntry ===== */ + +NTSTATUS +DriverEntry( + _In_ PDRIVER_OBJECT DriverObject, + _In_ PUNICODE_STRING RegistryPath + ) +{ + NTSTATUS status; + + UNREFERENCED_PARAMETER(RegistryPath); + + /* 1. Initialize the event ring buffer lock */ + KeInitializeSpinLock(&g_Context.BufferLock); + g_Context.EventCount = 0; + g_Context.EventHead = 0; + g_Context.EventTail = 0; + g_Context.TotalEvents = 0; + g_Context.DroppedEvents = 0; + g_Context.MonitorActive = 0; + + /* 2. Register the minifilter */ + status = FltRegisterFilter( + DriverObject, + &FilterRegistration, + &g_Context.FilterHandle + ); + + if (!NT_SUCCESS(status)) { + return status; + } + + /* 3. Create the communication port */ + status = SetupCommunicationPort(); + if (!NT_SUCCESS(status)) { + FltUnregisterFilter(g_Context.FilterHandle); + return status; + } + + /* 4. Start filtering */ + status = FltStartFiltering(g_Context.FilterHandle); + if (!NT_SUCCESS(status)) { + FltCloseCommunicationPort(g_Context.ServerPort); + FltUnregisterFilter(g_Context.FilterHandle); + return status; + } + + return STATUS_SUCCESS; +} + +/* ===== FilterUnloadCallback ===== */ + +static NTSTATUS +FilterUnloadCallback( + _In_ FLT_FILTER_UNLOAD_FLAGS Flags + ) +{ + UNREFERENCED_PARAMETER(Flags); + + /* Close the server port first - prevents new connections */ + if (g_Context.ServerPort != NULL) { + FltCloseCommunicationPort(g_Context.ServerPort); + g_Context.ServerPort = NULL; + } + + /* Unregister the filter */ + if (g_Context.FilterHandle != NULL) { + FltUnregisterFilter(g_Context.FilterHandle); + g_Context.FilterHandle = NULL; + } + + return STATUS_SUCCESS; +} diff --git a/Filerestore_sys/Filerestore_sys/driver.h b/Filerestore_sys/Filerestore_sys/driver.h new file mode 100644 index 0000000..c3f9f93 --- /dev/null +++ b/Filerestore_sys/Filerestore_sys/driver.h @@ -0,0 +1,59 @@ +/* + * driver.h - Internal driver declarations + */ + +#ifndef _FILERESTORE_DRIVER_H_ +#define _FILERESTORE_DRIVER_H_ + +#include +#include +#include "common.h" + +/* Pool tag: 'MsFR' (stored little-endian as 'RFsM') */ +#define POOL_TAG 'RFsM' + +/* Maximum pending events in the kernel ring buffer */ +#define MAX_PENDING_EVENTS 256 + +/* ===== Global context ===== */ + +typedef struct _GLOBAL_CONTEXT { + PFLT_FILTER FilterHandle; + PFLT_PORT ServerPort; /* server-side communication port */ + PFLT_PORT ClientPort; /* connected client port (single client) */ + + /* Kernel event ring buffer */ + KSPIN_LOCK BufferLock; + LONG EventCount; /* number of events currently buffered */ + LONG EventHead; /* next write position */ + LONG EventTail; /* next read position */ + DELETE_NOTIFICATION Events[MAX_PENDING_EVENTS]; + + /* Statistics */ + volatile LONG TotalEvents; + volatile LONG DroppedEvents; + volatile LONG MonitorActive; /* 1 = monitoring active */ +} GLOBAL_CONTEXT, *PGLOBAL_CONTEXT; + +extern GLOBAL_CONTEXT g_Context; + +/* ===== Function prototypes ===== */ + +/* communication.c */ +NTSTATUS +SetupCommunicationPort(VOID); + +VOID +BufferPushEvent( + _In_ const DELETE_NOTIFICATION* Event + ); + +/* filter.c */ +FLT_PREOP_CALLBACK_STATUS +PreSetInformation( + _Inout_ PFLT_CALLBACK_DATA Data, + _In_ PCFLT_RELATED_OBJECTS FltObjects, + _Flt_CompletionContext_Outptr_ PVOID *CompletionContext + ); + +#endif /* _FILERESTORE_DRIVER_H_ */ diff --git a/Filerestore_sys/Filerestore_sys/filter.c b/Filerestore_sys/Filerestore_sys/filter.c new file mode 100644 index 0000000..a4456e1 --- /dev/null +++ b/Filerestore_sys/Filerestore_sys/filter.c @@ -0,0 +1,275 @@ +/* + * filter.c - PreSetInformation callback and file metadata capture + */ + +#include "driver.h" + +/* Forward declarations */ +static VOID +CaptureDeleteEvent( + _Inout_ PFLT_CALLBACK_DATA Data, + _In_ PCFLT_RELATED_OBJECTS FltObjects, + _In_ FILE_INFORMATION_CLASS InfoClass + ); + +static NTSTATUS +QueryFileLCNs( + _In_ PCFLT_RELATED_OBJECTS FltObjects, + _Inout_ PDELETE_NOTIFICATION Notification + ); + +/* ===== PreSetInformation callback ===== */ + +FLT_PREOP_CALLBACK_STATUS +PreSetInformation( + _Inout_ PFLT_CALLBACK_DATA Data, + _In_ PCFLT_RELATED_OBJECTS FltObjects, + _Flt_CompletionContext_Outptr_ PVOID *CompletionContext + ) +{ + FILE_INFORMATION_CLASS infoClass; + + *CompletionContext = NULL; + + /* Check if monitoring is active */ + if (!g_Context.MonitorActive) { + return FLT_PREOP_SUCCESS_NO_CALLBACK; + } + + infoClass = Data->Iopb->Parameters.SetFileInformation.FileInformationClass; + + switch (infoClass) { + + case FileDispositionInformation: + { + PFILE_DISPOSITION_INFORMATION dispInfo; + dispInfo = (PFILE_DISPOSITION_INFORMATION) + Data->Iopb->Parameters.SetFileInformation.InfoBuffer; + if (dispInfo == NULL || !dispInfo->DeleteFile) { + return FLT_PREOP_SUCCESS_NO_CALLBACK; + } + break; + } + + case FileDispositionInformationEx: + { + PFILE_DISPOSITION_INFORMATION_EX dispInfoEx; + dispInfoEx = (PFILE_DISPOSITION_INFORMATION_EX) + Data->Iopb->Parameters.SetFileInformation.InfoBuffer; + if (dispInfoEx == NULL || + !(dispInfoEx->Flags & FILE_DISPOSITION_DELETE)) { + return FLT_PREOP_SUCCESS_NO_CALLBACK; + } + break; + } + + default: + /* Not a delete operation */ + return FLT_PREOP_SUCCESS_NO_CALLBACK; + } + + /* Capture metadata before the delete reaches NTFS */ + CaptureDeleteEvent(Data, FltObjects, infoClass); + + /* Never block the original operation */ + return FLT_PREOP_SUCCESS_NO_CALLBACK; +} + +/* ===== CaptureDeleteEvent ===== */ + +static VOID +CaptureDeleteEvent( + _Inout_ PFLT_CALLBACK_DATA Data, + _In_ PCFLT_RELATED_OBJECTS FltObjects, + _In_ FILE_INFORMATION_CLASS InfoClass + ) +{ + DELETE_NOTIFICATION notif; + PFLT_FILE_NAME_INFORMATION nameInfo = NULL; + FILE_STANDARD_INFORMATION stdInfo; + FILE_BASIC_INFORMATION basicInfo; + NTSTATUS status; + USHORT copyLen; + + UNREFERENCED_PARAMETER(InfoClass); + + RtlZeroMemory(¬if, sizeof(notif)); + + __try { + + /* 1. Get file name */ + status = FltGetFileNameInformation( + Data, + FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, + &nameInfo + ); + + if (NT_SUCCESS(status)) { + status = FltParseFileNameInformation(nameInfo); + if (NT_SUCCESS(status)) { + copyLen = nameInfo->Name.Length; + if (copyLen > (MAX_FILE_PATH_CHARS - 1) * sizeof(WCHAR)) { + copyLen = (MAX_FILE_PATH_CHARS - 1) * sizeof(WCHAR); + } + RtlCopyMemory(notif.FileName, nameInfo->Name.Buffer, copyLen); + notif.FileNameLength = copyLen; + notif.FileName[copyLen / sizeof(WCHAR)] = L'\0'; + } + FltReleaseFileNameInformation(nameInfo); + nameInfo = NULL; + } + + /* 2. Get file size */ + status = FltQueryInformationFile( + FltObjects->Instance, + FltObjects->FileObject, + &stdInfo, + sizeof(stdInfo), + FileStandardInformation, + NULL + ); + + if (NT_SUCCESS(status)) { + notif.FileSize = stdInfo.EndOfFile; + notif.AllocationSize = stdInfo.AllocationSize; + } + + /* 3. Get timestamps */ + status = FltQueryInformationFile( + FltObjects->Instance, + FltObjects->FileObject, + &basicInfo, + sizeof(basicInfo), + FileBasicInformation, + NULL + ); + + if (NT_SUCCESS(status)) { + notif.CreationTime = basicInfo.CreationTime; + notif.LastWriteTime = basicInfo.LastWriteTime; + } + + /* 4. Get LCN mapping (may fail for resident files - that's OK) */ + QueryFileLCNs(FltObjects, ¬if); + + /* 5. Fill remaining fields */ + notif.ProcessId = FltGetRequestorProcessId(Data); + notif.DeleteType = DELETE_TYPE_PERMANENT; + KeQuerySystemTimePrecise(¬if.Timestamp); + + /* 6. Push to ring buffer */ + BufferPushEvent(¬if); + InterlockedIncrement(&g_Context.TotalEvents); + } + __except (EXCEPTION_EXECUTE_HANDLER) { + /* Silent failure - never interfere with the original delete */ + if (nameInfo != NULL) { + FltReleaseFileNameInformation(nameInfo); + } + } +} + +/* ===== QueryFileLCNs ===== */ + +static NTSTATUS +QueryFileLCNs( + _In_ PCFLT_RELATED_OBJECTS FltObjects, + _Inout_ PDELETE_NOTIFICATION Notification + ) +{ + NTSTATUS status; + STARTING_VCN_INPUT_BUFFER vcnInput; + PRETRIEVAL_POINTERS_BUFFER rpBuf = NULL; + ULONG rpBufSize = 4096; + ULONG returned; + ULONG i; + ULONG count; + LARGE_INTEGER prevVcn; + + vcnInput.StartingVcn.QuadPart = 0; + + /* Allocate initial buffer */ + rpBuf = (PRETRIEVAL_POINTERS_BUFFER)ExAllocatePool2( + POOL_FLAG_NON_PAGED, + rpBufSize, + POOL_TAG + ); + + if (rpBuf == NULL) { + return STATUS_INSUFFICIENT_RESOURCES; + } + + /* Query retrieval pointers */ + status = FltFsControlFile( + FltObjects->Instance, + FltObjects->FileObject, + FSCTL_GET_RETRIEVAL_POINTERS, + &vcnInput, + sizeof(vcnInput), + rpBuf, + rpBufSize, + &returned + ); + + if (status == STATUS_BUFFER_OVERFLOW) { + /* Retry with larger buffer */ + ExFreePoolWithTag(rpBuf, POOL_TAG); + rpBufSize = 16384; + rpBuf = (PRETRIEVAL_POINTERS_BUFFER)ExAllocatePool2( + POOL_FLAG_NON_PAGED, + rpBufSize, + POOL_TAG + ); + if (rpBuf == NULL) { + return STATUS_INSUFFICIENT_RESOURCES; + } + + status = FltFsControlFile( + FltObjects->Instance, + FltObjects->FileObject, + FSCTL_GET_RETRIEVAL_POINTERS, + &vcnInput, + sizeof(vcnInput), + rpBuf, + rpBufSize, + &returned + ); + } + + if (!NT_SUCCESS(status)) { + /* Resident files or other failures - LCNCount stays 0 */ + ExFreePoolWithTag(rpBuf, POOL_TAG); + return status; + } + + /* Parse extents */ + count = rpBuf->ExtentCount; + if (count > MAX_LCN_ENTRIES) { + count = MAX_LCN_ENTRIES; + } + + prevVcn = rpBuf->StartingVcn; + for (i = 0; i < count; i++) { + LARGE_INTEGER nextVcn = rpBuf->Extents[i].NextVcn; + LARGE_INTEGER lcn = rpBuf->Extents[i].Lcn; + + /* LCN == -1 means sparse/unallocated extent, skip */ + if (lcn.QuadPart != -1) { + Notification->LCNEntries[Notification->LCNCount].StartLCN = + (ULONGLONG)lcn.QuadPart; + Notification->LCNEntries[Notification->LCNCount].ClusterCount = + (ULONG)(nextVcn.QuadPart - prevVcn.QuadPart); + Notification->LCNEntries[Notification->LCNCount].Reserved = 0; + Notification->LCNCount++; + + if (Notification->LCNCount >= MAX_LCN_ENTRIES) { + break; + } + } + + prevVcn = nextVcn; + } + + ExFreePoolWithTag(rpBuf, POOL_TAG); + return STATUS_SUCCESS; +} diff --git a/Filerestore_sys/Filerestore_sys/packages.config b/Filerestore_sys/Filerestore_sys/packages.config new file mode 100644 index 0000000..bb6933b --- /dev/null +++ b/Filerestore_sys/Filerestore_sys/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/dev_notes/kernel_solution.docx b/dev_notes/kernel_solution.docx new file mode 100644 index 0000000000000000000000000000000000000000..7956ea8f576cc233328d0d9719fde6568c58aea4 GIT binary patch literal 41390 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 literal 0 HcmV?d00001 diff --git a/dev_notes/kernel_solution.md b/dev_notes/kernel_solution.md new file mode 100644 index 0000000..f441245 --- /dev/null +++ b/dev_notes/kernel_solution.md @@ -0,0 +1,884 @@ +# 内核层删除监控方案 — 技术设计文档 + +> 背景:MFT 复用后丢失文件的精确大小和 LCN 信息,导致签名扫描退化为全盘盲扫。 +> 本文档讨论两种解决方案:用户态 MFT 快照、内核态微过滤驱动实时监控。 + +--- + +## 一、问题定义 + +``` +现有痛点: + 文件删除 → MFT 记录被复用 → 丢失大小和 LCN → 签名扫描盲目 + +解决思路: + 在 MFT 复用之前,保存文件的关键元数据(大小、LCN、文件名) + 恢复时利用这些元数据进行定向扫描 +``` + +--- + +## 二、方案一:MFT 元数据快照(用户态) + +### 2.1 核心优势 + +| 已知信息 | 收益 | +|---------|------| +| 精确大小 | 避免截断/过度读取 | +| LCN 范围 | 扫描范围缩小 99% | +| 文件名 | 无需人工识别 | + +### 2.2 快照数据结构 + +每条记录约 100-200 字节: + +```cpp +struct MFTSnapshotEntry { + uint64_t mftRecordNo; // 8 bytes + uint64_t parentRecordNo; // 8 bytes + uint32_t sequenceNo; // 4 bytes + uint64_t fileSize; // 8 bytes + uint64_t allocSize; // 8 bytes + uint16_t lcnCount; // 2 bytes + uint64_t timestamps[4]; // 32 bytes (创建/修改/访问/变更) + uint32_t attributes; // 4 bytes + uint16_t nameLen; // 2 bytes + wchar_t name[256]; // 512 bytes (可变长) + LCNEntry lcnEntries[]; // 变长 (offset + length) +}; +``` + +### 2.3 空间估算(D 盘 731,904 条记录) + +- 精简版(无文件名):~70 MB +- 完整版(含文件名):~150 MB +- 压缩后(ZSTD):~30-50 MB + +### 2.4 实现策略 + +``` +快照管理系统: + ├── 全量快照(每日定时 / 手动触发) + ├── 增量快照(USN 驱动,检测到删除时抢读 MFT) + └── 快照合并与自动清理 + │ + ▼ + 快照存储引擎(二进制 + 索引) + │ + ┌─────┼──────┐ + ▼ ▼ ▼ +签名扫描 大小验证 LCN 过滤 +精准定位 完整性校验 范围限制 +``` + +### 2.5 对签名扫描的改进 + +**当前流程(盲目扫描):** + +``` +全盘扫描 → 找到签名头 → 猜测大小 → 读取 → 验证尾部 +问题: 大小不确定,可能截断或过度读取 +``` + +**快照辅助流程:** + +``` +查快照获取 LCN 范围 → 只扫描这些簇 → 已知精确大小 → 精准读取 +优势: 扫描范围 1% + 精确截断 + 可验证完整性 +``` + +### 2.6 命令设计 + +```bash +# 创建快照 +listdeleted D --create-snapshot + +# 自动后台快照(可选) +listdeleted D --snapshot-schedule hourly + +# 使用快照恢复 +recover D document.docx D:\out --use-snapshot + +# 签名扫描 + 快照辅助 +carvepool D all D:\out --snapshot-guided + +# 快照管理 +snapshot list D +snapshot delete D --older-than 7d +``` + +### 2.7 效果预估 + +``` +无快照: 有快照: + 扫描范围: 550 GB 扫描范围: ~5.5 GB (1%) + 扫描时间: ~4 分钟 扫描时间: ~2 秒 + 大小精度: 估计值(误差大) 大小精度: 精确值 +``` + +--- + +## 三、方案二:内核层实时监控(微过滤驱动) + +### 3.1 技术路径 + +使用 Windows 文件系统微过滤驱动(Minifilter),通过 FltMgr 框架注册 Pre-operation 回调,在文件删除操作到达文件系统之前捕获元数据。 + +### 3.2 可获取的信息 + +``` +内核层能获取的精确信息: +├── 文件大小 (FILE_STANDARD_INFORMATION) +├── LCN 列表 (FSCTL_GET_RETRIEVAL_POINTERS) +├── 完整路径 +├── 删除类型 (回收站 vs Shift+Delete) +├── 删除进程 PID/名称 +├── 精确时间戳 (微秒级) +└── 文件属性 (压缩/加密/稀疏) +``` + +### 3.3 整体架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 用户态 (Filerestore_CLI.exe) │ +│ │ +│ MonitorClient │ +│ ├── Connect() → CreateFile(设备) │ +│ ├── StartMonitoring() → DeviceIoControl(START) │ +│ ├── StopMonitoring() → DeviceIoControl(STOP) │ +│ ├── GetEvents() → 读取共享内存队列 │ +│ └── WaitForData() → WaitForSingleObject(事件) │ +└─────────────────────────┬──────────────────▲─────────────────────┘ + │ DeviceIoControl │ 共享内存 + ▼ │ +┌─────────────────────────────────────────────────────────────────┐ +│ 内核态 (FileRestoreMonitor.sys) │ +│ │ +│ DriverEntry │ +│ ├── 创建设备对象 \Device\FileRestoreMon │ +│ ├── 创建符号链接 \DosDevices\FileRestoreMon │ +│ ├── 注册 IRP 派发函数 │ +│ └── 注册文件系统过滤器 │ +│ │ │ +│ ┌───────┼───────────────────┐ │ +│ ▼ ▼ ▼ │ +│ IRP 派发 过滤回调 共享内存管理 │ +│ IRP_CREATE PreSetInformation RingBuffer │ +│ IRP_CLOSE PreCleanup DataSection │ +│ IRP_IOCTL PreCreate Event │ +│ │ │ +│ ▼ │ +│ 写入环形缓冲区 → 触发事件通知 │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ + NTFS / ReFS +``` + +### 3.4 挑战与应对 + +| 挑战 | 解决方案 | +|------|---------| +| 驱动签名 | 开发阶段:测试签名模式;发布:EV 证书 + 硬件开发人员计划 | +| 稳定性 (BSOD) | 严格错误处理 + 内存池管理 + 参数校验 | +| 性能开销 | 无锁环形缓冲区 + Per-CPU 队列 | +| 安装部署 | 可选组件,用户态降级方案(无驱动时退回快照模式) | + +--- + +## 四、用户态-内核态通信机制对比 + +### 4.1 DeviceIoControl(传统方式) + +``` +用户态 内核态 + │ │ + │ CreateFile("\\\\.\\FileRestoreMon") │ + │ ─────────────────────────────────────────►│ IRP_MJ_CREATE + │ │ + │ DeviceIoControl(IOCTL_GET_EVENTS) │ + │ ─────────────────────────────────────────►│ IRP_MJ_DEVICE_CONTROL + │ │ 查询缓冲区 + │ ◄─────────────────────────────────────────│ 返回数据 + │ │ + │ CloseHandle() │ + │ ─────────────────────────────────────────►│ IRP_MJ_CLOSE +``` + +### 4.2 共享内存 + 事件(高性能方式) + +``` +用户态 内核态 + │ │ + │ OpenFileMapping() │ + │ ─────────────────────────────────────────►│ + │ MapViewOfFile() ◄──────────────────────►│ ZwCreateSection + │ ▲ ▲ │ + │ └────────── 共享内存区域 ────────┘ │ + │ │ + │ WaitForSingleObject() ◄────────────────►│ KeSetEvent() + │ ▲ ▲ │ + │ └────────── 事件通知 ───────────┘ │ + │ │ + │ 等待事件 → 读取共享内存 → 处理数据 │ +``` + +### 4.3 对比 + +| 维度 | DeviceIoControl | 共享内存 + 事件 | +|------|----------------|----------------| +| 实现复杂度 | 低(标准模式) | 中(需同步机制) | +| 每次通信开销 | 系统调用 + IRP 构建 | 仅内存访问 | +| 适合场景 | 低频、小数据量 | 高频、大数据流 | +| 缓冲区管理 | 内核分配,用户拷贝 | 预分配,零拷贝 | +| 同步机制 | 隐式(调用阻塞) | 显式(事件/信号量) | +| 数据方向 | 双向灵活 | 适合单向流 | + +### 4.4 删除监控场景分析 + +``` +删除事件特征: +├── 频率: 高(每秒可能数十次) +├── 方向: 单向(内核 → 用户态) +├── 数据量: 每条 ~200-500 bytes +├── 实时性要求: 中(毫秒级可接受) +└── 缓冲需求: 需要队列(突发流量) +``` + +**结论:混合方案最优** + +``` +推荐架构: +├── 控制通道 (DeviceIoControl) +│ ├── 启动/停止监控 +│ ├── 配置过滤规则 +│ ├── 获取统计信息 +│ └── 获取事件句柄 +│ +└── 数据通道 (共享内存 + 事件) + ├── 高吞吐量删除事件流 + ├── 零拷贝读取 + └── 事件驱动通知 +``` + +--- + +## 五、核心代码参考 + +### 5.1 全局上下文与驱动入口 + +```cpp +// driver.cpp +#include +#include + +typedef struct _GLOBAL_CONTEXT { + PDEVICE_OBJECT DeviceObject; + PFLT_FILTER FilterHandle; + PFLT_PORT ServerPort; + HANDLE SectionHandle; + PVOID SharedMemoryBase; + SIZE_T SharedMemorySize; + KEVENT DataReadyEvent; + KSPIN_LOCK BufferLock; + ULONG WriteIndex; + ULONG ReadIndex; +} GLOBAL_CONTEXT, *PGLOBAL_CONTEXT; + +static GLOBAL_CONTEXT g_Context = {0}; + +NTSTATUS DriverEntry( + PDRIVER_OBJECT DriverObject, + PUNICODE_STRING RegistryPath +) { + NTSTATUS status; + UNICODE_STRING deviceName = RTL_CONSTANT_STRING(L"\\Device\\FileRestoreMon"); + UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\DosDevices\\FileRestoreMon"); + + // 1. 创建控制设备 + status = IoCreateDevice( + DriverObject, 0, &deviceName, + FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, + FALSE, &g_Context.DeviceObject + ); + if (!NT_SUCCESS(status)) return status; + + // 2. 创建符号链接(用户态通过 \\\\.\\FileRestoreMon 访问) + status = IoCreateSymbolicLink(&symLink, &deviceName); + if (!NT_SUCCESS(status)) { + IoDeleteDevice(g_Context.DeviceObject); + return status; + } + + // 3. 注册 IRP 派发函数 + DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate; + DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose; + DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoControl; + DriverObject->DriverUnload = DriverUnload; + + // 4. 初始化共享内存 + status = InitializeSharedMemory(); + if (!NT_SUCCESS(status)) { + IoDeleteSymbolicLink(&symLink); + IoDeleteDevice(g_Context.DeviceObject); + return status; + } + + // 5. 注册文件系统过滤器 + status = RegisterFilter(DriverObject); + if (!NT_SUCCESS(status)) { + CleanupSharedMemory(); + IoDeleteSymbolicLink(&symLink); + IoDeleteDevice(g_Context.DeviceObject); + return status; + } + + return STATUS_SUCCESS; +} +``` + +### 5.2 共享内存初始化 + +```cpp +// shared_memory.cpp +#define SHARED_MEMORY_SIZE (16 * 1024 * 1024) // 16 MB 环形缓冲区 + +NTSTATUS InitializeSharedMemory() { + NTSTATUS status; + OBJECT_ATTRIBUTES attr; + LARGE_INTEGER maxSize; + UNICODE_STRING sectionName = + RTL_CONSTANT_STRING(L"\\BaseNamedObjects\\FileRestoreMonBuffer"); + + // 1. 创建命名 Section(用户态可通过名称打开) + maxSize.QuadPart = SHARED_MEMORY_SIZE; + InitializeObjectAttributes( + &attr, §ionName, + OBJ_KERNEL_HANDLE | OBJ_OPENIF, + NULL, NULL + ); + + status = ZwCreateSection( + &g_Context.SectionHandle, + SECTION_ALL_ACCESS, &attr, &maxSize, + PAGE_READWRITE, SEC_COMMIT, NULL + ); + if (!NT_SUCCESS(status)) return status; + + // 2. 映射到内核地址空间 + SIZE_T viewSize = SHARED_MEMORY_SIZE; + status = ZwMapViewOfSection( + g_Context.SectionHandle, + ZwCurrentProcess(), + &g_Context.SharedMemoryBase, + 0, 0, NULL, &viewSize, + ViewUnmap, 0, PAGE_READWRITE + ); + if (!NT_SUCCESS(status)) { + ZwClose(g_Context.SectionHandle); + return status; + } + + // 3. 初始化环形缓冲区 + RtlZeroMemory(g_Context.SharedMemoryBase, SHARED_MEMORY_SIZE); + g_Context.WriteIndex = 0; + g_Context.ReadIndex = 0; + KeInitializeSpinLock(&g_Context.BufferLock); + + // 4. 创建通知事件 + KeInitializeEvent(&g_Context.DataReadyEvent, NotificationEvent, FALSE); + + return STATUS_SUCCESS; +} +``` + +### 5.3 文件系统过滤回调 + +```cpp +// filter_callback.cpp +#include + +// 过滤器操作注册表 +FLT_OPERATION_REGISTRATION filterOps[] = { + { IRP_MJ_SET_INFORMATION, 0, PreSetInformation, NULL }, + { IRP_MJ_CREATE, 0, PreCreate, NULL }, + { IRP_MJ_OPERATION_END } +}; + +FLT_REGISTRATION filterRegistration = { + sizeof(FLT_REGISTRATION), + FLT_REGISTRATION_VERSION, + 0, + NULL, // Context + filterOps, // Operation Registration + UnloadFilter, + NULL, NULL, NULL, NULL, NULL, NULL +}; + +NTSTATUS RegisterFilter(PDRIVER_OBJECT DriverObject) { + return FltRegisterFilter( + DriverObject, &filterRegistration, &g_Context.FilterHandle + ); +} + +// 核心回调:拦截删除操作 +FLT_PREOP_CALLBACK_STATUS PreSetInformation( + PFLT_CALLBACK_DATA Data, + PCFLT_RELATED_OBJECTS FltObjects, + PVOID *CompletionContext +) { + FILE_INFORMATION_CLASS infoClass = + Data->Iopb->Parameters.SetFileInformation.FileInformationClass; + + // 只处理删除和重命名(回收站是重命名操作) + switch (infoClass) { + case FileDispositionInformation: + case FileDispositionInformationEx: + break; + case FileRenameInformation: + case FileRenameInformationEx: + break; + default: + return FLT_PREOP_SUCCESS_NO_CALLBACK; + } + + // 检查是否真的要删除 + if (infoClass == FileDispositionInformation) { + PFILE_DISPOSITION_INFO dispInfo = + (PFILE_DISPOSITION_INFO)Data->Iopb->Parameters + .SetFileInformation.InfoBuffer; + if (!dispInfo->DeleteFile) { + return FLT_PREOP_SUCCESS_NO_CALLBACK; + } + } + + // 捕获删除事件 + CaptureDeleteEvent(FltObjects->FileObject, infoClass); + + return FLT_PREOP_SUCCESS_NO_CALLBACK; +} +``` + +### 5.4 删除事件捕获 + +```cpp +// capture.cpp + +VOID CaptureDeleteEvent( + PFILE_OBJECT FileObject, + FILE_INFORMATION_CLASS InfoClass +) { + NTSTATUS status; + DELETE_EVENT_RECORD record = {0}; + + // 1. 获取文件基本信息(时间戳、属性) + status = QueryFileBasicInfo(FileObject, &record); + if (!NT_SUCCESS(status)) return; // 静默失败,不影响原始操作 + + // 2. 获取文件大小 + status = QueryFileStandardInfo(FileObject, &record); + if (!NT_SUCCESS(status)) return; + + // 3. 获取完整路径 + status = QueryFileName(FileObject, record.FullPath, sizeof(record.FullPath)); + if (!NT_SUCCESS(status)) return; + + // 4. 获取 LCN 信息(关键) + status = QueryFileLCNs(FileObject, &record); + // 注意:这可能失败(常驻文件没有非常驻数据属性) + + // 5. 填充其他信息 + record.DeleteType = (InfoClass == FileRenameInformation) + ? DELETE_TYPE_RECYCLE + : DELETE_TYPE_PERMANENT; + record.Timestamp = KeQueryPerformanceCounter(NULL).QuadPart; + record.ProcessId = (ULONG)(ULONG_PTR)PsGetCurrentProcessId(); + + // 6. 写入环形缓冲区 + WriteToRingBuffer(&record); +} +``` + +### 5.5 获取 LCN 信息(关键技术点) + +```cpp +// lcn_query.cpp +// +// 使用 FSCTL_GET_RETRIEVAL_POINTERS 获取文件的 VCN → LCN 映射。 +// 这是 NTFS 特有的 FSCTL,返回文件数据在磁盘上的物理位置。 + +NTSTATUS QueryFileLCNs( + PFILE_OBJECT FileObject, + PDELETE_EVENT_RECORD Record +) { + NTSTATUS status; + IO_STATUS_BLOCK iosb; + STARTING_VCN_INPUT_BUFFER vcnInput = {0}; + RETRIEVAL_POINTERS_BUFFER *rpBuf = NULL; + ULONG bufSize = 4096; // 初始缓冲区 + + vcnInput.StartingVcn.QuadPart = 0; + + // 分配缓冲区并查询 + rpBuf = (RETRIEVAL_POINTERS_BUFFER*) + ExAllocatePoolWithTag(NonPagedPool, bufSize, 'CnLG'); + if (!rpBuf) return STATUS_INSUFFICIENT_RESOURCES; + + status = FltFsControlFile( + FltObjects->Instance, // 需要从回调参数传入 + FileObject, + FSCTL_GET_RETRIEVAL_POINTERS, + &vcnInput, sizeof(vcnInput), + rpBuf, bufSize, + &iosb + ); + + // 缓冲区不够时重新分配 + if (status == STATUS_BUFFER_OVERFLOW) { + ExFreePoolWithTag(rpBuf, 'CnLG'); + bufSize = (ULONG)iosb.Information; + rpBuf = (RETRIEVAL_POINTERS_BUFFER*) + ExAllocatePoolWithTag(NonPagedPool, bufSize, 'CnLG'); + if (!rpBuf) return STATUS_INSUFFICIENT_RESOURCES; + + status = FltFsControlFile( + FltObjects->Instance, + FileObject, + FSCTL_GET_RETRIEVAL_POINTERS, + &vcnInput, sizeof(vcnInput), + rpBuf, bufSize, + &iosb + ); + } + + if (NT_SUCCESS(status)) { + // 解析 VCN → LCN 映射 + LARGE_INTEGER prevVcn = rpBuf->StartingVcn; + Record->LCNCount = 0; + + for (ULONG i = 0; + i < rpBuf->ExtentCount && Record->LCNCount < MAX_LCN_ENTRIES; + i++) + { + Record->LCNEntries[Record->LCNCount].StartLCN = + rpBuf->Extents[i].Lcn.QuadPart; + Record->LCNEntries[Record->LCNCount].ClusterCount = + (ULONG)(rpBuf->Extents[i].NextVcn.QuadPart - prevVcn.QuadPart); + + prevVcn = rpBuf->Extents[i].NextVcn; + Record->LCNCount++; + } + } + + if (rpBuf) ExFreePoolWithTag(rpBuf, 'CnLG'); + return status; +} +``` + +### 5.6 环形缓冲区数据结构与写入 + +```cpp +// ring_buffer.cpp + +typedef struct _DELETE_EVENT_RECORD { + ULONG Magic; // 0xDE1E7E54 + ULONG RecordSize; // 本条记录总大小 + LARGE_INTEGER Timestamp; // 删除时间 + ULONG ProcessId; // 删除进程 PID + ULONG DeleteType; // 0=永久删除, 1=回收站 + ULONG FileNameOffset; // 文件名在记录中的偏移 + USHORT FileNameLength; // 文件名长度 (bytes) + + // 文件基本信息 + LARGE_INTEGER FileSize; + LARGE_INTEGER AllocationSize; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastWriteTime; + + // LCN 信息 + ULONG LCNCount; + struct { + ULONGLONG StartLCN; + ULONG ClusterCount; + } LCNEntries[64]; // 最多 64 个片段 + + // 文件名紧随结构体之后(变长) + WCHAR FullPath[1]; +} DELETE_EVENT_RECORD, *PDELETE_EVENT_RECORD; + + +VOID WriteToRingBuffer(PDELETE_EVENT_RECORD Record) { + KIRQL oldIrql; + ULONG recordSize = Record->RecordSize; + ULONG newWriteIndex; + + KeAcquireSpinLock(&g_Context.BufferLock, &oldIrql); + + newWriteIndex = g_Context.WriteIndex + recordSize; + + if (newWriteIndex >= SHARED_MEMORY_SIZE) { + // 环绕到缓冲区开头 + newWriteIndex = recordSize; + if (g_Context.ReadIndex < recordSize) { + // 缓冲区满,丢弃最旧记录 + g_Context.ReadIndex = recordSize; + } + } + + // 检查是否会覆盖未读数据 + if (newWriteIndex > g_Context.ReadIndex && + g_Context.WriteIndex < g_Context.ReadIndex) { + KeReleaseSpinLock(&g_Context.BufferLock, oldIrql); + return; // 缓冲区满,丢弃本条 + } + + // 写入记录 + RtlCopyMemory( + (PUCHAR)g_Context.SharedMemoryBase + g_Context.WriteIndex, + Record, recordSize + ); + g_Context.WriteIndex = newWriteIndex; + + KeReleaseSpinLock(&g_Context.BufferLock, oldIrql); + + // 通知用户态有新数据 + KeSetEvent(&g_Context.DataReadyEvent, IO_NO_INCREMENT, FALSE); +} +``` + +### 5.7 IRP 派发(DeviceIoControl 处理) + +```cpp +// dispatch.cpp + +#define IOCTL_FILE_RESTORE_START_MONITORING \ + CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) +#define IOCTL_FILE_RESTORE_STOP_MONITORING \ + CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS) +#define IOCTL_FILE_RESTORE_GET_EVENT_HANDLE \ + CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS) +#define IOCTL_FILE_RESTORE_GET_STATS \ + CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS) + +typedef struct _MONITOR_STATS { + ULONG TotalEvents; + ULONG DroppedEvents; + ULONG BufferSize; + ULONG UsedSize; +} MONITOR_STATS, *PMONITOR_STATS; + +NTSTATUS DispatchIoControl( + PDEVICE_OBJECT DeviceObject, + PIRP Irp +) { + NTSTATUS status = STATUS_SUCCESS; + PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp); + ULONG ioControlCode = irpSp->Parameters.DeviceIoControl.IoControlCode; + ULONG outBufLen = irpSp->Parameters.DeviceIoControl.OutputBufferLength; + PVOID outBuf = Irp->AssociatedIrp.SystemBuffer; + + switch (ioControlCode) { + case IOCTL_FILE_RESTORE_START_MONITORING: + // 启动监控(可扩展:从输入缓冲区读取过滤条件) + status = STATUS_SUCCESS; + break; + + case IOCTL_FILE_RESTORE_STOP_MONITORING: + status = STATUS_SUCCESS; + break; + + case IOCTL_FILE_RESTORE_GET_STATS: + if (outBufLen >= sizeof(MONITOR_STATS)) { + PMONITOR_STATS stats = (PMONITOR_STATS)outBuf; + stats->TotalEvents = g_Context.TotalEvents; + stats->DroppedEvents = g_Context.DroppedEvents; + stats->BufferSize = SHARED_MEMORY_SIZE; + stats->UsedSize = g_Context.WriteIndex - g_Context.ReadIndex; + Irp->IoStatus.Information = sizeof(MONITOR_STATS); + } else { + status = STATUS_BUFFER_TOO_SMALL; + } + break; + + default: + status = STATUS_INVALID_DEVICE_REQUEST; + break; + } + + Irp->IoStatus.Status = status; + if (!NT_SUCCESS(status)) Irp->IoStatus.Information = 0; + IoCompleteRequest(Irp, IO_NO_INCREMENT); + return status; +} +``` + +### 5.8 用户态客户端 + +```cpp +// monitor_client.cpp (用户态,集成到 Filerestore_CLI) +#include +#include +#include + +struct DeleteEvent { + std::wstring FilePath; + uint64_t FileSize; + LARGE_INTEGER DeleteTime; + bool IsRecycled; + struct { uint64_t StartLCN; uint32_t ClusterCount; }; + std::vector LCNRanges; // 简化示意 +}; + +class FileRestoreMonitorClient { + HANDLE hDevice = INVALID_HANDLE_VALUE; + HANDLE hSharedMemory = NULL; + HANDLE hEvent = NULL; + PVOID pSharedBuffer = nullptr; + SIZE_T sharedMemorySize = 0; + ULONG readIndex = 0; + +public: + bool Connect() { + // 1. 打开驱动设备 + hDevice = CreateFileW( + L"\\\\.\\FileRestoreMon", + GENERIC_READ | GENERIC_WRITE, + 0, NULL, OPEN_EXISTING, 0, NULL + ); + if (hDevice == INVALID_HANDLE_VALUE) return false; + + // 2. 打开共享内存 + hSharedMemory = OpenFileMappingW( + FILE_MAP_READ, FALSE, + L"Local\\FileRestoreMonBuffer" + ); + if (!hSharedMemory) { + CloseHandle(hDevice); + return false; + } + + pSharedBuffer = MapViewOfFile( + hSharedMemory, FILE_MAP_READ, 0, 0, 0 + ); + + readIndex = 0; + return true; + } + + bool WaitForEvent(DWORD timeoutMs) { + return WaitForSingleObject(hEvent, timeoutMs) == WAIT_OBJECT_0; + } + + std::vector ReadEvents() { + std::vector events; + PUCHAR buffer = (PUCHAR)pSharedBuffer; + + while (readIndex < sharedMemorySize) { + auto* record = (PDELETE_EVENT_RECORD)(buffer + readIndex); + if (record->Magic != 0xDE1E7E54) break; + + DeleteEvent evt; + evt.FilePath = std::wstring( + (wchar_t*)((PUCHAR)record + record->FileNameOffset), + record->FileNameLength / sizeof(wchar_t) + ); + evt.FileSize = record->FileSize.QuadPart; + evt.DeleteTime = record->Timestamp; + evt.IsRecycled = (record->DeleteType == 1); + + // 提取 LCN 信息 + for (ULONG i = 0; i < record->LCNCount; i++) { + evt.LCNRanges.push_back({ + record->LCNEntries[i].StartLCN, + record->LCNEntries[i].ClusterCount + }); + } + + events.push_back(std::move(evt)); + readIndex += record->RecordSize; + } + + ResetEvent(hEvent); + return events; + } + + void PersistToFile(const std::wstring& path) { + // 将捕获的事件持久化到磁盘,供后续恢复使用 + // 格式与 MFT 快照兼容,便于统一查询 + } + + ~FileRestoreMonitorClient() { + if (pSharedBuffer) UnmapViewOfFile(pSharedBuffer); + if (hSharedMemory) CloseHandle(hSharedMemory); + if (hEvent) CloseHandle(hEvent); + if (hDevice != INVALID_HANDLE_VALUE) CloseHandle(hDevice); + } +}; +``` + +--- + +## 六、关键技术难点 + +| 难点 | 解决方案 | +|------|---------| +| 获取 LCN 需要 FltInstance | 从 `PCFLT_RELATED_OBJECTS FltObjects` 参数获取 `FltObjects->Instance` | +| 文件名编码 | NTFS 使用 UTF-16LE,内核中使用 `UNICODE_STRING`,用户态转 `std::wstring` | +| 回收站识别 | 检测 `FileRenameInformation` 且目标路径包含 `$Recycle.Bin` | +| 高并发写入 | Per-CPU 队列 + 自旋锁(或无锁环形缓冲区) | +| 内存泄漏防护 | 使用 `ExAllocatePoolWithTag` / `ExFreePoolWithTag` 严格配对,`DriverUnload` 中清理所有资源 | +| BSOD 防护 | `__try/__except` 包裹可能失败的操作,所有指针校验后再访问 | +| 常驻文件无 LCN | `FSCTL_GET_RETRIEVAL_POINTERS` 对常驻文件返回失败,需优雅处理 | + +--- + +## 七、两方案对比 + +| 维度 | MFT 快照 | 内核监控 | +|------|---------|---------| +| 开发成本 | 低(复用 MFTCache) | 高(全新开发) | +| 实时性 | 延迟(取决于快照/轮询间隔) | 实时(Pre-op 回调) | +| 捕获率 | 取决于快照频率 | ~100% | +| 大文件恢复提升 | 5% → 30-50% | 5% → 80%+ | +| 技术风险 | 低 | 中(BSOD、兼容性) | +| 部署复杂度 | 简单(纯用户态) | 需安装驱动 + 签名 | +| 现有基础 | MFTCache 可直接复用 | 全新开发 | +| 学习价值 | 低 | 很高(内核编程) | + +--- + +## 八、实施路径 + +### Phase 1:MFT 快照(优先,复用现有基础设施) + +``` +├── 复用 MFTCache/MFTReader 读取 MFT 元数据 +├── 添加快照持久化(二进制格式 + 索引)/ 加载 +├── USN 触发模式:轮询 USN Journal,检测到删除后抢读 MFT +├── 修改 carvepool 签名扫描接入快照信息 +└── 预计提升: 扫描速度 100x,大文件恢复率 5% → 30%+ +``` + +### Phase 2:内核监控(学习项目,独立开发) + +``` +├── 搭建 WDK 开发环境,配置测试签名 +├── 实现最小化微过滤驱动原型(仅捕获删除事件,打印日志) +├── 逐步添加 LCN 查询、共享内存、环形缓冲区 +├── 用户态客户端集成到 Filerestore_CLI +└── 预计提升: 实时删除捕获 ~100% +``` + +### Phase 3:混合方案(远期) + +``` +├── 检测驱动是否已安装/运行 +├── 有驱动 → 使用实时事件流 +├── 无驱动 → 降级到 MFT 快照模式 +└── 两种数据源统一存储格式,恢复逻辑无需区分来源 +``` + +--- + +*文档整理自项目讨论记录,2026-02-17* diff --git a/dev_notes/monitor_daemon_implementation.md b/dev_notes/monitor_daemon_implementation.md new file mode 100644 index 0000000..897ac5a --- /dev/null +++ b/dev_notes/monitor_daemon_implementation.md @@ -0,0 +1,165 @@ +# Monitor Daemon + TUI Dashboard 实现总结 + +## 概述 + +本次实现将 `monitor` 命令从进程内 `std::thread` 模式升级为独立守护进程架构。CLI 退出后守护进程继续运行,通过共享内存 IPC 实现状态查询,支持开机自启动和 TUI 实时面板。 + +## 架构 + +``` + 共享内存 (MonitorSharedState) + ┌─────────────────────────┐ + CLI/TUI 进程 │ magic, version, pid │ 守护进程 + ┌──────────┐ 读取 │ 统计计数器 │ ┌──────────────┐ + │ monitor │◄────────►│ 环形缓冲区(最近事件) │◄─────│ RunDaemonMain│ + │ status │ │ daemonRunning 标志 │ 写入 │ │ + └──────────┘ └─────────────────────────┘ │ UsnDelete │ + │ Monitor │ + Named Event └──────┬───────┘ + ┌──────────┐ SetEvent │ + │ StopEvent│─────────────────────────────────────────────────►│ WaitFor + └──────────┘ │ SingleObject + ▼ + Named Mutex 退出主循环 + ┌──────────┐ + │ Mutex │ 单例保证(同一驱动器只有一个守护进程) + └──────────┘ +``` + +## 新增/修改文件清单 + +### 新建文件 + +| 文件 | 说明 | +|------|------| +| `src/fileRestore/MonitorDaemon.h` | 共享内存 POD 结构体 + MonitorDaemon 类声明 | +| `src/fileRestore/MonitorDaemon.cpp` | 守护进程启停、共享内存 IPC、注册表自启动、RunDaemonMain 主循环 | + +### 修改文件 + +| 文件 | 变更内容 | +|------|----------| +| `src/fileRestore/UsnDeleteMonitor.h` | 新增 `EventCallback` 类型和 `SetEventCallback()` | +| `src/fileRestore/UsnDeleteMonitor.cpp` | `HandleDeleteEvent` 成功/失败路径末尾调用回调 | +| `src/core/Main.cpp` | 新增 `--monitor-daemon ` 参数解析,守护进程入口 | +| `src/commands/UsnRecoverCommands.cpp` | 重写 MonitorCommand,删除 `g_monitor`,改为通过 MonitorDaemon IPC | +| `src/tui/TuiApp.h` | ViewMode 枚举新增 `Monitor`,新增 `monitorDrive_` 成员 | +| `src/tui/TuiApp.cpp` | 菜单新增 "USN Delete Monitor",Monitor Dashboard 渲染和键盘处理 | +| `src/tui/CommandHelper.cpp` | 新增 `monitor`/`snapshot`/`snapshotquery` 命令元数据 | +| `Filerestore_CLI.vcxproj` | 添加 MonitorDaemon.cpp/.h | +| `Filerestore_CLI.vcxproj.filters` | 添加 MonitorDaemon.cpp/.h 到筛选器 | + +## 核心数据结构 + +### MonitorSharedState(共享内存,固定大小 POD) + +```cpp +#pragma pack(push, 8) +struct MonitorSharedState { + DWORD magic, version; // 0x46524D44, 1 + char driveLetter; // 监控的驱动器 + DWORD pid; // 守护进程 PID + FILETIME startTime, lastUpdate; + volatile LONGLONG totalEvents, capturedCount, missedCount, skippedCount, snapshotCount; + DWORD pollIntervalMs; + volatile LONG recentEventCount, recentEventHead; + MonitorRecentEvent recentEvents[16]; // 环形缓冲区 + volatile LONG daemonRunning, autoStartEnabled; +}; +#pragma pack(pop) +``` + +### 命名对象约定 + +| 对象 | 命名格式 | 用途 | +|------|----------|------| +| Mutex | `Global\FileRestoreMonitor_D` | 单例保证 | +| FileMapping | `Global\FileRestoreMonitor_D_Mem` | 共享内存 | +| Event | `Global\FileRestoreMonitor_D_Stop` | 停止信号(手动复位) | + +## 命令接口 + +``` +monitor start 启动守护进程(CREATE_NO_WINDOW + DETACHED_PROCESS) +monitor stop 发送停止事件,等待守护进程退出 +monitor status 读取共享内存,显示统计和最近事件 +monitor autostart 写入 HKCU\...\Run 注册表键 +monitor unautostart 删除注册表键 +``` + +## TUI Monitor Dashboard + +### 菜单入口 +主菜单第 5 项 "USN Delete Monitor",选中后进入 `ViewMode::Monitor`。 + +### 面板布局 +- 头部:驱动器、PID、运行状态、自启动状态、启动时间 +- 统计区:Events / Captured / Missed / Skipped / Snapshots +- 事件表:最近 8 条删除事件(MFT#、大小、状态、文件名) +- 底部:快捷键提示 + +### 键盘快捷键 +| 按键 | 功能 | +|------|------| +| S | 启动守护进程 | +| T | 停止守护进程 | +| A | 切换自启动开关 | +| Esc | 返回主菜单 | + +Dashboard 每 200ms 自动刷新(复用异步刷新线程)。 + +## 守护进程生命周期 + +### 启动流程 (`StartDaemon`) +1. `OpenMutexW` 检查是否已有实例 +2. `GetModuleFileNameW` 获取自身 exe 路径 +3. `CreateProcessW` 以 `CREATE_NO_WINDOW | DETACHED_PROCESS` 启动子进程 +4. 命令行:`"" --monitor-daemon D` +5. 轮询最多 3 秒等待 Mutex 出现 + +### 守护进程主循环 (`RunDaemonMain`) +1. `CreateMutexW` 获取单例锁 +2. `CreateEventW` 创建停止事件(手动复位) +3. `CreateFileMappingW` + `MapViewOfFile` 创建共享内存 +4. 初始化 `MonitorSharedState` +5. 创建 `UsnDeleteMonitor`,设置 `EventCallback`(写入环形缓冲区) +6. `CaptureExistingDeleted(24)` 捕获已有删除记录 +7. `Start()` 启动后台轮询 +8. 主循环:`WaitForSingleObject(stopEvent, 500)` + 同步统计到共享内存 +9. 收到停止信号后:`Stop()` + 清理所有 Handle + +### 停止流程 (`StopDaemon`) +1. `OpenEventW` 打开停止事件 +2. `SetEvent` 通知守护进程 +3. 轮询最多 5 秒等待 Mutex 消失 + +## 自启动注册表 + +- 键路径:`HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run` +- 值名称:`FileRestoreMonitor_D`(D 为驱动器字母) +- 值数据:`"" --monitor-daemon D` +- API:`RegOpenKeyExW` / `RegSetValueExW` / `RegDeleteValueW` / `RegQueryValueExW` + +## 构建验证 + +Release x64 编译通过,0 error,0 warning。 + +``` +Filerestore_CLI.vcxproj -> D:\Users\21405\source\repos\Filerestore_CLI\x64\Release\Filerestore_CLI.exe +``` + +## 功能验证步骤 + +```bash +# CLI 验证 +Filerestore_CLI.exe --cmd "monitor D start" # 启动守护进程 +tasklist | findstr Filerestore # 查看进程 +Filerestore_CLI.exe --cmd "monitor D status" # 查看状态 +Filerestore_CLI.exe --cmd "monitor D autostart" # 启用自启动 +Filerestore_CLI.exe --cmd "monitor D stop" # 停止守护进程 + +# TUI 验证 +Filerestore_CLI.exe --tui # 进入 TUI +# 选择 "USN Delete Monitor" → Dashboard 面板 +# S=启动 T=停止 A=切换自启动 Esc=返回 +``` From 7cf4b698e9c1161968ba1bc24d803437438f6cd6 Mon Sep 17 00:00:00 2001 From: Orange20000922 <2140523341@qq.com> Date: Thu, 19 Feb 2026 20:40:34 +0800 Subject: [PATCH 2/7] 111 --- .claude/settings.local.json | 3 +- Filerestore_CLI/Filerestore_CLI.vcxproj | 8 + .../Filerestore_CLI.vcxproj.filters | 24 + .../src/commands/UsnRecoverCommands.cpp | 455 +++++++++++++++++- Filerestore_CLI/src/commands/cmd.h | 9 + Filerestore_CLI/src/core/Main.cpp | 17 + .../src/fileRestore/FileCarver.cpp | 153 +++--- .../src/fileRestore/FileRestore.cpp | 1 - Filerestore_CLI/src/fileRestore/MFTCache.cpp | 81 ++-- Filerestore_CLI/src/fileRestore/MFTCache.h | 5 +- Filerestore_CLI/src/fileRestore/MFTParser.cpp | 98 ++++ Filerestore_CLI/src/fileRestore/MFTParser.h | 19 + Filerestore_CLI/src/fileRestore/MFTReader.cpp | 9 +- Filerestore_CLI/src/fileRestore/MFTReader.h | 3 +- .../src/fileRestore/UsnTargetedRecovery.cpp | 62 ++- .../src/fileRestore/UsnTargetedRecovery.h | 9 + Filerestore_CLI/src/tui/CommandHelper.cpp | 17 + Filerestore_CLI/src/tui/TuiApp.cpp | 233 ++++++++- Filerestore_CLI/src/tui/TuiApp.h | 4 + 19 files changed, 1072 insertions(+), 138 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 380812e..2ad811c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -8,7 +8,8 @@ "Bash(powershell:*)", "Bash(python -c:*)", "WebSearch", - "Bash(python:*)" + "Bash(python:*)", + "Bash(du:*)" ] } } diff --git a/Filerestore_CLI/Filerestore_CLI.vcxproj b/Filerestore_CLI/Filerestore_CLI.vcxproj index 1fddce0..81e67c3 100644 --- a/Filerestore_CLI/Filerestore_CLI.vcxproj +++ b/Filerestore_CLI/Filerestore_CLI.vcxproj @@ -212,6 +212,10 @@ + + + + @@ -263,6 +267,10 @@ + + + + diff --git a/Filerestore_CLI/Filerestore_CLI.vcxproj.filters b/Filerestore_CLI/Filerestore_CLI.vcxproj.filters index 776dbe2..1aeec44 100644 --- a/Filerestore_CLI/Filerestore_CLI.vcxproj.filters +++ b/Filerestore_CLI/Filerestore_CLI.vcxproj.filters @@ -234,6 +234,18 @@ 源文件\FileRestore\Carving + + 源文件\FileRestore\Carving + + + 源文件\FileRestore\Carving + + + 源文件\FileRestore\Carving + + + 源文件\FileRestore\Carving + 源文件\FileRestore\Carving @@ -402,6 +414,18 @@ 头文件 + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + 头文件 diff --git a/Filerestore_CLI/src/commands/UsnRecoverCommands.cpp b/Filerestore_CLI/src/commands/UsnRecoverCommands.cpp index 3974205..069d2bd 100644 --- a/Filerestore_CLI/src/commands/UsnRecoverCommands.cpp +++ b/Filerestore_CLI/src/commands/UsnRecoverCommands.cpp @@ -17,8 +17,11 @@ #include "FileCarverRecovery.h" #include "TripleValidator.h" #include "MFTCache.h" +#include "MFTSnapshotStore.h" +#include "UsnDeleteMonitor.h" +#include "MonitorDaemon.h" #include "components/TuiInputBridge.h" - +#include "CarvedResultsCache.h" namespace fs = std::filesystem; using namespace std; @@ -568,6 +571,7 @@ void RecoverCommand::Execute(string command) { string targetStr; string outputDir; string patternStr; + bool hascarveresult = false; int hoursArg = 1; // 默认搜索最近1小时 // 收集位置参数 @@ -632,6 +636,14 @@ void RecoverCommand::Execute(string command) { MFTParser parser(&reader); UsnTargetedRecovery recovery(&reader, &parser); + // 尝试加载快照存储(用于 MFT 已复用时的回退恢复) + MFTSnapshotStore snapshotStore; + string snapshotPath = MFTSnapshotStore::GenerateStorePath(driveLetter); + if (snapshotStore.LoadFromFile(snapshotPath)) { + recovery.SetSnapshotStore(&snapshotStore); + cout << " 快照存储已加载 (" << snapshotStore.GetCount() << " 个快照)" << endl; + } + cout << "\n=== 智能文件恢复 ===" << endl; cout << "驱动器: " << driveLetter << ":/" << endl; @@ -811,31 +823,63 @@ void RecoverCommand::Execute(string command) { // ========== 第3步:签名扫描(回退路径)========== cout << "\n[3/4] 签名扫描磁盘..." << endl; - - // 根据文件扩展名确定要扫描的类型 - wstring ext = UsnTargetedRecovery::GetExtension(targetFileName); - string extNarrow = UsnTargetedRecovery::WideToNarrow(ext); - transform(extNarrow.begin(), extNarrow.end(), extNarrow.begin(), ::tolower); - - vector scanTypes; - if (!extNarrow.empty()) { - // 映射扩展名到签名类型 - if (extNarrow == "docx" || extNarrow == "xlsx" || extNarrow == "pptx") { - scanTypes.push_back("zip"); // Office 文档是 ZIP 格式 + cout << " 尝试加载签名扫描缓存" << endl; + FileCarver carver(&reader); + FileCarverRecovery carveRecovery(&reader, carver.GetSignatures()); + HybridScanConfig hybridconfig; + vector carveResults; + if (!CarvedResultsCache::HasValidCache(driveLetter)) { + // 根据文件扩展名确定要扫描的类型 + cout << " 不存在有效缓存,将执行完整扫描 " << endl; + wstring ext = UsnTargetedRecovery::GetExtension(targetFileName); + string extNarrow = UsnTargetedRecovery::WideToNarrow(ext); + transform(extNarrow.begin(), extNarrow.end(), extNarrow.begin(), ::tolower); + + vector scanTypes; + if (!extNarrow.empty()) { + // 映射扩展名到签名类型 + if (extNarrow == "docx" || extNarrow == "xlsx" || extNarrow == "pptx") { + scanTypes.push_back("zip"); // Office 文档是 ZIP 格式 + } + else { + scanTypes.push_back(extNarrow); + } + } + else { + // 没有扩展名,扫描常见类型 + scanTypes = { "zip", "pdf", "jpg", "png" }; + } + carveResults = carver.ScanHybridMode(scanTypes, hybridconfig, CARVE_SMART, 1000); + cout << " 找到 " << carveResults.size() << " 个候选文件" << endl; + } + else { + cout << " 使用缓存的签名扫描结果" << endl; + CarvedResultsCache carvecache; + if (carvecache.InitFromDrive(driveLetter) && + carvecache.LoadAllResults(carveResults, driveLetter)) { + cout << " 加载到 " << carveResults.size() << " 个候选文件" << endl; } else { - scanTypes.push_back(extNarrow); + cout << " 缓存加载失败,执行完整扫描..." << endl; + wstring ext = UsnTargetedRecovery::GetExtension(targetFileName); + string extNarrow = UsnTargetedRecovery::WideToNarrow(ext); + transform(extNarrow.begin(), extNarrow.end(), extNarrow.begin(), ::tolower); + + vector scanTypes; + if (!extNarrow.empty()) { + if (extNarrow == "docx" || extNarrow == "xlsx" || extNarrow == "pptx") { + scanTypes.push_back("zip"); + } else { + scanTypes.push_back(extNarrow); + } + } else { + scanTypes = { "zip", "pdf", "jpg", "png" }; + } + carver.SetSimdEnabled(true); + carveResults = carver.ScanHybridMode(scanTypes, hybridconfig, CARVE_SMART, 1000); + cout << " 找到 " << carveResults.size() << " 个候选文件" << endl; } - } else { - // 没有扩展名,扫描常见类型 - scanTypes = {"zip", "pdf", "jpg", "png"}; } - FileCarver carver(&reader); - FileCarverRecovery carveRecovery(&reader, carver.GetSignatures()); - vector carveResults = carver.ScanForFileTypes(scanTypes, CARVE_SMART, 200); - - cout << " 找到 " << carveResults.size() << " 个候选文件" << endl; - // ========== 第4步:三角交叉验证 ========== cout << "\n[4/4] 执行三角交叉验证 (USN + MFT + 签名)..." << endl; @@ -855,7 +899,7 @@ void RecoverCommand::Execute(string command) { cout << " 提示: 使用 'listdeleted " << driveLetter << " cache' 预先构建缓存以加速恢复" << endl; validator.BuildLcnIndex(true, false); } - + // 加载 USN 记录 validator.LoadUsnDeletedRecords(matchedUsn); @@ -1135,3 +1179,368 @@ void RecoverCommand::Execute(string command) { } } } + +// ============================================================================ +// SnapshotCommand - 立即扫描 USN 删除记录并捕获 MFT 快照 +// ============================================================================ +// 用法: snapshot [hours] +// 示例: snapshot D +// snapshot D 72 +// ============================================================================ +DEFINE_COMMAND_BASE(SnapshotCommand, "snapshot |name |name", TRUE) +REGISTER_COMMAND(SnapshotCommand); + +void SnapshotCommand::Execute(string command) { + if (!CheckName(command)) { + return; + } + + if (GET_ARG_COUNT() < 1) { + cout << "\n=== MFT 快照捕获 ===" << endl; + cout << "用法: snapshot [hours]" << endl; + cout << "功能: 扫描 USN 删除记录,为每个被删文件捕获 MFT 元数据快照" << endl; + cout << "\n示例:" << endl; + cout << " snapshot D # 捕获最近 24 小时内删除文件的快照" << endl; + cout << " snapshot D 72 # 捕获最近 72 小时内删除文件的快照" << endl; + cout << "\n说明:" << endl; + cout << " 快照保存了被删文件的 Data Run (LCN 映射) 信息。" << endl; + cout << " 即使 MFT 记录被复用,也可以通过快照直接定位文件数据。" << endl; + cout << " 建议在发现误删后立即运行此命令。" << endl; + return; + } + + string driveStr = GET_ARG_STRING(0); + char driveLetter; + if (!CommandUtils::ValidateDriveLetter(driveStr, driveLetter)) { + cout << "错误: 无效的驱动器字母" << endl; + return; + } + + int maxHours = 24; + if (GET_ARG_COUNT() >= 2) { + try { + maxHours = stoi(GET_ARG_STRING(1)); + } catch (...) { + cout << "警告: 无效的小时数,使用默认值 24" << endl; + } + } + + cout << "\n=== MFT 快照捕获 ===" << endl; + cout << "驱动器: " << driveLetter << ":/" << endl; + cout << "回溯时间: " << maxHours << " 小时" << endl; + + // 加载已有快照 + MFTSnapshotStore store; + store.SetDriveLetter(driveLetter); + string storePath = MFTSnapshotStore::GenerateStorePath(driveLetter); + store.LoadFromFile(storePath); + size_t existingCount = store.GetCount(); + + // 使用 UsnDeleteMonitor 的一次性扫描功能 + UsnDeleteMonitor monitor(driveLetter); + + // 将已有快照传递给 monitor(通过直接操作 store) + // 注意: monitor 内部有自己的 store,这里单独使用 CaptureExistingDeleted + cout << "\n正在扫描 USN 日志..." << endl; + size_t captured = monitor.CaptureExistingDeleted(maxHours); + + // 合并到持久化存储 + // 从 monitor 的 store 获取所有新捕获的快照 + auto& monitorStore = monitor.GetSnapshotStore(); + + // 保存 monitor 的快照(直接保存即可,因为是独立 store) + monitorStore.SaveToFile(storePath); + + cout << "\n=== 快照捕获完成 ===" << endl; + cout << "新捕获: " << captured << " 个快照" << endl; + cout << "总计: " << monitorStore.GetCount() << " 个快照" << endl; + cout << "存储位置: " << storePath << endl; + + auto& stats = monitor.GetStats(); + if (stats.missedCount > 0) { + cout << "未能捕获: " << stats.missedCount.load() << " 个 (MFT 记录无法解析)" << endl; + } + if (stats.skippedCount > 0) { + cout << "已跳过: " << stats.skippedCount.load() << " 个 (目录/系统文件)" << endl; + } +} + +// ============================================================================ +// MonitorCommand - 启动/停止后台 USN 删除监控 +// ============================================================================ +// 用法: monitor [start|stop|status] +// ============================================================================ +DEFINE_COMMAND_BASE(MonitorCommand, "monitor |name |name", TRUE) +REGISTER_COMMAND(MonitorCommand); + +void MonitorCommand::Execute(string command) { + if (!CheckName(command)) { + return; + } + + if (GET_ARG_COUNT() < 1) { + cout << "\n=== USN 删除监控守护进程 ===" << endl; + cout << "用法: monitor [start|stop|status|autostart|unautostart]" << endl; + cout << "功能: 启动独立后台守护进程,实时监控文件删除并自动捕获 MFT 快照" << endl; + cout << "\n示例:" << endl; + cout << " monitor D start # 启动 D 盘监控守护进程" << endl; + cout << " monitor D stop # 停止守护进程" << endl; + cout << " monitor D status # 查看守护进程状态" << endl; + cout << " monitor D autostart # 启用开机自启动" << endl; + cout << " monitor D unautostart # 禁用开机自启动" << endl; + return; + } + + string driveStr = GET_ARG_STRING(0); + char driveLetter; + if (!CommandUtils::ValidateDriveLetter(driveStr, driveLetter)) { + cout << "错误: 无效的驱动器字母" << endl; + return; + } + + string action = "start"; + if (GET_ARG_COUNT() >= 2) { + action = GET_ARG_STRING(1); + transform(action.begin(), action.end(), action.begin(), ::tolower); + } + + MonitorDaemon daemon; + + if (action == "start") { + if (daemon.IsDaemonRunning(driveLetter)) { + DWORD pid = daemon.GetDaemonPID(driveLetter); + cout << "监控守护进程已在运行中 (PID: " << pid << ")" << endl; + return; + } + + cout << "正在启动监控守护进程..." << endl; + if (daemon.StartDaemon(driveLetter)) { + DWORD pid = daemon.GetDaemonPID(driveLetter); + cout << "USN 删除监控守护进程已启动: 驱动器 " << driveLetter << ":/" << endl; + cout << "PID: " << pid << endl; + cout << "守护进程在后台独立运行,CLI 退出后仍会继续监控。" << endl; + cout << "使用 'monitor " << driveLetter << " status' 查看状态,'monitor " << driveLetter << " stop' 停止。" << endl; + } else { + cout << "错误: 无法启动监控守护进程" << endl; + } + } + else if (action == "stop") { + if (!daemon.IsDaemonRunning(driveLetter)) { + cout << "监控守护进程未在运行" << endl; + return; + } + + // 先读取统计信息 + MonitorSharedState state = {}; + if (daemon.AttachSharedMemory(driveLetter)) { + daemon.ReadState(state); + daemon.DetachSharedMemory(); + } + + cout << "正在停止监控守护进程..." << endl; + if (daemon.StopDaemon(driveLetter)) { + cout << "USN 删除监控守护进程已停止" << endl; + cout << " 检测到的删除事件: " << state.totalEvents << endl; + cout << " 成功捕获快照: " << state.capturedCount << endl; + cout << " 未能捕获: " << state.missedCount << endl; + cout << " 快照总数: " << state.snapshotCount << endl; + } else { + cout << "错误: 无法停止守护进程(可能需要手动结束进程)" << endl; + } + } + else if (action == "status") { + if (!daemon.IsDaemonRunning(driveLetter)) { + cout << "监控守护进程未在运行" << endl; + + // 检查是否有已保存的快照 + string path = MFTSnapshotStore::GenerateStorePath(driveLetter); + MFTSnapshotStore store; + if (store.LoadFromFile(path)) { + cout << "已保存的快照: " << store.GetCount() << " 个" << endl; + } + + cout << "自启动: " << (MonitorDaemon::IsAutoStartInstalled(driveLetter) ? "已启用" : "未启用") << endl; + return; + } + + if (!daemon.AttachSharedMemory(driveLetter)) { + cout << "错误: 无法读取守护进程状态" << endl; + return; + } + + MonitorSharedState state; + if (!daemon.ReadState(state)) { + cout << "错误: 共享内存数据无效" << endl; + daemon.DetachSharedMemory(); + return; + } + + // 格式化启动时间 + SYSTEMTIME st; + FileTimeToSystemTime(&state.startTime, &st); + + cout << "=== 监控守护进程状态 ===" << endl; + cout << "驱动器: " << state.driveLetter << ":/" << endl; + cout << "状态: 运行中" << endl; + cout << "PID: " << state.pid << endl; + cout << "启动时间: " << st.wYear << "-" + << setfill('0') << setw(2) << st.wMonth << "-" + << setw(2) << st.wDay << " " + << setw(2) << st.wHour << ":" + << setw(2) << st.wMinute << ":" + << setw(2) << st.wSecond << setfill(' ') << endl; + cout << "轮询间隔: " << state.pollIntervalMs << "ms" << endl; + cout << "自启动: " << (state.autoStartEnabled ? "已启用" : "未启用") << endl; + cout << "========================================" << endl; + cout << "删除事件: " << state.totalEvents << endl; + cout << "捕获快照: " << state.capturedCount << endl; + cout << "未能捕获: " << state.missedCount << endl; + cout << "已跳过: " << state.skippedCount << endl; + cout << "快照总数: " << state.snapshotCount << endl; + + // 显示最近事件 + LONG eventCount = state.recentEventCount; + if (eventCount > 0) { + cout << "========================================" << endl; + cout << "最近事件:" << endl; + int displayCount = min((int)eventCount, MONITOR_RECENT_EVENT_MAX); + LONG head = state.recentEventHead; + for (int i = displayCount - 1; i >= 0; i--) { + LONG idx = (head - 1 - i + MONITOR_RECENT_EVENT_MAX * 100) % MONITOR_RECENT_EVENT_MAX; + auto& evt = state.recentEvents[idx]; + // wide-to-narrow for console output + string narrowName; + for (size_t c = 0; c < wcslen(evt.fileName); c++) { + wchar_t wc = evt.fileName[c]; + if (wc < 128) narrowName += (char)wc; + else narrowName += '?'; + } + cout << " " << (evt.captured ? "[OK]" : "[MISS]") + << " MFT#" << evt.mftRecord + << " " << narrowName << endl; + } + } + + daemon.DetachSharedMemory(); + } + else if (action == "autostart") { + if (MonitorDaemon::InstallAutoStart(driveLetter)) { + cout << "已启用开机自启动: 驱动器 " << driveLetter << ":/" << endl; + } else { + cout << "错误: 无法设置自启动(请检查权限)" << endl; + } + } + else if (action == "unautostart") { + if (MonitorDaemon::UninstallAutoStart(driveLetter)) { + cout << "已禁用开机自启动: 驱动器 " << driveLetter << ":/" << endl; + } else { + cout << "错误: 无法移除自启动设置" << endl; + } + } + else { + cout << "错误: 未知操作 '" << action << "',可用: start, stop, status, autostart, unautostart" << endl; + } +} + +// ============================================================================ +// SnapshotQueryCommand - 查询快照存储中的文件 +// ============================================================================ +// 用法: snapshotquery [pattern] +// ============================================================================ +DEFINE_COMMAND_BASE(SnapshotQueryCommand, "snapshotquery |name |name", TRUE) +REGISTER_COMMAND(SnapshotQueryCommand); + +void SnapshotQueryCommand::Execute(string command) { + if (!CheckName(command)) { + return; + } + + if (GET_ARG_COUNT() < 1) { + cout << "\n=== 快照查询 ===" << endl; + cout << "用法: snapshotquery [pattern]" << endl; + cout << "功能: 查询快照存储中的文件" << endl; + cout << "\n示例:" << endl; + cout << " snapshotquery D # 列出所有快照" << endl; + cout << " snapshotquery D .cpp # 搜索 .cpp 文件" << endl; + cout << " snapshotquery D document # 搜索含 'document' 的文件" << endl; + return; + } + + string driveStr = GET_ARG_STRING(0); + char driveLetter; + if (!CommandUtils::ValidateDriveLetter(driveStr, driveLetter)) { + cout << "错误: 无效的驱动器字母" << endl; + return; + } + + // 加载快照 + MFTSnapshotStore store; + string storePath = MFTSnapshotStore::GenerateStorePath(driveLetter); + if (!store.LoadFromFile(storePath)) { + cout << "未找到快照存储文件。" << endl; + cout << "提示: 使用 'snapshot " << driveLetter << "' 先捕获快照。" << endl; + return; + } + + cout << "\n=== 快照查询 ===" << endl; + cout << "快照总数: " << store.GetCount() << endl; + + // 查询 + wstring pattern = L""; + if (GET_ARG_COUNT() >= 2) { + pattern = UsnTargetedRecovery::NarrowToWide(GET_ARG_STRING(1)); + } + + vector results; + if (pattern.empty()) { + // 列出所有(限制 100 条) + results = store.SearchByName(L""); + } else { + results = store.SearchByName(pattern); + } + + if (results.empty()) { + cout << "未找到匹配的快照" << endl; + return; + } + + // 显示结果 + size_t displayCount = min(results.size(), (size_t)100); + cout << "\n找到 " << results.size() << " 个快照"; + if (results.size() > 100) { + cout << " (显示前 100 条)"; + } + cout << "\n" << endl; + + cout << left << setw(8) << "MFT#" + << setw(6) << "Seq" + << setw(12) << "大小" + << setw(8) << "Runs" + << setw(10) << "类型" + << "文件名" << endl; + cout << string(70, '-') << endl; + + for (size_t i = 0; i < displayCount; i++) { + const MFTSnapshot* snap = results[i]; + + // 格式化大小 + string sizeStr; + if (snap->fileSize < 1024) { + sizeStr = to_string(snap->fileSize) + " B"; + } else if (snap->fileSize < 1024 * 1024) { + sizeStr = to_string(snap->fileSize / 1024) + " KB"; + } else { + sizeStr = to_string(snap->fileSize / (1024 * 1024)) + " MB"; + } + + string typeStr = snap->isResident ? "常驻" : "非常驻"; + + cout << left << setw(8) << snap->recordNumber + << setw(6) << snap->sequenceNumber + << setw(12) << sizeStr + << setw(8) << snap->dataRuns.size() + << setw(10) << typeStr + << UsnTargetedRecovery::WideToNarrow(snap->fileName) << endl; + } +} + diff --git a/Filerestore_CLI/src/commands/cmd.h b/Filerestore_CLI/src/commands/cmd.h index a928e4a..43b3dbc 100644 --- a/Filerestore_CLI/src/commands/cmd.h +++ b/Filerestore_CLI/src/commands/cmd.h @@ -161,6 +161,15 @@ DECLARE_COMMAND(UsnRecoverCommand); // 智能恢复向导(USN + 签名扫描联合) DECLARE_COMMAND(RecoverCommand); +// MFT 快照捕获(扫描 USN 删除记录,保存 MFT 元数据快照) +DECLARE_COMMAND(SnapshotCommand); + +// USN 删除监控(后台实时监控删除事件,自动捕获快照) +DECLARE_COMMAND(MonitorCommand); + +// 快照查询(查询已保存的 MFT 快照) +DECLARE_COMMAND(SnapshotQueryCommand); + // ============================================================================ // 文件修复命令 // ============================================================================ diff --git a/Filerestore_CLI/src/core/Main.cpp b/Filerestore_CLI/src/core/Main.cpp index fc5f44d..fcc3186 100644 --- a/Filerestore_CLI/src/core/Main.cpp +++ b/Filerestore_CLI/src/core/Main.cpp @@ -10,6 +10,7 @@ #include "CrashHandler.h" #include "LocalizationManager.h" #include "TuiApp.h" +#include "MonitorDaemon.h" using namespace std; // ============================================================================ @@ -130,6 +131,8 @@ int main(int argc, char* argv[]) std::string cmdArg; bool hasCmd = false; bool testMode = false; + bool daemonMode = false; + char daemonDrive = 0; for (int i = 1; i < argc; i++) { std::string arg = argv[i]; @@ -139,6 +142,10 @@ int main(int argc, char* argv[]) else if (arg == "--test") { testMode = true; } + else if (arg == "--monitor-daemon" && i + 1 < argc) { + daemonDrive = (char)toupper(argv[++i][0]); + if (isalpha((unsigned char)daemonDrive)) daemonMode = true; + } else if (arg == "--cmd" || arg == "-c") { // 下一个参数是命令字符串 if (i + 1 < argc) { @@ -154,6 +161,16 @@ int main(int argc, char* argv[]) } } + // --monitor-daemon 模式:作为无窗口后台守护进程运行 + if (daemonMode) { + logger.SetConsoleOutput(false); + LOG_INFO_FMT("Starting monitor daemon for drive %c:", daemonDrive); + int result = MonitorDaemon::RunDaemonMain(daemonDrive); + logger.Close(); + CrashHandler::Uninstall(); + return result; + } + // --cmd 模式优先级最高(直接执行命令并退出) if (hasCmd) { cout << "Executing command: " << cmdArg << endl; diff --git a/Filerestore_CLI/src/fileRestore/FileCarver.cpp b/Filerestore_CLI/src/fileRestore/FileCarver.cpp index ca2cb12..dc66873 100644 --- a/Filerestore_CLI/src/fileRestore/FileCarver.cpp +++ b/Filerestore_CLI/src/fileRestore/FileCarver.cpp @@ -3,6 +3,7 @@ #include "TimestampExtractor.h" #include "MFTLCNIndex.h" #include "Logger.h" +#include "../tui/TuiProgressTracker.h" #include #include #include @@ -601,16 +602,21 @@ vector FileCarver::ScanForFileTypes(const vector& fileTy // 更新进度 if (stats.scannedClusters - lastProgressLCN > PROGRESS_UPDATE_INTERVAL) { - DWORD elapsed = GetTickCount() - startTime; - double progress = (double)stats.scannedClusters / stats.totalClusters * 100.0; - double speedMBps = (elapsed > 0) ? - ((double)stats.bytesRead / (1024 * 1024)) / (elapsed / 1000.0) : 0; - - cout << "\rProgress: " << fixed << setprecision(1) << progress << "% | " - << "Scanned: " << (stats.scannedClusters / 1000) << "K clusters | " - << "Skipped: " << (stats.skippedClusters / 1000) << "K | " - << "Found: " << stats.filesFound << " files | " - << "Speed: " << setprecision(1) << speedMBps << " MB/s" << flush; + if (TuiProgressTracker::Instance().IsEnabled()) { + TuiProgressTracker::Instance().UpdateProgress( + "Signature Scan", stats.scannedClusters, stats.totalClusters, stats.filesFound); + } else { + DWORD elapsed = GetTickCount() - startTime; + double progress = (double)stats.scannedClusters / stats.totalClusters * 100.0; + double speedMBps = (elapsed > 0) ? + ((double)stats.bytesRead / (1024 * 1024)) / (elapsed / 1000.0) : 0; + + cout << "\rProgress: " << fixed << setprecision(1) << progress << "% | " + << "Scanned: " << (stats.scannedClusters / 1000) << "K clusters | " + << "Skipped: " << (stats.skippedClusters / 1000) << "K | " + << "Found: " << stats.filesFound << " files | " + << "Speed: " << setprecision(1) << speedMBps << " MB/s" << flush; + } lastProgressLCN = stats.scannedClusters; } @@ -626,16 +632,21 @@ vector FileCarver::ScanForFileTypes(const vector& fileTy // 更新进度 if (stats.scannedClusters - lastProgressLCN > PROGRESS_UPDATE_INTERVAL) { - DWORD elapsed = GetTickCount() - startTime; - double progress = (double)stats.scannedClusters / stats.totalClusters * 100.0; - double speedMBps = (elapsed > 0) ? - ((double)stats.bytesRead / (1024 * 1024)) / (elapsed / 1000.0) : 0; + if (TuiProgressTracker::Instance().IsEnabled()) { + TuiProgressTracker::Instance().UpdateProgress( + "Signature Scan", stats.scannedClusters, stats.totalClusters, stats.filesFound); + } else { + DWORD elapsed = GetTickCount() - startTime; + double progress = (double)stats.scannedClusters / stats.totalClusters * 100.0; + double speedMBps = (elapsed > 0) ? + ((double)stats.bytesRead / (1024 * 1024)) / (elapsed / 1000.0) : 0; - cout << "\rProgress: " << fixed << setprecision(1) << progress << "% | " - << "Scanned: " << (stats.scannedClusters / 1000) << "K clusters | " - << "Skipped: " << (stats.skippedClusters / 1000) << "K | " - << "Found: " << stats.filesFound << " files | " - << "Speed: " << setprecision(1) << speedMBps << " MB/s" << flush; + cout << "\rProgress: " << fixed << setprecision(1) << progress << "% | " + << "Scanned: " << (stats.scannedClusters / 1000) << "K clusters | " + << "Skipped: " << (stats.skippedClusters / 1000) << "K | " + << "Found: " << stats.filesFound << " files | " + << "Speed: " << setprecision(1) << speedMBps << " MB/s" << flush; + } lastProgressLCN = stats.scannedClusters; } @@ -647,6 +658,7 @@ vector FileCarver::ScanForFileTypes(const vector& fileTy ((double)stats.bytesRead / (1024 * 1024)) / (stats.elapsedMs / 1000.0) : 0; // 显示结果 + TuiProgressTracker::Instance().FinishProgress(); cout << "\r " << endl; cout << "\n============================================" << endl; cout << " Scan Complete" << endl; @@ -958,16 +970,21 @@ vector FileCarver::ScanForFileTypesAsync(const vector& f // 更新进度 if (stats.scannedClusters - lastProgressUpdate > PROGRESS_UPDATE_INTERVAL) { - DWORD elapsed = GetTickCount() - startTime; - double progress = (double)stats.scannedClusters / stats.totalClusters * 100.0; - double speedMBps = (elapsed > 0) ? - ((double)stats.bytesRead / (1024 * 1024)) / (elapsed / 1000.0) : 0; + if (TuiProgressTracker::Instance().IsEnabled()) { + TuiProgressTracker::Instance().UpdateProgress( + "Async Scan", stats.scannedClusters, stats.totalClusters, stats.filesFound); + } else { + DWORD elapsed = GetTickCount() - startTime; + double progress = (double)stats.scannedClusters / stats.totalClusters * 100.0; + double speedMBps = (elapsed > 0) ? + ((double)stats.bytesRead / (1024 * 1024)) / (elapsed / 1000.0) : 0; - cout << "\rProgress: " << fixed << setprecision(1) << progress << "% | " - << "Scanned: " << (stats.scannedClusters / 1000) << "K | " - << "Skipped: " << (stats.skippedClusters / 1000) << "K | " - << "Found: " << stats.filesFound << " | " - << "Speed: " << setprecision(1) << speedMBps << " MB/s [ASYNC]" << flush; + cout << "\rProgress: " << fixed << setprecision(1) << progress << "% | " + << "Scanned: " << (stats.scannedClusters / 1000) << "K | " + << "Skipped: " << (stats.skippedClusters / 1000) << "K | " + << "Found: " << stats.filesFound << " | " + << "Speed: " << setprecision(1) << speedMBps << " MB/s [ASYNC]" << flush; + } lastProgressUpdate = stats.scannedClusters; } @@ -1000,6 +1017,7 @@ vector FileCarver::ScanForFileTypesAsync(const vector& f } // 显示结果 + TuiProgressTracker::Instance().FinishProgress(); cout << "\r " << endl; cout << "\n============================================" << endl; cout << " Async Scan Complete" << endl; @@ -1218,27 +1236,33 @@ vector FileCarver::ScanForFileTypesThreadPool(const vector PROGRESS_UPDATE_INTERVAL) { - DWORD elapsed = GetTickCount() - startTime; - double progress = (double)stats.scannedClusters / stats.totalClusters * 100.0; - double speedMBps = (elapsed > 0) ? - ((double)stats.bytesRead / (1024 * 1024)) / (elapsed / 1000.0) : 0; - ULONGLONG filesFound = scanThreadPool->GetTotalFilesFound(); - double poolProgress = scanThreadPool->GetProgress(); - - // 构建进度信息 - cout << "\rI/O: " << fixed << setprecision(1) << progress << "% | " - << "Pool: " << setprecision(1) << poolProgress << "% | " - << "Scanned: " << (stats.scannedClusters / 1000) << "K | " - << "Skipped: " << (stats.skippedClusters / 1000) << "K | " - << "Found: " << filesFound; - - // 如果ML启用,显示ML增强计数 - if (mlEnabled) { - cout << " (ML:" << scanThreadPool->GetMLEnhancedCount() << ")"; - } - cout << " | Speed: " << setprecision(1) << speedMBps << " MB/s" << flush; + if (TuiProgressTracker::Instance().IsEnabled()) { + TuiProgressTracker::Instance().UpdateProgress( + "ThreadPool Scan", stats.scannedClusters, stats.totalClusters, filesFound); + } else { + DWORD elapsed = GetTickCount() - startTime; + double progress = (double)stats.scannedClusters / stats.totalClusters * 100.0; + double speedMBps = (elapsed > 0) ? + ((double)stats.bytesRead / (1024 * 1024)) / (elapsed / 1000.0) : 0; + + double poolProgress = scanThreadPool->GetProgress(); + + // 构建进度信息 + cout << "\rI/O: " << fixed << setprecision(1) << progress << "% | " + << "Pool: " << setprecision(1) << poolProgress << "% | " + << "Scanned: " << (stats.scannedClusters / 1000) << "K | " + << "Skipped: " << (stats.skippedClusters / 1000) << "K | " + << "Found: " << filesFound; + + // 如果ML启用,显示ML增强计数 + if (mlEnabled) { + cout << " (ML:" << scanThreadPool->GetMLEnhancedCount() << ")"; + } + + cout << " | Speed: " << setprecision(1) << speedMBps << " MB/s" << flush; + } lastProgressLCN = stats.scannedClusters; } @@ -1265,6 +1289,7 @@ vector FileCarver::ScanForFileTypesThreadPool(const vector& results, bool show processedCount++; if (showProgress && processedCount % 100 == 0) { - double progress = (double)processedCount / results.size() * 100.0; - cout << "\rML Enhancement: " << fixed << setprecision(1) << progress << "%" << flush; + if (TuiProgressTracker::Instance().IsEnabled()) { + TuiProgressTracker::Instance().UpdateProgress( + "ML Enhancement", processedCount, results.size(), enhancedCount); + } else { + double progress = (double)processedCount / results.size() * 100.0; + cout << "\rML Enhancement: " << fixed << setprecision(1) << progress << "%" << flush; + } } } DWORD elapsed = GetTickCount() - startTime; if (showProgress) { + TuiProgressTracker::Instance().FinishProgress(); cout << "\r " << endl; cout << "\n--- ML Enhancement Complete ---" << endl; cout << "Time: " << (elapsed / 1000) << "." << ((elapsed % 1000) / 100) << " seconds" << endl; @@ -1509,14 +1540,20 @@ vector FileCarver::ScanWithMLOnly(CarvingMode mode, // 进度显示 if (stats.scannedClusters % PROGRESS_UPDATE_INTERVAL == 0) { - double progress = (double)stats.scannedClusters / totalClusters * 100.0; - cout << "\rML Scan: " << fixed << setprecision(1) << progress << "% | " - << "Found: " << stats.filesFound << flush; + if (TuiProgressTracker::Instance().IsEnabled()) { + TuiProgressTracker::Instance().UpdateProgress( + "ML Scan", stats.scannedClusters, totalClusters, stats.filesFound); + } else { + double progress = (double)stats.scannedClusters / totalClusters * 100.0; + cout << "\rML Scan: " << fixed << setprecision(1) << progress << "% | " + << "Found: " << stats.filesFound << flush; + } } } stats.elapsedMs = GetTickCount() - startTime; + TuiProgressTracker::Instance().FinishProgress(); cout << "\r " << endl; cout << "\n--- ML-Only Scan Complete ---" << endl; cout << "Time: " << (stats.elapsedMs / 1000) << "." << ((stats.elapsedMs % 1000) / 100) << " seconds" << endl; @@ -1799,13 +1836,19 @@ vector FileCarver::ScanHybridMode( // 进度显示 if (currentLCN % (BUFFER_CLUSTERS * 10) == 0) { - double progress = (double)currentLCN / totalClusters * 100.0; - cout << "\rML Scan: " << fixed << setprecision(1) << progress << "% | " - << "Candidates: " << mlCandidates << " | Found: " << mlResults.size() << flush; + if (TuiProgressTracker::Instance().IsEnabled()) { + TuiProgressTracker::Instance().UpdateProgress( + "ML Scan", currentLCN, totalClusters, mlResults.size()); + } else { + double progress = (double)currentLCN / totalClusters * 100.0; + cout << "\rML Scan: " << fixed << setprecision(1) << progress << "% | " + << "Candidates: " << mlCandidates << " | Found: " << mlResults.size() << flush; + } } } DWORD elapsed = GetTickCount() - startTime; + TuiProgressTracker::Instance().FinishProgress(); cout << "\r " << endl; cout << "ML scan found: " << mlResults.size() << " files (candidates: " << mlCandidates << ", time: " << (elapsed / 1000) << "s)" << endl; diff --git a/Filerestore_CLI/src/fileRestore/FileRestore.cpp b/Filerestore_CLI/src/fileRestore/FileRestore.cpp index 51ebded..834cdb8 100644 --- a/Filerestore_CLI/src/fileRestore/FileRestore.cpp +++ b/Filerestore_CLI/src/fileRestore/FileRestore.cpp @@ -34,7 +34,6 @@ FileRestore::FileRestore() : currentDrive(0), volumeOpened(false) { FileRestore::~FileRestore() { CloseDrive(); - // unique_ptr 自动释放资源,无需手动 delete } bool FileRestore::OpenDrive(char driveLetter) { diff --git a/Filerestore_CLI/src/fileRestore/MFTCache.cpp b/Filerestore_CLI/src/fileRestore/MFTCache.cpp index 6e824e9..ae5e6a7 100644 --- a/Filerestore_CLI/src/fileRestore/MFTCache.cpp +++ b/Filerestore_CLI/src/fileRestore/MFTCache.cpp @@ -1,5 +1,6 @@ #include "MFTCache.h" #include "MFTReader.h" +#include "MFTParser.h" #include "Logger.h" #include #include @@ -69,6 +70,8 @@ bool MFTCache::BuildFromMFT(char drive, bool includeActive, bool showProgress) { ULONGLONG indexedFiles = 0; ULONGLONG deletedCount = 0; + MFTParser parser(&reader); + for (ULONGLONG startRecord = 0; startRecord < totalRecords; startRecord += BATCH_SIZE) { ULONGLONG recordCount = min(BATCH_SIZE, totalRecords - startRecord); vector> records; @@ -153,54 +156,21 @@ bool MFTCache::BuildFromMFT(char drive, bool includeActive, bool showProgress) { } } } - // $DATA (0x80) + // $DATA (0x80) - 使用 ExtractFileDataInfo 提取完整 Data Runs else if (attrHeader->Type == AttributeData && !hasData) { - if (attrHeader->NonResident == 1) { - PNONRESIDENT_ATTRIBUTE nrAttr = (PNONRESIDENT_ATTRIBUTE)(record + attrOffset + sizeof(ATTRIBUTE_HEADER)); - entry.fileSize = nrAttr->RealSize; - entry.isResident = false; - - // 解析第一个数据运行获取起始 LCN - const BYTE* dataRuns = record + attrOffset + nrAttr->DataRunOffset; - size_t maxLen = attrHeader->Length - nrAttr->DataRunOffset; - - if (maxLen > 0 && dataRuns[0] != 0) { - BYTE hdr = dataRuns[0]; - int lengthSize = hdr & 0x0F; - int offsetSize = (hdr >> 4) & 0x0F; - - if (lengthSize > 0 && offsetSize > 0 && 1 + lengthSize + offsetSize <= maxLen) { - // 读取长度 - ULONGLONG runLength = 0; - for (int j = 0; j < lengthSize; j++) { - runLength |= ((ULONGLONG)dataRuns[1 + j] << (j * 8)); - } - - // 读取偏移 - LONGLONG runOffset = 0; - for (int j = 0; j < offsetSize; j++) { - runOffset |= ((LONGLONG)dataRuns[1 + lengthSize + j] << (j * 8)); - } - // 符号扩展 - if (dataRuns[lengthSize + offsetSize] & 0x80) { - for (int j = offsetSize; j < 8; j++) { - runOffset |= (0xFFLL << (j * 8)); - } - } - - if (runOffset > 0) { - entry.startLCN = (ULONGLONG)runOffset; - entry.totalClusters = runLength; - hasData = true; - } - } + auto dataInfo = parser.ExtractFileDataInfo( + const_cast(record), recordSize); + if (dataInfo) { + entry.fileSize = dataInfo->fileSize; + entry.isResident = dataInfo->isResident; + entry.dataRuns = dataInfo->dataRuns; + + // 兼容性:保留 startLCN 和 totalClusters + entry.startLCN = dataInfo->dataRuns.empty() ? 0 : dataInfo->dataRuns[0].first; + entry.totalClusters = 0; + for (const auto& [lcn, count] : dataInfo->dataRuns) { + entry.totalClusters += count; } - } else { - // 驻留数据 - PRESIDENT_ATTRIBUTE resAttr = (PRESIDENT_ATTRIBUTE)(record + attrOffset + sizeof(ATTRIBUTE_HEADER)); - entry.fileSize = resAttr->ValueLength; - entry.isResident = true; - entry.startLCN = 0; hasData = true; } } @@ -284,6 +254,14 @@ void MFTCache::SerializeEntry(ofstream& out, const MFTCacheEntry& entry) { if (extLen > 0) { out.write((char*)entry.extension.data(), extLen * sizeof(WCHAR)); } + + // Data Runs (v3) + DWORD runCount = (DWORD)entry.dataRuns.size(); + out.write((char*)&runCount, sizeof(DWORD)); + for (const auto& [lcn, count] : entry.dataRuns) { + out.write((char*)&lcn, sizeof(ULONGLONG)); + out.write((char*)&count, sizeof(ULONGLONG)); + } } bool MFTCache::DeserializeEntry(ifstream& in, MFTCacheEntry& entry) { @@ -316,6 +294,17 @@ bool MFTCache::DeserializeEntry(ifstream& in, MFTCacheEntry& entry) { in.read((char*)entry.extension.data(), extLen * sizeof(WCHAR)); } + // Data Runs (v3) + DWORD runCount = 0; + in.read((char*)&runCount, sizeof(DWORD)); + if (runCount > 0 && runCount < 10000) { + entry.dataRuns.resize(runCount); + for (DWORD j = 0; j < runCount; j++) { + in.read((char*)&entry.dataRuns[j].first, sizeof(ULONGLONG)); + in.read((char*)&entry.dataRuns[j].second, sizeof(ULONGLONG)); + } + } + return in.good(); } diff --git a/Filerestore_CLI/src/fileRestore/MFTCache.h b/Filerestore_CLI/src/fileRestore/MFTCache.h index d9da4c6..dca3a68 100644 --- a/Filerestore_CLI/src/fileRestore/MFTCache.h +++ b/Filerestore_CLI/src/fileRestore/MFTCache.h @@ -11,7 +11,7 @@ using namespace std; // MFT 缓存魔术字节 constexpr DWORD MFT_CACHE_MAGIC = 0x4D465443; // "MFTC" -constexpr DWORD MFT_CACHE_VERSION = 2; +constexpr DWORD MFT_CACHE_VERSION = 3; // ============================================================================ // MFT 缓存条目 - 轻量级,只保留恢复必需的信息 @@ -19,7 +19,7 @@ constexpr DWORD MFT_CACHE_VERSION = 2; struct MFTCacheEntry { ULONGLONG recordNumber; // MFT 记录号 ULONGLONG fileSize; // 文件大小 - ULONGLONG startLCN; // 起始 LCN(第一个数据运行) + ULONGLONG startLCN; // 起始 LCN(第一个数据运行,兼容旧查询) ULONGLONG totalClusters; // 总簇数 FILETIME creationTime; // 创建时间 FILETIME modificationTime; // 修改时间 @@ -30,6 +30,7 @@ struct MFTCacheEntry { bool isDirectory; // 是否是目录 bool isResident; // 数据是否驻留在 MFT 中 WORD sequenceNumber; // 序列号(用于验证) + vector> dataRuns; // 完整 Data Runs: (LCN, 簇数) MFTCacheEntry() : recordNumber(0), fileSize(0), startLCN(0), totalClusters(0), diff --git a/Filerestore_CLI/src/fileRestore/MFTParser.cpp b/Filerestore_CLI/src/fileRestore/MFTParser.cpp index 9a761e2..c4d1ce3 100644 --- a/Filerestore_CLI/src/fileRestore/MFTParser.cpp +++ b/Filerestore_CLI/src/fileRestore/MFTParser.cpp @@ -106,6 +106,104 @@ bool MFTParser::ParseDataRuns(BYTE* dataRun, vector>& return !runs.empty(); } +optional MFTParser::ExtractFileDataInfo(BYTE* recordBuffer, size_t recordSize) { + if (!recordBuffer || recordSize < sizeof(FILE_RECORD_HEADER)) { + LOG_ERROR("ExtractFileDataInfo: 记录缓冲区无效或太小"); + return nullopt; + } + + PFILE_RECORD_HEADER header = reinterpret_cast(recordBuffer); + + // 验证签名 "FILE" + if (header->Signature != 0x454C4946) { + LOG_DEBUG_FMT("ExtractFileDataInfo: 无效的 MFT 记录签名 0x%08X", header->Signature); + return nullopt; + } + + FileDataInfo info; + info.sequenceNumber = header->SequenceNumber; + + // 确定有效的记录边界 + size_t effectiveEnd = recordSize; + if (header->UsedSize > 0 && header->UsedSize <= recordSize) { + effectiveEnd = header->UsedSize; + } + + if (header->FirstAttributeOffset >= effectiveEnd) { + LOG_DEBUG("ExtractFileDataInfo: FirstAttributeOffset 超出边界"); + return nullopt; + } + + // 遍历属性查找未命名的 $DATA (0x80) + BYTE* attrPtr = recordBuffer + header->FirstAttributeOffset; + BYTE* endPtr = recordBuffer + effectiveEnd; + + while (attrPtr + sizeof(ATTRIBUTE_HEADER) <= endPtr) { + PATTRIBUTE_HEADER attrHeader = reinterpret_cast(attrPtr); + + // 结束标记 + if (attrHeader->Type == AttributeEndOfList || attrHeader->Length == 0) { + break; + } + + // 边界检查 + if (attrPtr + attrHeader->Length > endPtr) { + break; + } + + // 找到 $DATA 属性 (0x80),跳过命名流 (ADS) + if (attrHeader->Type == AttributeData && attrHeader->NameLength == 0) { + if (attrHeader->NonResident == 0) { + // 常驻数据 + info.isResident = true; + if (attrPtr + sizeof(ATTRIBUTE_HEADER) + sizeof(RESIDENT_ATTRIBUTE) > endPtr) { + return nullopt; + } + + PRESIDENT_ATTRIBUTE resAttr = reinterpret_cast( + attrPtr + sizeof(ATTRIBUTE_HEADER)); + info.fileSize = resAttr->ValueLength; + + if (resAttr->ValueLength > 0) { + BYTE* dataStart = attrPtr + resAttr->ValueOffset; + if (dataStart + resAttr->ValueLength <= endPtr) { + info.residentData.assign(dataStart, dataStart + resAttr->ValueLength); + } + } + return info; + } + else { + // 非常驻数据 + info.isResident = false; + if (attrPtr + sizeof(ATTRIBUTE_HEADER) + sizeof(NONRESIDENT_ATTRIBUTE) > endPtr) { + return nullopt; + } + + PNONRESIDENT_ATTRIBUTE nonResAttr = reinterpret_cast( + attrPtr + sizeof(ATTRIBUTE_HEADER)); + info.fileSize = nonResAttr->RealSize; + + // 解析 Data Runs + BYTE* dataRunPtr = attrPtr + nonResAttr->DataRunOffset; + if (dataRunPtr >= endPtr) { + return nullopt; + } + + if (ParseDataRuns(dataRunPtr, info.dataRuns)) { + return info; + } + // Data runs 解析失败,但 fileSize 等信息仍有效 + return info; + } + } + + attrPtr += attrHeader->Length; + } + + LOG_DEBUG("ExtractFileDataInfo: 未找到 $DATA 属性"); + return nullopt; +} + bool MFTParser::ExtractFileData(vector& mftRecord, vector& fileData) { // 边界检查 if (mftRecord.size() < sizeof(FILE_RECORD_HEADER)) { diff --git a/Filerestore_CLI/src/fileRestore/MFTParser.h b/Filerestore_CLI/src/fileRestore/MFTParser.h index c3a946c..dcac3a1 100644 --- a/Filerestore_CLI/src/fileRestore/MFTParser.h +++ b/Filerestore_CLI/src/fileRestore/MFTParser.h @@ -2,11 +2,25 @@ #include #include #include +#include #include "MFTStructures.h" #include "MFTReader.h" using namespace std; +// ============================================================================ +// MFT 文件数据信息 - 从 MFT 记录提取的完整数据定位信息 +// ============================================================================ +struct FileDataInfo { + vector> dataRuns; // (LCN, clusterCount) + ULONGLONG fileSize; + bool isResident; + vector residentData; + WORD sequenceNumber; + + FileDataInfo() : fileSize(0), isResident(false), sequenceNumber(0) {} +}; + // MFT 解析器类 - 负责解析 MFT 记录内容 class MFTParser { @@ -22,6 +36,11 @@ class MFTParser bool ExtractFileData(vector& mftRecord, vector& fileData); bool GetIndexRoot(vector& mftRecord, vector& indexData); + // 从 MFT 记录缓冲区提取完整文件数据信息(Data Runs、文件大小、驻留状态等) + // 只解析未命名的 $DATA 属性(跳过 ADS) + // 返回 nullopt 如果记录无效或没有 $DATA 属性 + optional ExtractFileDataInfo(BYTE* recordBuffer, size_t recordSize); + // 文件信息提取 wstring GetFileNameFromRecord(vector& mftRecord, ULONGLONG& parentDir, bool enableDebug = false); wstring GetFileNameFromAttribute(BYTE* attr); diff --git a/Filerestore_CLI/src/fileRestore/MFTReader.cpp b/Filerestore_CLI/src/fileRestore/MFTReader.cpp index 9c2bc25..f533638 100644 --- a/Filerestore_CLI/src/fileRestore/MFTReader.cpp +++ b/Filerestore_CLI/src/fileRestore/MFTReader.cpp @@ -12,7 +12,6 @@ MFTReader::MFTReader() : } MFTReader::~MFTReader() { - // ScopedHandle 自动关闭 hVolume } bool MFTReader::OpenVolume(char driveLetter) { @@ -125,10 +124,14 @@ bool MFTReader::OpenVolume(char driveLetter) { return true; } -void MFTReader::CloseVolume() { - hVolume.Close(); +bool MFTReader::CloseVolume() +{ + hVolume.Close(); + return true; } + + bool MFTReader::ReadClusters(ULONGLONG startLCN, ULONGLONG clusterCount, vector& buffer) { if (!hVolume.IsValid()) { return false; diff --git a/Filerestore_CLI/src/fileRestore/MFTReader.h b/Filerestore_CLI/src/fileRestore/MFTReader.h index 31ea342..63cd325 100644 --- a/Filerestore_CLI/src/fileRestore/MFTReader.h +++ b/Filerestore_CLI/src/fileRestore/MFTReader.h @@ -67,9 +67,8 @@ class MFTReader // 卷操作 bool OpenVolume(char driveLetter); - void CloseVolume(); bool IsVolumeOpen() const { return hVolume.IsValid(); } - + bool CloseVolume(); // 基础读取操作 bool ReadClusters(ULONGLONG startLCN, ULONGLONG clusterCount, vector& buffer); bool ReadMFT(ULONGLONG fileRecordNumber, vector& record); diff --git a/Filerestore_CLI/src/fileRestore/UsnTargetedRecovery.cpp b/Filerestore_CLI/src/fileRestore/UsnTargetedRecovery.cpp index 697a089..076594a 100644 --- a/Filerestore_CLI/src/fileRestore/UsnTargetedRecovery.cpp +++ b/Filerestore_CLI/src/fileRestore/UsnTargetedRecovery.cpp @@ -1,4 +1,5 @@ #include "UsnTargetedRecovery.h" +#include "MFTSnapshotStore.h" #include "OverwriteDetector.h" #include "../utils/Logger.h" #include @@ -559,6 +560,31 @@ UsnTargetedRecoveryResult UsnTargetedRecovery::Validate(const UsnDeletedFileInfo } } + // 快照回退:Validate 阶段就检查快照,让 canRecover 正确反映可恢复性 + if (!result.canRecover && snapshotStore && + (result.status == UsnRecoveryStatus::MFT_RECORD_REUSED || + result.status == UsnRecoveryStatus::NO_DATA_ATTRIBUTE)) { + + ULONGLONG recordNum = usnInfo.GetMftRecordNumber(); + WORD expectedSeq = ExtractSequenceNumber(usnInfo.FileReferenceNumber); + + const MFTSnapshot* snapshot = snapshotStore->FindByRecord(recordNum, expectedSeq); + if (!snapshot) { + snapshot = snapshotStore->FindLatestByRecord(recordNum); + } + + if (snapshot && (!snapshot->dataRuns.empty() || snapshot->isResident)) { + result.canRecover = true; + result.status = UsnRecoveryStatus::SNAPSHOT_RECOVERED; + result.fileSize = snapshot->fileSize; + result.dataRuns = snapshot->dataRuns; + result.isResident = snapshot->isResident; + result.residentData = snapshot->residentData; + LOG_INFO_FMT("Validate: 快照可用于 MFT#%llu (seq %u), 标记为可恢复", + recordNum, snapshot->sequenceNumber); + } + } + result.statusMessage = GetStatusMessage(result.status); return result; } @@ -577,7 +603,38 @@ UsnTargetedRecoveryResult UsnTargetedRecovery::Recover( // 检查是否可以恢复 if (!result.canRecover && !forceRecover) { - return result; + // 快照回退:如果 MFT 已复用,尝试从快照存储恢复 + if (snapshotStore && + (result.status == UsnRecoveryStatus::MFT_RECORD_REUSED || + result.status == UsnRecoveryStatus::NO_DATA_ATTRIBUTE)) { + + ULONGLONG recordNum = usnInfo.GetMftRecordNumber(); + WORD expectedSeq = ExtractSequenceNumber(usnInfo.FileReferenceNumber); + + const MFTSnapshot* snapshot = snapshotStore->FindByRecord(recordNum, expectedSeq); + if (!snapshot) { + snapshot = snapshotStore->FindLatestByRecord(recordNum); + } + + if (snapshot && (!snapshot->dataRuns.empty() || snapshot->isResident)) { + LOG_INFO_FMT("MFT 已复用,使用快照恢复: MFT#%llu (seq %u)", + recordNum, snapshot->sequenceNumber); + + // 用快照数据恢复 + result.dataRuns = snapshot->dataRuns; + result.fileSize = snapshot->fileSize; + result.isResident = snapshot->isResident; + result.residentData = snapshot->residentData; + result.canRecover = true; + result.status = UsnRecoveryStatus::SNAPSHOT_RECOVERED; + result.statusMessage = GetStatusMessage(result.status); + // 继续执行恢复流程(不 return) + } + } + + if (!result.canRecover) { + return result; + } } // 读取文件数据 @@ -843,6 +900,7 @@ string UsnTargetedRecovery::GetStatusString(UsnRecoveryStatus status) { case UsnRecoveryStatus::READ_ERROR: return "READ_ERROR"; case UsnRecoveryStatus::WRITE_ERROR: return "WRITE_ERROR"; case UsnRecoveryStatus::RESIDENT_DATA: return "RESIDENT"; + case UsnRecoveryStatus::SNAPSHOT_RECOVERED: return "SNAPSHOT"; default: return "UNKNOWN"; } } @@ -873,6 +931,8 @@ string UsnTargetedRecovery::GetStatusMessage(UsnRecoveryStatus status) { return "写入文件错误"; case UsnRecoveryStatus::RESIDENT_DATA: return "小文件,数据完整(常驻数据)"; + case UsnRecoveryStatus::SNAPSHOT_RECOVERED: + return "通过 MFT 快照恢复(MFT 已复用,使用预保存的 Data Runs)"; default: return "未知错误"; } diff --git a/Filerestore_CLI/src/fileRestore/UsnTargetedRecovery.h b/Filerestore_CLI/src/fileRestore/UsnTargetedRecovery.h index 9204873..5289292 100644 --- a/Filerestore_CLI/src/fileRestore/UsnTargetedRecovery.h +++ b/Filerestore_CLI/src/fileRestore/UsnTargetedRecovery.h @@ -11,6 +11,8 @@ using namespace std; +class MFTSnapshotStore; // 前向声明 + // ============================================================================ // USN 定点恢复 - 结合 USN 日志和签名验证的精准文件恢复 // ============================================================================ @@ -29,6 +31,7 @@ enum class UsnRecoveryStatus { READ_ERROR, // 读取错误 WRITE_ERROR, // 写入错误 RESIDENT_DATA, // 常驻数据(小文件,数据在 MFT 记录内) + SNAPSHOT_RECOVERED, // 通过 MFT 快照恢复成功(MFT 已复用但快照中有 Data Runs) UNKNOWN_ERROR // 未知错误 }; @@ -99,6 +102,7 @@ class UsnTargetedRecovery { MFTReader* reader; MFTParser* parser; UsnJournalReader usnReader; + MFTSnapshotStore* snapshotStore = nullptr; // 可选的快照存储 // 签名数据库(简化版,用于快速验证) struct SimpleSignature { @@ -143,6 +147,11 @@ class UsnTargetedRecovery { UsnTargetedRecovery(MFTReader* mftReader, MFTParser* mftParser); ~UsnTargetedRecovery(); + // ==================== 快照集成 ==================== + + // 设置快照存储(可选,用于 MFT 复用时的回退恢复) + void SetSnapshotStore(MFTSnapshotStore* store) { snapshotStore = store; } + // ==================== 核心功能 ==================== // 验证单个 USN 记录的可恢复性 diff --git a/Filerestore_CLI/src/tui/CommandHelper.cpp b/Filerestore_CLI/src/tui/CommandHelper.cpp index b754f36..2b83ee5 100644 --- a/Filerestore_CLI/src/tui/CommandHelper.cpp +++ b/Filerestore_CLI/src/tui/CommandHelper.cpp @@ -141,6 +141,23 @@ void CommandHelper::InitializeMetadata() { {"output", "Output Directory", false, "", {}}, }}; + // ======== 监控命令 ======== + commandMetadata_["monitor"] = {"monitor", "Start/stop deletion monitor daemon", + "monitor [start|stop|status|autostart|unautostart]", { + {"drive", "Drive", true, "", {"C:", "D:", "E:", "F:"}}, + {"action", "Action", false, "start", {"start", "stop", "status", "autostart", "unautostart"}}, + }}; + commandMetadata_["snapshot"] = {"snapshot", "Capture MFT snapshots for deleted files", + "snapshot [hours]", { + {"drive", "Drive", true, "", {"C:", "D:", "E:", "F:"}}, + {"hours", "Hours", false, "24", {}}, + }}; + commandMetadata_["snapshotquery"] = {"snapshotquery", "Query saved MFT snapshots", + "snapshotquery [pattern]", { + {"drive", "Drive", true, "", {"C:", "D:", "E:", "F:"}}, + {"pattern", "Pattern", false, "", {}}, + }}; + // ======== 机器学习命令 ======== commandMetadata_["mlpredict"] = {"mlpredict", "ML-predict file integrity", "mlpredict ", { diff --git a/Filerestore_CLI/src/tui/TuiApp.cpp b/Filerestore_CLI/src/tui/TuiApp.cpp index 2587002..8b19390 100644 --- a/Filerestore_CLI/src/tui/TuiApp.cpp +++ b/Filerestore_CLI/src/tui/TuiApp.cpp @@ -5,6 +5,7 @@ #include "TuiProgressTracker.h" #include "cli.h" #include "Logger.h" +#include "MonitorDaemon.h" #include "ftxui/component/component.hpp" #include "ftxui/component/screen_interactive.hpp" @@ -253,6 +254,7 @@ void TuiApp::Run() { "Scan for Deleted Files", "Deep Scan (Signature Carving)", "Repair Corrupted Files", + "USN Delete Monitor", "Browse Previous Results", "Advanced (Command Line)", }; @@ -265,8 +267,9 @@ void TuiApp::Run() { "listdeleted", // 1: Scan Deleted "carvepool", // 2: Deep Scan "repair", // 3: Repair - "", // 4: Browse Results (直接执行) - "", // 5: Advanced + "", // 4: Monitor Dashboard + "", // 5: Browse Results (直接执行) + "", // 6: Advanced }; auto menuWithAction = CatchEvent(mainMenu, [&](Event event) { @@ -276,8 +279,14 @@ void TuiApp::Run() { EnterParamMode(menuCommands[menuSelected]); screen_.PostEvent(Event::Custom); } else if (menuSelected == 4) { - ExecuteCommand("carvelist"); + // Monitor Dashboard + monitorDrive_ = 0; // 自动检测 + SetViewMode(ViewMode::Monitor); + focusArea_ = 1; + screen_.PostEvent(Event::Custom); } else if (menuSelected == 5) { + ExecuteCommand("carvelist"); + } else if (menuSelected == 6) { SetViewMode(ViewMode::Output); focusArea_ = 1; AppendOutput("Command mode. Type commands below."); @@ -454,6 +463,169 @@ void TuiApp::Run() { ); mainContent = vbox(paramElements) | border; + } else if (mode == ViewMode::Monitor) { + // ---- Monitor Dashboard ---- + Elements monitorElems; + monitorElems.push_back( + text(" USN Delete Monitor Dashboard ") | bold | center + ); + monitorElems.push_back(separator()); + + // 读取共享内存快照(安全方式:try-catch + 边界保护) + MonitorSharedState state = {}; + bool hasState = false; + + try { + MonitorDaemon daemon; + char activeDrive = 0; + + if (monitorDrive_ != 0) { + // 只检查已知驱动器,避免每帧扫描 24 个 Mutex + if (daemon.IsDaemonRunning(monitorDrive_)) { + activeDrive = monitorDrive_; + } + } else { + // 首次进入时扫描一次 + for (char d = 'C'; d <= 'Z'; d++) { + if (daemon.IsDaemonRunning(d)) { + activeDrive = d; + monitorDrive_ = d; + break; + } + } + if (!activeDrive) monitorDrive_ = 'C'; // 默认 C + } + + if (activeDrive && daemon.AttachSharedMemory(activeDrive)) { + hasState = daemon.ReadState(state); + daemon.DetachSharedMemory(); + } + } catch (...) { + hasState = false; + } + + if (!hasState) { + // 未运行 + monitorElems.push_back(text("")); + monitorElems.push_back( + text(" No monitor daemon is currently running.") | color(Color::Yellow) + ); + monitorElems.push_back(text("")); + monitorElems.push_back( + text(std::string(" Target drive: ") + monitorDrive_ + ":") + ); + monitorElems.push_back( + text(" Press [S] to start a monitor daemon.") + ); + monitorElems.push_back( + text(" Press [Esc] to return to main menu.") | dim + ); + } else { + // 头部信息 + SYSTEMTIME st; + FileTimeToSystemTime(&state.startTime, &st); + std::ostringstream startStr; + startStr << st.wYear << "-" + << std::setfill('0') << std::setw(2) << st.wMonth << "-" + << std::setw(2) << st.wDay << " " + << std::setw(2) << st.wHour << ":" + << std::setw(2) << st.wMinute << ":" + << std::setw(2) << st.wSecond; + + monitorElems.push_back(hbox({ + text(" Drive: ") | bold, + text(std::string(1, state.driveLetter) + ":/") | color(Color::Green), + text(" PID: ") | bold, + text(std::to_string(state.pid)), + text(" Status: ") | bold, + text(state.daemonRunning ? "Running" : "Stopped") | + color(state.daemonRunning ? Color::Green : Color::Red), + text(" AutoStart: ") | bold, + text(state.autoStartEnabled ? "ON" : "OFF") | + color(state.autoStartEnabled ? Color::Green : Color::GrayDark), + })); + monitorElems.push_back( + text(" Started: " + startStr.str() + + " Poll: " + std::to_string(state.pollIntervalMs) + "ms") | dim + ); + monitorElems.push_back(separator()); + + // 统计面板 + monitorElems.push_back(text(" Statistics ") | bold); + monitorElems.push_back(hbox({ + vbox({ + hbox({text(" Events: ") | bold, text(std::to_string(state.totalEvents))}), + hbox({text(" Captured: ") | bold, text(std::to_string(state.capturedCount)) | color(Color::Green)}), + }) | size(WIDTH, EQUAL, 30), + vbox({ + hbox({text(" Missed: ") | bold, text(std::to_string(state.missedCount)) | color(Color::Red)}), + hbox({text(" Skipped: ") | bold, text(std::to_string(state.skippedCount))}), + }) | size(WIDTH, EQUAL, 30), + vbox({ + hbox({text(" Snapshots: ") | bold, text(std::to_string(state.snapshotCount)) | color(Color::Cyan)}), + }), + })); + monitorElems.push_back(separator()); + + // 最近事件表 + LONG eventCount = state.recentEventCount; + if (eventCount > 0) { + monitorElems.push_back(text(" Recent Events ") | bold); + monitorElems.push_back(hbox({ + text(" Status") | bold | size(WIDTH, EQUAL, 8), + text(" MFT#") | bold | size(WIDTH, EQUAL, 10), + text(" Size") | bold | size(WIDTH, EQUAL, 12), + text(" File Name") | bold, + })); + + int displayCount = std::min((int)eventCount, MONITOR_RECENT_EVENT_MAX); + displayCount = std::min(displayCount, 8); + LONG head = state.recentEventHead; + for (int i = 0; i < displayCount; i++) { + LONG idx = (head - 1 - i + MONITOR_RECENT_EVENT_MAX * 100) % MONITOR_RECENT_EVENT_MAX; + if (idx < 0 || idx >= MONITOR_RECENT_EVENT_MAX) continue; // 安全边界检查 + + auto& evt = state.recentEvents[idx]; + + // 强制 null 终止,防止 wcslen 越界 + evt.fileName[259] = L'\0'; + + // 文件名 wide -> narrow(安全边界) + std::string narrowName; + for (int c = 0; c < 259 && evt.fileName[c] != L'\0'; c++) { + wchar_t wc = evt.fileName[c]; + if (wc > 0 && wc < 128) narrowName += (char)wc; + else narrowName += '?'; + if (narrowName.size() >= 50) break; // 截断显示 + } + + // 大小格式化 + std::string sizeStr; + if (evt.fileSize == 0) sizeStr = "-"; + else if (evt.fileSize < 1024) sizeStr = std::to_string(evt.fileSize) + " B"; + else if (evt.fileSize < 1024 * 1024) sizeStr = std::to_string(evt.fileSize / 1024) + " KB"; + else sizeStr = std::to_string(evt.fileSize / (1024 * 1024)) + " MB"; + + monitorElems.push_back(hbox({ + text(evt.captured ? " [OK] " : " [MISS]") | + color(evt.captured ? Color::Green : Color::Red) | + size(WIDTH, EQUAL, 8), + text(" " + std::to_string(evt.mftRecord)) | size(WIDTH, EQUAL, 10), + text(" " + sizeStr) | size(WIDTH, EQUAL, 12), + text(" " + narrowName), + })); + } + } else { + monitorElems.push_back(text(" No events captured yet.") | dim); + } + } + + monitorElems.push_back(separator()); + monitorElems.push_back( + text(" [S] Start [T] Stop [A] Toggle AutoStart [Esc] Back") | dim + ); + mainContent = vbox(monitorElems) | border; + } else { // ---- 命令输出 ---- auto lines = GetOutputLines(); @@ -641,6 +813,59 @@ void TuiApp::Run() { } } + // Monitor Dashboard 键盘快捷键 + if (currentView_.load() == ViewMode::Monitor && !commandRunning_) { + if (event == Event::Character('s') || event == Event::Character('S')) { + // 启动守护进程 + char drive = monitorDrive_ != 0 ? monitorDrive_ : 'C'; + MonitorDaemon d; + if (d.IsDaemonRunning(drive)) { + AppendLog("[INFO] Monitor daemon already running on " + std::string(1, drive) + ":"); + } else { + AppendLog("[INFO] Starting monitor daemon on " + std::string(1, drive) + ":..."); + if (d.StartDaemon(drive)) { + monitorDrive_ = drive; + AppendLog("[OK] Monitor daemon started (PID: " + std::to_string(d.GetDaemonPID(drive)) + ")"); + } else { + AppendLog("[ERROR] Failed to start monitor daemon"); + } + } + screen_.PostEvent(Event::Custom); + return true; + } + if (event == Event::Character('t') || event == Event::Character('T')) { + // 停止守护进程 + if (monitorDrive_ != 0) { + MonitorDaemon d; + if (d.IsDaemonRunning(monitorDrive_)) { + AppendLog("[INFO] Stopping monitor daemon on " + std::string(1, monitorDrive_) + ":..."); + if (d.StopDaemon(monitorDrive_)) { + AppendLog("[OK] Monitor daemon stopped"); + } else { + AppendLog("[ERROR] Failed to stop monitor daemon"); + } + } else { + AppendLog("[INFO] No daemon running on " + std::string(1, monitorDrive_) + ":"); + } + screen_.PostEvent(Event::Custom); + return true; + } + } + if (event == Event::Character('a') || event == Event::Character('A')) { + // 切换自启动 + char drive = monitorDrive_ != 0 ? monitorDrive_ : 'C'; + if (MonitorDaemon::IsAutoStartInstalled(drive)) { + MonitorDaemon::UninstallAutoStart(drive); + AppendLog("[OK] AutoStart disabled for " + std::string(1, drive) + ":"); + } else { + MonitorDaemon::InstallAutoStart(drive); + AppendLog("[OK] AutoStart enabled for " + std::string(1, drive) + ":"); + } + screen_.PostEvent(Event::Custom); + return true; + } + } + return false; }); @@ -650,7 +875,7 @@ void TuiApp::Run() { std::thread refreshThread([this]() { while (running_) { std::this_thread::sleep_for(std::chrono::milliseconds(200)); - if (commandRunning_) { + if (commandRunning_ || currentView_.load() == ViewMode::Monitor) { screen_.PostEvent(Event::Custom); } } diff --git a/Filerestore_CLI/src/tui/TuiApp.h b/Filerestore_CLI/src/tui/TuiApp.h index b22c2c7..93f3061 100644 --- a/Filerestore_CLI/src/tui/TuiApp.h +++ b/Filerestore_CLI/src/tui/TuiApp.h @@ -33,6 +33,7 @@ class TuiApp { ParamInput, // 参数填充表单 Scan, // 扫描进度(Phase 3) Results, // 结果表格(Phase 4) + Monitor, // 实时监控面板 }; void SetViewMode(ViewMode mode); @@ -78,4 +79,7 @@ class TuiApp { // 进入参数填充模式 void EnterParamMode(const std::string& cmdName); + + // Monitor Dashboard 当前驱动器 + char monitorDrive_ = 0; }; From a7403436900c722b94fa1b017d78d40eb8d7a199 Mon Sep 17 00:00:00 2001 From: Orange20000922 <2140523341@qq.com> Date: Thu, 19 Feb 2026 21:15:15 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E6=9B=B4=E6=96=B0README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 386 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 300 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 87364b3..8d71693 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Filerestore_CLI - NTFS 文件恢复工具 -[![Version](https://img.shields.io/badge/version-v0.3.2-blue.svg)](https://github.com/Orange20000922/Filerestore_CLI/releases) +[![Version](https://img.shields.io/badge/version-v1.0.0-blue.svg)](https://github.com/Orange20000922/Filerestore_CLI/releases) [![Platform](https://img.shields.io/badge/platform-Windows-lightgrey.svg)](https://www.microsoft.com/windows) [![Language](https://img.shields.io/badge/language-C%2B%2B20-orange.svg)](https://isocpp.org/) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) @@ -8,7 +8,7 @@ **简体中文** | [English](#english-documentation) -> NTFS 文件恢复工具,支持 MFT 扫描、签名搜索恢复、ML 文件分类、TUI 界面和多线程优化 +> NTFS 文件恢复工具,支持 MFT/USN 联合恢复、实时删除监控、签名搜索、ML 文件分类、TUI 界面、内核驱动桥接和多线程优化 --- @@ -21,53 +21,114 @@ --- -## 最新更新 (2026-02-07) +## 最新更新 (2026-02-19) -### v0.3.2 - TUI 界面与测试框架 +### v1.0.0 - USN 精准恢复、实时删除监控与内核驱动桥接 -#### 🎨 新增:TUI 现代化界面 -- **Terminal UI**:基于 FTXUI 的现代化终端界面 -- **三区域布局**:菜单导航 | 命令输入 | 状态面板 -- **交互式参数填充**:自动表单生成,可视化参数输入 -- **实时进度显示**:集成原有进度条,统一渲染 -- **智能恢复向导**:Smart Recovery (USN + Signature 联合扫描) +本次为大版本更新,新增 USN 精准恢复体系、实时删除监控守护进程、MFT 快照存储、内核驱动桥接(实验性),并全面优化 TUI 界面的进度显示和交互体验。 -```bash -# 启动 TUI 模式 -Filerestore_CLI.exe --tui +--- -# 传统 CLI 模式 -Filerestore_CLI.exe -``` +#### 1. USN 精准恢复体系 + +新增三个核心命令,基于 USN 变更日志实现精准的文件恢复: -**TUI 特性**: -- 📁 **快速菜单**:Smart Recovery, Scan Deleted, Deep Scan, Repair -- ⌨️ **命令模式**:支持所有 CLI 命令,Tab 自动补全,历史记录 -- 📊 **状态面板**:实时显示驱动器、MFT、USN、缓存状态 -- 🔄 **进度条**:无缝集成,显示扫描速度和 ETA +| 命令 | 功能 | +|------|------| +| `usnlist ` | 列出最近删除的文件,结合 MFT 验证和置信度评分 | +| `usnrecover ` | 按索引/文件名/MFT 记录号恢复文件 | +| `recover [filename] [output]` | 智能恢复向导(USN + MFT + 签名联合扫描) | -#### 🧪 新增:单元测试框架 -- **Google Test 集成**:45 个单元测试覆盖核心功能 -- **CLI 参数测试**(26个):命令解析、参数验证、边界条件 -- **SIMD 签名匹配测试**(19个):SSE2/AVX2 优化验证 -- **自动化测试脚本**:`build_and_test.ps1` 一键测试 -- **CI/CD 集成**:GitHub Actions 自动运行测试 +- **USN + MFT 交叉验证**:通过序列号比对确认文件数据未被覆盖 +- **签名回退**:当 MFT 记录被覆盖时,自动使用签名扫描搜索文件内容 +- **三重验证**:USN 元数据 + MFT 数据运行 + 文件签名,给出综合置信度评分 +- **批量操作**:支持按大小、扩展名过滤,批量恢复已删除文件 ```bash -# 运行单元测试 -cd Filerestore_CLI_Tests -.\build_and_test.ps1 +# 列出最近删除的文件 +usnlist C + +# 按索引恢复 +usnrecover C 3 D:\recovered\ + +# 智能恢复(交互式向导) +recover C myfile.docx D:\recovered\ ``` -#### ⚡ 性能优化:SIMD 签名匹配 -- **SSE2/AVX2 加速**:签名匹配速度提升 50-70% -- **智能回退**:自动检测 CPU 特性,不支持时回退标量 -- **零风险优化**:完整的单元测试验证正确性 +--- + +#### 2. MFT 快照存储 + +新增 `MFTSnapshotStore` 模块,在文件删除的瞬间捕获完整的 MFT 元数据快照: + +- **删除时快照**:在 MFT 记录被覆盖前保存文件名、大小、数据运行、时间戳等完整元数据 +- **持久化存储**:快照序列化到磁盘,重启后不丢失 +- **多维查询**:支持按 MFT 记录号、序列号、文件名模式查找 +- **自动过期清理**:可配置时间阈值,自动清理过期快照 +- **线程安全**:全部操作支持多线程并发访问 + +--- + +#### 3. USN 删除监控 + +新增 `UsnDeleteMonitor` 后台监控模块,实时轮询 USN 变更日志: + +- **实时监控**:持续监听文件删除事件,立即触发 MFT 快照 +- **事件回调**:支持注册自定义回调函数,响应删除事件 +- **定时保存**:可配置自动保存间隔,防止数据丢失 +- **一次性扫描**:支持扫描已有 USN 记录,补全历史删除信息 + +--- + +#### 4. 监控守护进程 + +新增 `MonitorDaemon` 守护进程管理器: + +- **共享内存 IPC**:通过命名共享内存与 CLI/TUI 通信,查询守护进程状态 +- **事件环形缓冲**:记录最近删除事件,供 CLI/TUI 查询 +- **Windows 自启动**:支持注册为开机自启动,实现 7x24 监控 +- **状态查询**:PID、事件计数、最近事件列表 + +--- + +#### 5. 内核驱动桥接(实验性) + +新增 `KernelBridgeClient`,可选连接 FileRestoreMon minifilter 驱动: + +- **内核级通知**:通过 `FilterConnectCommunicationPort` 接收内核级删除通知 +- **LCN 映射**:直接获取文件的物理簇位置,跳过 MFT 解析 +- **默认禁用**:需启用 `ENABLE_KERNEL_BRIDGE` 预处理宏 +- **独立分支**:驱动源码位于 `feature/kernel-driver` 分支 + +> 内核驱动源码(`Filerestore_sys/`)位于 `feature/kernel-driver` 分支,基于 Windows minifilter 框架开发,拦截 IRP_MJ_CREATE 的删除操作并通过通信端口转发给用户态。 + +--- + +#### 6. TUI 全面增强 + +- **多视图模式**:欢迎页、命令输出、参数表单、扫描进度、结果表格五种视图 +- **交互式参数填充**:自动生成参数表单,可视化输入 +- **FileCarver 进度同步**:6 个扫描函数(Signature Scan / Async Scan / ThreadPool Scan / ML Enhancement / ML Scan / Hybrid ML Scan)的进度全部同步到 TUI 进度条 +- **命令历史与自动补全**:支持 Tab 补全和上下键历史浏览 + +--- + +#### 7. MFT 缓存 v2 -#### 🔧 新增:自动化测试支持 -- **--cmd 选项**:非交互式命令执行,支持 CI/CD -- **退出码支持**:成功返回 0,失败返回 1 -- **日志系统增强**:性能指标、缓存命中率自动记录 +- **序列号字段**:新增 `sequenceNumber` 用于删除验证 +- **LCN 映射**:签名扫描结果与 MFT 记录关联 +- **全局单例**:`MFTCacheManager` 跨命令共享缓存 +- **有效期检查**:按缓存时间自动失效 + +--- + +#### 8. 其他改进 + +- **MFTParser**:新增 `EnrichWithMFT()` 方法,从 MFT 补全文件大小、时间戳等信息 +- **UsnTargetedRecovery**:新增大小/扩展名静态过滤器,支持批量操作 +- **MFTReader**:优化簇读取性能 +- **代码重构**:移除废弃的 `cmd.cpp`,统一命令注册架构 +- **多项 Bug 修复** --- @@ -86,21 +147,33 @@ Filerestore_CLI.exe --tui - Browse Results: 浏览历史扫描结果 ``` -### 2. MFT 文件恢复 +### 2. USN 精准恢复 (v1.0.0+) +```bash +usnlist C # 列出最近删除文件 +usnrecover C 3 D:\recovered\ # 按索引恢复 +recover C important.docx D:\recovered\ # 智能恢复向导 +``` + +### 3. 实时删除监控 (v1.0.0+) +- 后台守护进程监听 USN 删除事件 +- 文件删除瞬间自动捕获 MFT 快照 +- 支持内核驱动桥接获取 LCN 映射(实验性) + +### 4. MFT 文件恢复 ```bash listdeleted C # 列出已删除文件 searchdeleted C doc .docx # 搜索文件 restorebyrecord C 12345 D:\out.docx # 恢复文件 ``` -### 3. 签名搜索恢复 (File Carving) +### 5. 签名搜索恢复 (File Carving) ```bash carve C zip D:\recovered\ # 异步扫描ZIP文件 carvepool C jpg,png D:\recovered\ # 线程池扫描图片 carvepool D all D:\recovered\ 8 # 指定8线程扫描所有类型 ``` -### 4. 混合扫描模式 (v0.3.0+) +### 6. 混合扫描模式 (v0.3.0+) ```bash # 自动选择最佳方式:有签名用签名,无签名用 ML carvepool C all D:\recovered\ @@ -246,7 +319,13 @@ msbuild Filerestore_CLI.vcxproj /p:Configuration=Release /p:Platform=x64 | `listdeleted ` | 列出已删除文件 | | `searchdeleted ` | 搜索文件 | | `restorebyrecord ` | 恢复文件 | -| `recover [filename] [output]` | 智能恢复 | +| `recover [filename] [output]` | 智能恢复向导(USN + MFT + 签名) | + +### USN 恢复 (v1.0.0+) +| 命令 | 说明 | +|------|------| +| `usnlist ` | 列出最近删除文件(含置信度评分) | +| `usnrecover ` | 按索引/文件名/记录号恢复 | ### 签名搜索 | 命令 | 说明 | @@ -274,7 +353,14 @@ msbuild Filerestore_CLI.vcxproj /p:Configuration=Release /p:Platform=x64 Filerestore_CLI/ ├── src/ │ ├── tui/ # TUI 界面 (v0.3.2+) -│ ├── fileRestore/ # 文件恢复(SIMD 优化) +│ ├── commands/ # 命令实现 +│ │ └── UsnRecoverCommands.cpp # USN 恢复命令 (v1.0.0+) +│ ├── fileRestore/ # 文件恢复核心 +│ │ ├── MFTSnapshotStore.* # MFT 快照存储 (v1.0.0+) +│ │ ├── UsnDeleteMonitor.* # USN 删除监控 (v1.0.0+) +│ │ ├── MonitorDaemon.* # 监控守护进程 (v1.0.0+) +│ │ ├── KernelBridgeClient.* # 内核驱动桥接 (v1.0.0+) +│ │ └── ... # MFT/签名/ML 模块 │ └── ... ├── Filerestore_CLI_Tests/ # 单元测试 (v0.3.2+) │ ├── tests/ # 45 个测试 @@ -283,12 +369,34 @@ Filerestore_CLI/ │ ├── ftxui/ # FTXUI(手动克隆) │ └── onnxruntime/ # ONNX(可选) └── document/ # 技术文档 + +# 内核驱动(独立分支 feature/kernel-driver) +Filerestore_sys/ +├── Filerestore_sys.sln +└── Filerestore_sys/ + ├── driver.c # 驱动入口 + ├── filter.c # Minifilter 回调 + ├── communication.c # 用户态通信 + └── Filerestore_sys.inf # 驱动安装信息 ``` --- ## 更新日志 +### v1.0.0 (2026-02-19) +- **新增** USN 精准恢复体系(`usnlist`、`usnrecover`、`recover` 命令) +- **新增** MFT 快照存储,删除瞬间捕获完整元数据 +- **新增** USN 删除监控后台守护进程 +- **新增** 监控守护进程管理器(共享内存 IPC、Windows 自启动) +- **新增** 内核驱动桥接客户端(实验性,minifilter 通信) +- **新增** FileCarver 全部扫描函数 TUI 进度同步 +- **改进** MFT 缓存 v2(序列号验证、全局单例、有效期检查) +- **改进** TUI 多视图模式(参数表单、扫描进度、结果表格) +- **改进** UsnTargetedRecovery 批量操作和 MFT 富化 +- **重构** 统一命令注册架构,移除废弃 cmd.cpp +- **修复** 多项已知问题 + ### v0.3.2 (2026-02-07) - **新增** TUI 现代化界面(FTXUI) - **新增** Google Test 单元测试(45 个) @@ -336,13 +444,13 @@ Filerestore_CLI/ # Filerestore_CLI - NTFS File Recovery Tool -[![Version](https://img.shields.io/badge/version-v0.3.2-blue.svg)](https://github.com/Orange20000922/Filerestore_CLI/releases) +[![Version](https://img.shields.io/badge/version-v1.0.0-blue.svg)](https://github.com/Orange20000922/Filerestore_CLI/releases) [![Platform](https://img.shields.io/badge/platform-Windows-lightgrey.svg)](https://www.microsoft.com/windows) [![Language](https://img.shields.io/badge/language-C%2B%2B20-orange.svg)](https://isocpp.org/) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![Build Status](https://img.shields.io/github/actions/workflow/status/Orange20000922/Filerestore_CLI/msbuild.yml?branch=master)](https://github.com/Orange20000922/Filerestore_CLI/actions) -> NTFS file recovery tool with MFT scanning, signature-based carving, ML file classification, TUI interface, and multi-threading optimization +> NTFS file recovery tool with MFT/USN joint recovery, real-time deletion monitoring, signature-based carving, ML file classification, TUI interface, kernel driver bridge, and multi-threading optimization --- @@ -355,53 +463,112 @@ Filerestore_CLI/ --- -## Latest Update (2026-02-07) +## Latest Update (2026-02-19) -### v0.3.2 - TUI Interface & Testing Framework +### v1.0.0 - USN Targeted Recovery, Real-time Deletion Monitoring & Kernel Driver Bridge -#### 🎨 New: Modern TUI Interface -- **Terminal UI**: Modern terminal interface based on FTXUI -- **Three-Area Layout**: Menu navigation | Command input | Status panel -- **Interactive Parameter Forms**: Auto-generated forms with visual parameter input -- **Real-time Progress**: Integrated progress bar with unified rendering -- **Smart Recovery Wizard**: USN + Signature combined scanning +Major version update with USN-based targeted recovery, real-time deletion monitoring daemon, MFT snapshot storage, kernel driver bridge (experimental), and comprehensive TUI progress integration. -```bash -# Launch TUI mode -Filerestore_CLI.exe --tui +--- -# Traditional CLI mode -Filerestore_CLI.exe -``` +#### 1. USN Targeted Recovery System + +Three new core commands based on USN change journal for precise file recovery: -**TUI Features**: -- 📁 **Quick Menu**: Smart Recovery, Scan Deleted, Deep Scan, Repair -- ⌨️ **Command Mode**: All CLI commands supported, Tab autocomplete, command history -- 📊 **Status Panel**: Real-time display of drive, MFT, USN, cache status -- 🔄 **Progress Bar**: Seamlessly integrated, shows scan speed and ETA +| Command | Function | +|---------|----------| +| `usnlist ` | List recently deleted files with MFT validation and confidence scoring | +| `usnrecover ` | Recover by index, filename, or MFT record number | +| `recover [filename] [output]` | Smart recovery wizard (USN + MFT + signature joint scan) | -#### 🧪 New: Unit Testing Framework -- **Google Test Integration**: 45 unit tests covering core functionality -- **CLI Parameter Tests** (26): Command parsing, argument validation, edge cases -- **SIMD Signature Tests** (19): SSE2/AVX2 optimization verification -- **Automated Test Scripts**: One-click testing with `build_and_test.ps1` -- **CI/CD Integration**: Automatic test execution via GitHub Actions +- **USN + MFT Cross-Validation**: Verify file data is not overwritten via sequence number comparison +- **Signature Fallback**: Auto-fallback to signature scanning when MFT records are overwritten +- **Triple Validation**: USN metadata + MFT data runs + file signature, with composite confidence scoring +- **Batch Operations**: Filter by size/extension, batch recover deleted files ```bash -# Run unit tests -cd Filerestore_CLI_Tests -.\build_and_test.ps1 +# List recently deleted files +usnlist C + +# Recover by index +usnrecover C 3 D:\recovered\ + +# Smart recovery (interactive wizard) +recover C myfile.docx D:\recovered\ ``` -#### ⚡ Performance: SIMD Signature Matching -- **SSE2/AVX2 Acceleration**: 50-70% faster signature matching -- **Smart Fallback**: Auto-detect CPU features, fallback to scalar when unsupported -- **Zero-Risk Optimization**: Comprehensive unit tests verify correctness +--- + +#### 2. MFT Snapshot Store + +Captures complete MFT metadata snapshots at the moment of file deletion: + +- **Deletion-time Snapshots**: Save filename, size, data runs, timestamps before MFT records get overwritten +- **Persistent Storage**: Serialized to disk, survives restarts +- **Multi-dimensional Query**: Lookup by MFT record number, sequence number, or filename pattern +- **Auto-expiry Cleanup**: Configurable time threshold for automatic cleanup +- **Thread-safe**: All operations support concurrent access + +--- + +#### 3. USN Delete Monitor + +Background monitor polling the USN change journal in real-time: + +- **Real-time Monitoring**: Continuously listen for file deletion events, trigger MFT snapshots immediately +- **Event Callbacks**: Register custom callbacks for deletion events +- **Periodic Auto-save**: Configurable save interval to prevent data loss +- **One-time Scan**: Scan existing USN records to fill in historical deletion info + +--- + +#### 4. Monitor Daemon + +Daemon process manager with shared memory IPC: + +- **Shared Memory IPC**: Named shared memory for CLI/TUI communication and status queries +- **Event Ring Buffer**: Record recent deletion events for CLI/TUI query +- **Windows Auto-start**: Register as startup service for 24/7 monitoring +- **Status Query**: PID, event counts, recent event list + +--- + +#### 5. Kernel Driver Bridge (Experimental) + +Optional connection to the FileRestoreMon minifilter driver: + +- **Kernel-level Notifications**: Receive delete notifications via `FilterConnectCommunicationPort` +- **LCN Mapping**: Direct physical cluster location, bypassing MFT parsing +- **Disabled by Default**: Requires `ENABLE_KERNEL_BRIDGE` preprocessor macro +- **Separate Branch**: Driver source code on `feature/kernel-driver` branch + +--- + +#### 6. TUI Enhancements + +- **Multi-view Modes**: Welcome, Output, Parameter Form, Scan Progress, Results Table +- **Interactive Parameter Forms**: Auto-generated forms with visual input +- **FileCarver Progress Sync**: All 6 scan functions now forward progress to TUI +- **Command History & Autocomplete**: Tab completion and arrow-key history browsing + +--- + +#### 7. MFT Cache v2 + +- **Sequence Number Field**: Added for delete validation +- **LCN Mapping**: Correlate signature scan results with MFT records +- **Global Singleton**: `MFTCacheManager` shared across commands +- **Validity Check**: Auto-expire by cache age -#### 🔧 New: Automation Support -- **--cmd Option**: Non-interactive command execution for CI/CD -- **Exit Codes**: Returns 0 on success, 1 on failure -- **Enhanced Logging**: Performance metrics and cache hit rate auto-logging +--- + +#### 8. Other Improvements + +- **MFTParser**: New `EnrichWithMFT()` for filling file size/timestamp info +- **UsnTargetedRecovery**: Static filters for size/extension, batch operations +- **MFTReader**: Optimized cluster read performance +- **Code Refactoring**: Removed deprecated `cmd.cpp`, unified command registration +- **Multiple Bug Fixes** --- @@ -420,21 +587,33 @@ Filerestore_CLI.exe --tui - Browse Results: Browse historical scan results ``` -### 2. MFT File Recovery +### 2. USN Targeted Recovery (v1.0.0+) +```bash +usnlist C # List recently deleted files +usnrecover C 3 D:\recovered\ # Recover by index +recover C important.docx D:\recovered\ # Smart recovery wizard +``` + +### 3. Real-time Deletion Monitoring (v1.0.0+) +- Background daemon monitors USN deletion events +- Auto-capture MFT snapshots at the moment of file deletion +- Optional kernel driver bridge for LCN mapping (experimental) + +### 4. MFT File Recovery ```bash listdeleted C # List deleted files searchdeleted C doc .docx # Search files restorebyrecord C 12345 D:\out.docx # Restore file ``` -### 3. Signature-Based Carving +### 5. Signature-Based Carving ```bash carve C zip D:\recovered\ # Async scan ZIP files carvepool C jpg,png D:\recovered\ # Thread pool scan images carvepool D all D:\recovered\ 8 # Specify 8 threads scan all types ``` -### 4. Hybrid Scanning (v0.3.0+) +### 6. Hybrid Scanning (v0.3.0+) ```bash # Auto-select best method: signature if available, ML otherwise carvepool C all D:\recovered\ @@ -580,7 +759,13 @@ msbuild Filerestore_CLI.vcxproj /p:Configuration=Release /p:Platform=x64 | `listdeleted ` | List deleted files | | `searchdeleted ` | Search files | | `restorebyrecord ` | Restore file | -| `recover [filename] [output]` | Smart recovery | +| `recover [filename] [output]` | Smart recovery wizard (USN + MFT + signature) | + +### USN Recovery (v1.0.0+) +| Command | Description | +|---------|-------------| +| `usnlist ` | List recently deleted files (with confidence scoring) | +| `usnrecover ` | Recover by index/filename/record number | ### Signature Carving | Command | Description | @@ -608,7 +793,14 @@ msbuild Filerestore_CLI.vcxproj /p:Configuration=Release /p:Platform=x64 Filerestore_CLI/ ├── src/ │ ├── tui/ # TUI interface (v0.3.2+) -│ ├── fileRestore/ # File recovery (SIMD optimized) +│ ├── commands/ # Command implementations +│ │ └── UsnRecoverCommands.cpp # USN recovery commands (v1.0.0+) +│ ├── fileRestore/ # Core file recovery +│ │ ├── MFTSnapshotStore.* # MFT snapshot storage (v1.0.0+) +│ │ ├── UsnDeleteMonitor.* # USN delete monitor (v1.0.0+) +│ │ ├── MonitorDaemon.* # Monitor daemon (v1.0.0+) +│ │ ├── KernelBridgeClient.* # Kernel driver bridge (v1.0.0+) +│ │ └── ... # MFT/signature/ML modules │ └── ... ├── Filerestore_CLI_Tests/ # Unit tests (v0.3.2+) │ ├── tests/ # 45 tests @@ -617,12 +809,34 @@ Filerestore_CLI/ │ ├── ftxui/ # FTXUI (manual clone) │ └── onnxruntime/ # ONNX (optional) └── document/ # Technical documentation + +# Kernel driver (separate branch: feature/kernel-driver) +Filerestore_sys/ +├── Filerestore_sys.sln +└── Filerestore_sys/ + ├── driver.c # Driver entry + ├── filter.c # Minifilter callbacks + ├── communication.c # User-mode communication + └── Filerestore_sys.inf # Driver installation info ``` --- ## Changelog +### v1.0.0 (2026-02-19) +- **Added** USN targeted recovery system (`usnlist`, `usnrecover`, `recover` commands) +- **Added** MFT snapshot storage, capturing complete metadata at deletion time +- **Added** USN delete monitor background daemon +- **Added** Monitor daemon manager (shared memory IPC, Windows auto-start) +- **Added** Kernel driver bridge client (experimental, minifilter communication) +- **Added** FileCarver progress sync to TUI for all scan functions +- **Improved** MFT cache v2 (sequence number validation, global singleton, expiry check) +- **Improved** TUI multi-view modes (parameter forms, scan progress, results table) +- **Improved** UsnTargetedRecovery batch operations and MFT enrichment +- **Refactored** Unified command registration, removed deprecated cmd.cpp +- **Fixed** Multiple known issues + ### v0.3.2 (2026-02-07) - **Added** Modern TUI interface (FTXUI) - **Added** Google Test unit testing (45 tests) From de969d252300068bf02c9f191b5032bb7c0fcc7b Mon Sep 17 00:00:00 2001 From: Orange20000922 <2140523341@qq.com> Date: Fri, 20 Feb 2026 00:01:01 +0800 Subject: [PATCH 4/7] 111 --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 709f6c0..9b92e92 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,8 @@ x86/ *.VC.db-wal *.VC.opendb +#development notes +dev_notes/ # NuGet packages (can be restored via nuget restore) packages/ *.nupkg From f86934f408ffa60fc9c1a2e11c59f40814ada6bc Mon Sep 17 00:00:00 2001 From: Orange20000922 <2140523341@qq.com> Date: Fri, 20 Feb 2026 15:35:18 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=86=85=E6=A0=B8?= =?UTF-8?q?=E5=B1=82=E4=BB=A3=E7=A0=81=E7=9A=84=E5=AE=89=E5=85=A8=E6=80=A7?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Filerestore_sys/Filerestore_sys.sln | 35 ----------------------------- 1 file changed, 35 deletions(-) delete mode 100644 Filerestore_sys/Filerestore_sys.sln diff --git a/Filerestore_sys/Filerestore_sys.sln b/Filerestore_sys/Filerestore_sys.sln deleted file mode 100644 index b08bbc5..0000000 --- a/Filerestore_sys/Filerestore_sys.sln +++ /dev/null @@ -1,35 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.14.36811.4 d17.14 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Filerestore_sys", "Filerestore_sys\Filerestore_sys.vcxproj", "{4E97BA7A-9203-3699-D57F-5596CAD720A0}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|ARM64 = Debug|ARM64 - Debug|x64 = Debug|x64 - Release|ARM64 = Release|ARM64 - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Debug|ARM64.Build.0 = Debug|ARM64 - {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Debug|x64.ActiveCfg = Debug|x64 - {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Debug|x64.Build.0 = Debug|x64 - {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Debug|x64.Deploy.0 = Debug|x64 - {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Release|ARM64.ActiveCfg = Release|ARM64 - {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Release|ARM64.Build.0 = Release|ARM64 - {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Release|ARM64.Deploy.0 = Release|ARM64 - {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Release|x64.ActiveCfg = Release|x64 - {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Release|x64.Build.0 = Release|x64 - {4E97BA7A-9203-3699-D57F-5596CAD720A0}.Release|x64.Deploy.0 = Release|x64 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {C73C08B3-4928-41BE-980A-DC28CB001DD0} - EndGlobalSection -EndGlobal From f5a040c9653bb67ea2e008935aa6b657a5689bf3 Mon Sep 17 00:00:00 2001 From: Orange20000922 <2140523341@qq.com> Date: Fri, 20 Feb 2026 15:35:28 +0800 Subject: [PATCH 6/7] 111 --- Filerestore_sys/Filerestore_sys/common.h | 13 +++ .../Filerestore_sys/communication.c | 81 ++++++++++++++++++- Filerestore_sys/Filerestore_sys/driver.h | 2 +- 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/Filerestore_sys/Filerestore_sys/common.h b/Filerestore_sys/Filerestore_sys/common.h index ae3f61b..fc0cbe9 100644 --- a/Filerestore_sys/Filerestore_sys/common.h +++ b/Filerestore_sys/Filerestore_sys/common.h @@ -31,6 +31,19 @@ extern "C" { #define MAX_FILE_PATH_CHARS 520 #define DRIVER_VERSION_STRING "1.0.0" +/* Connection authentication */ +#define FILERESTORE_CONNECTION_MAGIC 0x46524D43 /* 'FRMC' */ +#define FILERESTORE_PROTOCOL_VERSION 1 +#define FILERESTORE_CLIENT_IMAGE_NAME L"\\Filerestore_CLI.exe" + +/* Connection context (user -> kernel, passed via FilterConnectCommunicationPort) */ +typedef struct _CONNECTION_CONTEXT { + ULONG Magic; /* must be FILERESTORE_CONNECTION_MAGIC */ + ULONG Version; /* must be FILERESTORE_PROTOCOL_VERSION */ + ULONG ProcessId; /* caller PID (kernel can cross-validate) */ + ULONG Reserved; /* alignment padding */ +} CONNECTION_CONTEXT, *PCONNECTION_CONTEXT; + /* ===== Structures ===== */ /* LCN (Logical Cluster Number) entry for file extent mapping */ diff --git a/Filerestore_sys/Filerestore_sys/communication.c b/Filerestore_sys/Filerestore_sys/communication.c index d9348e4..24b97f4 100644 --- a/Filerestore_sys/Filerestore_sys/communication.c +++ b/Filerestore_sys/Filerestore_sys/communication.c @@ -70,6 +70,66 @@ SetupCommunicationPort(VOID) return status; } +/* ===== VerifyCallerImage ===== */ + +/* ZwQueryInformationProcess is not declared in standard WDK headers */ +NTSYSAPI NTSTATUS NTAPI ZwQueryInformationProcess( + _In_ HANDLE ProcessHandle, + _In_ PROCESSINFOCLASS ProcessInformationClass, + _Out_writes_bytes_(ProcessInformationLength) PVOID ProcessInformation, + _In_ ULONG ProcessInformationLength, + _Out_opt_ PULONG ReturnLength + ); + +/* + * VerifyCallerImage - Verify the calling process image name matches the + * expected client executable. Uses ZwQueryInformationProcess(ProcessImageFileName) + * to get the NT path, then checks if the path suffix matches + * FILERESTORE_CLIENT_IMAGE_NAME. + */ +static NTSTATUS VerifyCallerImage(VOID) +{ + NTSTATUS status; + UCHAR buffer[512]; + ULONG returnedLength; + PUNICODE_STRING imagePath; + UNICODE_STRING expectedSuffix; + UNICODE_STRING actualSuffix; + USHORT suffixBytes; + + status = ZwQueryInformationProcess( + NtCurrentProcess(), + ProcessImageFileName, /* = 27 */ + buffer, + sizeof(buffer), + &returnedLength + ); + if (!NT_SUCCESS(status)) { + return status; + } + + imagePath = (PUNICODE_STRING)buffer; + RtlInitUnicodeString(&expectedSuffix, FILERESTORE_CLIENT_IMAGE_NAME); + + suffixBytes = expectedSuffix.Length; + if (imagePath->Length < suffixBytes) { + return STATUS_ACCESS_DENIED; + } + + /* Extract the suffix of the image path matching the expected length */ + actualSuffix.Buffer = (PWCH)((PUCHAR)imagePath->Buffer + + imagePath->Length - suffixBytes); + actualSuffix.Length = suffixBytes; + actualSuffix.MaximumLength = suffixBytes; + + /* Case-insensitive comparison */ + if (RtlCompareUnicodeString(&actualSuffix, &expectedSuffix, TRUE) != 0) { + return STATUS_ACCESS_DENIED; + } + + return STATUS_SUCCESS; +} + /* ===== ConnectNotifyCallback ===== */ static NTSTATUS @@ -81,9 +141,26 @@ ConnectNotifyCallback( _Outptr_result_maybenull_ PVOID *ConnectionCookie ) { + PCONNECTION_CONTEXT ctx; + UNREFERENCED_PARAMETER(ServerPortCookie); - UNREFERENCED_PARAMETER(ConnectionContext); - UNREFERENCED_PARAMETER(SizeOfContext); + + /* 1. Validate connection context (handshake) */ + if (ConnectionContext == NULL || + SizeOfContext < sizeof(CONNECTION_CONTEXT)) { + return STATUS_ACCESS_DENIED; + } + + ctx = (PCONNECTION_CONTEXT)ConnectionContext; + if (ctx->Magic != FILERESTORE_CONNECTION_MAGIC || + ctx->Version != FILERESTORE_PROTOCOL_VERSION) { + return STATUS_ACCESS_DENIED; + } + + /* 2. Verify caller process image name */ + if (!NT_SUCCESS(VerifyCallerImage())) { + return STATUS_ACCESS_DENIED; + } g_Context.ClientPort = ClientPort; *ConnectionCookie = NULL; diff --git a/Filerestore_sys/Filerestore_sys/driver.h b/Filerestore_sys/Filerestore_sys/driver.h index c3f9f93..af4611e 100644 --- a/Filerestore_sys/Filerestore_sys/driver.h +++ b/Filerestore_sys/Filerestore_sys/driver.h @@ -8,7 +8,7 @@ #include #include #include "common.h" - +#include /* Pool tag: 'MsFR' (stored little-endian as 'RFsM') */ #define POOL_TAG 'RFsM' From bbff118f271008bfd707a28578ab6a3b3f06eef0 Mon Sep 17 00:00:00 2001 From: Orange20000922 <2140523341@qq.com> Date: Fri, 20 Feb 2026 15:42:15 +0800 Subject: [PATCH 7/7] 111 --- .claude/settings.local.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2ad811c..672c19a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,8 @@ "Bash(python -c:*)", "WebSearch", "Bash(python:*)", - "Bash(du:*)" + "Bash(du:*)", + "Bash(git -C \"D:\\\\Users\\\\21405\\\\source\\\\repos\\\\Filerestore_CLI\" log --oneline -3 master)" ] } }