这篇文章主要是针对设备指纹层面的一个整体的对抗思路 ,可能网上会有相似的文章,但是都不适用于现在的环境 ,想着简单梳理一下,之前是二次打包,只能在客户端去修改,所以需要去hook binder的这些方法,实现hook的拦截 ,但是留的痕迹也很多,很难做到不修改客户端,但是现在我们是在Root的环境下 ,很多东西玩法就变了 。最早是Magisk可以在系统层做一些文件替换,现在的Apatch出来以后,很多对抗都放在了内核里面,直接从应用层修改直接跳转到内核 (说好的不进入内核呢?),最早之前很多可能还是通过编译自定义ROM或者自定义内核之类的去实现对抗 ,现在看起来有点鸡肋 。
下面的内容也只是我个人所感所想,如果有更好的思路,或者文中有什么地方没有讲述清除,讲述错误的地方也可以随时回复。
基础知识:
风控对抗基础随笔
设备指纹第一篇:
聊聊大厂设备指纹获取和对抗&设备指纹看着一篇就够了!
设备指纹第二篇:
聊聊大厂设备指纹其二&Hunter环境检测思路详解!
设备指纹第三篇:
聊聊大厂设备指纹其三&如何在风控对抗这场“猫鼠游戏”中转换角色!
这篇文章是第四篇 ,主要是针对架构的事情进行演练和梳理,通过“推演”的方式去模拟一场战斗。而我们
站在“玩家”视角去推断这场战斗胜负。
其实在第三篇文章里面结论:
我们只需要让客户端安全SDK检测“失效”即可做到黑产的自动化爬取 。
这个失效并不是说让他不上报,而是检测不到 ,但是在客户端还要实现那么多对抗 ,比如一键新机,包括各种复杂的环境检测都需要绕过 ,也不是一件简单的事情 。
我之前写过一个软件叫Hunter,它主要就是检测客户端的风险项,在不断和黑产对抗测试的过程中发现,很多攻击者的攻击方式很有趣,有硬件断点逃逸内存CRC检测,有内核注入的,有直接在服务端修改直接修改客户端返回值的。发现绕过的办法千奇百怪 ,但是他们核心的思路就是 。
客户端不注入不修改一丁点代码,但是可以实现一键新机 。
下来我们就聊一下,客户端应该怎么做才能实现这个功能的 ?中间有哪些坑?为什么要这么做?
原理&细节这几个角度去综合分析一下。
假设我们的对手是目前国内Apk风控的天花板 ,每个策略都是熟练掌握客户端对抗的人 。
客户端也是顶级检测团队 ,如果发现客户端一丁点风险可以写策略对其打击 ,比如下面的一些常见操作。
行为识别&Socket埋点上报(检测用户是否是正常用户,判断指定场景埋点等信息是否正常)
查杀分离(当发现客户端存在问题的时候,不会立刻去封你的号,防止客户端测试找到封号点 )
点击路径矩阵(记录当前对当前页面的点击操作坐标)
多层策略嵌套(服务端有历史各种策略,如果某一个点没有改机改全,即可封号,被降权)
比如在A地方获取设备信息B ,在C地方获取的也是设备信息B 。如果A和C获取结果不一样,则认为设备异常,这种多级校准策略
安全SDK检测能力天花板:
(安全SDK检测能力也是国内的天花板,Hook,沙箱,重打包,Hook框架等检测也是最强,我们暴露给的只有手机解锁)
AI用户行为&策略学习
(不断地自动化推导当前用户是否是一个正常设备)
实时监控与响应:
(实时监控客户端行为,及时发现异常并采取相应的应对措施。)
IP聚集性检测等
抓取频次策略
(比如一分钟/一小时/24小时,抓取多少)
在上述情况同时存在的情况下 ,我们应该如何绕过,实现抓取量化呢?
第一步:
5台手机,20个账号 。 一个账号假设阈值为1000数据量/一天。超过数据量即封号。先把第一步跑通以后,后面逐步增加设备和账号。
这些是一个简单的架构图,客户端里面的【设备指纹】和【环境信息对抗】是这部分最核心的 。全文30%架构设计,70%跟设备对抗相关 。
手机系统有要求:
需要Android 11以上,13以上最好,刷入了Apatch 。
这块有个细节点,为什么需要Android11以上的手机呢?
这个其实是因为Android的一个严重漏洞导致的,就是Android11以下,哪怕没有权限也可以通过netlinker直接获取到你得网卡信息 ,
详细代码实现可以参考我之前写的文章:这个代码在github有开源 https://bbs.kanxue.com/thread-271698.htm 。
(不过我只有10以下的手机的话,可以直接往内核打补丁,然后重新编译内核刷入,也是可以的,不过麻烦点,不推荐)
不需要任何授权,而且还可以读一些net文件直接通过cat的方式获取 ,而且11以下,还有可以使用老的SD卡读写权限,直接往SD卡写入。
新版本只允许你往/sdcard/Android/data/包名里面去写入SD卡相关数据,这样在Apk卸载的时候直接就会一起删除。有很多大厂的指纹非常恶心。
会往SD卡里面写入一些文件种子,如果不进行隔离,很难改的全。
后面我会详细慢慢说,为什么需要使用Apatch ,包括为什么不用Magisk。
客户端想要实现的功能就两部分【设备指纹对抗】&【环境信息对抗】 ,我修改指纹之前,需要先需要知道设备指纹是怎么产生的,比如一些有用的设备指纹比如最基本的一个AndroidId这种,他的产生就在服务端 。
我们为了尽可能让客户端设备指纹对抗做的隐藏,因为我们不能修改目标Apk任何代码,所以就需要在他生产的地方进行修改。
下面简单将设备指纹梳理一下 。
这个进程比较特殊,init进程,init进程是最早的进程。内核启动的第一条进程 ,
目前发现DRM产生位置是在init进程产生的 。底层通过native binder进行的通讯 。
比如DRM如果想修改,可以在客户端获取的时候去修改,Java和C 都需要进行Hook 。比如常见的Java和C获取 。
Java获取如下:
c获取如下:
这个C和Java的最终流程都是调用init进程的IPC进行获取的 。
最终进程位置在 ,android.hardware.drm这个里面 。
环境相关指的是getprop的方式去获取的,一些环境相关的设备指纹。
经过上面的梳理 ,我们发现设备指纹主要的产生位置分为五大类,这五大类,可以包含安卓目前全部的设备指纹产生方式 。
下来我们需要对这些进程进行Hook和修改 ,因为我们不能再客户端进程去修改,所以需要在这五大设备指纹产生类型的地方进行修改。
我们需要用Magisk模块去实现一个Xposed ,为什么要自己去实现一个呢?
因为在可以用Magisk模块在指定进程注入lsplant,实现Java Hook, 比如修改system_server里面的去hook一些Java方法
很方便,自己实现的话逻辑也可控 ,方便Hook除了目标Apk以外的其他进程。
这块实现也很简单,lsposed是开源的 。这块还是建议从头写一遍,加深印象,如果有什么不明白的 再去lsposed里面翻源码 。
这块简单说一下具体的实现思路 。
Magisk模块提供了 Zygisk进行注入 , 其实是提供了五个方法,分别是 。
onLoad
preAppSpecialize
postAppSpecialize
preServerSpecialize
postServerSpecialize
这五个方法也是zygisk对应的接口,相当于Xposed的Loadpackage 。
其实我们会发现 有一个pre和一个post ,
pre方法是指,当前没有被设置沙箱化 ,比如我们的Apk在zygote里面孵化出来的时候,就是一条普通的进程,
为什么只允许读取/data/data/包名下的内容 ,而不能读取别的apk里面的内容 ,是因为被专业化的操作了 。
Magisk模块里面叫specialization 。下面是magisk模块头文件里面的原话,翻译是gpt翻译的。
所以我们可以在某个Apk 没有被设置selinux规则和沙箱话之前进行一些操作 。比如chroot这种都可以进行,比如shamiko好像就是执行chroot,这时候各种权限都没有,让我们的目标App单独隔离到一个新的根目录下,实现的过掉root检测 。
post是被设置专业化完毕以后的回调,pre是设置专业化之前 。
五个方法其中两个是系统服务的,pre和post,另外的是当某个App加载的pre和post 。
为了方便学习,我让gtp把这个zygisk.hpp翻译了一下 ,可以参考中文去学习magisk模块的基本Api操作 。
更多Api操作,包括系统文件替换,修改selinux权限等操作 ,这里不过多介绍 ,详细的话可以参考官网 。
传送门:https://topjohnwu.github.io/Magisk/guides.html
掌握了magisk模块基础以后可以尝试,可以尝试自己实现一个lsposed,lsposed开源版本正好也不支持android 15 ,实现了只要Lsplant支持到15
即可在android 15上去使用 。这里再次感谢lsplant的开发者 ~
我们需要先把编译的java代码在magisk模块里面执行 ,让他在系统服务(system_server)里面去执行java代码 。然后在执行后续的操作 。
下面是具体的实现流程:
我们可以先把我们自己编译的Java代码,丢到magisk模块的system/framework文件夹下 ,让系统服务在启动的时候加载我们的jar包 。
在系统服务回调里面直接用PathClassloader去加载jar包即可 ,然后反射调用里面的静态方法 。第一步实现从Magisk模块转到java调用 。
因为jar包放的是system/framework ,这个文件里面只有system_server有权限去读 ,其他的Apk是没有权限去读取的,所以不需要担心别的Apk读取到你自己的特征 。
通过上面的设置,已经可以实现了调用java方法 ,我们下来再java方法里面对系统服务进行替换和mock , 然后再系统服务里面挂载我们的服务 可以在添加服务的时候替换成你自己的 。
如何hook添加函数呢?
可以动态代理即可 。当系统开始addService时候,添加服务是指定服务的时候 你可以把对应的服务替换成你得,你得服务里面包含【原始服务】和 【你自己的服务】
这样在别人请求的时候你可以用你的服务去做一些事情 。
直接动态代理 ServiceManager ,这样在系统服务添加各种各样的Manager的时候,如果发现是指定的系统服务,替换成你自己的即可 。你得这个系统服务需要实现Binder ,添加完毕以后 我们在发服务端暴露一个接口 ,做共享内存 。
首先说一下这块为什么要用共享内存去实现 ,之前edxp这种他是在你需要hook进程里面去注入dex,这样的话dex已经被注入了 ,特征太多 。
检测也很好检测,直接用内存漫游去内存里面扣DexFile File 的个数,很容易检测到内存加载了多少个Dex
我们可以在服务端(system_service)搞个接口 ,等待客户端过来请求 ,然后用共享内存的方式把dex分配到指定客户端 。
比如我希望Hook Hunter, 我可以在Hunter启动里面 去请求服务端这个分配共享内存的接口 。服务端返回一个fd句柄 ,hunter直接用这个句柄mmap到内存里 ,然后用InMemoryDexClassLoader 去加载这个mmap到内存里的值 。得到classloader以后再去classloader里面findclass,去调用里面的方法即可 。
我们希望的是全局用一份dex即可,而不是每个进程都加载一份dex ,这样效率也不高 。下面这段代码就是当服务端收到客户端的请求,准备开始分配dex的代码 。
服务端直接打开/system/framework/myjar.jar , myjar里面装的是Dex
打开这个jar包以后,把这个jar包用SharedMemory加载到内存里 ,加载没问题以后直接往客户端写入 对应的SharedMemory句柄,这块支持多个dex写入,
写入方式如下 :
我写入的格式是先写入具体加载fd的个数 ,然后对应的文件句柄 fd和fd的size 。
客户端从包裹里面读取服务端写入的值,按照顺序,先读取具体加载的fd句柄个数 ,然后for循环去读取fd和fd大小,加载fd即可 ,mmap加载到内存以后使用InMemoryDexClassLoader去加载 。
这块在指定客户端加载的时候,你只需要初始化一下lsplant,即可实现对任意进程的java hook
如果我们注入了一条进程,但是目标进程通过内存漫游拿到的全部的classloader,然后去对每个classloader去loadclass ,
直接load比如某一个特征class,这样会导致这个class被暴露 ,导致我们代码泄漏 ,所以这块可以用一个内存混淆 ,主要用的是lsposed里面的obfuscation.cpp
他这个原理是对内存dex进行混淆 ,比如正常的路径是com/zhenxi/hack ,他可以混淆成A/B/C这种随机字符串包名 ,这块添加点在系统服务打开dex的时候,得到了这个SharedMemory以后,对里面的内容进行混淆 。
因为你混淆了,还需要在服务端暴露一个查询接口 ,比如客户端得到的A/B/C的原始路径是com/zhenxi/hack,防止反射失败 。
可以站在obfuscation.cpp里面配置好自己需要混淆的路径即可。具体源码可以参考Lsposed 。
系统服务的都很简单,直接在system_service初始化lsplant以后 ,直接用Xposedapi去hook即可 。
举个例子,修改整个android系统的android id 。直接hook SettingsProvider 里面的call方法即可对全部的android id进行修改 。
因为系统服务是系统全局的,所以任何Apk读取到的都是你mock过的 ,如果想实现包名隐藏,或针对某个Apk读取,让他读取不到指定包的存在。
直接在系统服务里面去hook packageManager相关即可 。代码如下 。hook完毕以后任何Apk都读取不到你hook的包 ,可以在isHandler里面整体做分发处理 。
获取当前调用的uid ,在获取包名 ,判断是谁获取的 获取的包名是什么 ,都一清二楚 。
系统服务的hook比较简单 ,只需要基本的Xposed hook即可 。还有其他IMEI,,ICCID ,网卡,蓝牙都可以在系统服务进行处理 。
直接Hook对应的Api 即可, 这块可以直接吧system/framework里面frameword.jar 解压成class,导入AndroidStudio ,方便类的操作 。
不然每次hook还需要去findclass ,不方便 ,gradle编译设置成只编译即可 。
目前只发现oaid,vaid,aaid 这三个基础的是在小米安全中心产生的,他提供一个内容提供者,这块处理也很简单。
直接在Magisk模块发现加载是小米安全中心的时候,注入lsplant,请求服务端,拿到java代码句柄,实现和上面类似的逻辑 。
直接hook对应的代码即可 。可以根据你自己的逻辑去修改对应的hook逻辑 。
这块其实一直我一个很蛋疼的点 ,因为在这个值的产生是在内核里面,不属于任何Apk 范畴,但是又可以当设备指纹 ,
比如最基本的一个boot id ,虽然这玩意重启以后会发生变化 。
他获取方式就可能三种多 ;
1、比如svc去open这个文件读取内容 。
2、popen cat这个文件内容,
3、在搞版本里面 也可以直接进入到当前进程的fd句柄下 ,直接ls - l 也能查到对应的boot_id
比如上面的第三行, /dev/ashmem 后面这一串就是对应的boot_id ,5bc1633e-ea26-444e-b8a6-d7f607b9dd37
因为我们当前手机可能有10个账号同时在,所以我们希望每个账号单独一份boot_id ,比如当A账号启动的时候切换到A账号的Boot_id 。
这样。
我之前的修改方案是直接编译小米内核,发现坑居多 ,小米内核很多驱动不开源,比如什么屏幕下指纹解锁等都是不开源的 。
低版本android编译还好,还顺利,但是高版本编译的话各种坑 ,环境也是不全的 。最后发现单独编译内核在内核里面修改是【一条死路】 。
这块划重点 ,当时研究编译高版本小米内核研究了一个多月,也没搞定 。
这块还有一个坑,就是你为什么不用自定义ROM去做?
因为国内环境对原生ROM检测能力较强,意义不大,改动成本也太大,不适合做插件化,随开随关。
正当我一筹莫展时候,发现Apatch 支持了Hook内核的能力 ,只要你的手机刷了Apatch ,即可使用KPM模块,去开发自己内核hook工具 。
Apatch 不仅仅可以运行Magisk模块,还可以支持内核模块Hook 。
举个例子 :
如果想修改Boot_id,我们先看内核他是怎么获取的 。
他是通过sysctl函数在内核里面暴露给外读取的一个函数 对应的。源码位置如下 。初始化如下 Kernel\drivers\char\random.c
可以看到data 指向真是数据在 sysctl_bootid
其实就是一个16字节的数组 static char sysctl_bootid[16]; 在c文件的最上面 。
我们直接使用KPM模块直接hook即可 ,把原始的值地址保存起来,然后自定义一个syscall,往内核里面写入一个Mock 的boot_id即可 。
直接调用这个自定义的Syscall直接往原始boot_id写入即可 。已经通过kallsyms_lookup_name 获取到了原始的字节数组指针 。
这个Boot_id修改以后还有个坑 ,修改以后任何APk 都打不开了 ,原因是因为服务端因为已经加载了 ,保存的是原始的boot_id 。
然后我们往服务端发送具体的改机指令,服务端开始通过Syscall往内核写入mock boot_id ,因为我们需要在客户端启动之前完成mock指纹的工作
客户端刚刚启动 ,这时候客户端读取服务端资源的时候,是通过共享内存的方式去写入的 ,客户端拿到的也是一个fd文件句柄 ,客户端尝试去打开这个文件句柄的时候,发现找不到这个boot_id导致任何Apk都打不开,这块解决办法也很简单 ,可以使用mknod进行backup即可解决。
其他的字段修改也很简单,在内核里面没那么多事,直接hook对应的函数,实现修改即可 。
搞一个mock value,搞一个自定义syscall 给mockvalue赋值 。如果mockvalue不等于null ,hook内核函数优先读取mockvalue即可 。
最早的时候我实现内核对抗的时候采用也是自定义内核和编译内核模块的方式,但是高版本刷入不进去,刷完手机就死机。
之前用的版本一直都是Android 10 ,就拿Boot_Id来说 ,这块其实他就是一个sysctl函数 ,这个函数是方便内核和应用层交互的 。
改内核也很简单 ,可以直接在内核里面添加一个sysctl函数,直接对应用层暴露一个命令,直接写入即可 。
内核里面注册对应的sysctl表, 应用层可以直接
修改的话直接一条命令行 ,也可以实现。
不过现在有了Apatch以后就抛弃了。
这个是最特殊的一个设备指纹,比如刚开头的时候说过 ,DRMID这种 产生就在Init 进程产生的。init作为一切进程的始祖,它里面包含的东西太多了 。
正是因为时间太早。所以想要hook这个进程成本很高 ,需要注入进去 ,我之前找空大师(zygisk_next作者 ,lsposed作者之一)聊过,想着在zygisk_next添加init进程的回调,但是也不知道现在开发进度如何了 。不知道等到猴年马月 ,但是这种init进程想要注入只有一个办法了 ,直接用ptrace打进去 ,但是这块还有个坑爹的地方 ,如果用ptrace注入进去以后,怎么把mock值传过去呢? 传到目标进程里面 。
上面的指纹整体 都是通过系统服务去保存的,想着通过env去调用binder 拿到class 去和服务端通讯 ,但是init进程里面不允许这样 ,因为init进程里面 他创建时间太早了,连JavaVM都没有创建,所以没办法拿到env 。
而且init进程,它属于vendor分区的东西 ,不属于system分区 。
在Android8以上,安卓出了个分区隔离 ,system分区里面的不允许读取vendor里面的内容 ,vendor也不允许读取system里面 ,这样各种应用层在Hal层进行修改 。所以就没办法注入目标进程以后通过/system/lib64/libandroid_runtime.so调用_ZN7android14AndroidRuntime7mJavaVME虚拟机的创建 。
后来想了很久,直接通过ptrace 往目标进程写入一个字符串是最好的办法 。
我们还是讲drm的进程修改,这玩意其实核心产生位置是在/vendor/lib64/libwvhidl.so 下面这个符号里面去产生的。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2024-7-31 23:30
被珍惜Any编辑
,原因: 图片丢失