Всем привет, сегодня я хотел бы вам рассказать о некоторых вариантах передачи данных между двумя андроид приложениями и рассмотреть их с точки зрения безопасности. Я решил написать эту статью по двум причинам. Первая — я начал часто сталкиваться с непониманием разработчиков механизмов работы компонентов андроид приложения. Вторая — я перестал понимать на чем основан выбор того или иного механизма при реализации фич и захотел донести, как он должен выглядеть на минималках.
У нас есть 2 приложения, которые обращаются к одному API. Клиенты могут обращаться в API по токену доступа (sessionId). Необходимо реализовать бесшовный переход из одного приложения в другое. Для этого между ними нужно что-то пошарить, для примера пусть это будет sessionId.
Самый очевидный вариант — передавать токен в Query DeepLink. Выглядеть это будет примерно так:
В этом случае реципиент сможет извлечь sessionId и пользоваться им, не запрашивая у пользователя авторизацию. Со стороны разработчика выглядит, что задача выполнена, но давайте копнем немного глубже.
Поскольку любое приложение может зарегистрировать схему tinkoff://, OS может открыть не то приложение. Это возможно из-за того, что нет никакой регистрации и ограничений на использование схем. Вредоносное приложение может зарегистрировать схему tinkoff:// и перехватить запрос к приложению Tinkoff и запустить себя. В этом случае sessionId попадет к злоумышленникам, и ваш аккаунт будет скомпрометирован. Кроме того, DeepLink Hijacking позволяет проводить фишинг, например, отображая поля для ввода логина и пароля.
Концептуально процесс выглядит так:

Есть 2 решения этой проблемы. Первое — AppLinks-технология больше не позволяет разработчикам настраивать схему, вместо этого используются http/https. В этом случае OS берет ссылку slave.com/profile и связывается с хостом slave.com для проверки. Второе — Intent URL — вместо вызова slave:// вызывается intent://, куда передается уникальный идентификатор приложения, которое нужно запустить. Выглядит это так:
В этом случае уже не получится перехватить запуск приложения, так как указан конкретный пакет. Все равно остается проблема, что пользователь может поставить приложение из стороннего источника с таким же packageId, как у вашего slave. В таком случае, если у вас не стояло легитимное приложение slave, вредоносное приложение установится и получит ваш токен.
Это атака, при которой злоумышленник вынуждает клиента установить сеанс с целевым ПО, используя sessionId, предоставленный злоумышленником. Как только пользователь аутентифицируется, злоумышленник сможет использовать этот, уже привилегированный, идентификатор в своих целях. Атака использует тот факт, что целевое ПО использует тот же sessionId после повышения привилегий.

Как это выглядит в нашем случае:
Правильно было бы это пофиксить на API, выдавая другой sessionId после повышения привилегий, но мы пишем мобильное приложение. И наш путь — это отказаться от передачи токена от master к slave. Плюс, это даст нам защиту в глубину и, если на API что-то сломается, и токены не будут меняться при повышении привилегий, то атака все равно будет невозможна.
Еще один минус этого варианта. Многие используют сторонние сервисы для DeepLink из-за удобства генерации ссылок, аналитики и прочих крутых штук. В этом случае вы просто дарите ваш токен сторонней компании.
Как будем делать? Определим у master Content-Provider и заставим slave сходить в этот Content-Provider за токеном.

Таким образом, мы избавляемся от риска передачи не тому приложению токена в случае DeepLink Hijacking и делаем невозможным атаку Session Fixation. Но у нас появляются другие проблемы — в текущем варианте вообще любое приложение может запросить токен в любой момент времени, даже если мы не инициировали его запуск.
В большинстве случаев вам нужно проверить, что slave подписано тем же ключом, что и master, то есть принадлежат одному автору. Для этого случая в менеджере пакетов есть метод checkSignatures, который проверяет подписи приложений. Чтобы воспользоваться этой функцией необходимо добавить permission с protectionLevel=«signature» у Content-Provider в манифесте приложения:
Схема почти не изменится с предыдущего рисунка, только появится гарантия, что доступ к токену получат только приложения с подписью от того же автора.
Есть одна очень неприятная особенность в том, что имена у permission не уникальные, чем может воспользоваться вредоносное приложение и зарегать permission с нашим именем и protectionLevel=«normal» до нас. В этом случае, при установке нашего приложения, в OS уже будет существовать permission и он не перезапишется. Следовательно, наш content-provider останется без защиты и с разрешенным доступом от любого приложения.
К сожалению, далеко не всегда приложения подписаны одним ключом, например, какое-то из приложений куплено, или «так исторически сложилось», но бесшовный переход все равно нужен. В этом случае мы берем проверку подписи на себя.
Как это можно реализовать:
У Content-Provider есть метод getCallingPackage(), по которому мы можем получить packageId обратившегося за данными приложения, а по packageId можем получить список подписей и проверить их с встроенными.

Кажется, что мы сделали все идеально, но нет.
Проблема заключается в том, что когда Android создает chain-of-trust, процесс проверки сравнивает только subject, а не проверяет подпись в поле certificate’s signer. В результате злоумышленник может построить chain-of-trust без фактической подписи.
Из-за этой ошибки генерируется неправильная цепочка сертификатов, которая может включать в себя легитимные сертификаты, встроенные в APK, но фактически не использовавшиеся для подписи приложения. В конце оставлю ссылку на коммит, который исправляет данную уязвимость. Проблема исправлена в версии android 4.4, так что нам остается повысить API Level до 19.
Сегодня мы разобрали, как должен проходить анализ фич при разработке.
Также разобрали варианты передачи секрета между двумя приложениями, в процессе которого разбирали проблемы каждого варианта и придумывали способы их избежать.
Всем безопасных приложений!
Задача
У нас есть 2 приложения, которые обращаются к одному API. Клиенты могут обращаться в API по токену доступа (sessionId). Необходимо реализовать бесшовный переход из одного приложения в другое. Для этого между ними нужно что-то пошарить, для примера пусть это будет sessionId.
Вариант #1: QUERY DEEPLINK
Самый очевидный вариант — передавать токен в Query DeepLink. Выглядеть это будет примерно так:
slave://main?sessionId=686A885A4FB644053C584B9BE2A70C7D
В этом случае реципиент сможет извлечь sessionId и пользоваться им, не запрашивая у пользователя авторизацию. Со стороны разработчика выглядит, что задача выполнена, но давайте копнем немного глубже.
DeepLink Hijacking
Поскольку любое приложение может зарегистрировать схему tinkoff://, OS может открыть не то приложение. Это возможно из-за того, что нет никакой регистрации и ограничений на использование схем. Вредоносное приложение может зарегистрировать схему tinkoff:// и перехватить запрос к приложению Tinkoff и запустить себя. В этом случае sessionId попадет к злоумышленникам, и ваш аккаунт будет скомпрометирован. Кроме того, DeepLink Hijacking позволяет проводить фишинг, например, отображая поля для ввода логина и пароля.
Концептуально процесс выглядит так:

Есть 2 решения этой проблемы. Первое — AppLinks-технология больше не позволяет разработчикам настраивать схему, вместо этого используются http/https. В этом случае OS берет ссылку slave.com/profile и связывается с хостом slave.com для проверки. Второе — Intent URL — вместо вызова slave:// вызывается intent://, куда передается уникальный идентификатор приложения, которое нужно запустить. Выглядит это так:
intent://main/#Intent;scheme=slave;package=com.example.slave.client.android;end"
В этом случае уже не получится перехватить запуск приложения, так как указан конкретный пакет. Все равно остается проблема, что пользователь может поставить приложение из стороннего источника с таким же packageId, как у вашего slave. В таком случае, если у вас не стояло легитимное приложение slave, вредоносное приложение установится и получит ваш токен.
Session Fixation
Это атака, при которой злоумышленник вынуждает клиента установить сеанс с целевым ПО, используя sessionId, предоставленный злоумышленником. Как только пользователь аутентифицируется, злоумышленник сможет использовать этот, уже привилегированный, идентификатор в своих целях. Атака использует тот факт, что целевое ПО использует тот же sessionId после повышения привилегий.

Как это выглядит в нашем случае:
- злоумышленник получает анонимную сессию у приложения
- кидает красиво письмо жертве от лица банка, в котором ему предлагается перейти в личный кабинет
- при переходе по ссылке мы попадаем на DeepLink с сессией злоумышленника slave://main?sessionId=686A885A4FB644053C584B9BE2A70C7D
- мобильное приложение берет сессию, понимает, что у нее не хватает прав и просит пользователя пройти аутентификацию
- пользователь ее проходит, у сессии повышаются права
- пользователь в приложении, злоумышленник с привилегированной сессией, профит
Правильно было бы это пофиксить на API, выдавая другой sessionId после повышения привилегий, но мы пишем мобильное приложение. И наш путь — это отказаться от передачи токена от master к slave. Плюс, это даст нам защиту в глубину и, если на API что-то сломается, и токены не будут меняться при повышении привилегий, то атака все равно будет невозможна.
3rd Party Leakage
Еще один минус этого варианта. Многие используют сторонние сервисы для DeepLink из-за удобства генерации ссылок, аналитики и прочих крутых штук. В этом случае вы просто дарите ваш токен сторонней компании.
Вариант #2: CONTENT PROVIDER
Как будем делать? Определим у master Content-Provider и заставим slave сходить в этот Content-Provider за токеном.

Таким образом, мы избавляемся от риска передачи не тому приложению токена в случае DeepLink Hijacking и делаем невозможным атаку Session Fixation. Но у нас появляются другие проблемы — в текущем варианте вообще любое приложение может запросить токен в любой момент времени, даже если мы не инициировали его запуск.
Protection Level
В большинстве случаев вам нужно проверить, что slave подписано тем же ключом, что и master, то есть принадлежат одному автору. Для этого случая в менеджере пакетов есть метод checkSignatures, который проверяет подписи приложений. Чтобы воспользоваться этой функцией необходимо добавить permission с protectionLevel=«signature» у Content-Provider в манифесте приложения:
<permission
android:name="com.example.contentprovider.access"
android:protectionLevel="signature"/>
<application>
<provider
...
android:readPermission="com.example.contentprovider.access">
</application>
Схема почти не изменится с предыдущего рисунка, только появится гарантия, что доступ к токену получат только приложения с подписью от того же автора.
Permission Race Condition
Есть одна очень неприятная особенность в том, что имена у permission не уникальные, чем может воспользоваться вредоносное приложение и зарегать permission с нашим именем и protectionLevel=«normal» до нас. В этом случае, при установке нашего приложения, в OS уже будет существовать permission и он не перезапишется. Следовательно, наш content-provider останется без защиты и с разрешенным доступом от любого приложения.
Different Signatures
К сожалению, далеко не всегда приложения подписаны одним ключом, например, какое-то из приложений куплено, или «так исторически сложилось», но бесшовный переход все равно нужен. В этом случае мы берем проверку подписи на себя.
Как это можно реализовать:
У Content-Provider есть метод getCallingPackage(), по которому мы можем получить packageId обратившегося за данными приложения, а по packageId можем получить список подписей и проверить их с встроенными.
String pkg = this.getCallingPackage();
PackageInfo pkgInfo = pkgmgr.getPackageInfo(pkg, GET_SIGNATURES);
Signatures[] signatures = pkgInfo.signatures;
for (Signature sig: signatures) {
if (sig.equals(TRUSTED_SIGNATURE)) {
// trusted signature found, trust the application
}
}

Кажется, что мы сделали все идеально, но нет.
Fake id vulnerability
Проблема заключается в том, что когда Android создает chain-of-trust, процесс проверки сравнивает только subject, а не проверяет подпись в поле certificate’s signer. В результате злоумышленник может построить chain-of-trust без фактической подписи.
Из-за этой ошибки генерируется неправильная цепочка сертификатов, которая может включать в себя легитимные сертификаты, встроенные в APK, но фактически не использовавшиеся для подписи приложения. В конце оставлю ссылку на коммит, который исправляет данную уязвимость. Проблема исправлена в версии android 4.4, так что нам остается повысить API Level до 19.
Выводы
Сегодня мы разобрали, как должен проходить анализ фич при разработке.
Также разобрали варианты передачи секрета между двумя приложениями, в процессе которого разбирали проблемы каждого варианта и придумывали способы их избежать.
Всем безопасных приложений!
Ссылки
- Fake id vulnerability fix commit
- Презентация Fake id vulnerability
- CWE: Session Fixation