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