Как я случайно поучаствовал в Bug Hunting Яндекса и взломал почти все умные колонки и ТВ
- понедельник, 19 мая 2025 г. в 00:00:06
Предыстория
Являюсь инженером и одновременно владельцем сервиса по ремонту электроники.
Однажды мне принесли Яндекс Станция Мини и попросили поглядеть. Дефект был: нет звука, но при этом слышит. Не исправен оказался DAC (i2s) цифро‑аналоговый преобразователь, но найти такой оказалось очень проблемно, поэтому была найдена точно такая же колонка, но заблокированная из за не оплаченной подписки.
Суть подписки в том, что устройство покупается в рассрочку и оплачивается ежемесячно до полного выкупа, а если пропустить платёж, то устройство блокируется, пока он не будет внесен. Идея, конечно, хорошая и полезная, но этим начали пользоваться мошенники покупая колонки на подписке, а потом сбывая их за полную стоимость, пока они еще активны и в последствии жертве устройство разблокировать «НЕВОЗМОЖНО».
Пересадка DAC полностью оживила колонку, а вот плату от заблокированной оставили мне. Бросив её на полку, я благополучно о ней забыл.
Однако, через пару месяцев, была сильная просадка по работе, было прям скучно, и она мне снова попала в руки. Когда‑то давно, более 15 лет назад, я занимался портированием android 1.6 на wm телефон toshiba g900, поэтому решил оживить знания. Тогда я еще не знал, что такое кроличья нора, насколько она глубока и как туда залезть.
Оговорочка: сразу сообщаю, я не был знаком с Yandex f*ck или с кем-то еще, всё делал сам в виде челленжа для себя, не преследуя никакую наживу. Всё было проделано исключительно в целях самообразования, а цикл статей решил написать так как Яндекс не совсем честно поступил по моему субъективному мнению, а именно не считает, что уязвимость есть и возможно не собирается/не может это исправить.
Начало:
Вводные YANDEX MINI 1:
Процессор: amlogic A113x, поддержка Trust Security
Флеш память: nand TC58NVG1S3HTA00
Подключившись по USB и найдя uart, понял, что используется Trust Security цепочка загрузки, корнем доверия у которой является так называемые efuse, который хранится прямо в процессоре и не взаимодействует напрямую с внешним миром совсем, а только через security monitor.
Uart в процессе загрузки переключается в silent режим и начинает молчать, почти сразу после загрузки, если попытается прервать процесс через ctrl+c то выскакивает сообщение RH challenge [хекс число размером в 32 байта, которое всегда разное]
и ждет ответ RH response размером в 340 байт в виде [0-9A-F] строки, а USB просит пароль, который (точнее его хеш и соль) хранится в efuse процессора.
На этом этапе я ничего не знал, но уже стало интересно.
Сняв дамп прошивки с NAND, очистив его от ECC и разбив на разделы, вначале обратил внимание на раздел rootfs.
Раздел был упакован в файловую систему ubifs.
Одним из первых файлов, что меня привлек был /etc/init.d/S01enable_sh с содержимым:
#!/bin/sh
case $1 in
start)
T=$(/bin/fw_printenv | grep rabbit_hole_debug)
if [ "$T" == 'rabbit_hole_debug=1' ]; then
sh > /dev/ttyS0 < /dev/ttyS0 &
fi
;;
*)
echo $"Usage: $0 {start}"
exit 1
esac
exit $?
Что же тут происходит:
при старте системы запускается скрипт с параметром start
дальше запускается бинарник /bin/fw_printenv, который просто печатает параметры окружения u-boot в котором grep ищет строку rabbit_hole_debug (забавно, Алиса и кроличья нора), если эта переменная найдена и она равна 1, то запускается шел sh и на порт uart перенаправляется ввод и вывод.
То что нужно, подумал я, осталось только как-то эту переменную записать.
Дальнейший взор был обращен на env раздел.
Board_id=12
EnableSelinux=enforcing
PCB_id=0
active_slot=_a
aml_serial=xxxxxxxxxx
baudrate=115200
bcb_cmd=get_valid_slot;
boot_part=boot
boot_to_recovery=0
bootargs=rootfstype=ramfs init=/init console=ttyS0,115200 no_console_suspend earlycon=aml_uart,0xff803000 ramoops.pstore_en=1 ramoops.record_size=0x8000 ramoops.console_size=0x4000 logo=,loaded,androidboot.selinux=enforcing androidboot.firstboot=1 jtag=apao androidboot.hardware=amlogic slot_suffix=_a androidboot.serialno=xxxxxxxxxx androidboot.rpmb_state=0
bootcmd=yandex_io_check_recovery; run storeboot
bootdelay=1
cmdline_keys=if keyman init 0x1234; then if keyman read usid ${loadaddr} str; then setenv bootargs ${bootargs} androidboot.serialno=${usid};setenv serial ${usid};else if printenv aml_serial; then setenv bootargs ${bootargs} androidboot.serialno=${aml_serial};setenv serial ${aml_serial};else setenv bootargs ${bootargs} androidboot.serialno=1234567890;setenv serial 1234567890;fi;fi;if keyman read mac ${loadaddr} str; then setenv bootargs ${bootargs} mac=${mac} androidboot.mac=${mac};fi;if keyman read deviceid ${loadaddr} str; then setenv bootargs ${bootargs} androidboot.deviceid=${deviceid};fi;fi;
dtb_mem_addr=0x1000000
factory_reset_poweroff_protect=echo wipe_data=${wipe_data}; echo wipe_cache=${wipe_cache};if test ${wipe_data} = failed; then run storeargs;if mmcinfo; then run recovery_from_sdcard;fi;if usb start 0; then run recovery_from_udisk;fi;run recovery_from_flash;fi; if test ${wipe_cache} = failed; then run storeargs;if mmcinfo; then run recovery_from_sdcard;fi;if usb start 0; then run recovery_from_udisk;fi;run recovery_from_flash;fi;
fdt_high=0x20000000
filesize=29e
firstboot=1
identifyWaitTime=1000
initargs=rootfstype=ramfs init=/init console=ttyS0,115200 no_console_suspend earlycon=aml_uart,0xff803000 ramoops.pstore_en=1 ramoops.record_size=0x8000 ramoops.console_size=0x4000
irremote_update=if irkey 2500000 0xe31cfb04 0xb748fb04; then echo reb04; then run update;else if itest ${irkey_value} == 0xb748fb04; then run update;
fi;fi;fi;
jtag=apao
loadaddr=1080000
preboot=run factory_reset_poweroff_protect;run upgrade_check;run storeargs;run switch_bootmode;
rabbit_hole_debug=0
reboot_mode=normal
recovery_from_flash=setenv bootargs ${bootargs} aml_dt=${aml_dt} recovery_part={recovery_part} recovery_offset={recovery_offset};if imgread kernel ${recovery_part} ${loadaddr} ${recovery_offset}; then wipeisb; bootm ${loadaddr}; fi
recovery_from_sdcard=if fatload mmc 0 ${loadaddr} aml_autoscript; then autoscr ${loadaddr}; fi;if fatload mmc 0 ${loadaddr} recovery.img; then if fatload mmc 0 ${dtb_mem_addr} dtb.img; then echo sd dtb.img loaded; fi;wipeisb; bootm ${loadaddr};fi;
recovery_from_udisk=if fatload usb 0 ${loadaddr} aml_autoscript; then autoscr ${loadaddr}; fi;if fatload usb 0 ${loadaddr} recovery.img; then if fatload usb 0 ${dtb_mem_addr} dtb.img; then echo udisk dtb.img loaded; fi;wipeisb; bootm ${loadaddr};fi;
recovery_offset=0
recovery_part=recovery
rpmb_state=0
sdc_burning=sdc_burn ${sdcburncfg}
sdcburncfg=aml_sdc_burn.ini
serial=xxxxxxxxxx
stderr=serial
stdin=serial
stdout=serial
storeargs=setenv bootargs ${initargs} logo=${display_layer},loaded,androidboot.selinux=${EnableSelinux} androidboot.firstboot=${firstboot} jtag=${jtag}; setenv bootargs ${bootargs} androidboot.hardware=amlogic;setenv bootargs ${bootargs} slot_suffix=${active_slot};run cmdline_keys;
storeboot=if imgread kernel ${boot_part} ${loadaddr}; then bootm ${loadaddr}; fi;run update;
switch_bootmode=get_rebootmode;if test ${reboot_mode} = factory_reset; then run recovery_from_flash;else if test ${reboot_mode} = update; then run update;else if test ${reboot_mode} = cold_boot; then run try_auto_burn; else if test ${reboot_mode} = fastboot; then fastboot;fi;fi;fi;fi;
try_auto_burn=update 700 1000;
ubootversion=Oct 21 2019-20:39:26
update=run usb_burning; run sdc_burning; if mmcinfo; then run recovery_from_sdcard;fi;if usecovery_from_flash;
upgrade_check=echo upgrade_step=${upgrade_step}; if itest ${upgrade_step} == 3; then run storeargs; run update;else fi;
upgrade_key=if gpio input GPIOAO_3; then echo detect upgrade key; run update;fi;
upgrade_step=2
usb_burning=update 1000
wipe_cache=successful
wipe_data=successful
Увидев rabbit_hole_debug=0, конечно первым делом я изменил на 1 и залил программатором, но попытка естественно потерпела неудачу, так как мне не известен алгоритм коррекции ECC применяемый amlogic и процессор скорректировал обратно в 0.
Ну что ж, раз нахрапом не получилось надо читать документацию.
Проштудировав как работает uboot и вообще процесс безопасной загрузки, в голову полезли мысли, что всё, ничего не получится. Но все-таки продолжил.
Взгляд зацепился за чтение aml_autoscript скрипта, за что отвечали строчки recovery_from_sdcard и recovery_from_udisk, а запустить их можно было из update, который как раз вызывается storeboot вызываемый u-boot в процессе запуска
storeboot=if imgread kernel ${boot_part} ${loadaddr}; then bootm ${loadaddr}; fi;run update;
Но нюанс в том, что эта команда отработает только если не удалось загрузить ядро ос.
второй нюанс, нет разъёма под карту памяти mmc и recovery_from_udisk там не было чтобы запуститься с OTG USB.
Изучив плату поближе стало понятно, что wifi модуль общается по интерфейсу sdio прям как карта памяти emmc.
Немного проштудировав интернет нахожу как раз упоминание, что удалось загрузиться с карты памяти, как раз подключённой к SDIO параллельно WIFI модулю, но дальше загрузка не идет, так как не находит откуда грузиться (там NAND был отключен).
Но мне это и не надо, а надо всего то, чтобы считался файлик aml_autoscript.
Когда была распаяна карта памяти, отдыхая за чашкой кофе и читая снова env заметил еще параметр factory_reset_poweroff_protect и там как раз был вызов recovery_from_udisk, но уже было все готово для карты памяти, а OTG переходника под рукой не было.
Скопировав bootloader на карту памяти и сняв nand с платы, проверил, что это действительно работает и зависаем.
AXG:BL1:d1dbf2:a4926f;FEAT:F0DC31BE:2000;POC:F;EMMC:800;NAND:81;SD:0;READ:0;0.0;0.0;CHK:0;
……………………
co-phase 0x3, tx-dly 0, clock 400000
co-phase 0x3, tx-dly 0, clock 400000
co-phase 0x3, tx-dly 0, clock 400000
emmc/sd response timeout, cmd8, status=0x300a800
emmc/sd response timeout, cmd55, status=0x300a800
emmc/sd response timeout, cmd1, status=0x300a800
MMC init failed
Using default environment
Итак отформатировав карту памяти в fat32 с разделом, на всякий случай, в 100мб и подготовив aml_autoscript файл командой:
mkimage -C none -A arm -T script -d aml_autoscript.cmd aml_autoscript
с содержимым aml_autoscript.cmd
setenv rabbit_hole_debug 1;
setenv silent 1;
saveenv
После этого я специально повредил часть дампа (раздел BKupdate) и вставив карту памяти включил питание.
Колонка начала загрузку в логе по uart пробежали строки до
In: serial
Out: serial
Err: serial
silent=1
aml log : R1024 check pass!
И ничего. Тишина. Как будто ничего и не поменялось. Пока я полез проверять, не опечатался ли я, Алиса перезагрузилась и внезапно silent
cтало 0, а rabbit_hole_debug 1
и побежали логи загрузки ядра, а как только ядро прогрузилось то появился символ #.
УРА, победа!
Первая команда была id и она сообщила, что вход выполнен от имени root.
Как потом стало понятно, колонка не смогла загрузиться из-за поврежденного дата раздела (bkupdate), далее сработал сторожевой таймер и отправил колонку в рекавери, где собственно и подгрузился и выполнился скрипт aml_autoscript.
Дальше пошло изучение как работает Алиса.
Оказывается Алиса — это монолитное приложение с именем maind, по пути /system/vendor/quasar/maind
которое содержит в себе несколько сервисов
самым интересным конечно же является сама Алиса и brickd.
А конфиг лежит рядом в файле /system/vendor/quasar/quasar.cfg
"aliced" : {
"allowedForRandomLoggingQuasmodromGroups" : [ "beta", "arabic_prod" ],
"apiKey" : "51ae06cc-5c8f-48dc-93ae-7214517679e6",
"app_id" : "aliced",
"app_version" : "1.0",
..................
"uniProxyUrl" : "wss://uniproxy.alice.yandex.net/uni.ws",
},
.................
"common" : {
"accessPointName" : "Linkplay-A98",
"backendUrl" : "https://quasar.yandex.net",
"caCertsFile" : "${QUASAR}/ca-certificates.crt",
"cryptography" : {
"devicePrivateKeyPath" : "/secret/device_key.pem",
"devicePublicKeyPath" : "/secret/device_key.public.pem",
"privateKeyPath" : "${DATA}/private.pem",
"publicKeyPath" : "${DATA}/public.pem"
},
Внимание привлек конечно сразу "backendUrl" : "https://quasar.yandex.net", заменив его на свой логирующий прокси я продолжил.
Добавил колонку в свой умный дом от Яндекс, но при попытке заговорить с Алисой сразу получал голосовое сообщение "Продлите подписку и устройство продолжит вас радовать", ну это и понятно - она заблокирована, кирпич.
Напомню, колонка у меня была без DAC, но я-то знаю, что, если соединить цифровой пин data I2S со входом усилителя, я получу шипящий звук, но вполне различимый.
Пришло время логов, изучая их сразу было видно, что конфиг приходит с сервера яндекс в открытом виде (если не учитывать HTTPS), конфигом на колонке занимается демон syncd
который периодически запрашивает его с https://quasar.yandex.net/get_sync_info?device_id=******&platform=****&revision=*****&version=*****С заголовками
X-Quasar-Alice-App-Id: alicedAuthorization: OAuth **************
В ответ же получаем json файл с настройками, достаточно объёмный, а за блокировку отвечает параметр subscription, который достаточно легко подменить (Но это я пока не раскрою). Этот параметр передаётся сервису brickd, и тот в свою очередь блокирует колонку.
Ну что ж, подменяем параметр, отвечаем, как надо и получаем разблокированную Алису, ответ от сервера в виде аудио "Что-то пошло не так, попробуй позднее", увы как оказалось проверка есть еще и на сервере (wss://uniproxy.alice.yandex.net/uni.ws). Как потом я узнал это было введено после масштабной аферы с прошивкой колонок командой "yandex f*ck".
Это меня охладило, и я забросил эти дела на неделю. Но так и не отпустило, было жгучее желание (да оно и сейчас есть) написать свой uniproxy с локальным распознаванием. Но судьба решила иначе и мне удалось победить проверку на сервере Яндекс uniproxy с родным серийным номером, поменяв всего 1 строчку в конфиге quasar (Пока тоже не могу сказать какую, так как возможно повторение истории с масштабной аферой и не хочу стать причиной подобного).
Вдохновившись этим стало интересно попробовать уже на устройстве побольше, а именно на Яндекс Станция Макс с Zigbee (YNDX-00053) и заодно поучаствовать в Яндекс баг хантинг, о котором я узнал случайно, когда завершил изыскания с Максом.
Забегая вперед, могу сказать, что мне удалось найти уязвимость, написать свой первый эксплоит, и получить root, обойти активацию подписки, даже получить вознаграждение от Яндекс. Заново выучить ассемблер, разобраться как работает Trust Chain, даже чуть-чуть научиться эмулировать в unicorn загрузку u-boot. Но это уже будет в следующей статье, а потом и про Яндекс Duo и Яндекс ТВ Про, в которых тоже удалось получить root и обойти подписку, но получить ничего от Яндекс, так как по их меркам это не уязвимость.
Update
Раз настолько всем было интересно, начну работать над второй частью.
Но займет время.