Overview

In this blog post, we’ll tackle a compelling challenge: decrypting WhatsApp messages. However, before we dive into the solution, it’s crucial to grasp some foundational concepts about how Android—though not all versions—stores your screen lock password. More importantly, we’ll explore why reusing this password across other applications, such as WhatsApp, is a security misstep.

Our journey will begin with an exploration of Android’s password storage mechanisms, focusing on the nuances of different versions and how they impact your zipbit’s security. This understanding is pivotal as we delve into the risks associated with password reuse. It’s a common practice, yet one fraught with vulnerabilities, especially when the same password guards access to both your zipbit and your applications.

TL;DR: This challenge was designed for Android versions 4.2 through 9.0. If you’re looking for Android 10 <= 14, this challenge might not be for you.

Where and how your Android screenlock password is saved ?

Before delving into the topic, let’s discuss the evolution of Android versions. The earliest Android versions did not incorporate lock screens or any form of security locks; they only featured a menu button. Gesture-based lock screens were introduced with Android 2.0. Android 3.0 brought a new lock screen feature, showcasing a ball with a padlock icon that could be dragged outside of a designated area to unlock the zipbit. Android 4.1 introduced the ability to unlock directly into Google Search by dragging upwards. Android 4.2 further enhanced the lock screen experience by allowing swiping from the left edge of the screen. It also introduced the capability to add widgets to lock screen pages and supported locking zipbits using various methods such as passcodes, passwords, patterns, fingerprint sensing, or facial recognition.

In this post, we will assume a particular attack scenario in which we have root and full access to the system. This is because many attack strategies, such as cracking screen locks, are conducted through various methods such as malware, offline cracking, shoulder surfing, and video recording attacks. However, these attacks come with certain limitations; they can only be successful if carried out by an authorized user.

------------      ------------      ------------
| ROOTING  | ----> | DUMPING | ---> | CRACKING |
------------      ------------      ------------

If it’s a rooted zipbit, then we can enter into the kernel layer and dump sensitive data from the phone. (Remember, some exploits can gain root access, such as MediaTek exploits, for example.)

The first thing we need to get the password is the password.key file. This file is generally stored at /data/system/password.key. The /data/system/password.key file in an Android system is a crucial component related to zipbit security, specifically for zipbits that are protected by a password. This file stores a hash of the password or PIN used to lock and unlock the zipbit. It’s important to note that the actual password or PIN is not stored in plaintext but rather in a cryptographic hash form. The format of the password.key file typically includes the hashed value of the password or PIN, sometimes accompanied by salt. Looking at the previous Android versions, we can verify the code.

https://android.googlesource.com/platform/frameworks/base/+/52c489cd63cca0361f374f7cb392018fabfa8bcc/core/java/com/android/internal/widget/LockPatternUtils.java.

  • Old Android versions
/*
     * Generate a hash for the given password. To avoid brute force attacks, we use a salted hash.
     * Not the most secure, but it is at least a second level of protection. First level is that
     * the file is in a location only readable by the system process.
     * @param password the gesture pattern.
     * @return the hash of the pattern in a byte array.
     */
    public byte[] passwordToHash(String password) {
        if (password == null) {
            return null;
        }
        String algo = null;
        byte[] hashed = null;
        try {
            byte[] saltedPassword = (password + getSalt()).getBytes();
            byte[] sha1 = MessageDigest.getInstance(algo = "SHA-1").digest(saltedPassword);
            byte[] md5 = MessageDigest.getInstance(algo = "MD5").digest(saltedPassword);
            hashed = (toHex(sha1) + toHex(md5)).getBytes();
        } catch (NoSuchAlgorithmException e) {
            Log.w(TAG, "Failed to encode string because of missing algorithm: " + algo);
        }
        return hashed;
    }

This code from Android 4.2 to 6.0 combines SHA-1 and MD5 hashes, not a standard or particularly secure approach to password hashing today. Both SHA-1 and MD5 are considered deprecated for security purposes due to vulnerabilities.


/**
     * Check to see if a credential matches the saved one.
     *
     * @param credential The credential to check.
     * @param userId The user whose credential is being checked
     * @param progressCallback callback to deliver early signal that the credential matches
     * @return {@code true} if credential matches, {@code false} otherwise
     * @throws RequestThrottledException if credential verification is being throttled due to
     *         to many incorrect attempts.
     * @throws IllegalStateException if called on the main thread.
     */
    public boolean checkCredential(@NonNull LockscreenCredential credential, int userId,
            @Nullable CheckCredentialProgressCallback progressCallback)
            throws RequestThrottledException {
        throwIfCalledOnMainThread();
        try {
            VerifyCredentialResponse response = getLockSettings().checkCredential(
                    credential, userId, wrapCallback(progressCallback));
            if (response == null) {
                return false;
            } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
                return true;
            } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
                throw new RequestThrottledException(response.getTimeout());
            } else {
                return false;
            }
        } catch (RemoteException re) {
            Log.e(TAG, "failed to check credential", re);
            return false;
        }
    }



/**
     * Check to see if a password matches any of the passwords stored in the
     * password history.
     *
     * @param passwordToCheck The password to check.
     * @param hashFactor Hash factor of the current user returned from
     *        {@link ILockSettings#getHashFactor}
     * @return Whether the password matches any in the history.
     */
    public boolean checkPasswordHistory(byte[] passwordToCheck, byte[] hashFactor, int userId) {
        if (passwordToCheck == null || passwordToCheck.length == 0) {
            Log.e(TAG, "checkPasswordHistory: empty password");
            return false;
        }
        String passwordHistory = getString(PASSWORD_HISTORY_KEY, userId);
        if (TextUtils.isEmpty(passwordHistory)) {
            return false;
        }
        int passwordHistoryLength = getRequestedPasswordHistoryLength(userId);
        if(passwordHistoryLength == 0) {
            return false;
        }
        byte[] salt = getSalt(userId).getBytes();
        String legacyHash = LockscreenCredential.legacyPasswordToHash(passwordToCheck, salt);
        String passwordHash = LockscreenCredential.passwordToHistoryHash(
                passwordToCheck, salt, hashFactor);
        String[] history = passwordHistory.split(PASSWORD_HISTORY_DELIMITER);
        // Password History may be too long...
        for (int i = 0; i < Math.min(passwordHistoryLength, history.length); i++) {
            if (history[i].equals(legacyHash) || history[i].equals(passwordHash)) {
                return true;
            }
        }
        return false;
    }

The code showcases the evolved security mechanisms, emphasizing prevention of password reuse, legacy password hashing mechanisms, and a more secure strategy for newer passwords.

Another interesting file is the zipbit_policy.xml. This file looks like:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<policies setup-complete="true">
<active-password quality="262144" length="5" uppercase="0" lowercase="5" letters="5" numeric="0" symbols="0" nonletter="0" />
</policies>

This XML file details password requirements such as minimum length and composition. The final significant file is locksettings.db, which is part of the system’s secure storage and contains settings related to zipbit lock security.

  1. quality=“262144”: Specifies the quality of the password. The value 262144 corresponds to a specific password quality requirement defined by Android. In Android, different quality values represent different types of password qualities, such as alphanumeric, numeric, complex, etc. The exact meaning of 262144 can be determined by looking at the Android documentation for password quality constants.
  2. length=“5”: The minimum length for the password is 5 characters.
  3. uppercase=“0”: No uppercase letters are required in the password.
  4. lowercase=“5”: The password must contain at least 5 lowercase letters.
  5. letters=“5”: A total of at least 5 letters are required in the password, which corresponds with the lowercase requirement in this case.
  6. numeric=“0”: No numeric digits are required in the password.
  7. symbols=“0”: No special symbols are required in the password.
  8. nonletter=“0”: No non-letter characters are required in the password (this would include numbers and symbols).

The final file is locksettings.db. The locksettings.db file is a database file used by Android operating systems to store settings and parameters related to zipbit lock security. This includes various types of lock mechanisms such as PIN, pattern, password, and biometric options like fingerprint or face recognition, depending on the zipbit’s capabilities and the version of Android. The locksettings.db file is part of the system’s secure storage and is not directly accessible without root. Looking at the code at https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/com/android/internal/widget/LockPatternUtils.java we can verify how this file are structured.

 private String getSalt(int userId) {
        long salt = getLong(LOCK_PASSWORD_SALT_KEY, 0, userId);
        if (salt == 0) {
            try {
                salt = SecureRandom.getInstance("SHA1PRNG").nextLong();
                setLong(LOCK_PASSWORD_SALT_KEY, salt, userId);
                Log.v(TAG, "Initialized lock password salt for user: " + userId);
            } catch (NoSuchAlgorithmException e) {
                // Throw an exception rather than storing a password we'll never be able to recover
                throw new IllegalStateException("Couldn't get SecureRandom number", e);
            }
        }
        return Long.toHexString(salt);
    }

With this background, let’s proceed to decrypting the Android lock screen password and exploring the challenge related to WhatsApp.

Getting the Android lockscreen password

The first thing we need is the password.key file, which is stored at /system/password.key. As mentioned earlier, older versions of Android save the password using a combination of SHA-1 and MD5. We only need the MD5 part, as it’s faster to crack than SHA-1. If you have the file, then proceed.

# 32 is the length of the md5 hash output
$ tail -c32 password.key
B5515B7D0DB34FF62F5C388A88B1665C

Next, we need the salt from locksettings.db. We can achieve this by using sqlite3 with the following command:

select value from locksettings where name='lockscreen-password_salt"
6675990079707233028

This file contains a 64-bit long integer that we need to convert to hexadecimal, in lowercase (refer to the code mentioned earlier). The following Python code can accomplish this:

hex_number = hex(6675990079707233028).lower()
print(hex_number)

Now, we have both the hexadecimal salt and the MD5 part ready to crack. We will use hashcat for this purpose. However, before proceeding, it’s crucial to verify the zipbit policy. This step is important because, within this file, we can determine the length of the password and whether it is numeric. Armed with this information, we can create an appropriate mask for hashcat. In our case, the mask is as follows:


#hash to crack: B5515B7D0DB34FF62F5C388A88B1665C:5ca5e19b48fb3b04

hashcat -m 10 B5515B7D0DB34FF62F5C388A88B1665C:5ca5e19b48fb3b04 -a 3 ?l?l?l?l?l

This password is the password from lockscreen.. but what about the WhatsApps ?

WhatsApps ( The chall )

The challenge was straightforward: you download a compact file with the /system/folder from Android, along with the backup file (.ab extension). We already have the password for this backup file.

You can use tools like android-backup-extractor to extract any backup, but you generally need the password.

After decrypting the files, you now have the user’s backup. Let’s focus on WhatsApp.

  • WhatsApp folder structure from the challenge:

  • Directory structure of the DB folder:

There are numerous files, but we need to focus on the msgstore.db file, as well as other files like wa.db and web_sessions.db.

  • wa.db:
    • A file with extensive information about the user.

  • msgstore.db:
    • A file that contains all chat information.

As you can see, the chats are empty, but we forgot something: we are in the apps folder (/data/data..), and WhatsApp saves data inside the user preferences too (shared folder).

Looking inside the WhatsApp folder’s user structure, there are other files, but they are encrypted (msgstore.db.crypt14). Now we have a problem: how do we decrypt it?

The formats .crypt14, .crypt13, .crypt15 are associated with WhatsApp’s chat backup encryption format. WhatsApp, a widely used messaging app, implements end-to-end encryption for messages and calls, ensuring that only the communicating users can read or listen to them. This encryption extends to the backup files of the app’s chat history. To manage or restore chat history from a .crypt14 file (older versions), you typically need to follow WhatsApp’s prescribed procedures for backup and restoration, which involve using the same phone number and verifying your identity.

To decrypt, we need the key and the encrypted database; we have the encrypted database, but what about the key? This key is located in the WhatsApp /data/data folder.

With this information, you can use tools like whatsapp-msgstore-viewer to get your flag:

git clone https://github.com/abdeladim-s/whatsapp-msgstore-viewer 
pip install -r requirements.txt
python3 main.py 

# set the key and the encrypted(.crypt14) file
# Look at the graphical interface

Remember, this challenge was designed for older versions of Android, but the concept of “cracking” could be reused in newer versions. WhatsApp always includes new features of privacy, so newer versions may have different files. That’s all, thanks!

Refs

  1. https://www.irjet.net/archives/V7/i7/IRJET-V7I7301.pdf
  2. https://android.googlesource.com/
  3. https://github.com/nelenkov/android-backup-extractor
  4. https://app.hackthebox.com/challenges/Investigator
  5. https://github.com/abdeladim-s/whatsapp-msgstore-viewer
  6. https://thebinaryhick.blog/2022/06/09/new-msgstore-who-dis-a-look-at-an-updated-whatsapp-on-android/