
Недавно я купил дешёвую камеру Tapo, чтобы понимать, чем занимается мой пёс, пока меня нет дома.
И что в результате? Я выполнил реверс-инжиниринг потоков онбординга, декомпилировал APK, занимался MITM TLS-сессий и писал криптографические скрипты.
Основной моей мотивацией к созданию этого проекта стало то, что с первого дня установки камера начала меня раздражать. Настраивать её во frigate было довольно утомительно: похоже, никто точно не знает, как эти камеры работают онлайн.
ПРИМЕЧАНИЕ: если вы хотите, чтобы во frigate работало двунаправленное аудио, то необходимо использовать для основного потока конфигурацию go2rtc
tapo://
, а не обычнуюrtsp://
. TP-Link ленив и реализует двустороннее аудио только в собственном проприетарном API.
Я споткнулся об следующее незадокументированное поведение: после онбординга API устройства должно было принимать учётные данные admin
:<облачный-пароль-tapo>
. Однако побившись головой о стену пару часов, я обнаружил, что если поменять облачный пароль после онбординга, то связанные устройства не получают уведомления об этом.
Из этого я сделал два вывода, заставивших крутиться шестерёнки в мозгу:
Должно быть, во время онбординга происходит вызов, синхронизирующий пароль устройства с облачным паролем.
До этого этапа устройство или должно разрешать вызовы без аутентификации, или у него есть некий пароль по умолчанию.
Поэтому, учитывая мои мучения с онбордингом и то, что меня начинает трясти каждый раз, когда приложение tapo пытается втюхать мне подписку на «Tapo Care», всё более привлекательной выглядела мысль о безоблачном решении для онбординга в устройство.
Чтобы взломать эту систему, нужно было первым делом разнюхать, о чём переговариваются приложение и камера при онбординге. То есть организовать атаку посредника (man in the middle).
Man in the middle
Чтобы выполнить атаку посредника на телефонное приложение, необходима возможность маршрутизации всего трафика http(s) через контролируемый нами прокси-сервер. Раньше реализовать это было довольно просто: достаточно настроить прокси на компьютере, добавить в хранилище доверенных сертификатов телефона самоподписанный прокси-сервером сертификат и сконфигурировать телефон, чтобы он общался с прокси.
Однако сегодня такое решение неэффективно из-за неприятных трюков, используемых современными телефонными приложениями. В частности, они нагло игнорируют прокси, отказываются от использования хранилища доверенных сертификатов и своевольно пользуются закреплением сертификатов (certificate pinning).
Следовательно, самой надёжной методикой обобщённого MITM приложения становится динамический инструментарий наподобие frida
. Он позволяет заставить приложение использовать указанные нами прокси и сертификаты, борясь с их попытками выполнять действия наподобие закрепления сертификатов.
Итак, моя система в конечном итоге выглядела так (полное руководство по настройке можно посмотреть на Github):

После запуска mitmproxy
, инъецирования скриптов frida и онбординга камеры мы наконец-то видим начальный поток логина ещё до того, как меняется пароль администратора:
Однако последующие запросы выглядят так:
{
"method": "securePassthrough",
"params": {
"request": "bAhdgihJ9j6PrrknnbXWATBohGTZK5llv3MEzRcmoAmcxexmlVNz3OUX2r0h9a9EG/3X0tBpPi654T2+BjqVEOn2D178kokBpf8RQj01AvBZLYD5S5sFeaCXWiRXA7MgQUppROV4AbrU4f+GOM37KgPqT59qgLVja2slw6CzrKjPzOrG4Ho6Mu6wBa1xepcj"
}
}
А ответы — так:
{
"seq": 584,
"result": {
"response": "Gqz1wbXAig/3wL+kXzY2Ig3hq+JSYasYI7FXdMNZR5PyH8bpLX+GJqQbImUtby9IEj5HQDhxqcTa+dUqQjI0GaGCxuGHqmrgQ0FeyCTQjBiW5gslAPQG33wj44OOkAep"
},
"error_code": 0
}
То есть из этого первого исследования мы выяснили следующее:
У Tapo совершенно точно есть пароль по умолчанию, потому что она выполняет полный логин, ещё ничего не зная про облачный пароль.
У Tapo есть зашифрованный канал
securePassthrough
для вызовов API, чтобы любопытствующие вроде меня не разнюхали все секреты.
JADX
Следующим логичным шагом будет декомпиляция apk в JADX и поиск пароля по умолчанию.
Первый запрос логина, который мы перехватили, ссылается на имя пользователя admin
:
{
"method": "login",
"params": {
"cnonce": "AD0E189F6E1BA335",
"encrypt_type": "3",
"username": "admin"
}
}
Поискав "admin"
в JADX, мы найдём множество совпадений, но любопытными кажутся немногие, находящиеся в классе CameraOnboardingViewModel
:

Похоже, функция m98131y2
возвращает пароль, который затем передаётся вызову new Account()
. Проследовав за этой функцией по цепочке, мы находим золотую жилу:
Мы уже знаем, что устройство использует encrypt_type: 3
, то есть пароль по умолчанию будет таким: TPL075526460603
Учим mitmproxy новым трюкам
Узнав пароль по умолчанию, мы теперь имеем все возможности получения ключей сессий и декодирования сообщений securePassthrough
.
Единственное, что помогло бы нам ещё больше — это справочная реализация потока аутентификации. Здесь нам на помощь приходит PyTapo.
Воспользовавшись PyTapo в качестве справки, мы можем сдампить состояние сессии и зашифрованные сообщения из mitmproxy, а затем написать скрипт, выполняющий статический анализ дешифрованных запросов и ответов; однако здорово, что mitmproxy
сам поддерживает скриптинг.
Это означает, что можно передать mitmproxy скрипт на Python, чтобы он непосредственно дешифровал полезные нагрузки запросов и ответов, продолжая при этом выполнять перехват.
Я написал скрипт tapo_decrypt_pretty.ph
, который:
Наблюдает за handshake логина (
cnonce
,nonce
,device_confirm
)Извлекает из него ключи сессии
lsk
/ivb
Прозрачно дешифрует последующие вызовы API
Выводит их при помощи pretty-print в UI mitmproxy в полях
request_decrypted
иresponse_decrypted
Дампит их в файлы JSON для дальнейшего анализа
Анализируем результаты
Вот полный список вызовов, выполняемых приложением Tapo во время онбординга:
getAppComponentList
setLanguage
scanApList
bindToCloud
changeAdminPassword
setTimezone
setRecordPlan
setDeviceLocation
connectAp
getConnectStatus
setAccountEnabled
changeThirdAccount
Всё это сводится всего к четырём самым важным вызовам:
scanApList
— создаёт список точек доступа Wi-FisetAccountEnabled
+changeThirdAccount
— включает аккаунт RTSP/ONVIFchangeAdminPassword
— меняет пароль по умолчанию на облачныйconnectAp
— подключается к выбранной точке доступа Wi-Fi
Всё остальное было второстепенным: часовые пояса, планы записи, привязка к облаку.
В заключение
В конечном итоге, моим призом за все эти мучения стал грязный маленький скрипт на tapo_onboard.sh
, который:
Выполняет вход с паролем администратора по умолчанию,
Сканирует и выбирает точку доступа Wifi,
Отключает раздражающий экранный логотип в фиде камеры,
Включает функции RTSP/ONVIF
Меняет пароль администратора,
И, наконец, подключается к Wi-Fi.
Разделывание этой луковицы позволило мне сделать некоторые наблюдения о прошивке Tapo.
Часть конечных точек использует SHA-256 для хэширования, другие же продолжают использовать MD5, как будто за окном 2003 год.
Для отправки паролей на устройство есть два публичных ключа — один общий с клиентом, а другой, сверхсекретный, прошит в приложении. Проще всего понять, какой из них какой, подбросив монетку.
Синхронизация паролей между приложением и управляемыми им устройствами держится на чистых вайбах.
Вся эта система выглядит так, как будто её изготовил консорциум диванных криптографов. Впрочем, это самая дешёвая домашняя камера на Amazon, так чего же ещё ожидать?
Кроме всего прочего, мне наконец-то удалось выяснить, чем же занимается моя собака, пока меня нет.
Она спит. На диване. А иногда даже на своей лежанке.
MAXH0
Замечательно... Подозреваю что у каждого админа облака по пару терабайт коллекции домашних нюдсов пользователей на все вкусы и случаи жизни...