Loading learning content...
Imagine needing to find every file that changed on a terabyte volume since your last backup. Without special support, you'd have to scan every file—checking modification times, comparing hashes—a process that could take hours. NTFS provides a elegant solution: the Change Journal (also known as the USN Journal or $UsnJrnl).
The Change Journal maintains a persistent log of every file and directory change on a volume. When Windows Search needs to update its index, when backup software identifies changed files, when antivirus solutions monitor for suspicious activity—they all leverage this journal. Understanding the Change Journal is essential for building efficient file monitoring applications and conducting forensic investigations.
By the end of this page, you will understand the Change Journal's architecture, the structure of USN records, how to query and interpret journal data, practical applications for monitoring and forensics, and the limitations and management of this powerful feature.
What is the Change Journal?
The NTFS Change Journal is a system-maintained log that records changes to files and directories on a volume. Every creation, deletion, modification, rename, and attribute change generates a journal record. The journal is persistent—surviving reboots—and enables efficient change tracking.
Key Characteristics:
Historical Context:
Introduced with Windows 2000 (NTFS 3.0), the Change Journal was created primarily to support:
Location in the File System:
The Change Journal is stored in the $Extend directory (a hidden system folder) as $UsnJrnl. It consists of two data streams:
$Max: Configuration data (maximum size, allocation delta)$J: The actual journal records1234567891011121314151617181920212223242526272829303132333435363738394041424344
// Change Journal location// \$Extend\$UsnJrnl:$J <- Journal data stream// \$Extend\$UsnJrnl:$Max <- Configuration stream // USN Journal configuration (from $Max stream)typedef struct _USN_JOURNAL_DATA_V2 { DWORDLONG UsnJournalID; // Journal instance ID USN FirstUsn; // First valid USN in journal USN NextUsn; // Next USN to be assigned USN LowestValidUsn; // Lowest valid USN USN MaxUsn; // Maximum USN value DWORDLONG MaximumSize; // Maximum journal size in bytes DWORDLONG AllocationDelta; // Size to grow/shrink by WORD MinSupportedMajorVersion; WORD MaxSupportedMajorVersion; DWORD Flags; // USN_JOURNAL_DATA_FLAG_* DWORDLONG RangeTrackChunkSize; LONGLONG RangeTrackFileSizeThreshold;} USN_JOURNAL_DATA_V2; // USN - Update Sequence Number// A 64-bit value that is also the byte offset into the $J streamtypedef LONGLONG USN; // Querying journal stateBOOL GetJournalInfo(HANDLE hVolume, USN_JOURNAL_DATA_V2 *journalData) { DWORD bytesReturned; return DeviceIoControl( hVolume, FSCTL_QUERY_USN_JOURNAL, NULL, 0, journalData, sizeof(USN_JOURNAL_DATA_V2), &bytesReturned, NULL );} // Example output:// Journal ID: 0x01D9A7B3C4E5F600// First USN: 0x00000000F8000000 (oldest record)// Next USN: 0x00000000FA234560 (next to assign) // Maximum Size: 33554432 bytes (32 MB)// Current Size: ~31 MB (FA234560 - F8000000)A clever design aspect: the USN value is simultaneously a unique identifier AND the byte offset of the record within the $J stream. To read a record, simply seek to position USN in the journal. This eliminates the need for separate indexing structures while maintaining O(1) record lookup.
Each entry in the Change Journal is a USN Record. The record contains information about what changed, which file changed, and when. NTFS has evolved through multiple record versions:
USN_RECORD_V2 Structure (Most Common):
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 bytes | RecordLength | Total record length including filename |
| 0x04 | 2 bytes | MajorVersion | 2 for V2 records |
| 0x06 | 2 bytes | MinorVersion | 0 |
| 0x08 | 8 bytes | FileReferenceNumber | MFT reference of the file |
| 0x10 | 8 bytes | ParentFileReferenceNumber | MFT reference of parent directory |
| 0x18 | 8 bytes | Usn | This record's USN |
| 0x20 | 8 bytes | TimeStamp | Time of the change (FILETIME) |
| 0x28 | 4 bytes | Reason | Reason flags (what changed) |
| 0x2C | 4 bytes | SourceInfo | Source of change |
| 0x30 | 4 bytes | SecurityId | Security ID (from $Secure) |
| 0x34 | 4 bytes | FileAttributes | DOS file attributes |
| 0x38 | 2 bytes | FileNameLength | Filename length in bytes |
| 0x3A | 2 bytes | FileNameOffset | Offset to filename |
| 0x3C | Variable | FileName | Unicode filename (UTF-16LE) |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
// USN_RECORD_V2 structuretypedef struct _USN_RECORD_V2 { DWORD RecordLength; WORD MajorVersion; // 2 WORD MinorVersion; // 0 DWORDLONG FileReferenceNumber; DWORDLONG ParentFileReferenceNumber; USN Usn; LARGE_INTEGER TimeStamp; DWORD Reason; DWORD SourceInfo; DWORD SecurityId; DWORD FileAttributes; WORD FileNameLength; WORD FileNameOffset; WCHAR FileName[1]; // Variable length} USN_RECORD_V2, *PUSN_RECORD_V2; // USN_RECORD_V3 - for 128-bit file IDs (ReFS, and NTFS with GUIDs)typedef struct _USN_RECORD_V3 { DWORD RecordLength; WORD MajorVersion; // 3 WORD MinorVersion; // 0 FILE_ID_128 FileReferenceNumber; // 16 bytes FILE_ID_128 ParentFileReferenceNumber; // 16 bytes USN Usn; LARGE_INTEGER TimeStamp; DWORD Reason; DWORD SourceInfo; DWORD SecurityId; DWORD FileAttributes; WORD FileNameLength; WORD FileNameOffset; WCHAR FileName[1];} USN_RECORD_V3, *PUSN_RECORD_V3; // Parsing a USN recordvoid ParseUsnRecord(PUSN_RECORD_V2 record) { WCHAR fileName[MAX_PATH]; // Extract filename memcpy(fileName, (BYTE*)record + record->FileNameOffset, record->FileNameLength); fileName[record->FileNameLength / sizeof(WCHAR)] = L'\0'; // Extract timestamp FILETIME ft; ft.dwLowDateTime = record->TimeStamp.LowPart; ft.dwHighDateTime = record->TimeStamp.HighPart; SYSTEMTIME st; FileTimeToSystemTime(&ft, &st); wprintf(L"USN: 0x%016llX\n", record->Usn); wprintf(L"File: %s\n", fileName); wprintf(L"MFT Ref: %llu (seq %u)\n", record->FileReferenceNumber & 0xFFFFFFFFFFFF, (DWORD)(record->FileReferenceNumber >> 48)); wprintf(L"Parent: %llu\n", record->ParentFileReferenceNumber & 0xFFFFFFFFFFFF); wprintf(L"Time: %04d-%02d-%02d %02d:%02d:%02d\n", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); wprintf(L"Reason: 0x%08X\n", record->Reason);}The Reason field in a USN record is a bit field that indicates what changes occurred to the file. Multiple bits can be set simultaneously—for example, a file might be both created and have data written in the same operation.
Reason Code Breakdown:
| Flag | Value | Meaning |
|---|---|---|
| USN_REASON_DATA_OVERWRITE | 0x00000001 | Data in one or more data streams was overwritten |
| USN_REASON_DATA_EXTEND | 0x00000002 | Data was appended to a data stream |
| USN_REASON_DATA_TRUNCATION | 0x00000004 | Data was truncated from a data stream |
| USN_REASON_NAMED_DATA_OVERWRITE | 0x00000010 | Named data stream was overwritten |
| USN_REASON_NAMED_DATA_EXTEND | 0x00000020 | Named data stream was extended |
| USN_REASON_NAMED_DATA_TRUNCATION | 0x00000040 | Named data stream was truncated |
| USN_REASON_FILE_CREATE | 0x00000100 | File or directory was created |
| USN_REASON_FILE_DELETE | 0x00000200 | File or directory was deleted |
| USN_REASON_EA_CHANGE | 0x00000400 | Extended attributes changed |
| USN_REASON_SECURITY_CHANGE | 0x00000800 | Security descriptor changed |
| USN_REASON_RENAME_OLD_NAME | 0x00001000 | File renamed (old name entry) |
| USN_REASON_RENAME_NEW_NAME | 0x00002000 | File renamed (new name entry) |
| USN_REASON_INDEXABLE_CHANGE | 0x00004000 | Content indexable flag changed |
| USN_REASON_BASIC_INFO_CHANGE | 0x00008000 | Basic info changed (times, attributes) |
| USN_REASON_HARD_LINK_CHANGE | 0x00010000 | Hard link added or removed |
| USN_REASON_COMPRESSION_CHANGE | 0x00020000 | Compression state changed |
| USN_REASON_ENCRYPTION_CHANGE | 0x00040000 | Encryption state changed |
| USN_REASON_OBJECT_ID_CHANGE | 0x00080000 | Object ID changed |
| USN_REASON_REPARSE_POINT_CHANGE | 0x00100000 | Reparse point changed |
| USN_REASON_STREAM_CHANGE | 0x00200000 | Named stream added or removed |
| USN_REASON_TRANSACTED_CHANGE | 0x00400000 | Transactional change (TxF) |
| USN_REASON_INTEGRITY_CHANGE | 0x00800000 | Integrity stream changed (ReFS) |
| USN_REASON_CLOSE | 0x80000000 | File handle closed (records batched) |
Understanding Record Sequences:
A single user action can generate multiple USN records. For example, creating and editing a new document:
1. USN 0x1000: Reason=FILE_CREATE (Empty file created)
2. USN 0x1080: Reason=DATA_EXTEND (First write)
3. USN 0x1100: Reason=DATA_OVERWRITE (Editing content)
4. USN 0x1180: Reason=BASIC_INFO_CHANGE (Modify time updated)
5. USN 0x1200: Reason=CLOSE (Handle closed)
The CLOSE Flag:
The USN_REASON_CLOSE flag (0x80000000) provides aggregation. When many small changes occur before a file is closed, NTFS may combine them into a single record with CLOSE set. This record summarizes all changes since the file was opened.
Rename Operations:
Renames generate two records:
RENAME_OLD_NAME: Contains the old filenameRENAME_NEW_NAME: Contains the new filenameBoth have the same USN timestamp but sequential USN values. The file reference remains the same (it's the same file).
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
// Human-readable reason decodingvoid DecodeReason(DWORD reason, WCHAR *buffer, size_t bufLen) { buffer[0] = L'\0'; if (reason & USN_REASON_FILE_CREATE) wcscat_s(buffer, bufLen, L"CREATE "); if (reason & USN_REASON_FILE_DELETE) wcscat_s(buffer, bufLen, L"DELETE "); if (reason & USN_REASON_DATA_OVERWRITE) wcscat_s(buffer, bufLen, L"DATA_OVERWRITE "); if (reason & USN_REASON_DATA_EXTEND) wcscat_s(buffer, bufLen, L"DATA_EXTEND "); if (reason & USN_REASON_DATA_TRUNCATION) wcscat_s(buffer, bufLen, L"DATA_TRUNCATION "); if (reason & USN_REASON_RENAME_OLD_NAME) wcscat_s(buffer, bufLen, L"RENAME_OLD "); if (reason & USN_REASON_RENAME_NEW_NAME) wcscat_s(buffer, bufLen, L"RENAME_NEW "); if (reason & USN_REASON_SECURITY_CHANGE) wcscat_s(buffer, bufLen, L"SECURITY "); if (reason & USN_REASON_BASIC_INFO_CHANGE) wcscat_s(buffer, bufLen, L"BASIC_INFO "); if (reason & USN_REASON_HARD_LINK_CHANGE) wcscat_s(buffer, bufLen, L"HARD_LINK "); if (reason & USN_REASON_ENCRYPTION_CHANGE) wcscat_s(buffer, bufLen, L"ENCRYPTION "); if (reason & USN_REASON_REPARSE_POINT_CHANGE) wcscat_s(buffer, bufLen, L"REPARSE "); if (reason & USN_REASON_CLOSE) wcscat_s(buffer, bufLen, L"CLOSE ");} // Reason categories for filtering#define USN_REASON_DATA_MODIFIED \ (USN_REASON_DATA_OVERWRITE | USN_REASON_DATA_EXTEND | \ USN_REASON_DATA_TRUNCATION) #define USN_REASON_METADATA_MODIFIED \ (USN_REASON_BASIC_INFO_CHANGE | USN_REASON_SECURITY_CHANGE | \ USN_REASON_EA_CHANGE | USN_REASON_OBJECT_ID_CHANGE) #define USN_REASON_RENAMED \ (USN_REASON_RENAME_OLD_NAME | USN_REASON_RENAME_NEW_NAME) // Filter example: Find only content modificationsbool IsContentModification(DWORD reason) { return (reason & USN_REASON_DATA_MODIFIED) != 0;}Windows provides several DeviceIoControl operations for interacting with the Change Journal. Understanding these is essential for building monitoring applications.
Key FSCTL Operations:
| FSCTL Code | Purpose |
|---|---|
| FSCTL_QUERY_USN_JOURNAL | Get journal configuration and state |
| FSCTL_READ_USN_JOURNAL | Read journal records sequentially |
| FSCTL_READ_FILE_USN_DATA | Get latest USN for a specific file |
| FSCTL_ENUM_USN_DATA | Enumerate all files with their USN |
| FSCTL_CREATE_USN_JOURNAL | Create/enable the journal |
| FSCTL_DELETE_USN_JOURNAL | Delete the journal |
Reading Journal Records:
The FSCTL_READ_USN_JOURNAL operation reads records starting from a given USN. It's the primary tool for monitoring changes:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
// Input structure for FSCTL_READ_USN_JOURNALtypedef struct _READ_USN_JOURNAL_DATA_V1 { USN StartUsn; // Where to start reading DWORD ReasonMask; // Filter by reason (0 = all) DWORD ReturnOnlyOnClose; // Only return CLOSE records DWORDLONG Timeout; // Wait timeout in 100ns units (0 = don't wait) DWORDLONG BytesToWaitFor; // Min bytes before returning DWORDLONG UsnJournalID; // Journal ID (from query) WORD MinMajorVersion; // Min record version WORD MaxMajorVersion; // Max record version} READ_USN_JOURNAL_DATA_V1; // Reading journal records exampleBOOL ReadJournalRecords(HANDLE hVolume, USN startUsn, DWORDLONG journalId) { READ_USN_JOURNAL_DATA_V1 readData = {0}; readData.StartUsn = startUsn; readData.ReasonMask = 0xFFFFFFFF; // All reasons readData.UsnJournalID = journalId; readData.MinMajorVersion = 2; readData.MaxMajorVersion = 3; // Buffer to receive records BYTE buffer[64 * 1024]; // 64KB buffer DWORD bytesReturned; BOOL success = DeviceIoControl( hVolume, FSCTL_READ_USN_JOURNAL, &readData, sizeof(readData), buffer, sizeof(buffer), &bytesReturned, NULL ); if (!success) return FALSE; // First 8 bytes: next USN to query USN nextUsn = *(USN*)buffer; // Remaining bytes: USN records PUSN_RECORD_V2 record = (PUSN_RECORD_V2)(buffer + sizeof(USN)); BYTE *end = buffer + bytesReturned; while ((BYTE*)record < end && record->RecordLength > 0) { // Process this record ProcessUsnRecord(record); // Move to next record record = (PUSN_RECORD_V2)((BYTE*)record + record->RecordLength); } // For continuous monitoring, read again with nextUsn return TRUE;} // Continuous monitoring loopvoid MonitorVolume(HANDLE hVolume) { USN_JOURNAL_DATA_V2 journalData; GetJournalInfo(hVolume, &journalData); USN currentUsn = journalData.NextUsn; // Start from now while (TRUE) { READ_USN_JOURNAL_DATA_V1 readData = {0}; readData.StartUsn = currentUsn; readData.ReasonMask = 0xFFFFFFFF; readData.Timeout = 100000000; // 10 seconds readData.BytesToWaitFor = 0; // Return immediately if data readData.UsnJournalID = journalData.UsnJournalID; BYTE buffer[64 * 1024]; DWORD bytesReturned; if (DeviceIoControl(hVolume, FSCTL_READ_USN_JOURNAL, &readData, sizeof(readData), buffer, sizeof(buffer), &bytesReturned, NULL)) { currentUsn = *(USN*)buffer; // Update position // Process records... PUSN_RECORD_V2 rec = (PUSN_RECORD_V2)(buffer + sizeof(USN)); while ((BYTE*)rec < buffer + bytesReturned && rec->RecordLength) { OnFileChange(rec); // Application callback rec = (PUSN_RECORD_V2)((BYTE*)rec + rec->RecordLength); } } Sleep(100); // Brief pause between queries }}For real-time monitoring, use the Timeout and BytesToWaitFor parameters. Setting a timeout causes the IOCTL to block until:
This is far more efficient than polling, eliminating busy-wait loops.
The Change Journal enables several critical Windows features and third-party applications. Understanding these use cases illustrates the journal's power.
Windows Search Indexing:
The Windows Search service maintains a content index for fast file searches. Rather than periodically scanning all files:
This makes indexing efficient—only changed content is re-indexed.
Incremental Backup:
Backup software uses the journal for fast incremental backups:
File Synchronization (DFS-R):
Distributed File System Replication uses the journal to detect changes:
Security Monitoring:
SIEM and EDR solutions monitor the journal for suspicious activity:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
// Example: Incremental backup using Change Journal typedef struct _BACKUP_STATE { DWORDLONG JournalId; USN LastBackupUsn; WCHAR BackupRoot[MAX_PATH];} BACKUP_STATE; // Perform incremental backupvoid IncrementalBackup(HANDLE hVolume, BACKUP_STATE *state) { // Verify journal hasn't been recreated USN_JOURNAL_DATA_V2 journal; GetJournalInfo(hVolume, &journal); if (journal.UsnJournalID != state->JournalId || journal.FirstUsn > state->LastBackupUsn) { // Journal was recreated or records purged // Must do full backup wprintf(L"Journal reset detected - full backup required\n"); FullBackup(hVolume, state); return; } // Read changes since last backup READ_USN_JOURNAL_DATA_V1 readData = {0}; readData.StartUsn = state->LastBackupUsn; readData.ReasonMask = USN_REASON_DATA_OVERWRITE | USN_REASON_DATA_EXTEND | USN_REASON_DATA_TRUNCATION | USN_REASON_FILE_CREATE; // Include new files readData.UsnJournalID = journal.UsnJournalID; BYTE buffer[256 * 1024]; DWORD bytesReturned; USN currentUsn = state->LastBackupUsn; int filesBackedUp = 0; while (DeviceIoControl(hVolume, FSCTL_READ_USN_JOURNAL, &readData, sizeof(readData), buffer, sizeof(buffer), &bytesReturned, NULL)) { USN nextUsn = *(USN*)buffer; if (nextUsn == currentUsn) break; // No more records PUSN_RECORD_V2 record = (PUSN_RECORD_V2)(buffer + sizeof(USN)); while ((BYTE*)record < buffer + bytesReturned && record->RecordLength) { // Skip directories if (!(record->FileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { // Get full path from MFT reference WCHAR filePath[MAX_PATH]; if (GetPathFromFileRef(hVolume, record->FileReferenceNumber, filePath)) { BackupFile(filePath, state->BackupRoot); filesBackedUp++; } } record = (PUSN_RECORD_V2)((BYTE*)record + record->RecordLength); } currentUsn = nextUsn; readData.StartUsn = currentUsn; } // Update state state->LastBackupUsn = currentUsn; SaveBackupState(state); wprintf(L"Incremental backup complete: %d files backed up\n", filesBackedUp);} // Example: Ransomware detection heuristicsvoid CheckForRansomware(PUSN_RECORD_V2 record) { // High-entropy file extensions appearing static const WCHAR *SuspiciousExts[] = { L".encrypted", L".locked", L".crypto", L".crypt" }; // Mass renames to suspicious extensions if (record->Reason & USN_REASON_RENAME_NEW_NAME) { for (int i = 0; i < ARRAYSIZE(SuspiciousExts); i++) { if (wcsstr(GetFilename(record), SuspiciousExts[i])) { RaiseAlert(L"Possible ransomware: suspicious rename", record); } } } // Many files encrypted in short time if (record->Reason & USN_REASON_ENCRYPTION_CHANGE) { TrackEncryptionEvent(record); if (GetRecentEncryptionCount() > 100) { RaiseAlert(L"Mass encryption detected", record); } }}Always check that the Journal ID matches your saved state. If the journal was deleted and recreated, or if records were purged, your saved USN is invalid. In such cases, you must fall back to a full scan or full backup, as the intermediate changes are lost.
The Change Journal is a goldmine for digital forensics. Unlike file timestamps, which can be manipulated, journal records are append-only and provide an authoritative timeline of file system activity.
Forensic Value:
Key Forensic Indicators:
| Activity | Journal Signature |
|---|---|
| Malware execution | CREATE in \Temp, \AppData\Local\Temp, or user directories |
| Data exfiltration | CREATE followed by DELETE of archive files |
| Credential theft | Access to NTUSER.DAT, SAM, SECURITY hives |
| Defense evasion | DELETE of logs, journal, prefetch files |
| Persistence | CREATE in \Startup, registry hive modifications |
| Ransomware | Mass RENAME_NEW_NAME with encrypted extensions |
| Time stomping | BASIC_INFO_CHANGE without corresponding data changes |
| Lateral movement | CREATE of tools (psexec, mimikatz) from network paths |
Correlating Journal with MFT:
The journal stores file reference numbers, not full paths. For deleted files, the MFT entry may have been reused. However:
Even for deleted files, you can often reconstruct the full path by traversing parent references until finding an existing directory.
Tool Support:
Several forensic tools parse the Change Journal:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
// Raw journal extraction for offline analysis// The $J stream is sparse - skip empty regions void ExtractJournalRaw(HANDLE hVolume, const WCHAR *outputPath) { // Get journal info for boundaries USN_JOURNAL_DATA_V2 journal; GetJournalInfo(hVolume, &journal); // Open $J stream directly WCHAR jPath[MAX_PATH]; swprintf_s(jPath, L"\\\\.\\%c:\\$Extend\\$UsnJrnl:$J", VolumeLetter); HANDLE hJournal = CreateFileW( jPath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL ); // Journal is sparse; seek to FirstUsn (valid data start) LARGE_INTEGER seekPos; seekPos.QuadPart = journal.FirstUsn; SetFilePointerEx(hJournal, seekPos, NULL, FILE_BEGIN); // Read all records from FirstUsn to NextUsn HANDLE hOutput = CreateFileW(outputPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL); BYTE buffer[1024 * 1024]; // 1MB buffer DWORD bytesRead, bytesWritten; LONGLONG totalBytes = journal.NextUsn - journal.FirstUsn; LONGLONG bytesExtracted = 0; while (ReadFile(hJournal, buffer, sizeof(buffer), &bytesRead, NULL) && bytesRead > 0) { WriteFile(hOutput, buffer, bytesRead, &bytesWritten, NULL); bytesExtracted += bytesRead; if (bytesExtracted >= totalBytes) break; } CloseHandle(hJournal); CloseHandle(hOutput); wprintf(L"Extracted %lld bytes of journal data\n", bytesExtracted);} // Building file timeline from journaltypedef struct _TIMELINE_ENTRY { LARGE_INTEGER Time; WCHAR Filename[256]; DWORD Reason; WCHAR Action[64];} TIMELINE_ENTRY; void BuildTimeline(BYTE *journalData, size_t dataLen, TIMELINE_ENTRY *timeline, int *entryCount) { BYTE *ptr = journalData; BYTE *end = journalData + dataLen; *entryCount = 0; while (ptr < end) { PUSN_RECORD_V2 record = (PUSN_RECORD_V2)ptr; if (record->RecordLength == 0) { ptr += 8; // Skip padding continue; } // Extract record TIMELINE_ENTRY *entry = &timeline[(*entryCount)++]; entry->Time = record->TimeStamp; entry->Reason = record->Reason; // Copy filename memcpy(entry->Filename, (BYTE*)record + record->FileNameOffset, record->FileNameLength); entry->Filename[record->FileNameLength / 2] = L'\0'; // Human-readable action if (record->Reason & USN_REASON_FILE_CREATE) wcscpy_s(entry->Action, 64, L"Created"); else if (record->Reason & USN_REASON_FILE_DELETE) wcscpy_s(entry->Action, 64, L"Deleted"); else if (record->Reason & USN_REASON_RENAME_NEW_NAME) wcscpy_s(entry->Action, 64, L"Renamed To"); else if (record->Reason & USN_REASON_DATA_OVERWRITE) wcscpy_s(entry->Action, 64, L"Modified"); else wcscpy_s(entry->Action, 64, L"Changed"); ptr += record->RecordLength; }}System administrators may need to create, resize, or delete the Change Journal. Understanding management is important for both operations and forensics.
Creating/Enabling the Journal:
By default, Windows creates a Change Journal on system volumes. To create or configure one:
# Using fsutil
fsutil usn createjournal m=33554432 a=4194304 C:
# m = Maximum size (bytes)
# a = Allocation delta (growth increment)
Querying Journal State:
fsutil usn queryjournal C:
Usn Journal ID : 0x01d9a7b3c4e5f600
First Usn : 0x0000000012340000
Next Usn : 0x0000000015678000
Lowest Valid Usn : 0x0000000012340000
Max Usn : 0x7fffffffffffffff
Maximum Size : 0x0000000002000000 (32 MB)
Allocation Delta : 0x0000000000100000 (1 MB)
Deleting the Journal:
# Delete and wait for completion
fsutil usn deletejournal /d C:
# Delete without waiting (asynchronous)
fsutil usn deletejournal /n C:
Resizing the Journal:
To change journal size, delete and recreate:
fsutil usn deletejournal /d C:
fsutil usn createjournal m=67108864 a=8388608 C:
Deleting or disabling the Change Journal:
• Breaks Windows Search indexing • Disables efficient incremental backup • Disrupts DFS Replication • Destroys forensic evidence
Malicious actors may attempt to delete the journal to cover their tracks. Monitor for FSCTL_DELETE_USN_JOURNAL calls as a potential indicator of compromise.
Journal Size Considerations:
| Scenario | Recommended Size | Notes |
|---|---|---|
| Workstation | 32 MB | Default, adequate for typical use |
| File Server | 256-512 MB | Many changes, longer retention |
| Security Monitoring | 1+ GB | Maximum forensic retention |
| Limited Storage | 16 MB | Minimal, may lose records frequently |
Monitoring Journal Health:
Check for journal problems:
# Check if journal exists
fsutil usn queryjournal C:
# If error 0x00000001: journal doesn't exist
# If FirstUsn advances rapidly: journal rolling over
Programmatic Control:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
// Create/modify journaltypedef struct _CREATE_USN_JOURNAL_DATA { DWORDLONG MaximumSize; DWORDLONG AllocationDelta;} CREATE_USN_JOURNAL_DATA; BOOL CreateJournal(HANDLE hVolume, DWORDLONG maxSize, DWORDLONG delta) { CREATE_USN_JOURNAL_DATA createData; createData.MaximumSize = maxSize; createData.AllocationDelta = delta; DWORD bytesReturned; return DeviceIoControl( hVolume, FSCTL_CREATE_USN_JOURNAL, &createData, sizeof(createData), NULL, 0, &bytesReturned, NULL );} // Delete journaltypedef struct _DELETE_USN_JOURNAL_DATA { DWORDLONG UsnJournalID; DWORD DeleteFlags; // USN_DELETE_FLAG_DELETE, USN_DELETE_FLAG_NOTIFY} DELETE_USN_JOURNAL_DATA; #define USN_DELETE_FLAG_DELETE 0x00000001#define USN_DELETE_FLAG_NOTIFY 0x00000002 BOOL DeleteJournal(HANDLE hVolume, DWORDLONG journalId) { DELETE_USN_JOURNAL_DATA deleteData; deleteData.UsnJournalID = journalId; deleteData.DeleteFlags = USN_DELETE_FLAG_DELETE | USN_DELETE_FLAG_NOTIFY; return DeviceIoControl( hVolume, FSCTL_DELETE_USN_JOURNAL, &deleteData, sizeof(deleteData), NULL, 0, NULL, NULL );} // Monitor journal for anti-forensics detectionBOOL IsJournalBeingAttacked(HANDLE hVolume, DWORDLONG expectedId) { USN_JOURNAL_DATA_V2 journal; if (!GetJournalInfo(hVolume, &journal)) { // Journal might be deleted return TRUE; } if (journal.UsnJournalID != expectedId) { // Journal was recreated return TRUE; } // Check for unusual journal shrinkage static USN lastFirstUsn = 0; if (lastFirstUsn != 0 && journal.FirstUsn > lastFirstUsn + (1024 * 1024)) { // Large jump in FirstUsn - rapid record purging return TRUE; } lastFirstUsn = journal.FirstUsn; return FALSE;}While powerful, the Change Journal has limitations that developers and administrators must understand.
Key Limitations:
No Data Content: The journal records WHAT changed, not the actual data. You can see that a file was modified, but not what the modification was.
Circular Buffer: Old records are discarded when the journal reaches its size limit. There's no way to prevent this short of increasing journal size.
Per-Volume Only: Each volume has its own journal. Cross-volume operations aren't linked.
Path Resolution: Records contain file references, not paths. Resolving paths requires MFT access and may fail for deleted files.
Journal Recreation: Deleting and recreating the journal invalidates all saved USN positions.
No Network Support: The journal is local only. Network file operations on remote volumes aren't logged locally.
Privilege Required: Reading the journal requires administrative privileges or backup privilege.
For monitoring specific directories (not entire volumes), ReadDirectoryChangesW may be simpler. It provides real-time notifications for a directory tree without requiring administrative privileges. However, it:
• Only monitors specific directories • Can miss changes during high activity • Doesn't persist across reboots • Consumes a kernel buffer per watch
The Change Journal is preferred for volume-wide, reliable, retrospective monitoring.
We've explored the NTFS Change Journal in depth—one of the most powerful features for file system monitoring and forensic analysis. Let's consolidate our understanding:
What's Next:
Now that we understand Change Journal and file metadata storage, we'll examine NTFS Permissions—the comprehensive access control system that makes NTFS suitable for enterprise environments. We'll explore security descriptors, DACLs, SACLs, permission inheritance, and how Windows enforces file access control.
You now understand the NTFS Change Journal—its architecture, record formats, querying methods, and practical applications. You can build monitoring applications, conduct forensic analysis, and manage journal configuration. Next, we'll explore NTFS permissions.