设备指纹的本质:稳定 + 唯一的"设备画像"
风控 SDK(数美、顶象、同盾、腾讯防水墙)的核心产品是设备指纹——给每台设备算一个唯一 ID,让"换账号不换设备"的批量行为无所遁形。理解它的采集清单,才知道改机要改到什么程度。
设计约束决定采集思路:单个字段都不够唯一(机型一样的手机千千万),所以采集 30+ 个字段做联合哈希。字段选择遵循两个原则:稳定性(不能重启就变)和区分度(不同设备尽量不同)。
采集清单分类拆解
硬件层(区分度高,改机难度大):
CPU 型号/核心数/频率 /proc/cpuinfo
内存大小 /proc/meminfo
屏幕分辨率 + density DisplayMetrics
基带版本 Build.RADIO
主板/硬件平台 Build.BOARD / HARDWARE
电池容量/健康度 BatteryManager
传感器清单 SensorManager.getSensorList()系统层(改机工具的主战场):
Android ID / IMEI / MAC / 序列号
系统版本 / SDK 版本 / 安全补丁日期
Build.FINGERPRINT(编译指纹串)
开机时长 / 系统时间 / 时区
语言 / 输入法列表环境层(检测模拟器/root 的探针):
root 特征文件(/system/bin/su 等)
模拟器特征(qemu 文件、goldfish 驱动)
调试状态(Debugger.isConnected)
VPN/代理状态
已安装应用列表(部分 SDK 采集!)网络层:
IP / 网关 / DNS
WiFi SSID / BSSID(需要权限)
基站信息(需要权限)指纹生成:从字段到 deviceId
采集完不是简单拼接——各家有自己的加权算法:
# 通用结构(简化示意)
def gen_device_id(fields):
# 1. 字段分级:强标识(IMEI/AndroidID)+ 弱标识(机型/分辨率)
strong = fields['imei'] or fields['android_id']
weak = hash(fields['model'] + fields['resolution'] + fields['cpu'])
# 2. 联合哈希
device_id = md5(f"{strong}|{weak}|{salt}")
# 3. 服务端还有一层:相似度匹配(模糊指纹)
return device_id服务端做的是相似度匹配而非精确匹配——这就是"改一个字段没用"的原因:你改了 IMEI,但机型+分辨率+传感器清单+基站信息的组合没变,相似度 90%+,照样关联到原设备。
精髓一:字段的"权重"不一样
对抗设备指纹,先搞清楚哪些字段是高权重锚点:
- 传感器清单:不同机型差异巨大且几乎没人改——权重极高
- 基带版本/编译指纹:和机型强绑定,改机工具常改得不一致——一致性校验的重点
- 电池信息:模拟器/云手机的电池数据模式化(永远满电、温度恒定)——行为级破绽
改机对抗的正确姿势是以真实设备档案为模板整套克隆(前面"一键新机"篇详述),而不是随机化单字段。
精髓二:hook 采集点,看清 SDK 到底拿了什么
想确认某 SDK 的采集清单,Frida hook 系统 API 的读取点:
// hook 系统属性读取
var SystemProperties = Java.use('android.os.SystemProperties');
SystemProperties.get.overload('java.lang.String').implementation = function(k) {
var v = this.get(k);
console.log('[getprop] ' + k + ' = ' + v);
return v;
};
// hook 文件读取(/proc 系列)
var FileReader = Java.use('java.io.FileReader');
FileReader.$init.overload('java.lang.String').implementation = function(path) {
if (path.indexOf('/proc/') === 0) console.log('[readfile] ' + path);
this.$init(path);
};
// hook 传感器枚举
var SensorManager = Java.use('android.hardware.SensorManager');
SensorManager.getSensorList.implementation = function(type) {
console.log('[sensors] getSensorList called');
return this.getSensorList(type);
};跑一次 App,SDK 的采集清单全部现形——这比猜它的算法有用得多,你知道它拿什么,就知道该伪造什么。
精髓三:指纹的"生命周期"
设备指纹不是一次计算就完事,SDK 有完整的生命周期管理:
- 首启生成:第一次启动采集计算,存本地(SharedPreferences/文件/甚至写 SD 卡隐藏目录)
- 本地持久化:下次启动直接读缓存——所以"清数据重装"能不能换指纹,取决于它存了几个副本(高级 SDK 会在多个隐蔽位置冗余存储)
- 服务端校正:本地指纹上报后,服务端结合历史数据做归并(你本地生成了新指纹,服务端通过相似度还是能关联到旧设备)
对抗冗余存储的办法:改机后全路径排查——find /data/data/包名 -newer 基准文件 找首次启动新建的所有文件,全部清掉再启动。
总结
- 设备指纹 = 30+ 字段的联合哈希 + 服务端相似度匹配
- 高权重锚点:传感器清单、基带/编译指纹、电池行为
- hook 采集 API 拿 SDK 的真实采集清单,比猜算法有用
- 指纹有多副本持久化,改机要全路径清理
交流微信:run1255