Cracking Android Biometric Authentication with Frida
12 min read
September 15, 2024
Table of contents
Introduction
In this chapter of my Android pentesting series, I’ll take a closer look at local authentication—a critical security feature in modern apps. We’ll develop a small application to give you a hands-on understanding of how local authentication works using the BiometricPrompt API. After building the authentication layer, I’ll demonstrate how attackers can bypass it using Frida on a rooted emulator.
This practical approach will help you not only understand how to implement authentication but also reveal where its vulnerabilities lie and how they can be exploited.
In this chapter, I’ll cover:
- Setting up the Android Emulator for biometric testing.
- Developing and testing a basic BiometricPrompt authentication app.
- Bypassing authentication using Frida.
- Identifying common weaknesses in basic authentication setups.
By the end, you’ll have a solid grasp of how local authentication works, its limitations, and how to protect against potential bypass attacks. In future chapters, we’ll dive deeper into securing the authentication flow and leveraging cryptographic operations to strengthen your app’s security.
Using Android Studio Emulator for Biometric Testing
To accurately test biometric features such as fingerprint authentication, it's recommended to use the Android Emulator from Android Studio rather than third-party virtual machines. The Android Emulator provides built-in support for fingerprint sensors, making it ideal for testing biometric features. Here's a brief setup guide:
- Install Android Studio: First, download and install Android Studio, then open your project or create a new one.
- Set Up the Emulator: Go to Tools > AVD Manager, create a virtual device running Android 9.0 or higher (required for biometric support), and start the emulator.
- Install ADB (Android Debug Bridge): Ensure you have ADB installed on your host machine. ADB is crucial for interacting with the emulator from the command line and for debugging your app. You can install ADB by following this guide.
For detailed steps on each process, refer to the following tutorials:
With the emulator ready, it’s time to explore how Android manages local authentication. BiometricPrompt simplifies integrating biometric features like fingerprints and facial recognition into your app. Let’s delve into the key concepts of local authentication and how BiometricPrompt ensures secure user verification.
Understanding BiometricPrompt and Local Authentication in Android
When developing secure Android applications, authentication is a critical aspect, and Android provides several methods to verify users locally—without the need for online services. One of the most important tools for local authentication today is BiometricPrompt, an API introduced in Android 9 (Pie) that simplifies the integration of biometric security features like fingerprints or facial recognition into your app.
What is Local Authentication?
Local authentication is the process of verifying a user's identity directly on the device. Unlike remote authentication, which checks credentials on a server, local authentication ensures that access to certain data or features is controlled strictly within the app or the device itself.
Traditional methods of local authentication include:
- PINs or passwords, where users manually enter a passcode.
- Pattern unlocks, a method familiar to most Android users.
While these methods are still widely used, biometric authentication has become increasingly popular due to its convenience and higher level of security.
Introduction to BiometricPrompt
BiometricPrompt is Android’s modern framework for managing biometric authentication. It offers a unified way to prompt users for biometric data, handle the sensitive information securely, and provide developers with a simple API to integrate this into apps.
Why BiometricPrompt Matters
In earlier Android versions, developers used different APIs for each biometric type (e.g., FingerprintManager). This led to inconsistencies and security risks since developers had to handle more complexity themselves. BiometricPrompt solves this by:
- Standardizing biometric access: Whether the user has a fingerprint scanner or face unlock, the same API manages it.
- Improving security: Biometric data is handled inside the device’s Trusted Execution Environment (TEE) or Secure Hardware, which means neither the operating system nor any apps can directly access the biometric data.
- User experience: It ensures a consistent and familiar authentication prompt across all apps, which helps users feel more secure.
How BiometricPrompt Works
When you use BiometricPrompt in your app, here’s what happens under the hood:
- The system displays a secure prompt asking the user to authenticate using a registered biometric (e.g., fingerprint, face).
- Biometric data is captured and processed entirely in secure hardware, meaning the app never has direct access to the raw data.
- If authentication is successful, BiometricPrompt triggers a callback in the app, which can be used to unlock sensitive features or data.
The BiometricPrompt API also allows for secure cryptographic operations through its CryptoObject class. This feature binds a cryptographic operation (like signing or encrypting data) to successful biometric authentication, ensuring the app’s sensitive operations can only proceed when the user’s identity is confirmed. In the following chapters, we will delve deeper into how to leverage these cryptographic operations effectively, and how to secure them against potential bypass techniques.
Code Explanation: Implementing Biometric Authentication in Android
Now that we’ve covered the theory behind BiometricPrompt, let's see how to implement it in practice. The following code example walks through setting up biometric authentication in an Android app, from verifying if the device supports biometrics to handling successful authentication.
MainActivity: Setting Up the Interface
The MainActivity
is the entry point of the app, and the user interface is set up in the onCreate()
method. The layout of the activity is defined in activity_main.xml
, which includes a button (btn_authenticate
). When the button is clicked, the app triggers the method validateBiometricSupportAndAuthenticate()
to initiate the biometric authentication flow.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val btnAuthenticate: Button = findViewById(R.id.btn_authenticate)
btnAuthenticate.setOnClickListener {
validateBiometricSupportAndAuthenticate()
}
}
The setOnClickListener()
sets up a listener for the button, which calls the method to check for biometric support when pressed.
Checking for Biometric Support
The validateBiometricSupportAndAuthenticate()
method uses the BiometricManager class to check if the device supports biometric authentication. The method evaluates several conditions, providing feedback using Toast
messages to inform the user about the status of the biometric hardware and whether credentials are enrolled.
private fun validateBiometricSupportAndAuthenticate() {
val biometricManager = BiometricManager.from(this)
when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)) {
BiometricManager.BIOMETRIC_SUCCESS -> {
// The device supports biometric authentication
showBiometricPrompt()
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
// The device does not have biometric hardware
Toast.makeText(this, "No biometric hardware available", Toast.LENGTH_SHORT).show()
}
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
// The biometric hardware is currently unavailable
Toast.makeText(this, "Biometric hardware currently unavailable", Toast.LENGTH_SHORT).show()
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
// No biometric credentials are enrolled on the device
Toast.makeText(this, "No biometric credentials enrolled", Toast.LENGTH_SHORT).show()
}
}
}
If the device supports biometric authentication (BIOMETRIC_SUCCESS
), the app calls showBiometricPrompt()
to proceed with the authentication process. If the device doesn't support biometrics or lacks enrolled credentials, appropriate error messages are displayed using Toast
.
Displaying the Biometric Prompt
The showBiometricPrompt()
method configures and displays the biometric authentication dialog. It sets up an Executor to manage the callbacks and ensure the authentication process runs smoothly on the main UI thread. The BiometricPrompt instance is created with an AuthenticationCallback that listens for success or failure of the authentication attempt.
private fun showBiometricPrompt() {
val executor = ContextCompat.getMainExecutor(this)
val biometricPrompt = BiometricPrompt(this, executor, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
showSuccess()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Toast.makeText(this@MainActivity, "Authentication failed", Toast.LENGTH_SHORT).show()
}
})
- onAuthenticationSucceeded() is triggered when the user successfully authenticates, and it calls
showSuccess()
. - onAuthenticationFailed() handles authentication failures and informs the user via a
Toast
.
The biometric prompt is then configured using BiometricPrompt.PromptInfo.Builder()
, where you define the title, subtitle, and a fallback option for users who choose not to use biometrics.
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric Authentication")
.setSubtitle("Log in using your fingerprint")
.setNegativeButtonText("Use password")
.build()
biometricPrompt.authenticate(promptInfo)
The PromptInfo dialog informs the user that they need to authenticate using their fingerprint. If the user prefers, they can select the "Use password" option to authenticate using a different method.
Handling Successful Authentication
When the user successfully authenticates, the showSuccess()
method is invoked. This method displays a success message using Toast
and navigates the user to a new activity (SuccessActivity
) using an Intent. Optionally, finish()
is called to close the MainActivity
, preventing the user from navigating back to it without re-authenticating.
private fun showSuccess() {
Toast.makeText(this, "Authentication successful!", Toast.LENGTH_SHORT).show()
// Navigate to the SuccessActivity
val intent = Intent(this, SuccessActivity::class.java)
startActivity(intent)
finish() // Optionally finish MainActivity to prevent going back without re-authentication
}
The transition to SuccessActivity
completes the authentication process, showing that the user has been successfully authenticated.
package com.example.localauth
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class SuccessActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_success)
}
}
All code
package com.example.localauth
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import android.content.Intent
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val btnAuthenticate: Button = findViewById(R.id.btn_authenticate)
btnAuthenticate.setOnClickListener {
validateBiometricSupportAndAuthenticate()
}
}
private fun validateBiometricSupportAndAuthenticate() {
val biometricManager = BiometricManager.from(this)
when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)) {
BiometricManager.BIOMETRIC_SUCCESS -> {
// The device supports biometric authentication
showBiometricPrompt()
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
// The device does not have biometric hardware
Toast.makeText(this, "No biometric hardware available", Toast.LENGTH_SHORT).show()
}
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
// The biometric hardware is currently unavailable
Toast.makeText(this, "Biometric hardware currently unavailable", Toast.LENGTH_SHORT).show()
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
// No biometric credentials are enrolled on the device
Toast.makeText(this, "No biometric credentials enrolled", Toast.LENGTH_SHORT).show()
}
}
}
private fun showBiometricPrompt() {
val executor = ContextCompat.getMainExecutor(this)
val biometricPrompt = BiometricPrompt(this, executor, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
showSuccess()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Toast.makeText(this@MainActivity, "Authentication failed", Toast.LENGTH_SHORT).show()
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric Authentication")
.setSubtitle("Log in using your fingerprint")
.setNegativeButtonText("Use password")
.build()
biometricPrompt.authenticate(promptInfo)
}
private fun showSuccess() {
Toast.makeText(this, "Authentication successful!", Toast.LENGTH_SHORT).show()
// Navigate to the SuccessActivity
val intent = Intent(this, SuccessActivity::class.java)
startActivity(intent)
finish() // Optionally finish MainActivity to prevent going back without re-authentication
}
}
Layouts Explanation
The user interface for the app consists of two key layouts: one for the main activity where the user initiates authentication, and another for the success screen.
MainActivity Layout (activity_main.xml
)
This layout defines a simple user interface using a LinearLayout that centers a button on the screen. The button is used to initiate biometric authentication.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<Button
android:id="@+id/btn_authenticate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Biometric Authentication" />
</LinearLayout>
- The LinearLayout ensures that the button is vertically centered on the screen.
- The button, with
id="btn_authenticate"
, displays the text "Biometric Authentication" and is linked to theMainActivity
via thefindViewById()
method. When clicked, it initiates the authentication process.
SuccessActivity Layout (activity_success.xml
)
This layout is for the success screen that the user sees after successful authentication. It contains a TextView displaying a success message and an ImageView for visual feedback.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<TextView
android:id="@+id/success_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Access Granted!"
android:textSize="24sp"
android:textColor="@android:color/black"
android:layout_marginBottom="16dp"/>
<ImageView
android:id="@+id/success_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/charmander_victory"
android:contentDescription="Charmander showing victory" />
</LinearLayout>
- The TextView displays the message "Access Granted!" with a large text size (24sp) and a bottom margin of
16dp
to provide spacing between the text and the image. - The ImageView displays an image (
charmander_victory
) that visually reinforces the success message. The image is centered below the text and uses the drawable resource@drawable/charmander_victory
.
Testing the App in the Android Studio Emulator
To begin testing the app, we first need to set up fingerprint authentication in the emulator. Start by navigating to the Security settings in the emulator and look for the Pixel Imprint option.
Once inside the Pixel Imprint section, follow the instructions to register a fingerprint. You’ll be prompted to touch the sensor multiple times until the fingerprint is fully registered, as shown in the image below:
After successfully registering the fingerprint, you should see it listed under Pixel Imprint in the security settings.
With the fingerprint configured, it’s time to run the app. In Android Studio, simply press the Run button to deploy the application to the emulator.
If everything is set up correctly, the app’s user interface (UI) will appear in the emulator. As previously developed, the main layout contains a single button. By pressing this button, the app will prompt you to authenticate using the fingerprint you just set up.
If the correct fingerprint is provided, you’ll successfully authenticate and be taken to the second screen, which features a cheerful Charmander! 😆
Bypassing local authentication
Now that we have the application installed, it’s time to demonstrate how to bypass local authentication using a rooted emulator.
Rooting the Emulator
The first step is to root the Android Studio emulator. To make this process simpler, we’ll use a script that automates everything for us. Follow these steps:
git clone https://gitlab.com/newbit/rootAVD.git
./rootAVD.sh ListAllAVDs
After running the script for the first time, it will provide instructions on how to root the device.
Simply copy and paste the command provided by the script into your terminal. The script will then handle the entire rooting process for you.
Once the emulator is rooted, you can verify root access by running the following commands:
adb shell
su
At this point, a dialog will appear in the emulator asking for permission to grant root access. Press Grant to confirm. You should now have root access to the emulator.
Installing frida server
To install Frida Server, we’ll use a Magisk module that automates the process. You can download the module from the link below:
Installing the Module
First, transfer the .zip
file from the module’s GitHub release page to the emulator using ADB:
adb push module.zip /sdcard/Download
Once the module is copied, open Magisk on the emulator and click Install From Storage. Then, select the .zip
file you just transferred.
After the module is installed, enable it and restart the emulator.
Verifying the Installation
If everything is set up correctly, you should see the Frida server running on port 27042. To verify this, run the following command:
netstat -tupln | grep "27042"
You should see the port listed as active.
Bypass using frida
Now that everything is set up, we’ll use Frida to bypass the local authentication in the app. This process is straightforward and allows you to test whether the application is vulnerable to this type of attack.
Download the Bypass Script
First, download the script from the following link:
This script performs several checks and, depending on the defenses implemented by the application, it will attempt different methods to bypass the authentication.
Running the Bypass Script
To use the script, launch the application with Frida by running the following command in your terminal:
frida -U -f com.example.localauth -l global-bypass.js
Once the app is running, navigate to the main screen. When you press the button to authenticate, the Frida script will automatically bypass the authentication, granting you access to the second screen of the app.
Conclusion
In this chapter of the Android pentesting series, we implemented a basic local authentication using BiometricPrompt and demonstrated how it can be bypassed using Frida on a rooted emulator.
Key insights:
- The BiometricPrompt API provides a standard way to handle biometric authentication securely.
- Rooted devices and tools like Frida expose vulnerabilities in basic authentication setups.
- Strengthening code is crucial to preventing bypass attacks.
In the next chapter, we’ll focus on making the authentication process more robust and resistant to these kinds of attacks.
Resources
- Biometric Authentication with BiometricPrompt. "Android Developers." Available at: https://developer.android.com/training/sign-in/biometric-auth
- Frida - Dynamic Instrumentation Toolkit. "Frida." Available at: https://frida.re
- Android Emulator Setup for Pentesting. "HackTricks." Available at: https://book.hacktricks.xyz/mobile-apps-pentesting/android-pentesting/android-emulator
- Rooting the Android Emulator. "RootAVD GitLab." Available at: https://gitlab.com/newbit/rootAVD
- Bypass Biometric Authentication in Android. "HackTricks." Available at: https://book.hacktricks.xyz/mobile-pentesting/android-app-pentesting/bypass-biometric-authentication-android
- OWASP Mobile Application Security Testing Guide (MASTG) - Biometric Authentication Testing. "OWASP." Available at: https://mas.owasp.org/MASTG/chapters/0x06b-Testing-Authentication-and-Session-Management
Chapters
Previous chapter
Next chapter