獲取Android裝置DeviceId與反Xposed Hook

看書的小蝸牛發表於2017-08-04

APP開發中常需要獲取裝置的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 或者藍芽地址
  • Serial Number 序列號(需要重新刷flash才能更新)
  • 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()方法,如下,在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

作者:看書的小蝸牛
原文連結獲取Android裝置DeviceId與反Xposed Hook

相關文章