July 16, 2026
Direct $MFT Parsing
Welcome to this new Medium post. In this one, we will talk about direct $MFT parsing, a technique to enumerate every file on an NTFS volume…

By S12 - 0x12Dark Development
19 min read
Welcome to this new Medium post. In this one, we will talk about direct $MFT parsing, a technique to enumerate every file on an NTFS volume without calling Windows's API like FindFirstFile or NtQueryDirectoryFile
When you use the standard Windows functions to list files, your calls go through many layers: the Win32 API, the NT layer, the I/O Manager, and the filesystem driver. Each of these layers is a place where security tools can intercept and monitor what you do. Direct $MFT parsing skips all of them. We read the raw volume like a disk, parse the NTFS structures ourselves, and get the full file list without touching any monitored path
This technique is also more powerful than the standard approach. We can enumerate deleted files, read file metadata that the API hides, discover alternate data streams, and build complete file paths, all from a single raw read of the $MFT. Let's see how it works
Courses: Learn how offensive development works on Windows OS from beginner to advanced taking our courses, all explained in C++.
All Courses Learn how real Windows offensive development works
Technique Database: Access 70+ real offensive techniques with weekly updates, complete with code, PoCs, and AV scan results:
Malware Techniques Database Explore an ever-growing collection of techniques
Modules: Dive deep into essential offensive topics with our modular text-training program! Get a new module every 14 days. Start at just $1.99 per module, or unlock lifetime access to all modules for $100.
0x12 Dark Development Learn the best offensive techniques for Windows OS, with content ranging from beginner to advanced levels. All…
Methodology
To enumerate every file on an NTFS volume by parsing the $MFT directly, we need to follow these logical steps:
- Open the Raw Volume: First, we need to open a handle to the volume itself, not to any folder or file inside it. We use
CreateFileWwith a path like\\.\C:to get direct access to the disk. This handle lets us read raw bytes from the volume - Get NTFS Metadata: Once we have the volume handle, we need to know where the $MFT starts on disk and how large each record is. We call
DeviceIoControlwithFSCTL_GET_NTFS_VOLUME_DATAto get this information. Without this, we do not know where to start reading - Read MFT Records in Chunks: Now we seek to the $MFT offset and start reading. We read many records at once in a buffer to avoid slow one by one reads. Each record is fixed size, usually 1024 bytes.
- Validate and Fix Each Record: Each MFT record has a signature (
FILE) and an update sequence that NTFS uses internally to protect sector boundaries. Before parsing, we must verify the signature and apply a fixup, restoring the real bytes that NTFS replaced. - Parse Attributes: Every MFT record contains a chain of attributes. We walk this chain and extract the ones we care about:
$STANDARD_INFORMATIONfor timestamps and flags,$FILE_NAMEfor the file name and parent reference, and$DATAfor size and data runs - Build Full Paths: The
$FILE_NAMEattribute does not give us a full path, it only gives us the name and the FRN (File Reference Number) of the parent directory. We build a map of every FRN to its name and parent, then walk up the chain until we reach the root to reconstruct the full path likeC:\Users\victim\secret.txt
Raw Volume Handle
│
▼
FSCTL_GET_NTFS_VOLUME_DATA ──► MFT offset + record size
│
▼
Read Chunk of MFT Records
│
▼
For each record:
├── Validate Signature (FILE)
├── Apply USN Fixup
└── Walk Attribute Chain
├── $STANDARD_INFORMATION → timestamps, flags
├── $FILE_NAME → name, parent FRN
└── $DATA → size, data runs / resident data
│
▼
Build FRN Map
│
▼
Resolve Full Paths → Results[]Raw Volume Handle
│
▼
FSCTL_GET_NTFS_VOLUME_DATA ──► MFT offset + record size
│
▼
Read Chunk of MFT Records
│
▼
For each record:
├── Validate Signature (FILE)
├── Apply USN Fixup
└── Walk Attribute Chain
├── $STANDARD_INFORMATION → timestamps, flags
├── $FILE_NAME → name, parent FRN
└── $DATA → size, data runs / resident data
│
▼
Build FRN Map
│
▼
Resolve Full Paths → Results[]Implementation
Now, let's look at how to translate that logic into C++ code. I have broken down the most important parts
Opening the Volume and Getting NTFS Metadata
The first thing we do is open the volume with CreateFileW. We pass GENERIC_READ and share mode FILE_SHARE_READ | FILE_SHARE_WRITE so we do not block other processes. The flag FILE_FLAG_NO_BUFFERING is important here, it tells Windows not to use the file cache, so we read directly from disk
HANDLE hVol = CreateFileW(L"\\\\.\\C:", GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, NULL);HANDLE hVol = CreateFileW(L"\\\\.\\C:", GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, NULL);Then we call DeviceIoControl to ask NTFS for its internal metadata. The result gives us MftStartLcn (the cluster where the MFT starts), BytesPerCluster, BytesPerFileRecordSegment (almost always 1024), and BytesPerSector
NTFS_VOLUME_DATA_BUFFER nvd = {};
DWORD got = 0;
DeviceIoControl(hVol, FSCTL_GET_NTFS_VOLUME_DATA,
NULL, 0, &nvd, sizeof(nvd), &got, NULL);
LONGLONG mftOffset = nvd.MftStartLcn.QuadPart * nvd.BytesPerCluster;
DWORD recSize = nvd.BytesPerFileRecordSegment;
DWORD sectSize = nvd.BytesPerSector;
DWORD64 totalRecs = nvd.MftValidDataLength.QuadPart / recSize;NTFS_VOLUME_DATA_BUFFER nvd = {};
DWORD got = 0;
DeviceIoControl(hVol, FSCTL_GET_NTFS_VOLUME_DATA,
NULL, 0, &nvd, sizeof(nvd), &got, NULL);
LONGLONG mftOffset = nvd.MftStartLcn.QuadPart * nvd.BytesPerCluster;
DWORD recSize = nvd.BytesPerFileRecordSegment;
DWORD sectSize = nvd.BytesPerSector;
DWORD64 totalRecs = nvd.MftValidDataLength.QuadPart / recSize;Reading Records in Chunks
Reading one record at a time is very slow. Instead, we allocate a buffer for 128 records and read them in one ReadFile call. We must also align the read size to the sector size, this is required when using FILE_FLAG_NO_BUFFERING
constexpr DWORD RECS_PER_CHUNK = 128;
DWORD chunkSize = recSize * RECS_PER_CHUNK;
BYTE* buf = (BYTE*)VirtualAlloc(NULL, chunkSize,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// Align to sector size before reading
DWORD toRead = AlignUp(recsLeft * recSize, sectSize);
ReadFile(hVol, buf, toRead, &bytesRead, NULL);constexpr DWORD RECS_PER_CHUNK = 128;
DWORD chunkSize = recSize * RECS_PER_CHUNK;
BYTE* buf = (BYTE*)VirtualAlloc(NULL, chunkSize,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// Align to sector size before reading
DWORD toRead = AlignUp(recsLeft * recSize, sectSize);
ReadFile(hVol, buf, toRead, &bytesRead, NULL);Applying the USN Fixup
NTFS protects each sector inside an MFT record by replacing the last 2 bytes of each sector with a special value called the Update Sequence Number (USN). Before we can read the record correctly, we must put the original bytes back
WORD* usa = (WORD*)(rec + hdr->UpdateSeqOffset);
WORD usn = usa[0]; // the USN value we must find at each sector tail
WORD nSec = hdr->UpdateSeqCount - 1;
for (WORD s = 0; s < nSec; ++s) {
DWORD tail = (s + 1) * sectSize - sizeof(WORD);
WORD* slot = (WORD*)(rec + tail);
if (*slot != usn) return false; // record is inconsistent
*slot = usa[s + 1]; // restore original bytes
}WORD* usa = (WORD*)(rec + hdr->UpdateSeqOffset);
WORD usn = usa[0]; // the USN value we must find at each sector tail
WORD nSec = hdr->UpdateSeqCount - 1;
for (WORD s = 0; s < nSec; ++s) {
DWORD tail = (s + 1) * sectSize - sizeof(WORD);
WORD* slot = (WORD*)(rec + tail);
if (*slot != usn) return false; // record is inconsistent
*slot = usa[s + 1]; // restore original bytes
}Parsing the $FILE_NAME Attribute
After fixup, we walk the attribute chain. Each attribute starts with a header that tells us its type code and length. We move forward by RecordLength at each step until we hit the end marker 0xFFFFFFFF
For $FILE_NAME (type 0x30), we cast the value bytes to our NTFS_FILE_NAME struct and extract the name and parent FRN
const auto* fn = (const NTFS_FILE_NAME*)(val);
std::wstring name(
(const WCHAR*)(fn + 1), // name bytes follow the struct
fn->FileNameLength
);
DWORDLONG parentFrn = fn->ParentFrn & 0x0000FFFFFFFFFFFFULL; // strip sequence numberconst auto* fn = (const NTFS_FILE_NAME*)(val);
std::wstring name(
(const WCHAR*)(fn + 1), // name bytes follow the struct
fn->FileNameLength
);
DWORDLONG parentFrn = fn->ParentFrn & 0x0000FFFFFFFFFFFFULL; // strip sequence numberResolving Full Paths
The FRN is a unique 48-bit number that identifies a file record. The parent FRN tells us which directory owns this file. After we process all records, we have a map of every FRN to its name and parent FRN. To build the full path, we just walk up the map until we reach FRN 5, that is always the root directory on NTFS
while (cur != MFT_ROOT_FRN) {
auto it = map.find(cur);
if (it == map.end()) break;
parts.push_back(&it->second.name);
cur = it->second.parentFrn & MFT_FRN_MASK;
}
// Reverse the parts and join with backslashwhile (cur != MFT_ROOT_FRN) {
auto it = map.find(cur);
if (it == map.end()) break;
parts.push_back(&it->second.name);
cur = it->second.parentFrn & MFT_FRN_MASK;
}
// Reverse the parts and join with backslashCode
#pragma once
#define NOMINMAX
#include <Windows.h>
#undef min
#undef max
#include <algorithm>
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <string>
#include <cstring>
#pragma pack(push, 1)
struct MFT_FILE_RECORD_HEADER {
DWORD Signature;
WORD UpdateSeqOffset;
WORD UpdateSeqCount;
ULONGLONG Lsn;
WORD SequenceNumber;
WORD HardLinkCount;
WORD FirstAttrOffset;
WORD Flags;
DWORD BytesInUse;
DWORD BytesAllocated;
ULONGLONG BaseFileRecord;
WORD NextAttrInstance;
WORD Reserved;
DWORD RecordNumber;
};
struct NTFS_ATTR_HEADER {
DWORD TypeCode;
DWORD RecordLength;
BYTE FormCode;
BYTE NameLength;
WORD NameOffset;
WORD Flags;
WORD Instance;
};
struct NTFS_ATTR_RESIDENT {
NTFS_ATTR_HEADER Common;
DWORD ValueLength;
WORD ValueOffset;
BYTE IndexedFlag;
BYTE Reserved;
};
struct NTFS_ATTR_NON_RESIDENT {
NTFS_ATTR_HEADER Common;
ULONGLONG StartingVcn;
ULONGLONG LastVcn;
WORD DataRunsOffset;
WORD CompressionUnitSize;
DWORD Padding;
ULONGLONG AllocatedSize;
ULONGLONG FileSize;
ULONGLONG InitializedSize;
// ULONGLONG CompressedSize; only present if CompressionUnitSize != 0
};
struct NTFS_FILE_NAME {
ULONGLONG ParentFrn;
ULONGLONG CreationTime;
ULONGLONG LastWriteTime;
ULONGLONG MftChangeTime;
ULONGLONG LastAccessTime;
ULONGLONG AllocatedSize;
ULONGLONG EndOfFile;
DWORD FileAttributes;
DWORD ReparseOrEaInfo;
BYTE FileNameLength;
BYTE FileNameNamespace; // 0=POSIX 1=Win32 2=DOS 3=Win32&DOS
};
struct NTFS_STANDARD_INFORMATION {
ULONGLONG CreationTime;
ULONGLONG LastModificationTime;
ULONGLONG MftModificationTime;
ULONGLONG LastAccessTime;
DWORD FileAttributes;
DWORD MaximumVersions;
DWORD VersionNumber;
DWORD ClassId;
// v3 extension, 72 bytes total (NTFS 3.0+, Windows 2000+)
DWORD OwnerId;
DWORD SecurityId;
ULONGLONG QuotaCharged;
ULONGLONG Usn;
};
struct NTFS_REPARSE_BUFFER_HEADER {
DWORD ReparseTag;
WORD ReparseDataLength;
WORD Reserved;
// reparse data follows
};
struct NTFS_SYMLINK_REPARSE_BUFFER {
WORD SubstituteNameOffset;
WORD SubstituteNameLength;
WORD PrintNameOffset;
WORD PrintNameLength;
DWORD Flags; // 1 = relative symlink
// WCHAR PathBuffer[] follows
};
struct NTFS_MOUNT_POINT_REPARSE_BUFFER {
WORD SubstituteNameOffset;
WORD SubstituteNameLength;
WORD PrintNameOffset;
WORD PrintNameLength;
// WCHAR PathBuffer[] follows
};
struct NTFS_EA_INFORMATION {
WORD PackedEaSize;
WORD NeedEaCount;
DWORD UnpackedEaSize;
};
struct FILE_FULL_EA_INFORMATION {
DWORD NextEntryOffset;
BYTE Flags;
BYTE EaNameLength;
WORD EaValueLength;
// char EaName[EaNameLength + 1]
// BYTE EaValue[EaValueLength]
};
struct NTFS_ATTR_LIST_ENTRY {
DWORD TypeCode;
WORD RecordLength;
BYTE AttributeNameLength;
BYTE AttributeNameOffset;
ULONGLONG StartingVcn;
ULONGLONG MftReference;
WORD AttributeInstance;
// WCHAR AttributeName[AttributeNameLength] follows at AttributeNameOffset
};
struct NTFS_SECURITY_DESCRIPTOR_HDR {
BYTE Revision;
BYTE Sbz1;
WORD Control; // SE_DACL_PRESENT(0x0004), SE_SACL_PRESENT(0x0010), etc.
DWORD OffsetOwner; // byte offset from SD start; 0 = not present
DWORD OffsetGroup;
DWORD OffsetSacl;
DWORD OffsetDacl;
};
static_assert(sizeof(NTFS_FILE_NAME) == 66, "pack check");
static_assert(sizeof(NTFS_STANDARD_INFORMATION) == 72, "pack check");
static_assert(sizeof(NTFS_ATTR_NON_RESIDENT) == 64, "pack check");
static_assert(sizeof(NTFS_ATTR_LIST_ENTRY) == 26, "pack check");
static_assert(sizeof(NTFS_SECURITY_DESCRIPTOR_HDR) == 20, "pack check");
#pragma pack(pop)
static constexpr DWORD MFT_RECORD_SIGNATURE = 0x454C4946UL;
static constexpr DWORD MFT_ATTR_STANDARD_INFO = 0x10UL;
static constexpr DWORD MFT_ATTR_ATTRIBUTE_LIST = 0x20UL;
static constexpr DWORD MFT_ATTR_FILE_NAME = 0x30UL;
static constexpr DWORD MFT_ATTR_OBJECT_ID = 0x40UL;
static constexpr DWORD MFT_ATTR_SECURITY_DESC = 0x50UL;
static constexpr DWORD MFT_ATTR_DATA = 0x80UL;
static constexpr DWORD MFT_ATTR_REPARSE_POINT = 0xC0UL;
static constexpr DWORD MFT_ATTR_EA_INFORMATION = 0xD0UL;
static constexpr DWORD MFT_ATTR_EA = 0xE0UL;
static constexpr DWORD MFT_ATTR_END = 0xFFFFFFFFUL;
static constexpr WORD MFT_FLAG_IN_USE = 0x0001;
static constexpr WORD MFT_FLAG_DIRECTORY = 0x0002;
static constexpr BYTE MFT_NS_POSIX = 0;
static constexpr BYTE MFT_NS_WIN32 = 1;
static constexpr BYTE MFT_NS_DOS = 2;
static constexpr BYTE MFT_NS_WIN32_DOS = 3;
static constexpr DWORDLONG MFT_FRN_MASK = 0x0000FFFFFFFFFFFFULL;
static constexpr DWORDLONG MFT_ROOT_FRN = 5ULL;
struct MftEntry {
std::wstring name;
DWORDLONG parentFrn;
bool isDirectory;
};
static inline DWORDLONG MftStripSeq(DWORDLONG frn) { return frn & MFT_FRN_MASK; }
// ─── MftSnapshot ─────────────────────────────────────────────────────────────
class MftSnapshot {
public:
struct FileInfo {
MFT_FILE_RECORD_HEADER RecordHeader;
// $STANDARD_INFORMATION (0x10)
struct StandardInformation {
ULONGLONG CreationTime;
ULONGLONG LastModificationTime;
ULONGLONG MftModificationTime;
ULONGLONG LastAccessTime;
DWORD FileAttributes;
DWORD MaximumVersions;
DWORD VersionNumber;
DWORD ClassId;
DWORD OwnerId;
DWORD SecurityId;
ULONGLONG QuotaCharged;
ULONGLONG Usn;
bool IsV3;
};
StandardInformation SI;
bool HasSI = false;
// $FILE_NAME (0x30) — one entry per namespace
struct FileNameAttr {
DWORDLONG ParentFrn;
ULONGLONG CreationTime;
ULONGLONG LastWriteTime;
ULONGLONG MftChangeTime;
ULONGLONG LastAccessTime;
ULONGLONG AllocatedSize;
ULONGLONG EndOfFile;
DWORD FileAttributes;
DWORD ReparseOrEaInfo;
BYTE Namespace;
std::wstring Name;
};
std::vector<FileNameAttr> FileNames;
// $DATA (0x80) — one entry per stream (main + ADS)
struct DataRun {
LONGLONG Vcn;
LONGLONG Lcn; // -1 = sparse
LONGLONG Length;
};
struct DataStream {
std::wstring Name;
ULONGLONG AllocatedSize;
ULONGLONG FileSize;
ULONGLONG InitializedSize;
bool IsResident;
std::vector<BYTE> ResidentData;
std::vector<DataRun> Runs;
};
std::vector<DataStream> DataStreams;
// $OBJECT_ID (0x40)
struct ObjectIdentifier {
GUID ObjectId;
GUID BirthVolumeId;
GUID BirthObjectId;
GUID DomainId;
};
ObjectIdentifier ObjId;
bool HasObjId = false;
// $REPARSE_POINT (0xC0)
struct ReparseData {
DWORD Tag;
std::wstring SubstituteName;
std::wstring PrintName;
std::vector<BYTE> RawBuffer;
};
ReparseData Reparse;
bool HasReparse = false;
// $EA_INFORMATION (0xD0)
struct EaInformation {
WORD PackedEaSize;
WORD NeedEaCount;
DWORD UnpackedEaSize;
};
EaInformation EaInfo;
bool HasEaInfo = false;
// $EA (0xE0)
struct ExtendedAttribute {
BYTE Flags;
std::string Name;
std::vector<BYTE> Value;
};
std::vector<ExtendedAttribute> EAs;
// $ATTRIBUTE_LIST (0x20)
struct AttributeListEntry {
DWORD TypeCode;
DWORDLONG MftReference;
ULONGLONG StartingVcn;
std::wstring Name;
};
std::vector<AttributeListEntry> AttributeList;
bool HasAttributeList = false;
// $SECURITY_DESCRIPTOR (0x50)
struct SecurityDescriptorAttr {
WORD Control;
std::vector<BYTE> OwnerSid;
std::vector<BYTE> GroupSid;
std::vector<BYTE> Dacl;
std::vector<BYTE> Sacl;
};
SecurityDescriptorAttr SecDesc;
bool HasSecDesc = false;
// Derived
std::wstring FullPath;
bool IsDirectory = false;
bool IsDeleted = false;
};
// Returns one FileInfo per MFT base record.
static std::vector<FileInfo> GetSnapshotViaMFT(const wchar_t* volumePath = L"\\\\.\\C:", const wchar_t* volumePrefix = L"C:", bool includeDeleted = false){
std::vector<FileInfo> results;
HANDLE hVol = CreateFileW(volumePath, GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, NULL);
if (hVol == INVALID_HANDLE_VALUE) {
std::wcerr << L"[-] CreateFileW: " << GetLastError() << L"\n";
return results;
}
NTFS_VOLUME_DATA_BUFFER nvd = {};
DWORD got = 0;
if (!DeviceIoControl(hVol, FSCTL_GET_NTFS_VOLUME_DATA,
NULL, 0, &nvd, sizeof(nvd), &got, NULL)) {
std::cerr << "[-] FSCTL_GET_NTFS_VOLUME_DATA: " << GetLastError() << "\n";
CloseHandle(hVol); return results;
}
const DWORD recSize = nvd.BytesPerFileRecordSegment;
const DWORD sectSize = nvd.BytesPerSector;
const LONGLONG mftOffset = nvd.MftStartLcn.QuadPart * nvd.BytesPerCluster;
const DWORD64 totalRecs = static_cast<DWORD64>(nvd.MftValidDataLength.QuadPart / recSize);
if (recSize == 0 || recSize > 65536 || (recSize & (recSize - 1)) != 0) {
std::cerr << "[-] Bad BytesPerFileRecordSegment: " << recSize << "\n";
CloseHandle(hVol); return results;
}
std::wcout
<< L"[*] MFT @ 0x" << std::hex << mftOffset << std::dec
<< L" records: " << totalRecs
<< L" (" << nvd.MftValidDataLength.QuadPart / 1024 / 1024 << L" MB)\n";
LARGE_INTEGER li; li.QuadPart = mftOffset;
if (!SetFilePointerEx(hVol, li, NULL, FILE_BEGIN)) {
std::cerr << "[-] SetFilePointerEx: " << GetLastError() << "\n";
CloseHandle(hVol); return results;
}
constexpr DWORD RECS_PER_CHUNK = 128;
const DWORD chunkSize = recSize * RECS_PER_CHUNK;
BYTE* buf = static_cast<BYTE*>(
VirtualAlloc(NULL, chunkSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
if (!buf) {
std::cerr << "[-] VirtualAlloc\n";
CloseHandle(hVol); return results;
}
results.reserve(static_cast<size_t>(totalRecs / 3));
std::unordered_map<DWORDLONG, MftEntry> frnMap;
frnMap.reserve(static_cast<size_t>(totalRecs));
DWORD64 recIdx = 0, nRead = 0;
while (recIdx < totalRecs) {
DWORD64 recsLeft = totalRecs - recIdx;
DWORD toRead = (recsLeft >= RECS_PER_CHUNK)
? chunkSize
: static_cast<DWORD>(recsLeft * recSize);
toRead = AlignUp(toRead, sectSize);
DWORD bytesRead = 0;
if (!ReadFile(hVol, buf, toRead, &bytesRead, NULL) || bytesRead == 0) break;
DWORD recsInBuf = bytesRead / recSize;
for (DWORD r = 0; r < recsInBuf; ++r) {
if (recIdx + r >= totalRecs) break;
FileInfo fi{};
if (!ProcessRecord(buf + r * recSize, recSize, sectSize, includeDeleted, fi))
continue;
// Build lightweight frnMap for FullPath resolution
const FileInfo::FileNameAttr* best = BestFileName(fi);
if (best) {
frnMap[fi.RecordHeader.RecordNumber] = MftEntry{
best->Name, best->ParentFrn, fi.IsDirectory
};
}
results.push_back(std::move(fi));
}
recIdx += recsInBuf;
nRead += recsInBuf;
if (nRead % 50'000 == 0 || recIdx >= totalRecs)
std::cout << "\r[*] " << recIdx << " / " << totalRecs
<< " live: " << results.size() << " " << std::flush;
}
std::cout << "\n";
VirtualFree(buf, 0, MEM_RELEASE);
CloseHandle(hVol);
for (auto& fi : results)
fi.FullPath = BuildPath(frnMap, fi.RecordHeader.RecordNumber, volumePrefix);
std::wcout << L"[+] Done: " << results.size() << L" entries\n";
return results;
}
private:
static DWORD AlignUp(DWORD v, DWORD a) { return (v + a - 1) & ~(a - 1); }
// Restore the two bytes that NTFS replaced with the USN at each sector tail.
static bool ApplyFixup(BYTE* rec, DWORD recSize, DWORD sectSize) {
auto* hdr = reinterpret_cast<MFT_FILE_RECORD_HEADER*>(rec);
if (hdr->Signature != MFT_RECORD_SIGNATURE) return false;
WORD* usa = reinterpret_cast<WORD*>(rec + hdr->UpdateSeqOffset);
WORD usn = usa[0];
WORD nSec = static_cast<WORD>(hdr->UpdateSeqCount - 1);
for (WORD s = 0; s < nSec; ++s) {
DWORD tail = static_cast<DWORD>(s + 1) * sectSize - sizeof(WORD);
if (tail + sizeof(WORD) > recSize) break;
WORD* slot = reinterpret_cast<WORD*>(rec + tail);
if (*slot != usn) return false;
*slot = usa[s + 1];
}
return true;
}
static const BYTE* ResidentVal(const BYTE* cur, const BYTE* attrEnd, DWORD& outLen) {
const auto* hdr = reinterpret_cast<const NTFS_ATTR_HEADER*>(cur);
if (hdr->FormCode != 0) return nullptr;
const auto* res = reinterpret_cast<const NTFS_ATTR_RESIDENT*>(cur);
const BYTE* val = cur + res->ValueOffset;
if (val + res->ValueLength > attrEnd) return nullptr;
outLen = res->ValueLength;
return val;
}
static bool ProcessRecord(BYTE* rec, DWORD recSize, DWORD sectSize,
bool includeDeleted, FileInfo& fi) {
auto* hdr = reinterpret_cast<MFT_FILE_RECORD_HEADER*>(rec);
// These fields are before any sector tail (offset < 510), safe to read before fixup
if (hdr->Signature != MFT_RECORD_SIGNATURE) return false;
fi.IsDeleted = !(hdr->Flags & MFT_FLAG_IN_USE);
if (fi.IsDeleted && !includeDeleted) return false;
if (hdr->BaseFileRecord != 0) return false; // extension record, no $FILE_NAME here
if (!ApplyFixup(rec, recSize, sectSize)) return false;
fi.RecordHeader = *hdr;
fi.IsDirectory = (hdr->Flags & MFT_FLAG_DIRECTORY) != 0;
if (hdr->FirstAttrOffset >= recSize) return false;
const BYTE* cur = rec + hdr->FirstAttrOffset;
const BYTE* end = rec + std::min(hdr->BytesInUse, recSize);
while (cur + sizeof(NTFS_ATTR_HEADER) <= end) {
const auto* attr = reinterpret_cast<const NTFS_ATTR_HEADER*>(cur);
if (attr->TypeCode == MFT_ATTR_END) break;
if (attr->RecordLength < sizeof(NTFS_ATTR_HEADER)) break;
if (cur + attr->RecordLength > end) break;
const BYTE* attrEnd = cur + attr->RecordLength;
switch (attr->TypeCode) {
case MFT_ATTR_STANDARD_INFO: ParseSI(cur, attrEnd, fi); break;
case MFT_ATTR_FILE_NAME: ParseFN(cur, attrEnd, fi); break;
case MFT_ATTR_DATA: ParseData(cur, attrEnd, fi); break;
case MFT_ATTR_OBJECT_ID: ParseObjId(cur, attrEnd, fi); break;
case MFT_ATTR_REPARSE_POINT: ParseReparse(cur, attrEnd, fi); break;
case MFT_ATTR_EA_INFORMATION: ParseEaInfo(cur, attrEnd, fi); break;
case MFT_ATTR_EA: ParseEA(cur, attrEnd, fi); break;
case MFT_ATTR_ATTRIBUTE_LIST: ParseAttrList(cur, attrEnd, fi); break;
case MFT_ATTR_SECURITY_DESC: ParseSecDesc(cur, attrEnd, fi); break;
}
cur += attr->RecordLength;
}
return !fi.FileNames.empty(); // base records always have at least one $FILE_NAME
}
static void ParseSI(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val || len < 48) return;
const auto* si = reinterpret_cast<const NTFS_STANDARD_INFORMATION*>(val);
fi.SI.CreationTime = si->CreationTime;
fi.SI.LastModificationTime = si->LastModificationTime;
fi.SI.MftModificationTime = si->MftModificationTime;
fi.SI.LastAccessTime = si->LastAccessTime;
fi.SI.FileAttributes = si->FileAttributes;
fi.SI.MaximumVersions = si->MaximumVersions;
fi.SI.VersionNumber = si->VersionNumber;
fi.SI.ClassId = si->ClassId;
fi.SI.IsV3 = (len >= 72);
if (fi.SI.IsV3) {
fi.SI.OwnerId = si->OwnerId;
fi.SI.SecurityId = si->SecurityId;
fi.SI.QuotaCharged = si->QuotaCharged;
fi.SI.Usn = si->Usn;
}
fi.HasSI = true;
}
static void ParseFN(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val || len < sizeof(NTFS_FILE_NAME)) return;
const auto* fn = reinterpret_cast<const NTFS_FILE_NAME*>(val);
const BYTE* fnEnd = val + sizeof(NTFS_FILE_NAME) + fn->FileNameLength * sizeof(WCHAR);
if (fnEnd > val + len) return;
if (NamespacePriority(fn->FileNameNamespace) < 0) return; // skip DOS
FileInfo::FileNameAttr e;
e.ParentFrn = fn->ParentFrn;
e.CreationTime = fn->CreationTime;
e.LastWriteTime = fn->LastWriteTime;
e.MftChangeTime = fn->MftChangeTime;
e.LastAccessTime = fn->LastAccessTime;
e.AllocatedSize = fn->AllocatedSize;
e.EndOfFile = fn->EndOfFile;
e.FileAttributes = fn->FileAttributes;
e.ReparseOrEaInfo = fn->ReparseOrEaInfo;
e.Namespace = fn->FileNameNamespace;
e.Name = std::wstring(reinterpret_cast<const WCHAR*>(fn + 1),
fn->FileNameLength);
fi.FileNames.push_back(std::move(e));
}
static void ParseData(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
const auto* attr = reinterpret_cast<const NTFS_ATTR_HEADER*>(cur);
FileInfo::DataStream s;
// Stream name: empty = main $DATA, nonempty = ADS
if (attr->NameLength > 0) {
const BYTE* nameBytes = cur + attr->NameOffset;
if (nameBytes + attr->NameLength * sizeof(WCHAR) <= attrEnd)
s.Name = std::wstring(reinterpret_cast<const WCHAR*>(nameBytes),
attr->NameLength);
}
if (attr->FormCode == 0) {
s.IsResident = true;
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (val) {
s.AllocatedSize = s.FileSize = s.InitializedSize = len;
s.ResidentData.assign(val, val + len);
}
}
else {
s.IsResident = false;
if (cur + sizeof(NTFS_ATTR_NON_RESIDENT) <= attrEnd) {
const auto* nr = reinterpret_cast<const NTFS_ATTR_NON_RESIDENT*>(cur);
s.AllocatedSize = nr->AllocatedSize;
s.FileSize = nr->FileSize;
s.InitializedSize = nr->InitializedSize;
const BYTE* runs = cur + nr->DataRunsOffset;
if (runs < attrEnd)
s.Runs = ParseDataRuns(runs, static_cast<size_t>(attrEnd - runs));
}
}
fi.DataStreams.push_back(std::move(s));
}
static void ParseObjId(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val || len < sizeof(GUID)) return;
memcpy(&fi.ObjId.ObjectId, val, sizeof(GUID));
if (len >= 4 * sizeof(GUID)) {
memcpy(&fi.ObjId.BirthVolumeId, val + 16, sizeof(GUID));
memcpy(&fi.ObjId.BirthObjectId, val + 32, sizeof(GUID));
memcpy(&fi.ObjId.DomainId, val + 48, sizeof(GUID));
}
fi.HasObjId = true;
}
static void ParseReparse(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
const auto* attr = reinterpret_cast<const NTFS_ATTR_HEADER*>(cur);
if (attr->FormCode != 0) { fi.HasReparse = true; return; } // nonresident: tag unknown
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val || len < sizeof(NTFS_REPARSE_BUFFER_HEADER)) return;
const auto* rbh = reinterpret_cast<const NTFS_REPARSE_BUFFER_HEADER*>(val);
fi.Reparse.Tag = rbh->ReparseTag;
const BYTE* data = val + sizeof(NTFS_REPARSE_BUFFER_HEADER);
const BYTE* dataEnd = std::min(data + rbh->ReparseDataLength, val + len);
fi.Reparse.RawBuffer.assign(data, dataEnd);
auto readPath = [&](const BYTE* pathBuf, WORD off, WORD byteLen) -> std::wstring {
if (pathBuf + off + byteLen > dataEnd) return {};
return std::wstring(reinterpret_cast<const WCHAR*>(pathBuf + off),
byteLen / sizeof(WCHAR));
};
if (rbh->ReparseTag == IO_REPARSE_TAG_SYMLINK &&
data + sizeof(NTFS_SYMLINK_REPARSE_BUFFER) <= dataEnd) {
const auto* sb = reinterpret_cast<const NTFS_SYMLINK_REPARSE_BUFFER*>(data);
const BYTE* pathBuf = data + sizeof(NTFS_SYMLINK_REPARSE_BUFFER);
fi.Reparse.SubstituteName = readPath(pathBuf, sb->SubstituteNameOffset, sb->SubstituteNameLength);
fi.Reparse.PrintName = readPath(pathBuf, sb->PrintNameOffset, sb->PrintNameLength);
}
else if (rbh->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT &&
data + sizeof(NTFS_MOUNT_POINT_REPARSE_BUFFER) <= dataEnd) {
const auto* mb = reinterpret_cast<const NTFS_MOUNT_POINT_REPARSE_BUFFER*>(data);
const BYTE* pathBuf = data + sizeof(NTFS_MOUNT_POINT_REPARSE_BUFFER);
fi.Reparse.SubstituteName = readPath(pathBuf, mb->SubstituteNameOffset, mb->SubstituteNameLength);
fi.Reparse.PrintName = readPath(pathBuf, mb->PrintNameOffset, mb->PrintNameLength);
}
fi.HasReparse = true;
}
static void ParseEaInfo(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val || len < sizeof(NTFS_EA_INFORMATION)) return;
const auto* ea = reinterpret_cast<const NTFS_EA_INFORMATION*>(val);
fi.EaInfo.PackedEaSize = ea->PackedEaSize;
fi.EaInfo.NeedEaCount = ea->NeedEaCount;
fi.EaInfo.UnpackedEaSize = ea->UnpackedEaSize;
fi.HasEaInfo = true;
}
static void ParseEA(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val) return;
const BYTE* ptr = val;
const BYTE* valEnd = val + len;
while (ptr + sizeof(FILE_FULL_EA_INFORMATION) <= valEnd) {
const auto* ea = reinterpret_cast<const FILE_FULL_EA_INFORMATION*>(ptr);
const BYTE* namePtr = ptr + sizeof(FILE_FULL_EA_INFORMATION);
const BYTE* valuePtr = namePtr + ea->EaNameLength + 1;
const BYTE* entryEnd = valuePtr + ea->EaValueLength;
if (entryEnd <= valEnd) {
FileInfo::ExtendedAttribute e;
e.Flags = ea->Flags;
e.Name = std::string(reinterpret_cast<const char*>(namePtr), ea->EaNameLength);
e.Value.assign(valuePtr, entryEnd);
fi.EAs.push_back(std::move(e));
}
if (ea->NextEntryOffset == 0) break;
ptr += ea->NextEntryOffset;
if (ptr >= valEnd) break;
}
}
static void ParseAttrList(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
fi.HasAttributeList = true;
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val) return;
const BYTE* ptr = val;
const BYTE* valEnd = val + len;
while (ptr + sizeof(NTFS_ATTR_LIST_ENTRY) <= valEnd) {
const auto* e = reinterpret_cast<const NTFS_ATTR_LIST_ENTRY*>(ptr);
if (e->RecordLength < sizeof(NTFS_ATTR_LIST_ENTRY)) break;
FileInfo::AttributeListEntry entry;
entry.TypeCode = e->TypeCode;
entry.MftReference = MftStripSeq(e->MftReference);
entry.StartingVcn = e->StartingVcn;
if (e->AttributeNameLength > 0) {
BYTE off = e->AttributeNameOffset > 0
? e->AttributeNameOffset
: static_cast<BYTE>(sizeof(NTFS_ATTR_LIST_ENTRY));
const BYTE* nameEnd = ptr + off + e->AttributeNameLength * sizeof(WCHAR);
if (nameEnd <= valEnd)
entry.Name = std::wstring(
reinterpret_cast<const WCHAR*>(ptr + off),
e->AttributeNameLength);
}
fi.AttributeList.push_back(std::move(entry));
ptr += e->RecordLength;
}
}
static void ParseSecDesc(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val || len < sizeof(NTFS_SECURITY_DESCRIPTOR_HDR)) return;
const auto* sd = reinterpret_cast<const NTFS_SECURITY_DESCRIPTOR_HDR*>(val);
fi.SecDesc.Control = sd->Control;
// Copy a SID: total size = 8 + SubAuthorityCount * 4 (SubAuthorityCount is at byte[1])
auto copySid = [&](DWORD offset, std::vector<BYTE>& dest) {
if (offset == 0) return;
const BYTE* p = val + offset;
if (p + 8 > val + len) return;
DWORD sidSize = 8u + p[1] * 4u;
if (p + sidSize <= val + len) dest.assign(p, p + sidSize);
};
// Copy an ACL: total size stored as WORD at byte[2] of the ACL header
auto copyAcl = [&](DWORD offset, std::vector<BYTE>& dest) {
if (offset == 0) return;
const BYTE* p = val + offset;
if (p + 4 > val + len) return;
WORD aclSize = *reinterpret_cast<const WORD*>(p + 2);
if (p + aclSize <= val + len) dest.assign(p, p + aclSize);
};
copySid(sd->OffsetOwner, fi.SecDesc.OwnerSid);
copySid(sd->OffsetGroup, fi.SecDesc.GroupSid);
copyAcl(sd->OffsetSacl, fi.SecDesc.Sacl);
copyAcl(sd->OffsetDacl, fi.SecDesc.Dacl);
fi.HasSecDesc = true;
}
// Decode NTFS runlength encoded data run list
static std::vector<FileInfo::DataRun> ParseDataRuns(const BYTE* ptr, size_t maxLen) {
std::vector<FileInfo::DataRun> runs;
const BYTE* end = ptr + maxLen;
LONGLONG curLcn = 0, curVcn = 0;
while (ptr < end) {
BYTE header = *ptr++;
if (header == 0) break;
int lenField = header & 0x0F;
int lcnField = (header >> 4) & 0x0F;
if (ptr + lenField + lcnField > end) break;
LONGLONG runLen = 0;
for (int i = 0; i < lenField; i++)
runLen |= static_cast<LONGLONG>(*ptr++) << (i * 8);
LONGLONG lcnDelta = 0;
for (int i = 0; i < lcnField; i++)
lcnDelta |= static_cast<LONGLONG>(*ptr++) << (i * 8);
// Sign-extend the LCN delta
if (lcnField > 0 && (lcnDelta >> (lcnField * 8 - 1)) & 1)
lcnDelta |= ~((1LL << (lcnField * 8)) - 1);
FileInfo::DataRun r;
r.Vcn = curVcn;
r.Lcn = (lcnField == 0) ? -1LL : curLcn + lcnDelta;
r.Length = runLen;
runs.push_back(r);
if (lcnField > 0) curLcn += lcnDelta;
curVcn += runLen;
}
return runs;
}
static const FileInfo::FileNameAttr* BestFileName(const FileInfo& fi) {
const FileInfo::FileNameAttr* best = nullptr;
int bestPrio = -1;
for (const auto& fn : fi.FileNames) {
int p = NamespacePriority(fn.Namespace);
if (p > bestPrio) { bestPrio = p; best = &fn; }
}
return best;
}
static int NamespacePriority(BYTE ns) {
switch (ns) {
case MFT_NS_WIN32_DOS: return 3;
case MFT_NS_WIN32: return 2;
case MFT_NS_POSIX: return 1;
case MFT_NS_DOS: return -1;
default: return 0;
}
}
static std::wstring BuildPath(const std::unordered_map<DWORDLONG, MftEntry>& map,
DWORDLONG frn, const std::wstring& prefix) {
std::vector<const std::wstring*> parts;
DWORDLONG cur = MftStripSeq(frn);
std::unordered_set<DWORDLONG> visited;
while (cur != MFT_ROOT_FRN) {
if (!visited.insert(cur).second) { parts.push_back(nullptr); break; }
auto it = map.find(cur);
if (it == map.end()) { parts.push_back(nullptr); break; }
parts.push_back(&it->second.name);
cur = MftStripSeq(it->second.parentFrn);
}
std::wstring path = prefix;
for (int i = static_cast<int>(parts.size()) - 1; i >= 0; --i) {
path += L'\\';
path += parts[i] ? *parts[i] : L"<?>";
}
return path;
}
};
int main(){
std::vector<MftSnapshot::FileInfo> files = MftSnapshot::GetSnapshotViaMFT(L"\\\\.\\C:", L"C:", false);
if (files.empty()) {
std::cout << "Error getting files" << std::endl;
return 1;
}
else {
std::cout << "Files retrieved correctly" << std::endl;
return 0;
}
}#pragma once
#define NOMINMAX
#include <Windows.h>
#undef min
#undef max
#include <algorithm>
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <string>
#include <cstring>
#pragma pack(push, 1)
struct MFT_FILE_RECORD_HEADER {
DWORD Signature;
WORD UpdateSeqOffset;
WORD UpdateSeqCount;
ULONGLONG Lsn;
WORD SequenceNumber;
WORD HardLinkCount;
WORD FirstAttrOffset;
WORD Flags;
DWORD BytesInUse;
DWORD BytesAllocated;
ULONGLONG BaseFileRecord;
WORD NextAttrInstance;
WORD Reserved;
DWORD RecordNumber;
};
struct NTFS_ATTR_HEADER {
DWORD TypeCode;
DWORD RecordLength;
BYTE FormCode;
BYTE NameLength;
WORD NameOffset;
WORD Flags;
WORD Instance;
};
struct NTFS_ATTR_RESIDENT {
NTFS_ATTR_HEADER Common;
DWORD ValueLength;
WORD ValueOffset;
BYTE IndexedFlag;
BYTE Reserved;
};
struct NTFS_ATTR_NON_RESIDENT {
NTFS_ATTR_HEADER Common;
ULONGLONG StartingVcn;
ULONGLONG LastVcn;
WORD DataRunsOffset;
WORD CompressionUnitSize;
DWORD Padding;
ULONGLONG AllocatedSize;
ULONGLONG FileSize;
ULONGLONG InitializedSize;
// ULONGLONG CompressedSize; only present if CompressionUnitSize != 0
};
struct NTFS_FILE_NAME {
ULONGLONG ParentFrn;
ULONGLONG CreationTime;
ULONGLONG LastWriteTime;
ULONGLONG MftChangeTime;
ULONGLONG LastAccessTime;
ULONGLONG AllocatedSize;
ULONGLONG EndOfFile;
DWORD FileAttributes;
DWORD ReparseOrEaInfo;
BYTE FileNameLength;
BYTE FileNameNamespace; // 0=POSIX 1=Win32 2=DOS 3=Win32&DOS
};
struct NTFS_STANDARD_INFORMATION {
ULONGLONG CreationTime;
ULONGLONG LastModificationTime;
ULONGLONG MftModificationTime;
ULONGLONG LastAccessTime;
DWORD FileAttributes;
DWORD MaximumVersions;
DWORD VersionNumber;
DWORD ClassId;
// v3 extension, 72 bytes total (NTFS 3.0+, Windows 2000+)
DWORD OwnerId;
DWORD SecurityId;
ULONGLONG QuotaCharged;
ULONGLONG Usn;
};
struct NTFS_REPARSE_BUFFER_HEADER {
DWORD ReparseTag;
WORD ReparseDataLength;
WORD Reserved;
// reparse data follows
};
struct NTFS_SYMLINK_REPARSE_BUFFER {
WORD SubstituteNameOffset;
WORD SubstituteNameLength;
WORD PrintNameOffset;
WORD PrintNameLength;
DWORD Flags; // 1 = relative symlink
// WCHAR PathBuffer[] follows
};
struct NTFS_MOUNT_POINT_REPARSE_BUFFER {
WORD SubstituteNameOffset;
WORD SubstituteNameLength;
WORD PrintNameOffset;
WORD PrintNameLength;
// WCHAR PathBuffer[] follows
};
struct NTFS_EA_INFORMATION {
WORD PackedEaSize;
WORD NeedEaCount;
DWORD UnpackedEaSize;
};
struct FILE_FULL_EA_INFORMATION {
DWORD NextEntryOffset;
BYTE Flags;
BYTE EaNameLength;
WORD EaValueLength;
// char EaName[EaNameLength + 1]
// BYTE EaValue[EaValueLength]
};
struct NTFS_ATTR_LIST_ENTRY {
DWORD TypeCode;
WORD RecordLength;
BYTE AttributeNameLength;
BYTE AttributeNameOffset;
ULONGLONG StartingVcn;
ULONGLONG MftReference;
WORD AttributeInstance;
// WCHAR AttributeName[AttributeNameLength] follows at AttributeNameOffset
};
struct NTFS_SECURITY_DESCRIPTOR_HDR {
BYTE Revision;
BYTE Sbz1;
WORD Control; // SE_DACL_PRESENT(0x0004), SE_SACL_PRESENT(0x0010), etc.
DWORD OffsetOwner; // byte offset from SD start; 0 = not present
DWORD OffsetGroup;
DWORD OffsetSacl;
DWORD OffsetDacl;
};
static_assert(sizeof(NTFS_FILE_NAME) == 66, "pack check");
static_assert(sizeof(NTFS_STANDARD_INFORMATION) == 72, "pack check");
static_assert(sizeof(NTFS_ATTR_NON_RESIDENT) == 64, "pack check");
static_assert(sizeof(NTFS_ATTR_LIST_ENTRY) == 26, "pack check");
static_assert(sizeof(NTFS_SECURITY_DESCRIPTOR_HDR) == 20, "pack check");
#pragma pack(pop)
static constexpr DWORD MFT_RECORD_SIGNATURE = 0x454C4946UL;
static constexpr DWORD MFT_ATTR_STANDARD_INFO = 0x10UL;
static constexpr DWORD MFT_ATTR_ATTRIBUTE_LIST = 0x20UL;
static constexpr DWORD MFT_ATTR_FILE_NAME = 0x30UL;
static constexpr DWORD MFT_ATTR_OBJECT_ID = 0x40UL;
static constexpr DWORD MFT_ATTR_SECURITY_DESC = 0x50UL;
static constexpr DWORD MFT_ATTR_DATA = 0x80UL;
static constexpr DWORD MFT_ATTR_REPARSE_POINT = 0xC0UL;
static constexpr DWORD MFT_ATTR_EA_INFORMATION = 0xD0UL;
static constexpr DWORD MFT_ATTR_EA = 0xE0UL;
static constexpr DWORD MFT_ATTR_END = 0xFFFFFFFFUL;
static constexpr WORD MFT_FLAG_IN_USE = 0x0001;
static constexpr WORD MFT_FLAG_DIRECTORY = 0x0002;
static constexpr BYTE MFT_NS_POSIX = 0;
static constexpr BYTE MFT_NS_WIN32 = 1;
static constexpr BYTE MFT_NS_DOS = 2;
static constexpr BYTE MFT_NS_WIN32_DOS = 3;
static constexpr DWORDLONG MFT_FRN_MASK = 0x0000FFFFFFFFFFFFULL;
static constexpr DWORDLONG MFT_ROOT_FRN = 5ULL;
struct MftEntry {
std::wstring name;
DWORDLONG parentFrn;
bool isDirectory;
};
static inline DWORDLONG MftStripSeq(DWORDLONG frn) { return frn & MFT_FRN_MASK; }
// ─── MftSnapshot ─────────────────────────────────────────────────────────────
class MftSnapshot {
public:
struct FileInfo {
MFT_FILE_RECORD_HEADER RecordHeader;
// $STANDARD_INFORMATION (0x10)
struct StandardInformation {
ULONGLONG CreationTime;
ULONGLONG LastModificationTime;
ULONGLONG MftModificationTime;
ULONGLONG LastAccessTime;
DWORD FileAttributes;
DWORD MaximumVersions;
DWORD VersionNumber;
DWORD ClassId;
DWORD OwnerId;
DWORD SecurityId;
ULONGLONG QuotaCharged;
ULONGLONG Usn;
bool IsV3;
};
StandardInformation SI;
bool HasSI = false;
// $FILE_NAME (0x30) — one entry per namespace
struct FileNameAttr {
DWORDLONG ParentFrn;
ULONGLONG CreationTime;
ULONGLONG LastWriteTime;
ULONGLONG MftChangeTime;
ULONGLONG LastAccessTime;
ULONGLONG AllocatedSize;
ULONGLONG EndOfFile;
DWORD FileAttributes;
DWORD ReparseOrEaInfo;
BYTE Namespace;
std::wstring Name;
};
std::vector<FileNameAttr> FileNames;
// $DATA (0x80) — one entry per stream (main + ADS)
struct DataRun {
LONGLONG Vcn;
LONGLONG Lcn; // -1 = sparse
LONGLONG Length;
};
struct DataStream {
std::wstring Name;
ULONGLONG AllocatedSize;
ULONGLONG FileSize;
ULONGLONG InitializedSize;
bool IsResident;
std::vector<BYTE> ResidentData;
std::vector<DataRun> Runs;
};
std::vector<DataStream> DataStreams;
// $OBJECT_ID (0x40)
struct ObjectIdentifier {
GUID ObjectId;
GUID BirthVolumeId;
GUID BirthObjectId;
GUID DomainId;
};
ObjectIdentifier ObjId;
bool HasObjId = false;
// $REPARSE_POINT (0xC0)
struct ReparseData {
DWORD Tag;
std::wstring SubstituteName;
std::wstring PrintName;
std::vector<BYTE> RawBuffer;
};
ReparseData Reparse;
bool HasReparse = false;
// $EA_INFORMATION (0xD0)
struct EaInformation {
WORD PackedEaSize;
WORD NeedEaCount;
DWORD UnpackedEaSize;
};
EaInformation EaInfo;
bool HasEaInfo = false;
// $EA (0xE0)
struct ExtendedAttribute {
BYTE Flags;
std::string Name;
std::vector<BYTE> Value;
};
std::vector<ExtendedAttribute> EAs;
// $ATTRIBUTE_LIST (0x20)
struct AttributeListEntry {
DWORD TypeCode;
DWORDLONG MftReference;
ULONGLONG StartingVcn;
std::wstring Name;
};
std::vector<AttributeListEntry> AttributeList;
bool HasAttributeList = false;
// $SECURITY_DESCRIPTOR (0x50)
struct SecurityDescriptorAttr {
WORD Control;
std::vector<BYTE> OwnerSid;
std::vector<BYTE> GroupSid;
std::vector<BYTE> Dacl;
std::vector<BYTE> Sacl;
};
SecurityDescriptorAttr SecDesc;
bool HasSecDesc = false;
// Derived
std::wstring FullPath;
bool IsDirectory = false;
bool IsDeleted = false;
};
// Returns one FileInfo per MFT base record.
static std::vector<FileInfo> GetSnapshotViaMFT(const wchar_t* volumePath = L"\\\\.\\C:", const wchar_t* volumePrefix = L"C:", bool includeDeleted = false){
std::vector<FileInfo> results;
HANDLE hVol = CreateFileW(volumePath, GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, NULL);
if (hVol == INVALID_HANDLE_VALUE) {
std::wcerr << L"[-] CreateFileW: " << GetLastError() << L"\n";
return results;
}
NTFS_VOLUME_DATA_BUFFER nvd = {};
DWORD got = 0;
if (!DeviceIoControl(hVol, FSCTL_GET_NTFS_VOLUME_DATA,
NULL, 0, &nvd, sizeof(nvd), &got, NULL)) {
std::cerr << "[-] FSCTL_GET_NTFS_VOLUME_DATA: " << GetLastError() << "\n";
CloseHandle(hVol); return results;
}
const DWORD recSize = nvd.BytesPerFileRecordSegment;
const DWORD sectSize = nvd.BytesPerSector;
const LONGLONG mftOffset = nvd.MftStartLcn.QuadPart * nvd.BytesPerCluster;
const DWORD64 totalRecs = static_cast<DWORD64>(nvd.MftValidDataLength.QuadPart / recSize);
if (recSize == 0 || recSize > 65536 || (recSize & (recSize - 1)) != 0) {
std::cerr << "[-] Bad BytesPerFileRecordSegment: " << recSize << "\n";
CloseHandle(hVol); return results;
}
std::wcout
<< L"[*] MFT @ 0x" << std::hex << mftOffset << std::dec
<< L" records: " << totalRecs
<< L" (" << nvd.MftValidDataLength.QuadPart / 1024 / 1024 << L" MB)\n";
LARGE_INTEGER li; li.QuadPart = mftOffset;
if (!SetFilePointerEx(hVol, li, NULL, FILE_BEGIN)) {
std::cerr << "[-] SetFilePointerEx: " << GetLastError() << "\n";
CloseHandle(hVol); return results;
}
constexpr DWORD RECS_PER_CHUNK = 128;
const DWORD chunkSize = recSize * RECS_PER_CHUNK;
BYTE* buf = static_cast<BYTE*>(
VirtualAlloc(NULL, chunkSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
if (!buf) {
std::cerr << "[-] VirtualAlloc\n";
CloseHandle(hVol); return results;
}
results.reserve(static_cast<size_t>(totalRecs / 3));
std::unordered_map<DWORDLONG, MftEntry> frnMap;
frnMap.reserve(static_cast<size_t>(totalRecs));
DWORD64 recIdx = 0, nRead = 0;
while (recIdx < totalRecs) {
DWORD64 recsLeft = totalRecs - recIdx;
DWORD toRead = (recsLeft >= RECS_PER_CHUNK)
? chunkSize
: static_cast<DWORD>(recsLeft * recSize);
toRead = AlignUp(toRead, sectSize);
DWORD bytesRead = 0;
if (!ReadFile(hVol, buf, toRead, &bytesRead, NULL) || bytesRead == 0) break;
DWORD recsInBuf = bytesRead / recSize;
for (DWORD r = 0; r < recsInBuf; ++r) {
if (recIdx + r >= totalRecs) break;
FileInfo fi{};
if (!ProcessRecord(buf + r * recSize, recSize, sectSize, includeDeleted, fi))
continue;
// Build lightweight frnMap for FullPath resolution
const FileInfo::FileNameAttr* best = BestFileName(fi);
if (best) {
frnMap[fi.RecordHeader.RecordNumber] = MftEntry{
best->Name, best->ParentFrn, fi.IsDirectory
};
}
results.push_back(std::move(fi));
}
recIdx += recsInBuf;
nRead += recsInBuf;
if (nRead % 50'000 == 0 || recIdx >= totalRecs)
std::cout << "\r[*] " << recIdx << " / " << totalRecs
<< " live: " << results.size() << " " << std::flush;
}
std::cout << "\n";
VirtualFree(buf, 0, MEM_RELEASE);
CloseHandle(hVol);
for (auto& fi : results)
fi.FullPath = BuildPath(frnMap, fi.RecordHeader.RecordNumber, volumePrefix);
std::wcout << L"[+] Done: " << results.size() << L" entries\n";
return results;
}
private:
static DWORD AlignUp(DWORD v, DWORD a) { return (v + a - 1) & ~(a - 1); }
// Restore the two bytes that NTFS replaced with the USN at each sector tail.
static bool ApplyFixup(BYTE* rec, DWORD recSize, DWORD sectSize) {
auto* hdr = reinterpret_cast<MFT_FILE_RECORD_HEADER*>(rec);
if (hdr->Signature != MFT_RECORD_SIGNATURE) return false;
WORD* usa = reinterpret_cast<WORD*>(rec + hdr->UpdateSeqOffset);
WORD usn = usa[0];
WORD nSec = static_cast<WORD>(hdr->UpdateSeqCount - 1);
for (WORD s = 0; s < nSec; ++s) {
DWORD tail = static_cast<DWORD>(s + 1) * sectSize - sizeof(WORD);
if (tail + sizeof(WORD) > recSize) break;
WORD* slot = reinterpret_cast<WORD*>(rec + tail);
if (*slot != usn) return false;
*slot = usa[s + 1];
}
return true;
}
static const BYTE* ResidentVal(const BYTE* cur, const BYTE* attrEnd, DWORD& outLen) {
const auto* hdr = reinterpret_cast<const NTFS_ATTR_HEADER*>(cur);
if (hdr->FormCode != 0) return nullptr;
const auto* res = reinterpret_cast<const NTFS_ATTR_RESIDENT*>(cur);
const BYTE* val = cur + res->ValueOffset;
if (val + res->ValueLength > attrEnd) return nullptr;
outLen = res->ValueLength;
return val;
}
static bool ProcessRecord(BYTE* rec, DWORD recSize, DWORD sectSize,
bool includeDeleted, FileInfo& fi) {
auto* hdr = reinterpret_cast<MFT_FILE_RECORD_HEADER*>(rec);
// These fields are before any sector tail (offset < 510), safe to read before fixup
if (hdr->Signature != MFT_RECORD_SIGNATURE) return false;
fi.IsDeleted = !(hdr->Flags & MFT_FLAG_IN_USE);
if (fi.IsDeleted && !includeDeleted) return false;
if (hdr->BaseFileRecord != 0) return false; // extension record, no $FILE_NAME here
if (!ApplyFixup(rec, recSize, sectSize)) return false;
fi.RecordHeader = *hdr;
fi.IsDirectory = (hdr->Flags & MFT_FLAG_DIRECTORY) != 0;
if (hdr->FirstAttrOffset >= recSize) return false;
const BYTE* cur = rec + hdr->FirstAttrOffset;
const BYTE* end = rec + std::min(hdr->BytesInUse, recSize);
while (cur + sizeof(NTFS_ATTR_HEADER) <= end) {
const auto* attr = reinterpret_cast<const NTFS_ATTR_HEADER*>(cur);
if (attr->TypeCode == MFT_ATTR_END) break;
if (attr->RecordLength < sizeof(NTFS_ATTR_HEADER)) break;
if (cur + attr->RecordLength > end) break;
const BYTE* attrEnd = cur + attr->RecordLength;
switch (attr->TypeCode) {
case MFT_ATTR_STANDARD_INFO: ParseSI(cur, attrEnd, fi); break;
case MFT_ATTR_FILE_NAME: ParseFN(cur, attrEnd, fi); break;
case MFT_ATTR_DATA: ParseData(cur, attrEnd, fi); break;
case MFT_ATTR_OBJECT_ID: ParseObjId(cur, attrEnd, fi); break;
case MFT_ATTR_REPARSE_POINT: ParseReparse(cur, attrEnd, fi); break;
case MFT_ATTR_EA_INFORMATION: ParseEaInfo(cur, attrEnd, fi); break;
case MFT_ATTR_EA: ParseEA(cur, attrEnd, fi); break;
case MFT_ATTR_ATTRIBUTE_LIST: ParseAttrList(cur, attrEnd, fi); break;
case MFT_ATTR_SECURITY_DESC: ParseSecDesc(cur, attrEnd, fi); break;
}
cur += attr->RecordLength;
}
return !fi.FileNames.empty(); // base records always have at least one $FILE_NAME
}
static void ParseSI(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val || len < 48) return;
const auto* si = reinterpret_cast<const NTFS_STANDARD_INFORMATION*>(val);
fi.SI.CreationTime = si->CreationTime;
fi.SI.LastModificationTime = si->LastModificationTime;
fi.SI.MftModificationTime = si->MftModificationTime;
fi.SI.LastAccessTime = si->LastAccessTime;
fi.SI.FileAttributes = si->FileAttributes;
fi.SI.MaximumVersions = si->MaximumVersions;
fi.SI.VersionNumber = si->VersionNumber;
fi.SI.ClassId = si->ClassId;
fi.SI.IsV3 = (len >= 72);
if (fi.SI.IsV3) {
fi.SI.OwnerId = si->OwnerId;
fi.SI.SecurityId = si->SecurityId;
fi.SI.QuotaCharged = si->QuotaCharged;
fi.SI.Usn = si->Usn;
}
fi.HasSI = true;
}
static void ParseFN(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val || len < sizeof(NTFS_FILE_NAME)) return;
const auto* fn = reinterpret_cast<const NTFS_FILE_NAME*>(val);
const BYTE* fnEnd = val + sizeof(NTFS_FILE_NAME) + fn->FileNameLength * sizeof(WCHAR);
if (fnEnd > val + len) return;
if (NamespacePriority(fn->FileNameNamespace) < 0) return; // skip DOS
FileInfo::FileNameAttr e;
e.ParentFrn = fn->ParentFrn;
e.CreationTime = fn->CreationTime;
e.LastWriteTime = fn->LastWriteTime;
e.MftChangeTime = fn->MftChangeTime;
e.LastAccessTime = fn->LastAccessTime;
e.AllocatedSize = fn->AllocatedSize;
e.EndOfFile = fn->EndOfFile;
e.FileAttributes = fn->FileAttributes;
e.ReparseOrEaInfo = fn->ReparseOrEaInfo;
e.Namespace = fn->FileNameNamespace;
e.Name = std::wstring(reinterpret_cast<const WCHAR*>(fn + 1),
fn->FileNameLength);
fi.FileNames.push_back(std::move(e));
}
static void ParseData(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
const auto* attr = reinterpret_cast<const NTFS_ATTR_HEADER*>(cur);
FileInfo::DataStream s;
// Stream name: empty = main $DATA, nonempty = ADS
if (attr->NameLength > 0) {
const BYTE* nameBytes = cur + attr->NameOffset;
if (nameBytes + attr->NameLength * sizeof(WCHAR) <= attrEnd)
s.Name = std::wstring(reinterpret_cast<const WCHAR*>(nameBytes),
attr->NameLength);
}
if (attr->FormCode == 0) {
s.IsResident = true;
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (val) {
s.AllocatedSize = s.FileSize = s.InitializedSize = len;
s.ResidentData.assign(val, val + len);
}
}
else {
s.IsResident = false;
if (cur + sizeof(NTFS_ATTR_NON_RESIDENT) <= attrEnd) {
const auto* nr = reinterpret_cast<const NTFS_ATTR_NON_RESIDENT*>(cur);
s.AllocatedSize = nr->AllocatedSize;
s.FileSize = nr->FileSize;
s.InitializedSize = nr->InitializedSize;
const BYTE* runs = cur + nr->DataRunsOffset;
if (runs < attrEnd)
s.Runs = ParseDataRuns(runs, static_cast<size_t>(attrEnd - runs));
}
}
fi.DataStreams.push_back(std::move(s));
}
static void ParseObjId(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val || len < sizeof(GUID)) return;
memcpy(&fi.ObjId.ObjectId, val, sizeof(GUID));
if (len >= 4 * sizeof(GUID)) {
memcpy(&fi.ObjId.BirthVolumeId, val + 16, sizeof(GUID));
memcpy(&fi.ObjId.BirthObjectId, val + 32, sizeof(GUID));
memcpy(&fi.ObjId.DomainId, val + 48, sizeof(GUID));
}
fi.HasObjId = true;
}
static void ParseReparse(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
const auto* attr = reinterpret_cast<const NTFS_ATTR_HEADER*>(cur);
if (attr->FormCode != 0) { fi.HasReparse = true; return; } // nonresident: tag unknown
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val || len < sizeof(NTFS_REPARSE_BUFFER_HEADER)) return;
const auto* rbh = reinterpret_cast<const NTFS_REPARSE_BUFFER_HEADER*>(val);
fi.Reparse.Tag = rbh->ReparseTag;
const BYTE* data = val + sizeof(NTFS_REPARSE_BUFFER_HEADER);
const BYTE* dataEnd = std::min(data + rbh->ReparseDataLength, val + len);
fi.Reparse.RawBuffer.assign(data, dataEnd);
auto readPath = [&](const BYTE* pathBuf, WORD off, WORD byteLen) -> std::wstring {
if (pathBuf + off + byteLen > dataEnd) return {};
return std::wstring(reinterpret_cast<const WCHAR*>(pathBuf + off),
byteLen / sizeof(WCHAR));
};
if (rbh->ReparseTag == IO_REPARSE_TAG_SYMLINK &&
data + sizeof(NTFS_SYMLINK_REPARSE_BUFFER) <= dataEnd) {
const auto* sb = reinterpret_cast<const NTFS_SYMLINK_REPARSE_BUFFER*>(data);
const BYTE* pathBuf = data + sizeof(NTFS_SYMLINK_REPARSE_BUFFER);
fi.Reparse.SubstituteName = readPath(pathBuf, sb->SubstituteNameOffset, sb->SubstituteNameLength);
fi.Reparse.PrintName = readPath(pathBuf, sb->PrintNameOffset, sb->PrintNameLength);
}
else if (rbh->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT &&
data + sizeof(NTFS_MOUNT_POINT_REPARSE_BUFFER) <= dataEnd) {
const auto* mb = reinterpret_cast<const NTFS_MOUNT_POINT_REPARSE_BUFFER*>(data);
const BYTE* pathBuf = data + sizeof(NTFS_MOUNT_POINT_REPARSE_BUFFER);
fi.Reparse.SubstituteName = readPath(pathBuf, mb->SubstituteNameOffset, mb->SubstituteNameLength);
fi.Reparse.PrintName = readPath(pathBuf, mb->PrintNameOffset, mb->PrintNameLength);
}
fi.HasReparse = true;
}
static void ParseEaInfo(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val || len < sizeof(NTFS_EA_INFORMATION)) return;
const auto* ea = reinterpret_cast<const NTFS_EA_INFORMATION*>(val);
fi.EaInfo.PackedEaSize = ea->PackedEaSize;
fi.EaInfo.NeedEaCount = ea->NeedEaCount;
fi.EaInfo.UnpackedEaSize = ea->UnpackedEaSize;
fi.HasEaInfo = true;
}
static void ParseEA(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val) return;
const BYTE* ptr = val;
const BYTE* valEnd = val + len;
while (ptr + sizeof(FILE_FULL_EA_INFORMATION) <= valEnd) {
const auto* ea = reinterpret_cast<const FILE_FULL_EA_INFORMATION*>(ptr);
const BYTE* namePtr = ptr + sizeof(FILE_FULL_EA_INFORMATION);
const BYTE* valuePtr = namePtr + ea->EaNameLength + 1;
const BYTE* entryEnd = valuePtr + ea->EaValueLength;
if (entryEnd <= valEnd) {
FileInfo::ExtendedAttribute e;
e.Flags = ea->Flags;
e.Name = std::string(reinterpret_cast<const char*>(namePtr), ea->EaNameLength);
e.Value.assign(valuePtr, entryEnd);
fi.EAs.push_back(std::move(e));
}
if (ea->NextEntryOffset == 0) break;
ptr += ea->NextEntryOffset;
if (ptr >= valEnd) break;
}
}
static void ParseAttrList(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
fi.HasAttributeList = true;
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val) return;
const BYTE* ptr = val;
const BYTE* valEnd = val + len;
while (ptr + sizeof(NTFS_ATTR_LIST_ENTRY) <= valEnd) {
const auto* e = reinterpret_cast<const NTFS_ATTR_LIST_ENTRY*>(ptr);
if (e->RecordLength < sizeof(NTFS_ATTR_LIST_ENTRY)) break;
FileInfo::AttributeListEntry entry;
entry.TypeCode = e->TypeCode;
entry.MftReference = MftStripSeq(e->MftReference);
entry.StartingVcn = e->StartingVcn;
if (e->AttributeNameLength > 0) {
BYTE off = e->AttributeNameOffset > 0
? e->AttributeNameOffset
: static_cast<BYTE>(sizeof(NTFS_ATTR_LIST_ENTRY));
const BYTE* nameEnd = ptr + off + e->AttributeNameLength * sizeof(WCHAR);
if (nameEnd <= valEnd)
entry.Name = std::wstring(
reinterpret_cast<const WCHAR*>(ptr + off),
e->AttributeNameLength);
}
fi.AttributeList.push_back(std::move(entry));
ptr += e->RecordLength;
}
}
static void ParseSecDesc(const BYTE* cur, const BYTE* attrEnd, FileInfo& fi) {
DWORD len = 0;
const BYTE* val = ResidentVal(cur, attrEnd, len);
if (!val || len < sizeof(NTFS_SECURITY_DESCRIPTOR_HDR)) return;
const auto* sd = reinterpret_cast<const NTFS_SECURITY_DESCRIPTOR_HDR*>(val);
fi.SecDesc.Control = sd->Control;
// Copy a SID: total size = 8 + SubAuthorityCount * 4 (SubAuthorityCount is at byte[1])
auto copySid = [&](DWORD offset, std::vector<BYTE>& dest) {
if (offset == 0) return;
const BYTE* p = val + offset;
if (p + 8 > val + len) return;
DWORD sidSize = 8u + p[1] * 4u;
if (p + sidSize <= val + len) dest.assign(p, p + sidSize);
};
// Copy an ACL: total size stored as WORD at byte[2] of the ACL header
auto copyAcl = [&](DWORD offset, std::vector<BYTE>& dest) {
if (offset == 0) return;
const BYTE* p = val + offset;
if (p + 4 > val + len) return;
WORD aclSize = *reinterpret_cast<const WORD*>(p + 2);
if (p + aclSize <= val + len) dest.assign(p, p + aclSize);
};
copySid(sd->OffsetOwner, fi.SecDesc.OwnerSid);
copySid(sd->OffsetGroup, fi.SecDesc.GroupSid);
copyAcl(sd->OffsetSacl, fi.SecDesc.Sacl);
copyAcl(sd->OffsetDacl, fi.SecDesc.Dacl);
fi.HasSecDesc = true;
}
// Decode NTFS runlength encoded data run list
static std::vector<FileInfo::DataRun> ParseDataRuns(const BYTE* ptr, size_t maxLen) {
std::vector<FileInfo::DataRun> runs;
const BYTE* end = ptr + maxLen;
LONGLONG curLcn = 0, curVcn = 0;
while (ptr < end) {
BYTE header = *ptr++;
if (header == 0) break;
int lenField = header & 0x0F;
int lcnField = (header >> 4) & 0x0F;
if (ptr + lenField + lcnField > end) break;
LONGLONG runLen = 0;
for (int i = 0; i < lenField; i++)
runLen |= static_cast<LONGLONG>(*ptr++) << (i * 8);
LONGLONG lcnDelta = 0;
for (int i = 0; i < lcnField; i++)
lcnDelta |= static_cast<LONGLONG>(*ptr++) << (i * 8);
// Sign-extend the LCN delta
if (lcnField > 0 && (lcnDelta >> (lcnField * 8 - 1)) & 1)
lcnDelta |= ~((1LL << (lcnField * 8)) - 1);
FileInfo::DataRun r;
r.Vcn = curVcn;
r.Lcn = (lcnField == 0) ? -1LL : curLcn + lcnDelta;
r.Length = runLen;
runs.push_back(r);
if (lcnField > 0) curLcn += lcnDelta;
curVcn += runLen;
}
return runs;
}
static const FileInfo::FileNameAttr* BestFileName(const FileInfo& fi) {
const FileInfo::FileNameAttr* best = nullptr;
int bestPrio = -1;
for (const auto& fn : fi.FileNames) {
int p = NamespacePriority(fn.Namespace);
if (p > bestPrio) { bestPrio = p; best = &fn; }
}
return best;
}
static int NamespacePriority(BYTE ns) {
switch (ns) {
case MFT_NS_WIN32_DOS: return 3;
case MFT_NS_WIN32: return 2;
case MFT_NS_POSIX: return 1;
case MFT_NS_DOS: return -1;
default: return 0;
}
}
static std::wstring BuildPath(const std::unordered_map<DWORDLONG, MftEntry>& map,
DWORDLONG frn, const std::wstring& prefix) {
std::vector<const std::wstring*> parts;
DWORDLONG cur = MftStripSeq(frn);
std::unordered_set<DWORDLONG> visited;
while (cur != MFT_ROOT_FRN) {
if (!visited.insert(cur).second) { parts.push_back(nullptr); break; }
auto it = map.find(cur);
if (it == map.end()) { parts.push_back(nullptr); break; }
parts.push_back(&it->second.name);
cur = MftStripSeq(it->second.parentFrn);
}
std::wstring path = prefix;
for (int i = static_cast<int>(parts.size()) - 1; i >= 0; --i) {
path += L'\\';
path += parts[i] ? *parts[i] : L"<?>";
}
return path;
}
};
int main(){
std::vector<MftSnapshot::FileInfo> files = MftSnapshot::GetSnapshotViaMFT(L"\\\\.\\C:", L"C:", false);
if (files.empty()) {
std::cout << "Error getting files" << std::endl;
return 1;
}
else {
std::cout << "Files retrieved correctly" << std::endl;
return 0;
}
}Proof of Concept
Open a CMD as administrator and run the compiled code:
C:\Windows\System32>"C:\Users\s12de\Documents\Github\cppclasses\File Management\GetFIleSystemSnapshot\x64\Release\GetFIleSystemSnapshot.exe"
[*] MFT @ 0xc0000000 records: 879104 (858 MB)
[*] 879104 / 879104 live: 191086
[+] Done: 191086 entries
Files retrieved correctlyC:\Windows\System32>"C:\Users\s12de\Documents\Github\cppclasses\File Management\GetFIleSystemSnapshot\x64\Release\GetFIleSystemSnapshot.exe"
[*] MFT @ 0xc0000000 records: 879104 (858 MB)
[*] 879104 / 879104 live: 191086
[+] Done: 191086 entries
Files retrieved correctlyIf you add a breakpoint (in visual studio you will need open the VS with admin permissions, and run the code with the breakpoint) you can navigate through the structure:
Detection
0x12DarkSandbox
Test your own payloads against the same stack with a free scan on sign up, or go deeper with a scan pack or monthly plan
Upload & Scan Upload malware samples for parallel analysis across isolated Windows VMs with multi-engine AV scanning
Result:
Results Job #225 Cloud-based malware analysis and execution telemetry platform
YARA
Here a YARA rule to detect this technique:
rule DirectMFTParsing_RawNTFSEnumeration
{
meta:
description = "Detects binaries that perform direct $MFT parsing to enumerate NTFS filesystem entries, bypassing monitored API layers (FindFirstFile, NtQueryDirectoryFile)"
author = "0x12 Dark Development"
reference = "https://medium.com/@s12deff"
created = "2025-07-16"
mitre_attack = "T1083 - File and Directory Discovery"
severity = "high"
confidence = "medium"
note = "May also match legitimate low-level disk tools (defragmenters, forensic suites). Combine with behavioral context for higher fidelity."
strings:
// === Volume Handle Acquisition ===
// CreateFileW / NtCreateFile paths used to open the raw volume device
$vol_c = "\\\\.\\C:" wide
$vol_d = "\\\\.\\D:" wide
$vol_e = "\\\\.\\E:" wide
$vol_f = "\\\\.\\F:" wide
$vol_phys = "\\\\.\\PhysicalDrive" wide
$vol_nt = "\\??\\C:" wide // NT native path alternative
// === NTFS-Specific IOCTLs ===
// FSCTL_GET_NTFS_VOLUME_DATA = 0x00090064
// Retrieves MFT start LCN, BytesPerFileRecordSegment,
// BytesPerCluster, MftValidDataLength — required to locate and walk the $MFT
$ioctl_nvd = { 64 00 09 00 }
// FSCTL_GET_NTFS_FILE_RECORD = 0x00090068
// Alternative approach: request individual MFT records by record number
$ioctl_nfr = { 68 00 09 00 }
// === MFT Record Internals ===
// FRN 48-bit mask: 0x0000FFFFFFFFFFFF (little-endian QWORD)
// Used to strip the sequence number field from file reference numbers.
// Uniquely tied to $MFT record number handling — low false-positive rate.
$frn_mask = { FF FF FF FF FF FF 00 00 }
// MFT record "FILE" signature = 0x454C4946 (little-endian DWORD)
// Every MFT parser validates this at the start of each record
$mft_sig = { 46 49 4C 45 }
// Attribute chain end marker: 0xFFFFFFFF as DWORD
// Every MFT attribute walker must check this to stop iteration
$attr_end = { FF FF FF FF }
// NTFS attribute type codes as DWORDs (little-endian)
$attr_si = { 10 00 00 00 } // $STANDARD_INFORMATION
$attr_fn = { 30 00 00 00 } // $FILE_NAME
$attr_data = { 80 00 00 00 } // $DATA
// === Debug / Verbose Build Strings ===
// These field names have no reason to appear outside of NTFS tools.
// If present alongside an IOCTL — very high confidence.
$s_lcn = "MftStartLcn"
$s_frs = "BytesPerFileRecordSegment"
$s_mft = "$MFT"
$s_mft_w = "$MFT" wide
condition:
uint16(0) == 0x5A4D and
filesize < 20MB and
(
// --- Variant A | High Confidence ---
// Core MFT bulk walker:
// Opens raw volume + queries NTFS metadata + manipulates FRNs
// This three-way combination is unique to direct $MFT traversal
(
(1 of ($vol_*)) and
$ioctl_nvd and
$frn_mask
)
or
// --- Variant B | High Confidence ---
// Attribute-level parser:
// Validates "FILE" signature + walks attribute chain + strips FRN sequence numbers
(
$mft_sig and
$frn_mask and
($ioctl_nvd or $ioctl_nfr) and
(2 of ($attr_si, $attr_fn, $attr_data)) and
$attr_end
)
or
// --- Variant C | Medium Confidence ---
// Per-record FSCTL approach:
// Some implementations request records one by one instead of bulk reading
(
(1 of ($vol_*)) and
$ioctl_nfr and
$frn_mask
)
or
// --- Variant D | High Confidence ---
// Debug or verbose build:
// NTFS internal struct field names have no reason to appear in non-NTFS software
(
($s_lcn or $s_frs) and
($ioctl_nvd or $ioctl_nfr)
)
or
// --- Variant E | Medium Confidence ---
// Explicit $MFT string reference (format strings, error messages) + IOCTL + volume path
(
($s_mft or $s_mft_w) and
($ioctl_nvd or $ioctl_nfr) and
(1 of ($vol_*))
)
)
}rule DirectMFTParsing_RawNTFSEnumeration
{
meta:
description = "Detects binaries that perform direct $MFT parsing to enumerate NTFS filesystem entries, bypassing monitored API layers (FindFirstFile, NtQueryDirectoryFile)"
author = "0x12 Dark Development"
reference = "https://medium.com/@s12deff"
created = "2025-07-16"
mitre_attack = "T1083 - File and Directory Discovery"
severity = "high"
confidence = "medium"
note = "May also match legitimate low-level disk tools (defragmenters, forensic suites). Combine with behavioral context for higher fidelity."
strings:
// === Volume Handle Acquisition ===
// CreateFileW / NtCreateFile paths used to open the raw volume device
$vol_c = "\\\\.\\C:" wide
$vol_d = "\\\\.\\D:" wide
$vol_e = "\\\\.\\E:" wide
$vol_f = "\\\\.\\F:" wide
$vol_phys = "\\\\.\\PhysicalDrive" wide
$vol_nt = "\\??\\C:" wide // NT native path alternative
// === NTFS-Specific IOCTLs ===
// FSCTL_GET_NTFS_VOLUME_DATA = 0x00090064
// Retrieves MFT start LCN, BytesPerFileRecordSegment,
// BytesPerCluster, MftValidDataLength — required to locate and walk the $MFT
$ioctl_nvd = { 64 00 09 00 }
// FSCTL_GET_NTFS_FILE_RECORD = 0x00090068
// Alternative approach: request individual MFT records by record number
$ioctl_nfr = { 68 00 09 00 }
// === MFT Record Internals ===
// FRN 48-bit mask: 0x0000FFFFFFFFFFFF (little-endian QWORD)
// Used to strip the sequence number field from file reference numbers.
// Uniquely tied to $MFT record number handling — low false-positive rate.
$frn_mask = { FF FF FF FF FF FF 00 00 }
// MFT record "FILE" signature = 0x454C4946 (little-endian DWORD)
// Every MFT parser validates this at the start of each record
$mft_sig = { 46 49 4C 45 }
// Attribute chain end marker: 0xFFFFFFFF as DWORD
// Every MFT attribute walker must check this to stop iteration
$attr_end = { FF FF FF FF }
// NTFS attribute type codes as DWORDs (little-endian)
$attr_si = { 10 00 00 00 } // $STANDARD_INFORMATION
$attr_fn = { 30 00 00 00 } // $FILE_NAME
$attr_data = { 80 00 00 00 } // $DATA
// === Debug / Verbose Build Strings ===
// These field names have no reason to appear outside of NTFS tools.
// If present alongside an IOCTL — very high confidence.
$s_lcn = "MftStartLcn"
$s_frs = "BytesPerFileRecordSegment"
$s_mft = "$MFT"
$s_mft_w = "$MFT" wide
condition:
uint16(0) == 0x5A4D and
filesize < 20MB and
(
// --- Variant A | High Confidence ---
// Core MFT bulk walker:
// Opens raw volume + queries NTFS metadata + manipulates FRNs
// This three-way combination is unique to direct $MFT traversal
(
(1 of ($vol_*)) and
$ioctl_nvd and
$frn_mask
)
or
// --- Variant B | High Confidence ---
// Attribute-level parser:
// Validates "FILE" signature + walks attribute chain + strips FRN sequence numbers
(
$mft_sig and
$frn_mask and
($ioctl_nvd or $ioctl_nfr) and
(2 of ($attr_si, $attr_fn, $attr_data)) and
$attr_end
)
or
// --- Variant C | Medium Confidence ---
// Per-record FSCTL approach:
// Some implementations request records one by one instead of bulk reading
(
(1 of ($vol_*)) and
$ioctl_nfr and
$frn_mask
)
or
// --- Variant D | High Confidence ---
// Debug or verbose build:
// NTFS internal struct field names have no reason to appear in non-NTFS software
(
($s_lcn or $s_frs) and
($ioctl_nvd or $ioctl_nfr)
)
or
// --- Variant E | Medium Confidence ---
// Explicit $MFT string reference (format strings, error messages) + IOCTL + volume path
(
($s_mft or $s_mft_w) and
($ioctl_nvd or $ioctl_nfr) and
(1 of ($vol_*))
)
)
}Here you have my collection of YARA rules:
GitHub — S12cybersecurity/YaraRules: Collection of interesting Yara Rules Collection of interesting Yara Rules. Contribute to S12cybersecurity/YaraRules development by creating an account on…
Conclusions
Direct $MFT parsing is a powerful technique that gives us complete visibility over the filesystem without touching any monitored API. By reading the raw volume and walking the NTFS structures ourselves, we can enumerate live files, deleted entries, alternate data streams, and full path
📌 Follow me: 🐦 X | 💬 Discord Server | 📸 Instagram | Newsletter | YouTube
S12.