Получение информации из журнала 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.
Автор: амдф
Дата: 22.12.2012
Избранное
Остальное
По вопросам сотрудничества и другим вопросам по работе сайта пишите на cleogroup[собака]yandex.ru