Как я случайно поучаствовал в Bug Hunting Яндекса и взломал почти все умные колонки и ТВ
Предыстория:
Являюсь инженером и одновременно владельцем сервиса по ремонту электроники.
Однажды мне принесли Яндекс Станция Мини и попросили поглядеть. Дефект был: нет звука, но при этом слышит. Не исправен оказался 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) и вставив карту памяти включил питание.
Колонка начала загрузку в логе по уарт пробежали строки до
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 и обойти подписку, но получить ничего от Яндекс, так как по их меркам это не уязвимость.
gotch
Можно поинтересоваться, что это была за афера?
angellfear Автор
Я особо не интересовался, но насколько я знаю, они поставили на поток разблокировку и обход активации на подписочных колонках. Притом чуть ли не в промышленных масштабах. Почитайте если интересно, в интернетах есть.
zlodey_megamorgen
Нет, мы не занимались никакими промышленными прошивками, 99% всех обратившихся имели одну колонку и были обмануты с помощью гигантского недочета со стороны Яндекса, который развязывал руки мошенникам, но компании плевать на это :-)