Журнал изменений файлов USN

Получение информации из журнала USN на NTFS и ReFS




В файловых системах NTFS и ReFS есть журнал изменений, который называется USN. Как только какой-то файл изменился, в журнал пишется информация об этом. Эту информацию можно из журнала извлечь. Журнал не бесконечный, количество записей в нём ограничено. Поэтому для какого-то конкретного файла записи в журнале может и не оказаться, например, если эта запись уже оказалась затёрта более новыми записями. Если файлов на локальном томе много и они постоянно изменяются, то записи в журнале будут жить не очень долго.

Поддерживается ли журналирование

Итак, допустим, есть произвольный файл. Стоит задача залезть в журнал изменений USN и вынуть оттуда информацию о последних производившихся с файлом операциях. Файл должен располагаться на файловой системе NTFS или ReFS. Эти файловые системы являются журналируемыми. Сначала в Windows только NTFS поддерживала журналирование. Но теперь таких файловых систем две. Вдруг в будущем появятся новые файловые системы с журналом USN? Поэтому, для определения того, есть ли вообще журнал USN или его нет на локальном томе, будем не сравнивать имена файловых систем, а будем смотреть флаги возможностей файловой системы. Какой бы она ни была, старой или новой, флаг покажет нам, поддерживает ли данная файловая система журнал USN или нет. Делается это так:

  // Проверка возможностей файловой системы до каких-либо запросов
  WCHAR sVolume[] = L"C:\\";
  DWORD dwVolumeSerialNumber, dwMaximumComponentLength;
  LPTSTR lpVolumeNameBuffer = (LPTSTR)HeapAlloc(GetProcessHeap(), 
    HEAP_ZERO_MEMORY, (MAX_PATH + 1) * sizeof(WCHAR));
  LPTSTR lpFileSystemNameBuffer = (LPTSTR)HeapAlloc(GetProcessHeap(), 
    HEAP_ZERO_MEMORY, (MAX_PATH + 1) * sizeof(WCHAR));
  
  GetVolumeInformation(
    sVolume,
    lpVolumeNameBuffer,
    MAX_PATH + 1,
    &dwVolumeSerialNumber,
    &dwMaximumComponentLength,
    &dwFsFlags,
    lpFileSystemNameBuffer,
    MAX_PATH + 1
  );

После этого вызова смотрим имя файловой системы в lpFileSystemNameBuffer, а также флаги её возможностей в dwFsFlags. Конкретно наличие журналирования проверяется так:

	if (dwFsFlags & FILE_SUPPORTS_USN_JOURNAL)
	{
		// поддерживает
	}

Выяснение версии журнала USN

Существует две актуальные версии журнала USN - версия 2.0 и версия 3.0. Последняя отличается тем, что в ней 128-битные идентификаторы файлов. Под эти две версии журнала используются разные структуры данных, отличающиеся размером. По-умолчанию, если не указывать специально, будут возвращаться структуры для версии 2.0. Но я покажу, как получать и данные новой версии, если эта версия поддерживается с системе.

Версия журнала USN 3.0 поддерживается в файловой системе ReFS. Эта файловая система на сегодняшний день присутствует только в операционной системе Windows 2012 Server. Поэтому, чтобы запрашивать данные версии 3.0, нужно прежде выяснить, что программа выполняется под управлением именно этой операционной системы, или более новой.

Подготавливаем два параметра, pReadUSN и uReadUSNSize, для последующего использования с вызовом FSCTL_READ_FILE_USN_DATA. Либо pReadUSN указывает на буфер с типом READ_FILE_USN_DATA, либо он указывает на NULL.

READ_FILE_USN_DATA rfud;
PREAD_FILE_USN_DATA pReadUSN;
DWORD uReadUSNSize;

OSVERSIONINFOEX osvi;
ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX));
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
GetVersionEx((OSVERSIONINFO*) &osvi);

// Windows 2012 Server или выше, USN версия 3.0
if ( osvi.dwMajorVersion >= 6 && osvi.dwMinorVersion >= 2 
  && osvi.wProductType != VER_NT_WORKSTATION)
{
	rfud.MinMajorVersion = 2;
	rfud.MaxMajorVersion = 3;
	pReadUSN = &rfud;
	uReadUSNSize = sizeof(rfud);
} else
{ // Другие версии, USN версии 2.0
	pReadUSN = NULL;
	uReadUSNSize = 0;
}

Получение номера USN-записи по хэндлу файла

Чтобы извлечь данные из USN-журнала о файле, нужно иметь номера записи об этом файле. Получить его можно с помощью вызова FSCTL_READ_FILE_USN_DATA. Номер извлекается из структуры, которая имеет две разные версии, которые имеют разный размер, это нужно учитывать. Чтобы учесть это, из начала структуры USN_RECORD нужно прочитать её версию, и только потом обрабатывать данные из неё.

4 Кб это достаточный размер для сохранения USN-информации об одном файле. Даже 1 Кб будет достаточно.

#define USN_SIZE 4096
CHAR usn_buf[USN_SIZE]={0};
PUSN_RECORD_V2 urv2 = (PUSN_RECORD_V2)&usn_buf;
PUSN_RECORD_V3 urv3 = (PUSN_RECORD_V3)&usn_buf;
UINT64 uUsnNumber;
ULONG uLength;

// на этом этапе получаем номер USN файла (urv2->Usn)
DeviceIoControl(hFile, FSCTL_READ_FILE_USN_DATA, pReadUSN, uReadUSNSize,
										urv3, USN_SIZE, &uLength, NULL );

// в зависимости от того, был ли вызов 2.0 или 3.0, получить USN номер файла
if (urv2->MajorVersion == 2)
{
	uUsnNumber = urv2->Usn;
} else
{
	uUsnNumber = urv3->Usn;
}

Теперь в uUsnNumber у нас есть номер USN-записи. Предполагаем, что она верна, хотя это может быть не так. Это может быть номер записи, которая уже затёрта в журнале другими записями. Под этим номером уже может скрываться запись совершенно о другом файле. Или это может быть номер записи в предыдущем журнале USN, который удаляли и пересоздали. Поэтому, после получения информации из журнала USN, нужно проверить, действительно ли информация оттуда относится к нашему файлу. Делается это сравнением идентификаторов файла. Идентификатор файла нужно предварительно получить (как это сделать, описано здесь).

Также, следующие запросы будут к хэндлу тома, а не файла. Нужно получить хэндл тома (hVolume), на котором расположен файл.

Получаем идентификатор USN-журнала

urd - структура READ_USN_JOURNAL_DATA может быть версии 0 или версии 1. Но отличаются они только двумя дополнительными элементами в конце структуры в версии 1. Поэтому буфер для обоих структур у нас один и тот же, а отличаться будет только размер структуры, который мы передаём в вызове (urd_size). Эти два дополнительных элемента инициализируем как 2 и 3, то есть вызов DeviceIoControl потом нам может вернуть информацию об USN либо версии 2.0, либо 3.0.

Делаем вызов FSCTL_QUERY_USN_JOURNAL, чтобы получить идентификатор текущего USN журнала тома, для того, чтобы потом сделать ещё один, последний вызов, с этим параметром. В итоге, в переменную urd типа READ_USN_JOURNAL_DATA пишется ранее полученный USN-номер записи журнала, и полученный на этом этапе идентификатор журнала.

USN_JOURNAL_DATA_V0 ujd0 = {0};
READ_USN_JOURNAL_DATA_V1 urd = {0, 0xFFFFFFFF, FALSE, 0, 0, 0, 2, 3};
DWORD urd_size;

// На этом этапе получаем ID журнала USN тома
DeviceIoControl(hVolume, FSCTL_QUERY_USN_JOURNAL, NULL, 0,
	&ujd0, sizeof(ujd0), &uLength, NULL );

urd.UsnJournalID = ujd0.UsnJournalID;
urd.StartUsn = uUsnNumber;

// Windows 2012 Server или выше, USN версия 3.0
if ( osvi.dwMajorVersion >= 6 && osvi.dwMinorVersion >= 2 
  && osvi.wProductType != VER_NT_WORKSTATION)
{
	urd_size = sizeof(READ_USN_JOURNAL_DATA_V1);
} else
{
	urd_size = sizeof(READ_USN_JOURNAL_DATA_V0);
}

Получаем запись из журнала USN

Теперь, когда сформирована структура READ_USN_JOURNAL_DATA, можно сделать последний вызов, который извлечёт информацию из журнала. Делает это вызов FSCTL_READ_USN_JOURNAL.

DeviceIoControl(hVolume, FSCTL_READ_USN_JOURNAL, &urd, urd_size,
					&usn_buf, USN_SIZE, &uLength, NULL );
          
// Так и не понял, зачем это, но первые 8 байт заняты 
// чем-то другим, а не структурой USN_RECORD:
urv2 = (PUSN_RECORD_V2)&usn_buf[8];
urv3 = (PUSN_RECORD_V3)&usn_buf[8];

if (urv2->MajorVersion == 2)
{
  // Обрабатываем как запись версии 2.0  
} else
if (urv3->MajorVersion == 3)
{
  // Обрабатываем как запись версии 3.0
}

В общем, в результате вызова, оба наших указателя urv2, и urv3 указывают на буфер, где располагается то ли структура USN_RECORD_V2, то ли USN_RECORD_V3. Нужно прочитать поле MajorVersion (по любому указателю), и затем, соответственно, использовать один из двух указателей соответствующей версии для получения информации об USN-записи.

Сравнение идентификаторов из USN v2.0

Но ещё нужно проверить, правильную ли запись мы получили, относится ли она к нашему файлу, а не к другому. Нужно сравнить идентификаторы. Этот код отличается в версии 2.0 и 3.0, так как размер идентификаторов разный. В версии USN 2.0 сравнение выглядит так:

LARGE_INTEGER fi = GetFileId(hFile);

if (urv2->FileReferenceNumber == (UINT64)fi.QuadPart)
{
  // правильная запись
}

См. код функции GetFileId() тут.

Сравнение идентификаторов из USN v3.0

В версии USN 3.0 сравнение выглядит так:

FILE_ID_INFO fii = {0};
EXT_FILE_ID_128 id_compare;
//128-битный ИД
GetFileIdEx(hFile, &fii);
memcpy(&id_compare, urv3->FileReferenceNumber, sizeof(EXT_FILE_ID_128));

if ((fii.FileId.LowPart == id_compare.LowPart)
 && (fii.FileId.HighPart == id_compare.HighPart))
{
  // правильная запись
}

См. код функции GetFileIdEx() тут.

Значение полей записей из журнала USN

В MSDN описана структура USN_RECORD. Там и смотрите описание полей структуры. Самые интересные поля это Reason и SourceInfo. Они позволяют узнать, какие именно действия привели к созданию записи в журнале USN. То есть позволяют выяснить, а что, собственно, изменилось в файле?

Казалось бы, самый первый вызов FSCTL_READ_FILE_USN_DATA и так возвращает структуру USN_RECORD, зачем остальные вызовы? А затем, что при вызове FSCTL_READ_FILE_USN_DATA в полученной таким образом структуре USN_RECORD поля Reason и SourceInfo всегда равны 0. Об этом даже в MSDN написано. Вот поэтому, одиночный вызов FSCTL_READ_FILE_USN_DATA не имеет никакого смысла. Он ничего полезного не сообщает о файле, кроме номера записи USN. Ведь все остальные поля структуры, кроме Reason и SourceInfo можно получить и более традиционными способами.

Моя программа NTFS Stream Explorer поддерживает просмотр данных USN.

Вкладка «USN»
Информация из USN журнала


Автор: амдф
Дата: 22.12.2012


При копировании материалов хорошим тоном будет указание авторства и ссылка на сайт.