В системе есть подмножество защищённых файлов, которые нельзя скопировать для анализа с рабочей машины. Они открываются\создаются функцией ядра
Содержание:
1. Основная идея
При проектировании ОС особое внимание уделяется безопасности. Подсистема Security контролирует все наши действия, с момента входа в окне Winlogon и вплоть до полного выключения машины. Удостоверением личности является здесь Token, который вручает нам сторож LSASS - именно в токене перечисляются наши права и привилегии.
Когда мы создаём объекты ядра от своего имени (а это файлы, процессы, и многое другое), они наследуют наши права. Если объект уже был открыт кем-то до нас, в его дескрипторе безопасности окажутся права родителя, а потому системный монитор SRM (Security Reference Monitor) своей api
В примере ниже моя попытка открыть файл-подкачки Pagefile.sys на безобидное чтение (без записи), но поскольку Win уже эксклюзивно открыла его до меня Shared=0, монитор отвергает мой запрос с кодом "ERROR_SHARING_VIOLATION=20h", хоть и зашёл я в систему как админ. Чтобы расшифровать код ошибки на понятный язык, в исходнике предусмотрена универсальная процедура с функцией
Ну не может и ладно.. Не пускают нас в дверь - тогда зайдём с форточки.
Суть в том, что если файл закрыт на амбарный замок, то корневой раздел самого диска открыт всегда, хотя и требует для доступа прав админа. Другими словами, чтобы прочитать абсолютно любой имеющийся на диске файл, мы должны обратиться не напрямую к файлу, а к разделу диска например
При работе с разделами накопителя мин.порцией обмена является сектор в 512-байт, и что важно, для наших целей требуется отключить буферизацию в кэш, которая включена по умолчанию. Механизм кэширования позволяет копить все изменения файла в специальном буфере, чтобы при каждом чихе не обращаться к относительно медленному харду. Диспетчер кэша сбрасывает буфер на диск в трёх случаях: по 8-сек таймеру (отсчёт начинается после момента записи), при переполнении буфера, или по нашему желанию в произвольный момент функцией
Таким образом, запрос на открытие корневого раздела диска должен выглядеть так, в результате чего система удовлетворит его:
Функция
Но здесь интересно другое. Как оказалось, NTFS знает, какие кластеры занимает файл с именем(X) на диске, а вот обратную связь вычислить уже не способна. То-есть она не в состоянии определить имя файла по номеру кластера! Этот нюанс позволит нам обходить системный монитор безопасности SRM, который стоит как злой Цербер на страже файлов. Хотя штатная утилита Win
2. Штатный механизм работы с файлами
На рис.ниже представлена взаимосвязь структур при открытии процессом файлов. Все они собираются в таблицу дескрипторов Handle, на которую указывает поле "ObjectTable" в родительской структуре ядра EPROCESS. Отметим, что в этой таблице лежат дескрипторы не только файлов, но и прочих объектов ядра, например исполняемых потоков Thread, открытых ALPC-портов, ключей реестра Key, объектов синхронизации типа Mutant\Event\Mutex, различных устройств, и прочее. То-есть это всё, что открыл или создал данный процесс. Если дескриптор олицетворяет файл на диске, он будет указывать на структуру FILE_OBJECT.
Расширение
Так мы получим адрес одного из файловых объектов процесса, и можно вскормить его расширению
Файловая система привязывает к каждому открытому файлу свою структуру FCB, (file control block). Если 2 разных процесса открывают один общий файл, то FCB для них так-же будет общей, а вот файловых объектов диспетчер в/в создаст уже два, и поле "FsContext" каждого из них будет смотреть на эту общую FCB.
Здесь важно понять, что ядро ОС хранит атрибуты доступа к файлу в структуре FILE_OBJECT, в то время как эти-же атрибуты NTFS прячет в собственную свою FCB. Это потому, что файловая система живёт своей жизнью, ведь диск может быть отформатирован не только в NTFS, но и в FAT32 или вообще ExFAT64. Фактически атрибуты в FCB имеют приоритет перед системными, ведь только на этом уровне возможен физ.контроль общего доступа к файлам.
К сожалению NTFS не документирована, т.е. на неё нет официальной спецификации от Microsoft, а потому все принадлежащие ей структуры являются внутренними, и не разглашаются в публичных символах отладки *.pdb драйвера Ntfs.sys. Как результат, даже WinDbg на запрос "покажи fcb" отвечает "не найдено", хотя заголовок Header отображает без проблем, т.к. им оперирует диспетчер в/в ядра, а не Ntfs.
3. Структура файловой системы NTFS - таблица и метафайлы
NTFS v1.0 родилась на свет в 1993 году, далее v2 как сквозь землю провалилась (ничего о ней не известно), в нулевых появилась сразу третья версия для Win2k, и наконец с 2001 года нашими хардами погоняет v3.1, которая и считается актуальной по сей день. За это время из неё было удалено много лишнего, а ещё больше добавлено, например поддержка шифрования Encrypt, сжатия данных Compressed, отказ от хранения на диске больших массивов нулей Sparse, отдельная база дескрипторов безопасности в виде метафайла $Secure, а так-же индексация записей FileRecord для быстрого поиска.
Архитектурно состоит из 11-ти функциональных модулей, которые принято называть метафайлами. Имена их начинаются со-знака($), а номера кластеров на диске хранятся в глобальной базе MFT (master file table). У каждого раздела\тома диска своя база MFT, поэтому в терминологию было введено такое понятие как LCN (logical cluster number). Отсчёт LCN ведётся с нуля относительно начала раздела, а не самого диска. Например LCN(0) разделов
Помимо логических, в структурах файловой записи используются ещё и вирт.кластеры VCN - это смещение от начала файла. Такой подход сокращает разрядность указателей на кластеры, и вместо 64-битных значений можно использовать 32-битный дворд. Кластер - это мин.единица данных в NTFS, в то время как на физ.уровне диск и все системные WinAPI оперируют исключительно 512-байтными секторами с адресацией LBA (logical block address).
При работе с NTFS, в качестве отправной точки используется загрузочный сектор раздела, который числится в штате как метафайл $Boot. То-есть открываем раздел функцией
Как только мы узнаем номер кластера MFT, остальное уже дело техники.
MFT - это централизированная база буквально всех файлов на данном разделе диска, в том числе и самих метафайлов. База состоит из массива структур FILE_RECORD размером 1024 байт каждая - одна структура описывает отдельно взятый файл на диске. Таким образом, размер базы MFT напрямую зависит от кол-ва файлов.
Под свои метафайлы, NTFS резервирует первые 12 записей FileRecord, и располагаются они в строго определённом порядке, как указано ниже. После служебных файлов идёт зарезервированное свободное пространство, а пользовательские файлы начинаются в MFT с индекса
Когда диск отформатирован в NTFS, файл может иметь до 16-ти внутренних атрибутов. Не нужно путать их с атрибутами типа: скрытый, сжатый, системный, и прочие. Например сведения об основном потоке данных хранится в атрибуте $DATA, хотя NTFS поддерживает ещё и скрытые от глаз альтернативные потоки ADS (alternate data stream), которые будет описывать расширенный атрибут $EA. В свою очередь имя файла прописывается в атрибут $FILE_NAME, а общие сведения в $STANDARD_INFO.
В записи FileRecord не обязательно должны присутствовать буквально все перечисленные выше атрибуты от 1 до 16, однако в минимальной комплектации три выделенных зелёным атрибута с идентификаторами типа
4. Основная база данных $MFT
Так, мелкими шагами мы вплотную подошли к тому, чтобы утянуть любой системный файл с диска без указания его имени, ведь иначе сторожа Win не хотят нам предоставлять к нему легальный доступ. На данном этапе имеет смысл скачать крутой двоичный редактор WinHEX, который из коробки имеет всё необходимое для понимания столь сложной темы. В частности, по своим готовым шаблонам он может отображать содержимое метафайлов $Boot, $MFT, структур FileRecord, и многое другое. В подвале статьи есть ссылки для скачивания.
Для начала посмотрим на формат структуры FileRecord - нам интересны всего 2 выделенных плюсом поля: смещение в байтах к первому Ntfs-атрибуту, а так-же флаги, что позволит искать (или наоборот пропускать) кластеры удалённых с диска файлов:
Прочитав линк на первый атрибут по смещению
Если содержимое атрибута полностью помещается в одну 1024-байтную запись FileRecord, его называют "Резидентным". Но для некоторых атрибутов этого пространства явно не достаточно, тогда тело его выносится за пределы FileRecord, а внутри записи остаётся лишь ссылка на внешний кластер - такие атрибуты считаются "Не резидентными". Ярким представителем нерезидентого является $DATA, поскольку в качестве его тела выступает собственно содержимое файла, например видео, картинка, исполяемый файл, и прочее. Посмотрите ещё раз на таблицу атрибутов выше - в столбце флагов указывается как-раз тип атрибута, резидентный он или нет.
Все 16 атрибутов имеют одинаковый для всех заголовок
К сожалению переменный размер заголовков заставляет нас ипользовать 3 структуры вместо 1, зато реализовать программный доступ к NTFS теперь намного проще. Вот формат первых двух атрибутов, которые потребуются нам в практической части, при этом в стандартном
А вот реальный дамп из WinHEX, где каждый атрибут я выделил в блок. Как и указано в структуре статического заголовка выше, первый дворд хранит тип атрибута, а во втором лежит его размер. Таким образом, прибавляя этот размер к текущему адресу, мы попадаем в следующий и далее, пока не встретим маркер последнего
4.1. Атрибут $DATA - полезная нагрузка файла
И наконец рассмотрим, как получить номера кластеров, чтобы собрать из них готовый к употреблению файл. Для этого нужно разобрать на винтики атрибут $DATA, а точнее его цепочку указателей на кластеры под названием "DataRuns". По большому счёту, это и есть сердце всего механизма хранения данных в NTFS, а потому требует к себе особого внимания (и понимания). Кстати если отбросить все украшательства, разница между NTFS и FAT кроется именно здесь.
Возьмём серый блок из примера выше, и спроецируем на него уже знакомую нам структуру заголовка NON_RESIDENT_HEADER. В первую очередь проверяем, что это точно нерезидент, т.к. если файл размером до 600 байт, его тело может находится прямо внутри атрибута $DATA. В данном случаем по смещению(8) флаг=1, а значит тушка файла лежит где-то снаружи, а где именно - указывает как-раз цепочка DataRuns по смещению(40h).
Обратите внимание, что номер последнего вирт.кластера VCN по смещению(18h), и размер потока оффсет(38h) имеют одинаковые значения, просто в первом случае указано в кластерах, а во-втором в байтах. Формула перевода кластеров в байты будет иметь такой вид:
Записи DataRuns имеют довольной сложный формат, а ещё сложнее реализовать их парсинг программно.
Первый байт называют "InfoByte" и разделён он на 2 тетрады по 4-бита. В младшей кодируется длина в байтах "счётчика последовательности", а в старших - разрядность указателя на кластер LCN в байтах. В данном случае этот байт имеет значение
Здесь повезло, что в цепочке всего одно звено, т.к. следующий инфо-байт после
Более того, начальные кластеры одной цепочки могут быть разбросаны по поверхности диска хаотично, например:
Важно понять, что номера кластеров в DataRuns указываются как смещение относительно предыдущего, т.е. это не абсолютный кластер LCN. На физ.кластер указывает только первое звено цепи, а все остальные нужно будет прибавлять к предыдущему. Такой подход позволяет экономить на разрядности номера кластера в записях Runs. Надеюсь схема ниже внесёт немного ясности в этот кавардак:
Когда файл содержит в себе фрагменты из более 4КБ нулей (1 кластер), NTFS не выделяет под них место на диске, а просто взводит в атрибуте файла флаг "Sparse=0x0200" (разряжённый, см.выше константы FILE_TYPE). При этом в записи DataRun хранится только счётчик забитых нулями кластеров, экономя таким образом свободное пространство диска. На Хабре есть готовая статья с подробным описанием этой фишки, поэтому повторяться не буду.
5. Практическая часть - софт для вычисления цепочки кластеров
Основная проблема здесь в том, чтобы программно вычислить знак числа в DataRuns, ведь значение указателя на кластер может быть произвольным от 1 до 8 байт, а потому нельзя предугадать его заранее. Если-бы разрядность была фиксирована 16\32\64-бита, достаточно было просто чекнуть старший бит
Эту проблему решает первый инфо-байт в записи Run - достаточно умножить старшую его тетраду на 8, как получим разрядность указателя на кластер в битах. Далее, среди инструкций процессоров х86 есть
Я написал небольшую демку, которая ищет первые 4 раздела на дисках, читает их загрузочные секторы в лице метафайла $Boot, и собирает сведения о записях FileRecord самой базы MFT каждого из найденных разделов в отдельности. Обратите внимание на сл.нюансы:
1. Как оказалось, глобальная база MFT на всех томах распологается по фиксированному кластеру 6.291.456.
2. Размер базы MFT напрямую зависит от кол-ва файлов, а не от размера самого тома, и расширяется база динамически.
3. Размеры секторов и кластеров обычно равны 512 и 4096 байт соответственно, но если диск не хард, а NVM, то могут быть больше.
4. Начиная с NTFS v3.1 длина записей FileRecord должна быть всегда 1024 байта. Если у себя вы обнаружите другое значение, плиз дайте мне знать.
5. Диапазон вирт.кластеров VCN - это всегда сумма лог.кластеров LCN, т.к. оба они определяют размер файла на диске в кластерах.
Я специально убрал в этом коде пимпу "Сохранить", чтобы вы реализовали бэкап самостоятельно. Поскольку в окне уже есть номера кластеров в трёх DataRuns, остаётся умножить их на размер одного кластера 4096, чтобы перевести в LBA для функции
По такой-же схеме можно узнать кластеры абсолютно любого файла на диске. В реальной ситуации, для оптимизации поиска сначала копируют в свой буфер всю MFT, и уже в буфере ищут структуру FileRecord нужного файла с шагом в 1024-байта. Иначе потребуются сотни тысяч обращений к медленному харду, и поиск будет проходить со скоростью улитки.
6. Заключение
NTFS можно сравнить с небольшой операционной системой, которая работает внутри Win, имеет своё личное ядро Ntfs.sys, и 16 внешних модулей в виде метафайлов с префиксами($). Она способна восстанавливать файлы при сбоях, для чего в штате числится журнал $LogFile. Суть в том, что перед тем-как произвести над файлом какое-либо действие, ядро NTFS записывает это действие сначала в журнал, и лишь потом меняет непосредственно файл. Поэтому если в момент правки отключится питание в сети и т.п., всегда будет возможность восстановить оригинал из лога. В общем говорить о ней можно бесконечно, а потому лучше поставить здесь точку.
В архив положил инклуд для fasm'a с описанием структур Ntfs, исходник моей демки, а так-же готовый экзе для тестов. Всем удачи, пока!
Несколько ссылок по теме:
Лучшее описание NTFS, которое можно найти в сети
Attributes - NTFS Documentation
PDF от энтузиастов, т.к. официальной спеки нет
https://dubeyko.com/development/FileSystems/NTFS/ntfsdoc.pdf
Разбор NTFS от Криса Касперски
https://konyakov.ru/pubs/books/kris-kaspersky-r_i_p/kris-kaspersky-20.pdf
Двоичный редактор WinHEX и шаблоны для него
WinHex Hex Editor
Additional Templates for WinHex & X-Ways Forensics
GitHub - kacos2000/WinHex_Templates: X-Ways Forensic/ WinHex templates
ZwCreateFile() в монопольном режиме с флагом ShareAccess=0, что полностью блокирует доступ из вне, пока родитель по собственной воле не закроет их дескриптор. В данной статье мы рассмотрим универсальный способ, который позволит оперировать абсолютно любыми файлами на диске прямо с прикладного уровня, правда для этого нужно будет отойти от привычных правил и реализовать поиск секторов в ручную, в обход драйвера NTFS.Содержание:
1. Основная идея
2. Штатный механизм работы с файлами
3. Структура файловой системы NTFS - таблицы и метафайлы
4. Основная база данных $MFT
5. Практическая часть - софт для вычисления цепочки кластеров
6. Заключение
1. Основная идея
При проектировании ОС особое внимание уделяется безопасности. Подсистема Security контролирует все наши действия, с момента входа в окне Winlogon и вплоть до полного выключения машины. Удостоверением личности является здесь Token, который вручает нам сторож LSASS - именно в токене перечисляются наши права и привилегии.
Когда мы создаём объекты ядра от своего имени (а это файлы, процессы, и многое другое), они наследуют наши права. Если объект уже был открыт кем-то до нас, в его дескрипторе безопасности окажутся права родителя, а потому системный монитор SRM (Security Reference Monitor) своей api
SeAccessCheck() может отказать нам в общем доступе. То есть при одинаковых правах владельцев, фактически получаем схему "кто первым встал, того и тапки".В примере ниже моя попытка открыть файл-подкачки Pagefile.sys на безобидное чтение (без записи), но поскольку Win уже эксклюзивно открыла его до меня Shared=0, монитор отвергает мой запрос с кодом "ERROR_SHARING_VIOLATION=20h", хоть и зашёл я в систему как админ. Чтобы расшифровать код ошибки на понятный язык, в исходнике предусмотрена универсальная процедура с функцией
FormatMessage() - она возвращает описание любой ошибки в текстовом виде, что удобно на этапе отладки приложения:
C-подобный:
format pe64 gui
include 'win64ax.inc'
entry start
;//----------
section '.code' code readable executable
start: sub rsp,8
invoke CreateFile,<'C:\pagefile.sys',0>,\
GENERIC_READ,
FILE_SHARE_READ + FILE_SHARE_WRITE, 0,\
OPEN_EXISTING, 0,0
stdcall GetError ;//<---- декодер ошибки
invoke ExitProcess,0
;//---- Процедура ---------------
proc GetError uses rax
locals
errMes rb 128 ;//<----- буферы в лок.памяти
buff rb 128
endl
invoke GetLastError
push rax
invoke FormatMessage, FORMAT_MESSAGE_FROM_SYSTEM,0, rax,0, addr errMes, 128,0
pop rax
cinvoke wsprintf, addr buff, <'WinError = %#04x',10,'%s',0>, rax, addr errMes
invoke MessageBox,0, addr buff, <'Last Error',0>, MB_ICONWARNING
ret
endp
;//----------
section '.idata' import data readable
library kernel32,'kernel32.dll',user32,'user32.dll'
include 'api\kernel32.inc'
include 'api\user32.inc'
Ну не может и ладно.. Не пускают нас в дверь - тогда зайдём с форточки.
Суть в том, что если файл закрыт на амбарный замок, то корневой раздел самого диска открыт всегда, хотя и требует для доступа прав админа. Другими словами, чтобы прочитать абсолютно любой имеющийся на диске файл, мы должны обратиться не напрямую к файлу, а к разделу диска например
С:\, D:\, и т.д. где этот файл лежит, после чего искать уже его смещение относительно корня.При работе с разделами накопителя мин.порцией обмена является сектор в 512-байт, и что важно, для наших целей требуется отключить буферизацию в кэш, которая включена по умолчанию. Механизм кэширования позволяет копить все изменения файла в специальном буфере, чтобы при каждом чихе не обращаться к относительно медленному харду. Диспетчер кэша сбрасывает буфер на диск в трёх случаях: по 8-сек таймеру (отсчёт начинается после момента записи), при переполнении буфера, или по нашему желанию в произвольный момент функцией
FlushFileBuffers().Таким образом, запрос на открытие корневого раздела диска должен выглядеть так, в результате чего система удовлетворит его:
C-подобный:
invoke CreateFile,<'\\.\C:',0> ,\
GENERIC_READ,\ ;// открыть только на чтение (в записи могут отказать)
FILE_SHARE_READ + FILE_SHARE_WRITE, 0,\ ;// при этом другие могут читать и записывать в него
OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, 0 ;//<---- флаг откл.кэша!
Функция
DeviceIoControl() имеет мощный аргумент FSCTL_GET_RETRIEVAL_POINTERS. По дескриптору открытого файла, он возвращает номера его кластеров на диске. Круто! ..однако данная api нам не подходит, т.к. ожидает на входе именно дескриптор, который мы не сможем получить, пока не откроем файл, с чем собственно у нас и проблема.Но здесь интересно другое. Как оказалось, NTFS знает, какие кластеры занимает файл с именем(X) на диске, а вот обратную связь вычислить уже не способна. То-есть она не в состоянии определить имя файла по номеру кластера! Этот нюанс позволит нам обходить системный монитор безопасности SRM, который стоит как злой Цербер на страже файлов. Хотя штатная утилита Win
fsutil.exe справляется всё-таки с задачей, но к монитору это не имеет отношения:
Код:
C:\> fsutil volume querycluster D:\ 0x2d
Кластер 0x002d используется P---D \pagefile.sys::$DATA
Флаги:
---------------------------------------------
P - Pagefile C - Compressed (сжатый)
R - Read-Only E - Encrypted (зашифрованный)
H - Hidden D - Поток $DATA внутри файла
S - System
2. Штатный механизм работы с файлами
На рис.ниже представлена взаимосвязь структур при открытии процессом файлов. Все они собираются в таблицу дескрипторов Handle, на которую указывает поле "ObjectTable" в родительской структуре ядра EPROCESS. Отметим, что в этой таблице лежат дескрипторы не только файлов, но и прочих объектов ядра, например исполняемых потоков Thread, открытых ALPC-портов, ключей реестра Key, объектов синхронизации типа Mutant\Event\Mutex, различных устройств, и прочее. То-есть это всё, что открыл или создал данный процесс. Если дескриптор олицетворяет файл на диске, он будет указывать на структуру FILE_OBJECT.
Расширение
!handle отладчика WinDbg возвращает список всех дескрипторов из таблицы, если передать ей адрес структуры EPROCESS. Из лога ниже видно, что у процесса Word.exe всего 459 открытых объектов, и чтобы найти среди них только файлы, можно использовать маску File в последнем аргументе (кстати чувствительна к регистру).
Код:
0: kd> !process 0 0 winword.exe
PROCESS fffffa80117dab00
Session: 1 Cid: 0620 Peb: 7efdf000 ParentCid : 05a0
DirBase: 1a3653000 ObjectTable: fffff8a00300bf90 HandleCount: 459
Image : WINWORD.EXE
0: kd> !handle 0 3 fffffa80117dab00 File
Searching for handles of type File
052c: Object: fffffa800c8db580 GrantedAccess: 00120089 Entry: fffff8a00d5fc4b0
Header: fffffa800c8db550 HandleCount : 1 PointerCount: 1
Name : \Program Files (x86)\Common Files\microsoft shared\PROOF\MSSP3RU.LEX
Так мы получим адрес одного из файловых объектов процесса, и можно вскормить его расширению
!fileobj для последующего анализа. Здесь видим, что файл взаимодействует с драйвером менеджера разделов диска Volmgr.sys, имеется лист его атрибутов Read + SharedReadWrite, а так-же указатель на структуру VPB или "Volume Parameters Block", где найдём описание раздела например С:\.
Код:
0: kd> !fileobj fffffa800c8db580
\Program Files (x86)\Common Files\microsoft shared\PROOF\MSSP3RU.LEX
DevObj: 0xfffffa800f924cd0 \Driver\volmgr
Vpb : 0xfffffa800f92a820
Access: Read SharedRead SharedWrite
Flags: 0xc0042
Synchronous IO
Cache Supported
Handle Created
Fast IO Read
FsContext : 0xfffff8a00423bc70 ;<---- линк на FSRTL_ADVANCED_FCB_HEADER
FsContext2: 0xfffff8a0029471d0
Cache Map : 0xfffffa800d035180
ByteOffset: 2e14b1
0: kd>
Файловая система привязывает к каждому открытому файлу свою структуру FCB, (file control block). Если 2 разных процесса открывают один общий файл, то FCB для них так-же будет общей, а вот файловых объектов диспетчер в/в создаст уже два, и поле "FsContext" каждого из них будет смотреть на эту общую FCB.
Здесь важно понять, что ядро ОС хранит атрибуты доступа к файлу в структуре FILE_OBJECT, в то время как эти-же атрибуты NTFS прячет в собственную свою FCB. Это потому, что файловая система живёт своей жизнью, ведь диск может быть отформатирован не только в NTFS, но и в FAT32 или вообще ExFAT64. Фактически атрибуты в FCB имеют приоритет перед системными, ведь только на этом уровне возможен физ.контроль общего доступа к файлам.
К сожалению NTFS не документирована, т.е. на неё нет официальной спецификации от Microsoft, а потому все принадлежащие ей структуры являются внутренними, и не разглашаются в публичных символах отладки *.pdb драйвера Ntfs.sys. Как результат, даже WinDbg на запрос "покажи fcb" отвечает "не найдено", хотя заголовок Header отображает без проблем, т.к. им оперирует диспетчер в/в ядра, а не Ntfs.
Код:
0: kd> dt _FSRTL_ADVANCED_FCB_HEADER 0xfffff8a00423bc70
nt!_FSRTL_ADVANCED_FCB_HEADER
+0x000 NodeTypeCode : 0n1797
+0x002 NodeByteSize : 0n496
+0x004 Flags : 0x40
+0x005 IsFastIoPossible : 0x2
+0x006 Flags2 : 0x2
+0x007 Version : 0y0001
+0x008 Resource : 0xfffffa80`10878d00 _ERESOURCE
+0x010 PagingIoResource : 0xfffffa80`10878da8 _ERESOURCE
+0x018 AllocationSize : 0x2e2000
+0x020 FileSize : 0x2e14b5
+0x028 ValidDataLength : 0x2e14b5
+0x030 FastMutex : 0xfffffa80`10878cc8 _FAST_MUTEX
+0x038 FilterContexts : _LIST_ENTRY
+0x048 PushLock : _EX_PUSH_LOCK
+0x050 ContextSupportPtr : 0xfffff8a0`0423bc60
0: kd> dqs 0xfffff8a00423bc70 + 0x58 ;<----- по сути это должна быть структура FCB,
fffff8a0`0423bcc8 fffff8a0`0423bde0 ;\ т.е. начинаться сразу после заголовка,
fffff8a0`0423bcd0 fffff8a0`0423bb70 ; \ но это не точно, т.к. документации нет.
fffff8a0`0423bcd8 fffff8a0`0423bb70 ; \
fffff8a0`0423bce0 fffff8a0`0423bb40 ; >--- массив каких-то указателей (валидны)
fffff8a0`0423bce8 fffffa80`0fc2d180 ;__/
fffff8a0`0423bcf0 00000000`10000230 ;<------ возможно флаги атрибутов
fffff8a0`0423bcf8 00000002`00000001
fffff8a0`0423bd00 00000001`00000001
0: kd>
3. Структура файловой системы NTFS - таблица и метафайлы
NTFS v1.0 родилась на свет в 1993 году, далее v2 как сквозь землю провалилась (ничего о ней не известно), в нулевых появилась сразу третья версия для Win2k, и наконец с 2001 года нашими хардами погоняет v3.1, которая и считается актуальной по сей день. За это время из неё было удалено много лишнего, а ещё больше добавлено, например поддержка шифрования Encrypt, сжатия данных Compressed, отказ от хранения на диске больших массивов нулей Sparse, отдельная база дескрипторов безопасности в виде метафайла $Secure, а так-же индексация записей FileRecord для быстрого поиска.
Архитектурно состоит из 11-ти функциональных модулей, которые принято называть метафайлами. Имена их начинаются со-знака($), а номера кластеров на диске хранятся в глобальной базе MFT (master file table). У каждого раздела\тома диска своя база MFT, поэтому в терминологию было введено такое понятие как LCN (logical cluster number). Отсчёт LCN ведётся с нуля относительно начала раздела, а не самого диска. Например LCN(0) разделов
С:\ и D:\ будут указывать на разные секторы диска.Помимо логических, в структурах файловой записи используются ещё и вирт.кластеры VCN - это смещение от начала файла. Такой подход сокращает разрядность указателей на кластеры, и вместо 64-битных значений можно использовать 32-битный дворд. Кластер - это мин.единица данных в NTFS, в то время как на физ.уровне диск и все системные WinAPI оперируют исключительно 512-байтными секторами с адресацией LBA (logical block address).
При работе с NTFS, в качестве отправной точки используется загрузочный сектор раздела, который числится в штате как метафайл $Boot. То-есть открываем раздел функцией
CreateFile(), и через ReadFile() читаем сразу первый-же его сектор LBA=0. Если по смещению(3) от начала сектора найдём строку с сигнатурой "NTFS", значит это точно наш клиент, и можно переходить к глобальной базе MFT - её кластер будет указан в поле "MftLcn".
Код:
struct BOOT_SECTOR ;<---------- Первый сектор раздела! = Ntfs $BOOT
JumpBootLoader rb 3 ; 00. инструкция jmp на вторичный загрузчик
FileSystemId rb 8 ; 03. строка с сигнатурой "NTFS"
BytePerSector dw 0 ; 0B. размер сектора в байтах (обычно 512, но для дисков NVM может быть 4096)
SectorPerCluster db 0 ; 0D. секторов в кластере (обычно 8, что даёт размер кластера 512*8=4КБ)
ReservedSector dw 0 ; 0E. резервных секторов (при исправном диске = нуль)
Reserved1 rb 5 ; 10.
Media db 0 ; 15. тип диска: 0xF8 = HDD, 0xFA = NVMe
Reserved2 dw 0 ; 16.
SectorPerTrack dw 0 ; 18. секторов в треке\цилиндре (всегда 63, актуально только для HDD)
Heads dw 0 ; 1A. всего головок чтения\записи (255, только для HDD)
HiddenSector dd 0 ; 1C. смещение от начала диска к этому разделу
Reserved3 dq 0 ; 20.
SectorCount dq 0 ; 28. всего секторов в разделе (по факту его размер)
MftLcn dq 0 ; 30. кластер таблицы $MFT <---------------------- линк на глобальную базу!
MftMirrLcn dq 0 ; 38. кластер таблицы бэкапа $MFTMirr (копия первых 4-х записей из оригинала)
BytePerMftEntry db 0 ; 40. размер одной записи FileRecord (для NTFS v3.1 это всегда 1024 байт, код = F6)
Reserved4 rb 3 ; 41.
BytePerIndexBuff db 0 ; 44. размер одной записи FileIndex
Reserved5 rb 3 ; 45.
VolumeSN dq 0 ; 48. серийный номер тома
BpbChecksum dd 0 ; 50. контрольная сумма: 0 = ОК!
;<----------------------------- 54. здесь начинается код вторичного загрузчика ОС
ends
Как только мы узнаем номер кластера MFT, остальное уже дело техники.
MFT - это централизированная база буквально всех файлов на данном разделе диска, в том числе и самих метафайлов. База состоит из массива структур FILE_RECORD размером 1024 байт каждая - одна структура описывает отдельно взятый файл на диске. Таким образом, размер базы MFT напрямую зависит от кол-ва файлов.
Под свои метафайлы, NTFS резервирует первые 12 записей FileRecord, и располагаются они в строго определённом порядке, как указано ниже. После служебных файлов идёт зарезервированное свободное пространство, а пользовательские файлы начинаются в MFT с индекса
Idx=24.Когда диск отформатирован в NTFS, файл может иметь до 16-ти внутренних атрибутов. Не нужно путать их с атрибутами типа: скрытый, сжатый, системный, и прочие. Например сведения об основном потоке данных хранится в атрибуте $DATA, хотя NTFS поддерживает ещё и скрытые от глаз альтернативные потоки ADS (alternate data stream), которые будет описывать расширенный атрибут $EA. В свою очередь имя файла прописывается в атрибут $FILE_NAME, а общие сведения в $STANDARD_INFO.
В записи FileRecord не обязательно должны присутствовать буквально все перечисленные выше атрибуты от 1 до 16, однако в минимальной комплектации три выделенных зелёным атрибута с идентификаторами типа
0x10,30,80 мы найдём всегда. Остальные 13 прилагаются в нагрузку, и для нас они сейчас не представляют никакого интереса.4. Основная база данных $MFT
Так, мелкими шагами мы вплотную подошли к тому, чтобы утянуть любой системный файл с диска без указания его имени, ведь иначе сторожа Win не хотят нам предоставлять к нему легальный доступ. На данном этапе имеет смысл скачать крутой двоичный редактор WinHEX, который из коробки имеет всё необходимое для понимания столь сложной темы. В частности, по своим готовым шаблонам он может отображать содержимое метафайлов $Boot, $MFT, структур FileRecord, и многое другое. В подвале статьи есть ссылки для скачивания.
Для начала посмотрим на формат структуры FileRecord - нам интересны всего 2 выделенных плюсом поля: смещение в байтах к первому Ntfs-атрибуту, а так-же флаги, что позволит искать (или наоборот пропускать) кластеры удалённых с диска файлов:
Код:
struct FILE_RECORD ;<---------------- размер 38h
Magic db 'FILE' ; 00. сигнатура
upsOffset dw 0 ; 04. смещение к "последовательности обновлений" (updata sequency)
upsSize dw 0 ; 06. ..и её размер в словах.
LSN dq 0 ; 08. порядковый номер в $LogFile (logfile seq.number)
SequenceNum dw 0 ; 10. порядковый номер записи
HardLinkCount dw 0 ; 12. счётчик ссылок на этот файл из вне
firstAttrOffset dw 0 ; 14. + смещение к первому атрибуту
Flags dw 0 ; 16. + флаги: 0=удалён, 1=запись активна, 2=каталог
fRecordRealSize dd 0 ; 18. реальный размер данной записи FileRecord,
fRecordAllocSize dd 0 ; 1C. ..и выделенный для неё размер (обычно 1024 байта = 2 сектора)
ReferBaseRecord dq 0 ; 20. ссылка на базовую запись в цепочке (см.поле#2)
NextAttrId dw 0 ; 28. ID сл.атрибута
Padding1 dw 0 ; 2A. (выравнивание)
MftRecordIdx dd 0 ; 2C. индекс этой записи в базе MFT (пользовательские начинаются с 24)
upsNumber dw 0 ; 30. значение "последовательности обновлений"
Padding2 rb 6 ; 32. (выравнивание)
;// первый атрибут.....
ends
Прочитав линк на первый атрибут по смещению
0x14, мы упрёмся в структуру STANDARD_INFO, поскольку именно она имеет первый из возможных Type=0х10. Ядро файловой системы выстраивает атрибуты в записях FileRecord по их индексам в метафайле $AttrDef, которые совпадают с идентификатором типов, например: 1=0x10, 2=0x20, и т.д. Это говорит о том, что NTFS разрабатывалась грамотно (а не абы как), с учётом всех мелочей.Если содержимое атрибута полностью помещается в одну 1024-байтную запись FileRecord, его называют "Резидентным". Но для некоторых атрибутов этого пространства явно не достаточно, тогда тело его выносится за пределы FileRecord, а внутри записи остаётся лишь ссылка на внешний кластер - такие атрибуты считаются "Не резидентными". Ярким представителем нерезидентого является $DATA, поскольку в качестве его тела выступает собственно содержимое файла, например видео, картинка, исполяемый файл, и прочее. Посмотрите ещё раз на таблицу атрибутов выше - в столбце флагов указывается как-раз тип атрибута, резидентный он или нет.
Все 16 атрибутов имеют одинаковый для всех заголовок
StaticHeader размером 16-байт, после которого идёт его продолжение в зависимости от резидентности, и лишь потом начинается уже непосредственно тело атрибута. Первые 2 поля в статическом хидере определяют тип и длину атрибута, а поскольку атрибуты в записи FileRecord плотно прижаты друг к другу, зная длину можно будет перемещаться к следующему. Маркером последнего атрибута в цепочке является значение 0xFFFFFFFF. Здесь я так-же выделил ключевые для нас поля плюсом:
Код:
struct STATIC_HEADER ;<---- Размер = 10h
Type dd 0 ; 00. + тип атрибута = 0x10,30,80, и т.д.
AttrLen dd 0 ; 04. + ВАЖНО! его длина в байтах
NonResidentFlag db 0 ; 08. + не резидентный(1), резидентный(0)
AttrNameLen db 0 ; 09. длина Unicode-имени атрибута (игнор)
AttrNameOffset dw 0 ; 0A. смещение к имени
Flags dw 0 ; 0C. + флаги файла: 0=Simple, 1=Compressed, 0x4000=Encrypted, 0x8000=Sparse
AttrId dw 0 ; 0E. ID атрибута (игнор)
ends
struct RESIDENT_HEADER ;<---- Размер = 18h (продолжение заголовка)
StaticHeader STATIC_HEADER
AttribLength dd 0 ; 10. + длина тела атрибута
AttribOffset dw 0 ; 14. + смещение к атрибуту
Indexed db 0 ; 16. флаг индексации (игнор)
Unused db 0 ; 17. выравнивание
;// тело резидентного...
ends
struct NON_RESIDENT_HEADER ;<---- Размер = 40h (внешний атрибут, например $DATA)
StaticHeader STATIC_HEADER
FirstCluster dq 0 ; 10. + первый кластер VCN атрибута
LastCluster dq 0 ; 18. + последний кластер
DataRunsOffset dw 0 ; 20. + <------------- ВАЖНО! смещение к массиву DataRuns
CompressionUnit dw 0 ; 22. + флаг сжатия данных
Unused dd 0 ; 24. выравнивание
AttribAllocated dq 0 ; 28. выделенный размер в байтах
AttribSize dq 0 ; 30. реальный размер трибута
StreamDataSize dq 0 ; 38. + размер именованного\безымянного потока NTFS (см.EA)
;// тело нерезидентного... ^^^^^^ а по факту размер файла в байтах
ends
К сожалению переменный размер заголовков заставляет нас ипользовать 3 структуры вместо 1, зато реализовать программный доступ к NTFS теперь намного проще. Вот формат первых двух атрибутов, которые потребуются нам в практической части, при этом в стандартном
0х10 нет ничего интересного, а время можно взять из 0х30, т.к. оно дублируется. Напомню, что оба этих атрибута резидентные, т.е. их тело полностью находится в файловой записи FileRecord:
Код:
struct ATTR_STD_INFO_10 ;<------ размер 0x60
Header RESIDENT_HEADER
fCreationTime FILETIME ; 18. время создания
fAlteredTime FILETIME ; 20. изменения
mftChangedTime FILETIME ; 28. изменения MFT
fReadTime FILETIME ; 30. время последнего доступа
fPermissions dd 0 ; 38. атрибуты файла (см.ниже FILE_TYPE_xx)
MaxVerNum dd 0 ; 3C. макс.версия файла
VerNum dd 0 ; 40. текущая
ClassId dd 0 ; 44. ID класса ???
OwnerId dd 0 ; 48. ID родителя
SecurityId dd 0 ; 4C. ID безопасности
QuotaCherged dq 0 ; 50. квота в байтах
USN dq 0 ; 58. Update Sequence Number
ends
struct ATTR_FILE_NAME_30 ;<------ размер зависит от длины имени файла
Header RESIDENT_HEADER
ParentFileRecord dq 0 ; 18. ссылка на род.каталог
fCreationTime FILETIME ; 20. время создания
fAlteredTime FILETIME ; 28. время изменения
mftChangedTime FILETIME ; 30. время изменения MFT
fReadTime FILETIME ; 38. время последнего доступа
AllocaFileSize dq 0 ; 40. выделенный под атрибут размер
RealFileSize dq 0 ; 48. реальный размер
FileFlags dd 0 ; 50. + атрибуты файла (см.ниже FILE_TYPE_xx)
Reserved dd 0 ; 54. резерв
FileNameLen db 0 ; 58. + длина имени файла
NameSpace db 0 ; 59. + формат имени (dos 8.3, win, unix)
FileName db 0 ; 5A. + Unicode имя файла
ends
FILE_TYPE_READ_ONLY = 0x0001 ; флаги Permissions и FileFlags
FILE_TYPE_HIDDEN = 0x0002
FILE_TYPE_SYSTEM = 0x0004
FILE_TYPE_ARCHIVE = 0x0020
FILE_TYPE_DEVICE = 0x0040
FILE_TYPE_NORMAL = 0x0080
FILE_TYPE_TEMPORARY = 0x0100
FILE_TYPE_SPARSE = 0x0200
FILE_TYPE_REPARSE = 0x0400
FILE_TYPE_COMPRESSED = 0x0800
FILE_TYPE_OFFLINE = 0x1000
FILE_TYPE_NOINDEXED = 0x2000
FILE_TYPE_ENCRYPTED = 0x4000
FILE_TYPE_DIRECTORY = 0x10000000
А вот реальный дамп из WinHEX, где каждый атрибут я выделил в блок. Как и указано в структуре статического заголовка выше, первый дворд хранит тип атрибута, а во втором лежит его размер. Таким образом, прибавляя этот размер к текущему адресу, мы попадаем в следующий и далее, пока не встретим маркер последнего
0xFFFFFFFF. В данном случае запись FileRecord принадлежит метафайлу $MFT, у которого всего 4 атрибута - это 0x10,30,80,B0.4.1. Атрибут $DATA - полезная нагрузка файла
И наконец рассмотрим, как получить номера кластеров, чтобы собрать из них готовый к употреблению файл. Для этого нужно разобрать на винтики атрибут $DATA, а точнее его цепочку указателей на кластеры под названием "DataRuns". По большому счёту, это и есть сердце всего механизма хранения данных в NTFS, а потому требует к себе особого внимания (и понимания). Кстати если отбросить все украшательства, разница между NTFS и FAT кроется именно здесь.
Возьмём серый блок из примера выше, и спроецируем на него уже знакомую нам структуру заголовка NON_RESIDENT_HEADER. В первую очередь проверяем, что это точно нерезидент, т.к. если файл размером до 600 байт, его тело может находится прямо внутри атрибута $DATA. В данном случаем по смещению(8) флаг=1, а значит тушка файла лежит где-то снаружи, а где именно - указывает как-раз цепочка DataRuns по смещению(40h).
Обратите внимание, что номер последнего вирт.кластера VCN по смещению(18h), и размер потока оффсет(38h) имеют одинаковые значения, просто в первом случае указано в кластерах, а во-втором в байтах. Формула перевода кластеров в байты будет иметь такой вид:
Байты = Кластеров*4096.
Код:
00. 0х00000080 = атрибут $DATA
04. 0х00000048 = ...его длина
08. 0х01 = не резидентный
09. 0х00 = длина имени атрибута (нет имени)
0А. 0х0040 = смещение к имени (игнор, т.к. имени нет)
0С. 0х0000 = флаг обычного файла Simple
0E. 0х0001 = id атрибута
10. 0х00000000`00000000 = вирт.кластер начала данных (всегда нуль)
18. 0х00000000`00005ABF = вирт.кластер конца
20. 0х0040 = смещение к массиву DataRuns (всегда 40h)
22. 0х0000 = данные не сжаты (флаг)
24. 0х00000000 = байты выравнивания на 8
28. 0х00000000`05AС0000 = выделенный размер в байтах
30. 0х00000000`05AС0000 = реальный размер
38. 0х00000000`05AС0000 = размер потока-данных в файле
40. 32 C0 5A 00 00 0C 00 00 = запись DataRun
Записи DataRuns имеют довольной сложный формат, а ещё сложнее реализовать их парсинг программно.
Первый байт называют "InfoByte" и разделён он на 2 тетрады по 4-бита. В младшей кодируется длина в байтах "счётчика последовательности", а в старших - разрядность указателя на кластер LCN в байтах. В данном случае этот байт имеет значение
0x32, поэтому берём следующие 2 байта после него и получаем счётчик C05A=0x5AC0 кластеров, а после них ещё 3 байта 00000C=0x0C0000, и это будет номером начального кластера. Итого нам нужно через ReadFile() прочитать 23.232 кластера, начиная с LCN=0x0C0000.Здесь повезло, что в цепочке всего одно звено, т.к. следующий инфо-байт после
0С имеет терминальное значение 0x00. Однако если файл на диске фрагментирован (т.е. хранится не целиком, а частями в произвольных кластерах), то кол-во звенье в массиве DataRun уже увеличится, ведь одна запись описывает один именно непрерывный блок кластеров. Сумма двух тетрад инфо-байта определяет длину своей DataRun, значит следующий инфо-байт найдём через 3+2=5 позиций, и если там нуль - это последний.Более того, начальные кластеры одной цепочки могут быть разбросаны по поверхности диска хаотично, например:
40-200-500-120-60. Здесь видно, что нам нужно сделать первые три шага вперёд, а вот дальше 120-60 указывают уже назад в область между кластерами 40-200. Поэтому в записях DataRun номер начального кластера указывается как число со знаком - если значение положительное (сброшен старший бит), то это смещение вперёд от текущего кластера, а если отрицательное - назад.Важно понять, что номера кластеров в DataRuns указываются как смещение относительно предыдущего, т.е. это не абсолютный кластер LCN. На физ.кластер указывает только первое звено цепи, а все остальные нужно будет прибавлять к предыдущему. Такой подход позволяет экономить на разрядности номера кластера в записях Runs. Надеюсь схема ниже внесёт немного ясности в этот кавардак:
Когда файл содержит в себе фрагменты из более 4КБ нулей (1 кластер), NTFS не выделяет под них место на диске, а просто взводит в атрибуте файла флаг "Sparse=0x0200" (разряжённый, см.выше константы FILE_TYPE). При этом в записи DataRun хранится только счётчик забитых нулями кластеров, экономя таким образом свободное пространство диска. На Хабре есть готовая статья с подробным описанием этой фишки, поэтому повторяться не буду.
5. Практическая часть - софт для вычисления цепочки кластеров
Основная проблема здесь в том, чтобы программно вычислить знак числа в DataRuns, ведь значение указателя на кластер может быть произвольным от 1 до 8 байт, а потому нельзя предугадать его заранее. Если-бы разрядность была фиксирована 16\32\64-бита, достаточно было просто чекнуть старший бит
or eax,eax --> js, но как быть с 24 или 40-битными значениями с длинною в 3 или 5-байт? Значит нужно придумать способ расширения любого числа до 64-бита.Эту проблему решает первый инфо-байт в записи Run - достаточно умножить старшую его тетраду на 8, как получим разрядность указателя на кластер в битах. Далее, среди инструкций процессоров х86 есть
BT (бит-тест), которая чекает как-раз бит в указанной позиции. Значение тестируемого бита отправляется прямиком во-флаг процессора CF, и если он окажется взведён, значит число отрицательное и нужно прыгать назад от текущего кластера. Этот момент представлен в записи Run(3) на правом скрине выше.Я написал небольшую демку, которая ищет первые 4 раздела на дисках, читает их загрузочные секторы в лице метафайла $Boot, и собирает сведения о записях FileRecord самой базы MFT каждого из найденных разделов в отдельности. Обратите внимание на сл.нюансы:
1. Как оказалось, глобальная база MFT на всех томах распологается по фиксированному кластеру 6.291.456.
2. Размер базы MFT напрямую зависит от кол-ва файлов, а не от размера самого тома, и расширяется база динамически.
3. Размеры секторов и кластеров обычно равны 512 и 4096 байт соответственно, но если диск не хард, а NVM, то могут быть больше.
4. Начиная с NTFS v3.1 длина записей FileRecord должна быть всегда 1024 байта. Если у себя вы обнаружите другое значение, плиз дайте мне знать.
5. Диапазон вирт.кластеров VCN - это всегда сумма лог.кластеров LCN, т.к. оба они определяют размер файла на диске в кластерах.
Я специально убрал в этом коде пимпу "Сохранить", чтобы вы реализовали бэкап самостоятельно. Поскольку в окне уже есть номера кластеров в трёх DataRuns, остаётся умножить их на размер одного кластера 4096, чтобы перевести в LBA для функции
ReadFile(). Так получим недоступную обычным способом резервную копию базы MFT прямо с работающей машины, для анализа уже в офлайн.По такой-же схеме можно узнать кластеры абсолютно любого файла на диске. В реальной ситуации, для оптимизации поиска сначала копируют в свой буфер всю MFT, и уже в буфере ищут структуру FileRecord нужного файла с шагом в 1024-байта. Иначе потребуются сотни тысяч обращений к медленному харду, и поиск будет проходить со скоростью улитки.
6. Заключение
NTFS можно сравнить с небольшой операционной системой, которая работает внутри Win, имеет своё личное ядро Ntfs.sys, и 16 внешних модулей в виде метафайлов с префиксами($). Она способна восстанавливать файлы при сбоях, для чего в штате числится журнал $LogFile. Суть в том, что перед тем-как произвести над файлом какое-либо действие, ядро NTFS записывает это действие сначала в журнал, и лишь потом меняет непосредственно файл. Поэтому если в момент правки отключится питание в сети и т.п., всегда будет возможность восстановить оригинал из лога. В общем говорить о ней можно бесконечно, а потому лучше поставить здесь точку.
В архив положил инклуд для fasm'a с описанием структур Ntfs, исходник моей демки, а так-же готовый экзе для тестов. Всем удачи, пока!
Несколько ссылок по теме:
Лучшее описание NTFS, которое можно найти в сети
Attributes - NTFS Documentation
PDF от энтузиастов, т.к. официальной спеки нет
https://dubeyko.com/development/FileSystems/NTFS/ntfsdoc.pdf
Разбор NTFS от Криса Касперски
https://konyakov.ru/pubs/books/kris-kaspersky-r_i_p/kris-kaspersky-20.pdf
Двоичный редактор WinHEX и шаблоны для него
WinHex Hex Editor
Additional Templates for WinHex & X-Ways Forensics
GitHub - kacos2000/WinHex_Templates: X-Ways Forensic/ WinHex templates