5 шокирующих уязвимостей EU‑приложения проверки возраста: как взломать за 2 минуты и что делать
17 апреля 2026 г.Вступление
В эпоху, когда цифровые идентификаторы становятся обязательным элементом повседневной жизни, вопросы защиты персональных данных выходят на первый план. Европейская инициатива по проверке возраста через мобильное приложение обещала упростить доступ к онлайн‑контенту, но уже в первые недели после публикации в открытый доступ исследователь безопасности Пол Мур продемонстрировал, что приложение можно скомпрометировать менее чем за две минуты, имея лишь физический доступ к смартфону. Эта новость вызвала бурную дискуссию в сообществе: от скептицизма до тревоги о потенциальных последствиях для миллионов пользователей.
「技術は刃、使い方次第で光にも闇にもなる」
— японское хокку, напоминающее о двойственной природе технологий.
Пересказ Reddit‑поста простыми словами
Исследователь Пол Мур обнаружил, что в приложении для проверки возраста (EU Age Verification) хранится зашифрованный PIN‑код в файле shared_prefs. На деле шифрование оказывается поверхностным: достаточно открыть файл, удалить записи PinEnc и PinIV, и приложение «запомнит», что PIN‑код отсутствует. После перезапуска пользователь может задать новый PIN, а приложение автоматически примет его и предоставит доступ к уже существующим данным проверки возраста.
Кроме того, в том же файле находятся параметры ограничения количества попыток ввода (rate‑limiting) и флаг, отвечающий за биометрическую аутентификацию (UseBiometricAuth). Сбросив счётчик попыток до нуля и установив флаг в false, злоумышленник полностью отключает защиту от перебора и биометрический контроль.
Самое тревожное – приложение сохраняет биометрические данные (NFC‑скан, фотографии лица) в виде обычных PNG‑файлов без шифрования. Любой, кто получит доступ к файловой системе устройства, может вытащить эти изображения и использовать их в дальнейшем.
Суть проблемы, хакерский подход и основные тенденции
- Неправильное хранение секретов. PIN‑код шифруется, но ключ и вектор инициализации хранятся рядом в открытом виде.
- Отсутствие привязки к защищённому хранилищу. Данные не связаны с Android Keystore, поэтому любой с root‑правами может их изменить.
- Слабая биометрическая защита. Флаг
UseBiometricAuth– обычный булевый параметр, который можно переключить без проверки подписи. - Незашифрованные биометрические изображения. PNG‑файлы легко копировать и использовать в атаках «подделки лица».
Тенденция, наблюдаемая в нескольких проектах с открытым исходным кодом, – стремление быстро выпустить демо‑версии без достаточного внимания к безопасности. Это создаёт «идеальное» поле для демонстраций уязвимостей, но также повышает риск, если такие версии попадают в продакшн.
Детальный разбор проблемы с разных сторон
Техническая сторона
Файл shared_prefs – это обычный XML‑файл, доступный приложению и, при наличии root‑прав, любому пользователю. В нём хранятся:
- Зашифрованный PIN (
PinEnc) и вектор (PinIV). - Счётчик попыток (
RateLimitCounter). - Флаг биометрии (
UseBiometricAuth).
Отсутствие подписи или проверки целостности позволяет просто удалить нужные строки, а приложение воспринимает отсутствие данных как «первый запуск» и предлагает задать новый PIN.
Юридическая и регуляторная сторона
EU Digital Identity Wallet (EUDI) позиционируется как часть стратегии ЕС по цифровой идентификации. Любые уязвимости могут привести к нарушению GDPR, поскольку биометрические данные считаются «особой категорией» персональных данных. Нарушения могут обернуться штрафами до 20 млн евро или 4 % от годового оборота компании.
Пользовательская сторона
Для обычного пользователя «физический доступ к устройству» кажется маловероятным, однако в реальном мире смартфоны часто оставляются без присмотра, а многие пользователи рутируют свои устройства для установки кастомных прошивок. В таком случае уязвимость становится реальной угрозой.
Экономическая сторона
Если приложение будет использоваться в коммерческих сервисах (онлайн‑казино, продажа алкоголя), компрометация может привести к финансовым потерям и репутационным рискам для компаний‑партнёров.
Практические примеры и кейсы
Ниже – упрощённый сценарий, демонстрирующий, как за 2 минуты можно получить доступ к данным:
- Подключаем смартфон к ПК через USB и получаем root‑доступ (например, через Magisk).
- Открываем
/data/data/com.eu.ageverification/shared_prefs/prefs.xmlв любом текстовом редакторе. - Удаляем строки
<string name="PinEnc">…</string>и<string name="PinIV">…</string>. - Сбрасываем
RateLimitCounterдо0и меняемUseBiometricAuthнаfalse. - Перезапускаем приложение, задаём новый PIN и получаем доступ к ранее сохранённым биометрическим данным.
Экспертные мнения из комментариев
«Ehh. That's just dumb IMO. You need physical access to an unlocked device. And to edit or access shared prefs, the device has to be rooted. I fail to see any realistic risk based on this. If someone has access to your unlocked rooted device practically all your apps are "hackable".» — i__suck__toes
«This is their Github: https://github.com/eu-digital-identity-wallet/eudi-app-android-wallet-ui. And honestly, it's too overblown as it currently is just a demo as said in its readme.» — Thaun_
«Physical device access is a tall order...» — apnorton
Большинство комментариев указывают на то, что уязвимость актуальна лишь при наличии root‑прав, но согласны, что даже демо‑версия должна соответствовать базовым требованиям безопасности.
Возможные решения и рекомендации
- Перенести хранение PIN‑кода в Android Keystore. Ключи и векторы будут защищены аппаратно и недоступны без авторизации.
- Шифровать биометрические изображения. Использовать AES‑GCM с ключом, хранящимся в Keystore, и сохранять файлы в зашифрованном виде.
- Подписывать файл shared_prefs. Добавить HMAC‑подпись, проверяемую при каждом запуске.
- Ужесточить политику доступа к файлам. Запретить чтение/запись без привилегий system‑uid.
- Внедрить детектирование root‑устройства. При обнаружении рут‑прав приложение должно отказываться от работы или переходить в ограниченный режим.
- Регулярные аудиты кода. Привлекать сторонних специалистов для проверки безопасности перед выпуском новых версий.
Заключение и прогноз развития
Уязвимость EU‑приложения проверки возраста демонстрирует, насколько важно учитывать безопасность уже на этапе прототипа. Ожидается, что в ближайшие годы регуляторы ЕС усилят требования к защите биометрических данных, а разработчики будут вынуждены интегрировать более надёжные механизмы шифрования и контроля доступа. Вероятно, появятся новые стандарты «Secure Mobile Identity», включающие обязательное использование Android Keystore и обязательный аудит кода перед публикацией.
Для пользователей советуем держать устройства обновлёнными, избегать рутования без надёжных мер защиты и использовать менеджеры паролей для хранения PIN‑кодов, а не полагаться исключительно на встроенные механизмы приложений.
Практический пример (моделирующий ситуацию)
# -*- coding: utf-8 -*-
# Пример демонстрирует, как можно «подменить» файл shared_prefs,
# удалив из него записи о PIN и отключив биометрию.
# В реальном мире такой файл находится в /data/data//shared_prefs/
# Для упрощения используем локальный XML‑файл.
import xml.etree.ElementTree as ET
import os
def load_prefs(path: str) -> ET.ElementTree:
"""Загружает XML‑файл настроек."""
return ET.parse(path)
def save_prefs(tree: ET.ElementTree, path: str):
"""Сохраняет изменённый XML‑файл."""
tree.write(path, encoding='utf-8', xml_declaration=True)
def patch_prefs(tree: ET.ElementTree):
"""Удаляет PIN, сбрасывает счётчик и отключает биометрию."""
root = tree.getroot()
# Удаляем элементы PinEnc и PinIV, если они есть
for tag in ['PinEnc', 'PinIV']:
elem = root.find(f".//string[@name='{tag}']")
if elem is not None:
root.remove(elem)
# Сбрасываем счётчик попыток
counter = root.find(".//int[@name='RateLimitCounter']")
if counter is not None:
counter.set('value', '0')
# Отключаем биометрию
bio = root.find(".//bool[@name='UseBiometricAuth']")
if bio is not None:
bio.set('value', 'false')
# Путь к тестовому файлу (в реальном случае – путь к shared_prefs)
prefs_path = 'prefs_demo.xml'
# Создаём примерный файл, если его нет
if not os.path.exists(prefs_path):
sample = '''
'''
with open(prefs_path, 'w', encoding='utf-8') as f:
f.write(sample)
# Загружаем, модифицируем и сохраняем
tree = load_prefs(prefs_path)
patch_prefs(tree)
save_prefs(tree, prefs_path)
print(f"Файл {prefs_path} успешно изменён.")
Код имитирует процесс «подмены» настроек: удаляет зашифрованный PIN, сбрасывает счётчик попыток и отключает биометрическую аутентификацию. В реальном сценарии такой файл находится в защищённой директории, доступ к которой возможен только при наличии root‑прав.
Оригинал