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

codeGoogle發表於2018-01-29

最近在做一個有趣的外掛的小玩意,前提我們要了解一個重要的類AccessibilityService

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

前言

提起AccessibilityService,你最容易聯想到的肯定是微信搶紅包外掛!但這個服務的設計初衷,是為了幫助殘障人士可以更好的使用App。

一些“調皮”的開發者利用AccessibilityService可以監控與操作其他App的特性加上系統遠超人類的反應速度,在某些競爭類場景開發出了作弊外掛,最常見的就是你所嫉憤的微信搶紅包外掛了。

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

OK,所以我們今天先來分析一下AccessibilityService執行原理,然後分享一些我在應對此類競爭場景下基於AccessibilityService等自動化作弊工具的防禦措施。

外掛簡史

先說下背景:

場景是和搶紅包類似的另一種:搶單。使用者下單後訂單會經過系統,在配送端App釋出,配送人員在配送端App通過距離、價錢、時間等維度進行篩選並搶單然後配送。顯而易見,價高距離短的訂單非常搶手,這樣就形成一種競爭環境,於是,自動搶單外掛也就有了存在的理由。

然後我們來看下外掛進化史:

image

  • 第一代外掛

    第一代外掛還比較粗糙,需要依賴按鍵精靈來實現,且需要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歲轉行做初級程式設計師是一種怎樣的體驗?

相信自己,沒有做不到的,只有想不到的

微信公眾號:終端研發部

技術

相關文章