Android無需權限顯示懸浮窗, 兼談逆向分析app


前言

最近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, v10com/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; }

可以猜到這個方法是往系統的WindowManageraddView的時候做權限檢查用的, 那個type就是我們在構造WindowManager.LayoutParams時賦值的type, 可以看到, 除了TYPE_TOAST, 其他都是要權限的, 而且非常喜感的是, 代碼中的注釋還說他們現在對這種type毫無限制, 應該引入標記來限制開發者.

處理兼容性

在這篇文章剛剛公布的時候, 就有同學反饋懸浮窗無法接收事件, 剛開始我並沒有特別在意, 在廖祜秋大神做了一個demo之后, 這篇文章閱讀量又漲了不少, 隨即收到更多反饋事件的問題, 我今天晚上借了台MIUI V5 4.2.2實測了一下, 這台機器上UC的快速搜索功能也無法正常使用.

在這個ROM上表現為:
使用TYPE_PHONE這類需要權限的type時, 只有在app處於前台時能顯示懸浮窗, 且能正常接受觸摸事件. 如果在應用詳情里面授懸浮窗權限, 則工作完全正常.
(這里是MIUI V5對懸浮窗的特殊處理, 現在的ROM, 包括MIUI V6上, 如果不授權, 無法顯示任何懸浮窗)
使用TYPE_TOAST這個不需要權限的type時, 懸浮窗正常顯示, 但不能接受觸摸事件.

我重新檢查了一下smali代碼, 發現UC是有分版本處理的, 不過因為smali代碼的規則問題, 很難直接看出來, 我把分析過程寫出來, 順便解釋一下smali的語法, 供大家以后逆向時拿來參考.

這次我是在OS X上反編譯的, 所以變量名可能略有區別.

接着上面com/uc/browser/b/a.smali中查看dpx的構造過程, 代碼如下:

.field dpx:Landroid/view/WindowManager$LayoutParams; (省略) # direct methods .method public constructor <init>(Landroid/content/Context;Z)V  .locals 7 (省略)  .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

為了方便說明, 我遵循smali的規則, 它用.line XX, 我們就說這是第XX行的代碼.

上面是我之前分析得到UC使用的是TYPE_TOAST的地方, 證據就是第70行的const/16 v1, 0x7d5, 但是要知道, smali代碼沒有跳轉的話, 就是從上往下執行, 我們看第69行的代碼如下:

.line 69 if-eqz p2, :cond_0

這句話的意思是如果p2等於0, 控制流跳轉到cond_0, 否則就是繼續順序往下執行. 也就是說UC只有在p2 != 0條件滿足的時候才會使用TYPE_TOAST, 我們看看cond_0對應的代碼.

 .line 72  :cond_0  iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;  const/16 v1, 0x7d2  iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I

這里很簡單, 就是將0x7d2賦給了type, 官網寫了0x000007d2TYPE_PHONE, 也就是說UC在某種情況下還是會用需要權限的老方法展示懸浮窗.

現在問題是條件是什么, 關鍵在p2, 在smali里面, 有兩種寄存器命名規則, 一種叫v命名規則, 另一種是p命名規則, 當然只是命名規則而已, 在使用apktool時是可以選的. 這里是p命名規則.

我剛才分析的賦值過程, 所在的方法是下面這個, 我在剛才的代碼片段中也保留了這個部分.

# direct methods .method public constructor <init>(Landroid/content/Context;Z)V  .locals 7

這就是com/uc/browser/b/a的構造方法, dpx就是在構造方法里初始化的, .locals 7告訴我們這個方法中將出現7個局部寄存器(local register), 名字是v0, v1...v6, 而這個方法的參數有3個, 隱式告訴我們這個方法中將出現3個參數寄存器(parameter register), 名字分別是p0, p1, p2.

我是怎么知道這個方法有3個參數的呢. smali中非靜態方法, 都隱含一個參數p0, 指向自身, 和Java中的this是一個意思, 而方法的參數寫在括號里, 也就是Landroid/content/Context;Z, 其中Landroid/content/Context;很明顯就是Android中的Context, 值存儲在p1里, 而Z對應的是Android中的boolean, p2就是他了.

也就是說, type是用TYPE_TOAST還是用TYPE_PHONE, 取決於這個構造方法的第二個參數, 那到底誰構造了com/uc/browser/b/a呢? 可以去代碼里面搜形如new-instance ***, Lcom/uc/browser/b/a;的代碼. 更保險的做法是搜Lcom/uc/browser/b/a然后一個一個的看.

我在com/uc/browser/b/f.smali里面找到了下面的代碼:

 .prologue  const/4 v0, 0x0  const/4 v1, 0x1 (省略)  new-instance v3, Lcom/uc/browser/b/a;  iget-object v4, v9, Lcom/uc/browser/b/e;->mContext:Landroid/content/Context;  sget v5, Landroid/os/Build$VERSION;->SDK_INT:I  const/16 v6, 0x13  if-lt v5, v6, :cond_0  move v0, v1  :cond_0  invoke-direct {v3, v4, v0}, Lcom/uc/browser/b/a;-><init>(Landroid/content/Context;Z)V

這段代碼首先是創建了com/uc/browser/b/a的實例, 存儲在v3中, 從另一處拿到了一個Context存儲在v4中, 然后拿到了當前系統的android.os.Build.VERSION.SDK_INT存儲在v5中, 此時將v6的值設為0x13, 千萬別粗心看成13了, 我好幾次都覺得這是13, 其實是十進制的19, 接下來是一個條件分支, 如果v5的值小於v6, 也就是說android.os.Build.VERSION.SDK_INT < 19, 直接跳轉到cond_0, 否則先將v1的值賦給v0, 再順序執行.

這句代碼

invoke-direct {v3, v4, v0}, Lcom/uc/browser/b/a;-><init>(Landroid/content/Context;Z)V

就是調用v3的構造方法, 參數是v4和v0, 分析一下上面這段代碼的邏輯就是:
如果當前系統API level小於19, 那么第二個參數就是0, 否則就是1.

而這第二個參數的值就是之前我們分析的p2的值, UC只有在p2 != 0條件滿足的時候才會使用TYPE_TOAST, 把整個邏輯串起來就是:

UC在API level >= 19的時候, 使用TYPE_TOAST, 其他情況使用TYPE_PHONE(需要權限).

可能是為了規避在低版本TYPE_TOAST不能接受事件的問題.

關於針對源代碼的分析, 請看Android懸浮窗使用TYPE_TOAST的小結

實測效果

我之前寫的一個app有懸浮窗播放功能, 支持拖動窗口和點擊暫停, 關閉窗口等等, 在4.4.4上實測功能正常.


無權限懸浮窗演示gif

感謝微博上關注的大神廖祜秋, 他做了個demo, 雖然交互和UC不同, 可以參考一下實現.


廖祜秋大神的demo

關於這個, 他也寫了一篇Android 懸浮窗的小結

其他補充

評論區的浮海大蝦同學有更多補充如下:

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: 屬於懸浮窗, 但是會被禁止

尾聲

現在我們都知道了如何在不申請權限的情況下顯示懸浮窗, 我相信以中國Android開發者的腦洞, 一定會有很多有趣或惡心的功能被開發出來, 一方面我自己覺得這個東西很有用, 可以實現一些很神奇的功能, 另一方面又擔心這個API被濫用, 最終不得不限制權限.

還有就是, 逆向分析僅用於學習, 不要干違法的事情.

本人技術有限, 如果文中有錯誤的歡迎指正, 以免誤導他人

利益聲明: 雖然我目前在UC實習, 但我並沒有UC瀏覽器中文版的代碼權限, 也不會將公司的代碼分享給外人. 本文完全是靠我自己開發經驗+逆向分析經驗+Google完成, 在此之前沒有看過UC瀏覽器的任何代碼.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM