Робот доставщик дороже автомобиля, мокрая трава и открытый JDWP

Как мы приручали SpeedyBot, потеряли к нему доступ и вернули контроль без отвёртки
В начале это выглядело как обычный интеграционный проект. У нас был большой четырёхколёсный робот‑доставщик SpeedyBot стоимостью как новый автомобиль. Главной задачей было отвязать его от китайского облака: оставить штатные навигацию и железо, но перенести управление, данные и операторский интерфейс в нашу Fleet‑систему. А затем научить робота самостоятельно ездить между точками в реальном парке и возвращаться на зарядку.
SpeedyBot выпускает китайская Suzhou Alpha Robotics, также известная по экосистеме CSJBot. Производитель описывает семейство SpeedyBot как роботов для доставки внутри и снаружи зданий, с автономной навигацией, объездом препятствий и автоматической зарядкой. В нашем экземпляре работали Linux на ARM64, ROS, SLAM, несколько камер и лидаров, Java SDK alpha-pro-sdk и отдельное приложение для картографирования alpha-pro-sdk-scan.
На рекламной странице всё это выглядело почти готовым продуктом. На практике получился увлекательный курс по робототехнике, сетям, реверс‑инжинирингу и цифровой криминалистике.
Почему нельзя было оставить заводское облако
Китайская программная экосистема работала нестабильно и плохо соответствовала модели реального сервиса. Производитель выдал один аккаунт дистрибьютора, а нормальной системы клиентских аккаунтов и разграничения доступа не было. API у облака существовал, но многие нужные нам операции через него либо отсутствовали, либо не давали построить надёжный Fleet‑продукт.
Поэтому мы решили заменить облачную часть: написать собственный backend и развернуть непосредственно на роботе свой Edge. Edge должен был общаться со штатными SDK и SLAM локально, а с нашей Fleet‑панелью через независимый канал. Такая схема сохраняла уже работающую низкоуровневую навигацию и не привязывала оператора к чужому аккаунту и интерфейсу.
Однако довольно быстро выяснилось, что простого адаптера поверх опубликованного API недостаточно. Для карт, управления питанием, русской озвучки, устойчивой навигации и диагностики приходилось разбираться во внутренних сервисах робота гораздо глубже, чем предполагала документация производителя.
Робот, который не хотел становиться российским
Первой проблемой оказалась связь. Российская SIM‑карта регистрировалась в сети, модем видел оператора и хороший сигнал, но передачи данных не было. Заводская служба ожидала интерфейс wwan0, которого на этой аппаратной конфигурации не существовало. Одновременно маршрут по умолчанию указывал на собственный адрес робота, а DNS‑запросы регулярно уходили в тайм‑аут.
Мы восстановили LTE через штатные NetworkManager и ModemManager, настроили APN, маршрутизацию и DNS, не ломая сервисную точку доступа. Робот наконец научился сам возвращаться в сеть после перезагрузки.
Следом выяснилось, что его голосовая жизнь в основном проходит на английском. Для проекта в России это было неудобно, поэтому мы исследовали штатную систему аудиособытий, подготовили русские реплики и установили локализованный комплект. Всего пришлось проверить 144 фактических пути к аудиофайлам: у робота даже простая фраза оказалась частью довольно разветвлённой внутренней системы.
Картографирование тоже не было полностью локальным. Вход мог отвечать формальным успехом без токена, сохранение карты зависело от облачного списка, а отдельный scan‑сервис однажды застрял со старым служебным токеном. Временами казалось, что робот способен построить карту местности, но не способен доказать самому себе, что имеет право её сохранить.
Трава как тест на честность телеметрии
Главный сценарий проекта был уличным, а не гостиничным. Клиент собирался запускать доставщика в парке, где маршруты проходят по дорожкам из мелкой каменной крошки и рядом с газонами. Именно эта поверхность показала слабое место штатной механики и навигации.
SpeedyBot умеет эффектно разворачиваться почти как танк. В одном из таких манёвров он крутит одним ведущим колесом, а специальные задние колёса состоят из поперечных роликов. На твёрдом гладком полу это позволяет развернуть тяжёлый корпус почти на месте. На траве, влажном грунте или сыпучей дорожке такой манёвр работает плохо: колесо прокручивается, ролики зарываются или скользят, а корпус остаётся почти на том же курсе.
Штатный planner усугублял проблему. Он строил маршрут от точки к точке, не учитывая начальный угол корпуса при выборе траектории. Перед началом движения робот пытался сначала повернуться на месте в направление первого участка маршрута. Колёса уже сообщали вращение, поэтому planner видел ложный прогресс, хотя SLAM‑поза почти не менялась.
Нагрузка на привод доходила примерно до 19 ампер, после чего аппаратная защита отключала его и колёса разблокировались. При этом навигационная задача могла остаться в состоянии running. В результате робот просто стоял в случайном месте парка, не ехал, не завершал задание и больше не удерживал колёса. Для публичного пространства это недопустимо: дорогой аппарат можно было просто укатить руками.
Это был важный урок: движение колёс и движение робота не одно и то же. Мы перестроили работу с маршрутами, исключили опасный стартовый разворот, добавили честную обработку отключения привода и добились устойчивых поездок по нашему маршруту. К демонстрации робот уже ездил между точками и возвращался на док без прежней постоянной пробуксовки.
Первый ключ лежал в SDK
Официальной документации для полноценного сервисного доступа у нас не было, а ответы производителя становились всё более сдержанными по мере того, как наши вопросы становились конкретнее.
Первый настоящий доступ мы нашли не перебором паролей. В текущем Java SDK оказался класс SshClientUtil, а в нём зашифрованные константы адреса, пользователя и пароля для SSH. Мы статически разобрали байткод, восстановили использованный алгоритм расшифровки и получили учётную запись nvidia.
Вход с проверкой SSH host key сработал. После этого мы спокойно работали с роботом: исправляли LTE, изучали ROS и штатный SDK, делали резервную копию накопителя, строили карты и постепенно заменяли облачную операторскую логику собственной Fleet‑панелью.
Одна деталь всё это время немного беспокоила. По LTE шёл интенсивный обмен с китайским облаком, по нашим наблюдениям в отдельные периоды около 16 обращений в секунду. Большая часть наблюдаемого обмена была похожа на телеметрию. Передачу видеопотока с камер мы не обнаружили, но и строго доказать её невозможность тогда не могли. Проект горел, робот ездил, поэтому глубокий сетевой аудит постоянно откладывался.
̶3̶ 22 сентября
Вечером 22 сентября привычная учётная запись внезапно перестала работать. Последний новый SSH‑вход прошёл в 17:08. В 17:17 пароль уже отвергался. Не помогли сохранённая заводская пара, пустой пароль, отдельно полученный пароль и несколько локальных ключей. Позже выяснилось, что наши публичные ключи вообще не были установлены в authorized_keys, поэтому пароль оставался единственной реальной дверью.
Мы проверили сеть, адрес и закреплённый host key: это был тот же робот, а SSH‑сервер продолжал отвечать. Попробовали доступ через сервисную AP и обычную LAN, перезагрузили робота штатной командой, исследовали пользователей root, alpha, nvidia и контейнерного firefly. Проверили NoMachine/NX и другие служебные поверхности. У нас оставалась полная копия накопителя, но разбирать робота и изменять образ офлайн не хотелось: это был самый тяжёлый и рискованный вариант.
Монитор и клавиатуру к этому экземпляру подключить было нельзя. Получилась классическая закрытая коробка: Linux внутри жив, сеть работает, данные наши, но обычного пути внутрь больше нет.
Дверь, которую забыли закрыть
При инвентаризации сервисов мы заметили два открытых Java Debug Wire Protocol порта. Основной alpha-pro-sdk слушал JDWP на 5005, а приложение картографирования alpha-pro-sdk-scan на 5006. Успешный путь нашёлся через 5006.
JDWP предназначен для отладки Java‑приложений: он позволяет остановить поток, посмотреть объекты и выполнить выражение в контексте процесса. В нашем случае scan‑приложение работало в привилегированном root‑контейнере, имело доступ к блочному устройству и bind‑mount на той же ext4, где находилась домашняя директория nvidia.
Мы действовали максимально щадяще. Сначала через read‑only debugfs проверили файловую систему, владельцев и inode. Затем проверили доступ к существующему каталогу .ssh через обычный Linux VFS. Только после этого, без смены паролей, без изменения sshd_config и без сырой записи в работающую ext4, добавили свой публичный ключ в authorized_keys. Для открытия каталога из контейнера пригодился системный вызов open_by_handle_at. Приватный ключ при этом никогда не покидал наш компьютер.
После освобождения остановленного Java‑потока новая SSH‑сессия открылась. Пользователь nvidia снова был наш, вместе с доступом к sudo и Docker. Робот не пришлось разбирать, перепрошивать или подключать к монитору.
Что рассказал журнал
Вернув доступ, мы смогли восстановить события почти по минутам.
В 17:09 штатный клиент frpc открыл внешний reverse‑proxy для SSH. В 17:10 на роботе началась интерактивная SSH‑сессия nvidia, которая локально выглядела как подключение с 127.0.0.1: типичный след проксирования через туннель. Затем последовательно запускались команды смены паролей root, alpha и nvidia. Смена alpha была отвергнута политикой паролей, остальные изменения прошли. В 17:17 сессия и соответствующее FRP‑соединение закрылись, а через несколько десятков секунд мы впервые получили отказ при обычном SSH‑входе.
То есть транспорт инцидента был установлен: встроенный в робот FRP‑туннель опубликовал SSH и NX наружу, после чего через него прошла интерактивная сессия со сменой паролей. Но журналы не позволяют честно назвать человека по другую сторону туннеля. Мы можем доказать путь и команды, но не личность оператора.
Заодно обнаружился второй внешний контур: root‑процесс штатного SDK поддерживал MQTT и HTTPS‑соединения с облаком производителя. Статический анализ показал, что этот канал умеет принимать не только телеметрию, но и команды движения, питания, карт, OTA и настройки самого FRP.
После расследования мы отключили frpc, заблокировали подтверждённые внешние адреса производителя на самом роботе и оставили работающими локальный SDK, ROS, LTE и наш Fleet realtime. JDWP сохранили только как локальный аварийный путь восстановления из доверенной сети: теперь он подробно задокументирован и не доступен через прежний внешний туннель.
Чем всё закончилось
Проект мы всё‑таки показали. Робот ездил по точкам, говорил по‑русски, возвращался на зарядку и управлялся через нашу панель. А ещё он окончательно перестал быть чёрным ящиком, который можно использовать только пока работает чужое облако и подходит найденный в SDK пароль.
Главный вывод оказался не про JDWP. Купленное оборудование остаётся вашим лишь тогда, когда у вас есть собственный, проверенный и документированный путь доступа, резервная копия, наблюдаемая сеть и возможность отключить чужой канал управления без потери основных функций.
Из этой истории получились два вполне практических правила.
Если вы внедряете чужое железо и получили к нему законный административный доступ, не откладывайте вопрос контроля «до завершения проекта». Сразу проведите инвентаризацию исходящих соединений, удалённого управления и обновлений. Отключите ненужные vendor‑туннели и облачные команды либо изолируйте их собственными сетевыми правилами. Установите собственный ключ, сохраните проверенный образ, подготовьте как минимум два независимых пути восстановления и действительно испытайте их. Пароль из SDK не является recovery‑планом.
Если вы создаёте собственное железо, задача обратная: не отдавайте заказчику устройство с открытым JDWP, зашитыми привилегированными учётными данными, root‑контейнерами с доступом к host filesystem и постоянно работающим reverse‑туннелем. Отладочные поверхности должны закрываться в production, сервисный доступ должен быть явным и аудируемым, ключи уникальными для каждого устройства, а процедура восстановления должна принадлежать владельцу, а не существовать случайно как побочный эффект уязвимости.
Лев Ларин, технический директор в Unibot.
Публичные сведения о продукте и производителе:
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.