紅包外掛史及AccessibilityService分析與防護

最近在作一個有趣的外掛的小玩意,前提咱們要了解一個重要的類AccessibilityServicejava

轉載請註明出處:lizhaoxuan.github.ionode

前言

提起AccessibilityService,你最容易聯想到的確定是微信搶紅包插件!但這個服務的設計初衷,是爲了幫助殘障人士能夠更好的使用App。android

一些「調皮」的開發者利用AccessibilityService能夠監控與操做其餘App的特性加上系統遠超人類的反應速度,在某些競爭類場景開發出了做弊外掛,最多見的就是你所嫉憤的微信搶紅包插件了。git

微信搶紅包插件對本來平等的競爭環境產生了不公,不過這是微信團隊要操心解決的事。可萬萬沒想到,有一天,我正在寫的App也遭此毒手!!!這都欺負到頭上了能忍嗎?不能啊!程序員

OK,因此咱們今天先來分析一下AccessibilityService運行原理,而後分享一些我在應對此類競爭場景下基於AccessibilityService等自動化做弊工具的防護措施。github

外掛簡史

先說下背景:bash

場景是和搶紅包相似的另外一種:搶單。用戶下單後訂單會通過系統,在配送端App發佈,配送人員在配送端App經過距離、價錢、時間等維度進行篩選並搶單而後配送。顯而易見,價高距離短的訂單很是搶手,這樣就造成一種競爭環境,因而,自動搶單外掛也就有了存在的理由。微信

而後咱們來看下外掛進化史:app

image

  • 第一代外掛async

    第一代外掛還比較粗糙,須要依賴按鍵精靈來實現,且須要Root權限。

    【防護】簡單反編譯拆包瞭解後,考慮暫時沒有更好的辦法禁止按鍵精靈對App的模擬點擊,直接封禁Root可能會有大量誤殺,第一代防護僅簡單的檢查是否安裝了按鍵精靈,而後限制用戶搶單。

  • 第二代外掛

    可能由於第一代的防護過於粗糙,第二代外掛很快有了新的改進,再也不須要單獨安裝按鍵精靈這個App,他們把按鍵精靈集成到了本身的app裏……

    【防護】此時團隊內部簡單商量後決定,快刀斬亂麻,直接封禁Root權限,檢測到Root後將限制搶單。

  • 第三代外掛

    禁止Root後終於消停了一段時間,但顯然人民羣衆的智慧是無限的,很快新的免Root外掛出世了……通過反編譯外掛後,第三代外掛採用了AccessibilityService來實現。

    【防護】此時已知的外掛並很少,因此除了繼續封禁Root之外,還創建了可遠程配置的外掛package name黑名單列表,若檢測到已安裝app列表裏存在特定外掛包名後,將會進行搶單限制。 package name須要先獲取到安裝包來查看包名,隨着外掛數量逐步上漲,外掛安裝包獲取難度大的缺點開始暴露了。

  • 【第三代防護】

    此時針對上一個版本的防護措施作了一次優化: 1.優先檢查外掛package name 2.次級檢測外掛app name,加package name白名單防誤判。這樣就不須要再獲取app的安裝包了 3.增長騎手舉報反饋入口 4.收集了已啓動的輔助模式列表備用(本想再快到斬亂麻的禁止輔助模式的開啓,但這個誤殺範圍實在是太大了,最終仍是停留在了想想的階段)

  • 第四代外掛

    在經過app name封禁後,外掛們掙扎了幾回都被即時遏制了。但很快,咱們收到了最新的外掛信息:新出來的外掛沒有圖標,看不到名字…… (大家厲害大家厲害!!!)

哎呀~真是活久見,兩波歷來沒見過的人在互相進步啊這是!!!禁止外掛安裝這種簡單的防護措施已經擋不住這幫瘋狂的人類了,我只能一頭扎進了AccessibilityService的源碼中,看這究竟是個啥東西,而後去思考相應的防護方案。

AccessibilityService運行原理

AccessibilityService內部運行

這不是一篇AccessibilityService教程文章,沒有AccessibilityService完整的使用示例代碼和源碼,但爲了上下文不至於斷檔太大,咱們這裏仍是會簡單貼一些小段代碼。同時須要說明的是,嚴謹的來講AccessibilityService只是一個Service,文本查找點擊事件等操做對於一個Service來講是徹底無法作到的。但爲了行文方便,因此後面某些AccessibilityService代指輔助模式服務。

@Override

    public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {

        //獲取eventType

        int eventType = accessibilityEvent.getEventType();

        if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED) {

            AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();

            if (nodeInfo != null) {

                //查詢文案爲BUTTON3的View

                List<AccessibilityNodeInfo> button3 = nodeInfo.findAccessibilityNodeInfosByText("BUTTON3");

                nodeInfo.recycle();

                for (AccessibilityNodeInfo item : button3) {

                    //對這個View執行點擊操做

                    item.performAction(AccessibilityNodeInfo.ACTION_CLICK);

                }

            }

        }

    }

    ...

}

複製代碼

AccessibilityService真的很簡單,只要寫一個Service繼承AccessibilityService,而後還有其餘一些配置,以後每當你監控的應用界面有變更時就會回調到這個onAccessibilityEvent這個方法,你能夠在裏面取得此時變更的event類型是什麼,還能拿到當前這個應用可視化的View樹,而後取得其中的某個View來執行某些操做。

那至於其原理,用屁股想一想也知道是確定是被監控的App發生界面改變時通知了系統,而後系統又通知給了咱們註冊的Service。嗯……屁股想的沒錯……那App怎麼通知系統的?系統又怎麼通知咱們的呢?

哎呀,屁股想不出來了,不要緊,屁股決定腦殼,腦殼知道怎麼辦。這個時候咱們就該鑽到源碼裏來一探究竟了。Emmm~就先從咱們繼承的這個AccessibilityService爲入口進行研究吧!

哎呀~RTFSC,這亂糟糟的一片源代碼催眠的一把好手,咱們仍是不看了,我給你畫個圖吧……

我理出一份AccessibilityService類圖:

image

乍一看好像亂糟糟的,沒事,我慢慢給你絮叨,確定比直接看源碼來的直觀有意思。

1.AccessibilityService有兩個抽象方法,onAccessibilityEvent()onInterrupt(),就是咱們要本身實現的那兩個,重點記onAccessibilityEvent(),它會出現不少次,咱們姑且先命名它爲AS-onAccessibilityEvent().onAccessibilityEvent()的參數類型是AccessibilityEvent,這個類簡而意之就是當系統中發生某些事件時,會發送這個類的對象來告知監控方,經過這個對象能夠知道是什麼類型的事件、什麼控件發出來等等。

2.另外AccessibilityService繼承了Service,但它僅複寫了onBind方法。在onBind方法中return了一個IAccessibilityServiceClientWrapper對象。

@Override

public final IBinder onBind(Intent intent) {

       return new IAccessibilityServiceClientWrapper(this, getMainLooper(), new Callbacks() {

       ...

       @Override

       public void onAccessibilityEvent(AccessibilityEvent event) {

           AccessibilityService.this.onAccessibilityEvent(event);

       }
   	...
}
複製代碼

3.IAccessibilityServiceClientWrapper繼承了IAccessibilityServiceClient.Stub,嗯~看到這你應該就明白一大塊了,AccessibilityService是一個跨進程通訊Service。IAccessibilityServiceClientWrapper是這個類的重點關注對象了,那他做爲一個AIDL的一個server端,他有哪些對外提供的方法呢?

interface IAccessibilityServiceClient {

    void init(in IAccessibilityServiceConnection connection, int connectionId, IBinder windowToken);

    void onAccessibilityEvent(in AccessibilityEvent event);

    void onInterrupt();

    void onGesture(int gesture);

    void clearAccessibilityCache();

    void onKeyEvent(in KeyEvent event, int sequence);

    void onMagnificationChanged(in Region region, float scale, float centerX, float centerY);

    void onSoftKeyboardShowModeChanged(int showMode);

    void onPerformGestureResult(int sequence, boolean completedSuccessfully);

}

複製代碼

這裏你又看到了onAccessibilityEvent(),咱們姑且叫他IASC-onAccessibilityEvent().

4.而後咱們在回頭看看IAccessibilityServiceClientWrapper的構造方法中的三個參數,Context、 Looper 、CallbacksContext不說了,Looper是一個MainLooper, 他們兩個的做用是建立一個HandlerCaller對象,HandlerCaller你能夠很粗狂的就把它當作Handler,想了解細節能夠本身看一下源碼:

public IAccessibilityServiceClientWrapper(Context context, Looper looper,Callbacks callback) {

            mCallback = callback;

            mCaller = new HandlerCaller(context, looper, this, true /*asyncHandler*/);

        }

複製代碼

5.而後咱們來看看Callbacks是個啥:

public interface Callbacks {

        void onAccessibilityEvent(AccessibilityEvent event);

        void onInterrupt();

        void onServiceConnected();

        void init(int connectionId, IBinder windowToken);

        boolean onGesture(int gestureId);

        boolean onKeyEvent(KeyEvent event);

        void onMagnificationChanged(@NonNull Region region,

                float scale, float centerX, float centerY);

        void onSoftKeyboardShowModeChanged(int showMode);

        void onPerformGestureResult(int sequence, boolean completedSuccessfully);

        void onFingerprintCapturingGesturesChanged(boolean active);

        void onFingerprintGesture(int gesture);

        void onAccessibilityButtonClicked();

        void onAccessibilityButtonAvailabilityChanged(boolean available);

    }
複製代碼

這和剛纔那個IAccessibilityServiceClient不是同樣嘛?沒錯,是這樣的,並且這裏面也有一個onAccessibilityEvent,咱們叫它Callbacks-onAccessibilityEvent

上面你應該看到Callbacks是一個匿名內部類,他實現的Callbacks-onAccessibilityEvent方法的就是一句:AccessibilityService.this.onAccessibilityEvent(event);直接調用了AS-onAccessibilityEvent(),先記下來哈。

6.哦對,IAccessibilityServiceClientWrapper還實現了一個HandlerCaller.Callback接口:

public void onAccessibilityEvent(AccessibilityEvent event, boolean serviceWantsEvent) {

    Message message = mCaller.obtainMessageBO(

            DO_ON_ACCESSIBILITY_EVENT, serviceWantsEvent, event);

    mCaller.sendMessage(message);

}

...

@Override

public void executeMessage(Message message) {

    switch (message.what) {

        case DO_ON_ACCESSIBILITY_EVENT: {

            ...

            	mCallback.onAccessibilityEvent(event);

            ...

            }

        } return;

        ...

	}

}

...
複製代碼

我只保留了最關鍵的代碼,咱們以onAccessibilityEvent爲線索方法捋一遍哈。當AIDL的Client端調用了IASC-onAccessibilityEvent時,會經過Handler發送一個message給本身,接收到之後會調用Callbacks-onAccessibilityEventCallbacks-onAccessibilityEvent咱們剛纔看過啦,會調用AS-onAccessibilityEvent(),這是個抽象方法,也就是咱們本身實現的MyAccessibilityService中的自定義代碼。

有點懵??不明白到底在幹啥?不要緊,我還畫了個搓搓的流程圖:

image

當View發生改變時,會發出一個AccessibilityEvent出來,這個Event會經過Binder驅動發送給IAccessibilityServiceClientWrapper,調用他的onAccessibilityEvent(AccessibilityEvent)方法,這個方法經過Handler發送了一個Message給本身,目的是爲了從Binder線程轉回主線程。而後調用了mCallback.onAccessibilityEvent(event),間接的調用了AccessibilityService.this.onAccessibilityEvent(event);,也就是咱們本身實現的。

這麼順下來,AccessibilityService的內部邏輯是不就感受很簡單了?

AccessibilityService外部運行

咱們梳理了一遍AccessibilityService的內部執行邏輯後,就會觸發不少新的問題,好比onBind是誰來調用的啊?爲何中間還要用Hander給本身發送一遍消息呢?當咱們本身實現onAccessibilityEvent方法時會作一些點擊一類的操做,這個是怎麼作到的啊?

哎呀,問題好多,這個源碼梳理下來確定要睡第二覺了,咱們不看了不看了,直接上圖吧:

image

1.一個可愛的用戶在設置頁面啓動了某個輔助模式服務 2.系統發送了一條廣播到AccessibilityManagerService,收到廣播後,AccessibilityManagerService綁定了咱們寫的AccessibilityService,就這樣調用了onBind方法。AIDL的Server端準備好了~ AccessibilityManagerService是一個系統服務,由SystemService啓動。

3.受到監控的App某個View發生了改變,其內部都會調用AccessibilityManager來發送event,其具體發送的對象是ViewRootImpl類來作的。 4.發出event後會經過Binder驅動調用到AccessibilityService,最終調用了咱們複寫的onAccessibilityEvent方法。 5.每個View在AccessibilityService中都會被映射爲一個AccessibilityNodeInfo對象,咱們經過這個對象去查找具體View、觸發事件,其本質是調用了AccessibilityInteractionClient類的對應方法。 6.AccessibilityInteractionClient咱們在Uiautomator也常常看到。後面咱們會繼續單獨分析,先大概說一下是個什麼東西,官方註釋是這樣的: This class is a singleton that performs accessibility interaction which is it queries remote view hierarchies about snapshots of their views as well requests from these hierarchies to perform certain actions on their views. 這個類是一個能夠執行可訪問性交互的單例對象,它查詢遠程視圖層次結構,查看視圖的快照,以及來自這些層次結構的請求,以便在視圖上執行某些操做。 7.若是利用AccessibilityInteractionClient操做正在被監控的App,好比點擊按鈕,那麼View發生變化,又發送出一個Event,這樣便造成一個循環。

AccessibilityInteractionClient 操做View細節

在咱們瞭解了AccessibilitySevice從View產生event事件發出到被輔助服務接收再操做View的一個流程以後,咱們僅僅知道了事件是如何通知到AccessibilityService的,而具體是如何經過文本查找View,點擊View則是AccessibilityInteractionClient來作的,那麼下面咱們就經過AccessibilityInteractionClient 的源碼探究一下里面的祕密。

咱們主要以findAccessibilityNodeInfosByText和performAccessibilityAction(ACTION_CLICK)兩個方法往下追。

總體代碼較爲簡單,基本是一條線往下調用的邏輯,因此我又畫了一張圖:

image

1.AccessibilityInteractionClient沒作什麼操做,直接經過Binder調用了AccessibilityManagerService對應的方法。

2.AccessibilityManagerService最終仍是經過Binder調用了ViewRootImpl對應的方法。

3.ViewRootImpl僅做爲Binder中的服務端接收調用,真正的操做交給AccessibilityInteractionController來作。

4.AccessibilityInteractionController對應的方法被調用以後,並無直接進行操做,而是經過Handler作了一次轉發,以便從Binder線程轉到UI線程。

5.以performAccessibilityAction(ACTION_CLICK)點擊事件爲例,最終調用的實際是View的mOnClickListener。

6.以findAccessibilityNodeInfosByText爲例,最終調用的實際是View的findViewsWithText方法,其方法內部實際對比的值是mContentDescription。須要特別說明的是TextView重寫了該方法,其內部實際對比的值是mText。

小結

咱們既然已經瞭解了AccessibilityService的運行原理,其內部就是一個跨進程通訊,沒什麼神祕的。最終操做View的是AccessibilityInteractionClient,AccessibilityInteractionClient是怎麼操做的經過源碼很容易的追到了View層具體的實現,那麼作防護的話簡直是手到擒來!

AccessibilityService防護

1.檢測 or 禁止相關外掛的輔助模式開啓

以前在外掛防護上,一直困擾個人一個問題是:AccessibilityService相似一個解耦很開的觀察者模式,做爲被觀察者沒法察覺到觀察者究竟有哪些,這致使咱們很是的被動。

不過研究過AccessibilityService源碼以後,咱們知道,每一個AccessibilityService在都是由AccessibilityManagerService註冊的,那豈不是說咱們能夠經過AccessibilityManagerService取得全部以安裝或以啓動的輔助模式應用?那麼AccessibilityManagerService有提供相關方法嗎? 有的:

AccessibilityManagerService.java

public class AccessibilityManagerService extends IAccessibilityManager.Stub {

	...

 	@Override

    public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(int userId) {...}

}
複製代碼

值得注意的是,這個方法幫咱們篩去了UiAutomationService。返回值AccessibilityServiceInfo是AccessibilityService的一些配置信息,其中包含咱們最關心的packageNames(AccessibilityService 監控哪些package發出的Event)

這裏有一個小問題,AccessibilityManagerService是com.android.server.accessibility包下的類,咱們沒有辦法直接使用。不過不要緊,你能夠經過AccessibilityManager來間接的操做AccessibilityManagerService,其內部利用Binder間接的調用了AccessibilityManagerService,獲得List以後,你能夠經過遍歷瞭解到本身的應用正在被那些輔助模式監控或「輔助」。

具體方法以下:

/**

 * 取得正在監控目標包名的AccessibilityService

 */

private List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(String targetPackage) {

    List<AccessibilityServiceInfo> result = new ArrayList<>();

    AccessibilityManager accessibilityManager = (AccessibilityManager) getApplicationContext().getSystemService(Context.ACCESSIBILITY_SERVICE);

    if (accessibilityManager == null) {

        return result;

    }

    List<AccessibilityServiceInfo> infoList = accessibilityManager.getInstalledAccessibilityServiceList();

    if (infoList == null || infoList.size() == 0) {

        return result;

    }

    for (AccessibilityServiceInfo info : infoList) {

        if (info.packageNames == null) {

            result.add(info);

        } else {

            for (String packageName : info.packageNames) {

                if (targetPackage.equals(packageName)) {

                    result.add(info);

                }

            }

        }

    }

    return result;

}

<
複製代碼
  • **須要特別說明的是:**當info.packageNames爲null時,表示監控全部包名。外掛有可能矇混其中,但若是一刀切,也有可能誤殺正常軟件。
  • getInstalledAccessibilityServiceList獲取全部已安裝的AccessibilityService,AccessibilityManager還有一個方法getEnabledAccessibilityServiceList,取得全部已經開啓的AccessibilityService,用法同上。但要注意的是。檢測外掛確定是在某個節點進行,好比咱們的App初次啓動,那麼用戶能夠在啓動App後再啓動外掛,這將是一個漏洞。

2.Event干擾

咱們一直知道AccessibilityServices在監控目標app發出的AccessibilityEvent,從而對應的做出某些操做。

例如某些微信紅包插件會監控Notification的彈出,那麼咱們是否能夠隨意發送這樣的Event出來,從而混干擾外掛插件的運行邏輯?

沒錯,能夠這樣作的,具體方式以下:

textView.sendAccessibilityEvent(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
複製代碼

但這個方案的缺陷是,大部分的外掛插件對特定類型的事件並非特別感興趣,他們僅在收到Event後檢查頁面上是否有某些特定的元素,從而決定是否進行下一步操做。

大部分狀況下是一個比較雞肋的措施,但也許會在某些場景起到意想不到的做用!

3.屏蔽AccessibilityServices文案檢查

在沒有探究AccessibilityServices源碼以前,不瞭解AccessibilityServices檢索文本信息原理的咱們可能惟一能想到的應對措施就是將關鍵問題替換爲圖片。

這能夠解決問題,可是問題替換爲圖片不但會有性能上的損耗,並且會丟失大部分本來TextView的兼容特性。

不過在瞭解AccessibilityServices源碼以後,咱們知道其內部核心原理就是調用TextView的findViewsWithText方法,再也不須要費勁心思將文本轉爲圖片,你須要作的僅僅是複寫這個方法就夠了:

public class DefensiveTextView extends android.support.v7.widget.AppCompatTextView {

    ...

    @Override

    public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) {

        outViews.remove(this);

    }

}

複製代碼

這樣AccessibilityServices文案檢查將會在這個View上失效。

4.屏蔽AccessibilityServices點擊事件

像上面同樣,經過源碼瞭解原理以後,咱們知道AccessibilityServices執行點擊事件最終在調用View的mOnClickListener。

那咱們只須要在這上面作文章就行了,最快捷的辦法是利用onTouch代替onClick。

5.檢測 or 禁止相關外掛軟件安裝

上述方式不管是檢測已安裝的AccessibilityServices列表仍是屏蔽AccessibilityServices的文本檢查和點擊事件,針對的都是AccessibilityServices自己。當你出臺這樣的方式後,確實後會讓現有的外掛消停一段時間,但能夠預見,很快會有基於其餘自動化措施的外掛面世,好比相似按鍵精力同樣的模擬Touch事件,圖像識別等,在出現應對這些手段以前,你仍是須要一些笨笨的老辦法,收集已知外掛,禁止其安裝。

檢查方法很是簡單,一句帶過:設立黑名單,遍歷系統內部全部已安裝的app,鑑別package name 和app name。

結語

在撥開了AccessibilityServices源碼的外衣以後,咱們會發現其實它的原理真的很簡單,惟一的核心是在Client - System - Server三者之間利用Binder作跨進程通訊,幾乎沒有太多的邏輯操做,一直在互相調用。

因此看着神奇且神祕的AccessibilityServices其實並無什麼了不得。

另外要說的是,在沒了解AccessibilityServices源碼以前,咱們能想到的防護措施可能很是少且低效,好比本來只用複寫一個方法,你卻須要動態生成圖片。瞭解源碼以後,你即可以單刀直入,直切重點用最有效最簡單的方式實現你想要的東西,因此閱讀源碼真的很重要!

最後先總結一下防護措施吧。

  • 經過AccessibilityManager檢測 or 禁止相關外掛的輔助模式開啓
  • 自定義TextView複寫findViewsWithText方法,屏蔽文案檢查
  • onTouch替換onClick,屏蔽點擊事件
  • 隨機發送Event干擾
  • 經過PackageManager檢測 or 禁止相關外掛軟件安裝

更多閱讀度

最新的15個Android庫,你瞭解嗎?

教你用Python動刷新搶12306火車票|附源碼!

內推啦!程序員還沒工做? 年底寒冬,讓這個冬天不太冷!

揭密微信跳一跳小遊戲那些外掛

調皮的內存抖動?前生今世及App解決卡頓慢30歲轉行作初級程序員是一種怎樣的體驗?

相信本身,沒有作不到的,只有想不到的

微信公衆號:終端研發部

技術
相關文章
相關標籤/搜索