Статья Механизмы функционирования рут-прав на Андроиде

Phoenix

Разработчик
Сообщения
2,112
Реакции
1,623
Механизмы функционирования рут-прав на Андроиде

Часть 1. Андроид до версии 4.3
Вернее, даже - не «до версии 4.3», и даже - не Linux, начнем мы, пожалуй, со времен совсем уж незапамятных, когда «солнце светило ярче, девушки были моложе», а концепции - проще.
Пользователь с номером (uid) 0 в операционных системах семейства Unix традиционно имел ряд привилегий, позволяющих, например, игнорировать права доступа к файлам, монтировать дисковые устройства, конфигурировать сетевые интерфейсы, посылать любой сигнал любому процессу и пр. Другими словами, этот пользователь, обычно именуемый root, мог выполнить любое действие в операционной системе, невзирая на существующие в ней ограничения, т. е., по существу, являлся администратором. Андроид использует ядро Linux, следовательно, является одной из разновидностей ОС семейства Unix, и, в этом смысле, пользователь root (с uid 0) в Андроиде имеет те же самые административные привилегии, что и в остальных ОС этого семейства, - ему разрешено все.
Другое дело, что в стандартном Андроиде отсутствует возможность выполнить операцию от имени пользователя с uid 0. Гугль попросту не кладет в прошивку нужные утилиты.
Это «ограничение» легко обходится: надо добавить в прошивку утилиту su, установить на исполняемый файл утилиты бит suid, а владельцем файла назначить пользователя с uid 0. Бит suid проинструктирует ядро ОС о том, что утилита должна выполняться от имени пользователя-владельца исполняемого файла (а не от имени пользователя, запустившего ее). А поскольку владельцем файла является root, то мы получим процесс, имеющий все привилегии. Собственно, этот механизм является стандартным способом получения административных привилегий в мире Unix и существовал задолго до Андроида.
Тем, кто хочет еще подробностей или кому пока ничего не понятно. Совсем.
В ОС семейства Unix традиционная система разделения привилегий основывается, в частности, на понятии идентификатора пользователя (uid) — неотрицательном целом числе. Каждый процесс имеет владельца (uid) и образ — исполняемый файл, содержащий программу (код, данные). Каждый файл, в том числе исполняемый, также имеет владельца. Когда процесс создается (а единственная возможность создать новый процесс — выполнить в существующем процессе системный вызов fork), владелец и образ нового процесса наследуются от процесса-родителя. При замещении образа на другой исполняемый файл (через системные вызовы семейства exec), владелец обычно не меняется. Однако, если у исполняемого файла стоит бит suid, то вместе с образом меняется и владелец процесса (на владельца исполняемого файла).

Вроде бы ничего сложного, однако в Андроиде механизм смены владельца у процесса окружен этаким флером таинственности, чему виной несколько причин. Во-первых и в-главных, большинство пользователей Андроида если и слышали про Unix и Linux, то только в новостях. (Впрочем, подавляющее большинство из них не представляет, как работают административные права и на иных операционных системах.) Во-вторых, в Андроиде существует своя система разграничения прав (с которой сталкивался любой пользователь, устанавливающий приложения из магазина Google Play), а для обозначения доступа от имени пользователя с uid 0 самый распространенный термин - «рут-права». В итоге, неудивительно, что большинство считает «рут-права» одним из прав Андроида, типа права отправлять СМС. А это, конечно, не так. Права Андроида (permissions) обеспечиваются самим Андроидом, права пользователя с uid 0 — ядром Linux. Разница тонкая, но существенная для понимания дальнейшего. Этому заблуждению способствует и «согласованность» прав Андроида и пользователя с uid 0 - когда, например, мы посылаем СМС, Андроид выполняет следующую проверку: имеет ли право приложение, выполняющее данную операцию, посылать СМС или (!) выполняется ли данный процесс от имени пользователя с uid 0. Зачем это понадобилось Гуглю, достоверно неизвестно. Конспирологи считают, что это было частью хитрого плана Гугля по завоеванию мира. Апологеты «корпорации добра» уверены, что инженеры Гугля специально предоставили такие возможности любителям покопаться в прошивках. Дополнительную путаницу вносит и выдумка некоторых моддеров прошивок - изобретение права android.permission.ACCESS_SUPERUSER. Зачем оно понадобилось вообще, внятного ответа сами эти моддеры дать, конечно, не в состоянии. Но в результате мы имеем якобы Андроидовское право как-то связанное с рут-правами, что сильно увеличивает сумятицу в мыслях. (Впрочем, и реальных проблем хватает тоже.) Гугль это право, естественно, игнорирует, поскольку оно определено не самим Андроидом, однако при установке приложений исправно сообщает о нем. Вернее сообщал до выхода Андроида 5.0, в котором Гугль сильно ограничил игры в надуманные права, фактически поставив запрет на использование прав, отличных от изобретенных им самим. (И вопреки воплям конспирологов, сделано это было не из-за права android.permission.ACCESS_SUPERUSER, - требовалось ударить по шаловливым ручкам совсем иным игрокам рынка приложений для Андроида.)
Теперь обсудим подробнее упомянутую ранее «тонкую, но существенную», разницу. Приложение, имеющее право посылать СМС, может сделать это, просто вызвав функцию Андроида. Однако, чтобы выполнить операцию, требующую рут-прав, необходимо, создав отдельный процесс, выполнить в нем утилиту su. Средства получить дополнительные права для уже существующего процесса в ОС семейства Unix отсутствуют, это попросту никогда не нужно, более того, вредно. (Именно поэтому в абзаце выше «приложение» и «процесс» упоминались отдельно, хотя, надеюсь, понятно, что приложение всегда выполняется в рамках какого-нибудь процесса.) Невозможность добавить прав процессу, в частности, означает следующее. Когда приложение-«файловый менеджер» копирует файлы на sd-карте, оно просто вызывает внутри себя соответствующие процедуры Андроида. Однако когда ему потребуется скопировать файлы в /system, приложению придется: 1) создать новый процесс; 2) запустить в этом новом процессе утилиту su, что приведет к смене владельца процесса; 3) утилита, выполнив авторизацию (об этом чуть позже), заменит себя на командную оболочку - шелл; 4) файловый менеджер запустит в шелле утилиту копирования файлов, cp. Как мы видим, сразу после пункта 1) Андроид остается не у дел, все операции выполняются, минуя его. Это, кстати, порождает проблемы, если нужно выполнить привилегированную операцию в рамках самого Андроида. К счастью, потребность в таких изысках невелика - Гугль снабдил Андроид ворохом различных утилит, якобы для отладки, позволяющих сделать практически все за исключением совсем уж экзотики.
В этом месте настало обещанное «позже», поговорим об авторизации «рут-прав», т. е. о контроле за их выдачей приложениям. В мире Unix все просто - запускаем su, она запрашивает пароль пользователя root, мы его вводим, su заменяет себя на шелл. В Андроиде никаких паролей у пользователей нет, да и заморачиваться с вводом пароля утилите su явно не с руки. Поэтому к утилите прилагается специальное приложение - запросчик рут-прав, называемое Superuser, CWM Superuser, SuperSU, тысячи их уже. Главной задачей этого запросчика является как раз авторизация рут-прав для того или иного приложения у человека. Дополнительно, любой запросчик ведет протоколирование операций, совершаемых от имени пользователя root. Обычно все считается перевернутым с ног на голову: приложение-запросчик якобы является главным, а утилита su - довесок к нему. Можно, наверное, представлять все именно так, но мы этого делать не будем. Хотя бы потому, что приложение-запросчик можно, как и любое другое приложение, установить из магазина Google Play, но никаких особых привилегий от такого действия не получишь. (Кстати, делать запросчик системным приложением, т. е. помещать его в каталог /system/app, совершенно не обязательно, откуда пошел этот «карго-культ», пусть разбираются будущие историки.) Поскольку методы обмена информацией об авторизации уникальны для каждого запросчика, реализации утилит su, для которых запросчики предназначены, несовместимы между собой. Другими словами, su для приложения Superuser не будет работать с приложениями CWM Superuser или SuperSU и наоборот.
Заканчивая разговор о рут-правах для Андроида ранних версий, опишу подробный алгоритм получения рут-прав приложениями:
1. Приложение порождает новый процесс (системный вызов fork), в котором выполняет утилиту su (системный вызов семейства exec). Потоки стандартного ввода, стандартного вывода и стандартного вывода ошибок (stdin, stout и stderr) назначаются на каналы (pipes), связывающие процесс-потомок (su) с процессом-родителем (приложением).
2. Поскольку у выполняемого файла su установлен бит suid, а владельцем является root, ядро при выполнении системного вызова exec назначает данному процессу нового владельца — пользователя root.
3. Утилита su авторизует операцию через специальное приложение-запросчик рут-прав (или, в некоторых случаях, выполняя необходимые проверки сама, впрочем, такие детали не очень важны). Запросчик спрашивает разрешение на выдачу прав у человека и возвращает ответ утилите su.
4. Если авторизация неуспешна, su извещает об этом запросчик рут-прав и завершает работу.
5. Если же авторизация успешна, su замещает свой образ на исполняемый файл с утилитой шелл (опять через системный вызов семейства exec), информируя запросчик об этом.
6. Теперь у приложения, запросившего рут-права, есть дочерний процесс с шеллом (выполняющийся от имени пользователя root), которому можно выдавать команды в канал stdin, получая ответы из stdout и stderr.

Примечание: Утилита su может запускать не только шелл и не только с владельцем root. У нее много всяких интересных опций, оставляю их изучение читателю для самостоятельных занятий.
Часть 2. Андроид версии 4.3 и выше
Начиная с 4.3 Гугль кардинально изменил правила игры, заменив традиционную систему привилегий Unix («руту доступно все») на систему «полномочий» (capabilities). Это, в свою очередь, привело к кардинальным изменениям в методах работы утилиты su.
При использовании системы «полномочий» каждое административное действие ограничено собственным полномочием. Например, полномочие CAP_FOWNER позволяет игнорировать права доступа к файлам, полномочие CAP_SYS_ADMIN разрешает монтировать дисковые устройства, CAP_NET_ADMIN - конфигурировать сетевые интерфейсы, CAP_KILL - игнорировать ограничения на посылку сигналов процессам. Такой подход позволяет избежать концентрации всех привилегий у одного пользователя-администратора (вируса, трояна). В ядре Linux система полномочий использовалась также задолго до появления Андроида. Другое дело, что в дистрибутивах Linux система полномочий практически не используется — пользователю root выдают все полномочия, остальным … ну, вы поняли. Поэтому, хотя ядро внутри себя трудолюбиво проверяет именно полномочия, снаружи этого не видно: по-прежнему, есть root и есть «твари дрожащие» все остальные пользователи.
Гугль, однако, сделал пользователя с uid 0 самым обычным пользователем, лишив все процессы полномочий. Небольшая лазейка, как вы понимаете, все же осталась (рут-то можно получить на всех Андроидах). Дело в том, что некоторые процессы с uid 0 по-прежнему обладают всем набором полномочий. Скажем, самый первый процесс, init, с номером (pid) 1, запускаемый самим ядром, имеет владельцем пользователя root (кого же еще) и все полномочия, процессы, запускаемые лично процессом init, наследуют набор полномочий от него. Но все приложения в Андроиде запускаются другим процессом (его называют zygote), который, хотя и является потомком init, однако сразу после запуска сбрасывает все полномочия, лишая таковых и все свои процессы-потомки. Причем не просто сбрасывает, а еще инструктирует ядро, чтобы не давало его процессам-потомкам (и их потомкам) никаких новых полномочий. Таким образом, начиная с Андроида 4.3, получить какие-либо полномочия приложения уже не в состоянии. А для пущей надежности Гугль еще и проинструктировал ядро Linux игнорировать suid биты на исполняемых файлах в разделе /system. Для этого прямо в виртуальную машину Dalvik был добавлен код, перемонтирующий раздел /system с опцией nosuid для процесса zygote (а, следовательно, и всех его потомков).
Отстреливание бита suid для процессов, порождаемых zygote, использует еще один непривычный механизм ядра Linux – пространство имен точек монтирования (mount namespace). Гугль впервые применил его еще в Андроиде 4.2 для поддержки многопользовательского режима в Андроиде. Однако пространства имен точек монтирования работали в 4.2, даже когда в системе был ровно один пользователь.
Замечу, что «пользователь» в Андроиде, хотя тоже идентифицируется целым неотрицательным числом, совершенно не совпадает с понятием uid ядра. Система uid в Андроиде с самого начала использовалась для идентификации приложений, и когда Гуглю пришлось таки изобрести еще и систему идентификации пользователей, инженеры нашли элегантное решение, поражающее своей непосредственностью, - идентификатор Андроид-пользователя (user id) хранится … в старших разрядах uid.
Вернемся к пространствам имен, пожалуй. В традиционных ОС семейства Unix (и в других привычных нам ОС) все имена были глобальными, доступными всем (с учетом привилегий, конечно), т. е., если два процесса использовали одно и то же имя, они гарантированно ссылались на один и тот же объект. Однако в Linux можно создать процесс с собственным отдельным пространством имен (обычно, для этого все же нужно обладать определенными правами), и одинаковые имена в разных пространствах имен уже не обязательно ссылаются на один и тот же объект. Существует несколько типов пространств имен (и Андроид использует некоторые из них), но для понимания механизмов функционирования «рут-прав» нас прежде всего интересуют пространства имен точек монтирования. Так вот, в Андроиде 4.2 такое пространство создавалось для каждого приложения в отдельности, поэтому когда приложение монтировало новую файловую систему (создавало новую точку монтирования) или перемонтировало существующую, это было видно только этому приложению и никому больше (строго говоря, еще и процессам, находящимся в этом же пространстве имен, но это малосущественно). Например, пользователь перемонтировал /system на чтение-запись в приложении-файловом менеджере, но, заходя в приложение-эмулятор терминала обнаруживал … правильно, /system, по-прежнему смонтированный только на чтение. (Что поначалу озадачивало не столько хомячков, сколько специалистов. Первые-то в таких случаях тупо изучали место, откуда, как им казалось еще недавно, растут их руки. Последние, конечно, включали голову, но большинству из них поначалу не верилось, что разработчик «нескушных обоин» для смартфонов и планшетов навернет в системе такое.)
В Андроиде 4.3, как я уже сказал выше, процесс zygote стал дополнительно перемонтировать раздел /system с опцией nosuid в своем собственном пространстве, поэтому и пространства имен приложений (как потомков пространства имен процесса zygote) наследовали /system с этой опцией.
Про все эти capabilities и namespaces можно было бы и не рассказывать, ограничившись утверждением «пользователь root не имеет привилегий в Андроидах 4.3 и выше», подкрепив сказанное выражениями «слово пацана», «зуб даю» и «инфа — 100 %», однако, во-первых, это не в моих правилах, а, во-вторых... А, во-вторых, помимо процесса zygote и приложений, есть еще демон adbd, отвечающий за функционирование Android Debug Bridge (ADB). Он создается процессом init (а не zygote), и ему концлагерь устроили в немного меньшей степени. Порождаемые демоном adbd процессы (а это сеансы ADB) также живут в собственном пространстве имен (одном на всех), но /system в нем смонтирован без опции nosuid, кроме того, Гугль великодушно оставил полномочие CAP_SETUID таким процессам, т. е. можно сменить владельца процесса на пользователя с uid 0. Вот только сделать что-либо осмысленное от имени такого пользователя уже не получится - ни CAP_FOWNER, ни CAP_SYS_ADMIN, ни CAP_KILL Гугль и таким процессам не выдал - демон отстреливает себе эти полномочия сразу после старта. Тем не менее, команда id исправно сообщает о текущем uid 0. Именно это и означаeт фраза «Гугль сделал root обычным пользователем» (Впрочем, поскольку, в отличие от zygote, запрета на получение новых полномочий в сеансе adb нет, то ситуацию можно легко поправить.)
Итак, что мы имеем в итоге. У нас есть начальное пространство имен точек монтирования для процесса init и его потомков, все процессы, кроме adbd (и его потомков) и zygote (и его потомков) имеют владельцем пользователя root и полный набор полномочий. Приложения (т. е. потомки zygote) не могут менять владельца для своих потомков. У приложений нет никаких полномочий и их принципиально нельзя добавить. В сеансах adb владельца можно поменять, но пользователь с uid 0 изначально не будет иметь никаких полезных привилегий, это можно будет изменить, явно установив нужные полномочия (например, задав их для исполняемого файла su, подобно биту suid). Кстати, тем, кто считает что это плохо, возражу, что это таки хорошо. Теперь зловредам (вирусам и троянам) на нерутованном Андроиде гораздо сложнее сделать что-то по-настоящему вредное.
Имея полноправный init, функции «рут-прав» придется выполнять от имени процесса - потомка init. Поэтому утилита su для Андроидов 4.3 и выше «разрезана» на две части: демон su, запускаемый init (или его потомком, что несущественно), и собственно утилита su, являющаяся мостом (proxy) между приложением (или сеансом adb) и демоном. Как запустить демон su - разговор отдельный и нам малоинтересный, способы со всеми подробностями легко находятся в других источниках. Поговорим лучше об изменениях в алгоритме выдачи рут-прав приложениям.
1. Приложение порождает новый процесс, в котором выполняет утилиту su. Потоки стандартного ввода, стандартного вывода и стандартного вывода ошибок назначаются на каналы , связывающие процесс-потомок (su) с процессом-родителем (приложением). [Этот пункт, конечно же, не меняется.]
2. «Поскольку у выполняемого файла su установлен бит suid...» Этот пункт полностью исчезает для приложений. Но и для сеансов adb - тоже, владельца процесса, конечно, можно сменить на uid 0, но толку-то с того.
2'. Поэтому утилита su связывается с демоном su, передавая ему свои потоки stdin, stout и stderr, полученные от приложения или сеанса adb. (В Linux такие фокусы возможны.) Механизмы передачи потоков для приложения и для сеанса слегка отличаются, но нам это несущественно.
3, 4 Где-то в этом месте (где именно, зависит от реализации утилиты) выполняется авторизация действия. Делает ли это сама утилита или демон, для нас не так важно. Конечно, для авторизации по-прежнему используется специальное приложение — запросчик рут-прав. И точно также демон или утилита (или даже оба, независимо друг от друга), получая ответ, информируют запросчик о результатах.
5. Демон порождает процесс, в котором выполняет шелл, и привязывает к этому процессу переданные ему потоки в качестве stdin, stdout и stderr. Таким образом, каналы, созданные в приложении, пройдя через утилиту и демон, оказываются прикрепленными к процессу с шеллом, uid 0 и всеми полномочиями.
6. Теперь приложение, запросившее рут-права [как и ранее] может выдавать команды шеллу в канал stdin, получая ответы из stdout и stderr.
7. Утилита su дожидается от демона кода возврата из процесса-шелла, чтобы вернуть этот код приложению. [Ранее, код возврата доставлялся приложению сам собой, поскольку рут-шелл был прямым потомком процесса с приложением.]

Как мы видим, принципиально ничего не изменилось - приложение по-прежнему имеет доступ к процессу с uid 0 и всеми полномочиями. Дьявол, однако, кроется в деталях. Поскольку теперь процесс с рут-шеллом не является прямым потомком приложения (он порожден демоном su), то и не доступны некоторые механизмы взаимодействия между процессом-родителем и процессом-потомком. Скажем, запросив номер процесса-родителя у рут-шелла, приложение с удивлением обнаруживает, что он не совпадает с номером процесса, в котоом выполняется оно само. Это, однако, не более чем досадные мелочи. Хуже другое - пространства имен точек монтирования различаются! Поэтому, передавая в рут-шелл, например, команду создания файла в каталоге на sd-карте, приложение получает в ответ ошибку «каталога не существует». Что очень удивляет не столько приложение, сколько пользователя. Но даже если демон su продублировал все точки монтирования из пространства имен приложения (их утилита su знает и может поделиться этим знанием с демоном), т.е. фактически имена в пространствах имен приложения и рут-шелла совпадают, сами по себе эти пространства различны. Скажем, примонтировав дисковое устройство в рут-шелле, приложение обнаруживает, что с его точки зрения на устройстве нет ни файлов, ни каталогов, о чем радостно сообщает сильно озадаченному пользователю.
Понятно, что такое внезапное поведение рут-прав порождает вопли хомячков о том, что файлы на устройстве недоступны, а также ответы илитных ыкспертов, что Гугль якобы запретил доступ пользователям к sd-карте. По не столь случайному стечению обстоятельств Гугль действительно слегка поменял правила доступа к sd-карте в Андроиде 4.3, но это отдельная тема обсуждения. Мы же имеем типичную двухходовую комбинацию (смена правил доступа к карте, изменение в поведении рут-прав), решить которую обычному человеку оказывается уже не под силу. Как правило, самые сообразительные догадываются только об одном аспекте проблемы, например, качают и запускают какой-нибудь «фиксер карты», впрочем, неспособность человеческого мозга решать задачки из двух действий для психологов уже давно является аксиомой.
Заканчивая рассказ про рут права в Андроидах 4.3 и выше замечу еще, что помимо capabilities и namespaces, Гугль задействовал в Андроидах также и подсистему SELinux (проект SEAndroid), причем в 4.3, 4.4 и 5.0 добавлялись все новые и новые ограничения. В итоге, например, даже имея «полноценный» рут-доступ, предоставляемый демоном su, нельзя отредактировать понравившийся системный файл, т. к. он защищен от изменений уже на уровне SELinux. В действительности, можно, конечно, но нужно знать, что такое контекст SELinux и как его установить для процесса. Думаю, однако, что в рамках данного повествования рассказ о SELinux будет уже излишним.
 
Последнее редактирование:
Назад
Сверху Снизу