模拟点击与群控——防护手段(一)

2,935 阅读7分钟

写在前面

模拟点击与群控技术经常被黑灰产用于获得非法利益的操作,这给应用厂商带来极大的危害。本文将从安全角度出发详细讲述行之有效的对抗手段,保障应用厂商的合法利益。目前群控作弊手段多种多样,具体内容可查看前面几篇文章介绍,本文将先对各类常见的模拟点击作弊手段的强特征进行检测。在下一篇文章再从算法维度讲述如何建立特定的模型来找出群控设备。

adb模拟点击特征

adb检测是最基本的一种特征,因为大部分群控手段都是需要手机连上PC端来进行远程操作,而这种连接经常是利用adb实现,因此我们对当前设备是否使用adb即可判断是否存在群控的风险。下面提供几种判断Android设备是否处于ADB开启状态的方法,分别如下。

  1. 利用Android系统属性,ADB开启时,一些系统属性会发生变化。可以通过检测这些属性的值,判断当前设备是否处于调试模式。常用的系统属性包括“sys.usb.state”和“ro.debuggable”等。判断代码示例如下:
private boolean checkADBEnabledBySystemProperties(Context context) {
    String state = android.os.SystemProperties.get(("sys.usb.state");
    String debuggable = android.os.SystemProperties.get("ro.debuggable");
    return (USB_STATE_VALUE.equals(state) && DEBUGGABLE_VALUE.equals(debuggable));
}
  1. 通过检查USB设备的连接状态来判断当前设备是否可能处于adb调试状态,代码如下:
private boolean isUsbConnected(Context context) {
    IntentFilter filter = new IntentFilter();
    filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
    filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);        
    
    UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
    HashMap<String, UsbDevice> usbDevices = usbManager.getDeviceList();
    for (Map.Entry<String, UsbDevice> entry : usbDevices.entrySet()) {
        UsbDevice device = entry.getValue();
        if (usbManager.hasPermission(device)) {
            if (device.getDeviceClass() == UsbConstants.USB_CLASS_MASS_STORAGE) {
                return true;
            }
        }
    }
    return false;
}
  1. 通过读取 Settings.Secure.ADB_ENABLED 系统设置的值来判断 Android 设备是否启用 adb,代码如下。
private boolean isAdbEnabled(Context context) {
    // 读取ADB_ENABLED设置的值
    int adbEnabled = Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.ADB_ENABLED, 0);
    // 检查设置是否为1,如果是,说明设备已启用ADB
    return adbEnabled == 1;
}
  1. 检查TCP端口状态,ADB默认使用TCP端口来与设备通信。可以通过检测TCP协议的5555端口,判断当前设备是否处于ADB调试模式。代码示例如下:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;

private static final int ADB_TCP_PORT = 5555;
private static final int TCP_PORT_SOCKET_TIMEOUT = 1000;

private boolean checkADBEnabledByTCPPort() {
    try {
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress("localhost", ADB_TCP_PORT), TCP_PORT_SOCKET_TIMEOUT);
        socket.close();
        return true;
    } catch (IOException e) {
        return false;
    }
}
  1. 检测adb进程,当设备处于ADB调试模式时,设备上会运行一个名为“adbd”的进程。可以通过执行“adb shell ps”命令,搜索是否存在该进程,以判断当前设备是否处于ADB调试模式。代码示例如下:
private boolean checkADBEnabledByProcess() {
    boolean adbProcessFound = false;
    try {
        Process process = Runtime.getRuntime().exec("ps");
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String line;
        while ((line = reader.readLine()) != null) {
            if (line.contains("adbd")) {
                 adbProcessFound = true;
                 break;
            }
        }
        reader.close();
        process.destroy();
    } catch (IOException e) {
        // handle exception
    }
    return adbProcessFound;
}

accessibility辅助特征

accessibility辅助能够实现对屏幕读取、字幕、文本放大、语音识别等提供支持。借助 AccessibilityService 服务,可以实现大部分的模拟点击操作。大部分用户在正常情况下都是无需开启该功能,因此我们可以判断当前设备是否有应用使用了辅助功能来是否可能存在作弊行为。

检查 AccessibilityService 的状态来判断 Android 设备是否启用了无障碍服务(Accessibility Service),具体代码如下:

private boolean isAccessibilityEnabled(Context context) {
    AccessibilityManager accessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
    List<AccessibilityServiceInfo> enabledServices = accessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC);
    return enabledServices != null && !enabledServices.isEmpty();
}

为了提高准确性,可以采集所有辅助服务的名称,从中找出存在作弊辅助服务名的设备。

autojs模拟点击特征

由于autojs使用的是辅助服务来实现的模拟点击操作,因此可以直接利用「accessibility辅助特征」中的方法,遍历当前设备的辅助服务列表,如果存在autojs,则可判定为存在autojs模拟点击。

检测用户是否使用了 Auto.js,还可以尝试获取系统中所有进程的信息,并查找是否存在 Auto.js 相关的进程,具体代码如下,示例代码中检查所有正在运行的进程列表中是否存在包含 "org.autojs." 字符串的进程名称。如果存在这样的进程,就说明用户正在运行 Auto.js。但是,这个方法可能并不可靠,因为用户可能使用任何定制的、修改过的或重命名的版本的 Auto.js,这些版本的进程名称可能与原始版本不同。

private boolean isAutoJsRunning() {
    ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
    List<ActivityManager.RunningAppProcessInfo> runningProcesses = activityManager.getRunningAppProcesses();

    if (runningProcesses != null && !runningProcesses.isEmpty()) {
        for (ActivityManager.RunningAppProcessInfo process : runningProcesses) {
            String processName = process.processName;
            if (processName != null && processName.toLowerCase().contains("org.autojs.")) {
                return true;
            }
        }
    }
    return false;
}

除了识别进程列表外,还有可以通过查看应用权限特征来进行识别。Auto.js应用会使用到一些相对特殊的权限,如:android.permission.READ_LOGSandroid.permission.READ_CALL_LOG等,具体权限可查看autojs工程源码,因此可以利用这些特殊的权限特征来进行识别,具体代码如下:

private boolean isAutoJsPermissionGranted() {
    PackageManager packageManager = getPackageManager();
    try {
        PackageInfo packageInfo = packageManager.getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS);
        if (packageInfo != null && packageInfo.requestedPermissions != null) {
            for (String permission : packageInfo.requestedPermissions) {
                if (permission != null && permission.equals("android.permission.READ_LOGS") && permission.equals("android.permission.READ_CALL_LOG")) {
                    return true;
                } 
            }
        }
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }
    return false;
}

还可以通过检查系统中是否存在特定的Auto.js脚本文件来判断用户是否使用了Auto.jsAuto.js将脚本文件保存在/sdcard/脚本/目录中,因此可以使用以下代码来检查是否存在Auto.js脚本:

private boolean isAutoJsScriptExists() {
    String autoJsScriptsDir = Environment.getExternalStorageDirectory() + "/脚本/";
    File scriptsDir = new File(autoJsScriptsDir);
    if (scriptsDir.exists() && scriptsDir.isDirectory()) {
        File[] files = scriptsDir.listFiles();
        if (files != null && files.length > 0) {
            for (File file : files) {
                if (isAutoJsScriptFile(file)) {
                    return true;
                }
            }
        }
    }
    return false;
}

private boolean isAutoJsScriptFile(File file) {
    return file.isFile() && file.getName().endsWith(".js") && file.getPath().contains("/Auto.js");
}

群控软件特征

其他的群控框架包括云控设备,基于openstf框架的群控设备,基于atx-server框架的群控设备,基于uiautomator框架的群控设备等,下面分别对这几类不同的框架实现针对性的识别方案。当然实际的群控设备并不止这些,其他的可以借鉴下面方法以此类推。

云控设备

如雷电云,红手指等云群控设备,通过云端模拟器的方式模拟真机设备。这类设备可以通过cpu架构来判断,不少云控设备使用的是x86架构的PC端设备模拟器实现,检测cpu架构的方法包括:

  • x86与arm的cpu架构中缓存形式的差异特征。
  • Qemu文件特征。
  • cpu品牌类型等特征。
  • maps文件中使用到的系统库特征。
  • 等等。

然而,随着技术的发展,arm架构的cpu越来越普及,目前也有不少云控PC设备直接使用arm架构的cpu,

  • 检测app列表特征。
  • 系统fingerprint特征。
  • 系统品牌特征。
  • 网卡特征。
  • 等等。

这块内容比较多,等后续有介绍到模拟器再来详细展开。

openstf架构群控

openstf架构会在移动端设备运行该框架所需的特定文件,因此我们可以根据这些特征文件来做识别。

  • openstf框架需要在设备上安装其特定的app,因此可以根据设备安装的app列表来判断,若存在包名为 ****jp.co.cyberagent.stf,则可认为当前设备存在使用openstf框架实现群控的风险。
  • 群控手机为了能够利用adb向手机app的text框输入中文内容,一般都会安装ADBKeyboard作为默认输入法,因此还可以判断是否存在包名**com.android.adbkeyboard**,若存在则认为有群控的风险。获取输入法列表代码如下:
InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
List<InputMethodInfo> inputMethodInfos = inputManager.getInputMethodList();
for (InputMethodInfo inputMethodInfo : inputMethodInfos) {
    String packageName = inputMethodInfo.getPackageName();
    String serviceName = inputMethodInfo.getServiceName();
}
  • 通过对特定端口,以及特征文件(如minicap.so,minitouch,minicap等)进行扫描,如果存在,则认为存在群控的风险。
  • 利用openstf框架自身的实现原理来进行识别,openstf是利用向/dev/input/event0发送指令来实现模拟点击,因此我们根据该文件的属性值来进行识别。

atx-server2 架构群控

atx-server2与openstf框架类似,识别检测方法包括:

  • atx-server2框架需要在设备上安装atx-agent客户端,因此可以根据设备安装的app列表来判断,若存在包名为 ****com.github.uiautomator com.github.uiautomator.test,则可认为当前设备存在使用atx-server2框架实现群控的风险。
  • atx-agent自带FastInputIME输入法,安装uiautomator2过程中,作者还推荐安装WhatsInput输入法。我们可以可以通过对这两种输入法这两款输入法进行判断,若存在则存在使用atx-server2群控的嫌疑。