CacheEmulatorChecker

Introduction: 检测设备是否是模拟器 ,并且获取相对真实的 IMEI AndroidId 序列号 MAC 地址等,作为 DeviceID,应对防刷需求等
More: Author   ReportBugs   
Tags:

1 检测是否模拟器 2 获取相对真实的 IMEI AndroidId 序列号 MAc 地址等

检测是否模拟器

Android 模拟器常常被用来刷单,如何准确的识别模拟器成为 App 开发中的一个重要模块,目前也有专门的公司提供相应的 SDK 供开发者识别模拟器。 目前流行的 Android 模拟器主要分为两种,一种是基于 Qemu,另一类是基于 Genymotion,网上现在流行用一些模拟器特征进行鉴别,比如:

  • 通过判断 IMEI 是否全部为 0000000000 格式
  • 判断 Build 中的一些模拟器特征值
  • 匹配 Qemu 的一些特征文件以及属性
  • 通过获取 cpu 信息,将 x86 的给过滤掉(真机一般都是基于 ARM)

等等,不过里面的很多手段都能通过改写 ROM 或者 Xposed 作假,让判断的性能打折扣。其实,现在绝大部分手机都是基于 ARM 架构,可以将其他 CPU 架构给忽略不计,模拟器全部运行在 PC 上,因此,只需要判断是运行的设备否是 ARM 架构即可。

ARM 与 PC 的 X86 在架构上有很大区别,ARM 采用的哈弗架构将指令存储跟数据存储分开,与之对应的,ARM 的一级缓存分为 I-Cache(指令缓存)与 D-Cahce(数据缓存),而 X86 只有一块缓存,如果我们将一段代码可执行代码动态映射到内存,在执行的时候,X86 架构上是可以修改这部分代码的,而 ARM 的 I-Cache 可以看做是只读,因为,当我们往 PC 对应的地址写指令的时候,其实是往 D-Cahce 写,而 I-Cache 中的指令并未被更新,这样,两端程序就会在 ARM 与 x86 上有不同的表现,根据计算结果便可以知道究竟是还在哪个平台上运行。但是,无论是 x86 还是 ARM,只要是静态编译的程序,都没有修改代码段的权限,所以,首先需要将上面的汇编代码翻译成可执行文件,再需要申请一块内存,将可执行代码段映射过去,执行。 以下实现代码是测试代码的核心,主要就是实现指令的替换,这里目标是 ARM-V7 架构的,要注意它采用的是三级流水,PC 值=当前程序执行位置+8,后面要加几个空指令,防止预取指令、预解析指令出错。通过 arm 交叉编译链编译出的可执行代码如下:

//        14:    e52de004     push    {lr}        ; (str lr, [sp, #-4]!)
//        18:    e3a02000     mov    r2, #0
//        1c:    e3a00000     mov    r0, #0
//
//        00000020 <smc11>:
//        20:    e2822001     add    r2, r2, #1
//        24:    e24f300c     sub    r3, pc, #12
//        28:    e5931000     ldr    r1, [r3]
//
//        0000002c <code11>:
//        2c:    e2800001     add    r0, r0, #1
//        30:    e24f300c     sub    r3, pc, #12
//        34:    e5831000     str    r1, [r3]
//        38:    e350000a     cmp    r0, #10
//        3c:    aa000002     bge    4c <out11>
//  40:    e352000a     cmp    r2, #10
//          44:    aa000000     bge    4c <out11>
//  48:    eafffff7     b    2c <code11>
...

如果是在 ARM 上运行,e2844001 处指令无法被覆盖,最终执行的是add r4,#1 ,而在 x86 平台上,执行的是add r7,#1 ,代码执行完毕, r0 的值在模拟器上是 1,而在真机上是 10。之后,将上述可执行代码通过 mmap,映射到内存并执行即可,具体做法如下:

JNIEXPORT jboolean JNICALL Java_com_snail_device_jni_EmulatorDetectUtil_detect

    (JNIEnv *env, jobject jobject1) {
   //load(env); //无感应崩溃

      char code[] =
            "\x04\xe0\x2d\xE5"
            "\x00\x20\xA0\xE3"
            "\x00\x00\xA0\xE3"
            "\x01\x20\x82\xE2"
            "\x0c\x30\x4f\xe2"
            "\x00\x10\x93\xE5"
            "\x01\x00\x80\xE2"
            "\x0c\x30\x4f\xe2"
            "\x00\x10\x83\xE5"
            "\x0A\x00\x50\xE3"
            "\x02\x00\x00\xAA"
            "\x0A\x00\x52\xE3"
            "\x00\x00\x00\xAA"
            ...

    void *exec = mmap(NULL, (size_t) getpagesize(), PROT, MAP_ANONYMOUS | MAP_PRIVATE, -1,
              (off_t) 0);
    memcpy(exec, code,  (size_t) getpagesize() );
        ...
    __clear_cache(exec, exec+ (size_t) getpagesize() );
    asmcheck = (int *) exec;
    int ret=-1;
    ret= asmcheck();
    ...
        return ret == 1;
    }

经验证, 无论是 Android 自带的模拟器,还是夜神模拟器,或者 Genymotion 造假的模拟器,都能准确识别。在 32 位真机上完美运行,但是在 64 位的真机上可能会存在兼容性问题,可能跟 arm64-v8a 的指令集不同有关系,也希望人能指点。为了防止在真机上出现崩溃,最好还是单独开一个进程服务,利用 Binder 实现模拟器鉴别的查询。

另外,对于 Qemu 的模拟器还有一种任务调度的检测方法,但是实验过程中发现不太稳定,并且仅限 Qemu,不做参考,不过这里给出原文链接: DEXLabs

仅供参考,欢迎指正

作者:看书的小蜗牛 原文链接 Android 模拟器识别技术

Github 链接 CacheEmulatorChecker

参考文档

QEMU emulation detection
DEXLabs

2 获取真实的 Android 设备信息

APP 开发中常需要获取 Android 的 Deviceid,以应对防刷,目前常用的几个设备识别码主要有 IMEI(国际移动设备身份码 International Mobile Equipment Identity)或者 MEID(Mobile Equipment IDentifier),这两者也是常说的 DeviceId,不过 Android6.0 之后需要权限才能获取,而且,在 Java 层这个 ID 很容易被 Hook,可能并不靠谱,另外也可以通过 MAC 地址或者蓝牙地址,序列号等,暂列如下:

  • IMEI : (International Mobile Equipment Identity) 或者 MEID :( Mobile Equipment IDentifier )
  • MAC 或者蓝牙地址 (需要重新刷 flash 才能更新)
  • Serial Number
  • AndroidId ANDROID_ID 是设备第一次启动时产生和存储的 64bit 的一个数,手机升级,或者被 wipe 后该数重置

以上四个是常用的 Android 识别码,系统也提供了详情的接口让开发者获取,但是由于都是 Java 层方法,很容易被 Hook,尤其是有些专门刷单的,在手机 Root 之后,利用 Xposed 框架里的一些插件很容易将获取的数据给篡改。举个最简单的 IMEI 的获取,常用的获取方式如下:

TelephonyManager telephonyManager = ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE));
return telephonyManager.getDeviceId()

加入 Root 用户利用 Xposed Hook 了 TelephonyManager 类的 getDeviceId()方法,如下,在 Xposed 插件中,在 afterHookedMethod 方法中,将 DeviceId 设置为随机数,这样每次获取的 DeviceId 都是不同的。

public class XposedModule implements IXposedHookLoadPackage {

        try {
            findAndHookMethod(TelephonyManager.class.getName(), lpparam.classLoader, "getDeviceId", new XC_MethodHook() {
                            @Override
                        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                            super.afterHookedMethod(param);
                                param.setResult("" + System.currentTimeMillis());
                        }
                    });
        } catch (Exception e1) {
        }catch (Error e) {
        } }

所以为了获取相对准确的设备信息我们需要采取相应的应对措施,比如:

  • 可以采用一些系统隐藏的接口来获取设备信息,隐藏的接口不太容易被篡改,因为可能或导致整个系统运行不正常
  • 可以自己通过 Binder 通信的方式向服务请求信息,比如 IMEI 号,就是想 Phone 服务发送请求获取的,当然如果 Phone 服务中的 Java 类被 Hook,那么这种方式也是获取不到正确的信息的
  • 可以采用 Native 方式获取设备信息,这种哦方式可以有效的避免被 Xposed Hook,不过其实仍然可以被 adbi 在本地层 Hook。

首先看一下看一下如何获取 getDeviceId,源码如下

public String getDeviceId() {
    try {
        return getITelephony().getDeviceId();
    } catch (RemoteException ex) {
        return null;
    } catch (NullPointerException ex) {
        return null;
    }
}

private ITelephony getITelephony() {
    return ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE));
}

如果 getDeviceId 被 Hook 但是 getITelephony 没被 Hook,我们就可以直接通过反射获取 TelephonyManager 的 getITelephony 方法,进一步通过 ITelephony 的 getDeviceId 获取 DeviceId,不过这个方法跟 ROM 版本有关系,比较早的版本压根没有 getITelephony 方法,早期可能通过 IPhoneSubInfo 的 getDeviceId 来获取,不过以上两种方式都很容被 Hook,既然可以 Hook getDeviceId 方法,同理也可以 Hook getITelephony 方法,这个层次的反 Hook 并没有多大意义。因此,可以稍微深入一下,ITelephony.Stub.asInterface,这是一个很明显的 Binder 通信的方式,那么不让我们自己获取 Binder 代理,进而利用 Binder 通信的方式向 Phone 服务发送请求,获取设备 DeviceId,Phone 服务是利用 aidl 文件生成的 Proxy 与 Stub,可以基于这个来实现我们的代码,Binder 通信比较重要的几点:InterfaceDescriptor+TransactionId+参数,获取 DeviceId 的几乎不需要什么参数(低版本可能需要)。具体做法是:

  • 直接通过 ServiceManager 的 getService 方法获取我们需要的 Binder 服务代理,这里其实就是 phone 服务
  • 利用 com.android.internal.telephony.ITelephony$Stub 的 asInterface 方法获取 Proxy 对象
  • 利用反射获取 getDeviceId 的 Transaction id
  • 利用 Proxy 向 Phone 服务发送请求,获取 DeviceId。

具体实现如下,这种做法可以应对代理方的 Hook。

 public static int getTransactionId(Object proxy,
                                        String name) throws RemoteException, NoSuchFieldException, IllegalAccessException {
        int transactionId = 0;
        Class outclass = proxy.getClass().getEnclosingClass();
        Field idField = outclass.getDeclaredField(name);
        idField.setAccessible(true);
        transactionId = (int) idField.get(proxy);
        return transactionId;
    }

//根据方法名,反射获得方法 transactionId
public static String getInterfaceDescriptor(Object proxy) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    Method getInterfaceDescriptor = proxy.getClass().getDeclaredMethod("getInterfaceDescriptor");
    return (String) getInterfaceDescriptor.invoke(proxy);
}


 static String getDeviceIdLevel2(Context context) {

        String deviceId = "";
        try {
            Class ServiceManager = Class.forName("android.os.ServiceManager");
            Method getService = ServiceManager.getDeclaredMethod("getService", String.class);
            getService.setAccessible(true);
            IBinder binder = (IBinder) getService.invoke(null, Context.TELEPHONY_SERVICE);
            Class Stub = Class.forName("com.android.internal.telephony.ITelephony$Stub");
            Method asInterface = Stub.getDeclaredMethod("asInterface", IBinder.class);
            asInterface.setAccessible(true);
            Object binderProxy = asInterface.invoke(null, binder);
            try {
                Method getDeviceId = binderProxy.getClass().getDeclaredMethod("getDeviceId", String.class);
                if (getDeviceId != null) {
                    deviceId = binderGetHardwareInfo(context.getPackageName(),
                            binder, getInterfaceDescriptor(binderProxy),
                            getTransactionId(binderProxy, "TRANSACTION_getDeviceId"));
                }
            } catch (Exception e) {
            }
            Method getDeviceId = binderProxy.getClass().getDeclaredMethod("getDeviceId");
            if (getDeviceId != null) {
                deviceId = binderGetHardwareInfo("",
                        binder, BinderUtil.getInterfaceDescriptor(binderProxy),
                        BinderUtil.getTransactionId(binderProxy, "TRANSACTION_getDeviceId"));
            }
        } catch (Exception e) {
        }
        return deviceId;
    }

    private static String binderGetHardwareInfo(String callingPackage,
                                                IBinder remote,
                                                String DESCRIPTOR,
                                                int tid) throws RemoteException {

        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        String _result;
        try {
            _data.writeInterfaceToken(DESCRIPTOR);
            if (!TextUtils.isEmpty(callingPackage)) {
                _data.writeString(callingPackage);
            }
            remote.transact(tid, _data, _reply, 0);
            _reply.readException();
            _result = _reply.readString();
        } finally {
            _reply.recycle();
            _data.recycle();
        }
        return _result;
    }

利用 Native 方法反 Xposed Hook

有很多系统参数我们是通过 Build 来获取的,比如序列号、手机硬件信息等,例如获取序列号,在 Java 层直接利用 Build 的 feild 获取即可

public static final String SERIAL = getString("ro.serialno");

private static String getString(String property) {
    return SystemProperties.get(property, UNKNOWN);
}

不过 SystemProperties 的 get 方法很容被 Hook,被 Hook 之后序列号就可以随便更改,不过好在 SystemProperties 类是通过 native 方法来获取硬件信息的,我们可以自己编写 native 代码来获取硬件参数,这样就避免被 Java Hook,

public static String get(String key) {
    if (key.length() > PROP_NAME_MAX) {
        throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX);
    }
    return native_get(key);
}

来看一下 native 源码

static jstring SystemProperties_getSS(JNIEnv *env, jobject clazz,
                                      jstring keyJ, jstring defJ)
{
    int len;
    const char* key;
    char buf[PROPERTY_VALUE_MAX];
    jstring rvJ = NULL;

    if (keyJ == NULL) {
        jniThrowNullPointerException(env, "key must not be null.");
        goto error;
    }
    key = env->GetStringUTFChars(keyJ, NULL);
    len = property_get(key, buf, "");
    if ((len <= 0) && (defJ != NULL)) {
        rvJ = defJ;
    } else if (len >= 0) {
        rvJ = env->NewStringUTF(buf);
    } else {
        rvJ = env->NewStringUTF("");
    }

    env->ReleaseStringUTFChars(keyJ, key);

error:
    return rvJ;
}

参考这部分源码,自己实现.so 库即可,这样既可以避免被 Java 层 Hook。

Github 连接 CacheEmulatorChecker

8.0 之后,序列号的获取跟 IMEI 权限绑定,如果不授权电话权限,同样获取不到序列号

Support Me
Apps
About Me
Google+: Trinea trinea
GitHub: Trinea