Перейти к содержанию

Возможность опосредованного запуска приватных Activity

Критичность: СРЕДНЯЯ
Способ обнаружения: IAST

Описание

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

Уязвимость присутствует в приложениях, которые используют Intent из недоверенного источника (например, полученные из стороннего приложения с помощью методов getIntent, getParcelableExtra или onActivityResult) для запуска (startActivity, startActivityForResult) своих внутренних Activity (в дальнейшем будет использован термин «to-be-redirected Intent», то есть тот Intent, который получен из недоверенного источника и далее используется для запуска Activity).

Например, вредоносное приложение может использовать такой код:

Intent intent = new Intent("vuln.app.pkg.LOGIN");
Intent redirIntent = new Intent();
redirIntent.setComponent(new ComponentName("vuln.app.pkg", "vuln.app.pkg.PrivateActivity"));
intent.putExtra("redirect_intent", redirIntent);
startActivity(intent);

Целевое приложение:

Intent redirectIntent = getIntent().getParcelableExtra("redirect_intent");
if(redirectIntent != null) {
    startActivity(redirectIntent);
    }
    else {
    startActivity(new Intent(this, MainActivity.class));
    }
    finish();

В результате вызова такого кода из вредоносного приложения будет запущена внутренняя Activity уязвимого приложения (vuln.app.pkg.PrivateActivity)

Рекомендации

Для устранения подобных проблем в приложении необходимо убедиться в соответствии нескольким правилам:

  1. Реализовать private/in-house видимость у компонентов, которые принимают «to-be-redirected Intent» и используют его для последующего запуска private/in-house Activity.

    Например, объявление Activity внутренней — отсутствуют intent-filter или флаг exported выставлен в значение false:

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
            package="com.swordfishsecurity.appsec.android.activity.privateactivity" >
            <application
                android:allowBackup="false"
                android:icon="@drawable/ic_launcher"
                android:label="@string/app_name" >
                <!-- Private activity -->
                <!-- *** 1 *** Не используйте taskAffinity -->
                <!-- *** 2 *** Не используйте launchMode -->
                <!-- *** 3 *** Явно указывайте атрибут exported="false" -->
                <activity
                    android:name=".PrivateActivity"
                    android:label="@string/app_name"
                    android:exported="false" />
                <!-- Public activity запускаемая по умолчанию -->
                <activity
                    android:name=".PrivateUserActivity"
                    android:label="@string/app_name"
                    android:exported="true" >
                    <intent-filter>
                        <action android:name="android.intent.action.MAIN" />
                        <category android:name="android.intent.category.LAUNCHER" />
                    </intent-filter>
                </activity>
            </application>
    </manifest>
    
  2. Проводить валидацию «to-be-redirected Intent» на предмет вредоносности — он не должен направляться в private/in-house Activity

    Intent intent = getIntent();
    Intent redirectIntent = (Intent) intent.getParcelableExtra(“redirect_intent”);
    ComponentName name = redirectIntent.resolveActivity(getPackageManager());
    // проверяем целевое имя пакета и класса
    if(name.getPackageName().equals(“safe_package”) && name.getClassName().equals(“safe_class”)) {
        startActivity(redirectIntent);
    }
    

Partner Activity

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

В рамках такого обмена вводится понятие Partner Activity. Это Activity, которая может совместно использоваться только из приложений, которые разработаны компаниями-партнёрами.

Существует риск, что стороннее приложение прочитает содержимое Intent, используемого для старта Activity. Поэтому нужно быть уверенным, что если в Intent, используемый для запуска Activity, включается чувствительная информация, то предприняты защитные меры, которые не позволят получить к ней доступ вредоносному приложению.

Создание Partner Activity

При создании Partner Activity необходимо придерживаться нескольких правил:

  1. Не используйте taskAffinity.
  2. Не используйте launchMode.
  3. Не объявляйте intent-filter и явно указывайте атрибут exported="true".
  4. Проверяйте, что сертификат вызывающего приложения содержится в списке сертификатов доверенных приложений.
  5. Проводите проверку и безопасную обработку полученного Intent, несмотря на то, что он был получен из того же приложения-партнёра.
  6. Возвращайте только ту информацию, к которой приложение-партнёр имеет право получить доступ.

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

Важно про получение подписи. Флаг PackageManager.GET_SIGNATURES (и поле PackageInfo.signatures) объявлен устаревшим начиная с API 28 и небезопасен: он не учитывает корректно несколько подписантов и исторически использовался в атаках на проверку подписи (например, Janus). Начиная с API 28 следует использовать PackageManager.GET_SIGNING_CERTIFICATES и объект PackageInfo.signingInfo. При этом необходимо учитывать, что у приложения может быть несколько подписей (ротация ключей через getSigningCertificateHistory() или несколько подписантов — hasMultipleSigners()), и проверять все актуальные сертификаты. На API 27 и ниже остаётся доступен только GET_SIGNATURES. Эти отличия учтены в коде PkgCert.java ниже.

Важно про getCallingActivity(). Метод getCallingActivity() (как и getCallingPackage()) возвращает non-null значение только в том случае, если текущая Activity была запущена через startActivityForResult(). Если она запущена обычным startActivity(), метод вернёт null. Поэтому нельзя полагаться на него как на единственный источник идентификации вызывающего приложения и обязательно нужно проверять результат на null (см. комментарий в onCreate() ниже). Для надёжной идентификации вызывающей стороны на современных версиях Android рассмотрите Activity.getLaunchedFromPackage() (API 34+).

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.swordfishsecurity.appsec.android.activity.partneractivity" >

    <application
            android:allowBackup="false"
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name" >
            <!-- Partner activity -->
            <!-- *** 1 *** Не используйте taskAffinity -->
            <!-- *** 2 *** Не используйте launchMode -->
            <!-- *** 3 *** Не обьявляйте intent фильтры и явно указывайте атрибут exported="true" -->
            <activity
                android:name=".PartnerActivity"
                android:exported="true" />
    </application>
</manifest>

PartnerActivity.java

package com.swordfishsecurity.appsec.android.activity.partneractivity;
import com.swordfishsecurity.appsec.android.shared.PkgCertWhitelists;
import com.swordfishsecurity.appsec.android.shared.Utils;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
public class PartnerActivity extends Activity {
    // *** 4 *** Проверяйте, что сертификат вызывающего приложения содержится в списке сертификатов доверенных приложений
    private static PkgCertWhitelists sWhitelists = null;
    private static void buildWhitelists(Context context) {
        boolean isdebug = Utils.isDebuggable(context);
        sWhitelists = new PkgCertWhitelists();

        // Регистрация хеша сертификата приложения-партнёра com.swordfishsecurity.appsec.android.activity.partneruser
        sWhitelists.add("com.swordfishsecurity.appsec.android.activity.partneruser", isdebug ?
                // Хеш сертификата ключа "androiddebugkey" из debug.keystore.
                "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255" :
                // Хеш сертификата ключа приложения-партнёра com.swordfishsecurity.appsec.android.activity.partneruser
                "1F039BB5 7861C27A 3916C778 8E78CE00 690B3974 3EB8259F E2627B8D 4C0EC35A");

        // Регистрация других приложений-партнёров осуществляется аналогично
    }
    private static boolean checkPartner(Context context, String pkgname) {
        if (sWhitelists == null) buildWhitelists(context);
        return sWhitelists.test(context, pkgname);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // *** 4 *** Проверяйте, что сертификат вызывающего приложения содержится в списке сертификатов доверенных приложений
        // ВНИМАНИЕ: getCallingActivity() возвращает non-null только если эта Activity была запущена
        // через startActivityForResult(). При обычном startActivity() метод вернёт null, и обращение
        // к getPackageName() приведёт к NullPointerException. Поэтому проверяем результат на null и,
        // если вызывающее приложение неизвестно, завершаем работу.
        ComponentName caller = getCallingActivity();
        if (caller == null || !checkPartner(this, caller.getPackageName())) {
            Toast.makeText(this,
                    "Вызывающее приложение не является партнёром",
                    Toast.LENGTH_LONG).show();
            finish();
            return;
        }

        // *** 5 *** Проводите проверку и безопасную обработку полученного Intent, несмотря на то, что он был получен из того же приложения-партнёра
        // См.п. "Безопасная обработка входных данных"

        Toast.makeText(this, "Запущено из приложения-партнёра", Toast.LENGTH_LONG).show();
    }
    public void onReturnResultClick(View view) {
        // *** 6 *** Возвращайте только ту информацию, к которой приложение-партнёр имеет право получить доступ, не больше
        Intent intent = new Intent();
        intent.putExtra("RESULT", "Информация для приложения-партнёра");
        setResult(RESULT_OK, intent);
        finish();
    }
}

PkgCertWhitelist.java

package com.swordfishsecurity.appsec.android.shared;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
public class PkgCertWhitelists {
        private Map<String, String> mWhitelists = new HashMap<String, String>();

        public boolean add(String pkgname, String sha256) {
            if (pkgname == null) return false;
            if (sha256 == null) return false;

            sha256 = sha256.replaceAll(" ", "");
            if (sha256.length() != 64) return false;    // SHA-256 -> 32 bytes -> 64 chars
            sha256 = sha256.toUpperCase();
            if (sha256.replaceAll("[0-9A-F]+", "").length() != 0) return false;    // found non hex char

            mWhitelists.put(pkgname, sha256);
            return true;
        }
        public boolean test(Context ctx, String pkgname) {
            // Получение хеша сертификата приложения pkgname из белого списка
            String correctHash = mWhitelists.get(pkgname);

            // Сравнение реального хеша сертификата приложения pkgname с хешем сертификата приложения из белого списка
            return PkgCert.test(ctx, pkgname, correctHash);
        }
}

PkgCert.java

package com.swordfishsecurity.appsec.android.shared;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.content.pm.SigningInfo;
import android.os.Build;
public class PkgCert {
public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
}
public static String hash(Context ctx, String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            // Флаг GET_SIGNATURES объявлен устаревшим с API 28 и уязвим к подмене подписи
            // (Janus, несколько подписантов). Используем GET_SIGNING_CERTIFICATES и SigningInfo.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                PackageInfo pkginfo = pm.getPackageInfo(pkgname, PackageManager.GET_SIGNING_CERTIFICATES);
                SigningInfo signingInfo = pkginfo.signingInfo;
                if (signingInfo == null) return null;
                // Если у приложения несколько подписантов (ротация ключей или multiple signers) —
                // проверять нужно ВСЕ сертификаты; для строгой проверки требуем ровно один.
                if (signingInfo.hasMultipleSigners()) return null;
                Signature[] signatures = signingInfo.getSigningCertificateHistory();
                if (signatures == null || signatures.length != 1) return null;
                byte[] cert = signatures[0].toByteArray();
                return byte2hex(computeSha256(cert));
            } else {
                // На API 27 и ниже доступен только GET_SIGNATURES
                @SuppressWarnings("deprecation")
                PackageInfo pkginfo = pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
                @SuppressWarnings("deprecation")
                Signature[] signatures = pkginfo.signatures;
                if (signatures == null || signatures.length != 1) return null;
                byte[] cert = signatures[0].toByteArray();
                return byte2hex(computeSha256(cert));
            }
        } catch (NameNotFoundException e) {
            return null;
        }
}
private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
}
private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

Использование Partner Activity

Если вы собираетесь использовать Activity из приложения-партнёра, необходимо следовать следующим правилам:

  1. Проверяйте, что сертификат целевого приложения присутствует в «белом списке».
  2. Не устанавливайте флаг FLAG_ACTIVITY_NEW_TASK в Intent, который будет использоваться для запуска Activity.
  3. Отправляйте (с помощью putExtra) только ту информацию, к которой приложение-партнёр имеет право получить доступ, не больше.
  4. Используйте явный Intent для запуска Partner Activity.
  5. Используйте startActivityForResult() для запуска Partner Activity.
  6. Проводите проверку и безопасную обработку полученных данных результата, несмотря на то, что они были получены из приложения-партнёра.
  7. Для реализации первого пункта можно воспользоваться следующими примерами кода:

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.swordfishsecurity.appsec.android.activity.partneruser" >
    <application
            android:allowBackup="false"
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name" >

            <activity
                android:name="com.swordfishsecurity.appsec.android.activity.partneruser.PartnerUserActivity"
                android:label="@string/app_name"
                android:exported="true" >
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    </application>
</manifest>

PartnerUserActivity.java

package com.swordfishsecurity.appsec.android.activity.partneruser;
import com.swordfishsecurity.appsec.android.shared.PkgCertWhitelists;
import com.swordfishsecurity.appsec.android.shared.Utils;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
public class PartnerUserActivity extends Activity {
    // *** 1 *** Проверяйте, что сертификат целевого приложения присутствует в "белом списке"
    private static PkgCertWhitelists sWhitelists = null;
    private static void buildWhitelists(Context context) {
        boolean isdebug = Utils.isDebuggable(context);
        sWhitelists = new PkgCertWhitelists();

        // Регистрация хеша сертификата приложения-партнёра com.swordfishsecurity.appsec.android.activity.partneractivity
        sWhitelists.add("com.swordfishsecurity.appsec.android.activity.partneractivity", isdebug ?
                // Хеш сертификата ключа "androiddebugkey" из debug.keystore.
                "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255" :
                // Хеш сертификата ключа приложения-партнёра com.swordfishsecurity.appsec.android.activity.partneractivity
                "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA");

        // Регистрация других приложений-партнёров осуществляется аналогично
    }
    private static boolean checkPartner(Context context, String pkgname) {
        if (sWhitelists == null) buildWhitelists(context);
        return sWhitelists.test(context, pkgname);
    }

    private static final int REQUEST_CODE = 1;
    // Информация о целевой partner Activity

    private static final String TARGET_PACKAGE =  "com.swordfishsecurity.appsec.android.activity.partneractivity";
    private static final String TARGET_ACTIVITY = "com.swordfishsecurity.appsec.android.activity.partneractivity.PartnerActivity";
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    public void onUseActivityClick(View view) {
    // *** 1 *** Проверяйте, что сертификат целевого приложения присутствует в "белом списке"
        if (!checkPartner(this, TARGET_PACKAGE)) {
            Toast.makeText(this, "Целевое приложение не партнёрское", Toast.LENGTH_LONG).show();
            return;
        }

        try {
            // *** 2 *** Не устанавливайте флаг FLAG_ACTIVITY_NEW_TASK в Intent, который будет использоваться для запуска Activity
            Intent intent = new Intent();

            // *** 3 *** Отправляйте (с помощью `putExtra`) только ту информацию, к которой приложение-партнёр имеет право получить доступ, не больше
            intent.putExtra("PARAM", "Информация для приложения-партнёра");

            // *** 4 *** Используйте явный Intent для запуска partner Activity
            intent.setClassName(TARGET_PACKAGE, TARGET_ACTIVITY);

            // ***  5 *** Используйте startActivityForResult() для запуска partner Activity
            startActivityForResult(intent, REQUEST_CODE);
        }
        catch (ActivityNotFoundException e) {
            Toast.makeText(this, "Целевая Actvity не найдена", Toast.LENGTH_LONG).show();
        }
    }
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode != RESULT_OK) return;

        switch (requestCode) {
        case REQUEST_CODE:
            String result = data.getStringExtra("RESULT");

            // *** 6 *** Проводите проверку и безопасную обработку полученных данных результата, несмотря на то, что они были получены из Activity того же самого приложения
            // См.п. "Безопасная обработка входных данных"

            Toast.makeText(this,
                    String.format("Received result: \"%s\"", result), Toast.LENGTH_LONG).show();
            break;
        }
    }
}

PkgCertWhitelist.java

package com.swordfishsecurity.appsec.android.shared;
import java.util.HashMap;
import java.util.Map;

import android.content.Context;

public class PkgCertWhitelists {
        private Map<String, String> mWhitelists = new HashMap<String, String>();

        public boolean add(String pkgname, String sha256) {
            if (pkgname == null) return false;
            if (sha256 == null) return false;

            sha256 = sha256.replaceAll(" ", "");
            if (sha256.length() != 64) return false;    // SHA-256 -> 32 bytes -> 64 chars
            sha256 = sha256.toUpperCase();
            if (sha256.replaceAll("[0-9A-F]+", "").length() != 0) return false; // found non hex char

            mWhitelists.put(pkgname, sha256);
            return true;
        }

        public boolean test(Context ctx, String pkgname) {
            // Получение хеша сертификата приложения pkgname из белого списка
            String correctHash = mWhitelists.get(pkgname);

            // Сравнение реального хеша сертификата приложения pkgname с хешем сертификата приложения из белого списка
            return PkgCert.test(ctx, pkgname, correctHash);
        }
}

PkgCert.java

package com.swordfishsecurity.appsec.android.shared;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.content.pm.SigningInfo;
import android.os.Build;

public class PkgCert {

        public static boolean test(Context ctx, String pkgname, String correctHash) {
            if (correctHash == null) return false;
            correctHash = correctHash.replaceAll(" ", "");
            return correctHash.equals(hash(ctx, pkgname));
        }

        public static String hash(Context ctx, String pkgname) {
            if (pkgname == null) return null;
            try {
                PackageManager pm = ctx.getPackageManager();
                // Флаг GET_SIGNATURES объявлен устаревшим с API 28 и уязвим к подмене подписи
                // (Janus, несколько подписантов). Используем GET_SIGNING_CERTIFICATES и SigningInfo.
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                    PackageInfo pkginfo = pm.getPackageInfo(pkgname, PackageManager.GET_SIGNING_CERTIFICATES);
                    SigningInfo signingInfo = pkginfo.signingInfo;
                    if (signingInfo == null) return null;
                    if (signingInfo.hasMultipleSigners()) return null;
                    Signature[] signatures = signingInfo.getSigningCertificateHistory();
                    if (signatures == null || signatures.length != 1) return null;
                    byte[] cert = signatures[0].toByteArray();
                    return byte2hex(computeSha256(cert));
                } else {
                    @SuppressWarnings("deprecation")
                    PackageInfo pkginfo = pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
                    @SuppressWarnings("deprecation")
                    Signature[] signatures = pkginfo.signatures;
                    if (signatures == null || signatures.length != 1) return null;
                    byte[] cert = signatures[0].toByteArray();
                    return byte2hex(computeSha256(cert));
                }
            } catch (NameNotFoundException e) {
                return null;
            }
        }

        private static byte[] computeSha256(byte[] data) {
            try {
                return MessageDigest.getInstance("SHA-256").digest(data);
            } catch (NoSuchAlgorithmException e) {
                return null;
            }
        }

        private static String byte2hex(byte[] data) {
            if (data == null) return null;
            final StringBuilder hexadecimal = new StringBuilder();
            for (final byte b : data) {
                hexadecimal.append(String.format("%02X", b));
            }
            return hexadecimal.toString();
        }
}

Ссылки

  1. https://developer.android.com/guide/topics/manifest/activity-element#exported

  2. https://blog.oversecured.com/Android-Access-to-app-protected-components/

  3. PackageManager.GET_SIGNING_CERTIFICATES

  4. SigningInfo

  5. Activity.getCallingActivity()

К началу