Атрибуты
После нахождения записи mft, самой важной задачей является поиск необходимого атрибута, например с данными. Атрибуты бывают двух видов: резидентные (resident) и нерезидентные (nonresident). Резидентный атрибут умещается в записи MFT, а нерезидентный нет. У обоих атрибутов есть общий заголовок, куда входят поля типа, длины, и пр. и далее свой собственный заголовок (либо для резидентного либо для нерезидентного). Атрибуты идентифицируются типом, но также могут быть и именованными, если поле name_length не нулевое. В общем, заголовок обоих атрибутов описывается структурой.
typedef struct _ATTR_RECORD { /*0x00*/ ATTR_TYPES type; //тип атрибута /*0x04*/ USHORT length; //длина заголовка; используется для перехода к //следующему атрибуту /*0x06*/ USHORT Reserved; /*0x08*/ UCHAR non_resident; //1 если атрибут нерезидентный, 0 - резидентный /*0x09*/ UCHAR name_length; //длина имени атрибута, в символах /*0x0A*/ USHORT name_offset; //смещение имени атрибута, относительно заголовка //атрибута /*0x0C*/ USHORT flags; //флаги, перечислены в ATTR_FLAGS /*0x0E*/ USHORT instance;
union { //Резидентный атрибут struct { /*0x10*/ ULONG value_length; //размер, в байтах, тела атрибута /*0x14*/ USHORT value_offset; //байтовое смещение тела, относительно заголовка //атрибута /*0x16*/ UCHAR resident_flags; //флаги, перечислены в RESIDENT_ATTR_FLAGS /*0x17*/ UCHAR reserved; } r; //Нерезидентный атрибут struct { /*0x10*/ ULARGE_INTEGER lowest_vcn; /*0x18*/ ULARGE_INTEGER highest_vcn; /*0x20*/ USHORT mapping_pairs_offset;//смещение списка отрезков /*0x22*/ UCHAR compression_unit; /*0x23*/ UCHAR reserved1[5]; /*0x28*/ ULARGE_INTEGER allocated_size; //размер дискового пространства, //которое было выделено под тело //атрибута /*0x30*/ ULARGE_INTEGER data_size; //реальный размер атрибута /*0x38*/ ULARGE_INTEGER initialized_size; } nr; } u; } ATTR_RECORD, *PATTR_RECORD;
Флаги атрибута описываются структурой. typedef enum { ATTR_IS_COMPRESSED = 0x1, //атрибут сжат (compressed) ATTR_IS_ENCRYPTED = 0x4000, //атрибут зашифрован (encrypted) ATTR_IS_SPARSE = 0x8000 //атрибут разрежен (sparse) } ATTR_FLAGS;
Важные типы атрибутов. typedef enum { AT_STANDARD_INFORMATION = 0x10, AT_ATTRIBUTE_LIST = 0x20, AT_FILE_NAME = 0x30, AT_OBJECT_ID = 0x40, AT_SECURITY_DESCRIPTOR = 0x50, AT_VOLUME_NAME = 0x60, AT_VOLUME_INFORMATION = 0x70, AT_DATA = 0x80, AT_INDEX_ROOT = 0x90, AT_INDEX_ALLOCATION = 0xa0, AT_BITMAP = 0xb0, AT_REPARSE_POINT = 0xc0, AT_END = 0xffffffff } ATTR_TYPES;
В заголовке MFT записи хранится байтовое смещение первого атрибута, относительно самой записи. Прибавляя это смещение к смещению записи, мы получим смещение первого атрибута. Для перехода к следующему элементу нужно прибавить к этому смещению значение поля length заголовка и так для всех последующих заголовков. Концом списка считается значение AT_END, считанное с начала атрибута. Важное замечание: в исходниках Linux NTFS, поле length имеет тип ULONG, однако при исследовании выяснилось, что для корректного обхода атрибутов следует сделать это поле как USHORT, а следующее за ним просто зарезервировать; бывают ситуации, когда в этих старших байтах хранился «мусор». К тому же этих двух байт вполне хватит для адресации смещения внутри MFT записи, размер которой обычно составляет 1 или 4 КБ.
Важное значение имеют флаги атрибутов, которые перечислены в ATTR_FLAGS. Они содержат информацию о характеристиках данного атрибута, например, является ли он зашифрованным или сжатым.
Если все атрибуты для файла не вмещаются в одну MFT запись, тогда для файла создаются расширенные записи (extra records). В таком случае основная (первичная) запись называется базовой и хранит атрибут $ATTRIBUTE_LIST, в котором хранятся ссылки на расширенные файловые записи.
Пользовательский файл обычно содержит атрибуты: $STANDART_INFORMATION – информация о файле (время создания, атрибуты), $FILE_NAME – имя файла, $SECURITY_DESCRIPTOR – дескриптор защиты, $DATA – данные. Описание этих атрибутов, а также их структуры можно найти у Кэрриэ в «Криминалистическом анализе файловых систем».
Для исследования NTFS на диске лучше всего использовать Runtime DiskExplorer for NTFS, поскольку она показывает очень много полезной информации и подробно структуры самой ФС.
Рис. 1. Список атрибутов и файлов у корневого каталога.
Резидентные атрибуты. Как уже упоминалось, такие атрибуты хранят свое тело в записи MFT и для них флаг non_resident в заголовке установлен в ноль. Для считывания данных такого атрибута достаточно определить смещение тела как сумму смещений заголовка атрибута и поля r.value_offset, а затем считать r.value_length байт в память.
Нерезидентные атрибуты. Для таких атрибутов флаг non_resident установлен в 1 и их тела хранятся в отдельных кластерах, на которые указывают отрезки. Отрезок (run) хранит цепочки кластеров, в которых находится содержимое атрибута. Массив отрезков называется списком отрезков (run list). Если атрибут имеет один отрезок, то он не фрагментирован и, соответственно, все кластеры, которые содержат данные являются смежными. Смещение списка отрезков определяется суммой смещения заголовка атрибута и поля nr.mapping_pairs_offset.
Отрезки находятся в сжатом виде и содержат сопоставления LCN-VCN для кластеров. Набор отрезков, описывающих кластеры для атрибута называется списком отрезков (run list). Поле mapping_pairs_offset в заголовке нерезидентного атрибута содержит смещение списка отрезков от начала заголовка атрибута. Фактически, список отрезков это массив структур переменного размера. Размер каждого из полей структуры указывается в предыдущем байте. Первый элемент структуры содержит размер отрезка в кластерах, а второй номер кластера. Байт, описывающий размеры полей размера и номера кластеров условимся называть байтом длин. Младший полубайт байта длин содержит длину поля размера, а старший длину поля номера кластера. Данные в полях структуры хранятся в формате Intel, т. е. младший байт по младшему адресу.
Рассмотрим пример. Нерезидентный атрибут имеет список отрезков вида. 32 90 3A 00 00 0C | 32 30 0F DA A7 1B | 32 A0 36 5E 89 05 | 00
Первый байт – байт длин описывает длины полей первого отрезка. Младший полубайт равен 2, значит на поле длины приходится два байта и длина отрезка равна 0x3A90.
Далее, старший полубайт байта длин равен трем и стартовый кластер равен 0xC0000. Получаем первый отрезок начинается с кластера 0xC0000 и заканчивается границей 0xC0000 + 0x3A90 = C3A90. Для перехода к следующему элементу следует прибавить размеры полей, и единицу для байта длин, т. е. 3 + 2 + 1. Для второго отрезка по байту длин видим, что размер полей такой же как в предыдущем отрезке, т. е. младший полубайт равен двойке, значит размер поля длины два и равен 0xF30, старший полубайт равен трем и стартовый VCN равен 0x1BA7DA. Для преобразования VCN в LCN, складываем первый LCN - 0xC0000 и VCN - 0x1BA7DA. Получаем 0xC0000 + 0x1BA7DA = 0x27A7DA. Получаем, второй отрезок начинается с кластера 0x27A7DA и продолжается до 0x27A7DA+0xF30 = 0x27B70A. Для перехода к следующему отрезку добавляем размеры полей плюс единицу для самого байта длин. Третий отрезок начинается с байта длин - 32 и размер в кластерах отрезка равен 0x36A0, VCN равен 0x5895E. Для преобразования VCN-LCN складываем предыдущий стартовый LCN с данным VCN, т. е. 0x27A7DA + 0x5895E = 0x2D3138. Получаем третий отрезок 0x2D3138 - 2D67D8. Следующий байт за отрезком нулевой, следовательно список отрезков закончен. Итак исходный атрибут размещается в кластерах 0xC0000 – 0xC3A90, 0x27A7DA - 0x27B70A, 0x2D3138 – 0x2D67D8 (не включая последний кластеры).
На практике возможны «нестандартные» цепочки кластеров, когда вышеприведенное правило сопоставления относительных смещений в LCN работать не будет, поэтому следует после распаковки списка отрезков сверять полученный из отрезков размер файла с тем, который указывается в заголовке нерезидентного атрибута. В «нормальном» случае эти значения должны совпадать. Кроме того следует выполнять проверку не является ли значение длины этого отрезка больше самого смещения этого отрезка. Если это так, то от распаковки такого отрезка лучше отказаться. Также таким способом нельзя распаковать отрезки разряженного файла.
Например, следующая функция из загрузчика ReactOS для NTFS распаковывает отрезок.
PUCHAR // возвращает указатель на следующий отрезок в списке NtfsDecodeRun( PUCHAR DataRun, //на входе, указатель на отрезок для распаковки LONGLONG *DataRunOffset, //на выходе, распакованное значение кластерного смещения ULONGLONG *DataRunLength //на выходе, распакованное значение числа кластеров ) { UCHAR DataRunOffsetSize; //размер поля смещения UCHAR DataRunLengthSize; //размер поля длины CHAR i;
//из старшего полубайта считаем размер поля смещения DataRunOffsetSize = (*DataRun >> 4) & 0xF; //из младшего размер поля смещения DataRunLengthSize = *DataRun & 0xF;
*DataRunOffset = 0; *DataRunLength = 0;
//указатель на сами данные DataRun++;
//цикл распаковки длины отрезка, с каждой итерацией значение сдвигается на i-байт и //прибавляется с длиной for (i = 0; i < DataRunLengthSize; i++) { *DataRunLength += *DataRun << (i << 3); DataRun++; }
/* NTFS 3+ sparse files, если файл разряжен */ if (DataRunOffsetSize == 0) { *DataRunOffset = -1; } else { //цикл распаковки смещения for (i = 0; i < DataRunOffsetSize - 1; i++) { *DataRunOffset += *DataRun << (i << 3); DataRun++; } //последний байт может быть знаковым, поэтому он обрабатывается отдельно *DataRunOffset = ((CHAR)(*(DataRun++)) << (i << 3)) + *DataRunOffset; }
//возвращаем указатель на следующий отрезок return DataRun; }
Рис. 2. DiskExplorer for NTFS показывает список отрезков для атрибута $BITMAP у файла $MFT. Кроме того, отображает другую полезную информацию об атрибутах, включая длину, резидентен или нет.
Рис. 3. У файла $MFT атрибут дата имеет всего один отрезок, что свидетельствует о том, что он не фрагментирован и его можно индексировать как обычный массив. Данные атрибута начинаются с кластера 0xC0000 и имеют длину 0xE7A8 кластеров.