hex.pp.ua

Журнал изменений файлов 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 журнала
система комментирования CACKLE

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


Разделы сайта
Главная
Блог
Native API
NTFS и ReFS
Микроконтроллеры
Справочник NTDLL
Коды NTSTATUS
Разное

Избранное
NTFS Stream Explorer
Native Shell
Тенгвар

Остальное
nvpnhcknn (архив)
English pages
Контакты

Ленты atom
Лента Atom сайта Лента Atom блога



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