Безопасность Android-приложений. Лекция в Яндексе

Разработчик Дмитрий Лукьяненко, чью лекцию мы публикуем сегодня, не только является специалистом Яндекса, но и умеет проверять на прочность решения разработчиков других компаний. Это позволяет учиться на чужих ошибках — не исключая порой своих, конечно. В докладе Дмитрий поделится примерами Android-уязвимостей, в том числе найденных им собственноручно. Каждый пример сопровождается рекомендациями — как нужно и как не нужно писать приложения под Android.

Меня зовут Дмитрий, я работаю в компании Яндекс в минском офисе, занимаюсь разработкой аккаунт-менеджера. Это библиотека, которая отвечает за авторизацию пользователей. Поэтому мы поговорим о безопасности Android-приложений.

Я бы выделил следующие источники проблем. Первые — сетевые, связанные с коммуникацией с сервером. Это когда ваши данные передаются не совсем защищенным способом. Вторые связаны с проблемой хранения данных: это когда ваши данные хранятся где-то в открытом виде, могут даже где-то на SD-карте храниться, либо они не зашифровываются, в общем, когда к ним есть доступ извне.

Третий вид проблем — воздействие сторонних приложений на ваше. Это когда стороннее приложение может получить несанкционированный доступ к вашему, выполнить какие-то действия от имени пользователя, либо даже украсть что-то и т. п.

В этом докладе мы остановимся именно на третьем виде проблем, связанном с воздействием сторонних приложений. Мы рассмотрим не только теорию, но ещё и конкретные примеры из реально существующих приложений. Причем некоторые эти примеры до сих пор актуальны, видимо, на них просто забили, но мне они кажутся достаточно интересными.

5a206d673cff47e3a2df4eb82172f7b0.jpg

Этот доклад будет полезен тем, кто занимается Android-разработкой. Я бы советовал уделить внимание этим аспектам всем, кто делает приложения. Например, тестировщикам. Тестировщики, мне кажется, если посмотрят эти нюансы, смогут какие-то премии выбивать, они начнут более глубоко вникать в разработку, даже программировать можно особо не учиться. Manifest-файл просто смогут анализировать и выявлять потенциально опасные места. И всем остальным, кто интересуется такими вопросами безопасности.

47a59009feae44cb87e5c52a55b36293.jpg

Прежде чем начнем, стоит упомянуть софт, который может пригодиться. В первую очередь BytecodeViewer, очень полезная программка, позволяет одним кликом получить из apk-файла все исходники, все ресурсы, в том числе manifest-файл.

ed220e1715ac4e969e08f47f2e9e56b4.jpg

Стоит отметить удобную утилиту для Android — ManifestViewer. Она позоляет очень быстро просматривать manifest-файлы всех установленных приложений. Я её использую по работе, например, когда ко мне приходят и говорят: аккаунт-менеджер не работает, что такое? В первую очередь всегда смотрю manifest-файл, чтобы убедиться, что аккаунт-менеджер был правильно интегрирован, то есть что все его компоненты объявлены как надо.
Эта программка полезна, чтобы вы на скорую руку открыли и посмотрели точки входа, вектор атак из manifest-файла.

Если вы любите всё делать из консоли, вот вам все эти программы отдельно. apktool позволяет выдернуть все ресурсы и manifest-файл. Dex2jar и enjarify позволяют конвертировать apk-файл в jar. Dex2jar может какие-то apk не конвертнуть, выдать с каким-то исключением. Не отчаивайтесь: часто такие файлы скушает enjarify, и вы получите jar. Это две хороших утилиты.

После того, как у вас есть jar-файлы, вы можете взять Java Decompiler и сконвертировать их в более удобный вид — в вид классов. Понятно, что их нельзя будет запустить, но анализировать их гораздо проще, чем код, который там получается, и т. п.

8eeb42df478e42d3b427afdf933afbcf.jpg

Давайте рассмотрим самые базовые проблемы. Во-первых, сначала нам нужно проанализировать manifest-файл, потому что в нем обязательно должны быть описаны все компоненты Android-приложения. Это Activity, Service, Broadcast Receiver, ContentProvider. Все они, за исключением Broadcast Reciever, обязательно должны там находиться.

Поэтому когда мы начинаем поиск каких-то проблем, мы смотрим эти компоненты. Вот конкретный пример. Это сервис, который был объявлен в manifest-файле одного приложения, он явно экспортируется. В нем даже явно указано, что он экспортирован. Это означает, что он доступен любому другому стороннему приложению. Любое другое приложение, в свою очередь, может послать в этот сервис интент, и сервис может выполнить какие-то действия, если они там предусмотрены.

В данном случае мы видим потенциальный вектор для атаки. Если копнем глубже и посмотрим исходники, по ним станут понятно, что этот сервис позволяет, допустим, удалить авторизованного пользователя в этой программе. Я сомневаюсь, что разработчики предусматривали такую фичу для каких-то сторонних приложений. Это конкретный пример, которому следует уделить внимание.

Ещё один момент: когда у нас какая-то компонента использует интент-фильтры, то по умолчанию она тоже становится экспортируемой. Тут, возможно, начинающие разработчики могут споткнуться. Если интент-фильтр есть, то всё, эта компонента уже публичная. Тут стоит быть внимательным.

Я сталкивался с такими видами проблем, когда над уведомлением, которое показывается в соответствующей панели, можно произвести ряд действий. Например, такое бывало в Яндекс.Почте и других приложениях.

01cfebd30d8d4e76b9df54d4ab9ece61.jpg

Что делало это событие? Оно обрабатывалось при помощи публичного BroadcastReciever. То есть злоумышленник тоже мог послать любой бродкаст — подобрать ID письма и, допустим, удалить его. Причем необязательно, чтобы это было сообщение в панели.

Когда-то давно в Яндекс.Почте была такая уязвимость. Уже всё поправили, так что можно пользоваться, всё отлично. Спасибо Ильдару, они оперативно всё это делают.

20eb3b3513f4470383245c6fc5a0c15a.jpg

Как это решилось? Компоненту просто сделали недоступной извне, поставили ещё permission дополнительно. Его можно было и не ставить, главное — если она не экспортируется, значит, уже никто не сможет к ней подключиться и что-то выполнить.

e46c1b4c9c254c5988de2fb77880ef57.jpg

Следующий вид проблем — использование неявных интентов, когда в интенте не задается конкретное место, в которое он должен прийти. Это означает, что у вас нет гарантии, что этот интент придет туда, куда вы хотите. Он может прийти куда-то ещё. Поэтому тут надо быть внимательным.

Ещё нюанс, который я отнес бы к неявным интентам, — это Browsable Activity, когда у нас из браузера можно открыть Activity стороннего приложения. Это происходит, когда задана какая-то схема, хост, там кликают и у вас открывается окно. Хорошо, когда просто открылось окно и например, передалась позиция на карте. Это не страшно. А страшно — следующее. Допустим, у нас некоторые ребята предлагают использовать Slack в компании. В Slack очень интересная авторизация. Если вы авторизовались в браузере на Android, то вы можете авторизоваться и в установленном приложении Slack. Это работает по такой схеме, когда в браузере есть URL, вы по нему кликаете и его перехватывает приложение Slack.

abfd1649fe2d458796988043dddbdec1.jpg

Но здесь нет гарантии, что URL не перехватит кто-то другой, причем по нему передается токен. Схема достаточно простая: когда вы кликаете в браузере, вас сразу авторизовывают в приложении Slack. Значит, этого URL достаточно для авторизации.

6ffca5cc6e504ee1aa6618cfd9a2a16a.jpg

Здесь отмечены кнопки, которые можно кликать в браузере и которые инициируют это открытие. Это не только «Открыть Slack», но и логотип — он всегда находится сверху, на любом экране.

Давайте посмотрим видео, которое демонстрирует использование этой проблемы. Мы авторизовались, зашли в свою директорию, нажали на логотип Slack —, но приложения Slack у нас нет, у нас злоумышленник подставил интент-фильтр. Вернулись, нажали… Можно фишинг сделать — точно такой же по внешнему виду, как Slack. Написать, что авторизация не сработала и всё такое. Пользователь ничего не поймет, а токен окажется у вас. Поэтому если пользуйтесь Slack, то будьте внимательны с этой браузерной авторизацией.

15665f946d2847c4adfc6175fe910408.jpg

Решение очень простое. Как говорил мой коллега, можно использовать явные интенты, которые открываются из браузера. Но когда я в Slack зарепортил и мой тикет получил номер 80000 с чем-то, они сказали, что это дубликат и что уже есть тикет 50000-какой-то. Если прикинуть, получится, что им это больше чем полгода назад сообщили. Исправляется вроде достаточно просто, но — не исправили. Значит, это не критичная проблема и можно тогда о ней рассказать.

Здесь есть рекомендация, что лучше всегда использовать явный интент. Если вы почитаете и вникнете более глубоко, то узнаете, что у Android была прикольная опечатка. Они в одном месте использовали неявный pending intent. Из-за этого можно было получить доступ к пользователю с правами system. Они эти ошибки исправили в Android 5. Такие ошибки у всех бывают, даже у Google.

Всегда лучше использовать явный интент или, если это невозможно, всегда задавать какой-то permission. Он позволяет ограничить доступ.

Рассмотрим другой вид уязвимостей — основанный на пользовательских данных.

20be6124eb1347738bdf8294a0ecf019.jpg

У нас есть приложения, которые могут получить какие-то данные от пользователей, например отправить какой-то файл, открыть его, просмотреть. Это обычно осуществляется при помощи интентов типа view и send.

Вот это зловредное приложение может вам сказать: отправь мне файл, который лежит у тебя же в приватной директории, в твоей же песочнице. Допустим, этот файл может быть вашей базой данных, содержащей всю переписку, или преференсы, если хранятся какие-то токены. Некоторые приложения могут выполнять с этими файлами какие-то действия. Они могут, например, файл закешировать на SD-карте или отправить куда-то. В том числе можно выполнить такое действие, что этот файл станет доступен злоумышленнику, хотя изначально они предполагали, что это будет их внутренний файл.

Можно взять getAbsolutePath () и проверить, совпадает ли директория с вашей приватной. Но это неправильно.

ebfd73d2725e4e81b93bc87780ab42f3.jpg

Вам злоумышленник может прислать не сам файл, а ссылку на него.

Как может создаваться эта ссылка? У злоумышленника создается readme.txt — он сделал ссылку на приватный файл вашего приложения. Затем он запихнул эту ссылку вам, и если вы будете использовать getAbsolutePath (), то вам вернётся путь ссылки — путь к файлу readme.txt. А вы подумаете, что всё нормально.

Чтобы это исправить, лучше использовать метод getCanonicalPath (). Он возвращает полный путь, самый настоящий. В данном случае система вернет не readme.txt, а какой-нибудь mailbox. Учитывайте эти проблемы.

Я сталкивался с ситуацией, когда подобная проблема была у одного приложения более чем с 50 млн пользователей. Говоришь: покажите мне этот файл. Они его кешируют на SD-карту и потом пытаются открыть. Так можно было все токены украсть. Они уже всё исправили, но попросили пока не разглашать. Тем не менее, такое встречается.

Следующий интересный момент: вам могут прислать ссылку на content provider, и он тоже может быть передан. Даже если он защищен через permission, даже если он не экспортируемый, то всё равно — когда ваше приложение попытается его открыть, выполнится метод query, который есть в content provider.

Тут надо быть осторожным. Вот ещё один реальный пример, уже из другого приложения. Они делали бэкап БД из метода query. Туда можно было передать action типа backup и программа сразу дублировала всю БД на SD-карту. Получается, вроде провайдер защищен, но его можно так подсунуть, чтобы получить в итоге всю БД.

616740b1367049578f066c9956e64b62.jpg

4c7d065e58ae4d8bb56f92f53610bd1c.jpg

Я больше нигде не встречал, чтобы кто-то делал запись в методе query, но там всегда лучше открывать что-то для чтения. А ещё — учитывать, что там может быть SQL-инъекция. Сейчас есть ряд приложений, в которых я обнаружил возможность SQL-инъекции. Однако доступы у них только для чтения, и я пока не понял, можно ли там дальше что-то сделать. Но вдруг найдется уязвимость в самой БД SQLite, которая позволит выполнить уже не настолько безобидную инъекцию? Всё-таки лучше их не допускать, даже если если при открытии есть доступ только для чтения.

1e05286c50364bffbaed925f45027574.jpg

Теперь давайте рассмотрим одну проблему с WebView, причем срабатывает она во всех Android до версии 5.0 и позволяет обойти SOP. Из этого мы сделаем вывод, что в WebView лучше никому не давать открывать свои файлы.

27bcbb115dae486dba40c580550bf25f.jpg

Рассмотрим этот вид атаки. Зловредное приложение может попросить ваше приложение, если оно имеет доступы к WebView: покажи мне файлик index.html. Предположим, ваше приложение открыло index.html. Содержимое файлика очень простое — JavaScript, который через 5 секунд сам файлик, самого себя, загружает в iframe, перезагружает как бы.

4bb0e8d409d24b68ab87dcb04ed0d537.jpg

В этот период времени зловредное приложение указанный файлик удаляет и заменяет на символическую ссылку на приватный файл из вашего приложения. Проходит 5 секунд, оно перезагружается, но контент в нём уже становится контентом того самого приватного файла. На Android до 5.0 ещё до WebView, видимо, стоит неправильная проверка на то, что это символическая ссылка. Таким образом можно прочитать контент. Оно смотрит — имена файлов совпадают, значит SOP не нарушена, контент можно отдать и значит, JavaScript может взять контент и отправить его на сервер.

02e43f51e78f4e8fa2b3950b46570f63.jpg

Такой вид атаки встречается не только в WebView. Он до сих пор работает в UCBrowser, у которого больше 100 млн установок. Я встречал такое и на некоторых версиях Android-браузера, до Android 5.0. Наверное, он тоже на WebView построен. Например, на телефонах Samsung у меня такое не получилось воспроизвести, а на моем телефоне Acer срабатывает.

2beba662a0134feebdc694eb5b4ce380.jpg

Глянем небольшое видео о том, как это работает в UCBrowser. Речь о небольшом зловредном приложение, но оно может сработать. Вы легли спать, телефон тоже, внезапно запускается activity, страничка. Вы спите, а там запустилось — какой-то iframe, подтверждение, алерт. Если алерт прочитал контент, значит, мы точно так же могли отправить этот контент куда угодно. Это bookmark.db, тут хранятся закладки. Думаю, кому интересно — может поисследовать. Возможно, удастся найти там более приватные пользовательские данные. Можно и куки украсть. Но куки у них названы по имени сайта, поэтому надо перебирать. ВКонтакте можно взять. Уязвимость до сих пор есть, вроде пока ещё не исправили и не спешат исправлять.

4cf32b664dec4c03a72c3cca3e670898.jpg

ff21c416621d434695763de41a2aba26.jpg

Вывод: если у вас есть WebView, то не давайте никому открывать в нём свои ссылки. А если всё-таки даёте, то хотя бы запретите открытие локальных файлов. В этом плане большие молодцы компания Qiwi, я их приложение пробовал, обрадовался, что сейчас открою файлик — ан нет, они запретили открывать локальные файлы и ничего не сделаешь.

Рассмотрим следующий вид нетривиальной проблемы. Она основана на особенностях десериализации в Android. Возможно разработчикам самой Java может быть полезно.

Представим, что у нас есть интент, который содержит какие-то данные, которые передаются в наше приложение — оно берет одно поле и считывает его с интента. И если приложение считало хоть одно поле, то происходит автоматическая десериализация всех данных в интенте. Даже если они не будут использованы, они всё равно десериализуются. Сейчас поймете, что здесь опасного.

Есть в Android до 5.0 такой бонус: на некоторых версиях при десериализации не проверяется, что класс действительно имплементирует интерфейс сериализации.

Это означает, что все классы, которые есть в вашем приложении, могут быть уязвимы. Да, они могут быть не сериализуемы, но если они подойдут под критерий, который будет описан дальше…

23309e792d5a4188870f00eda28b5a73.jpg

Итак, что тут опасного? Если объект был создан, он когда-нибудь будет удален. Обычно в том, что метод finalize вызовется, нет ничего опасного, ничего не реализуется. Однако когда есть нативная разработка, то обычно в методе finalize вызывают деструктор из нативной области и передают туда указатель. Я нативной разработкой не занимался, но поизучал, как туда передаются указатели. Если данный указатель сериализуется, то злоумышленник может подставить свой, а он, в свою очередь, может как вызвать ошибку выхода за границы памяти, так и указать на другую память, которая потом возьмет и выполнит какой-то код.

73984a3588e64ce3b4235993be81d6dc.jpg

Этим воспользовались ребята из компании IBM. Они проанализировали весь список классов на платформе Android — проверили больше тысячи или сколько их там. И нашли всего один, который удовлетворял критериям: он был сериализуемый, и он сериализовал native-указатель. Они взяли этот класс, подставили свой указатель, он сериализовался и десериализовался. Он был новым не каждый раз. Сделали proof of concept, демонстрацию, есть видео. Эта уязвимость позволила им при помощи этого класса заслать интент в системное приложение. Оно позволило им, допустим, удалить оригинальное приложение Facebook и заменить его на фейковое. Видео длится минут 7. Там приходится повертеть — пока finalize вызовется, пока очистится память. Но вывод — никогда не надо десериализовывать нативный указатель.

e3014f38685248c48e82835eca2ea14d.jpg

Подытожим. Лучше никогда не экспортировать компоненты, которые есть в вашем приложении. Либо — ограничивать доступ по permission. Интенты всегда лучше использовать явные, либо permission задать. Никогда не доверяйте тем данным, которые придут от пользователя — это могут данные на что угодно, даже на ваши приватные области. Вы их потом можете повредить. Внимательно отнеситесь к ContentProvider, в том числе к SQL-инъекции и т. п.

Такая штука, как WebView, — она вообще не для игрушек. Если она есть в вашем приложении, закройте её и никому не давайте с ней играться.

Сериализация, а точнее, нативная разработка — это тоже как спички, будьте с ней поосторожнее. Спасибо за внимание.

Комментарии (0)

© Habrahabr.ru