Activity劫持原理:恶意Activity如何通过任务栈劫持合法Activity
大家好,我是你们的老朋友。今天我们来聊聊Android安全里一个非常经典、也非常“脏”的攻击手法——Activity劫持。
说白了,就是恶意应用怎么偷偷把你的界面换成它的界面。你明明在登录银行App,看到的却是假的输入框。密码就这么被钓走了。
我当年第一次在测试环境复现这个攻击时,后背真的发凉。你想想看,用户完全不知情,所有操作看起来都正常。嗯,这就是Activity劫持的可怕之处。
任务栈(Task)是什么?
要理解劫持,先得理解任务栈。Android里每个App启动Activity时,系统会把这些Activity放进一个栈里。这个栈就叫任务栈(Task)。
栈的特点是什么?后进先出。你打开A,再打开B,再打开C。按返回键时,C先退出,然后B,最后A。这就是标准行为。
每个任务栈有一个根Activity。栈顶的Activity就是当前用户正在交互的那个。系统只把焦点给栈顶的Activity。
好,关键点来了——恶意应用可以把自己的Activity插入到你的任务栈里,甚至直接推到栈顶。这就是劫持的核心。
劫持的两种典型手法
我归纳了一下,常见的Activity劫持分两类。咱们一个一个说。
1. 任务栈注入(Task Injection)
恶意App启动一个Activity时,如果设置了特定的Intent Flag,它就能把自己的Activity放到你的任务栈里。
最常见的Flag是:
Intent.FLAG_ACTIVITY_NEW_TASK
Intent.FLAG_ACTIVITY_CLEAR_TOP
Intent.FLAG_ACTIVITY_SINGLE_TOP
组合使用这些Flag,恶意App可以做到:
- 把自己的Activity推到前台
- 覆盖在合法App的界面之上
- 甚至让用户以为它才是合法App
我在项目中遇到过这样一个案例:某恶意应用监听系统广播,一旦检测到银行App启动,立刻用FLAG_ACTIVITY_NEW_TASK启动一个伪造的登录界面。用户看到的确实是银行App的图标和名称,但界面是假的。
2. 界面覆盖(Overlay Attack)
这个更隐蔽。恶意App不需要启动自己的Activity,它只需要在合法Activity上面绘制一个悬浮窗。
用到的API是:
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
或者老版本的:
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
恶意App申请SYSTEM_ALERT_WINDOW权限后,就可以在任意界面之上绘制一个半透明的覆盖层。用户点击时,点击事件被覆盖层拦截,而不是底层的合法Activity。
我印象很深,有一次做渗透测试,发现一个恶意App就是利用这个手法。它在用户打开支付App时,在密码输入框上方覆盖了一个透明的“触摸拦截层”。用户输入密码时,密码被恶意App记录,同时事件也透传到底层App。用户完全没感觉。
- 任务栈注入:恶意Activity替换了合法Activity
- 界面覆盖:合法Activity还在,但上面盖了一层“透明纸”
攻击流程全景图
我画了一张图,帮你把整个攻击链路看清楚。
你看这张图。用户启动合法App后,合法Activity进入任务栈。恶意App通过系统接口把自己的Activity推到栈顶。用户看到的变成了恶意界面。合法App被压在下面,完全不可见。
这就是典型的“狸猫换太子”。
钓鱼攻击的具体实现
光说原理不够,咱们看看代码。恶意App怎么监听合法App的启动?
// 恶意App注册一个BroadcastReceiver
public class AppMonitorReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String packageName = intent.getStringExtra("package");
if (packageName.equals("com.target.bank")) {
// 目标App启动了,立刻劫持
Intent hijackIntent = new Intent(context, FakeLoginActivity.class);
hijackIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(hijackIntent);
}
}
}
这段代码里,恶意App监听系统广播。一旦检测到目标包名,立刻启动伪造的Activity。FLAG_ACTIVITY_NEW_TASK确保这个Activity能独立启动,并且出现在当前任务栈的顶部。
你可能会问:系统广播能随便监听吗?
嗯,这里有个坑。从Android 8.0开始,系统对隐式广播做了限制。但很多App仍然使用显式广播,或者通过轮询方式检测前台App。恶意App可以用UsageStatsManager来获取当前正在运行的App包名。
界面覆盖的代码实现
再来看看界面覆盖的写法。这个更隐蔽。
// 恶意App创建悬浮窗
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
// 创建覆盖层布局
View overlayView = LayoutInflater.from(this).inflate(R.layout.overlay_layout, null);
// 设置WindowManager参数
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
PixelFormat.TRANSLUCENT
);
// 添加到窗口
windowManager.addView(overlayView, params);
这段代码创建了一个全屏的、半透明的覆盖层。FLAG_NOT_FOCUSABLE让覆盖层不获取焦点,用户触摸事件可以穿透到底层。但恶意App可以通过onTouchEvent悄悄记录用户的点击位置和输入内容。
我曾经在分析一个恶意样本时,发现它甚至能精确覆盖到目标App的“确认支付”按钮上。用户点击时,恶意App先记录事件,再伪造一个“支付成功”的Toast。用户以为支付完成了,实际上钱转到了攻击者账户。
如何检测和防护?
说了这么多攻击手法,咱们也得聊聊怎么防。我总结了几条实用建议。
| 防护手段 | 说明 | 优先级 |
|---|---|---|
| 检查栈顶Activity | 通过ActivityManager.getRunningTasks检测当前栈顶是否属于自己 | 高 |
| 使用FLAG_SECURE | 设置WindowManager.LayoutParams.FLAG_SECURE,禁止截屏和覆盖 | 高 |
| 检测SYSTEM_ALERT_WINDOW权限 | 检查是否有其他App申请了悬浮窗权限 | 中 |
| 使用Android 12+的隐私保护 | 新版本系统对覆盖层有更严格的限制 | 中 |
我个人最推荐的做法是:在关键界面(如登录、支付)设置FLAG_SECURE。这样即使恶意App想覆盖,系统也会阻止。
// 在关键Activity的onCreate中设置
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
还有一个技巧:定期检查当前任务栈的根Activity是不是自己的。如果不是,说明可能被劫持了。
// 检查当前栈顶Activity
ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
List<ActivityManager.RunningTaskInfo> tasks = am.getRunningTasks(1);
String topActivity = tasks.get(0).topActivity.getClassName();
if (!topActivity.equals(getClass().getName())) {
// 可能被劫持,做相应处理
}
写在最后
Activity劫持这个漏洞,说难防也不难防,说简单也不简单。关键在于开发者有没有安全意识。很多App只关注业务功能,忽略了界面层的安全防护。
我见过太多App,登录界面没有任何防护措施。用户密码就这么裸奔在恶意App面前。你想想看,如果你的银行App被这样劫持,后果是什么?
嗯,该说的都说了。下一章咱们聊聊Provider数据泄露,那个坑更多。
公众号:蓝海资料掘金营,微信deep3321