前言 本文的项目来自于https://github.com/happylishang/AntiFakerAndroidChecker/
模拟器检测 MainActivity.java 在demo中展示的就是检测模拟器的代码
onCreate 1 2 3 4 super .onCreate(savedInstanceState);binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); mActivity = this ;
设置mActivity为当前MainActivity的实例,方便调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 binding.btnAsyncSimu.setOnClickListener(v -> EmuCheckUtil.checkEmulatorFromCache(getApplicationContext(), new EmuCheckUtil .CheckEmulatorCallBack() { @Override public void onCheckSuccess (boolean isEmulator) { TextView textView = (TextView) findViewById(R.id.btn_async_simu); textView.setText(" 内存异步非UI进程获取是否模拟器 " + isEmulator); } @Override public void onCheckFaild () { TextView textView = (TextView) findViewById(R.id.btn_async_simu); textView.setText(" 内存异步非UI进程获取是否模拟器 失败" ); } }));
binding.btnAsyncSimu.setOnClickListener(...)
为布局文件中 ID 为 btn_async_simu 的按钮设置一个点击事件监听器
布局文件app\src\main\res\layout\activity_main.xml
当用户点击这个按钮时,监听器内部的代码会被执行
点击事件触发后,会调用 EmuCheckUtil.checkEmulatorFromCache() 方法。这个方法是整个功能的核心
checkEmulatorFromCache 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public static void checkEmulatorFromCache (final Context context, @NonNull final EmuCheckUtil.CheckEmulatorCallBack callBack) { Intent intent = new Intent (context, EmulatorCheckService.class); context.bindService(intent, new ServiceConnection () { public void onServiceConnected (ComponentName name, IBinder service) { IEmulatorCheck IEmulatorCheck = com.snail.antifake.IEmulatorCheck.Stub.asInterface(service); if (IEmulatorCheck != null ) { try { callBack.onCheckSuccess(IEmulatorCheck.isEmulator()); context.unbindService(this ); } catch (RemoteException var5) { callBack.onCheckFaild(); context.unbindService(this ); } } } public void onServiceDisconnected (ComponentName name) { } }, BIND_AUTO_CREATE); }
详细分析:
一开始先创建了一个显式 Intent。这种类型的 Intent 用于应用程序内部通信 。它明确指明了目的地 :EmulatorCheckService
context.bindService 启动与 intent 指定的服务的连接,第二个参数是ServiceConnection 的实例,这是一个用于监视连接状态的接口 。这里,它被实现为一个匿名内部类
当与 EmulatorCheckService 的连接成功建立后,Android 系统会调用此回调方法。它接收一个 IBinder 对象,它是与远程服务的原始通信通道
IEmulatorCheck 接口在 .aidl 文件中定义。静态方法 asInterface() 接收来自服务的通用 IBinder 并将其转换为客户端代理对象 (IEmulatorCheck)。这个代理对象具有与远程服务接口相同的方法(例如 isEmulator()),允许客户端像调用本地方法一样调用它们
代码现在在代理对象上调用 isEmulator() 方法。这会触发 IPC 机制:方法调用及其参数被“编组”(序列化 ),跨进程边界发送到 EmulatorCheckService,并在那里执行。来自服务的布尔结果随后被“解组”并返回到此处
成功获取结果后,客户端解除与服务的绑定 。这对于资源管理至关重要。由于服务可能是通过 BIND_AUTO_CREATE 启动的,一旦所有客户端都解除绑定,系统将自动销毁它
如果服务进程崩溃或被终止,对远程服务的调用可能会失败。此类故障会抛出 RemoteException
此方法仅在意外断开连接(例如服务进程崩溃)时调用
**BIND_AUTO_CREATE**此标志告诉 bindService 如果服务尚未运行则创建它
什么是 AIDL?
AIDL 全称是 Android Interface Definition Language ,即 Android 接口定义语言 。
它是一种 IPC (Inter-Process Communication,进程间通信) 机制。当你的 Android 应用需要与另一个应用或服务(尤其是在不同进程中运行的服务)进行通信时,就需要一种双方都能理解的“协议”来传递数据和调用方法。AIDL 就是用来定义这个协议的工具 。
它的工作流程大致如下:
定义接口 :你使用类似 Java 接口的语法在一个 .aidl 文件中声明一个接口,包括你想暴露给客户端调用的方法 。
生成代码 :Android SDK 工具会处理这个 .aidl 文件,并自动生成一个同名的 Java 接口文件。这个文件中包含一个名为 Stub 的抽象内部类,它实现了接口并处理了所有远程调用的细节。
服务端实现 :服务(Service)需要创建一个 Stub 类的实例,并实现你在 .aidl 文件中定义的方法。Service 的 onBind() 方法会返回这个 Stub 实例。
客户端调用 :客户端在 onServiceConnected() 回调中接收到一个 IBinder 对象。通过调用 YourInterface.Stub.asInterface(binder),客户端可以得到一个接口的代理对象。之后,客户端就可以像调用本地方法一样调用这个代理对象的方法,而 Android 系统会在底层处理所有跨进程的数据打包(编组)、传输和解包(解组)工作。
为什么这里使用 AIDL 而不是更简单的方法?
查看项目中的 AndroidManifest.xml 文件
1 2 <service android:name =".jni.EmulatorCheckService" android:process =":EmulatorCheckService" />
关键在于 android:process=":EmulatorCheckService" 这一行。
属性值前面的冒号 (:) 表示这个 Service 将会运行在一个私有的、独立的进程 中,进程名是 你的应用包名:EmulatorCheckService
既然检测逻辑被刻意放到了一个独立的进程中,那么主应用进程与这个服务进程之间的通信就必须使用 IPC 机制。 AIDL 是 Android 官方提供的、功能强大且高效的 IPC 方式之一,因此被选用
提高安全性,防止 Hook :
Hook 工具通常是注入到目标应用的主进程里 ,它们无法直接影响到另一个独立的 EmulatorCheckService 进程。攻击者如果想破解,就必须设法去 Hook 这个新的服务进程,操作更复杂,从而有效地提升了反作弊的门槛。
异步性 : 检查不会阻塞主线程,确保流畅的用户体验。
隔离性 : 通过在单独的进程中运行检查,使得恶意工具(如 Hook 框架)更难从应用程序的主进程中篡改检测逻辑。
EmulatorCheckService 1 callBack.onCheckSuccess(IEmulatorCheck.isEmulator());
此处调用isEmulator函数,会触发IPC机制
为什么执行它就是执行EmulatorCheckService的isEmulator函数?
调用 AIDL 接口的 isEmulator() 方法之所以会执行 EmulatorCheckService 中的 isEmulator() 方法,是因为 Android 系统在背后为你构建了一座“通信桥梁”。这个桥梁由两部分组成:客户端的 代理(Proxy) 和服务端的 存根(Stub)
当你编译项目时,Android 构建工具会找到 IEmulatorCheck.aidl 文件,并自动生成一个 IEmulatorCheck.java 文件。这个自动生成的文件包含了所有实现跨进程通信所需要的“胶水代码”
在 EmulatorCheckService.java 中,创建了一个 IEmulatorCheck.Stub 的匿名内部类实例,并实现了 isEmulator() 方法
当服务启动并被绑定时,这个 Stub 对象(它本身就是一个 IBinder)被 Android 系统传递给了客户端
这里的 Stub.asInterface(service) 方法做了什么?
它检查发现客户端和服务端不在同一个进程。
于是,它创建并返回了一个 IEmulatorCheck.Stub.Proxy 对象。这个 Proxy 对象持有了指向服务端 Binder 的引用。
所以,AIDL 接口本身只是一个约定,而自动生成的 Proxy 和 Stub 类才是实现这个约定的“工匠”,它们联手完成了所有复杂的跨进程通信工作,让你感觉就像在调用一个本地方法一样简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class EmulatorCheckService extends Service { Handler mHandler=new Handler (); @Nullable @Override public IBinder onBind (Intent intent) { return new IEmulatorCheck .Stub() { @Override public boolean isEmulator () throws RemoteException { return EmulatorDetectUtil.isEmulator(EmulatorCheckService.this ); } @Override public void kill () throws RemoteException { stopSelf(); mHandler.postDelayed(new Runnable () { @Override public void run () { System.exit(0 ); } },500 ); } }; } @Override public void onDestroy () { super .onDestroy(); Process.killProcess(Process.myPid()); } }
EmulatorCheckService 是一个标准的 Android Service。它的特殊之处在于,通过在 AndroidManifest.xml 文件中的配置,它运行在一个独立的私有进程 中
1 2 <service android:name =".jni.EmulatorCheckService" android:process =":EmulatorCheckService" />
onBind(Intent intent) 方法
作用 :这是 Service 与客户端(EmuCheckUtil)建立连接的入口点。当客户端调用 bindService() 时,Android 系统会调用这个方法。
返回值 :它必须返回一个 IBinder 对象,这个对象是客户端与服务端之间通信的桥梁。
实现方式 :这里直接返回了一个 new IEmulatorCheck.Stub() { ... } 的匿名内部类实例。IEmulatorCheck.Stub 是由 AIDL 工具根据 IEmulatorCheck.aidl 文件自动生成的。我们在这里继承 Stub 类并实现其抽象方法,这就构成了 AIDL 的服务端实现 。
isEmulator()方法
1 2 3 4 @Override public boolean isEmulator () throws RemoteException { return EmulatorDetectUtil.isEmulator(EmulatorCheckService.this ); }
作用 :这是对 AIDL 接口中 isEmulator() 方法的具体实现。当客户端的代理对象调用 isEmulator() 时,经过一系列的 IPC(进程间通信)流程,最终会执行到这里的代码。
核心逻辑 :它没有自己实现检测逻辑,而是直接调用了 EmulatorDetectUtil.isEmulator()。
关键点 :因为 EmulatorCheckService 运行在独立的进程中
所以 EmulatorDetectUtil.isEmulator() 这个调用(以及它内部的 native 方法 detectS())也发生在这个独立进程中 。这就达到了将检测逻辑与主应用进程隔离的目的,极大地增加了 Hook 攻击的难度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .... static { System.loadLibrary("emulator_check" ); } @Keep native public static boolean detectS () ; public static boolean isEmulator (Context context) { return detectS(); }
1 2 3 4 5 6 7 8 //CMakeList.txt add_library ( emulator_check SHARED src/main/jni/emulator/emcheck64.c )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 int getArch (JNIEnv *env) { jclass cls = (*env)->FindClass (env, "com/snail/antifake/jni/EmulatorDetectUtil" ); jmethodID mid = (*env)->GetStaticMethodID (env, cls, "getSystemArch" , "()I" ); return (jint) (*env)->CallStaticIntMethod (env, cls, mid); } jboolean JNICALL detect (JNIEnv *env, jclass jclass1) { load (env); int type = getArch (env); if (type == 0 || type == 1 ) { return JNI_TRUE; } char code32[] = "\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" "\xf7\xff\xff\xea" "\x04\xf0\x9d\xE4" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" ; char code64[] = "\xff\xc3\x00\xd1" "\xfd\x7b\x02\xa9" "\x02\x00\x80\xd2" "\x00\x00\x80\xd2" "\x42\x04\x00\x91" "\xe3\xff\xff\x10" "\x61\x00\x40\xf9" "\x00\x04\x00\x91" "\xe3\xff\xff\x10" "\x61\x00\x00\xf9" "\x1f\x28\x00\xf1" "\x8a\x00\x00\x54" "\x5f\x28\x00\xf1" "\x4a\x00\x00\x54" "\xf9\xff\xff\x17" "\xfd\x7b\x42\xa9" "\xff\xc3\x00\x91" "\xc0\x03\x5f\xd6" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" ; void *exec = mmap (NULL , (size_t ) getpagesize (), PROT, MAP_ANONYMOUS | MAP_PRIVATE, -1 , (off_t ) 0 ); if (exec == (void *) -1 ) { int fd = fopen ("/dev/zero" , "w+" ); exec = mmap (NULL , (size_t ) getpagesize (), PROT, MAP_PRIVATE, fd, (off_t ) 0 ); if (exec == (void *) -1 ) { return 10 ; } } if (type == 2 ) { memcpy (exec, code32, sizeof (code32)); } else { memcpy (exec, code64, sizeof (code64)); } asmcheck = (int *) exec; __clear_cache(exec, exec + (size_t ) getpagesize ()); a = asmcheck (); munmap (exec, getpagesize ()); return a == 1 ; }
来到了检测模拟器的native层函数,详细分析如下:
1 2 int type = getArch(env);if (type == 0 || type == 1 ) { return JNI_TRUE; }
获取架构 :代码首先通过 JNI 回调 EmulatorDetectUtil.getSystemArch() 方法。这个 Java 方法会读取系统属性 ro.product.cpu.abi 来获取当前的 CPU ABI(应用二进制接口)
1 2 3 4 5 6 7 8 9 10 11 12 public static int getSystemArch () { String cpuAbi= PropertiesGet.getString("ro.product.cpu.abi" ); if ("armeabi-v7a" .equals(cpuAbi)) return Arch.ARM32; if ("arm64-v8a" .equals(cpuAbi)) return Arch.ARM64; if ("x86" .equals(cpuAbi) ) return Arch.X86; if ( "x86_64" .equals(cpuAbi)) return Arch.X86_64; return Arch.ARM64; }
判断架构 :
type == 0 对应 x86
type == 1 对应 x86_64
判定结果 :如果检测到 CPU 是 x86 或 x86_64 架构,函数就**立即返回 JNI_TRUE**,判定当前环境为模拟器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 char code32[] = "\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" "\xf7\xff\xff\xea" "\x04\xf0\x9d\xE4" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" ; char code64[] = "\xff\xc3\x00\xd1" "\xfd\x7b\x02\xa9" "\x02\x00\x80\xd2" "\x00\x00\x80\xd2" "\x42\x04\x00\x91" "\xe3\xff\xff\x10" "\x61\x00\x40\xf9" "\x00\x04\x00\x91" "\xe3\xff\xff\x10" "\x61\x00\x00\xf9" "\x1f\x28\x00\xf1" "\x8a\x00\x00\x54" "\x5f\x28\x00\xf1" "\x4a\x00\x00\x54" "\xf9\xff\xff\x17" "\xfd\x7b\x42\xa9" "\xff\xc3\x00\x91" "\xc0\x03\x5f\xd6" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" "\x00\x00\xa0\xe1" ; void *exec = mmap(NULL , (size_t ) getpagesize(), PROT, MAP_ANONYMOUS | MAP_PRIVATE, -1 , (off_t ) 0 ); if (exec == (void *) -1 ) { int fd = fopen("/dev/zero" , "w+" ); exec = mmap(NULL , (size_t ) getpagesize(), PROT, MAP_PRIVATE, fd, (off_t ) 0 ); if (exec == (void *) -1 ) { return 10 ; } } if (type == 2 ) { memcpy (exec, code32, sizeof (code32)); } else { memcpy (exec, code64, sizeof (code64)); } asmcheck = (int *) exec; __clear_cache(exec, exec + (size_t ) getpagesize()); a = asmcheck(); munmap(exec, getpagesize()); return a == 1 ;
利用了真实 ARM 硬件分离的 I/D Cache 与 QEMU 动态翻译引擎在处理自修改代码时的缓存一致性行为差异来区分模拟器
真机 (ARM CPU) 的行为
哈佛/改进型哈佛架构 (Harvard/Modified Harvard Architecture):现代高性能 ARM 内核(如 Cortex-A 系列)普遍采用这种架构。其核心特征就是 指令总线和数据总线分离 ,从而实现了独立的指令缓存(I-Cache)和数据缓存(D-Cache)。
缓存一致性问题 (Cache Coherency Problem):当一个 CPU 核心执行 STR (Store) 指令修改一段内存时,这个写操作是通过数据路径进行的,它会更新 D-Cache 和最终的主存。然而,如果被修改的内存区域恰好也作为指令被加载到了 I-Cache 中,硬件本身 不会 自动使 I-Cache 中对应的缓存行(Cache Line)失效。
__clear_cache 的必要性 :为了解决这个问题,ARM 架构提供了特定的指令(如 IC IALLU, DC CIVAC)和相应的系统调用(最终由内核实现),允许软件显式地管理缓存。__clear_cache 这个 GCC 内建函数(或类似的库函数)就是上层对这些底层操作的封装。它的作用是:
将 D-Cache 中被修改的数据写回到主存(Write-Back/Clean)。
使 I-Cache 中覆盖了相同内存地址的缓存行失效(Invalidate)。 这样,当 CPU 再次从该地址取指时,由于 I-Cache Miss,它会从主存中重新加载最新的、已被修改的指令。
检测利用点 :如果不调用 __clear_cache,CPU 就会执行 I-Cache 中陈旧的指令,导致程序行为与预期不符。
模拟器 (QEMU) 的行为
**动态二进制翻译 (Dynamic Binary Translation - DBT)**:QEMU 的核心是它的 TCG (Tiny Code Generator)。它并非逐条解释执行 ARM 指令,而是将一个 ARM 指令块(Guest Code)动态翻译成一段等效的、能在宿主 CPU(Host CPU,通常是 x86)上直接运行的机器码块(Host Code)。
翻译缓存与内存监控 :QEMU 会缓存这些翻译好的代码块。为了保证正确性,QEMU 必须监控被模拟的客户机内存(Guest Memory)。当 QEMU 检测到一段已经被翻译过的 Guest Code 所在的内存区域被写操作修改时,它会废弃(Invalidate) 掉对应的、已缓存的 Host Code。
重新翻译 (Re-translation):当模拟的程序流程再次跳转到那段被修改过的地址时,QEMU 会发现没有可用的翻译缓存,于是它会重新从 Guest Memory 中读取 最新的指令 ,进行新一轮的翻译和执行。
检测利用点 :QEMU 的这种为保证模拟正确性而设计的机制,恰好“完美”地处理了自修改代码。它表现出的行为,等同于一个缓存永远同步 的理想化处理器。
说白了就是真机的i-cache和d-cache是不是随时同步的,当i-cache调用STR(store)指令时,是通过数据总线来更新d-cache和主存的,如果此时没及时调用__clear_cache的话,也会继续执行当前cache line的代码
cache line就是 CPU 缓存 (Cache) 和主内存 (RAM) 之间数据传输的最小单位 (毕竟如果一个字节一个字节从CPU中读取的话,那么太浪费资源了,因此就直接读很多个字节(称之为cache line),常见的大小是 32 字节、64 字节或 128 字节
而模拟器一般都是缓存永远同步的,即会执行最新修改的代码
这里利用的就是在真机中如果不调用 __clear_cache,CPU 就会执行 I-Cache 中陈旧的指令,导致程序行为与预期不符
__clear_cache 的作用就是 强制冲刷流水线(Pipeline Flush) 并使 I-Cache 失效
onCreate 1 2 3 4 5 6 7 8 9 10 11 12 13 binding.btnSycnSycSimu.setOnClickListener (new View.OnClickListener () { @Override public void onClick (View v) { for (int i = 0 ; i < 100 ; i++) { TextView textView = (TextView) findViewById (R.id.btn_sycn_syc_simu); textView.setText (" 内存同步是否模拟器 " + EmulatorDetectUtil.isEmulator (MainActivity.this )); } } });
通过EmulatorDetectUtil.isEmulator(MainActivity.this)来检测UI进程
同步的、在当前线程和当前进程中 执行的方法调用
而上面那种是异步的、通过AIDL在另一个服务进程中 执行的方法调用
1 2 3 4 5 6 7 8 9 binding.btnSample.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View view) { TextView textView = (TextView) findViewById(R.id.btn_sample); textView.setText("特征信息判断是否模拟器 " + AndroidDeviceIMEIUtil.isRunOnEmulator(MainActivity.this )); } });
调用AndroidDeviceIMEIUtil.isRunOnEmulator(MainActivity.this)方法来特征检测
1 2 3 4 5 6 7 8 public class AndroidDeviceIMEIUtil { public static boolean isRunOnEmulator (Context context) { return EmuCheckUtil.mayOnEmulator(context); } ... }
1 2 3 4 5 6 7 8 public static boolean mayOnEmulator (Context context) { return mayOnEmulatorViaQEMU(context) || isEmulatorViaBuild(context) || isEmulatorFromAbi() || isEmulatorFromCpu(); }
mayOnEmulatorViaQEMU 1 2 3 4 5 public static boolean mayOnEmulatorViaQEMU (Context context) { String qemu = PropertiesGet.getString("ro.kernel.qemu" ); return "1" .equals(qemu); }
其中getString函数如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.snail.antifake.jni;public class PropertiesGet { static { System.loadLibrary("property_get" ); } private static native String native_get (String key) ; private static native String native_get (String key,String def) ; public static String getString (String key) { return native_get(key); } public static String getString (String key,String def) { return native_get(key,def); } }
是通过native获得
在cmake中
1 2 3 4 5 6 7 add_library ( property_get SHARED src/main/jni/property/proget.c)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 #include <jni.h> #include <sys/system_properties.h> #include <android/log.h> #include <string.h> #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,"lishang" ,__VA_ARGS__) JNIEXPORT jstring JNICALL proGetSS (JNIEnv *env, jclass clazz, jstring keyJ, jstring defJ) { int len; const char *key; char buf[100 ]; jstring rvJ = NULL ; if (keyJ == NULL ) { LOGI ("key must not be null." ); goto error; } key = (*env)->GetStringUTFChars (env, keyJ, NULL ); len = __system_property_get(key, buf); if ((len <= 0 ) && (defJ != NULL )) { rvJ = defJ; } else if (len >= 0 ) { rvJ = (*env)->NewStringUTF (env, buf); } else { rvJ = (*env)->NewStringUTF (env, "" ); } (*env)->ReleaseStringUTFChars (env, keyJ, key); error: return rvJ; } JNIEXPORT jstring JNICALL proGet (JNIEnv *env, jclass clazz, jstring keyJ) { return proGetSS (env, clazz, keyJ, NULL ); } static int registerNativeMethods (JNIEnv *env, const char *className, JNINativeMethod *gMethods, int numMethods) { jclass clazz; clazz = (*env)->FindClass (env, className); if (clazz == NULL ) return JNI_FALSE; if ((*env)->RegisterNatives (env, clazz, gMethods, numMethods) < 0 ) { return JNI_FALSE; } return JNI_TRUE; } static const char *classPathName = "com/snail/antifake/jni/PropertiesGet" ;static JNINativeMethod methods[] = { {"native_get" , "(Ljava/lang/String;)Ljava/lang/String;" , (void *) proGet}, {"native_get" , "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;" , (void *) proGetSS}, }; jint JNI_OnLoad (JavaVM* vm, void * reserved) { JNIEnv* env = NULL ; jint result = -1 ; if ((*vm)->GetEnv (vm, (void **) &env, JNI_VERSION_1_4) != JNI_OK) goto bail; if (!registerNativeMethods (env, classPathName, methods, sizeof (methods)/sizeof (methods[0 ]))) goto bail; result = JNI_VERSION_1_4; bail: return result; }
首先通过registerNativeMethods注册com/snail/antifake/jni/PropertiesGet下的两个方法
然后proGet和proGetSS的返回值变量类型定义为JNIEXPORT jstring JNICALL
JNIEXPORT 的核心作用是将 native 函数标记为 “可导出符号”
JNICALL 的作用是统一函数的调用约定
jstring:是函数的返回值类型 ,对应 Java 中的 String 类型
len = __system_property_get(key, buf);主要是通过这个函数读取相应的数据
1 2 3 4 5 6 7 8 9 10 11 int __system_property_get(const char *name, char *value){ const prop_info *pi = __system_property_find(name); if (pi != 0 ) { return __system_property_read(pi, 0 , value); } else { value[0 ] = 0 ; return 0 ; } }
isEmulatorViaBuild 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static boolean isEmulatorViaBuild (Context context) { if (!TextUtils.isEmpty(PropertiesGet.getString("ro.product.model" )) && PropertiesGet.getString("ro.product.model" ).toLowerCase().contains("sdk" )) { return true ; } if (!TextUtils.isEmpty(PropertiesGet.getString("ro.product.manufacturer" )) && PropertiesGet.getString("ro.product.manufacture" ).toLowerCase().contains("unknown" )) { return true ; } if (!TextUtils.isEmpty(PropertiesGet.getString("ro.product.device" )) && PropertiesGet.getString("ro.product.device" ).toLowerCase().contains("generic" )) { return true ; } return false ;
ro.product.model: 这是一个系统属性,通常表示设备的型号 。在模拟器环境中,这个值经常包含 “sdk” 字样,例如 “sdk_gphone_x86”、”Android SDK built for x86” 等。
ro.product.manufacturer: 这是一个系统属性,表示设备的制造商 。在某些模拟器(尤其是 Genymotion 或一些早期模拟器)中,这个值可能被设置为 “unknown” 或其他非真实制造商的字符串。
ro.product.device: 这是一个系统属性,表示设备的代号或名称 。Android 模拟器(AVD)通常会使用 “generic” 作为其设备名称的一部分,例如 “generic_x86”、”generic_arm64” 等。
isEmulatorFromAbi 1 2 3 4 5 6 private static boolean isEmulatorFromAbi () { String abi= AndroidDeviceIMEIUtil.getCpuAbi(); return !TextUtils.isEmpty(abi) && abi.contains("x86" ); }
1 2 3 public static String getCpuAbi () { return PropertiesGet.getString("ro.product.cpu.abi" ); }
通过架构来判断模拟器
isEmulatorFromCpu 1 2 3 4 5 6 private static boolean isEmulatorFromCpu () { ShellAdbUtils.CommandResult commandResult = ShellAdbUtils.execCommand("cat /proc/cpuinfo" , false ); String cpuInfo = commandResult == null ? "" : commandResult.successMsg; return !TextUtils.isEmpty(cpuInfo) && ((cpuInfo.toLowerCase().contains("intel" ) || cpuInfo.toLowerCase().contains("amd" ))); }
直接通过命令行获取cpu信息,如果是intel或者amd的直接杀死
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public static CommandResult execCommand (String[] commands, boolean isRoot, boolean isNeedResultMsg) { int result = -1 ; if (commands == null || commands.length == 0 ) { return new CommandResult (result, null , null ); } Process process = null ; BufferedReader successResult = null ; BufferedReader errorResult = null ; StringBuilder successMsg = null ; StringBuilder errorMsg = null ; DataOutputStream os = null ; try { process = Runtime.getRuntime().exec(isRoot ? COMMAND_SU : COMMAND_SH); os = new DataOutputStream (process.getOutputStream()); for (String command : commands) { if (command == null ) { continue ; } os.write(command.getBytes()); os.writeBytes(COMMAND_LINE_END); os.flush(); } os.writeBytes(COMMAND_EXIT); os.flush(); result = process.waitFor(); if (isNeedResultMsg) { successMsg = new StringBuilder (); errorMsg = new StringBuilder (); successResult = new BufferedReader (new InputStreamReader (process.getInputStream())); errorResult = new BufferedReader (new InputStreamReader (process.getErrorStream())); String s; while ((s = successResult.readLine()) != null ) { successMsg.append(s); } while ((s = errorResult.readLine()) != null ) { errorMsg.append(s); } } } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } finally { try { if (os != null ) { os.close(); } if (successResult != null ) { successResult.close(); } if (errorResult != null ) { errorResult.close(); } } catch (IOException e) { e.printStackTrace(); } if (process != null ) { process.destroy(); } } return new CommandResult (result, successMsg == null ? null : successMsg.toString(), errorMsg == null ? null : errorMsg.toString()); }
通过process = Runtime.getRuntime().exec(isRoot ? COMMAND_SU : COMMAND_SH);运行一个shell
通过 DataOutputStream os = new DataOutputStream(process.getOutputStream()); 获取到子进程的标准输入流
代码会遍历 commands 数组,将每一条命令写入这个流中
每条命令后都会写入一个换行符 COMMAND_LINE_END,这模拟了回车操作,是命令得以执行的前提
在所有命令都写入后,会写入 COMMAND_EXIT 命令。这会使 su 或 sh 进程正常退出,从而让 process.waitFor() 调用可以结束等待。如果没有这一步,程序会永久阻塞
result = process.waitFor();: 这行代码会阻塞当前线程,直到子进程执行完毕并返回退出码。
如果 isNeedResultMsg 为 true,代码会创建 BufferedReader 来分别读取子进程的标准输出流 (process.getInputStream()) 和标准错误流 (process.getErrorStream()),并将结果存入 StringBuilder
onCreate 1 2 3 4 5 6 7 8 binding.btnSycnInteger.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View view) { TextView textView = (TextView) findViewById(R.id.btn_sycn_integer); textView.setText("综合判断是否模拟器 " + EmulatorDetectUtil.isEmulatorFromAll(MainActivity.this )); } });
1 2 3 4 5 6 public static boolean isEmulatorFromAll (Context context) { return AndroidDeviceIMEIUtil.isRunOnEmulator(context) || detectS(); }
就是上述几个判断的合体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 binding.btnHwinfo.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View view) { requestGetInfo(); } }); @SuppressLint("SetTextI18n") public void requestGetInfo () { if (ActivityCompat.checkSelfPermission(MainActivity.this , Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(MainActivity.this , new String []{Manifest.permission.READ_PHONE_STATE}, 0 ); } else { renderHWInfo(); } }
首先会检测是否有PackageManager.PERMISSION_GRANTED权限
如果没权限会执行ActivityCompat.requestPermissions弹出一个对话框,请求用户添加权限
如果权限已经被授予,则会执行renderHWInfo();函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @SuppressLint("SetTextI18n") private void renderHWInfo () { String apideviceId = null ; try { apideviceId = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId(); } catch (Exception ignored) { } binding.tvDeviceid.setText( " \n \n设备信息 " + " \n\n最终方法获取IMEI : " + AndroidDeviceIMEIUtil.getDeviceId(mActivity) + " 是否有效 " + AndroidDeviceIMEIUtil.isValidIMEI(AndroidDeviceIMEIUtil.getDeviceId(mActivity)) + "\n最终方法获取MAC地址 : " + AndroidDeviceIMEIUtil.getMacAddress(mActivity) + " 是否有效 " + AndroidDeviceIMEIUtil.isValidAddress(MacAddressUtils.getMacAddress(mActivity)) + "\n最终方法获取AndroidID : " + AndroidDeviceIMEIUtil.getAndroidId(mActivity) + "\n最终方法获取序列号 : " + AndroidDeviceIMEIUtil.getSerialno() + "\n特征值检测是否模拟器 : " + EmuCheckUtil.mayOnEmulator(mActivity) + " \n\nIMEI详细信息: " + " \n\n可Hook系统API获取Deviceid: " + apideviceId + "\nProxy代理获取Deviceid level0 : " + IPhoneSubInfoUtil.getDeviceIdLevel0(mActivity) + "\nProxy代理获取Deviceid level1 :" + IPhoneSubInfoUtil.getDeviceIdLevel1(mActivity) + "\nProxy代理获取Deviceid level2 :" + IPhoneSubInfoUtil.getDeviceIdLevel2(mActivity) + "\nITelephonyUtil获取DeviceId level0: " + ITelephonyUtil.getDeviceIdLevel0(mActivity) + "\nITelephonyUtil获取DeviceId level1 : " + ITelephonyUtil.getDeviceIdLevel1(mActivity) + "\nITelephonyUtil获取DeviceId level2 :" + ITelephonyUtil.getDeviceIdLevel2(mActivity) ); binding.tvAndroididMacSerial.setText("\n\n序列号信息 :" + " \n\n系统API反射获取序列号 : " + SysAPIUtil.getSerialNumber(mActivity) + "\n系统API反射获取序列号 : " + SysAPIUtil.getJavaSerialNumber(mActivity) + "\n直接通过 Build Serial " + Build.SERIAL + "\n通过ADB Build Serial " + AndroidDeviceIMEIUtil.getSerialno() + "\n直接native获取 Serial " + PropertiesGet.getString("ro.serialno" ) + "\n\nMAC地址信息 :" + "\n\n通过系统API获取MAC地址 : " + SysAPIUtil.getMacAddress(mActivity) + "\nIwifmanager 获取mac level 0 : " + IWifiManagerUtil.getMacAddress(mActivity) + "\n通过NetworkInterface获取MAC地址 : " + MacAddressUtils.getMacAddressByWlan0(mActivity) + "\n通过ADB获取MAC地址 : " + MacAddressUtils.getMacInfoByAdb() + "\n\nAndroidID信息 :" + "\n\n通过系统API获取ANDROID_ID (XPOSED可以HOOK) : " + SysAPIUtil.getAndroidId(mActivity) + "\n反射获取系统 ANDROID_IDISettingUtils : " + ISettingUtils.getAndroidProperty(mActivity, Settings.Secure.ANDROID_ID) + "\n反射获取系统 ANDROID_ID ISettingUtils level2 : " + ISettingUtils.getAndroidPropertyLevel1(mActivity, Settings.Secure.ANDROID_ID) + "\n\n其他手机型号信息 :" + "\n系统API获取手机型号 (作假) : " + SysAPIUtil.getPhoneManufacturer() + "\nnative ro.product.manufacturer" + PropertiesGet.getString("ro.product.manufacturer" ) + "\nnative ro.product.model " + PropertiesGet.getString("ro.product.model" ) + "\nnative ro.product.device " + PropertiesGet.getString("ro.product.device" ) + "\nnative ro.product.name" + PropertiesGet.getString("ro.product.name" ) + "\n\n" + "\n系统架构 " + PropertiesGet.getString("ro.product.cpu.abi" ) + "\n获取链接的路由器地址 " + MacAddressUtils.getConnectedWifiMacAddress(getApplication()) ); }
deviceid获取 AndroidDeviceIMEIUtil.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 package com.snail.antifake.deviceid;import android.annotation.SuppressLint;import android.content.Context;import android.content.Intent;import android.content.IntentFilter;import android.os.Build;import android.telephony.TelephonyManager;import android.text.TextUtils;import com.snail.antifake.deviceid.androidid.IAndroidIdUtil;import com.snail.antifake.deviceid.deviceid.DeviceIdUtil;import com.snail.antifake.deviceid.emulator.EmuCheckUtil;import com.snail.antifake.deviceid.macaddress.MacAddressUtils;import com.snail.antifake.jni.PropertiesGet;public class AndroidDeviceIMEIUtil { public static boolean isRunOnEmulator (Context context) { return EmuCheckUtil.mayOnEmulator(context); } public static String getDeviceId (Context context) { return DeviceIdUtil.getDeviceId(context); } public static String getAndroidId (Context context) { return IAndroidIdUtil.getAndroidId(context); } public static String getMacAddress (Context context) { return MacAddressUtils.getMacAddress(context); } @SuppressLint("MissingPermission") public static String getSerialno () { String serialno = "" ; try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { serialno = Build.getSerial(); } else { serialno = PropertiesGet.getString("ro.serialno" ); if (TextUtils.isEmpty(serialno)) { serialno = Build.SERIAL; } } } catch (Exception e) { } return serialno; } public static String getManufacturer () { return PropertiesGet.getString("ro.product.manufacturer" ); } public static String getBrand () { return PropertiesGet.getString("ro.product.brand" ); } public static String getModel () { return PropertiesGet.getString("ro.product.model" ); } public static String getCpuAbi () { return PropertiesGet.getString("ro.product.cpu.abi" ); } public static String getDevice () { return PropertiesGet.getString("ro.product.device" ); } public static String getBoard () { return PropertiesGet.getString("ro.product.board" ); } public static String getHardware () { return PropertiesGet.getString("ro.hardware" ); } public static String getBootloader () { return PropertiesGet.getString("ro.bootloader" ); } @SuppressLint("MissingPermission") public static String getIMSI (Context context) { TelephonyManager telephonyManager = ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE)); return telephonyManager.getSubscriberId(); } public static BatteryChangeReceiver sBatteryChangeReceiver; public static void registerBatteryChangeListener (Context context) { if (sBatteryChangeReceiver == null ) { sBatteryChangeReceiver = new BatteryChangeReceiver (); IntentFilter filter = new IntentFilter (); filter.addAction(Intent.ACTION_BATTERY_CHANGED); context.registerReceiver(sBatteryChangeReceiver, filter); } } public static void unRegisterBatteryChangeListener (Context context) { if (sBatteryChangeReceiver == null ) { context.unregisterReceiver(sBatteryChangeReceiver); sBatteryChangeReceiver = null ; } } public static boolean isCharging () { return !(sBatteryChangeReceiver == null || sBatteryChangeReceiver.isCharging()); } public static int getCurrentBatteryLevel () { return sBatteryChangeReceiver != null ? sBatteryChangeReceiver.getCurrentLevel() : -1 ; } public static void getMac (IpScanner.OnScanListener listener) { IpScanner ipScanner = new IpScanner (); ipScanner.startScan(listener); } }
我们只看实现的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @SuppressLint("MissingPermission") public static String getSerialno () { String serialno = "" ; try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { serialno = Build.getSerial(); } else { serialno = PropertiesGet.getString("ro.serialno" ); if (TextUtils.isEmpty(serialno)) { serialno = Build.SERIAL; } } } catch (Exception e) { } return serialno; }
在 Android 8.0 (API 26) 及以上版本,使用Build.getSerial()(import android.os.Build;)获取设备的硬件序列号
在旧版本中,通过PropertiesGet.getString("ro.serialno")的JNI来读取系统属性,如果失败,则回退到使用 Build.SERIAL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public static String getManufacturer () { return PropertiesGet.getString("ro.product.manufacturer" ); } public static String getBrand () { return PropertiesGet.getString("ro.product.brand" ); } public static String getModel () { return PropertiesGet.getString("ro.product.model" ); } public static String getCpuAbi () { return PropertiesGet.getString("ro.product.cpu.abi" ); } public static String getDevice () { return PropertiesGet.getString("ro.product.device" ); } public static String getBoard () { return PropertiesGet.getString("ro.product.board" ); } public static String getHardware () { return PropertiesGet.getString("ro.hardware" ); } public static String getBootloader () { return PropertiesGet.getString("ro.bootloader" ); }
getManufacturer(): 获取设备制造商,如 “HUAWEI”, “Xiaomi”。
getBrand(): 获取设备品牌,如 “honor”, “Redmi”。
getModel(): 获取设备型号,如 “VOG-AL00”, “K30S Ultra”。
getCpuAbi(): 获取 CPU 的 ABI(应用二进制接口),如 “arm64-v8a”。这表明了 CPU 的架构。
getDevice(): 获取设备代号,如 “HWVOG”。
getBoard(): 获取主板名称,如 “goldfish”(常见于模拟器)。
getHardware(): 获取硬件名称,如 “kirin980”。
getBootloader(): 获取引导加载程序(Bootloader)的版本号。
1 2 3 4 5 6 @SuppressLint("MissingPermission") public static String getIMSI (Context context) { TelephonyManager telephonyManager = ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE)); return telephonyManager.getSubscriberId(); }
获取设备的 IMSI ,IMSI 是用于唯一识别移动网络中用户的编号,它存储在 SIM 卡中
但是从 Android 10 (API 29) 开始 ,普通应用调用 getSubscriberId() 将不再返回有效的 IMSI。如果应用不具备特殊的运营商权限,调用此方法会抛出 SecurityException。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static BatteryChangeReceiver sBatteryChangeReceiver;public static void registerBatteryChangeListener (Context context) { if (sBatteryChangeReceiver == null ) { sBatteryChangeReceiver = new BatteryChangeReceiver (); IntentFilter filter = new IntentFilter (); filter.addAction(Intent.ACTION_BATTERY_CHANGED); context.registerReceiver(sBatteryChangeReceiver, filter); } } public static void unRegisterBatteryChangeListener (Context context) { if (sBatteryChangeReceiver == null ) { context.unregisterReceiver(sBatteryChangeReceiver); sBatteryChangeReceiver = null ; } } public static boolean isCharging () { return !(sBatteryChangeReceiver == null || sBatteryChangeReceiver.isCharging()); } public static int getCurrentBatteryLevel () { return sBatteryChangeReceiver != null ? sBatteryChangeReceiver.getCurrentLevel() : -1 ; }
registerBatteryChangeListener注册一个BatteryChangeReceiver来监听系统的电池状态变化
所有方法位于BatteryChangeReceiver.java实现
unRegisterBatteryChangeListener注销之前注册的广播接收器
isCharging()获取当前是否在充电,通过sBatteryChangeReceiver.isCharging()
getCurrentBatteryLevel获取当前的电量,通过sBatteryChangeReceiver.getCurrentLevel()
1 2 3 4 public static void getMac (IpScanner.OnScanListener listener) { IpScanner ipScanner = new IpScanner (); ipScanner.startScan(listener); }
在IpScanner.java中实现,其中ipScanner.startScan(listener);会扫描局域网内的其他设备
BatteryChangeReceiver.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.snail.antifake.deviceid;import android.content.BroadcastReceiver;import android.content.Context;import android.content.Intent;import android.os.BatteryManager;public class BatteryChangeReceiver extends BroadcastReceiver { private boolean mIsCharging; private int mCurrentLevel; @Override public void onReceive (Context context, Intent intent) { int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, 0 ); mCurrentLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0 ); switch ( ) { case BatteryManager.BATTERY_STATUS_CHARGING: case BatteryManager.BATTERY_STATUS_FULL: case BatteryManager.BATTERY_STATUS_UNKNOWN: mIsCharging = true ; break ; case BatteryManager.BATTERY_STATUS_NOT_CHARGING: case BatteryManager.BATTERY_STATUS_DISCHARGING: mIsCharging = false ; break ; } } public boolean isCharging () { return mIsCharging; } public int getCurrentLevel () { return mCurrentLevel; } }
public class BatteryChangeReceiver extends BroadcastReceiver继承了BroadcastReceiver类,这意味着它具备了广播接收器的能力。
onReceive方法的调用时机:要让 onReceive 方法被调用,需要两个步骤:
创建广播接收器 :我们已经有了 BatteryChangeReceiver 这个类。
注册广播接收器 :我们需要告诉安卓系统,“嘿,我这里有一个接收器,它对某个特定的事件感兴趣。当这个事件发生时,请通知我。”
这个“注册”的动作发生在 AndroidDeviceIMEIUtil.java 文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static void registerBatteryChangeListener (Context context) { if (sBatteryChangeReceiver == null ) { sBatteryChangeReceiver = new BatteryChangeReceiver (); IntentFilter filter = new IntentFilter (); filter.addAction(Intent.ACTION_BATTERY_CHANGED); context.registerReceiver(sBatteryChangeReceiver, filter); } }
Intent.ACTION_BATTERY_CHANGED 是安卓系统预定义的一个广播动作(Action)
当系统检测到电池的状态发生了变化,比如:
开始充电(插上充电器)
停止充电(拔掉充电器)
电量百分比发生改变(例如从 81% 掉到 80%)
电池充满
电池温度变化等
系统就会发送一个带有 ACTION_BATTERY_CHANGED 动作的广播
由于我们的 BatteryChangeReceiver 已经注册监听了这个动作,所以安卓系统就会自动调用它的 onReceive 方法,并将包含详细信息的 Intent 对象传递进来
status也可以通过定义看到,其值是通过intent监听Broadcast获取的
或者说是BroadcastReceiver带给intent的值
不过也可知这是关于当前电池状态的变化的状态的
BatteryManager.BATTERY_STATUS_CHARGING:设备正在充电。
BatteryManager.BATTERY_STATUS_DISCHARGING:设备正在使用电池。
BatteryManager.BATTERY_STATUS_NOT_CHARGING:设备未在充电。
BatteryManager.BATTERY_STATUS_FULL:电池已充满。
BatteryManager.BATTERY_STATUS_UNKNOWN:电池状态未知。
如果设备正在充电、电池已充满或者电池状态未知,mIsCharging 标志被设置为 true。
如果状态是 BATTERY_STATUS_NOT_CHARGING 或 BATTERY_STATUS_DISCHARGING,mIsCharging 标志被设置为 false。
1 2 3 4 5 6 7 public boolean isCharging () { return mIsCharging; } public int getCurrentLevel () { return mCurrentLevel; }
BinderUtil.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.snail.antifake.deviceid;import android.os.RemoteException;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;public class BinderUtil { 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; } public static String getInterfaceDescriptor (Object proxy) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Method getInterfaceDescriptor = proxy.getClass().getDeclaredMethod("getInterfaceDescriptor" ); return (String) getInterfaceDescriptor.invoke(proxy); } }
这段代码是一个名为 BinderUtil 的工具类,这个工具类的核心目的是通过 Java 反射 技术,来获取 Android 系统底层 Binder 通信机制 中的一些关键信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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; }
在 Binder 通信中,每个可以被远程调用的方法都有一个唯一的整数 ID,称为 Transaction ID。当客户端发起调用时,并不会把方法名传给服务端,而是传递这个 Transaction ID。服务端根据这个 ID 来判断应该执行哪个具体的方法。 这些 ID 通常在编译时由 AIDL(Android 接口定义语言)工具自动生成,并作为 static final int 常量字段定义在 Stub 类中,例如 TRANSACTION_getDeviceId。
这个方法的作用就是通过反射,在运行时动态地获取这个 Transaction ID。
1 2 3 4 5 6 7 public static String getInterfaceDescriptor (Object proxy) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Method getInterfaceDescriptor = proxy.getClass().getDeclaredMethod("getInterfaceDescriptor" ); return (String) getInterfaceDescriptor.invoke(proxy); }
每个 Binder 服务都有一个唯一的字符串名称,称为“接口描述符”(Interface Descriptor)。它通常是接口的完整类名(例如 "android.telephony.ITelephony")。这个描述符用于在 Binder 通信的两端验证对方是不是自己想要通信的那个服务。 IBinder 接口本身就定义了 getInterfaceDescriptor() 方法,所有 Binder 对象都实现了它
这个工具方法的作用就是通过反射来调用 getInterfaceDescriptor() 方法
但是这里有个问题就是,这个项目本身在使用这个方法是通过:
1 2 3 4 5 Class Stub = Class.forName("com.android.internal.telephony.IPhoneSubInfo$Stub" );Method asInterface = Stub.getDeclaredMethod("asInterface" , IBinder.class);asInterface.setAccessible(true ); Object binderProxy = asInterface.invoke(null , binder);BinderUtil.getTransactionId(binderProxy, "TRANSACTION_getDeviceId" );
这样调用的,而如此调用会发生一个错误:
1 java.lang.NoSuchMethodException: com.android.internal.telephony.IPhoneSubInfo$Stub$Proxy.getInterfaceDescriptor []
原因是:对于这个Proxy来说,不存在getInterfaceDescriptor这个方法,getInterfaceDescriptor是在IBinder底层类就有实现的,而这个通过asInterface返回的Proxy代理类是由系统在编译时根据提供的AIDL文件生成的,它只包含AIDL中的业务方法而不会包含像getInterfaceDescriptor这种IBinder底层管理方法
因此要先获取其IBinder然后调用对应的方法:
1 2 3 4 5 6 7 8 public static String getInterfaceDescriptor (Object proxy) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, RemoteException { if (proxy instanceof IInterface) { return ((IInterface) proxy).asBinder().getInterfaceDescriptor(); } Method getInterfaceDescriptorMethod = proxy.getClass().getMethod("getInterfaceDescriptor" ); return (String) getInterfaceDescriptorMethod.invoke(proxy); }
CrashHandler.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.snail.antifake.deviceid;import android.app.ActivityManager;import android.app.Application;import android.content.Context;import android.os.Process;public class CrashHandler implements Thread .UncaughtExceptionHandler { private Application mApplication; public CrashHandler (Application application) { mApplication=application; } @Override public void uncaughtException (Thread thread, Throwable ex) { Process.killProcess( Process.myPid()); ActivityManager manager = (ActivityManager) mApplication.getSystemService(Context.ACTIVITY_SERVICE); for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) { if (processInfo.pid == Process.myPid()) { if (!mApplication.getPackageName().equals(processInfo.processName)) { Process.killProcess( Process.myPid()); } break ; } } } }
implements的作用是**声明一个类正在“实现”一个或多个接口 (Interface)**,可以一次声明多个接口
如果是一个普通类 (Concrete Class): 必须实现该接口及其所有父接口中定义的 所有抽象 (abstract) 方法。如果漏掉了任何一个,Java 编译器都会报错。
如果是一个抽象类 (Abstract Class): 不需要 实现所有方法。你可以选择实现一部分、或一个都不实现。
Thread.UncaughtExceptionHandler 是 Java 提供的一个接口。任何实现了这个接口的类都可以被注册为“默认的未捕获异常处理器”。当应用中的任何线程(包括主线程)抛出一个没有被 try-catch 语句捕获的异常时,JVM 就会自动调用这个处理器中的 uncaughtException 方法。
这个 CrashHandler 的设计目的并不仅仅是为了处理常规的程序崩溃,它还包含了一层基础的反注入和反调试保护 。
常规崩溃处理 :在任何未捕获异常发生时,它会确保应用进程被彻底终止,而不是依赖系统默认的崩溃对话框。
安全检测 :它在应用崩溃的瞬间,检查自身进程名是否与预期的包名一致。如果发现不一致,它会再次强制杀死进程。这可以防止在某些被注入或篡改的环境下,即使应用主逻辑崩溃,注入的代码仍然可能继续在应用的进程空间中运行。
IpScanner.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private String getHostIP () { String hostIp = null ; try { Enumeration nis = NetworkInterface.getNetworkInterfaces(); InetAddress ia; while (nis.hasMoreElements()) { NetworkInterface ni = (NetworkInterface) nis.nextElement(); Enumeration<InetAddress> ias = ni.getInetAddresses(); while (ias.hasMoreElements()) { ia = ias.nextElement(); if (ia instanceof Inet6Address) { continue ; } String ip = ia.getHostAddress(); if (!"127.0.0.1" .equals(ip)) { hostIp = ia.getHostAddress(); break ; } } } } catch (SocketException e) { Log.i("kalshen" , "SocketException" ); e.printStackTrace(); } return hostIp; }
Enumeration nis = NetworkInterface.getNetworkInterfaces();
这是获取网络信息的关键入口。NetworkInterface 类代表一个网络接口,如Wi-Fi网卡 (wlan0)、移动数据网络接口或以太网卡 (eth0)。
getNetworkInterfaces() 方法返回一个 Enumeration (枚举) 对象,其中包含了设备上所有可用的网络接口。
Enumeration<InetAddress> ias = ni.getInetAddresses();
对于每一个网络接口,调用 getInetAddresses() 方法。一个网络接口可能绑定了多个IP地址(例如,一个IPv4地址和一个或多个IPv6地址)。此方法返回包含所有这些地址的 Enumeration 对象
一旦找到了一个既不是IPv6也不是本地回环地址的IP,就将其赋值给 hostIp 变量,然后停止循环
但是这里的代码并不能确保获取的IPv4就是本机ip地址
可以进行改进:
intf.getName().equalsIgnoreCase("wlan0"): 明确指定查找名为 “wlan0” 的接口,这是绝大多数Android设备上Wi-Fi接口的名称。
intf.isUp(): 确保这个网络接口是“活跃”的。
!inetAddress.isLoopbackAddress(): 这是比 !"127.0.0.1".equals(ip) 更规范的判断回环地址的方法。
inetAddress instanceof java.net.Inet4Address: 这是比 !(ia instanceof Inet6Address) 更直接的判断IPv4的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public void startScan (final OnScanListener listener) { final List<String> ipList = new ArrayList <>(); final Map<String, String> map = new HashMap <>(); String hostIP = getHostIP(); if (TextUtils.isEmpty(hostIP)) return ; int lastIndexOf = hostIP.lastIndexOf("." ); final String substring = hostIP.substring(0 , lastIndexOf + 1 ); new Thread (new Runnable () { @Override public void run () { DatagramPacket dp = new DatagramPacket (new byte [0 ], 0 , 0 ); DatagramSocket socket; try { socket = new DatagramSocket (); int position = 2 ; while (position < 255 ) { Log.e("kalshen" , "run: udp-" + substring + position); dp.setAddress(InetAddress.getByName(substring + String.valueOf(position))); socket.send(dp); position++; if (position == 125 ) { socket.close(); socket = new DatagramSocket (); } } socket.close(); execCatForArp(listener); } catch (SocketException e) { e.printStackTrace(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }).start(); }
主要的逻辑发生在下方的run中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void run () { DatagramPacket dp = new DatagramPacket (new byte [0 ], 0 , 0 ); DatagramSocket socket; try { ... } catch (SocketException e) { e.printStackTrace(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }).start();
DatagramPacket dp = new DatagramPacket(new byte[0], 0, 0);
创建一个UDP数据包(DatagramPacket)。这个数据包的内容是空的(new byte[0]),因为发送数据本身不重要。重要的是发送这个动作本身
1 2 3 4 5 6 7 8 9 10 11 12 13 14 socket = new DatagramSocket (); int position = 2 ;while (position < 255 ) { Log.e("kalshen" , "run: udp-" + substring + position); dp.setAddress(InetAddress.getByName(substring + String.valueOf(position))); socket.send(dp); position++; if (position == 125 ) { socket.close(); socket = new DatagramSocket (); } } socket.close(); execCatForArp(listener);
position从2到254,避开网路地址(.0)、常见网关(.1)和广播地址(.255)
dp.setAddress(InetAddress.getByName(substring + String.valueOf(position)));
在循环中,为UDP包设置目标IP地址,例如 "192.168.1.2", "192.168.1.3", ...
socket.send(dp);
核心操作 。向目标IP发送这个空的UDP包。当操作系统执行这个发送命令时,它需要知道目标IP对应的MAC地址
如果操作系统的ARP缓存中没有这个IP的记录,它就会在局域网中广播一个ARP请求:“谁是 192.168.1.x?请告诉我你的MAC地址。”
如果网络中存在该IP的设备,它会响应这个ARP请求。
无论目标设备是否真的在监听UDP端口,只要它响应了ARP请求,它的IP-MAC映射关系就会被记录到本机的ARP缓存中。
**if (position == 125) { ... }**经验优化,分两段发包,可以学习!
最后执行execCatForArp函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 private void execCatForArp (final OnScanListener listener) { new Thread (new Runnable () { @Override public void run () { try { final Map<String, String> map = new HashMap <>(); Process exec = Runtime.getRuntime().exec("cat proc/net/arp" ); InputStream is = exec.getInputStream(); BufferedReader reader = new BufferedReader (new InputStreamReader (is)); String line; while ((line = reader.readLine()) != null ) { Log.e("kalshen" , "run: " + line); if (!line.contains("00:00:00:00:00:00" ) && !line.contains("IP" )) { String[] split = line.split("\\s+" ); map.put(split[3 ], split[0 ]); } } mHandler.post(new Runnable () { @Override public void run () { listener.scan(map); } }); } catch (IOException e) { e.printStackTrace(); } } }).start(); }
在完善完ARP表之后cat proc/net/arp
然后map.put(split[3], split[0]);将MAC地址和IP地址分别作为key和value存储在map中
1 2 3 4 5 6 7 mHandler.post(new Runnable () { @Override public void run () { listener.scan(map); } });
mHandler是通过private Handler mHandler = new Handler(Looper.getMainLooper());来获取主线程的Looper,然后将这个 Runnable 对象发送到 mHandler 所绑定的消息队列中,也就是主线程的消息队列
主线程有一个持续运行的 Looper,它会不断地从自己的消息队列中取出消息或 Runnable 来执行。当主线程的 Looper 处理到我们刚刚投递的这个 Runnable 对象时,它就会在主线程的环境下 调用该对象的 run() 方法
而这个scan方法,是在这个接口定义
1 2 3 public interface OnScanListener { void scan (Map<String, String> resultMap) ; }
在getMac中
1 2 3 4 public static void getMac (IpScanner.OnScanListener listener) { IpScanner ipScanner = new IpScanner (); ipScanner.startScan(listener); }
listener并没有被实现,因此这个listener的处理是通过用户外部传入的,处理的参数就是最后传出来的map结果
ShellAdbUtils.java ShellAdbUtils 类被设计为不可实例化,它只包含静态方法和静态常量,这是一个典型的工具类设计模式
这个类是一个用于在 Android 设备上执行 Shell 命令的工具类
它封装了通过 Runtime.getRuntime().exec() 执行命令的逻辑,并提供了检查 root 权限、以 root 或普通用户身份执行单个或多个命令的功能
androidid 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.snail.antifake.deviceid.androidid;import android.content.Context;import android.provider.Settings;import android.text.TextUtils;public class IAndroidIdUtil { public static String getAndroidId (Context context) { String androidId; if (!TextUtils.isEmpty(androidId = ISettingUtils.getAndroidPropertyLevel1(context, Settings.Secure.ANDROID_ID)) || !TextUtils.isEmpty(androidId = ISettingUtils.getAndroidProperty(context, Settings.Secure.ANDROID_ID))) { return androidId; } return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 public static String getAndroidPropertyLevel1 (Context context, String name) { ContentResolver resolver = context.getContentResolver(); try { Class mUserHandle = Class.forName("android.os.UserHandle" ); Method getUserId = mUserHandle.getDeclaredMethod("getUserId" , int .class); getUserId.setAccessible(true ); int uid = (int ) getUserId.invoke(null , Process.myUid()); HashSet<String> MOVED_TO_SECURE = new HashSet <>(); HashSet<String> MOVED_TO_LOCK_SETTINGS = new HashSet <>(); HashSet<String> MOVED_TO_GLOBAL = new HashSet <>(); try { Class Global = Class.forName("android.provider.Settings$Global" ); Field field = Global.getDeclaredField("MOVED_TO_SECURE" ); field.setAccessible(true ); MOVED_TO_SECURE = (HashSet<String>) field.get(Global); } catch (Exception e) { } try { Class Secure = Class.forName("android.provider.Settings$Secure" ); Field field = Secure.getDeclaredField("MOVED_TO_LOCK_SETTINGS" ); field.setAccessible(true ); MOVED_TO_LOCK_SETTINGS = (HashSet<String>) field.get(Secure); } catch (Exception e) { } try { Class Secure = Class.forName("android.provider.Settings$Secure" ); Field field = Secure.getDeclaredField("MOVED_TO_GLOBAL" ); field.setAccessible(true ); MOVED_TO_GLOBAL = (HashSet<String>) field.get(Secure); } catch (Exception e) { } if (MOVED_TO_SECURE.contains(name)) { } else if (MOVED_TO_GLOBAL.contains(name)) { Class mSecure = Class.forName("android.provider.Global" ); Method getStringForUser = mSecure.getDeclaredMethod("getStringForUser" , ContentResolver.class, String.class, int .class); getStringForUser.setAccessible(true ); return (String) getStringForUser.invoke(null , resolver, name, uid); } else if ((MOVED_TO_LOCK_SETTINGS.contains(name))) { Class ServiceManager = Class.forName("android.os.ServiceManager" ); Method getService = ServiceManager.getDeclaredMethod("getService" ); getService.setAccessible(true ); IBinder binder = (IBinder) getService.invoke(null , "lock_settings" ); Class Stub = Class.forName("com.android.internal.widget.ILockSettings$Stub" ); Method asInterface = Stub.getDeclaredMethod("asInterface" , IBinder.class); asInterface.setAccessible(true ); Object binderProxy = asInterface.invoke(null , binder); boolean sIsSystemProcess = Process.myUid() == Process.SYSTEM_UID; if (MOVED_TO_LOCK_SETTINGS.contains(name)) { if (binderProxy != null && !sIsSystemProcess) { Class proxy = binderProxy.getClass(); Method getString = proxy.getDeclaredMethod("getString" , String.class, String.class, int .class); return (String) getString.invoke(name, "0" , uid); } } } Class Secure = Class.forName("android.provider.Settings$Secure" ); Field field = Secure.getDeclaredField("sNameValueCache" ); field.setAccessible(true ); Object sNameValueCache = field.get(null ); Class NameValueCache = sNameValueCache.getClass(); Method getStringForUser = NameValueCache.getDeclaredMethod("getStringForUser" , ContentResolver.class, String.class, int .class); return (String) getStringForUser.invoke(sNameValueCache, resolver, name, uid); } catch (Exception e) { e.printStackTrace(); } return "" ;
通过大量的反射,调用很多安卓内部的方法和字段名
android.os.UserHandle类中的getUserId静态方法获取uid
android.provider.Settings$Global内部类中的MOVED_TO_SECURE字段
MOVED_TO_SECURE 是一个 private static final HashSet<String> 类型的字段。它的作用是记录那些从 Settings.Global 表迁移到 Settings.Secure 表的设置项的键名
当系统尝试访问一个曾经在 Global 中但现在已迁移的设置时,会检查这个 HashSet。如果设置项的键名存在于 MOVED_TO_SECURE 中,系统就会重定向到 Settings.Secure 去读取,并打印一条警告日志
总结来说,MOVED_TO_SECURE 字段是 Settings.Global 类内部用于处理设置项迁移的一个私有静态成员
如果当在MOVED_TO_SECURE这个字段中存在Settings.Secure.ANDROID_ID的话,那么就会调用android.provider.Settings 类中一个内部类 NameValueCache 的 getStringForUser 方法。这个方法是 Android 系统中从设置(Settings)数据库(如 System, Secure, Global 表)中读取一个特定键值(Setting)的核心逻辑
根据给定的设置名称(name)和用户句柄(userHandle),安全、高效地从内容提供者(Content Provider)中检索对应的字符串值
android.provider.Settings$Secure内部类中的MOVED_TO_LOCK_SETTINGS字段
如果该字段存在对应的键名,会先调用android.os.ServiceManager类中的getService静态方法,这个函数是 Android 框架中用于获取系统服务(System Service)的 IBinder 接口的核心方法,lock_settings 是 LockSettingsService 服务的注册名称。这个服务运行在 system_server 进程中,专门负责管理与设备锁屏安全相关的设置
之后通过asInterface返回的Proxy远程调用getString来获取对应字段的值
1 2 3 4 5 6 protected final LockSettingsStorage mStorage;@Override public String getString (String key, String defaultValue, int userId) { checkReadPermission(key, userId); return mStorage.getString(key, defaultValue, userId); }
android.provider.Settings$Secure内部类中的MOVED_TO_GLOBAL字段
1 2 3 4 Class mSecure = Class.forName("android.provider.Global" );Method getStringForUser = mSecure.getDeclaredMethod("getStringForUser" , ContentResolver.class, String.class, int .class);getStringForUser.setAccessible(true ); return (String) getStringForUser.invoke(null , resolver, name, uid);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public 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, BinderUtil.getInterfaceDescriptor(binderProxy), BinderUtil.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; }
ServiceManager 提供的 IBinder 和 ITelephony 的 asInterface 方法分别工作在 连接层 和 接口协议层 ,它们各司其职,共同完成了整个跨进程通信(IPC)的流程
deviceid 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @SuppressLint("MissingPermission") public static String getDeviceId (Context context) { String deviceId; if (!TextUtils.isEmpty(deviceId = ITelephonyUtil.getDeviceIdLevel2(context)) || !TextUtils.isEmpty(deviceId = IPhoneSubInfoUtil.getDeviceIdLevel2(context))) { return deviceId; } if (!TextUtils.isEmpty(deviceId = ITelephonyUtil.getDeviceIdLevel1(context)) || !TextUtils.isEmpty(deviceId = IPhoneSubInfoUtil.getDeviceIdLevel1(context))) { return deviceId; } if (!TextUtils.isEmpty(deviceId = IPhoneSubInfoUtil.getDeviceIdLevel0(context)) || !TextUtils.isEmpty(deviceId = ITelephonyUtil.getDeviceIdLevel0(context))) { return deviceId; } try { TelephonyManager telephonyManager = ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE)); deviceId =telephonyManager.getDeviceId(); }catch (Exception ignore){ } return deviceId; }
该方法按照从最难 Hook 到最容易 Hook 的顺序
Level2 第一个先从ITelephonyUtil.getDeviceIdLevel2(context)
IPhoneSubInfoUtil.getDeviceIdLevel2(context)这两个函数获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public 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 , "iphonesubinfo" ); Class Stub = Class.forName("com.android.internal.telephony.IPhoneSubInfo$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, BinderUtil.getInterfaceDescriptor(binderProxy), BinderUtil.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; }
1 2 3 4 Class ServiceManager = Class.forName("android.os.ServiceManager" );Method getService = ServiceManager.getDeclaredMethod("getService" , String.class);getService.setAccessible(true ); IBinder binder = (IBinder) getService.invoke(null , "iphonesubinfo" );
这里获取的是IBinder对象,是android.os.BinderProxy类
通过asInterface调用转换成...IPhoneSubInfo$Stub$Proxy类
简单来说,asInterface 就是一个适配器(Adapter)或者工厂方法 ,它将一个通用的、底层的 BinderProxy 对象,转换成了一个专用的、易于使用的服务接口代理$Stub$Proxy对象
1 2 3 4 5 Method getDeviceId = binderProxy.getClass().getDeclaredMethod("getDeviceId" , String.class);if (getDeviceId != null ) { deviceId = binderGetHardwareInfo(context.getPackageName(), binder, BinderUtil.getInterfaceDescriptor(binderProxy), BinderUtil.getTransactionId(binderProxy, "TRANSACTION_getDeviceId" ));
而真正的核心实现在于binderGetHardwareInfo,模拟了底层的IPC交互
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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; }
1 2 android.os.Parcel _data = android.os.Parcel.obtain(); android.os.Parcel _reply = android.os.Parcel.obtain();
首先获取两个Parcel对象
Parcel 是 Android 中用于在进程间传递数据的高性能容器。
_data: 用来打包所有需要发送给 远程服务的数据,包括要调用的接口信息和方法参数
_reply: 用来接收远程服务返回的 数据,包括返回值和可能的异常信息
obtain(): 从一个全局池中获取可复用的 Parcel 对象,以提高性能,避免频繁创建和销毁对象
然后写入接口描述符 (writeInterfaceToken),_data.writeInterfaceToken(DESCRIPTOR)
其中DESCRIPTOR是唯一标识字符串
代表了要调用的接口(例如"com.android.internal.telephony.IPhoneSubInfo"),在 _data 包的最开始写入这个“令牌 ”,远程服务在收到数据后会首先读取并验证它,确保客户端请求的是正确的服务接口,防止调用混乱
之后写入方法参数
1 2 3 if (!TextUtils.isEmpty(callingPackage)) { _data.writeString(callingPackage); }
这里将 getDeviceId 方法所需的参数写入 _data 包,此时参数是通过context.getPackageName()获取,即是调用方的包名
如果 callingPackage 不为空,就将其写入 _data,对应 getDeviceId(String pkg) 这种方法签名。
如果 callingPackage 为空,就不写入任何东西,对应 getDeviceId() 这种无参的方法签名。
发起远程事务 (transact)
remote.transact(tid, _data, _reply, 0);
这是整个过程的核心,它真正地发起了跨进程调用。
remote: 这是从 ServiceManager 获取到的原始 IBinder 对象(一个 BinderProxy 实例),代表了通往远程服务的连接通道。
tid参数就是TransactionId(事务id),然后_data包含接口令牌和参数的输入数据包,_reply用于接收返回结果的空数据包,0是事务标志位,0 表示这是一次标准的同步调用,方法会阻塞直到远程服务处理完毕并返回结果
1 2 3 4 5 6 _reply.readException(); _result = _reply.readString(); } finally { _reply.recycle(); _data.recycle(); }
最后就处理返回结果和回收Parcel对象了
但是从 Android 10 (API 29) 开始,获取是可以正常获取,但是就无法正常使用了
1 TelephonyPermissions com.android.phone W reportAccessDeniedToReadIdentifiers:com.sheep.anti:getDeviceId:-1
会报错误显示没权限访问
Level1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public static String getDeviceIdLevel1 (Context context) { try { Class ServiceManager = Class.forName("android.os.ServiceManager" ); Method getService = ServiceManager.getDeclaredMethod("getService" , String.class); getService.setAccessible(true ); IBinder binder = (IBinder) getService.invoke(null , "iphonesubinfo" ); Class Stub = Class.forName("com.android.internal.telephony.IPhoneSubInfo$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 ) { return (String) getDeviceId.invoke(binderProxy, context.getPackageName()); } } catch (Exception e) { } Method getDeviceId = binderProxy.getClass().getDeclaredMethod("getDeviceId" ); if (getDeviceId != null ) { return (String) getDeviceId.invoke(binderProxy); } } catch (Exception e) { } return "" ; }
就是直接调用com.android.internal.telephony.IPhoneSubInfo$Stub$Proxy->getDeviceId方法
Level0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static String getDeviceIdLevel0 (Context context) { TelephonyManager telephonyManager = ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE)); try { Method method = TelephonyManager.class.getDeclaredMethod("getSubscriberInfo" ); method.setAccessible(true ); Object binderProxy = method.invoke(telephonyManager); try { Method getDeviceId = binderProxy.getClass().getDeclaredMethod("getDeviceId" , String.class); if (getDeviceId != null ) { return (String) getDeviceId.invoke(binderProxy, context.getPackageName()); } } catch (Exception e) { } Method getDeviceId = binderProxy.getClass().getDeclaredMethod("getDeviceId" ); if (getDeviceId != null ) { return (String) getDeviceId.invoke(binderProxy); } } catch (Exception e) { } return "" ; }
通过getSubscriberInfo获取其代理对象,剩下步骤都差不多了