Получение информации из журнала 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