Mobile Hacking Lab - IoT Connect

Exploiting Broadcast Vulnerabilities on Android: A Mobile Hacking Lab CTF Challenge

Broadcast receivers in Android apps can sometimes introduce critical vulnerabilities if not implemented with proper care and attention. Today, we’ll explore a challenge from the Mobile Hacking Lab CTF that puts your skills to the test in identifying and exploiting broadcast receiver flaws.

BroadcastReceiver in Android is like a listener that waits for certain messages to arrive. These messages, called broadcasts, can come from the system or other apps and can signal things like a phone call coming in, the battery getting low, or even the phone booting up.

When you want your app to react to these kinds of events, you create a BroadcastReceiver and tell it what kinds of messages to listen for. When a matching message arrives, the BroadcastReceiver wakes up and does whatever action you’ve told it to do, like displaying a notification or updating some data.

BroadcastReceivers can be set up to run either when your app is open or even when it’s not actively being used. They’re a handy way to make your app respond to all sorts of different situations without needing to constantly check for them.

You can access the challenge here: Mobile Hacking Lab - IoT Connect

The Challenge Overview

The goal is to exploit a flaw in the “IOT Connect” Android app’s Broadcast Receiver feature. This exploit will allow you to activate the master switch and take control of all connected devices! The challenge is to send a broadcast message in a way that guest users can’t, bypassing their limitations… Let’s go!

Discovery Phase

After deploying the APK on my device and launching the IoT Connect app, I began exploring its functionalities. Upon creating an account, I was presented with a range of smart devices that the app could control. However, I noticed that some of these devices were locked, suggesting that certain features were restricted.

Further exploration of the app revealed an additional tab designed to control all the connected devices simultaneously. However, to access this functionality, a PIN code was required, acting as an authentication measure to prevent unauthorized users from gaining complete control over the entire ecosystem:

No better tool than Deeeeper to find Activities, let’s run it:

We see the a broadcast receiver!

  • com.mobilehackinglab.iotconnect.MasterReceiver: This is the name of the MasterReceiver class in the application. It includes the package name com.mobilehackinglab.iotconnect followed by the class name MasterReceiver.
  • (exported=true): This indicates that the MasterReceiver is exported. It means that it can be activated by components of other apps.
  • MASTER_ON: This is an action name that MasterReceiver is listening for. MASTER_ON is the intent action that when broadcasted, the MasterReceiver is set up to receive and process.

Awesome, I’m on the right track.

Time to dive into the code and see what we’ve got to work with…

Code Analysis

Lots of code to go through, but let’s try to keep it short and sweet.

CommunicationManager

Let’s focus on this block:

public final class CommunicationManager {
    public static final CommunicationManager INSTANCE = new CommunicationManager();
    private static BroadcastReceiver masterReceiver;
    private static SharedPreferences sharedPreferences;

    private CommunicationManager() {
    }

    public final BroadcastReceiver initialize(Context context) {
        Intrinsics.checkNotNullParameter(context, "context");
        masterReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context2, Intent intent) {
                if (Intrinsics.areEqual(intent != null ? intent.getAction() : null, "MASTER_ON")) {
                    int key = intent.getIntExtra("key", 0);
                    if (context2 != null) {
                        if (Checker.INSTANCE.check_key(key)) {
                            CommunicationManager.INSTANCE.turnOnAllDevices(context2);
                            Toast.makeText(context2, "All devices are turned on", Toast.LENGTH_LONG).show();
                            return;
                        }
                        Toast.makeText(context2, "Wrong PIN!!", Toast.LENGTH_LONG).show();
                    }
                }
            }
        };
        return masterReceiver;
    }
}

Instantiation and Definition of masterReceiver

masterReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context2, Intent intent) {
        // ... Implementation of onReceive
    }
};

The masterReceiver field is assigned a new BroadcastReceiver object. The onReceive method is overridden to define the behaviour when the BroadcastReceiver receives the intent.

It’s exactly what we saw from the Deeeeper result: com.mobilehackinglab.iotconnect.MasterReceiver

This means we can’t call MasterReceiver directly? let’s keep going…

Behavior of onReceive Method

public void onReceive(Context context2, Intent intent) {
    if (Intrinsics.areEqual(intent != null ? intent.getAction() : null, "MASTER_ON")) {
        int key = intent.getIntExtra("key", 0);
        if (context2 != null) {
            if (Checker.INSTANCE.check_key(key)) {
                CommunicationManager.INSTANCE.turnOnAllDevices(context2);
                Toast.makeText(context2, "All devices are turned on", 1).show();
                return;
            }
            Toast.makeText(context2, "Wrong PIN!!", 1).show();
        }
    }
}

This method checks if the received Intent has the action "MASTER_ON" like we saw from Deeeeper.

It extracts an integer extra named "key" from the Intent. If "key" is not found, it defaults to 0. It checks this key using Checker.INSTANCE.check_key(key). If the key is correct, it executes turnOnAllDevices(context2) to turn on all devices and shows a Toast message indicating success. If the key is wrong, it shows a Toast with a “Wrong PIN!!” message.

This will help build our command! That’s great.

Now onto the Checker class, this one is located in the defpackage

Checker

The class Checker is used to check if a given key can correctly decrypt a predefined encrypted string (ds) to match a known plaintext value "master_on".

Class Definition and Singleton Pattern

public final class Checker {
    public static final Checker INSTANCE = new Checker();
    private static final String algorithm = "AES";
    private static final String ds = "OSnaALIWUkpOziVAMycaZQ==";

    private Checker() {
    }
    ...
}
  • algorithm: A static final String specifying the encryption algorithm, “AES”.
  • ds: A static final String holding the base64-encoded encrypted data.

Checking the Key

public final boolean check_key(int key) {
    try {
        return Intrinsics.areEqual(decrypt(ds, key), "master_on");
    } catch (BadPaddingException e) {
        return false;
    }
}

check_key(int key): It attempts to decrypt the encrypted string ds with the provided key and checks if the result equals to master_on. It returns true if the decryption matches and false when a BadPaddingException is thrown, it indicates that the key is incorrect due to improper padding after decryption.

Decryption Method

public final String decrypt(String ds2, int key) {
    Intrinsics.checkNotNullParameter(ds2, "ds");
    SecretKeySpec secretKey = generateKey(key);
    Cipher cipher = Cipher.getInstance(algorithm + "/ECB/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, secretKey);
    if (Build.VERSION.SDK_INT >= 26) {
        byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(ds2));
        Intrinsics.checkNotNull(decryptedBytes);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }
    throw new UnsupportedOperationException("VERSION.SDK_INT < O");
}

This method is very important!

decrypt(String ds2, int key): takes the base64-encoded encrypted string ds2 and a key, and attempts to decrypt it.

  1. It first generates a SecretKeySpec with the provided key.
  2. Initializes a Cipher instance for AES in ECB mode with PKCS#5 padding.
  3. Checks if the Android SDK version is at least 26 (Android Oreo), as the decryption process uses API methods available in Oreo and later.
  4. Decodes the base64-encoded ds2, decrypts it, and returns the decrypted string in UTF-8 format.
  5. Throws an UnsupportedOperationException if the SDK version is below 26.

Cracking the Pin

Since we have the ds value, we can build a python script to crack the pin:

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from base64 import b64decode
import itertools

encoded_ds = "OSnaALIWUkpOziVAMycaZQ=="
target = "master_on"

decoded_ds = b64decode(encoded_ds)

for key_int in itertools.count(start=1):
    key_bytes = str(key_int).encode("utf-8").ljust(16, b'\0')[:16]

    try:
        cipher = AES.new(key_bytes, AES.MODE_ECB)
        decrypted = unpad(cipher.decrypt(decoded_ds), AES.block_size).decode("utf-8")

        if decrypted == target:
            print(f"Found key: {key_int}")
            break
    except Exception as e:
        continue

    if key_int > 1000000:
        print("Key not found within limit.")
        break
  • Import required modules:
    • Crypto.Cipher.AES: Used for performing AES encryption and decryption.
    • Crypto.Util.Padding: Provides padding and unpadding functions for the plaintext.
    • base64: Handles base64 encoding and decoding.
    • itertools: Provides iterators for efficient looping.
  • Base64-encoded and encrypted data setup:
    • encoded_ds: This is the encrypted data we found in the Checker class.
    • target: This is the known plaintext string we want to find by decrypting the ds value.
  • Decode base64: The encrypted data is base64-decoded to get the actual encrypted bytes.
  • Brute-force the key:
    • The script uses an itertools.count iterator to generate integer keys starting from 1 and incrementing by 1 with each iteration.
    • Each integer key key_int is converted to a byte string, padded with null bytes to ensure it’s 16 bytes long, which is the required key length for AES with a block size of 128 bits.
  • Attempt to decrypt:
    • Within a try block, the script creates a new AES cipher object for each key using AES.MODE_ECB.
    • It tries to decrypt the data with this key and then unpad it according to the AES block size. If the padding is invalid, an exception will be thrown, and the script will continue to the next key.
    • It then decodes the decrypted bytes to a UTF-8 string.
  • Check for the target string:
    • If the decrypted string matches the target, it prints the key and breaks out of the loop :) :)
  • Exception handling:
    • If any exception is raised during the decryption or unpadding process the loop continues with the next key continue.
  • Limit the brute-force attempt:
    • The script stop trying after 1,000,000 try. If it reaches this key without decrypting it, it prints a message indicating the key was not found within the limit and breaks out of the loop.

Running the script give us the pin!:

We have the secret pin: 345

Can we use it to call the broadcast receiver? Let’s try to exploit it…

Exploitation

Since the CommunicationManager reveals that masterReceiver is not a standalone class but an anonymous inner class of BroadcastReceiver. This anonymous inner class is instantiated within the CommunicationManager class.

Given this setup, if you try to call the MasterReceiver like you normally would with adb, it won’t work, the system can’t find a MasterReceiver.

Good news is, we can send a broadcast to trigger the anonymous inner BroadcastReceiver via ADB, you generally don’t need to specify the receiver class, because broadcast intents are handled by the Android system and dispatched to all interested BroadcastReceivers that have the appropriate intent filters set up. Which in our case, works!

We can use adb for this, we pass the action MASTER_ON, the --ei extra_key extra_int_value which is the extra "key" and the int value 345, and run pidcat for monitoring:

adb shell am broadcast -a MASTER_ON --ei key 345

Send the command:

The output from pidcat indicates that all devices have been successfully activated!

All our devices are up and running, and there’s a cool green glow on each button to show they’re on!

Conclusion

Another thrilling challenge conquered at Mobile Hacking Lab!

The “IoT Connect” challenge threw us into broadcast receivers vulnerabilities. This challenge revolved around an IoT device control app that help the remote management of smart devices. However, it contained a critical flaw that opened the door to unauthorized device control. Imagine having someone else control all your devices? nightmare!

Let’s move on to the next challenge!

Thank you Mobile Hacking Lab :)