一
前言
之前这篇文章(https://bbs.kanxue.com/thread-273759.htm)介绍了设备指纹基础对抗思路和原理,很多都是基础部分。这一篇将更详细的介绍一些常见的检测思路和方法,还有一些主流对抗手段。
其中包括很多大厂头疼的一些事情,如何手机恢复出厂设置也能保证设备指纹不发生变化,保证稳定性。这篇文章里面也都会详细介绍。
如果没看过我之前的这篇文章,可以先看看之前有的这篇文章里面就不多二次介绍了。
还有就是一个设备指纹大厂会使用多种方式去获取,那么我们应该如何进行对抗,我也会在文章里面说一下我自己的见解和方案,如何在一个“最佳”点去解决问题,当然如果你有更好的方案,也可以私聊我。
二
设备指纹
Android Id
聊到设备指纹最经典的一个字段就是Android id,就我目前所知,他的获取方式不下5种,分别介绍一下。
方法1:
最基础的Android id获取方式,这个不多说,直接Hook就行。
//原始获取android id
String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
CLog.i(String.format("android_id -> 2222 %s", androidId));
方法2:
第一种获取以后,系统会把Android id 保存起来,保存到一个HashMap里面,防止多次IPC初始化,所以为了验证第一种方法的准确性,可以二次获取cache。
和上面的Android id进行对比,9.0以上需要绕过Android id 的反射限制。具体获取方法如下:
//通过反射查询android id cache
ArrayMap mValues = null;
try {
Field sNameValueCache = Settings.Secure.class.getDeclaredField("sNameValueCache");
sNameValueCache.setAccessible(true);
Object sLockSettings = sNameValueCache.get(null);
Field fieldmValues = sLockSettings.getClass().getDeclaredField("mValues");
fieldmValues.setAccessible(true);
mValues = (ArrayMap<String,String>) fieldmValues.get(sLockSettings);
String android_id = (String)mValues.get("android_id");
CLog.i(String.format("android_id -> 3333 %s", android_id));
} catch (Throwable e) {
e.printStackTrace();
}
方法3:
方法3也是很基础的Api,主要通过ContentResolver 进行间接获取,很多大厂也都在使用。
try {
Bundle callResult = context.getContentResolver().call(
Uri.parse("content://settings/secure"), "GET_secure", "android_id", new Bundle()
);
String androidIdValue = callResult.getString("value");
CLog.i(String.format("android_id -> 1111 %s", androidIdValue));
} catch (Exception e) {
CLog.e(e.toString(), e);
}
方法4:
通过query命令去查询,获取Android id,这种方式底层走的也是ContentResolver。
//通过content命令查询android id
String android_id = NativeEngine.popen(
"content query --uri content://settings/secure --where "name=\'android_id\'"",
"");
CLog.i(String.format("android_id -> 4444 %s", android_id));
方法5:
方法4的代码反射实现,我自己测试在高版本总是有问题,不是很稳定,所以这里面就不发了。
硬盘字节总大小:
在设备指纹里面,如果想回复出厂设置也能保证原有的设备信息,这个字段可以在服务端的相似度算法里面占比很重,可以以型号进行分类。我之前测试过,回复出厂设置指纹也不发生变化的设备指纹核心的设备指纹就几个。
比如硬盘大小,ipv6,还有一个就是MAC地址,这几个设备指纹也是很核心的设备指纹。首先先介绍硬盘字节大小。也是三种获取方法,但是方法底层都是一条系统调用。所以如果要进行对抗的话,只需要在SVC层进行处理即可。获取三种方法如下,不建议分别进行处理,可能会导致有地方泄漏,特别是直接开启一条进程通过execve去执行,然后管道传过来,导致很容易Hook不全。
jclass pJclass = env->FindClass("android/os/StatFs");
jmethodID id = env->GetMethodID(pJclass, "<init>", "(Ljava/lang/String;)V");
jobject pJobject =
env->NewObject(pJclass, id, env->NewStringUTF("/storage/emulated/0"));
jlong i = env->CallLongMethod(pJobject, env->GetMethodID(pJclass, "getTotalBytes", "()J"));
LOG(ERROR) << "Java获取getTotalBytes "<<i;
char buffer[1024];
FILE *fp = popen("stat -f /storage/emulated/0", "r");
if (fp != nullptr) {
while (fgets(buffer, sizeof(buffer), fp) != nullptr) {
//LOGI("ps -ef %s",buffer)
LOG(INFO) << "stat -f /storage/emulated/0" << buffer;
}
pclose(fp);
}
struct statfs64 buf={};
if (statfs64("/storage/emulated/0", &buf) == -1) {
LOG(ERROR) << "statfs64系统信息失败";
return;
}
LOG(INFO) << "f_type (文件系统类型): " << buf.f_type;
LOG(INFO) << "f_bsize (块大小): " << buf.f_bsize;
LOG(INFO) << "f_blocks (总数据块): " << buf.f_blocks;
LOG(INFO) << "f_bfree (空闲块): " << buf.f_bfree;
LOG(INFO) << "f_bavail (非特权用户可用的空闲块): " << buf.f_bavail;
LOG(INFO) << "f_files (总文件节点数): " << buf.f_files;
LOG(INFO) << "f_ffree (空闲文件节点数): " << buf.f_ffree;
LOG(INFO) << "f_fsid (文件系统 ID): " << buf.f_fsid.__val[0] << ", " << buf.f_fsid.__val[1];
LOG(INFO) << "f_namelen (最大文件名长度): " << buf.f_namelen;
我之前也是参考的平头哥,Hook的Java方法,但是发现Native层并不能全量拦截,后来弃用。平头哥方法代码如下:
private void relocateRomMemInfo(VirtualBaseInfo virtualBaseInfo) {
final String androidSystemOsClassName = "android.system.Os";
Class<?> androidSystemOsClass =
RposedHelpers.findClassIfExists(androidSystemOsClassName, ClassLoader.getSystemClassLoader());
if (androidSystemOsClass == null) {
CLog.e(">>>>>>>>>>>>> vfs android.system.Os not found");
return;
}
RposedHelpers.findAndHookMethod(androidSystemOsClass, "statvfs", String.class, new RC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
if(param.getResult() == null) {
return;
}
StructStatVfs structStatVfs = (StructStatVfs) param.getResult();
if (structStatVfs.f_blocks == 0) {
return;
}
if(param.args[0] instanceof String) {
long mockRomOut = getMockRomOut(structStatVfs.f_blocks, virtualBaseInfo, structStatVfs.f_blocks);
CLog.i(TAG, "vfs statvfs path:" + param.args[0] + " f_blocks:" + structStatVfs.f_blocks + " mock value:" + mockRomOut);
RposedHelpers.setObjectField(
structStatVfs,
"f_blocks",
mockRomOut
);
}
}
});
}
上面这种方式只适合Java获取,具体三种获取方法可以看下面。
这三种方法底层走的都是statfs64 或者statfs函数,对抗的话也很简单,直接在statfs64 或者statfs 的after里面对参数2进行替换和复写即可。
对抗如下,具体如何拦截svc代码可以参考proot,可以参考我之前写的文章 https://bbs.kanxue.com/thread-273160.htm。
这种方法的好处就是不管如何开进程,都可以在底层统一处理。
//int statfs(const char *path, struct statfs *buf);
//int statfs64(const char *path, struct statfs64 *buf);
case SC_statfs:
case SC_statfs64: {
if (isMockFingerptint()) {
if ((int) syscall_result < 0) {
break;
}
// f_type:文件系统类型。
// f_bsize:文件系统块的大小。
// f_blocks:文件系统中的总块数。
// f_bfree:文件系统中的可用块数。
// f_bavail:非超级用户可获取的块数。
// f_files:文件系统中的总文件节点数。
// f_ffree:文件系统中的可用文件节点数。
// f_fsid:文件系统标识。
// f_namelen:文件名的最大长度。
char pathBuff[PATH_MAX];
word_t pPath = peek_reg(tracee, ORIGINAL, SYSARG_1);
int ret = read_string(tracee, pathBuff, pPath, PATH_MAX);
if(ret < 0){
break;
}
if(get_sysnum(tracee, ORIGINAL) == SC_statfs64){
struct statfs64 fs = {};
word_t arg2 = peek_reg(tracee, ORIGINAL, SYSARG_2);
read_data(tracee,&fs,arg2,sizeof (struct statfs64));
NativeFingerHandler::StatfsHandler64(pathBuff,&fs);
write_data(tracee,arg2,&fs,sizeof (struct statfs64));
} else{
struct statfs fs = {};
word_t arg2 = peek_reg(tracee, ORIGINAL, SYSARG_2);
read_data(tracee,&fs,arg2,sizeof (struct statfs));
NativeFingerHandler::StatfsHandler32(pathBuff,&fs);
write_data(tracee,arg2,&fs,sizeof (struct statfs));
}
}
break;
}
Mac地址:
这个没啥好说的,基础字段,Java层获取,netlink获取,命令行获取。读文件获取,四种获取方法,和上面类似,直接在svc的 recvmsg,recv,recvfrom的after进行数据包替换即可,netlink获取mac方法可以参考我之前的代码。
https://bbs.kanxue.com/thread-271698.htm
如果判断是netlink的消息,并且是获取网卡类型直接对里面的数据包解析和替换即可。
case SC_recvmsg: {
//LOGI("start handle SC_recvmsg systexit after")
if (isMockFingerptint()) {
NetlinkMacHandler::netlinkHandler_recmsg(tracee);
}
break;
}
case SC_recv:
case SC_recvfrom: {
//LOGE("start handle SC_recvfrom systexit after")
//recv底层走的recvfrom,所以不需要处理recvfrom
if (isMockFingerptint()) {
NetlinkMacHandler::netlinkHandler_recv(tracee);
}
break;
}
在读文件获取这块因为网卡信息已经在内存里面,所以直接IO重定向过去即可。
常用的获取网卡信息的文件,以wlan0为例子,场景的获取目录如下:可以cat获取,也可以直接读文件。
/sys/class/net/wlan0/address
/sys/devices/virtual/net/wlan0/address
...
附近网卡信息:
这个字段主要是监控群控的一些信息的,主要作用是获取当前wifi 附近的人MAC信息的。
比如大厂一般检测群控的手段就是获取附近的网卡,如果有聚集性就可以认为是群控。获取的方式也也跟上面一样,五种获取方法。
获取方法底层也是和MAC获取方法一样,底层都是netlink,比如可以直接执行 popen获取。
popen("ip neigh show", "r");
也可以直接直接读文件,路径如下:
/proc/net/arp
还可以直接netlink获取,在收到消息以后判断消息类型是 hdr->nlmsg_type == RTM_NEWNEIGH 直接进行替换即可。
直接在recv收到消息以后对数据里面的buff进行替换即可,主要核心代码如下。包括上面的mac地址替换。
static void _getifaddrs_callback(void *context, nlmsghdr *hdr) {
auto **out = reinterpret_cast<ifaddrs **>(context);
//首先先判断消息类型是不是RTM_NEWLINK类型
if (hdr->nlmsg_type == RTM_NEWLINK) {
auto *ifi = reinterpret_cast<ifinfomsg *>(NLMSG_DATA(hdr));
ifaddrs_storage new_addr(out);
new_addr.interface_index = ifi->ifi_index;
new_addr.ifa.ifa_flags = ifi->ifi_flags;
// Get the interface name
char ifname[IFNAMSIZ];
if_indextoname(ifi->ifi_index, ifname);
// Go through the various bits of information and find the name.
rtattr *rta = IFLA_RTA(ifi);
//获取这个消息的长度
size_t rta_len = IFLA_PAYLOAD(hdr);
//这块是判断这个消息是否是合格的消息
while (RTA_OK(rta, rta_len)) {
if (rta->rta_type == IFLA_ADDRESS){
if (RTA_PAYLOAD(rta) < sizeof(new_addr.addr)) {
void *data = RTA_DATA(rta);
//修改mac地址
setMacInData(data, ifname, ZHENXI_RUNTIME_NETLINK_MAC, false);
new_addr.SetAddress(AF_PACKET, data, RTA_PAYLOAD(rta));
new_addr.SetPacketAttributes(ifi->ifi_index, ifi->ifi_type,
RTA_PAYLOAD(rta));
}
}
else if (rta->rta_type == IFLA_BROADCAST) {
if (RTA_PAYLOAD(rta) < sizeof(new_addr.ifa_ifu)) {
void *data = RTA_DATA(rta);
size_t byteCount = RTA_PAYLOAD(rta);
new_addr.SetBroadcastAddress(AF_PACKET, data, byteCount);
new_addr.SetPacketAttributes(ifi->ifi_index, ifi->ifi_type,
RTA_PAYLOAD(rta));
}
}
else if (rta->rta_type == IFLA_IFNAME) {
if (RTA_PAYLOAD(rta) < sizeof(new_addr.name)) {
memcpy(new_addr.name, RTA_DATA(rta), RTA_PAYLOAD(rta));
new_addr.ifa.ifa_name = new_addr.name;
}
}
rta = RTA_NEXT(rta, rta_len);
}
}
else if (hdr->nlmsg_type == RTM_NEWADDR) {
//这个类型在获取网卡的时候未发现调用
auto *msg = reinterpret_cast<ifaddrmsg *>(NLMSG_DATA(hdr));
// We should already know about this from an RTM_NEWLINK message.
const auto *addr = reinterpret_cast<const ifaddrs_storage *>(*out);
while (addr != nullptr && addr->interface_index != static_cast<int>(msg->ifa_index)) {
//LOGE("Current interface index: %d", addr->interface_index); // 添加当前接口索引日志
addr = reinterpret_cast<const ifaddrs_storage *>(addr->ifa.ifa_next);
}
// If this is an unknown interface,
// ignore whatever we're being told about it.
if (addr == nullptr) {
//LOGE ("_getifaddrs_callback RTM_NEWADDR return")
return;
}
ifaddrs_storage new_addr(out);
strcpy(new_addr.name, addr->name);
new_addr.ifa.ifa_name = new_addr.name;
new_addr.ifa.ifa_flags = addr->ifa.ifa_flags;
new_addr.interface_index = addr->interface_index;
// Go through the various bits of information and find the address
// and any broadcast/destination address.
rtattr *rta = IFA_RTA(msg);
size_t rta_len = IFA_PAYLOAD(hdr);
while (RTA_OK(rta, rta_len)) {
LOGE("RTA type: %d", rta->rta_type);
if (rta->rta_type == IFA_ADDRESS) {
//LOGE ("_getifaddrs_callback RTM_NEWADDR IFA_ADDRESS %d ",msg->ifa_family)
if (msg->ifa_family == AF_INET || msg->ifa_family == AF_INET6) {
void *data = RTA_DATA(rta);
// 确保 RTA_DATA(rta) 的大小是正确的
if (msg->ifa_family == AF_INET6 && RTA_PAYLOAD(rta) < sizeof(struct in6_addr)) {
LOGE("RTA_PAYLOAD size is less than sizeof(struct in6_addr)");
return;
}
struct in6_addr addr_v6_address{};
memcpy(&addr_v6_address, RTA_DATA(rta), sizeof(struct in6_addr));
char str[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &addr_v6_address, str, sizeof(str));
LOGE("RTM_NEWADDR&IFA_ADDRESS&AF_INET6 111 %s", str)
size_t byteCount = RTA_PAYLOAD(rta);
LOGE ("RTM_NEWADDR&IFA_ADDRESS %zu %s ",
byteCount, getpData(data, byteCount).c_str())
new_addr.SetAddress(msg->ifa_family, data, byteCount);
new_addr.SetNetmask(msg->ifa_family, msg->ifa_prefixlen);
}
}
else if (rta->rta_type == IFA_BROADCAST) {
if (msg->ifa_family == AF_INET||msg->ifa_family == AF_INET6) {
void *data = RTA_DATA(rta);
size_t byteCount = RTA_PAYLOAD(rta);
LOGE ("RTM_NEWADDR&IFA_BROADCAST %zu %s ",
byteCount, getpData(data, byteCount).c_str())
new_addr.SetBroadcastAddress(msg->ifa_family, data,
byteCount);
}
}
else if (rta->rta_type == IFA_LOCAL) {
//LOGE ("_getifaddrs_callback RTM_NEWADDR IFA_LOCAL %d ",msg->ifa_family)
if (msg->ifa_family == AF_INET || msg->ifa_family == AF_INET6) {
void *data = RTA_DATA(rta);
struct in6_addr addr_v6_local{};
memcpy(&addr_v6_local, RTA_DATA(rta), sizeof(struct in6_addr));
char str[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &addr_v6_local, str, sizeof(str));
LOGE("RTM_NEWADDR&IFA_ADDRESS&AF_INET6 222 %s", str)
size_t byteCount = RTA_PAYLOAD(rta);
LOGE ("RTM_NEWADDR&IFA_LOCAL %zu %s ",
byteCount, getpData(data, byteCount).c_str())
new_addr.SetLocalAddress(msg->ifa_family, data, byteCount);
}
}
rta = RTA_NEXT(rta, rta_len);
}
}
else if (hdr->nlmsg_type == RTM_NEWNEIGH) {
// RTM_NEWNEIGH 类型消息为网上邻居(arp表),需要进行随机化
auto *ifinfo = reinterpret_cast<ndmsg *>(NLMSG_DATA(hdr));
rtattr *rta = NDA_RTA(ifinfo);
size_t rta_len = NDA_PAYLOAD(hdr);
int if_index = ifinfo->ndm_ifindex;
char if_name[IFNAMSIZ];
if_indextoname(if_index, if_name);
//遍历具体的消息类型
while (RTA_OK(rta, rta_len)) {
//a neighbor cache n/w layer destination address
//邻居缓存nw层目标地址,ip地址区分32和64
//ip地址,ip可以是v4也可以是v6
if (rta->rta_type == NDA_DST) {
if (ifinfo->ndm_family == AF_INET) {
//32
struct in_addr addr{};
memcpy(&addr, RTA_DATA(rta), sizeof(struct in_addr));
char *ntoa = inet_ntoa(addr);
//LOGE("NDA_DST&AF_INET %s", inet_ntoa(addr))
} else if (ifinfo->ndm_family == AF_INET6) {
//64
struct in6_addr addr{};
memcpy(&addr, RTA_DATA(rta), sizeof(struct in6_addr));
char str[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &addr, str, sizeof(str));
//LOGE("NDA_DST&AF_INET6 %s", str);
}
} else if (rta->rta_type == NDA_LLADDR) {
//网卡地址
auto *data = RTA_DATA(rta);
setMacInData(data, if_name, ZHENXI_RUNTIME_NETLINK_NEIGH, true);
}
rta = RTA_NEXT(rta, rta_len);
}
}
else if (hdr->nlmsg_type == RTM_GETADDR) {
//LOGE("RTM_GETADDR ")
auto *ifa = reinterpret_cast<ifaddrmsg *>(NLMSG_DATA(hdr));
// Get the interface name
char ifname[IFNAMSIZ];
if_indextoname(ifa->ifa_index, ifname);
// Process the attributes
rtattr *rta = IFA_RTA(ifa);
size_t rta_len = IFA_PAYLOAD(hdr);
while (RTA_OK(rta, rta_len)) {
if (rta->rta_type == IFA_ADDRESS) {
if (ifa->ifa_family == AF_INET6) {
// Ensure RTA_DATA(rta) size is correct
if (RTA_PAYLOAD(rta) < sizeof(struct in6_addr)) {
LOGE("RTM_GETADDR RTA_PAYLOAD size is less than sizeof(struct in6_addr)");
return;
}
struct in6_addr addr_v6_address{};
memcpy(&addr_v6_address, RTA_DATA(rta), sizeof(struct in6_addr));
char str[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &addr_v6_address, str, sizeof(str));
LOGE("RTM_GETADDR RTM_GETADDR&IFA_ADDRESS&AF_INET6 %s", str);
}
}
rta = RTA_NEXT(rta, rta_len);
}
}
}
IPV6:
设个设备指纹也是很核心的设备指纹,这个玩意底层获取也是netlink,但是netlink获取,但是这块处理很不好处理,我暂时也没进行处理。
常用的获取方式比如,Java获取,命令获取。如果需要进行替换的话,只需要处理命令行和Java的Hook即可。
命令行可以在对方执行命令之前,将命令换成cat命令,去cat自己提前Mock好的文件,效果是一样的。
当然,还有另一种思路,其实这个字段可以服务端获取,客户端二次上报,进行匹配。
try {
NetworkInterface networkInterface;
InetAddress inetAddress;
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
networkInterface = en.nextElement();
for (Enumeration<InetAddress> enumIpAddr = networkInterface.getInetAddresses(); enumIpAddr.hasMoreElements(); ) {
inetAddress = enumIpAddr.nextElement();
if (inetAddress instanceof Inet6Address) {
CLog.e("Java 获取 ipv6 " + inetAddress.getHostAddress());
}
}
}
} catch (Throwable ex) {
CLog.e("printf ipv6 info error " + ex);
}
命令行获取如下,ip命令获取如下:
ip -6 addr show
打印的内容如下:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
3: dummy0: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 state UNKNOWN qlen 1000
inet6 fe80::b86c:79ff:fe96:4945/64 scope link
valid_lft forever preferred_lft forever
10: rmnet_data0@rmnet_ipa0: <UP,LOWER_UP> mtu 1500 state UNKNOWN qlen 1000
inet6 fe80::2ad1:b5a0:792b:9ec4/64 scope link
valid_lft forever preferred_lft forever
30: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 3000
inet6 fe80::8670:a04c:b8cf:467c/64 scope link stable-privacy
valid_lft forever preferred_lft forever
系统内核信息:
这玩意底层走的都是uname函数,直接对uname系统调用处理即可。获取方法比如,可以直接svc调用uname函数,也可以直接根据命令行。
修改的话也很简单,直接在uname的after里面直接对数据进行替换即可。
uname -a
包名随机路径:
这个是一个非常非常核心的字段,就是/data/app/随机Base64路径/base.apk。
这个随机路径就是设备指纹,比如一些大厂会玩,读取你微信的随机路径,获取微信的包信息,然后获取里面的随机路径。
比如微信,快手,京东,淘宝这种随机路径,作为核心的唯一设备指纹,只要你不卸载微信,或者其他大厂apk,你得设备指纹永远不发生变化,无论你如何修改他自己Apk里面的信息,跟他都不产生任何影响。
系统账号:
一般尝试比如小米之类的,登入了指定账号,可以得到一个账号的id信息,这个也需要处理一下,最好的办法是不登入账号。
三
环境检测:
检测环境大多数围绕Hunter的源码检测思路去复现,很多都是Hunter的源码,很多也都是行业内没有公开的一些检测思路,现在市面上检测已经很多没更新了,加速行业内卷,我辈刻不容缓。
Apk签名:
提到环境检测不得不说的就是Apk重打包检测,现在检测方法千奇百怪,我这边也是一一罗列一下,把一些可能存在的风险点,检测和绕过的原理详细叙述一下。当然如果你有更好的思路也可以在文章下面留言。
想要绕过签名检测最好的办法或者说成本最低有效的办法就是修改完毕以后不签名配合核心破解直接安装。
核心破解是lsp的模块,lsp商店直接下载,Hook apk的系统签名解析方法,直接绕过签名检测流程,已实现不签名直接安装。
首先先说一下大厂或者一些企业壳的检测点,Java层基础的获取签名的方法这块就不一一叙述了。
Native层获取签名方法:
检测:
这块以Hunter 源码开始介绍。
核心就三部分: