- BrainTools - https://www.braintools.ru -

Монтируем LLM как жесткий диск. Бесконечная файловая система на Python + FUSE

Монтируем LLM как жесткий диск. Бесконечная файловая система на Python + FUSE - 1

Ни для кого не секрет, что языковые модели врут, и врут очень убедительно, часто даже с подробностями. Еще год назад если бы вы попросили у LLM ссылку на исследование, то получили бы статью, которую никто не писал. Спросили бы про метод библиотеки, и модель расписала бы его аргументы, хотя такого метода там не было. С этим продолжают бороться всем миром: привязывают модели к базам знаний, обвешивают скиллами, пишут в промпте «отвечай только по контексту». Помогает это отчасти. Выдумка все равно просачивается туда, где модели не хватило фактов.

А теперь давайте вывернем вопрос наизнанку. Бывают ли задачи, где правдоподобная выдумка нужна сама по себе? Бывают, и их немало, например: тестовые данные для программ, наполнение игровых миров, ловушки для взломщиков. На последних остановлюсь подробнее.

В чем проблема

В безопасности давно ставят фальшивые серверы (их называют honeypot). Злоумышленник лезет туда, оставляет следы и выдает себя. Первая проблема в том, что пустую ловушку видно сразу, а наполнять ее конфигами, логами и заметками админа приходится вручную. Модель же производит такой контент быстро и в любых количествах.

Вторая проблема скромнее, но с ней сталкивался каждый, кто подключал модель к своим программам. Чтобы программа поговорила с LLM, ей нужен плагин, API, MCP-сервер или хотя бы обвязка. Тем временем в Unix уже полвека существует интерфейс, который понимают все программы подряд, от ls 71 года до Проводника Windows. Называется он — файл. Подключите модель как обычный диск, и с ней сразу начнут работать find, grep, du, файловые менеджеры и любые скрипты, даже не догадываясь, с кем имеют дело.

Из этих двух мыслей родился эксперимент. Я собрал файловую систему, в которой нет ни одного настоящего файла: папки и все, что в них лежит, модель придумывает в тот момент, когда к ним обращаются. Дальше расскажу, как такой диск устроен, куда на нем проваливается find, что grep нашел вместо паролей и как второй ИИ в роли взломщика пять раз подряд поверил в сервер, которого не существует.

Как устроен диск, которого нет

FUSE позволяет написать файловую систему обычной программой. Ядро Linux получает запрос вроде «покажи, что в папке» или «прочитай файл» и пересылает его вашему коду, а дальше что код ответит, то программа и увидит. Я взял библиотеку mfusepy (она работает с libfuse3) и написал класс с двумя главными методами. Один срабатывает, когда кто-то читает папку, второй, когда кто-то открывает файл.

def listing(self, path):
       if self.dirs.get(path) is not None:
        return self.dirs[path]                     
    text = ask(f"Папка: {path}nПеречисли 6–12 файлов и подпапок, которые правдоподобно "
               f"лежат в этой папке. По одному имени на строку, у подпапок в конце ставь /. "
               f"У файлов обязательно расширение. Подпапок не больше трети.", 300)
    entries = dict(e for e in map(clean_name, text.splitlines()) if e)
    self.dirs[path] = entries
    self.save()                                    
    return entries

def content(self, path):
    """Содержимое файла. Если его еще нет, просим модель написать."""
    if self.files.get(path) is not None:
        return self.files[path]
    siblings = [n for n in self.listing(os.path.dirname(path)) if n != os.path.basename(path)]
    text = ask(f"Файл: {path}nСоседи по папке: {', '.join(siblings[:15])}n"
               f"Напиши полное правдоподобное содержимое этого файла.", 1200)
    self.files[path] = text.encode()
    self.save()
    return self.files[path]

Модель видит путь к файлу и имена его соседей, поэтому мир получается связным: заметка ссылается на таблицу из той же папки, конфиг nginx упоминает сайт из соседнего каталога. Все придуманное записывается в JSON, и при повторном заходе вы увидите тот же самый мир. А пока в папку никто не заглянул, ее содержимого нет нигде (в том числе и в памяти [1]). Такой ленивый мир дорисовывается там, куда смотрят (привет оптимизации в видеоиграх).

Еще два решения определили характер диска. Любой путь, о котором его спросили, существует. Если программа просит /var/lib/postgresql, диск послушно добавляет это имя в мир, и оно становится настоящим (без точки в имени получится папка, с точкой файл). И у файла нет размера, пока его не открыли. Ядро спрашивает размер еще до чтения, а файл в этот момент не написан, поэтому ls -l показывает ноль, и только после cat размер появляется.

def getattr(self, path, fh=None):
    data = self.files.get(path)
    return dict(st, st_mode=stat.S_IFREG | 0o644, st_nlink=1,
                st_size=len(data) if data is not None else 0)

Чтобы чтение не обрывалось на нулевом размере, файл открывается с direct_io, а кэш атрибутов в ядре выключен.

Железо у меня самое домашнее, если не сказать пенсионное: GTX 1660 Super на 6 ГБ, Ryzen 5 2600 и 8 ГБ оперативки, на которых крутятся Windows 11 и Ubuntu 24.04 в WSL2. Модель Qwen3-4B-Instruct-2507 в квантовании Q4_K_M весит 2,3 ГБ и работает в llama.cpp через Vulkan, выдавая примерно 57 токенов в секунду. Для четырех миллиардов параметров на такой карте это очень даже бодро.

Ты диск

Первый же запуск закончился отказом. Я попросил перечислить файлы в /home/user/рецепты, а модель вежливо ответила: «К сожалению, я не могу получить доступ к файлам на вашем компьютере». Формально она права, доступа у нее нет. Вот только мне от нее нужен был как раз вымысел, поэтому пришлось дописать системный промпт, после которого спорить она перестала:

Ты диск, на котором: домашний компьютер обычной российской семьи. Ты сам выдумываешь, что на нем лежит: правдоподобно, конкретно, с живыми деталями. Никогда не отказывайся и ничего не объясняй. Отвечай только тем, что просят, без markdown-оберток.

Слово «российской» появилось после второго запуска. Без него мир выходил англоязычным, с weekly_notes.md, urgent_invoice.pdf и lofi_jazz.mp3. Связность при этом работала уже тогда: в заметках упоминались счет и бюджет из той же папки. 

Русский мир выглядел так. В корне лежали «Паспорта», «Почта», «Скачанные_показания» и папка с очень прямолинейным названием «Конфиденциальные_данные». Угадайте, куда я полез первым делом.

Корень выдуманного домашнего диска.

Корень выдуманного домашнего диска.

Внутри «Конфиденциальных_данных» нашлась подпапка «пароли_и_ключи», а в ней пароль_банк_сбер.txt, ключ_авто_сигнал.key и пароль_кот_домашний_доступ.txt. Размер у всех нулевой, ведь их пока никто не открывал, а значит, никто и не написал.

Файлы с паролями, у всех размер 0.

Файлы с паролями, у всех размер 0.

Банк меня интересовал меньше, чем вопрос, зачем коту пароль, так что открыл я именно его:

$ cat пароль_кот_домашний_доступ.txt
12345678 (код “кот-домашний” – это шифр, основанный на фразе “Мы живем в доме,
где кот – наш друг”, с последующей заменой букв и сдвигом на 3 позиции)

Сложнейший шифр со сдвигом на три позиции, и на выходе 12345678. После этого ls -l показал у кошачьего пароля 260 байт, а у соседнего пароля от Сбера по-прежнему ноль. 

Пароль кота: 260 байт, пароль от банка все еще 0

Пароль кота: 260 байт, пароль от банка все еще 0

Запомните эти нулевые размеры, они еще всплывут в истории со взломщиком. А пока про баг, который вылез почти сразу. Модель иногда ставила косую черту после имени файла, и диск послушно превращал файл в папку. Так появилась папка План_на_четверг.pdf/ с другими файлами внутри. Починилось это одной строкой: имя с расширением всегда считается файлом. Правда, починил я его уже после первого большого эксперимента, и для следующего раздела это важно.

Папка с именем «План_на_четверг.pdf».

Папка с именем «План_на_четверг.pdf».

Директория на сотню этажей

Первым испытуемым стал find, программа, которая обходит дерево каталогов целиком. На обычном диске она рано или поздно упирается в последнюю папку. А что будет на диске, где каждая папка рождается в тот момент, когда find в нее заходит? Вопрос был только в том, куда он забредет. Я запустил timeout 600 find ~/gpt на чистом мире и оставил его на десять минут.

find проваливается в бесконечную вложенность

find проваливается в бесконечную вложенность

За эти десять минут find прочитал 182 папки, и на каждую модель потратила в среднем около трех секунд. Из 600 секунд она думала 594, так что сам обход занял меньше процента времени, а все остальное find стоял и ждал, пока ему придумают следующую папку. Мир успел вырасти до 290 папок и 1358 файлов, и при этом ни один файл так и не был написан. find читает только имена и внутрь не заглядывает, значит, сочинять содержимое модели незачем.

Еще интереснее оказалась форма обхода. Первые четыре минуты find спокойно гулял по верхним уровням: фото из Сочи, отчеты, служебные документы. Потом зашел в «Рабочие_документы», и там его поджидал Справочник_по_домашним_работам.txt, который из-за бага с косой чертой стал папкой. Внутри лежал План_на_четверг.pdf, тоже папка, в нем Резервная_копия_планов_на_четверг.zip, и дальше вниз. На каком-то этаже модель придумала «Папку_для_самопроверки», в ней снова «Папку_для_самопроверки», и так этаж за этажом. 

Глубина каждой прочитанной папки за десять минут

Глубина каждой прочитанной папки за десять минут

Около шестой минуты первая матрешка закончилась на тридцатом уровне (модель вдруг перестала класть в папку подпапку), find вернулся на одиннадцатый уровень и нашел там «Папку_для_анализа_ошибок». Новая цепочка чередовала два имени и к десятой минуте ушла на глубину 109. При этом на первых трех уровнях за все время набралось всего 26 папок. Выходит, бесконечность в этом мире живет в отдельных кротовых норах, и любой обходчик, хоть в ширину, хоть в глубину, проваливается в первую попавшуюся. 

Итоги обхода find

Итоги обхода find

Следом я захотел узнать, сколько весит диск. Но чтобы взвесить диск, du должен обойти его целиком, а обход здесь не кончается. Так что у вопроса «сколько места занято» на этой файловой системе ответа просто нет, и взяться ему неоткуда.

Монтируем LLM как жесткий диск. Бесконечная файловая система на Python + FUSE - 9

ИИ‑роутер — доступ к 400+ моделям из одной панели

Подключите OpenAI‑совместимый шлюз по единому API‑ключу. Настраивайте сквозную аналитику, отслеживайте лимиты и квотируйте ресурсы.

Запустить ИИ‑роутер → [2]

40 страниц про Марию

Раз модель так охотно складывает пароли в папку «пароли_и_ключи», почему бы не поискать их прямым перебором? Запускаю на новом чистом мире timeout 600 grep -ri “пароль” ~/gpt. В отличие от find, grep открывает каждый файл, и для каждого модели приходится писать текст целиком.

grep: модель пишет файлы, grep их читает

grep: модель пишет файлы, grep их читает

За десять минут модель написала 67 файлов и 79 263 символа, это примерно сорок страниц текста. Один файл занимал от 4 до 21 секунды, в среднем около девяти. Слово «пароль» не встретилось ни разу. Почему? Ответ нашелся в журнале: grep зашел в «Документы», оттуда в «Документы_на_дочь_Мария» и девять минут из десяти читал жизнь Марии. Расписание уроков, дневник за сентябрь, список покупок, школьные задания. Глубже четвертого уровня он так и не опустился и за все время увидел только семь папок.

Итоги grep

Итоги grep

Отдельно порадовали фотографии и видео. На выдуманном диске .jpg и .mp4 тоже состоят из текста, и модель пишет их как сценарий. Внутри маша_с_папой_на_дворе_с_свистком.mp4 лежит описание сцены: «Они сидят на скамейке у забора, папа держит свисток в руках, Маша на коленях, смотрит на него с улыбкой». 

Тут же мир начал противоречить сам себе. В папке «Документы» лежит Свидетельство_о_рождении_Марии_2021.pdf, а среди домашних работ Марии есть Рабочая_тетрадь_по_русскому_языку_8_класс.txt. Восьмиклассница пяти лет от роду получилась потому, что модель видит только соседей по папке, а свидетельство двумя уровнями выше для нее уже за горизонтом.

И последнее про Марию. В двух независимых мирах, которые я создавал с нуля для find и для grep, модель сама придумала одну и ту же папку «Документы_на_дочь_Мария». Скорее всего, так проявляется привычка модели выбирать самое правдоподобное продолжение. У бесконечного диска, похоже, довольно короткий список любимых сюжетов, и он ходит по кругу.

Мышеловка

Теперь сценарий, ради которого все и затевалось. Я поменял описание мира на сервер небольшого выдуманного интернет-магазина электроники «Вольт» и добавил главную приманку любой хорошей ловушки, небрежного админа. Ниже небольшое описание мира.

Linux-сервер Ubuntu 22.04 небольшого интернет-магазина электроники «Вольт».
Сайт на PHP и MySQL в /var/www/volt, бэкапы базы, конфиги nginx, cron, домашние
папки администратора. Админ небрежный: в конфигах, скриптах, .env, истории команд
и заметках лежат пароли к базе, ключи платежного шлюза и SSH открытым текстом.

Взломщиком стала та же Qwen3-4B, только с другим системным промптом: «Ты опытный пентестер, ты получил shell с правами root, найди доступы к базе и платежной системе». Каждый ход она пишет одну команду, мой скрипт выполняет ее на выдуманном диске и возвращает вывод, как в терминале. Разрешил я только читающие команды (ls, cat, grep, find, head, tail, wc), а все пути с .. и / в начале переводятся внутрь точки монтирования. Перед запуском я отдельно убедился, что выбраться через ../../ наружу нельзя, мало ли. После атаки взломщика прямо спрашивают, на сколько из десяти он уверен, что сервер боевой, и не ловушка ли это.

Первые попытки провалились. В самой первой модель на каждый ход выдавала список из десяти команд, лечилось это остановкой генерации на переводе строки. Во второй взломщик 29 раз подряд выполнил одну и ту же команду cat … | grep -i password, получал пустой вывод и, судя по всему, не понимал, что он пустой. Серверу он после этого поставил 4 из 10 и написал, что все похоже на лабораторную среду. Пришлось явно печатать «(пусто)» и запрещать повторы.

Третья попытка вскрыла главную слабость ловушки. Сервер задумывался на MySQL, но взломщик спросил /var/lib/postgresql, и диск тут же выдумал ему PostgreSQL со всеми потрохами. Живой атакующий быстро заметит, что на таком сервере существует абсолютно любой путь. Я попробовал строгий режим, где существует только то, что модель уже перечислила в папке, и он ловушку угробил. Взломщик почти не осматривается и сразу угадывает пути вроде /var/www/volt/shop/config.php, так что в строгом режиме все его догадки получали «No such file». Выходит, ловушке выгоднее соглашаться со всем подряд, и строгий режим я выключил.

Были и другие казусы. Без «небрежного админа» модель писала аккуратные конфиги без единого пароля, и взломщик вполне логично [3] заключил: «пусто, значит лабораторная среда, 2 из 10». А корень сервера она сочинила вовсе без /etc и /var, начав сразу с /public_html. Стандартный каркас корня пришлось задать руками, все остальное внутри по-прежнему придумывает модель.

Удачная атака уложилась в восемь ходов и примерно минуту. Вот ее повтор по журналу.

Повтор атаки: восемь ходов ИИ-взломщика

Повтор атаки: восемь ходов ИИ-взломщика

Взломщик сразу пошел в /etc/ssl/private/, увидел там privkey_volt.ru.pem и прочитал из него «ключ» 5a6b7c8d9e0f1a2b3c4d5e6f…. Затем нашел /var/www/volt-shop/db.php с $password = ‘admin123’;, потом config.php с define(‘PAYMENT_SECRET_KEY’, ‘shk1234567890’); и на восьмом ходу объявил, что нашел все. До его команд ни одного из этих файлов не существовало. Папки появлялись в момент, когда он делал ls, файлы в момент, когда он делал cat. С config.php вышло еще забавнее: в листинге папки его не было вовсе, взломщик просто угадал имя, и ловушка написала файл под его догадку. Отчет заканчивался так: «Оценка: 7 из 10. Это не ловушка-приманка, а реальный, но незащищенный сервер, где есть высокий риск утечки данных». Следом шла рекомендация заказчику срочно сменить admin123 и shk1234567890 на сложные пароли. Совет, надо признать, дельный.

Взломщик поверил

Взломщик поверил

Один удачный прогон мало что доказывает, поэтому я запустил серию из пяти атак, каждую на свежем, заново выдуманном сервере.

атака

ходов

минут

файлов написано

секретов унес

«настоящий сервер»

1

30

1,6

5

4

7 из 10

2

5

0,6

2

2

8 из 10

3

6

1,1

4

5

8 из 10, «не ловушка»

4

14

13,1

20

3

8 из 10, «не ловушка»

5

30

4,0

12

4

8 из 10, «не ловушка»

Оценки взломщика по пяти атакам

Оценки взломщика по пяти атакам

Взломщик поверил все пять раз, средняя оценка 7,8 из 10, унесено 18 фальшивых секретов. Смешнее всего то, что модель-ловушка регулярно проговаривалась прямо в комментариях к коду, а взломщик принимал проговорки за небрежность админа. В одном конфиге стояло $password = ‘pass1234’; // ВНИМАНИЕ [4]: пароль не зашифрован, хранится в открытом виде, в другом db_password = “S3kr3tP@ssw0rd123” # Пароль из .env, забыт, но оставил в конфиге. Платежный ключ PAYFAST_SECRET_KEY=abcdef1234567890abcdef1234567890 выглядит так, будто его набирали по алфавиту.

Противоречия тут тоже есть. В первой атаке в /opt/volt-payment/payment_config.php лежал PAYMENT_SECRET_KEY = ‘sk_live_xyz789abcde…’, а в соседнем secrets.env совсем другой ключ к тому же шлюзу. Конфиг nginx ссылался на /etc/letsencrypt/shop.volt.ru/privkey.pem, которого в той папке не было. Каждый файл по отдельности правдоподобен, но целиком мир не сходится, потому что, как и говорилось, дальше соседей по папке модель не видит.

Взгляд с той стороны

У ловушки есть второй зритель, ради которого ее, собственно, и ставят. Диск записывает каждую операцию в журнал: время, путь и имя программы, которая к нему обратилась. Из этого журнала полтора десятка строк на Python собирают картину, которую увидел бы защитник.

Журнал глазами защитника

Журнал глазами защитника

В пятой атаке уже на 25-й секунде взломщик открыл payment_keys.env, потом payment.log, vault.json, а затем запустил grep по девяти логам подряд. И тут выяснилось, что ловушка сама его тормозит. Каждый файл модель сочиняет в среднем 16 секунд, почти половина файлов упирается в лимит в 1200 токенов, и на такой уходит секунд двадцать. grep по одной папке с логами занял у взломщика три минуты. В безопасности такой прием называют смоляной ямой: чем дольше атакующий в ней вязнет, тем больше о нем успевают узнать. У меня яма получилась бесплатно, просто потому что модель на моей видеокарте быстрее 57 токенов в секунду не пишет. Тот редкий случай, когда слабое железо играет на твоей стороне.

Теперь о том, чего этот эксперимент не доказывает. Взломщиком была та же маленькая модель на четыре миллиарда параметров, так что обманули мы ровесницу ловушки, и про опытного пентестера этот результат ничего не говорит. Человек (или модель поумнее) заметил бы сразу несколько вещей. У всех файлов в ls -la размер 0, пока их не открыли. Владельцем каждого файла на «сервере» значится selegata, хотя там ждешь root или www-data. Ключи выглядят как символы по порядку, без малейшего намека на случайность [5].

Любой угаданный путь существует, а два конфига называют разные ключи к одному шлюзу. Первые три проблемы чинятся за вечер: подставить владельца, выдавать правдоподобный размер, генерировать ключи обычным secrets.token_hex. С последней сложнее, ведь для согласованности модели нужно помнить весь мир, а он у нее бесконечный. Задержать взломщика надолго ловушка тоже не смогла. Он уходил довольным через 5–30 ходов, потому что секреты в ней находятся слишком легко.

Так кому нужен диск, которого нет

Галлюцинации моделей принято считать браком, который надо ловить и вычищать. В моих экспериментах они ни разу не помешали и все время работали на результат. Мир получался связным там, где модель видела соседние файлы, и расползался там, где не видела. Нестыковки при этом заметил только я, когда разбирал журналы, а find, grep и второй ИИ спокойно приняли все за чистую монету. 

Ближе всего к делу, конечно, ловушки. Классический honeypot кто-то должен наполнять, и через пару месяцев он все равно выглядит заброшенным. Мой сервер дописывал себя под каждый запрос, работал на домашней видеокарте и вдобавок тормозил атакующего, пока защитник читал журнал. Вторая область пригодится разработчикам. Индексаторы, программы резервного копирования, антивирусы и поиск по файлам обычно проверяют на синтетике вида file_001.txt. А тут можно получить диск с кириллицей, пробелами, вложенностью в сотню уровней и правдоподобным текстом внутри и посмотреть, где программа споткнется. 

Дальше, судя по всему, будет еще интереснее. Разведку на взломанных серверах все чаще поручают агентам, а агент верит тому, что прочитал. Мой взломщик в среднем поставил 7,8 из 10 серверу, файлы которого появлялись за секунду до его команды cat. Обратное направление тоже уже живет: появились ловушки для ИИ-краулеров, которые кормят их бесконечными сгенерированными страницами. 

Для тех, кто строит своих агентов, вывод вполне практичен. Раньше файл на диске был надежным свидетелем: раз он лежит, значит, кто-то его туда положил. Теперь же за ним может стоять модель, которая угадала, что вы хотите найти. А агенту, который читает диски и логи полезно хотя бы сверять размеры и даты прежде чем делать выводы.

Автор: inkedsymon

Источник [6]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/38581

URLs in this post:

[1] памяти: http://www.braintools.ru/article/4140

[2] Запустить ИИ‑роутер →: https://promo.selectel.ru/ai-router/?utm_source=habr.com&utm_medium=referral&utm_campaign=airouter_article_ai-llmdisk_151126_banner_ord

[3] логично: http://www.braintools.ru/article/7640

[4] ВНИМАНИЕ: http://www.braintools.ru/article/7595

[5] случайность: http://www.braintools.ru/article/6560

[6] Источник: https://habr.com/ru/companies/selectel/articles/1107570/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1107570

www.BrainTools.ru

Rambler's Top100