前言:https://bbs.kanxue.com/thread-273759.htm
现在大厂的设备指纹层出不穷,但是想要确保稳定性和唯一性高精准其实也挺难的一件事,有的是通过设备信息比重进行的设备ID唯一值确认。比如A设备信息占比10%,B设备信息占比20%,当比重超过60%以上,设备指纹才会发生变化。这样的好处就是当你只修改某一个字段的时候,设备指纹不发生变化。还有的干脆找一个隐蔽的并且唯一的设备信息,作为缓存,每次读取缓存的方式去判断,设备信息是唯一,比如常见的有Native获取DRM,popen cat /sys/devices/soc0/serial_number ,svc读取bootid并且保存到文件,netlinker获取网卡。都是很常见并且隐蔽的的设备指纹。这篇文章主要介绍了各种指纹的获取情况,如何修改,站在上帝视角去俯看,攻击者和被攻击者遇到的问题 。
看完这篇文章主要你能学到如下:
1,常用的指纹检测有哪些?
2,如何修改设备指纹,难点在哪里,会有哪些坑需要踩?
3,现在的国内的指纹面临哪些问题?
前奏知识:
Android IPC代理是什么?
Android本身是CS架构,客户端(client)服务端(server),我们常用的通过context上下文调用的API都是直接调用代理人的方式去调用的,而真正的服务端是ActivityManagerServer 简称,AMS, 他有很多代理,比如PackageManager,ActivityManager 等,这些都是AMS的代理人。而AMS就是被代理人。代理模式是一种设计模式,代理人可以提供被代理人的部分或者全部功能,实现代码封装,做鉴权,代码安全的角度,代理模式很常用的设计模式。
AMS和代理们通过Binder进行通讯,Binder是什么,有什么好处这里就不详细展开了,安卓面试八股文,可以理解成进程间通讯的东西,底层实现是通过共享内存,数据传输,读取速度更快。当我们调用代理人的API得时候,本质上是通过Binder去发送一些数据包,和AMS通讯,当AMS收到消息以后把结果在传输给对应的代理人。然后返回给调用方。在每个Manager里面都有一个代理人 。
之前很久之前有一种动态代理的技术,原理就是替换了里面的代理人,因为代理是一个接口,然后我们自己通过Proxy这个类创建一个代理,然后反射set回去,就可以实现常用的API拦截和Hook。类似VA的沙盒,对多开的App提供一份自己实现的代理,然后控制这些代理的返回值,以此实现沙盒相关操作。还有一种比较好的过APK签名的方法就是直接Hook"水管" 也就是hook binder的通讯的方法,当接收到指定事件以后,直接修改具体的结果,以此对Java层进行全量Hook(binder的通讯方法被Hook以后,调用者和代理人只能拿到被修改以后的结果,以此实现Java层的全量Hook,后面再讲签名验证的时候我在详细说。)
如何绕过9.0隐藏API限制?
因为在获取或者分析的时候,需要绕过反射9.0限制,这里采用的是Lsp作者的AndroidHiddenApiBypass进行隐藏API的调用 ,
在手机装了EDXP或者Magisk,以后一般隐藏API的限制都是默认去除的。因为在Edxp和Magisk里面也需要进行使用一些隐藏API。
设备指纹:
设备指纹主要分为三部分,Java层设备指纹,Native设备指纹,popen执行一些命令获取设备信息,包括一些核心的设备指纹。
这篇文章也主要围绕这三部分进行展开讨论。比如一些内核文件等信息,我也会放在Native层进行讨论。
所有的每个设备指纹我都分为两部分
Get(如何获取,站在开发者角度) Mock (如何进行修改测试,站在攻击者角度)
所有的指纹我都分为三种类型,普通指纹,次要指纹,重要指纹。
Java层设备指纹:
Setting相关(重要):
Get:
在setting里面大家经常遇到的可能就是android id的获取的
API如下:
Settings.Secure.getString(context.getContentResolver(),Settings.Secure.ANDROID_ID)
但是其实Setting里面还有很多别的功能东西,常见的就是Settings.Secure 和 Settings.Global
在Settings.Global 里面其实还有一些别的字段,具体API如下。这些都是一些比较隐蔽的设备指纹。
Settings.Global.getString(context.getContentResolver(),"mi_health_id")
Settings.Global.getString(context.getContentResolver(),"mi_health_id")
Settings.Global.getString(context.getContentResolver(),"gcbooster_uuid")
Settings.Global.getString(context.getContentResolver(),"key_mqs_uuid")
Settings.Global.getString(context.getContentResolver(),"ad_aaid")
Mock:
方法Hook
Global和Secure 都是实现的NameValueTable接口。
public static String getString(ContentResolver resolver, String name) {
return getStringForUser(resolver, name, resolver.getUserId());
}
底层调用的是getStringForUser(resolver, name, resolver.getUserId()) 三个参数,如果Hook的话可以对这个方法进行入手。
Settings.Secure->getStringForUser & Settings.Global ->getStringForUser
内存反射:
很多开发者会采用内存反射的方式去获取变量,所以仅仅是通过mock方法的方式不够,如果进行Mock需要将Settings.Secure 和 Settings.Global 里面的内存变量进行修复,Settings.Global是放了一些全局变量,Settings.Secure放一些安全相关,
Settings.Secure->getStringForUser Settings.Global ->getStringForUser 和 具体方法如下。
private static final HashSet
MOVED_TO_GLOBAL;
private static final NameValueCache sNameValueCache = new NameValueCache(
CONTENT_URI,
CALL_METHOD_GET_SYSTEM,
CALL_METHOD_PUT_SYSTEM,
sProviderHolder,
System.class);
public static String getStringForUser(ContentResolver resolver, String name,
int userHandle) {
if (MOVED_TO_GLOBAL.contains(name)) {
Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.Secure"
+ " to android.provider.Settings.Global.");
return Global.getStringForUser(resolver, name, userHandle);
}
...
return sNameValueCache.getStringForUser(resolver, name, userHandle);
}
@UnsupportedAppUsage
public static String getStringForUser(ContentResolver resolver, String name,
int userHandle) {
if (MOVED_TO_SECURE.contains(name)) {
Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.System"
+ " to android.provider.Settings.Secure, returning read-only value.");
return Secure.getStringForUser(resolver, name, userHandle);
}
if (MOVED_TO_GLOBAL.contains(name) || MOVED_TO_SECURE_THEN_GLOBAL.contains(name)) {
Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.System"
+ " to android.provider.Settings.Global, returning read-only value.");
return Global.getStringForUser(resolver, name, userHandle);
}
}
可以看到,整体的cache都是放在sNameValueCache变量和MOVED_TO_GLOBAL变量内部进行存储 。
我们可以直接反射MOVED_TO_GLOBAL这个HashSet或者去sNameValueCache 这个变量然后去获取这个值的话,也是很容易可以拿到最真实的值的。所以光mock是不够的。
比如很多大厂就是Android高版本绕过了反射限制以后,或者判断当前手机没有API反射限制以后直接通过反射变量的方式去获取。
sNameValueCache在高版本是一个对象,低版本安卓他是一个ArrayMap 这块需要注意。
sNameValueCache修改的话可以调用API putStringForUser 往里面强制赋值 。这么一来下次对方在通过API去调用的时候就会拿到你已经进行过Mock的值。所以你修改的时候需要进行判断,当前获取的值是否是你已经Mock过的。
蓝牙网卡MAC(普通):
蓝牙的网卡不是普通的网卡,后面会介绍netlinker获取真实的网卡。
Get:
主要方法就是通过BluetoothAdapter->getAddress
public String getAddress() {
try {
return mManagerService.getAddress(mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
return null;
}
Mock:
可以看到这个方法主要是通过IPC的代理类方式去获取的。所以Hook的话尽可能先Hook代理的IPC类。先尝试反射 android.bluetooth.IBluetooth$Stub$Proxy然后Hook IPC里面的getAddress 而不是直接HookBluetoothAdapter->getAddress
因为很多大厂获取设备的指纹的时候会检测这个方法是否被Hook,检测也很简单,只需要获取这个artmethod结构体以后
判断这个方法入口是否被替换,比如Sandhook之类的常用的Hook框架,低版本采用的是inlinehook形式,在高版本里面采用的是入口替换,可以直接获取到方法的入口的函数地址,判断一下函数所在的so即可。所以尽可能HookIPC的方法。如果用XPosed去修改的话,还需要注意魔改,否则大厂会通过XposedHelpers->sHookedMethodCallbacks变量把你Hook的方法进行上报。
小技巧:这个变量是一个静态变量,所以我们只需要拿到XposedHelpers这个class即可。想要拿到class必须先拿到这个类的classloader,正常的Xposed是通过系统的classloader作为父类classloader,但是edxp这种,是一个方法内部的成员变量,没有任何地方引用这个classloader,所以想拿到这个classloader需要用到内存漫游。把内存全部的classloader都从内存抠出来,然后挨个去反射获取XposedHelpers 即可。代码可参考如下:
https://bbs.pediy.com/thread-269094.htm
sHookedMethodCallbacks里面保存了XPosed全部的Hook方法信息,用于石锤当前方法是否被Hook。获取被Hook方法具体如下:
XposedHelpers->methodCache 不建议使用,如果攻击者使用了XposedBridge->HookAllmethod 的话,可能会导致Hook方法上报的遗漏。
private void getHookItemDemo() {
ArrayList
留言
張貼留言