现在大厂的设备指纹层出不穷,但是想要确保 稳定性 和 唯一性 高精准其实也挺难的一件事,有的是通过设备信息比重进行的设备ID唯一值确认。比如A设备信息占比10%,B设备信息占比20%,当比重超过60%以上,设备指纹才会发生变化。这样的好处就是当你只修改某一个字段的时候,设备指纹不发生变化。还有的干脆找一个隐蔽的并且唯一的设备信息,作为缓存,每次读取缓存的方式去判断,设备信息是 唯一,比如常见的有Native获取DRM,popen cat /sys/devices/soc0/serial_number ,svc读取bootid并且保存到文件,netlinker获取网卡。都是很常见并且隐蔽的的设备指纹。这篇文章主要介绍了各种指纹的获取情况,如何修改,站在上帝视角去俯看,攻击者和被攻击者遇到的问题 。
看完这篇文章主要你能学到如下:
1,常用的指纹检测有哪些?
2,如何修改设备指纹,难点在哪里,会有哪些坑需要踩?
3,现在的国内的指纹面临哪些问题?
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限制,这里采用的是Lsp作者的 AndroidHiddenApiBypass 进行隐藏API的调用 ,
在手机装了EDXP或者Magisk,以后一般隐藏API的限制都是默认去除的。因为在Edxp和Magisk里面也需要进行使用一些隐藏API。
设备指纹主要分为三部分,Java层设备指纹,Native设备指纹,popen执行一些命令获取设备信息,包括一些核心的设备指纹。
这篇文章也主要围绕这三部分进行展开讨论。比如一些内核文件等信息,我也会放在Native层进行讨论。
所有的每个设备指纹我都分为两部分
Get(如何获取,站在开发者角度) Mock (如何进行修改测试,站在攻击者角度)
所有的指纹我都分为三种类型, 普通指纹,次要指纹,重要指纹。
在setting里面大家经常遇到的可能就是android id的获取的
API如下:
但是其实Setting里面还有很多别的功能东西,常见的就是Settings.Secure 和 Settings.Global
在Settings.Global 里面其实还有一些别的字段,具体API如下。这些都是一些比较隐蔽的设备指纹。
方法Hook
Global和Secure 都是实现的NameValueTable接口。
底层调用的是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 和 具体方法如下。
可以看到, 整体的cache都是放在sNameValueCache变量和MOVED_TO_GLOBAL变量内部进行存储 。
我们可以直接反射MOVED_TO_GLOBAL这个HashSet或者去sNameValueCache 这个变量然后去获取这个值的话,也是很容易可以拿到最真实的值的。所以光mock是不够的。
比如很多大厂就是 Android高版本绕过了反射限制以后,或者判断当前手机没有API反射限制以后直接通过反射变量的方式去获取。
sNameValueCache在高版本是一个对象,低版本安卓他是一个ArrayMap 这块需要注意。
sNameValueCache修改的话可以调用API putStringForUser 往里面强制赋值 。这么一来下次对方在通过API去调用的时候就会拿到你已经进行过Mock的值。所以你修改的时候需要进行判断,当前获取的值是否是你已经Mock过的。
蓝牙的网卡不是普通的网卡,后面会介绍netlinker获取真实的网卡。
主要方法就是通过BluetoothAdapter->getAddress
可以看到这个方法主要是通过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方法上报的遗漏。
这个变量在高版本里面基本已经拿不到,及时拿到了也是一个unknow,但是也需要兼容低版本的Android 。
如果返回的不是空,并且不是 unknow 或者UNKNOWN,随机一份原始长度的字符串即可。另外该字段同上,也可以直接对IPC类进行处理,直接Hook IPC对象getSerialForPackage 方法即可 ,的实现方法具体如下 。
这些基础的Java设备指纹字段没啥好说的,百度一下就能找到具体的获取方法,但是修改的时候需要注意,不要直接Hook,尝试优先Hook ipc即可 。
Build里面还是有很多有用的东西,比如手机是否开启adb ,usb接口的状态之类的。我们主要将Build里面分为两部分 。指纹相关又分为两部分,单一字段 /复合字段。
1,配置相关
2,指纹相关
单一字段(只有一个设备信息)
复合字段(多个单一字段复合而成)
这个单独通过Java层去修改是完全不够的,底层走的是system_property_get 这个方法(在native指纹部分会详细介绍)。
还有要防止popen getprop 这种方法去扫描全部的Build相关参数(popen getprop 在popen相关会详细介绍,这里只介绍Java应该如何处理),这个Build相关需要重点关注,他在 Android底层实现类似树状结构。也就是说很多树枝都会有相同的内容。目前所有的作用域一共有七种。
举个例子,比如常见的fingerprint复合字段系列 ,就分为如下七种作用域。
ro.build.fingerprint/
ro.build.build.fingerprint/
ro.bootimage.build.fingerprint/
ro.odm.build.fingerprint/
ro.product.build.fingerprint/
ro.system_ext.build.fingerprint/
ro.system.build.fingerprint/
ro.vendor.build.fingerprint/
作用域分别如下
这里面的值构成顺序也都是一样,所以Hook的话也需要全部进行hook,只处理单一是没用的。因为很多大厂做采集,不会只收集一项。
会七个作用域都进行收集。
常见的配置如下,这些字段其实修改不修改不重要,因为很多大厂如果手机开了开发者选项或者debug模式之类的。
会增加当前手机的风险值。 所以尝试进行Mock 和修改 。
在不修改机型的前提下,下面这些应该都是需要处理的 。大厂扫描频率很高的Build参数 ,随机的话,在原有的基础上开头或者结尾,随机几位数即可。
复合字段是多个单一字段拼成的字段,常用的有ro.build.description 还有之前说的7个fingerprint 相关。 这些Mock以后的值要和之前单一字段Mock的值对等。比如某个单一字段值被mock成A 以后,复合字段里面的内也应该是A 。
android.os.SystemProperties->get 底层调用的是native_get ,一个native方法,所以Hook的时候优先处理 native_get
Java hook完毕以后 还需要反射将Build里面的成员变量进行set。防止采集通过反射的方式去获取
很多大厂会把这个字段也作为指纹的一部分,所以这个方法也需要处理。
优先Hook ipc
优先Hook ipc
这个函数不需要太多处理,每个手机类型基本都差不多,每次打乱一下返回结果排序顺序即可。
优先Hook ipc
这个DRM是水印相关,主要为了处理不同手机加水印的唯一ID 核心的是一个叫deviceUniqueId 的东西,这玩意是一个随机的32位字节数组。很多大厂用这个作为核心的设备指纹,不仅在Java层进行获取,还有在Native层进行获取,在后面Native设备指纹会再次介绍到。
Hook的话很简单,这个方法没有IPC底层有自己的实现,直接Hook get的方法即可 。java层Hook是远远不够的,还需要处理native层。
每次随机32位字节数组即可。
大厂应该不会信任Java层的mac,底层都是通过netlinker直接获取网卡,或者直接popen执行 ip a 进行网卡信息的全量获取(详细参考后面popen相关介绍)。我直接在底层处理的netlinker socket通讯的时候,所以Java层不进行处理。任何获取网卡的方法,底层最终走的都是netlinker去获取的网卡
直接通过netlinker获取网卡,这种方式在安卓10上面貌似已经失效了,但是手机Root以后是没有限制的(亲测android 13 开发板获取成功),这种方式还可以用来检测当前手机是否Root。
但是当执行ip a这种命令的时候,或者调用Java层原始API的时候,底层还是走的netlinker,直接在底层通过ptrace在函数调用执行完毕以后,对寄存器进行Mock 和 Set即可 。详细获取方式可以参考我之前的帖子 。 Android netlink&svc 获取 Mac方法深入分析
很多大厂会收集/sdcard/ 或者相册目录的一些创建时间,作为设备指纹,但是很多文件都是默认的1970时间戳,有的少数文件夹创建时间也是很重要的设备标识 。Java里面File对象有文件的创建时间。
聊了挺多Java相关的设备指纹,其实Java层采集的指纹,并不是关键因素,核心的指纹基本都在native层进行处理的。Native部分会详细介绍包括内核文件,还有一些获取指纹的骚操作 。
之前在Java层介绍了,Java获取最终总的是native_get,而native_get底层走的就是这个system_property_get 。
在介绍之前我们需要先看看这个函数的源码,android 9以上和9以下实现的方式是不同的。
android 9:
android 9以下 :
安卓9以下是直接实现的这个方法,所以这块又有个细节,android 9 以上 hook __system_property_get 不仅仅需要Hook
入口方法,还需要Hook system_properties.Get 这个方法。
很多大厂在android 9以上会直接调用system_properties.Get ,先解析So获取到system_properties.Get 非导出函数的函数指针,强转成函数指针以后,直接去调用system_properties.Get ,而非直接调用 system_property_get ,如果只Hook system_property_get的话可能就会导致指纹泄漏。所以在android 9以上需要额外处理 system_properties.Get(name, value); 这个方法。
如果直接Hook __system_property_get 可能会存在短指令问题。因为这个方法就一个BL指令,普通的inlinehook 可能会失效。
这块需要用到异常Hook 。当然也可以直接判断安卓版本号在9.0以上直接Hook system_properties.Get 即可。这个system_properties.Get 是一个非导出函数,需要解析So获取到非导出函数的地址。可以参考sandhook的ELFUtils.cpp 。
同理read方法也是如此,也需要这么处理 ,在9.0以上需要特殊处理 。
Hook的时候需要注意一件事就是Mock的值长度不能大于原始长度。当 system_property_get 执行完毕以后memcpy 将Mock的value拷贝进去即可 。处理的过程函数如下 。实现也很简单。
这块有个细节问题:
因为get和read底层走的都是find函数,为什么不直接在find函数处理呢,find函数返回的是prop_info*
这个指针指向的是系统内存的变量,直接写入会直接sign11 如果使用mprotect如果直接对内存变量强制写入可能会导致系统的不稳定,导致出现问题。之前踩过这个坑。所以就只处理了get和find这两个函数 。
使用的话很简单,直接导入头文件就好。
这个指纹也是很多大厂用作唯一ID的核心指纹。处理的话也需要注意,很核心的一个设备指纹ID。
使用的话很简单,直接导入头文件就好。代码不超过10行 。
导入的头文件实现这个So在mediandk.so里面 ,所以cmake->target_link_libraries引入的时候别忘记添加mediandk 引入依赖。
这个值不同App 读取的内容都不一样,这块需要注意。
Hook的话也很简单,直接Hook这个函数地址就行,但是这个方法也是一个短指令,需要用到异常Hook。
处理逻辑如下,因为我们只需要关注description 即可。其他内容不处理。这块有时候直接写入可能会导致问题,需要先mprotect,不能直接用mprotect需要计算一下扇叶大小,是否内存对齐。
因为之前说过,Linux底层不管什么样的获取网卡,最终底层直接会走Netlinker去获取网卡。在android 10以下可以绕过系统权限从而获取网卡信息,高版本已经失效了 。底层都是svc直接调用recvfrom或者recvmsg去接受socket的消息 。所以不处理svc的话,无法做到全量修改的。
我用的是ptrace 在recvfrom 执行完毕以后,读取参数寄存器,将数据修改以后在重新覆盖寄存器即可 。 处理过程如下。
细节点:
socket主要接受消息的函数主要就三个,recvfrom,recvmsg,recv ,netlinker通讯就是通过这三个函数处理的,recv底层调用的是recvfrom ,所以我们只需要处理recvfrom,和 recvmsg 即可。recvfrom执行完毕以后参数是个数组,我们只需要把这个数组buff的值进行覆盖即可,但是recvmsg的话不能这么处理,他的参数是iovec 指针,这个东西大家可以理解成一个箱子。里面装了具体的内容,长度和开始位置 。所以修改的时候需要读取这个开始位置的指针才可以进行set。
netlinker获取的Mac方式详细看我之前的这篇帖子,这里不多重复介绍了 。
Android netlink&svc 获取 Mac方法深入分析
如何使用使用ptrace修改svc可以参考我的另一篇文章。
SVC的TraceHook沙箱的实现&无痕Hook实现思路
上述方法处理完毕以后,哪怕就是执行系统的ip -a命令,拿到的也是被修改以后的值。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2022-7-21 19:06
被珍惜Any编辑
,原因: 格式修改