Android無需許可權顯示懸浮窗, 兼談逆向分析app
本文先在簡書上釋出, 獲得許多反饋, 所以在CSDN也發一下, 與大家分享
前言
最近UC瀏覽器中文版出了一個快速搜尋的功能, 在使用其他app的時候, 如果複製了一些內容, 螢幕頂部會彈一個視窗, 提示一些操作, 點選後跳轉到UC, 顯示這個懸浮窗不需要申請android.permission.SYSTEM_ALERT_WINDOW
許可權.
如下圖, 截圖是在使用Chrome時截的, 但是螢幕頂部卻有UC的view浮在螢幕上. 我使用的是小米, 我並沒有給UC授懸浮窗許可權, 所以我看到這個懸浮窗時是很震驚的.
懸浮窗原理
做過懸浮窗功能的人都知道, 要想顯示懸浮窗, 要有一個服務執行在後臺, 通過getSystemService(Context.WINDOW_SERVICE)
WindowManager
, 然後向其中addView
, addView
第二個引數是一個WindowManager.LayoutParams
, WindowManager.LayoutParams
中有一個成員type
, 有各種值, 一般設定成TYPE_PHONE
就可以懸浮在很多view的上方了, 但是呼叫這個方法需要申請android.permission.SYSTEM_ALERT_WINDOW
許可權, 在很多機型上, 這個許可權的名字叫懸浮窗, 比如小米手機上預設是禁用這個許可權的, 有些惡意app會用這個許可權彈廣告, 而且很難追查是哪個應用彈的. 如果這個許可權被禁用, 那麼結果就是懸浮窗無法展示, 比如有道詞典現在UC能突破這個限制, 我很好奇它是怎麼做到的.
研究實現
Android開發有點蛋疼的地方就是太容易被反編譯, 但有時這也成為我們研究別人app的一種手段.
反編譯
使用apktool可以很輕鬆的反編譯UC.
找程式碼
逆向別人的app, 比較關鍵的地方是怎麼找程式碼, 因為程式碼基本上都是混淆的, 直接看肯定是看不懂的, 只能去找, 突破口一般在字元資源上, 比如我們看到上圖中的快速搜尋是UC的字元, 那麼我們到res/values/strings.xml
去找快速搜尋
<string name="dark_search_banner_search">快速搜尋</string>
這裡我們拿到了快速搜尋
對應的名字dark_search_banner_search
, Android在編譯時會給每個資源分配一個id, 我們grep一下這個字元資源的名字就能知道id是多少, 一般在R.java
, res/values/public.xml
中有定義, 我直接到public.xml中找到了它的id
<public type="string" name="dark_search_banner_search" id="0x7f070049" />
有了字元資源的id 0x7f070049
, 我們再在程式碼裡面grep一下這個id, 就能知道哪幾個檔案使用了這個字元資源.
之所以這麼確定是在程式碼裡, 是因為UC在我們複製的內容不同時, 懸浮窗標題會不一樣, 一定是在程式碼裡控制的, 結果如下
./com/uc/browser/b/f.smali
結果可能和大家不一樣, 但是一定會找到一個被混淆的smali檔案
看程式碼
這一部應該是最噁心的. smali程式碼和java程式碼的關係, 就像彙編程式碼和C++程式碼, 但是smali比彙編程式碼要容易理解的多, 不然也不會有那麼多公司故意將程式碼寫在C++層了.
雖然程式碼都被混淆了, 而且以我們不熟悉的方式出現, 但我們可以根據一些蛛絲馬跡來判斷程式碼的執行, 比如Framework的類和API是不能被混淆的, 這也是我們能看懂smali的原因之一, 我們可以結合這些麵包屑來還原整個app程式碼, 當然這需要我們對smali很熟悉, 如果不熟悉smali, 至少要對Android的API熟悉. 因為有時實在看不懂, 我們要靠猜來還原一段程式碼的邏輯.
首先在程式碼裡面找到0x7f070049
, 發現瞭如下程式碼
(省略)
const v3, 0x7f070049
invoke-virtual {v1, v3}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;
move-result-object v1
iput-object v1, v0, Lcom/uc/browser/b/a;->dpC:Ljava/lang/String;
:cond_9
(省略)
invoke-virtual {v0, v1}, Lcom/uc/browser/b/a;->o(Landroid/graphics/drawable/Drawable;)V
:try_end_2
.catch Ljava/lang/Exception; {:try_start_2 .. :try_end_2} :catch_0
goto/16 :goto_0
(省略)
這是0x7f070049
出現之後的一部分程式碼, 一路看下來, 其實都是在取值賦值, 就拿0x7f070049
來說:
#使v3暫存器的值為0x7f070049
const v3, 0x7f070049
#v1是Resources例項, 呼叫它的getString方法, 方法的引數是v3中的值
invoke-virtual {v1, v3}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;
#將結果存入v1暫存器
move-result-object v1
其實就是我們常用的getResources().getString
其實如果一直這麼看下去, 會發現毫無頭緒, 剩下的程式碼一直在幹差不多的事情, 所以我只截取了這部分, 注意最後一行
goto/16 :goto_0
也就是說, 有可能程式碼轉到goto_0
那兒去了, 那麼看看goto_0
那裡又寫了些什麼
:goto_0
(省略)
const-string v1, "window"
invoke-virtual {v0, v1}, Landroid/content/Context;->getSystemService(Ljava/lang/String;)Ljava/lang/Object;
move-result-object v0
check-cast v0, Landroid/view/WindowManager;
invoke-interface {v0}, Landroid/view/WindowManager;->getDefaultDisplay()Landroid/view/Display;
move-result-object v0
invoke-virtual {v0}, Landroid/view/Display;->getWidth()I
move-result v0
iget-object v1, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
iput v0, v1, Landroid/view/WindowManager$LayoutParams;->width:I
iget-object v0, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
invoke-virtual {v10}, Lcom/uc/browser/b/a;->getContext()Landroid/content/Context;
move-result-object v1
invoke-virtual {v1}, Landroid/content/Context;->getResources()Landroid/content/res/Resources;
move-result-object v1
const v2, 0x7f0d0022
invoke-virtual {v1, v2}, Landroid/content/res/Resources;->getDimension(I)F
move-result v1
float-to-int v1, v1
iput v1, v0, Landroid/view/WindowManager$LayoutParams;->height:I
iget-object v0, v10, Lcom/uc/browser/b/a;->mWindowManager:Landroid/view/WindowManager;
iget-object v1, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
invoke-interface {v0, v10, v1}, Landroid/view/WindowManager;->addView(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V
其實看到const-string v1, "window"
, 我們就應該有所警惕了, 這可能是關鍵程式碼了. 為什麼這麼說? 因為懸浮窗的實現裡面, 需要獲取WindowManager
, 從而需要呼叫Context.getSystemService(Context.WINDOW_SERVICE)
, 而官方文件寫了Context.WINDOW_SERVICE
就是常量window
. 而後我們看到程式碼中構造了WindowManager.LayoutParams
, 最終在addView
時傳入.
看到這裡, 我也覺得很奇怪, 我在懸浮窗原理中寫的是我知道的實現懸浮窗的方法, UC的實現好像跟我呼叫的是相同的API, 也沒看到反射之類可能展示奇技淫巧的程式碼, 為什麼UC就可以不需要許可權直接顯示懸浮窗呢?
猜測
我認為addView
的第二個引數WindowManager.LayoutParams
可能是關鍵, 所以我需要知道UC是如何構造這個WindowManager.LayoutParams
的.
由於是系統的類, 無法混淆, 直接搜尋LayoutParams
就找到了下面的程式碼
iget-object v1, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
這句話就是把v10
的值付給v1
, v10
是com/uc/browser/b/a
的成員dpx
, 那麼開啟com/uc/browser/b/a.smali
看看dpx
到底是怎麼構造的.
(省略)
.field dpx:Landroid/view/WindowManager$LayoutParams;
(省略)
.line 68
new-instance v0, Landroid/view/WindowManager$LayoutParams;
invoke-direct {v0}, Landroid/view/WindowManager$LayoutParams;-><init>()V
iput-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
.line 69
if-eqz p2, :cond_0
.line 70
iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
const/16 v1, 0x7d5
iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I
.line 74
:goto_0
iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;
const/4 v1, 0x1
iput v1, v0, Landroid/view/WindowManager$LayoutParams;->format:I
(省略)
這裡的程式碼就很簡單的, 我最先看的是下面這段
const/16 v1, 0x7d5
iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I
這兩句程式碼就是把WindowManager.LayoutParams.type
欄位設成0x7d5, 官網上寫了0x000007d5是WindowManager.LayoutParams.TYPE_TOAST
的值.
驗證
實際測試了一下, 將type設定成TYPE_TOAST果然有奇效, 不需要android.permission.SYSTEM_ALERT_WINDOW
許可權就能顯示一個懸浮窗.
之前我一直以為呼叫了系統WindowManager.addView
需要android.permission.SYSTEM_ALERT_WINDOW
許可權, 但實際上呼叫這個方法是不需要許可權的, 在Android原始碼中有這麼一段
public int checkAddPermission(WindowManager.LayoutParams attrs) {
int type = attrs.type;
if (type < WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW
|| type > WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
return WindowManagerImpl.ADD_OKAY;
}
String permission = null;
switch (type) {
case TYPE_TOAST:
// XXX right now the app process has complete control over
// this... should introduce a token to let the system
// monitor/control what they are doing.
break;
case TYPE_INPUT_METHOD:
case TYPE_WALLPAPER:
// The window manager will check these.
break;
case TYPE_PHONE:
case TYPE_PRIORITY_PHONE:
case TYPE_SYSTEM_ALERT:
case TYPE_SYSTEM_ERROR:
case TYPE_SYSTEM_OVERLAY:
permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW;
break;
default:
permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
}
if (permission != null) {
if (mContext.checkCallingOrSelfPermission(permission)
!= PackageManager.PERMISSION_GRANTED) {
return WindowManagerImpl.ADD_PERMISSION_DENIED;
}
}
return WindowManagerImpl.ADD_OKAY;
}
可以猜到這個方法是往系統的WindowManager
裡addView
的時候做許可權檢查用的, 那個type
就是我們在構造WindowManager.LayoutParams
時賦值的type
, 可以看到, 除了TYPE_TOAST
, 其他都是要許可權的, 而且非常喜感的是, 程式碼中的註釋還說他們現在對這種type毫無限制, 應該引入標記來限制開發者.
實測效果
看到有評論說這樣的是不支援點選的. 我之前寫的一個app有懸浮窗播放功能, 支援拖動視窗和點選暫停, 關閉視窗等等, 實測功能正常, 今天下班匆匆忙忙寫的這篇文章, 沒有錄製演示視訊.
但是在2.3上不能接收點選事件.
評論區的浮海大蝦同學有更多補充如下:
TYPE_TOAST一直都可以顯示, 但是用TYPE_TOAST顯示出來的在2.3上無法接收點選事件, 因此還是無法隨意使用.
下面是我之前研究後臺執行緒顯示對話方塊的時候記得筆記, 大家可以看看我們專案中有需求需要在後臺任務中顯示Dialog, 專案最初的做法是用Activity模擬Dialog, 一個Activity已經承載了近20種Dialog, 程式碼混亂至極. 後來我發現Dialog可以通過改變Window Type實現不依賴Activity顯示, 然後就很興奮的要在使用這種方式來作為新的實現方式.
最初WindowType是WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, 可是這是懸浮窗了, MIUI會預設禁止(真他媽操蛋,也沒有任何提示)最終放棄. 後來試著換成了WindowManager.LayoutParams.TYPE_TOAST, 起初效果很好,MIUI也不禁止了, 哪裡都能顯示, 這下開心了. 可是後來又發現在2.3上不能接收點選事件, 也就是說Dialog上的按鈕不能點選, 這他媽就很操蛋了, 又放棄了. 又試了試其他的Type都不能滿足需求, 結果如下:TYPE_SEARCH_BAR: 未知
TYPE_ACCESSIBILITY_OVERLAY: 拒絕使用
TYPE_APPLICATION: 只能配合Activity在當前APP使用TYPE_APPLICATION_ATTACHED_DIALOG: 只能配合Activity在當前APP使用
TYPE_APPLICATION_MEDIA: 無法使用(什麼也不顯示)
TYPE_APPLICATION_PANEL: 只能配合Activity在當前APP使用(PopupWindow預設就是這個Type)
TYPE_APPLICATION_STARTING: 無法使用(什麼也不顯示)
TYPE_APPLICATION_SUB_PANEL: 只能配合Activity在當前APP使用TYPE_BASE_APPLICATION: 無法使用(什麼也不顯示)
TYPE_CHANGED: 只能配合Activity在當前APP使用
TYPE_INPUT_METHOD: 無法使用(直接崩潰)
TYPE_INPUT_METHOD_DIALOG: 無法使用(直接崩潰)
TYPE_KEYGUARD_DIALOG: 拒絕使用
TYPE_PHONE: 屬於懸浮窗(並且給一個Activity的話按下HOME鍵會出現看不到桌面上的圖示異常情況)
TYPE_TOAST: 不屬於懸浮窗, 但有懸浮窗的功能, 缺點是在Android2.3上無法接收點選事件
TYPE_SYSTEM_ALERT: 屬於懸浮窗, 但是會被禁止
(TODO: 補充演示GIF)
更多問題
關於UC如何處理2.3的問題, 我並沒有仔細看, 因為我確實是沒有在2.3上測過使用TYPE_TOAST
的情況, 希望有機器的同學能幫忙測一下UC這個功能在2.3上的具體表現. 另外個人的解決方案是在2.3上使用級別更高的type, 我記得剛開始用小米的時候, 是沒有懸浮窗這個許可權的管理的, 加上2.3的手機現在很多都沒有維護了, 直接申請android.permission.SYSTEM_ALERT_WINDOW
也無妨.
但還是希望能有同學告知一下UC在2.3上是如何表現這個功能的.
尾聲
現在我們都知道了如何在不申請許可權的情況下顯示懸浮窗, 我相信以中國Android開發者的腦洞, 一定會有很多有趣或噁心的功能被開發出來, 一方面我自己覺得這個東西很有用, 可以實現一些很神奇的功能, 另一方面又擔心這個API被濫用, 最終不得不限制許可權.
還有就是, 逆向分析僅用於學習, 不要幹違法的事情.
本人技術有限, 如果文中有錯誤的歡迎指正, 以免誤導他人
利益宣告: 雖然我目前在UC實習, 但我並沒有UC瀏覽器中文版的程式碼許可權, 也不會將公司的程式碼分享給外人. 本文完全是靠我自己開發經驗+逆向分析經驗+Google完成, 在此之前沒有看過UC瀏覽器的任何程式碼.