Всем привет! Я Андрей, в Яндексе работаю над IoT‑железками в Городских сервисах. Но сегодня речь пойдёт не о них. Эта история началась с неожиданной находки в новой квартире — с обычной, на первый взгляд, трубки домофона. Кажется: ну трубка и трубка. Но вот что бросилось в глаза: она была подключена по Ethernet. А если есть Ethernet, значит, внутри что‑то с TCP/IP, то есть уже маленький компьютер.

Любопытство победило, и я решил разобраться, как же устроена эта железка и что она умеет. Провёл небольшое техническое расследование: от осмотра корпуса до анализа сетевых пакетов и выяснения, какие протоколы вообще использует домофон.

Я давно увлекаюсь железками и автоматизацией дома — эдакий «полу‑умный» дом, который вроде бы и работает сам, но иногда всё же требует вмешательства человека. Поэтому, обнаружив Ethernet‑домофон, я и решил разобраться в нём. 

Основная цель эксперимента — научить трубку открывать дверь по решению автоматизации в Home Assistant. Побочная — внести вклад в сообщество и, возможно, помочь самому вендору, если в процессе вскроются любопытные особенности или проблемы. 

Знай врага в лицо

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

Информации о модели в интернете оказалось немного, но производитель всё‑таки указал несколько ключевых характеристик:

  • веб‑интерфейс;

  • кнопка «Консьерж»;

  • режим громкой связи; 

  • поддержка протоколов SIP и TCP/IP; 

  • питание DC 5V/2A или PoE 802.3af; 

Веб‑интерфейс — уже хороший знак: значит, можно будет посмотреть настройки. Оставалось лишь найти способ заглянуть внутрь. И тут удача — со стороны крепления кто‑то заботливо подписал IP‑адрес трубки маркером прямо на корпусе. Видимо, устройство работает со статической адресацией. Отличная точка входа для исследования.

Наклейка с подсказками
Наклейка с подсказками

Подключаемся к сети домофонов

С точки зрения протоколов трубка оказалась вполне предсказуемой: она работает как обычный SIP VoIP‑телефон, точно такие же стоят, например, в офисах Яндекса. Значит, взаимодействовать с ней можно привычными средствами.

В моём случае трубка питалась прямо по Ethernet — через PoE. Этот стандарт часто используется в офисах для точек доступа и IP‑телефонов: питание идёт по тому же кабелю, что и данные, что избавляет от лишних проводов. Если вы сейчас на работе — посмотрите на потолок: возможно, там висит коробочка, питающаяся тем же способом и раздающая вам Wi‑Fi.

К счастью, мой роутер MikroTik RB5009UPr+S+ умеет запитывать устройства по стандарту 802.3af, так что вопрос с питанием решился сам собой.

Дальше — немного магии RouterOS. В терминологии MikroTik «бридж» — это программный коммутатор второго уровня.То есть нам нужно сделать отдельный мост между трубкой и сетью домофонов, чтобы не мешать основной сети.

  1. Создаём новый бридж, называем его intercom.

  2. Перевешиваем на него два порта — трубку и порт uplink, — чтобы изолировать от локальной сети. 

  3. Добавляем маршрут до сети домофонов. Маску мы не знаем, но можно взять /16 от IP, написанного на корпусе трубки. Например: /ip/route/add dst-address=10.17.0.0/16 gateway=eth-intercom-uplink

Эти же действия можно было выполнить и с помощью обычного неуправляемого коммутатора. Но под рукой был только MikroTik.

После всех манипуляций втыкаем оба провода в роутер. Теперь он работает как прозрачный коммутатор между трубкой и сетью домофонов. Снимаем трубку — слышен гудок. Отлично: значит, устройство подключено к SIP‑серверу и связь работает.

Что там в админке

Маршруты настроены, связь есть — пора заглянуть внутрь. Обычно в такой момент кулхацкеры первым делом сканируют устройство на открытые порты. Но поскольку я уже знаю, что у трубки есть веб‑интерфейс, начнём с него. Вбиваю IP‑адрес в браузер — и вижу страницу с авторизацией.

Для начала нужно понять, как реализована авторизация. Открываю инструменты разработчика, пробую наугад логин admin и пароль admin. Браузер немного думает… и вот я попадаю внутрь.

Главная страница админки
Главная страница админки

Креды по умолчанию — admin/admin. Ну что ж, зато ничего ломать не пришлось.

На главной странице админки сразу отображаются базовые характеристики устройства:

  • ROM: 1,5/16 M

  • RAM: 1/23 M

Не густо, Python, конечно, не запустишь, но жить можно.

Что удалось найти внутри

Полазив по разделам, вытаскиваю несколько действительно полезных вещей:

  • Настройки сети. Тут, наконец, выяснилось, что маска подсети — /23.

  • Параметры SIP‑подключения. Имя пользователя и номер телефона формируются просто: 6 + номер квартиры.

  • DTMF‑код открытия двери. Это те самые тональные сигналы, которые мы все слышали в голосовом меню колл‑центров. В моём случае комбинация 123# отправляется на SIP‑сервер — и тот понимает, что дверь нужно открыть.

Кроме того, трубка унаследовала от своих старших братьев из линейки Fanvil богатую функциональность: поддержку OpenVPN, 802.1X‑аутентификации и других корпоративных штук, явно избыточных для обычной квартиры.

Пароль от SIP‑сервера, к сожалению, увидеть не удалось — в интерфейсе он скрыт звёздочками. Я проверил HTML‑код на всякий случай, но и там: value="****".

На удивление секурно!

Больше настроек богу настроек

На этом этапе я немного застрял: пароль от SIP‑сервера админка упрямо не показывала, а значит, подключиться напрямую без самой трубки не получится. В голове уже крутилась идея: а не послушать ли трафик между трубкой и сервером с помощью Wireshark? Тем более, RouterOS позволяет делать такие штуки буквально одним кликом: включил сниффер, направил на Wireshark, и вот тебе весь SIP‑трафик как на ладони.

Но прежде чем привлекать тяжёлую артиллерию, я вспомнил, что где‑то в интерфейсе промелькнула опция импорта и экспорта настроек. Если есть экспорт — значит, где‑то должен лежать файл конфигурации. А если есть файл, то, возможно, там и спрятан тот самый пароль.

Пора посмотреть, что именно отдаёт устройство и в каком формате сохраняет настройки.

Страница экспорта/импорта настроек
Страница экспорта/импорта настроек

Жмём на ссылку и получаем XML‑файлик — никакого хитрого шифрования. Внутри очень много настроек, но нам нужен всего лишь пароль…

<line index="1">
  <PhoneNumber>6149</PhoneNumber>
  <DisplayName>6149</DisplayName>
  <SipName></SipName>
  <RegisterAddr>10.17.0.1</RegisterAddr>
  <RegisterPort>5060</RegisterPort>
  <RegisterUser>6149</RegisterUser>
  <RegisterPswd>6149</RegisterPswd>
  <RegisterTTL>3600</RegisterTTL>
</line>
<!-- и еще 1500 строк -->

..а пароль (RegisterPswd) совпадал с юзернеймом SIP‑аккаунта. Сработавший admin/admin ничему меня не научил:( 

Копаем глубже

Итак, все данные сервера у меня есть. У самурая два пути:

  • полностью выкинуть трубку и подключиться к SIP‑серверу напрямую с помощью Asterisk — всё для этого уже готово;

  • заставить трубку делать всю работу через её собственный API (если он есть) — то есть писать софт для трубки.

Я выбрал второй вариант. Думаю, написать софт прямо для трубки домофона — это весело.

Вернёмся к XML с параметрами. И там — прекрасное.

<web>
    <EnableTelnet>0</EnableTelnet> <!-- ← это же Telnet! -->
    <TelnetPort>23</TelnetPort>
    <TelnetPrompt></TelnetPrompt>
    <LogonTimeout>30</LogonTimeout>
    <account index="1">
        <Name>admin</Name>
        <Password>admin</Password>
        <Level>10</Level>
    </account>
</web>

Telnet — это очень простой, как палка, протокол из 1972 года. Используется для удалённого подключения к шеллу. Галочку, включающую Telnet, в админке я не видел. Но на всякий случай захожу в раздел с настройками портов и проверяю инпуты в коде страницы.

В коде просто спрятали опцию через CSS. Очевидно, S в CSS — это Safety & Security. Убираем стили, нажимаем галочку и подключаемся по Telnet.

Удивительно, но admin/admin не подходит
Удивительно, но admin/admin не подходит

Теперь знаем две важные вещи:

  • Трубка работает на Linux, о чём свидетельствует using Poky 13.0 — это значит, что прошивка сделана на референсном дистрибутиве Yocto Project.

  • Название платформы, на которой эта трубка построена, — DSPG DVF97. Под этим названием скрывался SoC для VoIP‑телефонов (внезапно).

Тем временем я нашёл подробный мануал для другого телефона, но с такой же прошивкой. В мануале честно указано наличие Telnet в виде скрытой настройки.

<...> Then, type root and will go to the phone system. Погодите, что?
<...> Then, type root and will go to the phone system. Погодите, что?
Вот и шелл. Ещё и рутовый
Вот и шелл. Ещё и рутовый

Пользователем оказался root, а пароля вовсе не было. Такая безопасность, конечно, удручает, но и упрощает реверсерам жизнь. Железка окончательно повержена.

Грызем кактус и юзерспейс

Теперь есть полное понимание, что за железо стоит в трубке:

  • Слабенький одноядерный ARMv5-процессор.

  • Из 24 МБ ОЗУ свободно 1,5 МБ, а доступно около 6 МБ (в чём разница, объясняется на этой странице). 

  • Корневая ФС примонтирована как read‑only, используется SquashFS, а значит, «поправить» систему не получится.

Даже не знаю, что больше всего добавляет сложностей — настолько малый объём памяти или столь древняя архитектура.

Nintendo DS из 2004 года тоже работает на ARMv5
Nintendo DS из 2004 года тоже работает на ARMv5

Сразу отметаем интерпретируемые языки. В основном я пишу на Go, но он слишком прожорлив — получаются толстые бинарники, которые не поместились бы на флешку. C и C++ для такой задачи я отбросил: слишком больно и громоздко.

Остаётся только Rust. Он весьма экономичен по памяти (no GC, zero cost abstractions), и вокруг Rust сложилась экосистема — крутая система сборки и море пакетов. А ещё он поддерживает нужную мне архитектуру.

Сборка "Hello, world!"

Кросс‑компиляция (сборка под архитектуру, отличную от хоста) подразумевает установленные тулчейны. К счастью, в репозитории Fedora всё есть. Цели (target) для сборки бинарников задаются триплетами (triplet) — строками вида «архитектура‑платформа‑ОС‑ABI».

Для трубки подошёл таргет armv5te-unknown-linux-musleabi.

# добавляем целевую платформу для Rust
rustup target add armv5te-unknown-linux-musleabi

# и устанавливаем утилиты работы с бинарниками под ARM
# оттуда мне нужен только линковщик (ld)
# => Fedora
dnf install -y gcc-arm-linux-gnueabi
# => macOS
brew install arm-linux-gnueabihf-binutils

В настройки Cargo (.cargo/config.toml) подкидываем пути до линкера, иначе он попробует собрать всё с помощью линкера хоста.

[target.armv5te-unknown-linux-musleabi]
linker = "arm-linux-gnueabihf-ld"
ar = "arm-linux-gnueabihf-ar"

Написать «Hello, world!» в консоль — моветон. Я решил собрать HTTP‑сервер, да чтоб на модном асинхронном фреймворке. HTTP‑сервер нам ещё пригодится, но об этом позже.

use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(|| async { "Hello, World!" }));

    println!("Listening on port 3000");
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Собираем бинарник с --release‑флагом, скачиваем его в /tmp трубки (через wget). И запускаем.

Консоль трубки
Консоль трубки
Привет из браузера
Привет из браузера

Автозапуск в read-only-системе

Рабочий HTTP‑сервер — это замечательно, но надо бы научить его запускаться вместе с системой. Я планирую использовать MQTT для автоматизации, поэтому процесс должен работать фоново. Очевидно, что трубка где‑то хранит свои настройки. Узнать точки монтирования поможет команда mount.

root@dvf97:~# mount
rootfs on / type rootfs (rw)
/dev/root on / type squashfs (ro,relatime)
devtmpfs on /dev type devtmpfs (rw,relatime,size=12124k,nr_inodes=3031,mode=755)
proc on /proc type proc (rw,relatime)
sysfs on /sys type sysfs (rw,relatime)
debugfs on /sys/kernel/debug type debugfs (rw,relatime)
tmpfs on /run type tmpfs (rw,nosuid,nodev,mode=755)
tmpfs on /var/volatile type tmpfs (rw,relatime)
devpts on /dev/pts type devpts (rw,relatime,gid=5,mode=620)
/dev/mtdblock11 on /mnt type jffs2 (rw,relatime)
/dev/mtdblock7 on /base type squashfs (ro,relatime)
/dev/mtdblock9 on /userdata type squashfs (ro,relatime)
/dev/mtdblock8 on /usr type jffs2 (rw,relatime)

Тут что‑то странное:

  • /userdata, которая, казалось бы, должна хранить данные пользователя, примонтирована как RO. Там хранится кастомизация админской панели и рингтоны.

  • А вот /usr, где хранятся бинарники, смонтирована на запись. Используется JFFS2 — простая журналируемая ФС. Но свободно там всего лишь 32 КБ.

  • В /mnt тоже можно писать. И места много — свободно 1,4 МБ.

Чёрт ногу сломит! 

К слову, подробную таблицу про разделы я оставил в своём Гитхабе.

Раз уж вендор нам дал возможность писать в /usr, то можно заменить какой‑нибудь бинарник или подредактировать скрипт. А свой бинарник положу в /mnt, так как в /usr очень мало свободного места.

Патчим жертву

К счастью, найти жертву‑бинарник оказалось очень просто. Вот список процессов:

root@dvf97:~# ps -
PID USER TIME COMMAND
1 root 0:16 init [5]
 < ... kernel threads ... >
329 root 38:02 {afterUpgrade.sh} /bin/sh /bin/afterUpgrade.sh
350 root 0:00 /sbin/getty -L 115200 ttyS
351 root 0:00 /sbin/getty 38400 tty1
361 root 234:23 {callManager} XGUI
411 root 228:01 app_dsp
508 root 0:41 {ipwatchd-script} /bin/sh /usr/sbin/ipwatchd-script
597 root 0:01 telnetd -p 23
25323 root 0:00 -sh
29050 root 0:00 sleep 60
29067 root 0:00 -sh
29156 root 0:00 sleep 1

На трубке крутится пара процессов (XGUI и app_dsp), которые отвечают за не��осредственные функции телефона. Я не разбирался, как между ними реализован IPC и какие функции у каждого бинарника. Но в чём я уверен — их точно не стоит трогать.

А вот потрогать /bin/sh /usr/sbin/ipwatchd-script — звучит как идеальный план. Он лежит в /usr, куда можно писать. И этот скрипт написан на unix shell, а значит, можно его поправить, ничего не сломав.

Оригинальный скрипт
#!/bin/sh
CM_FIFO=/tmp/ipconflict_fifo
DEVICE=$1
IP=$2
MAC=$3
while true
do
        ipStr=`ifconfig eth0 | grep "inet addr" | awk '{ print $2}' | awk -F: '{print $2}'`
        if [ "$ipStr" ]; then
                arping  -D -c 1 -w 1 $ipStr
                if [ $? == 1 ]; then
                        echo "ip_conflict \"$ipStr\"" > $CM_FIFO
                fi
        fi
        sleep 60
done

exit 0

Скрипт раз в минуту проверяет ARP записи по IP‑адресу телефона. Если произошла коллизия, то выполняется запись в FIFO. Зачем это трубке и какой процесс это читает — не знаю.

Я не стал доверять жизненному циклу ipwatchd-script и решил использовать его как трамплин для своего скрипта. В начало добавим запуск своего init‑скрипта и отвяжемся от ipwatchd-script:

if [ -f /mnt/userdata/hottubes-init.sh ]; then
        /mnt/userdata/hottubes-init.sh &
fi
# ... старый код

Теперь записываю init‑скрипт в /mnt/userdata/hottubes-init.sh:

Код init скрипта
#!/bin/sh
PIDFILE="/run/hottubes.pid"
if [ -f "$PIDFILE" ]; then
    PID=$(cat "$PIDFILE")
    if kill -0 "$PID" 2>/dev/null; then
        echo "hottubes is already running (PID: $PID). Exiting."
        exit 0
    else
        echo "stale PID file found; removing it."
        rm -f "$PIDFILE"
    fi
fi

# ignore SIGHUP so that the child process isn't killed when this shell exits.
trap '' HUP

/mnt/userdata/hottubes </dev/null >/run/hottubes.log 2>&1 &

child_pid=$!

echo "$child_pid" > "$PIDFILE"
echo "started /mnt/userdata/hottubes detached with PID $child_pid."

Осталось переместить бинарник из /tmp в /mnt/userdata/hottubes и проверить в бою. Перезагружаем трубку. Всё работает!

Что делать дальше, я уже примерно представлял:

  • в админке задать вебхуки на localhost, чтобы понимать, в каком состоянии трубка; 

  • дёргать какую‑нибудь ручку для ответа на вызов и отправки DTMF‑кода; 

  • всё это обернуть в конечный автомат; 

  • писать в топики по MQTT и объяснить Home Assistant, как с ними работать. 

Почти что вебхуки

Вернёмся в самое начало — в админку. В разделе Phone → Action можно задать что‑то вроде вебхуков. На целевое устройство прилетает GET‑запрос без полезных данных. Но нужные события тут есть: Idle, Ringing и Talking.

Вот для этого и пригодился HTTP‑сервер! Прописываем вебхуки до 127.0.0.1 (localhost трубка не хочет принимать):

Снял трубку и увидел запросы в консоли — обратные вызовы работают!

Нажимаем кнопки

Для управления трубкой есть всего одна ручка. Ручка кривая, но что поделать. Как ни странно, её достаточно. Мануал нашёл здесь.

Трубка принимает GET /cgi-bin/ConfigManApp.com. Используется Basic‑аутентификация. В параметре key указывается клавиша, которую нужно «нажать». Последовательность простая:

  1. F_ACCEPT — принять вызов.

  2. 1;2;3;POUND — набрать DTMF‑код 123#. Это же действие совершает кнопка с человечком на трубке.

  3. F_RELEASE — положить трубку.

MQTT

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

Осталось только прикрутить Home Assistant 
Осталось только прикрутить Home Assistant 

Для этого идеально подходит MQTT — лёгкий протокол обмена сообщениями, изначально созданный для умных устройств с ограниченными ресурсами. Он работает по принципу publisher/subscriber: устройство публикует события в определённые топики, а другие системы (например, Home Assistant) на них подписываются.

В центре этой схемы стоит брокер — сервер, который принимает сообщения и раздаёт их подписчикам. Самый популярный брокер — Mosquitto, его можно запустить хоть на Raspberry Pi.

При старте бинарник собирает информацию о железке — модель, IP и MAC‑адрес — и отправляет в специальный топик homeassistant/device/{device_id}/config сообщение с длинным JSON.

Вот как формируется JSON
json!({  
    "device": {  
        "name": metadata.model,  
        "manufacturer": metadata.vendor,  
        "model": metadata.model,  
        "sw_version":  metadata.firmware_version,  
        "configuration_url": metadata.ip_address.as_ref().map(|address| format!("http://{}", address)),  
        "identifiers": [state.client_id],  
        "connections": [["mac", metadata.mac_address]],  
    },  
    "origin": {  
        "name": env!("CARGO_PKG_NAME"),  
        "sw": env!("CARGO_PKG_VERSION"),  
        "url": env!("CARGO_PKG_HOMEPAGE"),  
    },  
    "components": {
        "phone": {  
            "unique_id": format!("{}-phone-status", state.client_id),  
            "platform": "sensor",  
            "name": "Phone Status",  
            "device_class": "enum",  
            "icon": "mdi:phone-ring",  
            "options": [  
                "IDLE",  
                "TALKING",  
                "RINGING",  
            ],  
            "state_topic": format!("{}/phone", state.client_id),  
        },  
        "open": {  
            "unique_id": format!("{}-open-button", state.client_id),  
            "platform": "button",  
            "name": "Open Door",  
            "icon": "mdi:door",  
            "command_topic": format!("{}/open", state.client_id),  
        },  
        "ring": {  
            "unique_id": format!("{}-ring-trigger", state.client_id),  
            "automation_type": "trigger",  
            "platform": "device_automation",  
            "type": "phone",  
            "subtype": "ring",  
            "topic": format!("{}/phone", state.client_id),  
            "payload": "RINGING",  
        },  
    },  
});

Последний шаг к победе: реализовать запись в топики и подписаться на команду /open. Finita la commedia!

Что в итоге

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

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

Без хаков, конечно, ничего бы не вышло. Но когда железку можно взломать логином admin/admin и запустить Telnet галочкой, спрятанной через CSS, — понимаешь, в каком прекрасном мире мы живём.

Миссия выполнена. Дверь открыта.

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


  1. pnetmon
    18.11.2025 07:39

    Если уж хакерить то - а позвонить соседям? Хотя по логам могут и найти.

    Непонятен смысл действий.

    Открывать автоматически входную дверь по входящему звонку - так где идентификация входящего? Это как admin, admin.

    А для более сложных задач эта трубка SIP телефонии уже не нужна.


    1. AdrianoVisoccini
      18.11.2025 07:39

      Это как admin, admin.

      нужно сказать, что домофон без видео это буквально дыра в безопасности. Слишком уж просто он взламывается социальным индинирингом, начиная от "это ваш сосед с этажа Х, забыл ключи пустите пожалусто" до "Добрый день, доставка воды/скорая/почта/все-что-угодно пустите"
      так что на самом деле проверять смысла нет никакого. Тот кто захочет зайти - зайдет. Как пруф- тысячи закладкичков которые ежедневно обкладывают подъезды наркотиками без каких либо проблем