前言

以Android为例,外挂一般是基于Android系统为辅助系统(洋文AccessibilityService)开发。辅助系统是Android为视力障碍人士提供的API接口,方便一些公司为视力障碍人士开发专门针对手机系统使用的一些服务,比如可以自动阅读微信文字,自动在美团购外卖,或者是在滴滴上打车。然而这个接口却被广泛用于外挂中,比如自动抢微信红包。抢单类App应用也比较广泛。
下面以武侠小说的方式来说明,「外挂方」攻,「App方」防。

第一回合

如果App方不作任何防范,外挂方是可以抓取到界面元素节点做一些自动化操作。外挂方只需要按照Android开发者网站的说明,获取ViewmAccessibilityDelegate即可完成后续所有控制,比如读取TextView的文字,模拟用户触摸按下操作等等。

界面元素节点使用uiautomatorviewer如下图所示,先看一眼有一个直观的认识 有帮助的截图

左侧是手机界面,右侧是元素节点,已经可以获取到界面中的文字,每个元素的位置。

外挂方的弱点就是需要获取mAccessibilityDelegate,而这个对象是系统自动为每个View生成的对象,我们只需要让外挂方获取不到mAccessibilityDelegate就可以了。非常好,系统有这么一个接口View#setAccessibilityDelegate,我们只需在BaseActivity中要调用如下方法,就可以生成一个无效的对象,这个对象不能获取任何界面元素节点,也就无法完成模拟用户触摸点击等操作。

public static void banAccessNodeInfo(Activity activity) {
    activity.getWindow().getDecorView()
        .setAccessibilityDelegate(new View.AccessibilityDelegate() {
        public void onInitializeAccessibilityNodeInfo(View host, 
            AccessibilityNodeInfo info) {
        }
    });
}

这时我们再使用uiautomatorviewer查看: UIAutomator Viewer 可以看到左侧将无法定位任何元素,获取到的是一个0大小的View。

第二回合

经过上个回合,我们得知了App方是调用setAccessibilityDelegate来让对象无效的。那么我们改系统不允许他调用。

diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 166d6b7..d2692d2 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -8484,6 +8484,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
      * @see AccessibilityDelegate
      */
     public void setAccessibilityDelegate(@Nullable AccessibilityDelegate delegate) {
+        String msg = Log.getStackTraceString(new Throwable());
+        boolean needCatch = msg.contains("banAccessNodeInfo")
+        if (needCatch)
+            return;
         mAccessibilityDelegate = delegate;
     }

经过如此改造,外挂方又可以获取真实的界面元素节点。并进行外挂了。

APP方很生气,居然可以这样。从android.os.ServiceManager#sCache入手,通过反射将修改其值,可以达到屏蔽屏幕元素节点

public static void disableAccessibility() {
    if (!accessibilityDisabled) {
        accessibilityDisabled = true;
        try {
            Class cls = Class.forName("android.os.ServiceManager");
            if (cls != null) {
                Field declaredField = cls.getDeclaredField("sCache");
                if (declaredField != null) {
                    boolean isAccessible = declaredField.isAccessible();
                    declaredField.setAccessible(true);
                    Object obj = declaredField.get(null);
                    if (obj != null && (obj instanceof Map)) {
                        ((Map) obj).put("accessibility", new IBinder() {
                            public void dump(FileDescriptor fileDescriptor, String[] strArr) throws RemoteException {
                            }

                            public void dumpAsync(FileDescriptor fileDescriptor, String[] strArr) throws RemoteException {
                            }

                            public String getInterfaceDescriptor() throws RemoteException {
                                return null;
                            }

                            public boolean isBinderAlive() {
                                return true;
                            }

                            public void linkToDeath(DeathRecipient deathRecipient, int i) throws RemoteException {
                            }

                            public boolean pingBinder() {
                                return true;
                            }

                            public IInterface queryLocalInterface(String str) {
                                return null;
                            }

                            public boolean transact(int i, Parcel parcel, Parcel parcel2, int i2) throws RemoteException {
                                return true;
                            }

                            public boolean unlinkToDeath(DeathRecipient deathRecipient, int i) {
                                return true;
                            }
                        });
                        declaredField.setAccessible(isAccessible);
                    }
                }
            }
        } catch (Exception e) {
        }
    }
}

第三回合

在第二回合,App方确实是防住了,又获取不了屏幕元素节点了。而外挂方又开始了新一轮的攻击。只要屏蔽其对android.os.ServiceManager#sCache通过反射进行的修改就可以了。制定出了A/B两个计划。A计划在android.os.ServiceManager中进行反制处理;B计划在Field#setAccessible进行反制。 先实现B计划:

project libcore/
diff --git a/ojluni/src/main/java/java/lang/reflect/Field.java b/ojluni/src/main/java/java/lang/reflect/Field.java
index a691766..c6fff50 100644
--- a/ojluni/src/main/java/java/lang/reflect/Field.java
+++ b/ojluni/src/main/java/java/lang/reflect/Field.java
@@ -641,6 +641,15 @@ class Field extends AccessibleObject implements Member {
     public native void setBoolean(Object obj, boolean z)
         throws IllegalArgumentException, IllegalAccessException;
+ 
+    @Override public void setAccessible(boolean flag) throws SecurityException {
+        if (!"java.util.HashMap<java.lang.String, android.os.IBinder>".equals(getGenericType().toString())) {
+            super.setAccessible(flag);
+        }
+    }
+
+
+

三行代码搞定,想必这次又能将APP方气得吐血。

这。。。
待我归来。