В тихом омуте… или интересный режим работы смартфона OnePlus 6T

в 8:05, , рубрики: android, OnePlus, бэкдор, информационная безопасность, реверс-инжиниринг, системное программирование

Несколько лет назад один из членов нашей команды заказал себе OnePlus 6T прямо из Китая. Телефон пришел в оригинальной упаковке и типовой комплектации: с зарядным устройством, кабелем и чехлом. Смартфон без проблем проработал год, ничем, на первый взгляд, не отличаясь от тех, что продаются в России.
Но однажды приложения начали предупреждать о наличии root-доступа, а некоторые, особенно банковские, вообще перестали запускаться. При этом прошивка никаким образом не модифицировалась, а обновления устанавливались исключительно из официальных источников, относящихся к ОС. Такое странное поведение смартфона побудило нас провести исследование, результаты которого описаны в этой статье.

alt text

0. Поиск причины

Какое-то время нам не удавалось понять, в чем причина такого поведения приложений. Устройство проходило проверку Play Integrity Strong, а также подтверждалась подлинность цепочки загрузки при проверке аттестации приложением Key Attestation.

alt text

Через некоторое время стало ясно, что причиной такого поведения стали установленные параметры ro.debuggable=1 и ro.adb.secure=0. Разобраться в этом оказалось довольно просто: после множества тщетных попыток понять, в чем проблема, мы попытались выполнить команду adb root. В результате adb shell запустился с правами root без каких-либо ошибок:

Демон adbd осуществляет проверку параметра ro.debuggable при сбросе привилегий с root до shell (без него невозможно подключиться клиентом, сохранив права root), а параметр ro.adb.secure влияет на необходимость аутентификации при подключении adb-клиента.

OnePlus6T:/ # getprop|grep ro.debug
[ro.debuggable]: [1]
OnePlus6T:/ # getprop|grep ro.adb
[ro.adb.secure]: [0]

Встал вопрос: как такое возможно?
Наличие параметра androidboot.verifiedbootstate=green в командной строке ядра свидетельствует о заблокированном с ключами производителя загрузчике, что в свою очередь исключает компрометацию посредством повторной блокировки устройства. Это подтверждается тем, что устройство успешно проходило проверку play integrity. В случае разблокировки параметр verifiedbootstate изменяется на orange (или yellow после повторной блокировки), и при включении устройства отображается соответствующее окно .

yellow_orange

При перезапуске устройства в режиме recovery, оно также предоставляло рутовый шел через adb – это и начинало наводить на определенные мысли.

1. Как прошивка с включенной возможностью отладки попала в release-сборку?

Для анализа ситуации с использованием root-доступа был получен лог загрузки устройства (/proc/bootloader_log). Наиболее интересным в нем оказался фрагмент, содержащий командную строку запуска ядра:

OemCmdLine
OemInfo.OemCmdLine len 805
Cmdline: androidboot.hardware=qcom androidboot.console=ttyMSM0 video=vfb:640x400,bpp=32,memsize=3072000 msm_rtb.filter=0x237 ehci-hcd.park=3 lpm_levels.sleep_
Cmdline: disabled=1 service_locator.enable=1 swiotlb=2048 androidboot.configfs=true loop.max_part=7 androidboot.usbcontroller=a600000.dwc3 rootwait ro init=/i
Cmdline: nit buildvariant=user androidboot.verifiedbootstate=green androidboot.keymaster=1 dm="1 vroot none ro 1,0 5764704 verity 1 PARTUUID=58c917ed-d09f-07d
Cmdline: 1-c7af-88f6e15beec8 PARTUUID=58c917ed-d09f-07d1-c7af-88f6e15beec8 4096 4096 720588 720588 sha1 97af17c8d79629d048c11ad4058f343124279389 61efba56aa1fa
Cmdline: 6397190a518f0aa794bbd02a7f604536e1a402ccb5f173614b7 10 restart_on_corruption ignore_zero_blocks use_fec_from_device PARTUUID=58c917ed-d09f-07d1-c7af-
Cmdline: 88f6e15beec8 fec_roots 2 fec_blocks 726263 fec_start 726263" root=/dev/dm-0 androidboot.vbmeta.device=PARTUUID=c3e4fce8-096e-a707-d9d8-74da4faf0882 a
Cmdline: ndroidboot.vbmeta.device=PARTUUID=c3e4fce8-096e-a707-d9d8-74da4faf0882 androidboot.vbmeta.avb_version=1.0 androidboot.vbmeta.device_state=locked andr
Cmdline: oidboot.vbmeta.hash_alg=sha256 androidboot.vbmeta.size=7616 androidboot.vbmeta.digest=5ae631ed1c9936e5ea64c854419ef7d150b317798975bc7d94fe444267daeac
Cmdline: 2 androidboot.vbmeta.invalidate_on_error=yes androidboot.veritymode=enforcing androidboot.bootdevice=1d84000.ufshc androidboot.fstab_suffix=default a
Cmdline: ndroidboot.serialno=221e6695 androidboot.baseband=msm msm_drm.dsi_display0=dsi_samsung_s6e3fc2x01_cmd_display: androidboot.slot_suffix=_b skip_initra
Cmdline: mfs rootwait ro init=/init androidboot.dtbo_idx=21 androidboot.dtb_idx=1 panel_type=black androidboot.mode=normal androidboot.recoveryreason=000 andr
Cmdline: oidboot.project_name=18811 androidboot.project_codename=fajitat ddr_manufacture_info=Samsung ddr_row0_info=16 androidboot.hw_version=41 androidboot.r
Cmdline: f_version=11 androidboot.prj_version=0 androidboot.platform_id=321 androidboot.platform_name=SDM845 androidboot.startupmode=hard_reset androidboot.en
Cmdline: able_dm_verity=1 androidboot.at_location=factory androidboot.power_cut_test=0 androidboot.secboot=enabled androidboot.battery.absent=false androidboo
Cmdline: t.rpmb_enable=true androidboot.type=sdebug androidboot.product.hardware.sku=3 androidboot.logcat_log_level=49 androidboot.init_log_level=49 androidbo
Cmdline: ot.selinux_log_level=842348088 androidboot.cust=0 androidboot.prmec=true androidboot.opcarrier=none androidboot.bootcount=1919247457

В глаза бросился нестандартный параметр androidboot.type=sdebug. Хотя сам по себе флаг androidboot.type не является чем-то уникальным, упоминаний его выставления в sdebug оказалось немного. В ходе поиска удалось найти скрипт init.oem.rc, включающий adb (в том числе в режиме зарядки) в случае, если устройство запущено в режиме sdebug:

on property:sys.boot_completed=1 && persist.vendor.usb.config=none && property:ro.boot.type=sdebug
    setprop persist.sys.usb.config adb
...
on charger && property:ro.boot.type=sdebug
    setprop sys.usb.config adb

Однако на исследуемом устройстве этот файл найти не удалось (позднее выяснилось, что он был встроен в бинарный файл init). Тогда было принято решение изучить все элементы цепочки загрузки устройства в надежде найти место включения режима отладки. К этому моменту стало очевидно, что данный функционал устройства каким-то образом связан с оставленным производителем бэкдором.
В 2017 году в устройствах OnePlus уже находили бэкдор. Однако с того момента прошло уже 4 года (на устройстве стоит прошивка 2021 года).

Анализ процесса загрузки показал, что в бинарном файле init присутствует логика обработки данного параметра.
В функции android::init::PropertyLoadBootDefaults происходит обработка параметра ro.boot.type. В случае, если он имеет значение sdebug, выставляются параметры ro.debuggable=1 и ro.adb.secure=0, что и можно наблюдать на устройстве. Кроме того, в приведенном ниже фрагменте кода видно, что параметр ro.boot.verifiedbootstate при этом выставляется в orange:

Фрагмент кода init загрузчика
    ...
    std::string value = android::base::GetProperty("ro.boot.type", empty_str);
    if(std::string::compare(value, 5, "debug")){
        android::base::SetProperty("ro.secure", "0");
        android::base::SetProperty("ro.adb.secure", "0");
        android::base::SetProperty("ro.boot.verifiedbootstate", "orange");
        android::base::SetProperty("ro.debuggable", "1");
        android::base::SetProperty("ro.force.debuggable", "1");
    }
    if(std::string::compare(value, 6, "sdebug")){
        android::base::SetProperty("ro.adb.secure", "0");
        android::base::SetProperty("ro.boot.verifiedbootstate", "orange");
        android::base::SetProperty("ro.debuggable", "1");
        android::base::SetProperty("ro.force.debuggable", "1");
    }
    ...

Но если проверить свойство ro.boot.verifiedbootstate, его значение будет green. Неужели этот код не выполняется, и выставление параметров осуществляется в каком-то другом месте?

OnePlus6T:/ # getprop ro.boot.verifiedbootstate
green

Как оказалось, все намного проще: свойства типа ro в init процессе с использованием функции android::base::SetProperty можно выставить только один раз. Первоначально init реализует обработку командной строки ядра в функции ProcessKernelCmdline, где и выставляет передаваемые с префиксом androidboot. аргументы ядра в качестве параметров с префиксом ro.boot.. В том числе выставляется параметр ro.boot.verifiedbootstate=green, поскольку в командной строке было передано значение androidboot.verifiedbootstate. И только после этого вызывается функция android::init::PropertyLoadBootDefaults.

Не углубляясь в исходный код init, достаточно заметить, что в функции android::init::PropertyLoadBootDefaults уже используется параметр ro.boot.type. Следовательно он должен быть выставлен ранее.

Погрузимся глубже.

2. Как в командную строку ядра Android попала строка androidboot.type=sdebug?

Если вы не знакомы с тем, как устроен процесс загрузки современного Android-устройства, рекомендуем познакомиться с этой статьей.
Опустим детали и сразу перейдем к фрагменту реализации загрузчика LinuxLoader, запускаемого на финальном шаге.

Данный загрузчик имеет GUID F536D559-459F-48FA-8BBC-43B554ECAE8D и, судя по всему, основан на предоставляемом qualcomm edk2 приложении.

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

boot_tag = get_boot_tag();
switch (boot_tag){
    case 0xB8:
        boot_type = "nonrelease";
        break;
    case 0xA9E:
        boot_type = "sdebug";
        break;
    case 0xA0:
        boot_type = "auto";
        break;
    case 0xB7:
        boot_type = "debug";
        break;
    default:
        boot_type = "normal";
        break;
}
AsciiVSPrint(&buf, 64, " androidboot.type=%a", &boot_type);
set_sdebug_boot_type(&buf);

В зависимости от возвращаемого функцией get_boot_tag значения выставляется аргумент androidboot.type, который затем помещается в передаваемую ядру командную строку (детали этого процесса опустим).
Функция get_boot_tag в свою очередь реализует получение параметра из хранилища param (размещается в одноименном разделе прошивки). Для изменения этого значения в коде загрузчика реализована функция set_boot_tag:

int get_boot_tag(){
    int tmp;
    get_param_by_index_and_offset(0x12Cu, 0x84u, &tmp, 4u);
    return tmp;
}

void set_boot_tag(int* tag){
    set_param_by_index_and_offset(0x12Cu, 0x84u, tag, 4u);
}

Парсер param секции устройств OnePlus реализован тут, более подробно с форматом можно ознакомиться там же.

Таким образом, приходим к выводу, что появление параметра androidboot.type=sdebug в командной строке ядра напрямую связано с неким параметром, хранимым в секции param устройства. Попробуем продолжить цепочку и разобраться, как же он туда попал.

Если взглянуть на вызовы функции set_boot_tag, можно наткнуться на единственную ссылку из обработчика "секретной" команды ops boottype :

int ops_boottype_handler(wchar_t *arg){
    int tmp
    get_param_intranet(&tmp);
    if ( (get_boot_tag() != 0xA9E || !tmp)
        && !enable_advanced_ops )
    {
        fastboot_fail("unknown command");
        return;
    }
    DebugPrint(0x80000000, "fastboot cmdlist: cmdname=%sn", arg);
    tmp = strcmp(arg, L" normal");
    if ( tmp )
    {
        if ( !strcmp(arg, L" auto") )
        {
            tmp = 0xA0;
        }
        else if ( !strcmp(arg, L" debug") )
        {
            tmp = 0xB7;
        }
        else if ( !strcmp(arg, L" sdebug") )
        {
            tmp = 0xA9E;
        }
        else if ( !strcmp(arg, L" nonrelease") )
        {
            tmp = 0xB8;
        }
        else{
            fastboot_fail((__int64)"unknown command");
            return;
        }
    }
    set_boot_tag(tmp);
    fastboot_okay("");
    return;
}

Команда "секретная", поскольку ее обработчик объявлен в общей таблице со всеми командами типа ops, наименование которых хранится в закодированном виде. Полный список команд приведен под спойлером. Помимо безобидных команд также поддерживается изменение режима работы selinux (ops selinux), отключение и включение dm verity (ops disable_dm_verity и ops enable_dm_verity). И все это – на заблокированном загрузчике без каких-либо следов вмешательства после восстановления оригинального значения параметров.

Таблица поддерживаемых команд ops
encoded_cmds_60720[] = {
    "jqtxsth%xut", ops_console,
    "jqtxsthsz%xut", ops_unconsole,
    "yhjyji%pfjqrjrp%xut", ops_kmemleak_detect,
    "yhjyjisz%pfjqrjrp%xut", ops_kmemleak_undetect,
    "s|tiyzmx2yttgjw%xut", ops_reboot_shutdown,
    "wjiftqyttg2yttgjw%xut", ops_reboot_bootloader,
    "lsnsnfwydjhwtk%xut", ops_force_training,
    "lsnsnfwydjhwtksz%xut", ops_unforce_training,
    "xkfyfi%xut", ops_datafs,
    "uqjm%xut", ops_help,
    "urzi%xut", ops_dump,
    "}zsnqjx%xut", ops_selinux,
    "jitrdyttg%xut", ops_boot_mode,
    "65K:IYXK<WYK=6WY5955:K9%xut", ops_4F50040TR18FTR7FSTD5F01,
    "75K:IYXK<WYK=6WY5955:K9%xut", ops_4F50040TR18FTR7FSTD5F02,
    "~ynwj{dridjqgfxni%xut", ops_disable_dm_verity,
    "~ynwj{dridjqgfsj%xut", ops_enable_dm_verity,
    "xkurjy%ysztr%xut", ops_mount_tempfs,
    "xkurjy%ysztrsz%xut", ops_unmount_tempfs,
    "lrndfyfidlsnlfjv%xut", ops_qeaging_data_img,
    "gifdjhwtk%xut", ops_force_adb,
    "yxzhdyjx%xut", ops_set_cust,
    "ju~yyttg%xut", ops_boottype,
    "lxrp{ji%xut", ops_devkmsg,
    "qj{jqdltqdqjswjp%xut", ops_kernel_log_level,
    "qj{jqdltqdyfhltq%xut", ops_logcat_log_level,
    "qj{jqdltqdynsn%xut", ops_init_log_level,
    "qj{jqdltqd}zsnqjx%xut", ops_selinux_log_level,
    "kwd|mdowu%xut", ops_prj_hw_rf,
    "tksn2jhn{ji%xut",ops_device_info,
    "stnyfhtqdyf%xut",ops_at_location,
    "yxjydyzhdwj|tu%xut",ops_power_cut_test,
    "yjxjwdrwf|dhnru%xut",ops_pmic_warm_reset,
    "owudxzqut%xut",ops_oplus_prj,
    "nxyidxzqut%xut",ops_oplus_dtsi,
    "lfqkiftqidwj{thjw%xut",ops_recover_dloadflag,
}

Все эти функции добавляются в связный список обработчиков команд `fastboot` в декодированном виде функцией `fastboot_add_encoded_cmd` (наименование автора). Декодирование наименований команд производится при помощи простого алгоритма:

def decode(cmd):
    res = ""
    for i in cmd[::-1]:
        res += chr(ord(i)-5)
    return res

В основном цикле обработки fastboot-команд можно видеть некоторые условия их выполнения. Для большей загадочности в функции `is_ops_commands` проверяемая команда предварительно кодируется и в таком виде сравнивается со значением `xut`:

if ( !is_ops_commands(cmd->prefix) || get_ops_allowed() || get_param_intranet_status())
{
    cmd->handle(&input_cmd[cmd->prefix_len], download_base, download_size);
}
else
{
    fastboot_fail("unknown command");
}

Остается разобраться с функцией get_ops_allowed. Она возвращает некоторое значение op_token_count, которое обнуляется в нескольких случаях: загадочной командой oem unoproot, по истечении 10 загрузок устройства (не во всех случаях) или выполнения команды ops 4F50040TR18FTR7FSTD5F02. Ненулевым значением данная переменная инициализируется только в случае выполнения команды flash:oproot с некоторым подписанным на ключе платформы токеном. Куда интереснее непонятный параметр intranet, выставление которого в значение 3 также приводит к разблокировке описанных выше команд.

3. Как выключить этот режим?

Целью было выключение неведомого режима, а поскольку стандартная реализация fastboot не позволяет отправлять команды типа ops для взаимодействия с устройством, был использован пакет adb для python:

from adb import fastboot
fastboot_dev = fastboot.FastbootCommands()
fastboot_dev.ConnectDevice()
fastboot_dev._SimpleCommand(b"ops 4F50040TR18FTR7FSTD5F01")
fastboot_dev._SimpleCommand(b"ops boottype normal")
fastboot_dev._SimpleCommand(b"ops 4F50040TR18FTR7FSTD5F02")

В результате выполнения скрипта root-доступ на устройстве исчез, а выполнение команд ops стало невозможным.

4. А как вернуть это режим?

После выполнения пункта 3 возник вопрос: а возможно ли вернуть этот режим?
Уже после блокировки данного режима выяснилось, что для его разблокировки необходимо иметь специальный подписанный на ключе производителя токен. Получить помещаемые в токен данные можно командой oem oproot, а разблокируется данный режим путем прошивки специального файла oproot (flash:oproot).

Дабы продолжить эксперименты с рутованным девайсом, было принято решение воспользоваться инструментом edl для прошивки на устройство модифицированного раздела param. Путем выполнения нехитрых команд root-доступ был успешно восстановлен:

edl printgpt
edl r param param.bin
py op8t_param.py read -f param.bin
py op8t_param.py write -f param.bin -o param_patched.bin 16 2718
edl w param param_patched.bin

Также удивило и то, что в репозитории edl нашелся скрипт oneplus_param.py, судя по всему основанный на результатах исследований APK-файла из выявленного в 2017 году бэкдора.
В скрипте подробно описаны варианты значений различных флагов этого раздела и перечислены команды ops. Также в нем представлен код для генерации QR для разблокировки с использованием инженерного приложения. На прошивке 2021 года данная процедура не актуальна.

Послесловие

Точно выяснить историю включения этого режима на устройстве, увы, уже не представляется возможным. Предположительно его появление связано с перепрошивкой на глобальную версию с помощью MSM Download Tool, проведенной перед продажей устройства. Впоследствии смартфон получал обновления, но активированный режим продолжал функционировать и был замечен благодаря усилению antiroot-механизмов в банковских приложениях.
Надеемся, что данная статья позволила вам глубже погрузиться в особенности работы смартфонов OnePlus и осознать, что даже самые современные устройства могут таить в себе неожиданные сюрпризы. Дальнейшие исследования этого функционала вряд ли имеют смысл, так как сейчас он представляет больше исторический интерес и отсутствует на новых моделях (по крайней мере, проверялся OnePlus 12).
Если у вас есть схожий опыт или интересные мысли по теме, делитесь ими в комментариях!

Автор: RaccoonSecurity

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js