前言 本文几乎都是来自于珍惜any大佬 的文章,主要是作为小白的学习记录
原文:
https://bbs.kanxue.com/thread-273838.htm
https://bbs.kanxue.com/thread-273759.htm
https://bbs.kanxue.com/thread-277402.htm
https://bbs.kanxue.com/thread-277637.htm
其中一些较为难懂的(其实是我看不懂)的代码都做了分析的随笔(其实是AI)
基本概念 设备指纹(Device Fingerprint)
设备指纹是应用用来识别你手机的一种技术,类似于给你的设备一个”身份证”
它会收集你的:IMEI、Android ID、MAC地址、设备型号、屏幕分辨率等 信息
应用通过这些信息来识别是否是同一台设备,用于风控、防作弊等
Hook Binder
Binder是Android的核心IPC(进程间通信)机制
应用获取设备信息时,都要通过Binder与系统服务通信
1 2 3 应用 → Binder → 系统服务(获取IMEI等信息) ↑ Hook这里拦截和修改返回值
缺点:
需要在应用进程内注入代码(二次打包或Xposed/Frida)
留下很多痕迹(注入特征、Hook特征)
APatch - 内核层对抗
新一代Root方案,基于内核模块
相比Magisk,它可以直接在内核层进行修改
Magisk是通过修改/boot.img
,在系统启动时挂载系统文件,来达到root的目的
1 2 3 4 5 传统Hook流程: 应用 → [Hook框架在应用层拦截] → Binder → 系统服务 → 内核 APatch方式: 应用 → Binder → 系统服务 → [在内核层直接拦截并返回假数据]
优点:
绕过应用层的检测(反Frida、反Xposed检测 )
在内核层修改,应用层完全无感知
对抗的本质:谁在更底层,谁就有优势
风控的全称应该是风险控制,为了解决和预防将要发生,或者可能发生的一些危险情况,从而减轻损失。
风控概述 蜜罐数据 什么是蜜罐数据?当发现作弊以后返回的数据是非正常的数据 ,可能存在埋点 等信息,比如视频里面在随机帧率里面添加水印,或者返回一些错误数据或者重复的数据 ,这些数据往往是已经被污染或者肉眼无法识别是否正确,从而欺骗攻击者 。
IP限制: 这个不多说,当某一个IP过量或者过快请求的时候会进行限制,返回错误的数据或者返回蜜罐数据。
很多爬虫会买入很多代理,这些代理也都是不安全的,很多大厂也会买入一部分代理,代理毕竟是谁都可以用的,买完以后在服务端直接配置上黑名单即可,很多黑产或者攻击者会采用流量的方式进行请求,将数据转发到路由器,一个类似“猫池”的路由器,里面内置很多手机卡,可以通过设置,将数据通过内部随机手机卡进行发送请求,从而实现动态代理。从而规避一些IP限制的检测。
设备指纹 设备指纹主要为了解决就是设备的唯一性,防护方通过采集手机的某些字段,从而实现得到设备唯一的标识
客户端的采集准确程度,有时候也大幅度的决定了后端策略的风控方向
设备指纹唯一ID,相当于用户的token
,设备指纹的准确性和唯一性,决定安全SDK的强度
其检测方式也只是为了提升攻击者绕过的成本
但是手机重置以后或者恢复出厂设置以后,大部分大厂的设备指纹都无法做到设备唯一性的确认
App环境信息 当策略或者防护发现某个token或者某个设备唯一标识出现问题的时候,可能第一时间就是去检测这个设备的环境信息,用于是否石锤当前用户是否作弊
正常设备 (字面含义,正常用户使用的设备,服务端信任此设备)
灰产设备 (可能存在作弊可能的设备,当手机环境存在root等,hook框架等)
黑产设备 (已经被石锤作弊的设备)
常用的环境检测主要包含以下几部分
Root检测 传统检测方法: 检测su文件
1 2 3 4 5 var suPaths = [ "/system/bin/su", "/system/xbin/su", "/sbin/su" ];
现代检测方法: Magisk检测,LSPosed/EdXposed检测,系统属性检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 // 1. 检测Magisk特征文件 var magiskPaths = [ "/sbin/.magisk", "/data/adb/magisk", "/data/adb/modules", "/cache/magisk.log" ]; // 2. 检测Magisk端口(默认:随机端口) // Native层扫描 /proc/net/tcp // 3. 扫描/proc/self/maps内存映射 function checkMapsForMagisk() { // 读取 /proc/self/maps // 查找关键字:magisk, zygisk, riru } // 4. 检测/proc/self/mount挂载点 function checkMountForMagisk() { // 检测tmpfs等可疑挂载 } // LSPosed 特征路径 var lsposedPaths = [ "/data/misc/lsposed", "/system/framework/lspatch.jar" ]; // 检测Zygote注入 function checkZygoteHook() { // 检查 /proc/self/maps 是否包含 libriru.so // 检查 /proc/[pid]/maps 中的可疑库 } // Java层 String buildTags = Build.TAGS; boolean isTestKeys = buildTags.contains("test-keys"); // 非官方ROM标志 // Native层检测selinux状态 // getenforce() == "Permissive" 表示可能被修改
Hook检测 Xposed检测: 上报Hook方法清单,检测Xposed环境
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 // 扫描所有被Hook的方法 public static List<String> getXposedHookedMethods() { List<String> hookedMethods = new ArrayList<>(); // 遍历关键类的所有方法 Class<?> targetClass = YourClass.class; for (Method method : targetClass.getDeclaredMethods()) { try { // 检查方法是否包含Xposed桥接 Field fieldHookedMethod = Method.class.getDeclaredField("artMethod"); fieldHookedMethod.setAccessible(true); long artMethod = (long) fieldHookedMethod.get(method); // 分析ArtMethod结构,检测hook标志 // ... } catch (Exception e) {} } return hookedMethods; } // 检测XposedBridge类 try { Class.forName("de.robv.android.xposed.XposedBridge"); // 发现Xposed框架 } catch (ClassNotFoundException e) { // 未发现 } // 检测ClassLoader异常 ClassLoader classLoader = YourClass.class.getClassLoader(); String clName = classLoader.getClass().getName(); if (clName.contains("XposedBridge")) { // 检测到Xposed }
Frida检测: Inline Hook检测,Frida特征检测,检测常见Hook点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 // Native层实现 #include <elf.h> // 1. 解析本地ELF文件获取原始指令 uint32_t getOriginalInstruction(const char* soPath, const char* funcName) { // 解析ELF文件 // 获取函数符号表 // 读取函数偏移处的指令 return originalInstruction; } // 2. 读取内存中的当前指令 uint32_t getCurrentInstruction(void* funcAddr) { return *(uint32_t*)funcAddr; } // 3. 对比检测 bool isFunctionHooked(const char* soPath, const char* funcName, void* funcAddr) { uint32_t original = getOriginalInstruction(soPath, funcName); uint32_t current = getCurrentInstruction(funcAddr); if (original != current) { // 检测到Hook! log("Function %s is hooked!", funcName); log("Original: 0x%08x, Current: 0x%08x", original, current); return true; } return false; } // 检测Frida-server端口(默认27042) bool checkFridaPort() { FILE* fp = fopen("/proc/net/tcp", "r"); // 扫描端口:27042, 27043等 } // 检测Frida线程 bool checkFridaThread() { DIR* dir = opendir("/proc/self/task"); // 遍历线程,检查线程名:gmain, gdbus, gum-js-loop } // 检测Frida库文件 const char* fridaLibs[] = { "frida-agent", "frida-gadget", "libfrida", "re.frida" }; // 扫描 /proc/self/maps // 检查libc.so关键函数 void* funcs[] = { dlopen, dlsym, open, read, write, // ... }; for (int i = 0; i < sizeof(funcs)/sizeof(void*); i++) { if (isFunctionHooked("libc.so", funcNames[i], funcs[i])) { // 上报被Hook的函数 } }
沙箱&虚拟环境检测 进程环境检测,私有路径检测,文件描述符检测,IPC动态代理检测,Parent PID检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 void checkProcessCount () { FILE* fp = popen("ps -A" , "r" ); int count = 0 ; char buffer[256 ]; while (fgets(buffer, sizeof (buffer), fp)) { count++; } if (count < 20 ) { } } String dataDir = getApplicationInfo().dataDir; if (!dataDir.equals("/data/data/" + getPackageName())) { } void checkFileDescriptors () { DIR* dir = opendir("/proc/self/fd" ); struct dirent * entry ; while ((entry = readdir(dir)) != NULL ) { char fdPath[256 ]; sprintf (fdPath, "/proc/self/fd/%s" , entry->d_name); char linkPath[256 ]; ssize_t len = readlink(fdPath, linkPath, sizeof (linkPath)-1 ); if (strstr (linkPath, "virtualapp" ) || strstr (linkPath, "parallel" )) { } } } Object activityManager = context.getSystemService( Context.ACTIVITY_SERVICE); String className = activityManager.getClass().getName(); if (className.contains("Proxy" ) || className.contains("$Proxy" ) || className.contains("Hook" )) { log ("IPC被代理: " + className); } String[] services = { Context.ACTIVITY_SERVICE, Context.PACKAGE_SERVICE, Context.LOCATION_SERVICE, }; pid_t ppid = getppid();char cmdline[256 ];sprintf (cmdline, "/proc/%d/cmdline" , ppid);FILE* fp = fopen(cmdline, "r" ); fgets(cmdline, sizeof (cmdline), fp); if (!strstr (cmdline, "zygote" )) { log ("异常父进程: %s" , cmdline); }
此字段一般在环境占比中很重要,当当前App环境一旦被石锤沙箱环境以后,直接可认定当前用户为黑设备
APK签名检测 检测签名信息,检测签名来源(Android 9+),Native层校验(防篡改),
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 PackageInfo packageInfo = context.getPackageManager() .getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES); Signature[] signatures = packageInfo.signatures; String signMd5 = getMD5(signatures[0 ].toByteArray());String OFFICIAL_SIGN = "你的官方签名MD5" ;if (!signMd5.equals(OFFICIAL_SIGN)) { } PackageInfo info = pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES); SigningInfo signingInfo = info.signingInfo;if (signingInfo.hasMultipleSigners()) { } bool verifyApkIntegrity () { const char * apkPath = "/data/app/.../base.apk" ; char * hash = calculateSHA256(apkPath); if (strcmp(hash, OFFICIAL_HASH) != 0 ) { return false ; } return true ; }
模拟器检测 基础特征检测,CPU特征检测,传感器检测,硬件特征检测
自定义ROM 系统文件MD5检查,Build属性检测
查杀分离 这个也是一种很重要的策略,主要就是当服务端或者命中风控 以后,他不会及时封你的号,或者立刻给你号返回错误数据。
而是隔一段时间,可能是几个小时,也有可能是几天,这样做的好处防止你去不断地试探从而找到正确的检测规律。
防止攻击者不断试探的方式去获取正确的风控规则。规避风险。
用户行为&心跳包上报 检测原理: 一般大厂会使用这种方案,在一些SDK初始化以后会开启一个socket
,tcp长连接,覆盖App整个生命周期,当用户进行点击的时候
对页面某个位置点击的时候会进行上报,后台可以很清楚的看到当前用户的点击路径。如果攻击者直接通过rpc或者算法还原接口破解 的方式调用接口的话,就可能会存在心跳包遗漏的问题 ,当长时间无心跳以后,可能会直接认为当前IP是风险IP,从而实现封禁。
这个方法也是很好用的办法,可以通过AI等进行自动化的行为判断,等AI的模型和数据足够完善以后,即可实现,自动化判断自动化&非自动化(人手点击)的判断。
比如某些的自动点击框架,如果只是为了业务去点击的话,是没有一些多余操作的,而我们们的手在屏幕不断滑动的时候是会产生很多用户路径,我们称之为随机路径 。
对抗原理 自动化点击脚本控制App+用户随机路径,自动化脚本控制App去实现自动化点击,防止心跳包和用户点击路径的遗漏。
需要添加随机路径,防止被AI检测出来自动化操作,添加随机路径也很简单,在不影响点击结果的情况下,仿人触摸随机对屏幕滑动。
(可以提前录制一些用户的操作流程,将数据保存到Json里面,在对屏幕进行dispatch事件分发的时候,采用真人点击的event即可)
细节点:如何记录用户的点击行为?
手机屏幕好像是一个分发器,而屏幕的view是消费者。他可能选择消费这个事件,也可以选择抛出去,给下级view去消费。
但是事件只要被消费了就一定会走view->ontouch();
方法,所以我们只需要hook view的view->ontouch();
方法,把参数1进行toString打印和保存即可。即可得到全部的的点击事件消费对象objection
。
异常&行为埋点 检测原理 指的是在某个页面进行埋点,只有触发某条请求或者打开某个页面的时候才会进行埋点上报。
举个case:
当攻击者调用登入接口获取token的时候,正常肯定需要打开App的登入页面,而这个埋点是通过打开页面时候进行上报。
如果攻击者只进行了调用登入接口,没有调用埋点接口,可能会导致当前请求缺少前置埋点。 可能会导致数据被风控。
埋点上报其实在风控里面发挥的作用还是很大的,正常用户从登入到查看个人信息,需要触发5个埋点信息。
但是攻击者只触发了1-2个埋点,则可认定当前用户存在作弊行为,可能存在脱机的嫌疑。会被直接标识成黑设备。
在后台看的话就是一些点点,而这些点就是不同的埋点信息,哪个点被点亮,哪个点没有被点亮,和正常的用户做一下对比,很容易就可以确认。
对抗原理: 同上
总结: 说了这么多总结一下,用上述的方法可以有效对抗,RPC,或者常规的自动点击,包括一些大批量的数据获取。
当账号数量足够多的时候(账号足够成熟,很多新号会有限制),并且满足一下条件的时候:
群控+自动点击+用户点击随机路径+完善的改机软件+一机多号(设备够多无视),即可实现风控的对抗。
当然还需要分析,一些账号的临界值,不同的数据可能触发的风控点也不一样。
比如有的数据单用户日获取量不超过20条,那么你你就不可能获取超过20次。群控服务端还需要记录每个用户的日点击数,等信息。
设备指纹获取 基本概念 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 ,后面再讲签名验证的时候我在详细说。)
1 2 3 4 5 6 正常流程:App → 代理 → Binder → 系统服务 → 返回真实签名 Hook后:App → 代理 → [被Hook的Binder] → 系统服务 → 真实签名 ↓ 拦截并修改为伪造签名 ↓ App ← 代理 ← [被Hook的Binder] ← 伪造签名返回
设备指纹 设备指纹主要分为三部分,Java层设备指纹,Native设备指纹 ,popen执行一些命令获取设备信息,包括一些核心的设备指纹 。
Java层设备指纹 Setting相关(重要) Get: 在setting
里面大家经常遇到的可能就是android id
的获取的
API
如下:
1 Settings.Secure.getString(context.getContentResolver(),Settings.Secure.ANDROID_ID)
但是其实Setting
里面还有很多别的功能东西,常见的就是Settings.Secure
和 Settings.Global
在Settings.Global
里面其实还有一些别的字段,具体API
如下。这些都是一些比较隐蔽的设备指纹。
1 2 3 4 5 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
接口。
1 2 3 public static String getString(ContentResolver resolver, String name) { return getStringForUser(resolver, name, resolver.getUserId()); }
底层调用的是getStringForUser(resolver, name, resolver.getUserId())
三个参数,如果Hook的话可以对这个方法进行入手
1 Settings.Secure->getStringForUser & Settings.Global->getStringForUser
内存反射
很多开发者会采用内存反射的方式去获取变量,所以仅仅是通过mock
方法的方式不够,如果进行Mock需要将Settings.Secure
和 Settings.Global
里面的内存变量进行修复,Settings.Global
是放了一些全局变量,Settings.Secure
放一些安全相关
Settings.Secure->getStringForUser Settings.Global->getStringForUser
和 具体方法如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 private static final HashSet<String> 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
1 2 3 4 5 6 7 8 9 public String getAddress () { try { return mManagerService.getAddress(mAttributionSource); } catch (RemoteException e) { Log.e(TAG, "" , e); } return null ; } 应用进程 → Binder Proxy → Binder 驱动 → Binder Stub → system_server (实际服务)
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 即可。
sHookedMethodCallbacks
里面保存了XPosed全部的Hook方法信息,用于石锤当前方法是否被Hook。获取被Hook方法具体如下:
XposedHelpers->methodCache 不建议使用,如果攻击者使用了XposedBridge->HookAllmethod 的话,可能会导致Hook方法上报的遗漏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private void getHookItemDemo() { ArrayList<Object> choose = ChooseUtils.choose(ClassLoader.class, true); for(Object obj :choose){ ClassLoader clazzloader = (ClassLoader) obj; Class<?> clazzXh = null; Class<?> clazzBR = null; try { Log.e("Test", "-----------------------------------------------------------------"); clazzBR = Class.forName("de.robv.android.xposed.XposedBridge",false,clazzloader); Field callbacksField = clazzBR.getDeclaredField("sHookedMethodCallbacks"); callbacksField.setAccessible(true); Map<Member, Object> callback = (Map<Member, Object>) callbacksField.get(null); for(Member key :callback.keySet()) { Log.e("Test", "sHookedMethodCallbacks " + key.toString()); } Log.e("Test", "-----------------------------------------------------------------"); return; } catch (Throwable e) { Log.e("Test","find errror "+e.getMessage()); } } }
因为正常的使用zygisk的Xposed hook的classloader如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ┌─────────────────────────────────────────────┐ │ BootClassLoader (系统类) │ │ (String, Object, Activity...) │ └──────────────────┬──────────────────────────┘ │ parent ┌──────────────────▼──────────────────────────┐ │ PathClassLoader (系统 Framework) │ │ ✅ XposedBridge.jar 被注入到这里 │ │ ✅ 所有 App 共享这个 ClassLoader │ └──────────────────┬──────────────────────────┘ │ parent ┏━━━━━━━━━━┻━━━━━━━━━━┓ ▼ ▼ ┌────────────────┐ ┌────────────────┐ │ App1 的 │ │ App2 的 │ │ ClassLoader │ │ ClassLoader │ └────────────────┘ └────────────────┘
而使用riru的EdXposed
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ┌─────────────────────────────────────────────┐ │ BootClassLoader │ └──────────────────┬──────────────────────────┘ │ ┌──────────────────▼──────────────────────────┐ │ 系统 PathClassLoader │ │ ❌ 没有 XposedBridge(隐蔽性) │ └──────────────────┬──────────────────────────┘ │ ┏━━━━━━━━━━╋━━━━━━━━━━┓ ▼ ▼ ▼ ┌────────────┐ ┌──────────────────┐ ┌────────────┐ │ App 的 │ │ 🔒 隐藏的独立 │ │ 其他... │ │ ClassLoader│ │ ClassLoader │ └────────────┘ └────────────┘ │ XposedBridge.jar │ │ 加载在这里 │ │ ❗ 没有任何地方 │ │ 引用它! │ └──────────────────┘
没有任何的App加载了相应的ClassLoader
,因此需要通过内存漫游的方式获取所有的ClassLoader
,然后再加载相应的类
serial(普通) 这个变量在高版本里面基本已经拿不到,及时拿到了也是一个unknow,但是也需要兼容低版本的Android
get: 1 2 3 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return Build.getSerial(); }
Mock: 如果返回的不是空,并且不是 unknow
或者UNKNOWN
,随机一份原始长度的字符串即可。另外该字段同上,也可以直接对IPC
类进行处理,直接Hook IPC
对象getSerialForPackage
方法即可 ,实现方法具体如下
1 2 3 4 5 6 7 8 9 10 11 12 public static String getSerial () { IDeviceIdentifiersPolicyService service = IDeviceIdentifiersPolicyService.Stub .asInterface(ServiceManager.getService(Context.DEVICE_IDENTIFIERS_SERVICE)); try { Application application = ActivityThread.currentApplication(); String callingPackage = application != null ? application.getPackageName() : null ; return service.getSerialForPackage(callingPackage, null ); } catch (RemoteException e) { e.rethrowFromSystemServer(); } return UNKNOWN; }
IMEI , IMSI ,ICCID,Line1Number (普通) get:
IMEI
与你的手机是绑定关系 用于区别移动终端设备,DeviceId
就是IMEI
IMSI
与你的手机卡是绑定关系 用于区别移动用户的有效信息 IMSI是用户的标识。
ICCID
是卡的标识,由20位数字组成
ICCID
只是用来区别SIM卡 ,不作接入网络的鉴权认证 。而IMSI在接入网络的时候,会到运营商的服务器中进行验证。SimSerialNumber
就是ICCID
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @SuppressLint("HardwareIds") @RequiresPermission(READ_PHONE_STATE) public static String getPhoneStatus () { TelephonyManager tm = getTelephonyManager(); String str = "" ; str += "DeviceId(IMEI) = " + tm.getDeviceId() + "\n" ; str += "DeviceSoftwareVersion = " + tm.getDeviceSoftwareVersion() + "\n" ; str += "Line1Number = " + tm.getLine1Number() + "\n" ; str += "NetworkCountryIso = " + tm.getNetworkCountryIso() + "\n" ; str += "NetworkOperator = " + tm.getNetworkOperator() + "\n" ; str += "NetworkOperatorName = " + tm.getNetworkOperatorName() + "\n" ; str += "NetworkType = " + tm.getNetworkType() + "\n" ; str += "PhoneType = " + tm.getPhoneType() + "\n" ; str += "SimCountryIso = " + tm.getSimCountryIso() + "\n" ; str += "SimOperator = " + tm.getSimOperator() + "\n" ; str += "SimOperatorName = " + tm.getSimOperatorName() + "\n" ; str += "SimSerialNumber = " + tm.getSimSerialNumber() + "\n" ; str += "SimState = " + tm.getSimState() + "\n" ; str += "SubscriberId(IMSI) = " + tm.getSubscriberId() + "\n" ; str += "VoiceMailNumber = " + tm.getVoiceMailNumber(); return str; }
Mock: TelephonyManager
源码:https://github.com/aosp-mirror/platform_frameworks_base/blob/master/telephony/java/android/telephony/TelephonyManager.java
尝试优先Hook ipc
即可
Build相关(次要) Build里面还是有很多有用的东西,比如手机是否开启adb ,usb接口的状态之类的。我们主要将Build里面分为两部分 。指纹相关又分为两部分,单一字段 /复合字段 。
配置相关
指纹相关
单一字段(只有一个设备信息)
复合字段(多个单一字段复合而成)
这个单独通过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/
作用域分别如下
1 2 3 private static final String Region[] = { "build", "bootimage", "odm", "product", "system_ext", "system", "vendor" };
这里面的值构成顺序也都是一样,所以Hook的话也需要全部进行hook,只处理单一是没用的。因为很多大厂做采集,不会只收集一项。
会七个作用域都进行收集。
配置相关: 常见的配置如下,这些字段其实修改不修改不重要,因为很多大厂如果手机开了开发者选项或者debug模式之类的。
会增加当前手机的风险值。 所以尝试进行Mock 和修改 。
1 2 3 4 5 6 7 8 9 10 11 12 13 PUT_MOCK_AND_SAVE_ORG("sys.usb.config", "none", null, true); PUT_MOCK_AND_SAVE_ORG("sys.usb.state", "none", null, true); PUT_MOCK_AND_SAVE_ORG("persist.sys.usb.config", "none", null, true); PUT_MOCK_AND_SAVE_ORG("persist.sys.usb.qmmi.func", "none", null, true); //这两个config可能会拿不到,拿不到则不进行mock PUT_MOCK_AND_SAVE_ORG("vendor.usb.mimode", "none", null, true); PUT_MOCK_AND_SAVE_ORG("persist.vendor.usb.config", "none", null, true); PUT_MOCK_AND_SAVE_ORG("ro.debuggable", "0", null, true); PUT_MOCK_AND_SAVE_ORG("init.svc.adbd", "stopped", null, true); PUT_MOCK_AND_SAVE_ORG("ro.secure", "1", null, true); //手机解锁状态 PUT_MOCK_AND_SAVE_ORG("ro.boot.flash.locked", "1", null, true); PUT_MOCK_AND_SAVE_ORG("sys.oem_unlock_allowed", "1", null, true);
单一字段: 在不修改机型的前提下,下面这些应该都是需要处理的 。大厂扫描频率很高的Build参数 ,随机的话,在原有的基础上开头或者结尾,随机几位数即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 ro.build.id org-> [RKQ1.200826.002] mock -> [RKQ1.200826.945] ro.build.build.id org-> [RKQ1.200826.002] mock -> [RKQ1.200826.945] ro.bootimage.build.id org-> [RKQ1.200826.002] mock -> [RKQ1.200826.945] ro.odm.build.id org-> [RKQ1.200826.002] mock -> [RKQ1.200826.945] ro.product.build.id org-> [RKQ1.200826.002] mock -> [RKQ1.200826.945] ro.system_ext.build.id org-> [RKQ1.200826.002] mock -> [RKQ1.200826.945] ro.system.build.id org-> [RKQ1.200826.002] mock -> [RKQ1.200826.945] ro.vendor.build.id org-> [RKQ1.200826.002] mock -> [RKQ1.200826.945] ro.build.version.security_patch org-> [2021-11-01] mock -> [2021-11-19] ro.boot.vbmeta.digest org-> [ebe54be57a4fb91d8c22c3d69f68651931878d8925eb8a517d8110084fe45513] mock -> [ebe54be57a4fb91d8c22c3d69f68651931878d8925eb8a517d8110084fa69209] ro.netflix.bsp_rev org-> [Q8250-19134-1] mock -> [P3660-19134-1] gsm.version.baseband org-> [MPSS.HI.2.0.c7-00266-1025_0156_49a7b03461,MPSS.HI.2.0.c7-00266-1025_0156_49a7b03461] mock -> [MPSS.HI.2.0.c7-00266-1025_0156_49a7b03846,MPSS.HI.2.0.c7-00266-1025_0156_49a7b03846] ro.build.date.utc org-> [1639708288] mock -> [1663313901] ro.build.build.date.utc org-> [1639708288] mock -> [1663313901] ro.bootimage.build.date.utc org-> [1639708288] mock -> [1663313901] ro.odm.build.date.utc org-> [1639708288] mock -> [1663313901] ro.product.build.date.utc org-> [1639708288] mock -> [1663313901] ro.system_ext.build.date.utc org-> [1639708288] mock -> [1663313901] ro.system.build.date.utc org-> [1639708288] mock -> [1663313901] ro.vendor.build.date.utc org-> [1639708288] mock -> [1663313901] ro.build.display.id org-> [RKQ1.200826.002 test-keys] mock -> [RKQ1.200826.945] // maybe debug-key ro.build.tags org-> [release-keys] mock -> [release-keys] ro.build.build.tags org-> [release-keys] mock -> [release-keys] ro.bootimage.build.tags org-> [release-keys] mock -> [release-keys] ro.odm.build.tags org-> [release-keys] mock -> [release-keys] ro.product.build.tags org-> [release-keys] mock -> [release-keys] ro.system_ext.build.tags org-> [release-keys] mock -> [release-keys] ro.system.build.tags org-> [release-keys] mock -> [release-keys] ro.vendor.build.tags org-> [release-keys] mock -> [release-keys] ro.build.host org-> [m1-xm-ota-bd148.bj.idc.xiaomi.com] mock -> [m1-xm-ota-be811.bj.idc.xiaomi.com] ro.build.user org-> [builder] mock -> [buizcdn] ro.config.ringtone org-> [MiRemix.ogg] mock -> [MiRemix.acc] ro.miui.ui.version.name org-> [V125] mock -> [V635] ro.build.version.incremental org-> [V12.5.19.0.RKHCNXM] mock -> [V12.5.19.0.RKHWCRG] ro.build.build.version.incremental org-> [V12.5.19.0.RKHCNXM] mock -> [V12.5.19.0.RKHWCRG] ro.bootimage.build.version.incremental org-> [V12.5.19.0.RKHCNXM] mock -> [V12.5.19.0.RKHWCRG] ro.odm.build.version.incremental org-> [V12.5.19.0.RKHCNXM] mock -> [V12.5.19.0.RKHWCRG] ro.product.build.version.incremental org-> [V12.5.19.0.RKHCNXM] mock -> [V12.5.19.0.RKHWCRG] ro.system_ext.build.version.incremental org-> [V12.5.19.0.RKHCNXM] mock -> [V12.5.19.0.RKHWCRG] ro.system.build.version.incremental org-> [V12.5.19.0.RKHCNXM] mock -> [V12.5.19.0.RKHWCRG] ro.vendor.build.version.incremental org-> [V12.5.19.0.RKHCNXM] mock -> [V12.5.19.0.RKHWCRG]
复合字段: 复合字段是多个单一字段拼成的字段,常用的有ro.build.description
还有之前说的7个fingerprint
相关。 这些Mock以后的值要和之前单一字段Mock的值对等。比如某个单一字段值被Mock成A 以后,复合字段里面的内也应该是A 。
1 2 3 4 5 6 7 8 9 10 ro.build.description org-> [alioth-user 11 RKQ1.200826.002 V12.5.19.0.RKHCNXM release-keys] mock -> [alioth-user 11 RKQ1.200826.945 V12.5.19.0.RKHWCRG release-keys] ro.build.fingerprint org-> [Redmi/alioth/alioth:11/RKQ1.200826.002/V12.5.19.0.RKHCNXM:user/release-keys] mock -> [Redmi/alioth/alioth:11/RKQ1.200826.945/V12.5.19.0.RKHWCRG:user/release-keys] ro.build.build.fingerprint org-> [Redmi/alioth/alioth:11/RKQ1.200826.002/V12.5.19.0.RKHCNXM:user/release-keys] mock -> [Redmi/alioth/alioth:11/RKQ1.200826.945/V12.5.19.0.RKHWCRG:user/release-keys] ro.bootimage.build.fingerprint org-> [Redmi/alioth/alioth:11/RKQ1.200826.002/V12.5.19.0.RKHCNXM:user/release-keys] mock -> [Redmi/alioth/alioth:11/RKQ1.200826.945/V12.5.19.0.RKHWCRG:user/release-keys] ro.odm.build.fingerprint org-> [Redmi/alioth/alioth:11/RKQ1.200826.002/V12.5.19.0.RKHCNXM:user/release-keys] mock -> [Redmi/alioth/alioth:11/RKQ1.200826.945/V12.5.19.0.RKHWCRG:user/release-keys] ro.product.build.fingerprint org-> [Redmi/alioth/alioth:11/RKQ1.200826.002/V12.5.19.0.RKHCNXM:user/release-keys] mock -> [Redmi/alioth/alioth:11/RKQ1.200826.945/V12.5.19.0.RKHWCRG:user/release-keys] ro.system_ext.build.fingerprint org-> [Redmi/alioth/alioth:11/RKQ1.200826.002/V12.5.19.0.RKHCNXM:user/release-keys] mock -> [Redmi/alioth/alioth:11/RKQ1.200826.945/V12.5.19.0.RKHWCRG:user/release-keys] ro.system.build.fingerprint org-> [Redmi/alioth/alioth:11/RKQ1.200826.002/V12.5.19.0.RKHCNXM:user/release-keys] mock -> [Redmi/alioth/alioth:11/RKQ1.200826.945/V12.5.19.0.RKHWCRG:user/release-keys] ro.vendor.build.fingerprint org-> [Redmi/alioth/alioth:11/RKQ1.200826.002/V12.5.19.0.RKHCNXM:user/release-keys] mock -> [Redmi/alioth/alioth:11/RKQ1.200826.945/V12.5.19.0.RKHWCRG:user/release-keys]
Get: 1 android.os.SystemProperties->get(key)
Mock: android.os.SystemProperties->get
底层调用的是native_get
,一个native方法,所以Hook的时候优先处理 native_get
1 android.os.SystemProperties->native_get
Java hook
完毕以后 还需要反射将Build
里面的成员变量进行set
。防止采集通过反射的方式去获取
系统默认账号(普通): 很多大厂会把这个字段也作为指纹的一部分,所以这个方法也需要处理。
Get: 1 AccountManager->getAccounts
Mock: 优先Hook ipc
音量相关函数(普通) Get: 1 AudioManager->getStreamVolume
Mock: 优先Hook ipc
传感器相关(普通): 这个函数不需要太多处理,每个手机类型基本都差不多,每次打乱一下返回结果排序顺序即可。
Get: 1 SensorManager->getFullSensorList
Mock: 优先Hook ipc
Java层DRM相关(重要字段) 这个DRM是水印相关 ,主要为了处理不同手机加水印的唯一 ID 核心的是一个叫deviceUniqueId
的东西,这玩意是一个随机的32位字节数组 。很多大厂用这个作为核心的设备指纹 ,不仅在Java层进行获取,还有在Native层进行获取,在后面Native设备指纹会再次介绍到。
Get: 1 2 3 Med iaDrm->getPropertyByteArray MediaDrm->getPropertyString
Hook的话很简单,这个方法没有IPC底层有自己的实现 ,直接Hook get
的方法即可 。java层Hook是远远不够的,还需要处理native
层。
每次随机32位字节数组即可。
Java层网卡信息(普通) 大厂应该不会信任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对象有文件的创建时间。
Native设备指纹 聊了挺多Java相关的设备指纹,其实Java层采集的指纹,并不是关键因素,核心的指纹基本都在native层进行处理的。Native部分会详细介绍包括内核文件,还有一些获取指纹的骚操作
Build(system_property_get & system_property_read)(重要) Java获取最终总的是native_get,而native_get
底层走的就是这个system_property_get
。
在介绍之前我们需要先看看这个函数的源码,android 9以上和9以下实现的方式是不同的。
android 9:
1 2 3 __BIONIC_WEAK_FOR_NATIVE_BRIDGEint __system_property_get(const char* name, char* value) { return system_properties.Get(name, value); }
android 9以下 :
1 2 3 4 5 6 7 8 9 10 int __system_property_get(const char* name, char* value) { const prop_info* pi = __system_property_find(name); if (pi != 0) { return __system_property_read(pi, nullptr, value); } else { value[0] = 0; return 0; } }
安卓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拷贝进去即可 。处理的过程函数如下 。实现也很简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int new_system_property_get (const char *name, char *value) { int len = origin_system_property_get(name, value); string sname = string (name); if (gLobalfakeProperties.find(sname) == gLobalfakeProperties.end()) { return len; } string fake_value = gLobalfakeProperties[sname]; if (strlen (value) > 0 ) { memcpy (value, (char *) fake_value.c_str(), strlen (value)); } return len; }
为什么不直接在__system_property_find 函数处理?
因为get和read底层走的都是find函数,为什么不直接在find函数处理呢,find函数返回的是prop_info*
这个指针指向的是系统内存的变量,直接写入会直接sign11 如果使用mprotect如果直接对内存变量强制写入可能会导致系统的不稳定,导致出现问题。之前踩过这个坑。所以就只处理了get和read这两个函数 。
Get: 使用的话很简单,直接导入头文件就好。
1 2 3 #include <sys/system_properties.h> char sdk[PROP_VALUE_MAX] = {0 };__system_property_get("ro.build.version.sdk" , sdk);
Native获取DRM ID(重要) 这个指纹也是很多大厂用作唯一ID的核心指纹。处理的话也需要注意,很核心的一个设备指纹ID。
Get: 使用的话很简单,直接导入头文件就好。代码不超过10行 。
导入的头文件实现这个So在mediandk.so里面 ,所以cmake->target_link_libraries
引入的时候别忘记添加mediandk 引入依赖。
这个值不同App 读取的内容都不一样,这块需要注意。
1 2 3 4 5 6 7 8 9 10 11 12 #include <media/NdkMediaDrm.h> const uint8_t uuid[] = {0xed ,0xef ,0x8b ,0xa9 ,0x79 ,0xd6 ,0x4a ,0xce , 0xa3 ,0xc8 ,0x27 ,0xdc ,0xd5 ,0x1d ,0x21 ,0xed }; AMediaDrm *mediaDrm = AMediaDrm_createByUUID (uuid); AMediaDrmByteArray aMediaDrmByteArray; AMediaDrm_getPropertyByteArray (mediaDrm,PROPERTY_DEVICE_UNIQUE_ID, &aMediaDrmByteArray);string resut = Base64Utils::Encode ((uint8_t *)aMediaDrmByteArray.ptr,aMediaDrmByteArray.length);
Mock: Hook的话也很简单,直接Hook这个函数地址就行,但是这个方法也是一个短指令,需要用到异常Hook。
处理逻辑如下,因为我们只需要关注description 即可。其他内容不处理。这块有时候直接写入可能会导致问题,需要先mprotect,不能直接用mprotect需要计算一下扇叶大小,是否内存对齐。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 bool MPROTECT (size_t addr,size_t size,int __prot) { auto alignment = (unsigned ) ((unsigned long long ) addr % sysconf (_SC_PAGESIZE)); int i = mprotect ((void *) (addr - alignment), (size_t ) (alignment + size),__prot); return i == 0 ; } HOOK_DEF_DRM (media_status_t , AMediaDrm_getPropertyByteArray, AMediaDrm * drm, const char *propertyName, AMediaDrmByteArray *propertyValue){ media_status_t array = orig_AMediaDrm_getPropertyByteArray (drm, propertyName, propertyValue); if (propertyName != nullptr && strstr (propertyName, PROPERTY_DEVICE_UNIQUE_ID) != nullptr && propertyValue != nullptr && propertyValue->ptr != nullptr && propertyValue->length != 0 ){ if (gMockdrmid == nullptr ){ return array; } MPROTECT ((size_t )propertyValue->ptr, propertyValue->length, MEMORY_RWX); memcpy ((void *)propertyValue->ptr, gMockdrmid, strlen (gMockdrmid)); } return array; }
Netlinker获取网卡信息 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。
处理svc -> recvmsg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 void NetlinkMacHandler::netlinkHandler_recmsg (Tracer *tracee) { ssize_t bytes_read = TEMP_FAILURE_RETRY(peek_reg(tracee, CURRENT, SYSARG_RESULT)); if (bytes_read > 0 ) { word_t msg = peek_reg(tracee, CURRENT, SYSARG_2); if (msg == 0 ){ return ; } auto *my_Msg = (msghdr *) alloca(sizeof (msghdr)); int msg_ret = read_data(tracee, (void *) my_Msg, (word_t ) msg, sizeof (msghdr)); if (msg_ret == 0 ) { auto *iov = (iovec *) alloca(sizeof (iovec)); int iov_ret = read_data(tracee, (void *) iov, (word_t ) my_Msg->msg_iov, sizeof (iovec)); if (iov_ret == 0 ) { auto *temp_hdr = (nlmsghdr *) alloca(iov->iov_len); int hdr_ret = read_data(tracee, (void *) temp_hdr, (word_t ) iov->iov_base, iov->iov_len); if (hdr_ret == 0 ) { NetlinkMacHandler::handler_mac_callback_svc(tracee,temp_hdr, bytes_read); write_data(tracee, (word_t ) iov->iov_base, temp_hdr, iov->iov_len); } else { } } else { } } else { } } }
处理svc->recvfrom:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void NetlinkMacHandler::netlinkHandler_recv(Tracer *tracee) { ssize_t bytes_read = TEMP_FAILURE_RETRY(peek_reg(tracee, CURRENT, SYSARG_RESULT)); if (bytes_read > 0) { //get org value word_t buff = peek_reg(tracee, CURRENT, SYSARG_2); if(buff == 0){ return; } //buff长度 auto size = (size_t) peek_reg(tracee, CURRENT, SYSARG_3); if(size == 0){ return; } char tempBuff[size]; int readStr_ret = read_data(tracee, tempBuff, buff, size); if (readStr_ret != 0) { LOGE("svc netlink handler read_string error %s", strerror(errno)) return; } auto *hdr = reinterpret_cast<nlmsghdr *>(tempBuff); //netlink数据包结构体 NetlinkMacHandler::handler_mac_callback_svc(tracee,hdr, bytes_read); //将数据写入覆盖掉原来的数据 write_data(tracee, buff, tempBuff, size); } }
Android netlink&svc 获取 Mac方法深入分析
SVC的TraceHook沙箱的实现&无痕Hook实现思路
内核文件相关(重要) 内核文件指的是系统的相关文件,很多大厂会直接通过popen cat
或者直接fopen
只读的方式去读取文件内容。核心的也就那几个。
一般读取的时候都是直接svc openat 底层需要用到svc的IO重定向,如果这块不处理的话,基本没办法进行mock和修改 。
build.prop相关
1 2 3 4 "/system/build.prop" "/odm/etc/build.prop" "/product/build.prop" "/vendor/build.prop"
主要就是这四个文件,在低版本这个文件可以直接去读,所以这个文件也需要在Mock value以后生成一份新的,作为备份。
生成新的值要和之前Mock的值是一样的,防止出现不相同的情况 。当对方如果使用了SVC读取这个文件的时候,通过SVC的IO重定向绕过读取。
/proc/sys/kernel/random/boot_id 这个ID重启或者刷机以后发生变化,很多大厂会读取这个值,这个值类似一个UUID,SVC读取这个值,然后将这个值保存到私有目录。
跟DRM ID 相比,好处就是不同App读取的值是一样的。一个设备指纹占比很重的值。
/proc/sys/kernel/random/uuid 同上
/sys/block/mmcblk0/device/cid 同上
/sys/devices/soc0/serial_number 同上
/proc/misc 同上
/proc/version 这个是一个linux系统内核文件,里面记录了当前Linux系统版本的相关信息。里面的值类似如下:
1 eg. Linux version 3.18.31-perf-g9b0888a(builder@c3-miui-ota-bd96.bj)
这个文件在android 11以上基本读不到了 ,但是在android 9是可以读到的 。但是android 11有没有什么代替方案呢?答案是有的,svc 调用uname 。 使用方式类似如下,uname也是一个命令行,还可以通过popen uname -a
的方式去获取 (popen部分会介绍到)。这个函数在IOS上面也比较实用。
1 2 3 4 5 6 7 8 struct utsname buff ; int i = uname(&buff); LOGE("uname sysname %s " , buff.sysname) LOGE("uname nodename %s " , buff.nodename) LOGE("uname release %s " , buff.release) LOGE("uname version %s " , buff.version) LOGE("uname machine %s " , buff.machine) LOGE("uname domainname %s " , buff.domainname)
通过这几项就可以拿到/proc/version 里面的所有信息,
很多大厂会用/ popen uname -a / svc uname函数 / 和svc openat去读/proc/version以此判断获取的值是否准确,如果有一个对不上都会认为当前设备被修改。
修改方式如下 ,一般只需要处理release和Version即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void handlerLinuxEnvInfo (Tracer* tracee) { word_t reg = peek_reg(tracee, ORIGINAL, SYSARG_1); if (reg == 0 ){ LOGI("svc after org peek_reg reg 1 == 0 " ); return ; } auto *buff = (struct utsname *) alloca(sizeof (struct utsname)); int msg_ret = read_data(tracee, buff, reg, sizeof (struct utsname)); if (msg_ret == 0 ) { string mockLinuxRelease ; getZhenxiRuntimeMMKV()->getString(LINUX_VERSION_RELEASE,mockLinuxRelease); memcpy (buff->release,mockLinuxRelease.c_str() ,SYS_NMLN); string mockLinuxVersion ; getZhenxiRuntimeMMKV()->getString(LINUX_VERSION_VERSION,mockLinuxVersion); memcpy (buff->version,mockLinuxVersion.c_str() ,SYS_NMLN); int ret = write_data(tracee, reg, buff, sizeof (struct utsname)); if (ret!=0 ){ LOGE("linux info write data error %s " , strerror(errno)); } } else { LOGE("read_data read data error %s " , strerror(errno)); } }
popen相关 注意1:
因为popen底层走的是execve的这个命令行,是一个shell命令的入口,在64位So里面只要对svc的openat进行IO重定向,哪怕他执行的是execve 也可以进行IO重定向。因为64位execve 底层读取文件,走的也是openat
举个例子:
当我执行popen cat /sys/devices/soc0/serial_number
,如果/sys/devices/soc0/serial_number
这个文件被svc openat重定向到/sdcard/a
文件。最终cat /sys/devices/soc0/serial_number
读取到的也是/sdcard/a
,而不是原始的/sys/devices/soc0/serial_number 。
注意2:
popen这个函数它本身会开启一条线程去执行shell ,因为execve本身就是开一条线程去执行。返回的是一个File 句柄,如果我直接Hook popen 修改他的返回结果,把返回结果替换成我自己的句柄 ,这样是不被允许的,因为Linux 特性 默认情况A线程的文件创建的文件 B线程无法读取 ,跨线程找不到文件句柄fd(解决也很简单,可以将文件设置成组内可读即可 )
那么有没有一种方案可以不修改文件权限实现修改返回内容的Mock呢?
其实很简单,只需要把参数修改成 cat 被修改的返回结果路径即可
,这样他读取到的内容也是你修改过以后的。
或者直接吧参数1 设置成“ ” ,这样他读取到的内容一定是null。
uname -a 这个命令行在之前介绍过了,主要为了解决/proc/version 读取不到的问题。可以直接通过下面的方式去去获取Linux的一些版本信息。
Mock:
这个uname底层走的还是svc uname
函数,所以修改的时候只需要在svc层面直接修改uname函数返回结果 即可。
参考native /proc/version
修改。
getprop 这个执行的内容返回的值和,adb shell 以后执行getprop 结果是一样的。输出的是当前手机全部的Build
相关配置。
Mock: Hook的话也很简单,直接Hook popen 提前生产一份已经Mock好的 ,生产的这个要和Java层Build mock的值是一样的 ,然后直接换成cat 成你自己的文件即可
。这块需要注意,就是 getprop 有三种模式。代码如下
1 2 3 getprop ro.odm.build.id getprop | grep dalvik getprop
需要对这种过滤模式进行处理。
内核相关文件的cat 之前在native层说的内核文件都可以通过popen去cat 。代码如下,修改的话直接svc openat
io重定向就好
1 2 FILE *fp = popen("cat /sys/devices/soc0/serial_number", "r"); ...
ps -ef & ps 这个文件是扫描当前进程的 ,可以用来做反调试检测,比如刚启动的时候去获取一下当前进程列表。
就可以知道是否存在frida ,或者当前进程是否被ptrace ,因为用ptrace调试的话是需要多开启一条调试线程的。
ip a(重要) 其实就是ip addr
这个也是很核心的设备指纹,里面会获取当前手机的网卡信息,whan0 wlan1 p2p0
这些信息。这个底层走的也是netlinker
所以在netlinker
层直接修改拦截,他哪怕执行的命令行也是生效的 。返回的东西很多,可以自己尝试打印一下。很多大厂也会用这种方式去扫描你的网卡Mac地址 。
ls -al /sdcard/Android/data 扫描私有目录,返回私有目录的一些信息 。可以判断当前App是否存在其他App目录下,主要用于检测沙箱 。
其实检测沙箱还有一个很好的办法,就是检测手机的进程信息 。如果当前App在自己正常情况启动,只会有一条线程。
但是如果放在VA沙盒内部的话,VA沙盒本身会启动一条线程,自己的App本身也会启动一条线程。所以线程数量就对不上。也可以认为作弊。代码参考如下,绕过的话也很简单 Hook readdir 当发现读取的是调试线程直接return null即可 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void Test::checkSandbox () { DIR *pdr = opendir("/proc" ); if (pdr == nullptr) { return ; } dirent *read_ptr; while ((read_ptr = readdir(pdr)) != nullptr) { int procpid = atoi(read_ptr->d_name); LOG(INFO) << "find /proc/ child dir " << procpid; if (procpid && procpid != getpid()) { LOG(ERROR) << ">>>>> FIND OTHER THREAD SANDBOX " << procpid; closedir(pdr); } } closedir(pdr); LOG(ERROR) << ">>>>> NOT FIND SANDBOX " ; }
popen扫描Magisk 这些命令都可以进行magisk的列表的扫描,判断当前线程是否存在magisk等关键字,都是很好的办法。
1 2 3 popen("df | grep /sbin/.magisk" , "r" ); popen("mount | grep /sbin/.magisk" , "r" ); popen("ps | grep magisk" , "r" );
修改的话也很简单,如果是ps 或者 df 直接生成一份不存在magisk关键字的文件,(还有一些痕迹关键字,比如xposed,edxp,riru这些都是常用的检测关键字)
mout直接 svc IO重定向绕过即可
popen logcat 有很多大厂,他当发现你设备信息异常的时候,会直接执行popen logcat
直接扫描你当前手机的日志系统
把异常的log都进行上报 ,用于石锤当前用户是否作弊 。所以这个也需要处理 。代码如下:
1 2 3 4 5 pfile = popen("/system/bin/logcat -b main -d -v threadtime -t 200 --pid 当前线程pid" , "r" ); while (fgets(buf, sizeof (buf), pfile)) { LOGE("logcat -> %s" , buf); }
APK签名: 目前主要的获取签名就两种办法
Java 层直接 通过binder和 AMS 通讯获取真实签名信息。 直接和AMS通讯,获取最真实的签名信息
这么一来你不管你Hook pms里面的哪些方法也没啥用
绕过原理 因为和AMS通讯需要用到Binder,Binder可以理解成“水管” ,他虽然和AMS直接进行通讯,但是还是要经过我们的水管,我们直接对这个水管处理即可 。
在通讯时候对水管进行拦截。在BinderProxy->transact
的方法里面进行拦截和替换签名信息即可
我们会使用到PMS
,来获取apk的签名值 参考
1 2 3 4 5 6 7 8 9 10 private void getSignature () { try { PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES); Log.i(SHARK, "len:" +packageInfo.signatures.length); if (packageInfo.signatures != null ) { Log.i(SHARK, "sig:" +packageInfo.signatures[0 ].toCharsString()); } } catch (Exception e) { } }
我们要使用动态代理的方式替换掉这里的两个属性
ActivityThread的静态变量sPackageManager
ApplicationPackageManager对象里面的mPM变量
Hook demp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 //ServiceManagerWraper.java package com.shark.hookpms; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import android.content.Context; import android.content.pm.PackageManager; import android.util.Log; public class ServiceManagerWraper { public final static String SHARK = "Shark"; public static void hookPMS(Context context, String signed, String appPkgName, int hashCode) { try { // 获取全局的ActivityThread对象 Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); Object currentActivityThread = currentActivityThreadMethod.invoke(null); // 获取ActivityThread里面原始的sPackageManager Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager"); sPackageManagerField.setAccessible(true); Object sPackageManager = sPackageManagerField.get(currentActivityThread); // 准备好代理对象, 用来替换原始的对象 Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager"); Object proxy = Proxy.newProxyInstance( iPackageManagerInterface.getClassLoader(), new Class<?>[]{iPackageManagerInterface}, new PmsHookBinderInvocationHandler(sPackageManager, signed, appPkgName, 0)); // 1. 替换掉ActivityThread里面的 sPackageManager 字段 sPackageManagerField.set(currentActivityThread, proxy); // 2. 替换 ApplicationPackageManager里面的 mPM对象 PackageManager pm = context.getPackageManager(); Field mPmField = pm.getClass().getDeclaredField("mPM"); mPmField.setAccessible(true); mPmField.set(pm, proxy); } catch (Exception e) { Log.d(SHARK, "hook pms error:" + Log.getStackTraceString(e)); } } public static void hookPMS(Context context) { String qqSign = "30820253308201bca00302010202044bbb0361300d06092a864886f70d0101050500306d310e300c060355040613054368696e61310f300d06035504080c06e58c97e4baac310f300d06035504070c06e58c97e4baac310f300d060355040a0c06e885bee8aeaf311b3019060355040b0c12e697a0e7babfe4b89ae58aa1e7b3bbe7bb9f310b30090603550403130251513020170d3130303430363039343831375a180f32323834303132303039343831375a306d310e300c060355040613054368696e61310f300d06035504080c06e58c97e4baac310f300d06035504070c06e58c97e4baac310f300d060355040a0c06e885bee8aeaf311b3019060355040b0c12e697a0e7babfe4b89ae58aa1e7b3bbe7bb9f310b300906035504031302515130819f300d06092a864886f70d010101050003818d0030818902818100a15e9756216f694c5915e0b529095254367c4e64faeff07ae13488d946615a58ddc31a415f717d019edc6d30b9603d3e2a7b3de0ab7e0cf52dfee39373bc472fa997027d798d59f81d525a69ecf156e885fd1e2790924386b2230cc90e3b7adc95603ddcf4c40bdc72f22db0f216a99c371d3bf89cba6578c60699e8a0d536950203010001300d06092a864886f70d01010505000381810094a9b80e80691645dd42d6611775a855f71bcd4d77cb60a8e29404035a5e00b21bcc5d4a562482126bd91b6b0e50709377ceb9ef8c2efd12cc8b16afd9a159f350bb270b14204ff065d843832720702e28b41491fbc3a205f5f2f42526d67f17614d8a974de6487b2c866efede3b4e49a0f916baa3c1336fd2ee1b1629652049"; hookPMS(context, qqSign, "com.shark.hookpms", 0); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 //PmsHookBinderInvocationHandler.java package com.shark.hookpms; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; import android.util.Log; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class PmsHookBinderInvocationHandler implements InvocationHandler { private Object base; public final static String SHARK = "Shark"; //应用正确的签名信息 private String SIGN; private String appPkgName = ""; public PmsHookBinderInvocationHandler(Object base, String sign, String appPkgName, int hashCode) { try { this.base = base; this.SIGN = sign; this.appPkgName = appPkgName; } catch (Exception e) { Log.d(SHARK, "error:"+Log.getStackTraceString(e)); } } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Log.i(SHARK, method.getName()); //查看是否是getPackageInfo方法 if("getPackageInfo".equals(method.getName())){ String pkgName = (String)args[0]; Integer flag = (Integer)args[1]; //是否是获取我们需要hook apk的签名 if(flag == PackageManager.GET_SIGNATURES && appPkgName.equals(pkgName)){ //将构造方法中传进来的新的签名覆盖掉原来的签名 Signature sign = new Signature(SIGN); PackageInfo info = (PackageInfo) method.invoke(base, args); info.signatures[0] = sign; return info; } } return method.invoke(base, args); } }
来自于https://blog.csdn.net/cshao888/article/details/72859470:
Android应用ui是绘制在主线程中的,这个线程就是ActivityThread
。
但实际上看源码发现ActivityThread
并没有继承自Thread,而是一个独立的类 ,只是在其main方法中开了一个Looper 循环消息,不断接收处理发到主线程里面的消息 ,比如performLaunchActivity
而ApplicationThread
也不是一个Thread,是一个Binder
,主要用于应用进程和ActivityManagerService进程间通信 的。
整个ActivityThread框架是基于Binder通信的C/S结构 ,从图可知Server
端是ActivityThread、ApplicationThread
Client
是AMS
(ActivityManagerService),而ApplicationThreadProxy
可以看作AMS中Server代表。
Native层 svc读取/data/app/包名/base.apk 解析zip 解析里面的签名文件信息。 Java层是通过AMS通讯获取签名信息,我们直接用svc openat读取apk文件 , 进行手动解析apk 的签名 。不信任系统Api的解析结果。这个也是常用的检测签名办法。这样拿到的结果就是可信的结果。
绕过原理: svc openat的IO重定向
,当他读取原始/data/app/包名/base.apk 的时候我们将它修改成原始apk的路径。
这么一来他读取到的是原始apk路径,而不是被修改的路径,得到的签名也就是原始的签名。底层在处理一下svc readlink readlintat 防止检测路径被替换。
用这两种方案可以目前干掉市面上99%签名检测 。
设备指纹2 Android Id 获取方式
方法1: 1 2 3 //原始获取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
9.0以上需要绕过Android id
的反射限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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(); }
调用 get(null)
的原因是 sNameValueCache
这个字段 (field) 是一个 静态字段 (static field)
fieldmValues
是什么?
.get(sLockSettings)
是在做什么?
这是在调用 Field
对象的 get()
方法。
这个方法的含义是:“请通过我(fieldmValues
这个字段描述)去把 sLockSettings
这个具体对象实例里面的字段值给取出来。 ”
方法3: 方法3也是很基础的Api ,主要通过ContentResolver
进行间接获取 。很多大厂也都在使用 。
1 2 3 4 5 6 7 8 9 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); }
Bundle
是什么?
Bundle
是 Android 中用于在不同组件(如 Activities, Services, Content Providers
)之间传递数据的关键类。你可以把它想象成一个 Map<String, Object>
,它通过键值对(Key-Value
)的形式存储数据。它可以存储各种基本数据类型(String
, int
, boolean
等)以及可序列化的对象。
Bundle
之所以能被跨进程传递 ,是因为它实现了 Parcelable
接口。这个接口定义了如何将一个对象“拍扁”(序列化 /编组)成一串可被传输的数据,以及如何从这串数据中恢复(反序列化 /解组)成原始对象。
Bundle
是数据的容器
Binder
是通信的通道
方法4: 通过query命令去查询,获取Android id ,这种方式底层走的也是ContentResolver
1 2 3 4 5 //通过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));
硬盘字节总大小 在设备指纹里面,如果想恢复出厂设置也能保证原有的设备信息 ,这个字段可以在服务端的相似度算法里面占比很重 ,可以以型号进行分类。我之前测试过,回复出厂设置指纹也不发生变化的设备指纹核心的设备指纹就几个
比如硬盘大小
,ipv6
,还有一个就是MAC地址
,这几个设备指纹也是很核心的设备指纹
先介绍硬盘字节大小。 也是三种获取方法,但是方法底层都是一条系统调用 。所以如果要进行对抗的话,只需要在SVC层 进行处理即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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;
这三种方法底层走的都是statfs64
(arm
) 或者statfs
(arm64
)函数,对抗的话也很简单,直接在statfs64
或者statfs
的after
里面对参数2 进行替换和复写即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 case SC_statfs: case SC_statfs64: { if (isMockFingerptint()) { if ((int ) syscall_result < 0 ) { break ; } 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
的消息,并且是获取网卡类型直接对里面的数据包解析和替换即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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获取,也可以直接读文件
1 2 3 /sys/class/net/wlan0/address /sys/devices/virtual/net/wlan0/address ...
附近网卡信息 这个字段主要是监控群控的一些信息 的,主要作用是获取当前wifi
附近的人MAC
信息的 。
比如大厂一般检测群控的手段就是获取附近的网卡,如果有聚集性就可以认为是群控
获取的方式也也跟上面一样,五种获取方法 。
获取方法底层也是和MAC
获取方法一样 ,底层都是netlink
,比如可以直接执行 popen
获取 ,
1 popen("ip neigh show" , "r" );
也可以直接直接读文件 ,路径如下:
还可以直接netlink
获取 ,在收到消息以后判断消息类型是 hdr->nlmsg_type == RTM_NEWNEIGH
直接进行替换即可
直接在recv
收到消息以后对数据里面的buff
进行替换即可。主要核心代码如下,包括上面的mac地址替换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 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){ // MAC地址 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) { // IP 地址 //这个类型在获取网卡的时候未发现调用 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) { // 处理对 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好的文件,效果是一样的 。
当然,还有另一种思路,其实这个字段可以服务端获取,客户端二次上报,进行匹配 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 try { // 1. 声明变量 NetworkInterface networkInterface; InetAddress inetAddress; // 2. 外层循环:遍历所有网络接口 for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) { // 3. 获取当前网络接口 networkInterface = en.nextElement(); // 4. 内层循环:遍历当前接口上的所有 IP 地址 for (Enumeration<InetAddress> enumIpAddr = networkInterface.getInetAddresses(); enumIpAddr.hasMoreElements(); ) { // 5. 获取当前 IP 地址 inetAddress = enumIpAddr.nextElement(); // 6. 判断是否为 IPv6 地址 if (inetAddress instanceof Inet6Address) { // 7. 如果是,则打印该地址 CLog.e("Java 获取 ipv6 " + inetAddress.getHostAddress()); } } } } catch (Throwable ex) { // 8. 异常处理 CLog.e("printf ipv6 info error " + ex); }
遍历当前设备上所有的网络接口(Network Interface),找出并打印出与这些接口关联的所有 IPv6 地址
命令行获取如下,ip命令获取如下 。
1 2 3 4 5 6 7 8 9 10 11 12 13 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里面直接对数据进行替换即可。
包名随机路径 这个是一个非常非常核心的字段,就是/data/app/随机Base64路径/base.apk
这个随机路径就是设备指纹,比如一些大厂会玩,读取你微信的随机路径,获取微信的包信息,然后获取里面的随机路径
比如微信,快手,京东,淘宝这种随机路径 ,作为核心的唯一设备指纹,只要你不卸载微信,或者其他大厂apk ,你的设备指纹永远不发生变化,无论你如何修改他自己Apk里面的信息,跟他都不产生任何影响 。
系统账号 一般尝试比如小米之类的,登入了指定账号,可以得到一个账号的id
信息 ,这个也需要处理一下 。最好的办法是不登入账号 。
环境检测 检测环境大多数围绕Hunter的源码检测思路去复现 ,很多都是Hunter的源码 ,很多也都是行业内没有公开的一些检测思路
Apk签名 提到环境检测不得不说的就是Apk重打包检测 ,现在检测方法千奇百怪,我这边也是一一罗列一下,把一些可能存在的风险点,检测和绕过的原理详细叙述一下 。
想要绕过签名检测最好的办法或者说成本最低有效的办法就是修改完毕以后不签名配合核心破解直接安装。
核心破解是lsp
的模块,lsp
商店直接下载,Hook apk
的系统签名解析方法,直接绕过签名检测流程 ,已实现不签名直接安装
首先先说一下大厂或者一些企业壳的检测点,Java层基础的获取签名的方法这块就不一一叙述了 。
Native层获取签名方法 检测 这块以Hunter
源码开始介绍
核心就三部分 。
svc openat
读apk,去解析签名 。
检测打开的fd,对fd的路径进行反查,这块有个细节 buff[len] = '\0'
; 就是加这个,如果攻击者没修改readlinkat
的返回值,就可以检测出来 。
检测完毕路径以后对这个文件的权限进行反查 ,正常apk是在系统下的,权限GID和UID应该是1000 ,如果攻击者忘记修改权限也可以检测出来 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 const char *path = getApkPath(env, context); // 获取绝对路径 //check svc apk sign const string &string = checkSign(env, path).substr(0, 10); // 截取签名哈希值的前10个字符 LOG(INFO) << "apk sign " << string; if (string == Base64Utils::VTDecode("TFtCRU58UERAUQ==")) { // Base64变体 //check sign success,but maybe svc io hook //check apk path int fd = my_openat(AT_FDCWD, reinterpret_cast<const char *>(path), O_RDONLY | O_CLOEXEC, 0640); //check apk path char buff[PATH_MAX] = {0}; std::string fdPath("/proc/"); fdPath.append(to_string(getpid())).append("/fd/").append(to_string(fd)); long len = raw_syscall(__NR_readlinkat, AT_FDCWD, fdPath.c_str(), buff, PATH_MAX); if (len < 0) { return getItemData(env, "APK签名验证失败", "readlinkat error", true, RISK_LEAVE_DEADLY, TAG_REPACKAGE); } //截断,如果攻击者hook了readlinkat,只修改了参数,没修改返回值也可以检测出来。 buff[len] = '\0'; LOG(INFO) << "check apk sign path " << buff; if (my_strcmp(path, buff) == 0) { LOG(INFO) << "check apk sign path success "; //start check memory&location inode struct stat statBuff = {0}; long stat = raw_syscall(__NR_fstat, fd, &statBuff); if (stat < 0) { LOG(ERROR) << "check apk sign path fail __NR_fstat<0"; return getItemData(env, "APK签名验证失败", "fstat error", true, RISK_LEAVE_DEADLY, TAG_REPACKAGE); } //check uid&gid (1000 = system group) if (statBuff.st_uid != 1000 && statBuff.st_gid != 1000) { LOG(ERROR) << "check apk sign gid&uid fail "; return getItemData(env, "APK签名验证失败", nullptr, true, RISK_LEAVE_DEADLY, TAG_REPACKAGE); } size_t inode = getFileInMapsInode(path); if (statBuff.st_ino != inode) { LOG(ERROR) << "check apk sign inode fail "<<statBuff.st_ino<<" maps ->"<<inode; return getItemData(env, "APK签名验证失败", nullptr, true, RISK_LEAVE_DEADLY, TAG_REPACKAGE); } LOG(ERROR) << ">>>>>>>>>> check apk sign success! uid-> " << statBuff.st_uid << " gid-> " << statBuff.st_gid; } else { LOG(ERROR) << "check apk sign path fail "; return getItemData(env, "APK签名验证失败", nullptr, true, RISK_LEAVE_DEADLY, TAG_REPACKAGE); } LOG(INFO) << "check apk sign success"; return nullptr; }
Inode 校验检测的不是内存中的内容 是否被修改,而是检测内存的来源 是否被偷换
对抗 针对上面的检测对抗也很简单,对svc的openat
拦截了以后,对readlinkat
和stat
函数进行处理即可。很轻松即可绕过检测。很多加壳基本都是检测ROOT
检测LSP
调用栈之类的,并不只是单一的去检测签名一个纬度。比如发现了开启了seccomp
就会闪退,发现Root
就会闪退。
Java层获取签名方法 检测 检测CREATOR是否被替换 这里先说一下Hunter
的Java
层检测签名的方法,这块相当于反射CREATOR
变量,这个变量是保存一些IPC
通讯的东西
很多攻击者会用Lspatch
进行打包 ,对变量进行替换,这时候我们去检测这个变量的Classloader
是不是系统ClassLoader
防止被替换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 try { Field creatorField = PackageInfo.class.getField("CREATOR"); creatorField.setAccessible(true); Object creator = creatorField.get(null); if (creator != null) { ClassLoader creatorClassloader = creator.getClass().getClassLoader(); ClassLoader sysClassloader = ClassLoader.getSystemClassLoader(); if (creatorClassloader == null || sysClassloader == null) { return null; } // 系统的是 bootclassloader // 用户创建的都是 pathclassloader // 如果相等则认为系统的被替换 if (sysClassloader.getClass().getName(). equals(creatorClassloader.getClass().getName())) { return new ListItemBean("Apk签名验证失败!", ListItemBean.RiskLeave.Deadly, "Apk签名方法被替换!\n" + creatorClassloader.getClass().getName() + "\n" + sysClassloader.getClass().getName()); } return null; } } catch (Throwable e) { CLog.e("checkApkPackageInfoCreator error " + e); }
1 public static final Creator<PackageInfo> CREATOR
Creator<PackageInfo>
是说明这是一个用于创建PackageInfo
类型对象的创建器
我们之前分析Java 层直接 通过binder和 AMS 通讯获取真实签名信息
这个的时候
我们是使用getPackageInfo()
来获取签名的,如果通过Java反射找到PackageInfo.CREATOR
这个静态字段,然后将这个字段的值替换成自己编写的恶意Creator
实现,当App调用getPackageInfo()
获取签名时,Android系统内部会使用这个被污染的CREATOR
来创建PackageInfo
对象,恶意Creator
在创建对象时,将签名信息篡改为正版的签名,从而绕过App的签名校验
而这个检查的原理 是:系统类的Classloader
是BootClassloader
,而我们呢通过Hook模块的代码是由App的PathClassLoader
,因此只需要判断一下这个Classloader
正不正确即可
对抗 对抗的方法主要下面三种:
现在大厂一般会直接通过IPC直接和PMS进行通讯 ,不过这种思路也很好过 ,我这边也是参考的Lspatch
这块有两个很核心的思路 ,就是拦截Binder IPC
通讯的方法,和Hook
服务端的签名解析方法 。代码如下:
Hook服务端解析签名方法 在系统进行签名解析的时候进行签名替换替换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private static void hookPackageParser(Signature[] fakeSignature) { try { RposedBridge.hookAllMethods( RposedHelpers.findClass("android.content.pm.PackageParser", ClassLoader.getSystemClassLoader()), "generatePackageInfo", new RC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) { PackageInfo packageInfo = (PackageInfo) param.getResult(); if (packageInfo == null) return; if (packageInfo.packageName.equals(RuntimeToolKit.packageName)) { if (packageInfo.signatures != null && packageInfo.signatures.length > 0) { CLog.i("PackageParser signature info (method 1)"); packageInfo.signatures[0] = fakeSignature[0]; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (packageInfo.signingInfo != null) { CLog.i("PackageParser signature info (method 2)"); Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners(); if (signaturesArray != null && signaturesArray.length > 0) { signaturesArray[0] = fakeSignature[0]; } } } } } }); } catch (Throwable e) { CLog.e("hook apkSign PackageParser -> generatePackageInfo " + e.getMessage()); } }
替换CREATOR 这个思路也是抄的lspatch
,这种思路可以通过检测CREATOR
变量Classloader
的方式检测出来 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public static void byPassSignatureForCREATOR(Signature[] fakeSignature) { if (fakeSignature == null) { return; } Parcelable.Creator<PackageInfo> originalCreator = PackageInfo.CREATOR; Parcelable.Creator<PackageInfo> proxiedCreator = new Parcelable.Creator<PackageInfo>() { @Override public PackageInfo createFromParcel(Parcel source) { PackageInfo packageInfo = originalCreator.createFromParcel(source); if (packageInfo.packageName.equals(RuntimeToolKit.packageName)) { if (packageInfo.signatures != null && packageInfo.signatures.length > 0) { //CLog.i("CREATOR Replace signature info (method 1)"); packageInfo.signatures[0] = fakeSignature[0]; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (packageInfo.signingInfo != null) { //CLog.i("CREATOR Replace signature info (method 2)"); Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners(); if (signaturesArray != null && signaturesArray.length > 0) { signaturesArray[0] = fakeSignature[0]; } } } } return packageInfo; } @Override public PackageInfo[] newArray(int size) { return originalCreator.newArray(size); } }; RposedHelpers.setStaticObjectField(PackageInfo.class, "CREATOR", proxiedCreator); try { Map<?, ?> mCreators = (Map<?, ?>) RposedHelpers.getStaticObjectField(Parcel.class, "mCreators"); mCreators.clear(); } catch (NoSuchFieldError ignore) { } catch (Throwable e) { CLog.e("fail to clear Parcel.mCreators", e); } try { Map<?, ?> sPairedCreators = (Map<?, ?>) RposedHelpers.getStaticObjectField(Parcel.class, "sPairedCreators"); sPairedCreators.clear(); } catch (NoSuchFieldError ignore) { } catch (Throwable e) { CLog.e("fail to clear Parcel.sPairedCreators", e); } }
Binder transact 方法 有很多大厂会自己伪造IPC和服务端进行通讯,用这种方法可以进行签名的修改和替换,实现思路主要就是Hook binder
通讯解析的方法 。判断如果传输类型数据是获取签名的话就进行替换 。主要思路就是Hook
传输数据的“水管”,对水管里面的内容进行替换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 RposedHelpers.findAndHookMethod("android.os.BinderProxy", // 目标类名 context.getClassLoader(), // 类加载器 "transact", // 目标方法名 int.class, Parcel.class, Parcel.class, int.class, new RC_MethodHook() { // Hook 回调逻辑 @Override protected void afterHookedMethod(MethodHookParam param) { try { Object object = param.thisObject; int id = (int) param.args[0]; // 事务码 Parcel write = (Parcel) param.args[1]; // 请求数据 (data) Parcel out = (Parcel) param.args[2]; // 响应数据 (reply) // forward check if (write == null || out == null) { return; } // prevent recurise call (防止递归) if (id == IBinder.INTERFACE_TRANSACTION) { //IBinder协议事务代码:询问事务的收件人端以获取其规范接口描述符。 return; } String desc = (String) RposedHelpers.callMethod(object, "getInterfaceDescriptor"); if (desc == null || !desc.equals("android.content.pm.IPackageManager")) { // 筛选服务 return; } if (id == TRANSACTION_getPackageInfo_ID) { // 筛选目标操作 out.readException(); // 读取和解析原始结果 if (0 != out.readInt()) { // 检查是否有有效的 PackageInfo 返回 PackageInfo packageInfo = PackageInfo.CREATOR.createFromParcel(out); // 从 Parcel 中反序列化出 PackageInfo 对象 if (packageInfo.packageName.equals(context.getApplicationInfo().packageName)) { if (fakeSignature[0] == null) { CLog.e(">>>>>>>>>> byPassSignature fakeSignature == null"); System.exit(0); return; } //CLog.i( "org data size -> "+out.dataSize()); if (packageInfo.signatures != null && packageInfo.signatures.length > 0) { packageInfo.signatures[0] = fakeSignature[0]; //CLog.i(" byPassSignatureByLSPosed 1 !!"); } // 核心:替换签名 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (packageInfo.signingInfo != null) { Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners(); if (signaturesArray != null && signaturesArray.length > 0) { signaturesArray[0] = fakeSignature[0]; //CLog.i( " byPassSignatureByLSPosed 2 !!"); } } } // 将修改后的结果写回 Parcel out.setDataPosition(0); out.setDataSize(0); out.writeNoException(); out.writeInt(1); packageInfo.writeToParcel(out, PARCELABLE_WRITE_RETURN_VALUE); } } // reset pos 重置读写位置 out.setDataPosition(0); } } catch (Throwable err) { CLog.e(">>>>>>>>>> byPassSignatureByLSPosed error " + err.getMessage()); } } });
还有很多很好思路的代码 ,比如ISO
线程去读取apk
签名信息,防止SVC
被IO
重定向掉 。
"android.os.BinderProxy"
: 这是要 Hook 的目标类。BinderProxy
是客户端(应用进程)中 Binder 远程服务的一个代理对象。当你的应用调用一个系统服务(如 PackageManagerService
)时,实际上是调用了本地 BinderProxy
对象的方法
transact
方法是 Binder 通信的核心。所有跨进程的数据请求和响应都会经过这个方法
在确定了是发往 PackageManagerService
的通信后,我们再通过事务码 id
来判断具体的操作,TRANSACTION_getPackageInfo_ID
(这个常量需要在别处定义) 明确告诉我们,这是一个获取 PackageInfo
的请求
PackageInfo.CREATOR.createFromParcel(out)
: 这是安卓 Parcelable
机制的标准用法,将 Parcel
中的二进制数据反序列化成一个 PackageInfo
Java 对象
packageInfo.signatures[0] = fakeSignature[0]
: 在旧版 Android 中,签名信息直接存储在 PackageInfo
的 signatures
数组里。这里直接将第一个签名替换成我们预先准备好的 fakeSignature
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
: 从 Android 9 (Pie) 开始,引入了 SigningInfo
类来更好地支持多签名和签名轮换。因此,为了兼容新系统,必须同时修改 SigningInfo
里面的签名信息。这段代码处理了高版本的兼容性问题
模拟器检测 Java层基础的获取api架构啥的
Seccomp检测架构 主要思路来自B哥 ,感谢B哥 ,先安装seccomp
架子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void install_check_arch_seccomp() { struct sock_filter filter[15] = { BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (uint32_t) offsetof(struct seccomp_data, nr)), BPF_JUMP(BPF_JMP + BPF_JEQ, __NR_getpid, 0, 12), BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (uint32_t) offsetof(struct seccomp_data, args[0])), BPF_JUMP(BPF_JMP + BPF_JEQ, DetectX86Flag, 0, 10), BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (uint32_t) offsetof(struct seccomp_data, arch)), BPF_JUMP(BPF_JMP + BPF_JEQ, AUDIT_ARCH_X86_64, 0, 1), BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (864 & SECCOMP_RET_DATA)), BPF_JUMP(BPF_JMP + BPF_JEQ, AUDIT_ARCH_I386, 0, 1), BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (386 & SECCOMP_RET_DATA)), BPF_JUMP(BPF_JMP + BPF_JEQ, AUDIT_ARCH_ARM, 0, 1), BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (0xA32 & SECCOMP_RET_DATA)), BPF_JUMP(BPF_JMP + BPF_JEQ, AUDIT_ARCH_AARCH64, 0, 1), BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (0xA64 & SECCOMP_RET_DATA)), BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (6 & SECCOMP_RET_DATA)), BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) }; struct sock_fprog program = { .len = (unsigned short) (sizeof(filter) / sizeof(filter[0])), .filter = filter }; errno = 0; if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { LOG(ERROR) << "prctl(PR_SET_NO_NEW_PRIVS) " << strerror(errno); } errno = 0; if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program)) { LOG(ERROR) << "prctl(PR_SET_SECCOMP) " << strerror(errno); } }
Seccomp (Secure Computing Mode) : 这是Linux内核的一项安全功能,用于限制进程可以调用的系统调用(syscall)。它有两种模式:
SECCOMP_MODE_STRICT
: 极度严格,只允许 read
, write
, sigreturn
, exit
四个系统调用。
SECCOMP_MODE_FILTER
: 允许用户提供一个 BPF 程序,内核会对进程的每一次系统调用都运行这个 BPF 程序,由程序来决定是允许、拒绝、还是执行其他操作。本代码使用的就是这种模式。
BPF (Berkeley Packet Filter) : BPF 是一种在内核中运行的、功能强大的虚拟机。虽然它最初是为过滤网络数据包而设计的,但现在已被广泛用于内核的各个子系统,包括 seccomp。它执行一组简单的指令(BPF字节码),对输入数据进行判断。在 seccomp 的上下文中,这个“输入数据”就是 struct seccomp_data
,包含了当前系统调用的所有信息。
prctl()
系统调用 : 这是一个用于控制进程行为的通用系统调用。代码中用到了它的两个选项:
PR_SET_NO_NEW_PRIVS
: 这是安装 seccomp 过滤器前的关键安全步骤 。将其设置为 1 后,当前进程及其子进程将无法通过 execve
等方式获得比当前更高的权限(例如,通过执行 SUID 程序)。这可以防止恶意代码利用漏洞来绕过 seccomp 限制。
PR_SET_SECCOMP
: 用于实际安装 seccomp 过滤器。
总结:这段代码通过 Seccomp-BPF 实现了一个非常规但高效的架构检测机制。它选择了一个几乎无害的系统调用 (getpid
),并为其增加了一个“隐藏功能”:当以特定方式调用时,它不再返回进程ID,而是通过返回一个特定的 errno
来报告CPU架构 。这种方法在高度沙箱化的环境中非常有用,因为在这些环境中,像执行 uname
命令或读取 /proc
文件系统等常规的检测方法可能都已被禁止。
配合上面的代码 。启动调用getpid
, 上面的架子会对getpid
函数进行拦截 ,然后架构进行判断 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 string check_arch_by_seccomp() { if (get_sdk_level() < __ANDROID_API_N_MR1__){ return ""; } errno = 0; syscall(__NR_getpid, DetectX86Flag); if (errno == 386) { return "I386设备"; } else if (errno == 864) { return "X86_64设备"; } else if (errno == 0xA32 || errno == 0xA64) { return ""; }else if (errno == 0) { //可能是没有开启seccomp return ""; } return ("疑似X86模拟器设备"+ to_string(errno)); }
检测温度挂载文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int thermal_check() { DIR *dir_ptr; int count = 0; struct dirent *entry; if ((dir_ptr = opendir("/sys/class/thermal/")) != nullptr) { while ((entry = readdir(dir_ptr))) { if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) { continue; } char *tmp = entry->d_name; if (strstr(tmp, "thermal_zone") != nullptr) { count++; } } closedir(dir_ptr); } else { count = -1; } return count; }
模拟器特征文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 string simulator_files_check() { if (file_exist("/system/bin/androVM-prop")) {//检测androidVM return "/system/bin/androVM-prop"; } else if (file_exist("/system/bin/microvirt-prop")) {//检测逍遥模拟器--新版本找不到特征 return "/system/bin/microvirt-prop"; } else if (file_exist("/system/lib/libdroid4x.so")) {//检测海马模拟器 return "/system/lib/libdroid4x.so"; } else if (file_exist("/system/bin/windroyed")) {//检测文卓爷模拟器 return "/system/bin/windroyed"; } else if (file_exist("/system/bin/nox-prop")) {//检测夜神模拟器--某些版本找不到特征 return "/system/bin/nox-prop"; } else if (file_exist("system/lib/libnoxspeedup.so")) {//检测夜神模拟器 return "system/lib/libnoxspeedup.so"; } else if (file_exist("/system/bin/ttVM-prop")) {//检测天天模拟器 return "/system/bin/ttVM-prop"; } else if (file_exist("/data/.bluestacks.prop")) {//检测bluestacks模拟器 51模拟器 return "/data/.bluestacks.prop"; } else if (file_exist("/system/bin/duosconfig")) {//检测AMIDuOS模拟器 return "/system/bin/duosconfig"; } else if (file_exist("/system/etc/xxzs_prop.sh")) {//检测星星模拟器 return "/system/etc/xxzs_prop.sh"; } else if (file_exist("/system/etc/mumu-configs/device-prop-configs/mumu.config")) {//网易MuMu模拟器 return "/system/etc/mumu-configs/device-prop-configs/mumu.config"; } else if (file_exist("/system/priv-app/ldAppStore")) {//雷电模拟器 return "/system/priv-app/ldAppStore"; } else if (file_exist("system/bin/ldinit") && file_exist("system/bin/ldmountsf")) {//雷电模拟器 return "system/bin/ldinit"; } else if (file_exist("/system/app/AntStore") && file_exist("/system/app/AntLauncher")) {//小蚁模拟器 return "/system/app/AntStore"; } else if (file_exist("vmos.prop")) {//vmos虚拟机 return "vmos.prop"; } else if (file_exist("fstab.titan") && file_exist("init.titan.rc")) {//光速虚拟机 return "fstab.titan"; } else if (file_exist("x8.prop")) {//x8沙箱和51虚拟机 return "x8.prop"; } else if (file_exist("/system/lib/libc_malloc_debug_qemu.so")) {//AVD QEMU return "/system/lib/libc_malloc_debug_qemu.so"; } LOGD("simulator file check info not find "); return ""; }
模拟器基础特征 这块思路主要来自非虫 ,在次感谢 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 public static ListItemBean checkEmulator(Context context) { ArrayList<String> choose = new ArrayList<>(); // try { // String[] strArr = { // "/boot/bstmods/vboxguest.ko", // "/boot/bstmods/vboxsf.ko", // "/dev/mtp_usb", // "/dev/qemu_pipe", // "/dev/socket/baseband_genyd", // "/dev/socket/genyd", // "/dev/socket/qemud", // "/dev/socket/windroyed-audio", // "/dev/socket/windroyed-camera", // "/dev/socket/windroyed-gps", // "/dev/socket/windroyed-sensors", // "/dev/vboxguest", // "/dev/vboxpci", // "/dev/vboxuser", // "/fstab.goldfish", // "/fstab.nox", // "/fstab.ranchu-encrypt", // "/fstab.ranchu-noencrypt", // "/fstab.ttVM_x86", // "/fstab.vbox86", // "/init.goldfish.rc", // "/init.magisk.rc", // "/init.nox.rc", // "/init.ranchu-encrypt.rc", // "/init.ranchu-noencrypt.rc", // "/init.ranchu.rc", // "/init.ttVM_x86.rc", // "/init.vbox86.rc", // "/init.vbox86p.rc", // "/init.windroye.rc", // "/init.windroye.sh", // "/init.x86.rc", // "/proc/irq/20/vboxguest", // "/sdcard/Android/data/com.redfinger.gamemanage", // "/stab.andy", // "/sys/bus/pci/drivers/vboxguest", // "/sys/bus/pci/drivers/vboxpci", // "/sys/bus/platform/drivers/qemu_pipe", // "/sys/bus/platform/drivers/qemu_pipe/qemu_pipe", // "/sys/bus/platform/drivers/qemu_trace", // "/sys/bus/virtio/drivers/itolsvmlgtp", // "/sys/bus/virtio/drivers/itoolsvmhft", // "/sys/class/bdi/vboxsf-1", // "/sys/class/bdi/vboxsf-2", // "/sys/class/bdi/vboxsf-3", // "/sys/class/misc/qemu_pipe", // "/sys/class/misc/vboxguest", // "/sys/class/misc/vboxuser", // "/sys/devices/platform/qemu_pipe", // "/sys/devices/virtual/bdi/vboxsf-1", // "/sys/devices/virtual/bdi/vboxsf-2", // "/sys/devices/virtual/bdi/vboxsf-3", // "/sys/devices/virtual/misc/qemu_pipe", // "/sys/devices/virtual/misc/vboxguest", // "/sys/devices/virtual/misc/vboxpci", // "/sys/devices/virtual/misc/vboxuser", // "/sys/fs/selinux/booleans/in_qemu", // "/sys/kernel/debug/bdi/vboxsf-1", // "/sys/kernel/debug/bdi/vboxsf-2", // "/sys/kernel/debug/x86", // "/sys/module/qemu_trace_sysfs", // "/sys/module/vboxguest", // "/sys/module/vboxguest/drivers/pci:vboxguest", // "/sys/module/vboxpcism", // "/sys/module/vboxsf", // "/sys/module/vboxvideo", // "/sys/module/virtio_pt/drivers/virtio:itoolsvmhft", // "/sys/module/virtio_pt_ie/drivers/virtio:itoolsvmlgtp", // "/sys/qemu_trace", // "/system/app/GenymotionLayout", // "/system/bin/OpenglService", // "/system/bin/androVM-vbox-sf", // "/system/bin/droid4x", // "/system/bin/droid4x-prop", // "/system/bin/droid4x-vbox-sf", // "/system/bin/droid4x_setprop", // "/system/bin/enable_nox", // "/system/bin/genymotion-vbox-sf", // "/system/bin/microvirt-prop", // "/system/bin/microvirt-vbox-sf", // "/system/bin/microvirt_setprop", // "/system/bin/microvirtd", // "/system/bin/mount.vboxsf", // "/system/bin/nox", // "/system/bin/nox-prop", // "/system/bin/nox-vbox-sf", // "/system/bin/nox_setprop", // "/system/bin/noxd", // "/system/bin/noxscreen", // "/system/bin/noxspeedup", // "/system/bin/qemu-props", // "/system/bin/qemud", // "/system/bin/shellnox", // "/system/bin/ttVM-prop", // "/system/bin/windroyed", // "/system/droid4x", // "/system/etc/init.droid4x.sh", // "/system/etc/init.tiantian.sh", // "/system/lib/egl/libEGL_emulation.so", // "/system/lib/egl/libEGL_tiantianVM.so", // "/system/lib/egl/libEGL_windroye.so", // "/system/lib/egl/libGLESv1_CM_emulation.so", // "/system/lib/egl/libGLESv1_CM_tiantianVM.so", // "/system/lib/egl/libGLESv1_CM_windroye.so", // "/system/lib/egl/libGLESv2_emulation.so", // "/system/lib/egl/libGLESv2_tiantianVM.so", // "/system/lib/egl/libGLESv2_windroye.so", // "/system/lib/hw/audio.primary.vbox86.so", // "/system/lib/hw/audio.primary.windroye.so", // "/system/lib/hw/audio.primary.x86.so", // "/system/lib/hw/autio.primary.nox.so", // "/system/lib/hw/camera.vbox86.so", // "/system/lib/hw/camera.windroye.jpeg.so", // "/system/lib/hw/camera.windroye.so", // "/system/lib/hw/camera.x86.so", // "/system/lib/hw/gps.nox.so", // "/system/lib/hw/gps.vbox86.so", // "/system/lib/hw/gps.windroye.so", // "/system/lib/hw/gralloc.nox.so", // "/system/lib/hw/gralloc.vbox86.so", // "/system/lib/hw/gralloc.windroye.so", // "/system/lib/hw/sensors.nox.so", // "/system/lib/hw/sensors.vbox86.so", // "/system/lib/hw/sensors.windroye.so", // "/system/lib/init.nox.sh", // "/system/lib/libGM_OpenglSystemCommon.so", // "/system/lib/libc_malloc_debug_qemu.so", // "/system/lib/libclcore_x86.bc", // "/system/lib/libdroid4x.so", // "/system/lib/libnoxd.so", // "/system/lib/libnoxspeedup.so", // "/system/lib/modules/3.10.30-android-x86.hd+", // "/system/lib/vboxguest.ko", // "/system/lib/vboxpcism.ko", // "/system/lib/vboxsf.ko", // "/system/lib/vboxvideo.ko", // "/system/lib64/egl/libEGL_emulation.so", // "/system/lib64/egl/libGLESv1_CM_emulation.so", // "/system/lib64/egl/libGLESv2_emulation.so", // "/vendor/lib64/egl/libEGL_emulation.so", // "/vendor/lib64/egl/libGLESv1_CM_emulation.so", // "/vendor/lib64/egl/libGLESv2_emulation.so", // "/vendor/lib64/libandroidemu.so", // "/system/lib64/hw/gralloc.ranchu.so", // "/system/lib64/libc_malloc_debug_qemu.so", // "/system/usr/Keylayout/droid4x_Virtual_Input.kl", // "/system/usr/idc/Genymotion_Virtual_Input.idc", // "/system/usr/idc/droid4x_Virtual_Input.idc", // "/system/usr/idc/nox_Virtual_Input.idc", // "/system/usr/idc/windroye.idc", // "/system/usr/keychars/nox_gpio.kcm", // "/system/usr/keychars/windroye.kcm", // "/system/usr/keylayout/Genymotion_Virtual_Input.kl", // "/system/usr/keylayout/nox_Virtual_Input.kl", // "/system/usr/keylayout/nox_gpio.kl", // "/system/usr/keylayout/windroye.kl", // "system/etc/init/ndk_translation_arm64.rc", // "/system/xbin/noxsu", // "/ueventd.android_x86.rc", // "/ueventd.andy.rc", // "/ueventd.goldfish.rc", // "/ueventd.nox.rc", // "/ueventd.ranchu.rc", // "/ueventd.ttVM_x86.rc", // "/ueventd.vbox86.rc", // "/vendor/lib64/libgoldfish-ril.so", // "/vendor/lib64/libgoldfish_codecs_common.so", // "/vendor/lib64/libstagefright_goldfish_avcdec.so", // "/vendor/lib64/libstagefright_goldfish_vpxdec.so", // "/x86.prop" // }; // for (int i = 0; i < 7; i++) { // String f = strArr[i]; // if (new File(f).exists()) // choose.add(f); // } // } catch (Exception e) { // e.printStackTrace(); // } try { String[] myArr = { "generic", "vbox" }; for (String str : myArr) { if (Build.FINGERPRINT.contains(str)) choose.add(Build.FINGERPRINT); } } catch (Exception e) { e.printStackTrace(); } try { String[] myArr = { "google_sdk", "emulator", "android sdk built for", "droid4x" }; for (String str : myArr) { if (Build.MODEL.contains(str)) choose.add(Build.MODEL); } } catch (Exception e) { e.printStackTrace(); } try { String[] myArr = { "Genymotion" }; for (String str : myArr) { if (Build.MANUFACTURER.contains(str)) choose.add(Build.MANUFACTURER); } } catch (Exception e) { e.printStackTrace(); } try { String[] myArr = { "google_sdk", "sdk_phone", "sdk_x86", "vbox86p", "nox" }; for (String str : myArr) { if (Build.PRODUCT.toLowerCase(Locale.ROOT).contains(str)) choose.add(Build.PRODUCT); } } catch (Exception e) { e.printStackTrace(); } try { String[] myArr = { "nox" }; for (String str : myArr) { if (Build.BOARD.toLowerCase(Locale.ROOT).contains(str)) choose.add(Build.BOARD); } } catch (Exception e) { e.printStackTrace(); } try { String[] myArr = { "nox" }; for (String str : myArr) { if (Build.BOOTLOADER.toLowerCase(Locale.ROOT).contains(str)) choose.add(Build.BOOTLOADER); } } catch (Exception e) { e.printStackTrace(); } try { String[] myArr = { "ranchu", "vbox86", "goldfish" }; for (String str : myArr) { if (Build.HARDWARE.equalsIgnoreCase(str)) choose.add(Build.HARDWARE); } } catch (Exception e) { e.printStackTrace(); } try { Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces(); while (networkInterfaces.hasMoreElements()) { NetworkInterface ele = networkInterfaces.nextElement(); if (ele != null) { Enumeration<InetAddress> inetAddresses = ele.getInetAddresses(); while (inetAddresses.hasMoreElements()) { InetAddress nextElement = inetAddresses.nextElement(); if (!nextElement.isLoopbackAddress() && (nextElement instanceof Inet4Address)) { String ip = nextElement.getHostAddress(); if (ip == null) continue; if (ip.equalsIgnoreCase("10.0.2.15") || ip.equalsIgnoreCase("10.0.2.16")) { choose.add(ip); } } } } } } catch (Exception e) { e.printStackTrace(); } // try { // String[] qemuProps = { // "ro.kernel.qemu.avd_name", // "ro.kernel.qemu.gles", // "ro.kernel.qemu.gltransport", // "ro.kernel.qemu.opengles.version", // "ro.kernel.qemu.uirenderer", // "ro.kernel.qemu.vsync", // "ro.qemu.initrc", // "init.svc.qemu-props", // "qemu.adb.secure", // "qemu.cmdline", // "qemu.hw.mainkeys", // "qemu.logcat", // "ro.adb.qemud", // "qemu.sf.fake_camera", // "qemu.sf.lcd_density", // "qemu.timezone", // "init.svc.goldfish-logcat", // "ro.boottime.goldfish-logcat", // "ro.hardware.audio.primary", // "init.svc.ranchu-net", // "init.svc.ranchu-setup", // "ro.boottime.ranchu-net", // "ro.boottime.ranchu-setup", // "init.svc.droid4x", // "init.svc.noxd", // "init.svc.qemud", // "init.svc.goldfish-setup", // "init.svc.goldfish-logcat", // "init.svc.ttVM_x86-setup", // "vmos.browser.home", // "vmos.camera.enable", // "ro.trd_yehuo_searchbox", // "init.svc.microvirtd", // "init.svc.vbox86-setup", // "ro.ndk_translation.version", // "redroid.width", // "redroid.height", // "redroid.fps", // "ro.rf.vmname" // }; // // for (String str : qemuProps) { // String val = SystemPropertiesUtils.getProperty(str, null); // if (val != null) { // choose.add(str); // } // } // } catch (Throwable e) { // e.printStackTrace(); // } //判断是否存在指定硬件 PackageManager pm = null; try { pm = context.getPackageManager(); String[] features = { //PackageManager.FEATURE_RAM_NORMAL,//这个存在问题,自己组装的手机可能导致这个痕迹找不到 PackageManager.FEATURE_BLUETOOTH, PackageManager.FEATURE_CAMERA_FLASH, PackageManager.FEATURE_TELEPHONY }; for (String feature : features) { if (!pm.hasSystemFeature(feature)) { choose.add(feature); } } } catch (Throwable ignored) { } try { String[] emuPkgs = { "com.google.android.launcher.layouts.genymotion", "com.bluestacks", "com.bignox.app" }; for (String pkg : emuPkgs) { try { if (pm != null) { pm.getPackageInfo(pkg, 0); } choose.add(pkg); } catch (Throwable e) { //e.printStackTrace(); } } } catch (Throwable ignored) { } try { SensorManager sensor = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); int sensorSize = sensor.getSensorList(Sensor.TYPE_ALL).size(); for (int i = 0; i < sensorSize; i++) { Sensor s = sensor.getDefaultSensor(i); if (s != null && s.getName().contains("Goldfish")) { choose.add(s.getName()); } } } catch (Throwable ignored) { } try { if (checkSelfPermission(context, "android.permission.READ_SMS") == 0 || checkSelfPermission(context, "android.permission.READ_PHONE_NUMBERS") == 0 || checkSelfPermission(context, "android.permission.READ_PHONE_STATE") == 0) { TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); String phoneNumber = telephonyManager.getLine1Number(); String[] phoneNumbers = { "15555215554", "15555215556", "15555215558", "15555215560", "15555215562", "15555215564", "15555215566", "15555215568", "15555215570", "15555215572", "15555215574", "15555215576", "15555215578", "15555215580", "15555215582", "15555215584" }; if(phoneNumber!=null) { for (String phone : phoneNumbers) { if (phoneNumber.equalsIgnoreCase(phone)) { choose.add(phone); break; } } } } } catch (Exception e) { e.printStackTrace(); } if (choose.size() > 0) { ListItemBean item = new ListItemBean("检测到APK运行在虚拟机&模拟器中", ListItemBean.RiskLeave.Deadly, choose.toString() ); for (String str : choose) { item.putData(str); } return item; } return null; }
Android系统提供了一个android.os.Build
类,其中包含了大量关于设备硬件和软件版本的信息
**Build.FINGERPRINT
(设备指纹)**:官方模拟器(AVD)的指纹通常以 generic/
开头。许多基于VirtualBox的模拟器(如Genymotion、Nox)的指纹中可能会包含vbox
字样。而真实设备的指纹通常是制造商和型号的组合,如 samsung/sm-g9980/...
。
**Build.MODEL
(设备型号)**:AVD的型号通常是sdk_gphone64_x86_64
或Android SDK built for x86
等。Droid4X(卓壮模拟器)也会明确标识其型号。真实设备的型号则是Pixel 7 Pro
, SM-G9980
等。
**Build.MANUFACTURER
(制造商)**:Genymotion模拟器会将其制造商设置为Genymotion
。真实设备则为Google
, Samsung
, Xiaomi
等
**Build.PRODUCT
(产品名称)**:与型号类似,模拟器的产品名也通常是通用的sdk_x86
或带有模拟器标识的名称,如nox
(夜神模拟器)、vbox86p
(VirtualBox平台)
**Build.BOARD
/ Build.BOOTLOADER
(主板/引导程序)**:某些模拟器(如夜神)可能会在这些更底层的标识中留下自己的名字。
**Build.HARDWARE
(硬件名称)**:是非常可靠的指标。goldfish
是早期AVD使用的虚拟硬件平台名称。ranchu
是较新版AVD使用的虚拟硬件平台。vbox86
明确指向了VirtualBox x86平台。真实设备的硬件名称通常是芯片组的代号,如qcom
(高通), kirin
(麒麟), exynos
(猎户座)
**网络环境特征 (Network Environment)**:这是官方Android模拟器(AVD)的默认网络配置。在AVD内部,它通过一个虚拟路由器连接到宿主机网络。这个虚拟路由器将10.0.2.15
这个IP地址分配给模拟器本身。这是一个广为人知的特征。
硬件功能缺失 (Missing Hardware Features): 使用PackageManager.hasSystemFeature()
检查设备是否缺少蓝牙 (FEATURE_BLUETOOTH
)、相机闪光灯 (FEATURE_CAMERA_FLASH
) 或电话功能 (FEATURE_TELEPHONY
)。模拟器为了节省资源,通常不会去模拟这些不常用的硬件
特定应用包名 (Specific Application Packages): 检查设备上是否安装了特定包名的应用,如com.bluestacks
(蓝叠), com.bignox.app
(夜神)
com.google.android.launcher.layouts.genymotion
(Genymotion的定制启动器)。
许多模拟器会预装一些自家的辅助工具、应用商店或服务。通过检查这些应用的包名是否存在,可以直接识别出对应的模拟器
硬件和驱动特征 (Hardware and Driver Signatures): 与Build.HARDWARE
中的goldfish
类似,AVD模拟的传感器(如加速度计、陀螺仪等)也是基于”Goldfish”虚拟硬件的,因此它们的驱动名称会暴露这一信息。真实设备的传感器名称通常来自博世(Bosch)、意法半导体(STMicro)等硬件制造商
设备信息特征 (Device Information): 官方模拟器允许设置一个电话号码用于测试,这些1555...
开头的号码是Android文档中指定的官方测试号码。如果在设备上读到了这些号码,几乎可以肯定是模拟器环境。此项检测需要READ_PHONE_STATE
等相关权限
**文件系统特征 (File System Artifacts) - *已注释***:模拟器为了正常运行,会在Android文件系统的各个角落留下自己的组件。
QEMU/AVD相关 : qemu_pipe
, fstab.goldfish
, init.ranchu.rc
,
/sys/fs/selinux/booleans/in_qemu
等,这些都与QEMU虚拟机引擎直接相关。
VirtualBox相关 : vboxguest.ko
, vboxsf.ko
, mount.vboxsf
, /sys/module/vboxguest
等,这些是VirtualBox的Guest Additions(增强功能)组件,用于虚拟机和宿主机之间的通信与集成。
特定模拟器 : init.nox.rc
(夜神), droid4x
(卓壮), GenymotionLayout
(Genymotion), init.tiantian.sh
(天天模拟器), windroyed
(文卓爷) 等,这些是各家模拟器专属的配置文件、可执行文件或库文件。
**系统属性特征 (System Properties) - *已注释***:
除了Build
类提供的标准信息,Android系统内部还有大量的系统属性(ro.开头的只读属性和qemu.等其他属性)。模拟器会设置很多特定的属性来控制其行为。
ro.kernel.qemu.*
: 明确表示内核是在QEMU环境下运行。
init.svc.*
: 检查是否有模拟器相关的服务在运行,如init.svc.qemud
, init.svc.noxd
。
redroid.*
, ro.rf.vmname
: 这些是云手机方案(如Redroid, 红手指)特有的属性。
检测云手机 这块思路还是很多的,不同的云手机检测的思路也不一样 。大部分云手机做的还是很好的,很多都可以过掉Hunter
的检测 。
检测电流&电压 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 private final BroadcastReceiver batteryInfoReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // 电池状态 int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); // 电压(以毫伏为单位) int voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1); // 获取电池电流(毫安) int currentNow = -1; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { BatteryManager batteryManager = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE); currentNow = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW); } // 判断是否在充电 if (plugged == BatteryManager.BATTERY_PLUGGED_AC || plugged == BatteryManager.BATTERY_PLUGGED_USB || plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS) { // 在充电 if (voltage != -1 && currentNow != -1) { float voltageInVolts = voltage / 1000f; // 将电压转换为伏特 float currentInAmperes = currentNow / 1000000f; // 将电流转换为安培 float chargingPower = voltageInVolts * currentInAmperes; // 计算充电功率(瓦特) CLog.i(String.format("充电功率: %.2fW", chargingPower)); if (Math.abs(chargingPower) > 300) { CLog.e("充电功率过高"); handlerItemData(new ListItemBean( "电池异常:充电功率过高(可能是云手机)", ListItemBean.RiskLeave.Deadly, "检测到过大的充电功率 -> " + String.format("%.2fW", Math.abs(chargingPower)) )); } } } } };
检测摄像头&传感器相关 判断摄像头有个数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 try { CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); String[] cameraIds = manager.getCameraIdList(); //摄像头个数 CLog.i("cameraIds -> "+ Arrays.toString(cameraIds)); if(cameraIds.length < CAMERA_MINIMUM_QUANTITY_LIMIT){ items.add( new ListItemBean( "当前手机可能是模拟器&云手机", ListItemBean.RiskLeave.Warn, "camera size -> "+cameraIds.length )); } } catch (Throwable ignored) { }
检测传感器个数 这块思路就是直接获取个数,少于10
个可以直接认定为黑产 。 我目前没发现那个手机少于10个传感器 ,这块如果可能的话可以尝试调用一下传感器,保证传感器是否可用 ,防止云手机以假乱真 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 try { //3,检测传感器类型,支持的全部类型传感器 SensorManager sm = (SensorManager) context.getSystemService(SENSOR_SERVICE); List<Sensor> sensorlist = sm.getSensorList(Sensor.TYPE_ALL); ArrayList<Integer> sensorTypeS = new ArrayList<>(); for (Sensor sensor : sensorlist) { //获取传感器类型 int type = sensor.getType(); if (!sensorTypeS.contains(type)) { //发现一种类型则添加一种类型 sensorTypeS.add(type); } } //小米k40 51个传感器类型 //普通的pix 27个 //华为荣耀20 18个传感器 CLog.e("sensor types size -> " + sensorlist.size()); //我们认为传感器少于20个则认为是风险设备 if (sensorlist.size() < SENSOR_MINIMUM_QUANTITY_LIMIT) { items.add(new ListItemBean( "当前手机可能是模拟器&云手机", ListItemBean.RiskLeave.Warn, "sensor size -> ("+ sensorlist.size()+") \n" + "sensor type size -> ("+sensorTypeS.size()+") \n" //+ "sensor info -> \n"+ Sensorlist //打印全部传感器信息 )); }
检测传感器名称 这块检测思路主要是检测传感器的名称,正常小米之类的手机他是不可能存在叫什么 AOSP
的传感器的 。
这种AOSP
基本都是自己编译的ROM
,所以这块也可以作为监测点 。可以上报传感器的一些名称信息,也是环境检测一个很重要的抓手
一般小白肯定不会说去改传感器名称
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { ArrayList<Sensor> aospSensor = new ArrayList<>(); for(Sensor sensor:sensorlist){ if(sensor.getVendor().contains("AOSP")){ aospSensor.add(sensor); } } if (aospSensor.size()>3) { CLog.e("传感器参数是否异常(生产厂商为AOSP)"); items.add(new ListItemBean( "当前手机可能是模拟器&云手机", ListItemBean.RiskLeave.Warn, aospSensor.size() +"/"+sensorlist.size()+"传感器参数异常 -> "+ aospSensor )); } }
检测挂载文件 这块就是去遍历mounts
下面这几个文件,检测里面是否包含docker
关键字 ,防止一些云手机搞虚拟化,通过使用docker
进行挂载 。
这块也是很好的监测点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 String[] marks = {"docker"}; //检测proc/mounts是否包含docker关键字 String mark = NativeEngine.getZhenxiInfoK("/proc/mounts",marks ); if(mark == null){ mark = NativeEngine.getZhenxiInfoK("/proc/self/mountstats", marks); if(mark == null){ mark = NativeEngine.getZhenxiInfoK("/proc/self/mountinfo", marks); } } if(mark!=null){ items.add(new ListItemBean( "当前手机可能是模拟器&云手机", ListItemBean.RiskLeave.Warn, "(mounts异常)\n"+mark )); }
检测ROM是否Match 检测环境信息: 这块思路主要好多种 ,主要是为了防止一些自定义ROM
,通过修改机型的方法,绕过自定义ROM
检测逃逸
可以直接执行getprop
把所有的环境信息都拿到手 ,如果是小米手机,里面环境信息里面,肯定是有MIUI关键字。
比如小米的手机,我会去检测是否包含这几个关键环境信息
1 2 3 private static final String KEY_MIUI_VERSION_NAME = "ro.miui.ui.version.name"; private static final String KEY_MIUI_VERSION_CODE = "ro.miui.ui.version.code"; private static final String KEY_MIUI_INTERNAL_STORAGE = "ro.miui.internal.storage";
这块可以采集以后服务端进行判断。防止自定义ROM 机型伪造
检测服务列表: 这块还是执行 service list
,一般小米手机之类的,都会有小米的系统服务,这种东西很难去伪造,如果他伪造了假的 。你就尝试调用即可
这块还是建议上传到服务端,由服务端算法同学去根据相似度算法去推断,不要再本地进行判断 ,因为Hunter
是非联网Apk
,所以只是在客户端打了个样子
检测当前环境是否被Hook 这块检测方法千奇百怪,首先最基本maps去检测frida或者根据调用栈检测lsp特征 ,基础的检测方案不说了 。因为我觉得并不是一个很好的方案 。改个名就绕过了 。
比如frida特征三件套 。检测思路主要
1 2 3 static const char *FRIDA_THREAD_GUM_JS_LOOP = "gum-js-loop"; static const char *FRIDA_THREAD_GMAIN = "gmain"; static const char *FRIDA_NAMEDPIPE_LINJECTOR = "linjector";
Hook检测,我们其实只需要检测内存没有被修改即可 。这块需要介绍一下基础原理。和实现的伪代码 。
正常我们知道一个SO加载到内存里,本质上是通过mmap把so分配到内存里面 ,比如A函数的指令是BBB,那么加载到内存里面应该也是BBB 。
记住上面这句话 ,我们就可以对内存里面的指令转换成一个int值,然后累加 。如果内存没有被修改 ,累加值文件里面和内存里面的值应该是一样的 。
因为现在Hook基本都是text段和plt端,一个inlinehook
一个got
表 。当然Frida可能会延迟启动 ,所以开启一条检测线程,进行轮训操作。
这块还有一个设计问题,很多开发者也都没注意到 ,就是我开启的这一条线程,被攻击者anti
掉 应该怎么办呢?
因为想要anti掉一条检测线程,方法太多了,N种方法,比如监听全部线程的文件读写,看看那个线程在读取文件 ,只做这一件事,基本八九不离十 ,
也可以直接Hook开启线程的方法,对开启的线程进行anti 。当然这种思路 就没有好的对抗或者检测办法了么?
其实很简单 ,只需要把在你的检测代码里,对某个变量进行赋值 ,修改flag即可 。
然后第一次检测完毕以后将主进程的某个变量标志为true,可以使用__NR_process_vm_writev ,又因为是异步的关系,主线程可以延迟2秒对这个标识进行获取,判断是否为true ,
以确保检测线程成功开启 。
具体检测流程如下,以检测libc为例子,路径可以换成自己需要的路径 :
首先获取本地So文件的累加值 ,返回execSection
结构体 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 //记录可执行段的结构体,一个是plt段一个是text段 //所以对应的数量是2 typedef struct stExecSection { int execSectionCount; unsigned long offset[2]; unsigned long memsize[2]; unsigned long checksum[2]; unsigned long startAddrinMem; bool isSuccess = false; } execSection; /** * 获取本地文件的 Check sum * 读取耗时操作,只初始化一次保存到本地。 */ execSection fetch_checksum_of_library(const char *filePath) { execSection section = {0}; Elf_Ehdr ehdr; Elf_Shdr sectHdr; int fd; int execSectionCount = 0; fd = my_openat(AT_FDCWD, filePath, O_RDONLY, 0); if (fd < 0) { return section; } my_read(fd, &ehdr, sizeof(Elf_Ehdr)); my_lseek(fd, (off_t) ehdr.e_shoff, SEEK_SET); unsigned long memSize[2] = {0}; unsigned long offset[2] = {0}; //查找section的plt和text开始位置和长度 for (int i = 0; i < ehdr.e_shnum; i++) { my_memset(§Hdr, 0, sizeof(Elf_Shdr)); my_read(fd, §Hdr, sizeof(Elf_Shdr)); //通常 PLT and Text 一般都是可执行段 if (sectHdr.sh_flags & SHF_EXECINSTR) { offset[execSectionCount] = sectHdr.sh_offset; memSize[execSectionCount] = sectHdr.sh_size; execSectionCount++; if (execSectionCount == 2) { break; } } } if (execSectionCount == 0) { LOG(INFO) << "get elf section error " << filePath; my_close(fd); return section; } //记录个数 section.execSectionCount = execSectionCount; section.startAddrinMem = 0; for (int i = 0; i < execSectionCount; i++) { my_lseek(fd, (off_t) offset[i], SEEK_SET); //void * buffer = alloca(memSize[i] * sizeof(uint8_t)); //存放text或者plt全部的数据内容,大约5-10M大小,为了兼容小内存手机。 //所以放在堆里面,而不是栈,防止小内存手机栈指针溢出。 auto buffer = (void *) calloc(1, memSize[i] * sizeof(uint8_t)); if (buffer == nullptr) { free(buffer); return section; } my_read(fd, buffer, memSize[i]); section.offset[i] = offset[i]; section.memsize[i] = memSize[i]; section.checksum[i] = checksum(buffer, memSize[i]); // LOGE("fetch_checksum_of_library %s ExecSection:[%d][%ld][%ld][%ld]", // filePath, i, offset[i], memSize[i], section->checksum[i]) free(buffer); } section.isSuccess = true; my_close(fd); return section; }
然后和本地的指令去计算 。计算本地的指令方法就是对maps进行遍历,只遍历text和plt段 ,计算累加值和本地进行判断 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 /** * 检测问的check sum * 检测到check未修改返回0 * 检测已修改返回1 * 检测失败返回-1 */ int detect_elf_checksum(const char *soPath, execSection *pSection) { if (pSection == nullptr) { LOGI("detect_elf_checksum execSection == null "); return -1; } char map[MAX_LINE]; const char *maps_path = string("proc/").append(to_string(getpid())).append("/maps").c_str(); int fd = my_openat(AT_FDCWD, maps_path, O_RDONLY, 0); if (fd <= 0) { LOGE("detect_elf_checksum open %s fail ", PROC_MAPS); return -1; } int checkSum = 0; while ((read_one_line(fd, map, MAX_LINE)) > 0) { if (my_strstr(map, soPath) != nullptr) { checkSum = scan_executable_segments(map, pSection, soPath); if (checkSum == 1) { break; } } } my_close(fd); return checkSum; } /** * 检测问的check sum * 检测到check未修改返回0 * 检测已修改返回1 * 检测失败返回-1 */ int scan_executable_segments( char *mapItem, execSection *pElfSectArr, const char *libraryName) { unsigned long start, end; char buf[MAX_LINE] = ""; char path[MAX_LENGTH] = ""; char tmp[100] = ""; sscanf(mapItem, "%lx-%lx %s %s %s %s %s", &start, &end, buf, tmp, tmp, tmp, path); if (buf[2] == 'x') { if (buf[0] == 'r') { uint8_t *buffer; buffer = (uint8_t *) start; for (int i = 0; i < pElfSectArr->execSectionCount; i++) { if (start + pElfSectArr->offset[i] + pElfSectArr->memsize[i] > end) { if (pElfSectArr->startAddrinMem != 0) { buffer = (uint8_t *) pElfSectArr->startAddrinMem; pElfSectArr->startAddrinMem = 0; break; } } } for (int i = 0; i < pElfSectArr->execSectionCount; i++) { auto begin = (void *) (buffer + pElfSectArr->offset[i]); unsigned long size = pElfSectArr->memsize[i]; LOGI("%s [%p] size ->[%lu]", libraryName, begin, size); //MPROTECT((size_t)begin, size, MEMORY_R); unsigned long output = checksum(begin, size); LOGI("%s checksum:[%ld][%ld]", libraryName, output, pElfSectArr->checksum[i]) if (output != pElfSectArr->checksum[i]) { //和本地的So Checksum 对不上 return 1; } } } return 0; } else { if (buf[0] == 'r') { pElfSectArr->startAddrinMem = start; } } return 0; }
当然这块就一定是比较完善的检测方案了么?其实有很多对抗思路 。比如把maps里面的内存进行隐藏,变成匿名内存,这样他在扫描maps的就找不到,对应的比如上面说的libc的item
这块可以参考riru里面的 map_hide ,代码路径如下
https://github.com/RikkaApps/Riru/blob/master/riru/src/main/cpp/hide_utils.cpp
当然这种方式一定是安全的么?
其实,不是的,因为在他的这个代码里面我发现存在一个遗漏点,可以作为检测入手点 ,某个加固厂商,不仅仅会对maps item
进行遍历 ,还会对里面的匿名内存进行遍历 ,检测匿名内存里面的 magic
,比如so
文件的magic
是elf
,如果magic
匹配上一样当maps
去解析 去遍历。
所以可以在原有的基础上改改,在将内存变成匿名内存以后,把elf的前四个字节抹掉 ,也就是magic的 内容,抹掉以后记得把权限修改成和之前的一样 。防止内存检测 。
检测沙箱 这块检测核心逻辑全部放在ISO
线程检测 。可以配置一个服务,然后服务里使用如下变量即可 。
1 2 3 4 5 <service android:name=".ZhenxiServer" android:isolatedProcess="true" android:useAppZygote="true" />
这块有人可能会问什么是iso线程?可以理解成一个独立的安全的线程 ,只能通过和外部IPC交互的方式进行通讯 。useAppZygote 相当于让这个进程运行在Zygote中 。这个时候时机特别早 ,早到什么程度呢?就连libart.so 都没加载 ,所以这个检测进程只能调用一些原始的libc方法,不能调用任何Art相关的函数 。
下面也是主要介绍一下hunter
的检测思路 。
检测多余线程PID: 主要实现思路就是去检测proc下面是否有除了main进程以外的其他pid ,因为正常启动的话,肯定是只有一个main进程 。
但是沙箱的话会在启动之前去启动别的进程 ,所以这块可以进行bypass 。后面我会统一说这块应该如何对抗 ,包括如何绕过
这块先介绍检测思路 ,和检测原理 。这块里面有一个replaceSecInsns
是封装的一个函数,我担心 opendir
被Hook
了,所以每次执行都去把指令替换成
本地文件的指令,而不是去执行内存里面的指令 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 auto orig_opendir = reinterpret_cast<DIR *(*) (const char *)>(replaceSecInsns(getlibcPath(), "opendir")); DIR *pdr = orig_opendir("/proc"); if (pdr == nullptr) { return getItemData(env, "程序出错请放弃修改后重试", nullptr, true, RISK_LEAVE_DEADLY, TAG_SANDBOX); } auto orig_ator = reinterpret_cast<struct dirent *(*)(DIR *)> (replaceSecInsns(getlibcPath(), "readdir")); dirent *read_ptr; //在app启动之前检测当前app所有的进程,判断是否存在和main不一样的进程 while ((read_ptr = orig_ator(pdr)) != nullptr) { long procPid = strtol(read_ptr->d_name, nullptr, 10); //LOG(INFO) << "find /proc/ child dir " << procPid; //打开成功&&发现一条不等于主进程id的pid if (procPid && procPid != getpid()) { auto title = string("检测到APK存在沙箱内部"); char buff[200]; getNameByPid((pid_t) procPid, buff); LOG(ERROR) << ">>>>> FIND OTHER THREAD SANDBOX " << procPid << " " << buff; auto &data = string("当前线程主进程PID(").append(to_string(getpid())).append(")").append("\n"). append("异常pid -> ").append(to_string(procPid)). append("(").append("pid name: ").append(buff).append(")"); //可能存在多个异常pid而非一个 getItemData(env, title, data.c_str(), false, RISK_LEAVE_WARN, TAG_SANDBOX); } } closedir(pdr);
对抗: 这块想要绕过也很简单,opendir
底层其实就是 getdents64,getdents
如果遇到你想隐藏的文件直接对文件进行bypass
就可以了,直接指向下一个指针
代码来自proot
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 case SC_getdents64: case SC_getdents: { /* get the result of the syscall, which is the number of bytes read by getdents * 获取系统调用的结果,即getdents读取的字节数 * */ unsigned int res = peek_reg(tracee, CURRENT, SYSARG_RESULT); if (res <= 0) { break; } /* get the system call arguments */ word_t orig_start = peek_reg(tracee, CURRENT, SYSARG_2); unsigned int count = peek_reg(tracee, CURRENT, SYSARG_3); char orig[count]; char path[PATH_MAX]; int status = readlink_proc_pid_fd(tracee->pid, (int) peek_reg(tracee, ORIGINAL, SYSARG_1), path); if (status < 0) { break; } //不属于绑定路径则不处理,这块应该加个判断。如果这个path没有处理的路径应该返回 。 //目前需要处理的只有proc,用于隐藏调试线程。 // if(!belongs_to_guestfs(tracee, path)) // return 0; if (!StringUtils::containsInsensitive(path,"proc")){ break; } /* retrieve the data from getdents * 从getdents检索数据 * */ status = read_data(tracee, orig, orig_start, res); if (status < 0) { break; } /* allocate a space for the copy of the data we want * 为我们想要的数据的副本分配一个空间 * */ char copy[count]; /* curr will hold the current struct we're examining * curr将保存我们正在检查的当前结构 * */ struct linux_dirent64 *curr64; struct linux_dirent *curr32; /* pos keeps track of where in memory the copy is * pos跟踪副本在内存中的位置 * */ char *pos = copy; /* ptr keeps track of where in memory the original is * ptr跟踪原始文件在内存中的位置 * */ char *ptr = orig; /* nleft keeps track of how many bytes we've saved * nleft跟踪我们保存了多少字节 * */ unsigned int nleft = 0; /* while we're still within the memory allowed * 当我们还在允许的memory范围内时 * */ if (get_sysnum(tracee, ORIGINAL) == SC_getdents64) { while (ptr < orig + res) { /* get the current struct * 获取当前结构 * */ curr64 = (struct linux_dirent64 *) ptr; /* if the name does not matche a given prefix * 如果名称与给定前缀不匹配 * * 如果这个目录项的名称不以HIDDEN_PREFIX开始, * 也就是hasprefix(HIDDEN_PREFIX, curr64->d_name)返回false, * 那么这个目录项会被保留在结果中,否则,这个目录项会被忽略,也就是隐藏。 * */ //if (!hasprefix(HIDDEN_PREFIX, curr64->d_name)) { if (!isRuntimeHideDir(tracee->pid,curr64->d_name, tracer_pc(tracee), tracer_lr(tracee))) { /* copy the information * 复制信息 * */ mybcopy(ptr, pos, curr64->d_reclen); /* move the pos and nleft */ pos += curr64->d_reclen; nleft += curr64->d_reclen; } /* move to the next linux_dirent * 隐藏这个目录,指向下一个文件 * */ ptr += curr64->d_reclen; } } else { while (ptr < orig + res) { /* get the current struct */ curr32 = (struct linux_dirent *) ptr; /* if the name does not matche a given prefix */ // if (!hasprefix(HIDDEN_PREFIX, curr64->d_name)) { if (!isRuntimeHideDir(tracee->pid,curr32->d_name, tracer_pc(tracee), tracer_lr(tracee))) { /* copy the information */ mybcopy(ptr, pos, curr32->d_reclen); /* move the pos and nleft */ pos += curr32->d_reclen; nleft += curr32->d_reclen; } /* move to the next linux_dirent */ ptr += curr32->d_reclen; } } /* If there is nothing left * 这个文件夹里面本身没有东西,暂不处理 * */ if (!nleft) { // /* call getdents again */ // if (get_sysnum(tracee, ORIGINAL) == PR_getdents64) // register_chained_syscall(tracee, PR_getdents64, peek_reg(tracee, ORIGINAL, SYSARG_1), orig_start, count, 0, 0, 0); // else // register_chained_syscall(tracee, PR_getdents, peek_reg(tracee, ORIGINAL, SYSARG_1), orig_start, count, 0, 0, 0); } else { /* copy the data back into the register */ status = write_data(tracee, orig_start, copy, nleft); if (status < 0) { break; } /* update the return value to match the data */ poke_reg(tracee, SYSARG_RESULT, nleft); } break; }
执行ps命令: 这块的思路就是检测是否存在其他进程 ,通过执行ps -ef
命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 if (get_sdk_level() > ANDROID_Q) { //check process auto orig_popen = reinterpret_cast<FILE *(*)(const char *, const char *)>(replaceSecInsns(getlibcPath(), "popen")); FILE *file = orig_popen("ps -ef", "r"); if (file == nullptr) { return getItemData(env, "程序出错请放弃修改后重试", "ps error", true, 3, TAG_SANDBOX); } char buf[0x1000]; string buffStr; uint size = 0; // get process count size while (fgets(buf, sizeof(buf), file)) { buffStr.append(buf).append("\n"); //不包含++ if(!StringUtils::contains(buf,"hunter")){ size++; LOG(INFO) << "ps -ef match -> %s " << buf; } } if (size > 2) { //UID PID PPID C STIME TTY TIME CMD //u0_a531 6187 885 72 10:27:53 ? 00:00:00 com.zhenxi.hunter //u0_a531 6236 6187 0 10:27:53 ? 00:00:00 ps -ef pclose(file); return getItemData(env, "检测到APK存在沙箱内部(异常线程)", buffStr.c_str(), false, RISK_LEAVE_DEADLY, TAG_SANDBOX); } LOG(ERROR) << ">>>>> NOT FIND SANDBOX "; pclose(file); }
对抗: 可以先伪造一个正常的文件,在执行到 ps -ef的时候把命令换成cat 你自己的文件 即可 。
内存Choose: 这块的实现思路就是,检测内存里面的 Activity 或者 Application的个数 。正常我们的apk启动只会有我们自己的
Application,沙箱因为预启动的关系,会存在其他的Application ,这块也是一个很好的检测思路 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public static ListItemBean checkSandbox() { ArrayList<Object> choose; try { choose = ChooseUtils.choose(Activity.class, true); } catch (Throwable e) { return null; } if (choose != null) { ArrayList<Object> list = new ArrayList<>(); //移除我们的Activity,把其他的activity实例加到List里面 for(Object activty:choose){ String name = activty.getClass().getName(); if(!name.equals(MainActivity.class.getName())&& !name.equals(ShareActivity.class.getName())&& !name.equals(FeedbackActivity.class.getName()) ){ list.add(activty); } } //判断list数量是否大于1 if (list.size() >= 1) { ListItemBean item = new ListItemBean( "检测到APK存在沙箱内部", ListItemBean.RiskLeave.Deadly); for (Object obj : list) { item.putData(obj.getClass().getName()); } return item; } } return null; }
检测Google&lineageos 因为在国内手机里基本Google
和lineageos
都是黑产的标配 ,大部分都是自己刷的ROM
这种手机理论上在国内 不应该出现,如果出现也会被打上标签,被认定为黑产 。可以直接Build.MODEL 获取厂商
另外这块还有一个细节点 ,就是lineageos
一个特有文件/system/addon.d
。具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private static void isLineageOs(ArrayList<ListItemBean> items) { //lineage检测 String display = NativeEngine.getZhenxiInfoH("ro.build.display.id"); if (display.toLowerCase(Locale.ROOT).contains("lineage")) { items.add( new ListItemBean("当前手机为Lineage系统", ListItemBean.RiskLeave.Warn, "可能存在自定义ROM,当前设备不可信!" )); return; } //lineage 特有文件 File file = new File("/system/addon.d"); if (file.exists()) { items.add( new ListItemBean("当前手机为Lineage系统", ListItemBean.RiskLeave.Warn, "可能存在自定义ROM,当前设备不可信!" )); return; } }
当然这块有一个细节点:
直接获取 ro.build.display.id
可能会被Hook
,所以这块我的建议是直接去解析配置文件
核心解析(/dev/properties /u:object_r: )文件方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public PropArea(String area) throws IOException { area = "/dev/__properties__/u:object_r:" + area + ":s0"; File file = new File(area); if (!file.isFile()) throw new FileNotFoundException("Not a file: " + area); long size = file.length(); if (size <= 0 || size >= 0x7fffffffL) throw new IllegalArgumentException("invalid file size " + size); try (FileChannel channel = new FileInputStream(area).getChannel()) { data = channel.map(FileChannel.MapMode.READ_ONLY, 0, size).order(ByteOrder.nativeOrder()); } byteUsed = data.getInt(); data.getInt(); // serial int magic = data.getInt(); if (magic != PROP_AREA_MAGIC) throw new IllegalArgumentException("Bad file magic: " + magic); int version = data.getInt(); if (version != PROP_AREA_VERSION) throw new IllegalArgumentException("Bad area versin: " + version); data.position(data.position() + 28); // reserved } public List<String> findPossibleValues(String name) { List<String> values ; try { // atomic_uint_least32_t serial; // union { // char value[PROP_VALUE_MAX]; // struct { // char error_message[kLongLegacyErrorBufferSize]; // uint32_t offset; // } long_property; // }; final int LONG_PROP_FLAG = 1 << 16; final int PROP_VALUE_MAX = 92; final int VALUE_OFFSET = 4; final int NAME_OFFSET = VALUE_OFFSET + 92; values = new ArrayList<>(2); findFromBuffer(data.slice(), name.getBytes(StandardCharsets.UTF_8), (buffer, offset) -> { if (offset < NAME_OFFSET) return; int base = offset - NAME_OFFSET; int serial = buffer.getInt(base); if ((serial & LONG_PROP_FLAG) != 0) return; // Long properties are not supported values.add(toString(buffer, base + VALUE_OFFSET, PROP_VALUE_MAX)); }); CLog.i("Found " + name + "=" + values); return values; } catch (Throwable e) { CLog.e("findPossibleValues get error "+ name+" "+e); return null; } }
检测IDA和反调试: 这块检测方法太多了,比如核心A和B方法里面加个时间戳,如果A执行到B大于5秒就可以认为被调试 。
检测调试状态也是很不错的选择,不过这块可以利用ISO
线程去检测调试状态,防止被ptrace
service list 也可以执行 “service list
“ 获取服务列表 ,判断是否包含frida关键字 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public static CheckServerRet checkServer(Context context, String[] fit) { CheckServerRet ret = new CheckServerRet(); ArrayList<String> list = ret.list; BufferedReader reader = null; Process process; try { // 执行 service list 命令 process = Runtime.getRuntime().exec("service list"); // 读取命令输出结果 reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { if (line.length() > 1) { for (String fitItem : fit) { //比较不区分大小写 if (line.toLowerCase().contains(fitItem.toLowerCase())) { list.add(line); //CLog.e("checkServer find item is match " + Arrays.toString(fit) + " " + line); break; } else { //走到这里说明能拿到。返回的不是空 ret.isSuccess = true; } } } } // 关闭流 reader.close(); // 等待命令执行完毕 int exitValue = process.waitFor(); CLog.e("checkServer ret mark " + Arrays.toString(fit) + " " + ret); return ret; } catch (IOException | InterruptedException e) { e.printStackTrace(); if (reader != null) { try { reader.close(); } catch (IOException ignored) { } } } return ret; }
设备指纹3 前置知识 IPC代理&IPC协议是什么? Android是基础的CS架构,客户端和服务端架构 。安卓为什么要这么设计呢?如果服务端和客户端在一个进程内,客户端崩溃了,服务端也会一起崩溃,导致整个系统不稳定
安卓和Java相比多个一个Context,这个Context是调用安卓本身提供api的桥梁 ,里面有各种安卓系统提供的各种基础API
这些API可以直接操作Android系统 ,安卓本身通过各种各样的Manager去提供对应的Api去获取和修改 。比如PackageManager,ActivityManager
等,这些Manager里面都会持有一个代理人 。当我们去调用这个Manager里面的一些Api的时候,一些简单的Api他会尝试去自己在本进程Native或者Java去实现,如果一些复杂的字段,比如查询系统的一些信息,或者调用一些系统关键函数,这种时候他会去调用“IPC代理人 ”,这个IPC代理人就是像服务端通讯的关键 。他相当于是向服务端的传话得人 ,代理设计模式 。对不同的Manager提供不一样的功能 ,而他传的话就是对应的IPC协议 。这个协议如何传递的,就是通过底层的共享内存Binder
去实现的 。
而这个协议里面具体发送的内容,就是IPC协议装的“包裹”就是用的Parcel
。
这块举个栗子,当用户调用一个未初始化的API
时候,需要跨进程通讯,到底发生了哪些动作
用户调用系统API->Manager收到调用消息->判断是否需要调用服务端->调用IPC代理里面的方法->IPC代理构建发送的数据包调用Binder进行通讯数据写入以后返回
这个IPC代理实现了Binder的接口,当前进程调用的最后一个API就是
1 "android.os.BinderProxy"->transact
也就是说这个方法底层调用的是Binder的驱动,最终会去native层写入,剩下的就是开始运行服务端的逻辑了。把数据写入到transact方法的参数3里面。然后程序返回,下面是这个方法的原型
之前通过hook binder来bypass指纹,有一个方法就是通过hook这个transact
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 /** * Perform a binder transaction on a proxy. * * @param code The action to perform. This should * be a number between {@link #FIRST_CALL_TRANSACTION} and * {@link #LAST_CALL_TRANSACTION}. * @param data Marshalled data to send to the target. Must not be null. * If you are not sending any data, you must create an empty Parcel * that is given here. * @param reply Marshalled data to be received from the target. May be * null if you are not interested in the return value. * @param flags Additional operation flags. Either 0 for a normal * RPC, or {@link #FLAG_ONEWAY} for a one-way RPC. * * @return * @throws RemoteException */ public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { ... }
第一个参数就是code,这个code指的是具体的事件类型 ,不同的事件传入的数字也不一样
第二参数是发送的数据包 ,这时候IPC已经往里面进行了写入对应的数据包
第三个参数是reply,也就是服务端返回的保存内容
注意:
这块可能存在一个问题,有的数据,这个时候你在after去覆写这个参数3已结晚了,因为数据在别的进程已结写入了 。 如果你想对这个参数进行修改,最好的办法是直接模拟服务端手动往reply进行写入
第四个参数是flags,举个例子,比如你想获取正常的PackageInfo
,不需要获取签名之类的信息,你传入0即可
如果想获取签名就需要传入PackageManager.GET_SIGNATURES
,这个flag相当于告诉服务端,都需要哪些功能
1 2 getPackageManager().getPackageInfo("aaa",0); getPackageManager().getPackageInfo("aaa",PackageManager.GET_SIGNATURES);
比如第一个Api获取到的PackageInfo
里面是不包含签名信息的,第二个则包含 。当你需要什么功能的时候使用 “或” 连接即可。
这块我们得到一个结论,这个方法是Java层通讯最后一个方法,也就是当前进程能操作的最后一个方法,剩下的就是服务端进程的事情了。这个方法是Java层的 “边界值 ” ,这个边界值记住后面在总结里面会介绍到不同的边界值和风控的关系。
这块还有的大厂更恶心,他不走transact方法,因为transact方法底层走的就是Binder,可以直接在Native层调用的Binder 驱动,实现了transact 这个方法 。然后进行IPC通讯,直接不走Java层 。
当然他这种方法也是很不稳定,需要对每个android 版本都进行兼容,属于伤敌1000自损800类型 ,适配难度也很大。
随着安卓不断增强安全性,后面这种方式肯定会慢慢被PASS掉 ,现在利用跨进程在低版本越权App的太多了 。
动态代理IPC 就是我不用hook可以实现IPC的代理人替换么?
这块有一个动态代理的知识点,就是他代理人本身是实现了一个接口,我们可以直接反射把他这个代理人给替换成我们的 ,然后我们使用Proxy.newProxyInstance
动态代理这个接口类,也可以实现不需要Hook框架的情况下实现动态代理 。比如一些VA之类的用的就是这种,因为Hook其实稳定性啥的没有动态代理的稳定性好,Hook的话需要对不同版本兼容,一旦版本发生变化需要适配很多东西,而动态代理则不需要。
Hook的话痕迹可能更少一点,动态代理检测的话只需要反射这个IPC代理人,然后getClass().getName() 里面直接就有proxy之类的关键字 ,各有各的好处
设备指纹 IPCAndroid_Id 方法5: 直接构建IPC协议和服务端进行通讯 ,这块targetSdkVersion
必须升级到32以上
因为getAttributionSource
这个玩意32版本以上好像才有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 public String getAndroidId5(Context context) { try { // Acquire the ContentProvider Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); Method currentActivityThreadMethod = activityThreadClass.getMethod("currentActivityThread"); Object currentActivityThread = currentActivityThreadMethod.invoke(null); // 通过ActivityThread获取一个指向远程"settings" ContentProvider的代理对象 Method acquireProviderMethod = activityThreadClass.getMethod("acquireProvider", Context.class, String.class, int.class, boolean.class); Object provider = acquireProviderMethod.invoke(currentActivityThread, context, "settings", 0, true); // 获取IContentProvider接口的Class对象,为后续获取Binder通信的“事务码”做准备 Class<?> iContentProviderClass = Class.forName("android.content.IContentProvider"); // ContentProvider的代理对象内部有一个名为mRemote的成员变量,它就是真正的Binder通信对象 Field mRemoteField = provider.getClass().getDeclaredField("mRemote"); mRemoteField.setAccessible(true); IBinder binder = (IBinder) mRemoteField.get(provider); // Create the Parcel for the arguments Parcel data = Parcel.obtain(); data.writeInterfaceToken("android.content.IContentProvider"); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { context.getAttributionSource().writeToParcel(data, 0); // 1. 写入归因源 data.writeString("settings"); // 2. 写入 authority data.writeString("GET_secure"); // 3. 写入方法名 data.writeString("android_id"); // 4. 写入参数 data.writeBundle(Bundle.EMPTY); // 5. 写入额外Bundle } else if (android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.R) {// API 30 //android 11 data.writeString(context.getPackageName()); data.writeString(null); //featureId data.writeString("settings"); //authority data.writeString("GET_secure"); //method data.writeString("android_id"); //stringArg data.writeBundle(Bundle.EMPTY); } else if (android.os.Build.VERSION.SDK_INT == android.os.Build.VERSION_CODES.Q) { // API 29 //android 10 data.writeString(context.getPackageName()); data.writeString("settings"); //authority data.writeString("GET_secure"); //method data.writeString("android_id"); //stringArg data.writeBundle(Bundle.EMPTY); } else { // 更早版本 data.writeString(context.getPackageName()); data.writeString("GET_secure"); //method data.writeString("android_id"); //stringArg data.writeBundle(Bundle.EMPTY); } Parcel reply = Parcel.obtain(); // 调用transact向一个IBinder象(这里的binder变量是IBinder类)发送请求 binder.transact((int) iContentProviderClass.getDeclaredField("CALL_TRANSACTION").get(null), data, reply, 0); reply.readException(); Bundle bundle = reply.readBundle(); reply.recycle(); data.recycle(); return bundle.getString("value"); } catch (Exception e) { e.printStackTrace(); return null; } }
分析:标准获取Android ID的标准、公开方法是
1 2 3 import android.provider.Settings; String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
这个标准方法内部其实也是通过 ContentResolver
-> ContentProvider
的机制,最终通过 Binder IPC 与系统服务(具体是 SettingsProvider
)进行通信来获取值的。
我们调用 IBinder.transact()
给一个 IBinder 对象发送请求,然后经过 Binder Binder.onTransact()
得到调用,接着远程操作的目标得到对应的调用
IPCAppSign IPC获取签名也是一些大厂经常用检测签名的办法,修改的话也很简单,直接替换掉数据包即可
具体获取方法如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public static int TRANSACTION_getPackageInfo() { if(TRANSACTION_getPackageInfo == -1) { try { Field field = null; try { Class<?> pkmIPCClazz = Class.forName("android.content.pm.IPackageManager$Stub"); field = pkmIPCClazz.getDeclaredField("TRANSACTION_getPackageInfo"); } catch (Throwable e) { CLog.e(">>>>>>>>>> getTranscationId forName error " + e.getMessage()); } assert field != null; field.setAccessible(true); TRANSACTION_getPackageInfo = field.getInt(null); } catch (Throwable e) { e.printStackTrace(); CLog.e(">>>>>>>>>> getTranscationId error " + e.getMessage()); } } return TRANSACTION_getPackageInfo; } try { PackageManager packageManager = getBaseContext().getPackageManager(); // 1. 获取底层的 IPackageManager 代理对象 Object IPC_PM_Obj = RposedHelpers.getObjectField(packageManager, "mPM"); // 2. 从代理对象中获取真正的 IBinder 对象 IBinder mRemote = (IBinder) RposedHelpers.getObjectField(IPC_PM_Obj, "mRemote"); // 3. 准备数据 Parcel 和回复 Parcel Parcel _data = Parcel.obtain(); Parcel _reply = Parcel.obtain(); // 4. 按照 AIDL 协定,手动打包参数到 _data 中 _data.writeInterfaceToken("android.content.pm.IPackageManager"); // 写入接口令牌,用于验证 _data.writeString(getPackageName()); // 参数1: packageName (String) _data.writeLong(PackageManager.GET_SIGNATURES); // 参数2: flags (long), 注意这里用了writeLong _data.writeInt(android.os.Process.myUid()); // 参数3: userId (int) // 5. 发起远程调用 boolean _status = mRemote.transact(TransactCase.TRANSACTION_getPackageInfo(), _data, _reply, 0); _reply.readException(); // 检查远程调用是否发生异常 // 6. 从 _reply 中解包返回值 PackageInfo packageInfo = _reply.readTypedObject(PackageInfo.CREATOR); // 7. 回收 Parcel 对象 _data.recycle(); _reply.recycle(); CLog.e("签名信息: "+packageInfo.signatures[0].toCharsString()); } catch (Throwable e) { CLog.i("IPC_TEST_getPackageInfo error "+e); }
绕过高层 API Hook,直接通过底层 Binder IPC (进程间通信) 来获取应用签名信息
什么是 Transaction Code?
在 Android 的 Binder IPC 机制中,客户端(App)调用服务端(System Service)的方法时,并不是直接传递方法名。为了效率,每个方法都被映射到一个唯一的整数ID,这个ID就叫做 Transaction Code
。当客户端发起 transact
调用时,它会把这个整数ID和参数一起打包发送给服务端,服务端根据这个ID就知道要调用哪个具体的方法。
为什么要用反射获取?
TRANSACTION_getPackageInfo
是 IPackageManager.aidl
文件在编译后自动生成的 IPackageManager$Stub
类中的一个私有静态常量。它的值在不同 Android 版本中可能 会改变(虽然实践中很少改变)。如果直接在代码里硬编码一个整数值(例如 int a = 14;
),那么当应用运行在一个该值已改变的 Android 系统上时,调用就会失败。 因此,通过反射去动态获取当前运行环境下的确切值,是一种兼容性更好、更稳健 的做法。
Maps解析Apk签名 这块还有一个方案,主要实现思路就是因为我们当前进程去打开apk是存在风险的 。
很有可能被IO重定向,导致得到的签名是错误的,所以我们可以让三方进程去加载当前apk文件,通过共享内存的方式,然后当前进程对apk文件maps里面的内存签名进行解析即可 。这块需要双进程通讯 。
其他字段IPC 根据上面的两个经典IPC例子,可以发现只要是服务端获取的都可以使用IPC协议 的方式去获取 。
其他字段其实一样也可以这么玩 ,如果需要什么字段,就对照安卓源码客户端往里面写入对应的数据,直接IPC即可
分析SO文件的时候直接对jni交互进行监听,在保存的调用栈里面看
他如果调用了Parcel.obtain()
初始化或者 这种writeLong ()
写入数据的方法,基本就可以确认他是IPC获取的一些字段,具体看他写入的内容是什么,或者看他写入的token是什么,比如上面的获取签名的token就是"android.content.pm.IPackageManager"
,即可知道他想做什么字段的获取
IPC总结&反思 后来又思考了一下,IPC服务端这些设备指纹,或者说这些配置到底哪里来的,一直在源码里面跟。
发现就拿android id来说,他最终读取的文件路径是/data/system/users/0/settings_ssaid.xml
,这个目录下,/data/system/users/0/
我发现这里面全是各种注册表和各种配置信息。我这边尝试改了一下里面的android id
。然后直接手机重启 ,我发现我之前自己写的Hunter
获取的设备指纹android id
竟然变了
后来我把这些文件都拷贝出来,把里面熟悉的值都随机了一份,通过magisk 插件系统文件替换的方式,对文件/data/system/users/0/
进行替换 ,真没想到以前被封的设备解封了。而且不需要回复出厂设置,只需要软重启 一下就行
而且基本可以做到无痕 ,因为没有对apk任何修改,改的全是系统级别的变量,而我只需要Root ,替换系统文件,相当于每一次都是恢复出厂设置
现在基本大厂想要在回复出厂设置保持设备指纹不变基本不可能 。这套方案我测试过一段时间,现阶段基本大厂从客户端角度基本没办法对抗,只能靠一些服务端指纹去做检测。
服务端级别设备指纹 服务端去获取客户端的IPV6信息,配合客户端上报。IPV6号称能给世界上每粒沙子分配一个ip,2的128次方
可以在设备指纹初始化的时候调用一下接口,服务端网关层去获取IpV6信息 。将信息保存作为客户端设备指纹
tls最新版+socket进行通讯 ,用这个ipv6作为设备指纹 。当然这块也需要防代理,具体的方案也很多,比如一些大厂会去购买一些代理IP,试用的时候去请求自己的网关,然后把这些IP都拉黑
Hunter检测&反制 这块主要是介绍一些比较新奇的对抗和检测 ,也是我之前在做黑产对抗的时候发现的一些办法
MapIo重定向Anti 一般在实现一机多号的时候,因为需要对不同的账号进行IO重定向 ,把不同的账号,保存到自己的虚拟分身里面
这时候如果你要读取Maps
去遍历Item
的时候就会发现这个Item
异常,一般沙箱开发者会将MapsIo
重定向
当发现读取Maps
的时候指向自己的文件,因为这个Maps是不断变化的,所以需要在svc openat
这块进行拦截生成一份新的。然后指向到这份新的文件,在新的maps
里面他会对里面的item
路径进行反转,转换成正常的目录,而不是包含沙箱的目录,导致获取的数据被欺骗
这块读文件偏移完全可以不读取Maps
,而是读取proc/self/maps_files
对这个文件进行opendir
,对每个文件进行遍历,然后再路径拼接,通过readlinkat
去反查路径即可
“反调试”进程检测实现细节 一般Apk
都会开启一条线程作为检测反调试线程,这条线每隔几秒对线程进行一些特征进行检查当前进程是否被调试
有很多攻击者会Hook
线程创建的办法,然后在线程启动的时候进行pass
,不让其启动,以实现逃过检测的办法
这种情况其实对抗也很简单,可以在主线程搞个flag
,只有在调试线程开启的时候,并且检测执行成功的时候使用process_vm_writev
对flag
进行写入
因为是异步,所以主线程可以延迟2秒钟对这个flag
进行检测,判断调试线程是否开启,如果没开启上报埋点即可
自实现RegisterNativeMethod 我们正常注册一个native方法是调用的env->RegisterNatives
,但是这种直接api调用很有可能被Hook
所以我们可以自己实现一份 ,因为native注册底层本质上是给artmethod
里面的fnptr
进行赋值,最终调用artmethod
里面的RegisterNative
方法,所以我们可以不直接调用Jni
直接走artmethod
里面的注册方法
具体实现如下,因为artmethod
里面的注册方法每个版本的实现都不一样 ,所以这块需要根据不同版本进行case
分发
1 2 3 4 5 6 7 8 9 10 //call art method register if (!RegisterNativeMethod(env, NativeEngine, SignatureFixMethods, sizeof(SignatureFixMethods) / sizeof(SignatureFixMethods[0]))) { LOG(ERROR) << "JNI_OnLoad call art method register fail ,start env register natives! "; env->RegisterNatives(NativeEngine, SignatureFixMethods, sizeof(SignatureFixMethods) / sizeof(SignatureFixMethods[0])); }
调用的话很简单直接尝试调用我们自己实现的方法,如果失败了则调用系统的api ,这样可以有效防止jni
被hook
实现,jni RegisterNative
函数被监听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 // // Created by Zhenxi on 2022/8/22. // #include <jni.h> #include "../include/logging.h" #include "../include/libpath.h" #include "../include/dlfcn_compat.h" #include "../include/version.h" #include "../include/main.h" static void *art_method_register = nullptr; static void *class_linker_ = nullptr; size_t OffsetOfJavaVm(bool has_small_irt, int SDK_INT) { if (has_small_irt) { switch (SDK_INT) { case ANDROID_T: case ANDROID_SL: case ANDROID_S: return sizeof(void *) == 8 ? 624 : 300; case ANDROID_R: case ANDROID_Q: return sizeof(void *) == 8 ? 528 : 304; default: LOGE("OffsetOfJavaVM Unexpected android version %d", SDK_INT); abort(); } } else { switch (SDK_INT) { case ANDROID_T: case ANDROID_SL: case ANDROID_S: return sizeof(void *) == 8 ? 520 : 300; case ANDROID_R: case ANDROID_Q: return sizeof(void *) == 8 ? 496 : 288; default: LOGE("OffsetOfJavaVM Unexpected android version %d", SDK_INT); abort(); } } } template<typename T> int findOffset(void *start, size_t len, size_t step, T value) { if (nullptr == start) { return -1; } for (int i = 0; i <= len; i += step) { T current_value = *reinterpret_cast<T *>((size_t) start + i); if (value == current_value) { return i; } } return -1; } /** * 根据runtime获取class_linker * https://github.com/magician8520/BlackBox/blob/99f26925aa303fd0a71543e3713ef3fc57a08e81/Bcore/pine-core/src/main/cpp/android.h#L36 */ void *getClassLinker() { if (class_linker_ != nullptr) { return class_linker_; } int SDK_INT = get_sdk_level(); // If SmallIrtAllocator symbols can be found, then the ROM has merged commit "Initially allocate smaller local IRT" // This commit added a pointer member between `class_linker_` and `java_vm_`. Need to calibrate offset here. // https://android.googlesource.com/platform/art/+/4dcac3629ea5925e47b522073f3c49420e998911 // https://github.com/crdroidandroid/android_art/commit/aa7999027fa830d0419c9518ab56ceb7fcf6f7f1 bool has_smaller_irt = getSymCompat(getlibArtPath(), "_ZN3art17SmallIrtAllocator10DeallocateEPNS_8IrtEntryE") != nullptr; size_t jvm_offset = OffsetOfJavaVm(has_smaller_irt, SDK_INT); auto runtime_instance_ = *reinterpret_cast<void **> (getSymCompat(getlibArtPath(), "_ZN3art7Runtime9instance_E")); auto val = jvm_offset ? reinterpret_cast<std::unique_ptr<JavaVM> *>( reinterpret_cast<uintptr_t>(runtime_instance_) + jvm_offset)->get() : nullptr; if (val == getVm()) { LOGD("JavaVM offset matches the default offset"); } else { LOGW("JavaVM offset mismatches the default offset, try search the memory of Runtime"); int offset = findOffset(runtime_instance_, 1024, 4, getVm()); if (offset == -1) { LOGE("Failed to find java vm from Runtime"); return nullptr; } jvm_offset = offset; LOGW("Found JavaVM in Runtime at %zu", jvm_offset); } const size_t kDifference = has_smaller_irt ? sizeof(std::unique_ptr<void>) + sizeof(void *) * 3 : SDK_INT == ANDROID_Q ? sizeof(void *) * 2 : sizeof(std::unique_ptr<void>) + sizeof(void *) * 2; class_linker_ = *reinterpret_cast<void **>(reinterpret_cast<uintptr_t>(runtime_instance_) + jvm_offset - kDifference); return class_linker_; } bool call_MethodRegister(JNIEnv *env, void *art_method, void *native_method) { if (art_method_register == nullptr) { if (get_sdk_level() < ANDROID_S) { //android 11 art_method_register = getSymCompat(getlibArtPath(), "_ZN3art9ArtMethod14RegisterNativeEPKv"); if (art_method_register == nullptr) { art_method_register = getSymCompat(getlibArtPath(), "_ZN3art9ArtMethod14RegisterNativeEPKvb"); } } else { //12以上还是在libart里面,但是在linker里面实现,符号名称存在变化 art_method_register = getSymCompat(getlibArtPath(), "_ZN3art11ClassLinker14RegisterNativeEPNS_6ThreadEPNS_9ArtMethodEPKv"); } if (art_method_register == nullptr) { LOG(ERROR) << "register native method get art_method_register = null "; return false; } } if (get_sdk_level() >= ANDROID_S) { //12以上 //const void* RegisterNative(Thread* self, ArtMethod* method, const void* native_method) auto call = reinterpret_cast<void *(*)(void *, void *, void *, void *)>(art_method_register); //get self thread void *self = getSymCompat(getlibArtPath(), "_ZN3art6Thread14CurrentFromGdbEv"); if (self == nullptr) { LOG(ERROR) << "register native method get CurrentFromGdb = null "; return false; } //手动计算一下linker实例地址 void *classLinker = getClassLinker(); if (classLinker == nullptr) { LOG(ERROR) << "register native method get getClassLinker = null "; return false; } call(classLinker, self, art_method, native_method); //LOG(ERROR) << "register native method get getClassLinker success! "; } else if (get_sdk_level() >= ANDROID_R) { auto call = reinterpret_cast<void *(*)(void *, void *)>(art_method_register); call(art_method, native_method); } else { auto call = reinterpret_cast<void *(*)(void *, void *, bool)>(art_method_register); call(art_method, native_method, true); } return true; } inline static bool IsIndexId(jmethodID mid) { return ((reinterpret_cast<uintptr_t>(mid) % 2) != 0); } static jfieldID field_art_method = nullptr; bool RegisterNativeMethod(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, size_t nMethods) { if (env == nullptr) { LOG(ERROR) << "register native method JNIEnv = null "; return false; } void *arm_method = nullptr; for (int i = 0; i < nMethods; i++) { jmethodID methodId = env->GetMethodID(clazz, methods[i].name, methods[i].signature); if (methodId == nullptr) { //maybe static env->ExceptionClear(); methodId = env->GetStaticMethodID(clazz, methods[i].name, methods[i].signature); if (methodId == nullptr) { LOG(ERROR) << "register native method get orig method == null " << methods[i].signature; env->ExceptionClear(); return false; } } if (get_sdk_level() >= ANDROID_R) { if (field_art_method == nullptr) { jclass pClazz = env->FindClass("java/lang/reflect/Executable"); field_art_method = env->GetFieldID(pClazz, "artMethod", "J"); } if (field_art_method == nullptr) { LOG(ERROR) << "register native method get artMethod == null "; return false; } if (IsIndexId(methodId)) { jobject method = env->ToReflectedMethod(clazz, methodId, true); arm_method = reinterpret_cast<void *>(env->GetLongField(method, field_art_method)); //LOG(ERROR) << "arm_method "<<arm_method ; } } else { arm_method = methodId; } if (arm_method == nullptr) { LOG(ERROR) << "register native method art method == null "; return false; } if (!call_MethodRegister(env, arm_method, methods[i].fnPtr)) { LOG(ERROR) << "register native method fail " << methods[i].name << " " << methods[i].signature; return false; } // LOG(INFO) << "register native method success " << methods[i].name << " " // << methods[i].signature; } return true; }
通过牺牲稳定性和可移植性来换取高度的隐蔽性
无Root情况客户端对抗的边界值 如果在不Root的情况下,注入方法主要两种,重打包或者把Apk放到沙箱里面
主要的三个核心功能组成水桶木板分别如下
第一项每个大厂都不一样,根据不同的策略每个字段的比重占比也都不一样 。
把一些常见的或者第一篇和第二篇提到的对着改一下即可
第三项现在So层基本大厂都差不多,都是各种混淆配合控制流 ,但是Java层防护做的不够,参考https://bbs.kanxue.com/thread-255514.htm,可以直接废掉Jadx反编译软件
这块重点介绍一下第二项,包括一些常见的子项 ,每个子项还可以继续划分各种检测方式 。
环境&风险检测能力
重打包检测能力
Hook检测能力 。
模拟器&云手机&自定义ROM检测能力
多开&沙箱检测能力
风险Apk检测能力
上面说的这几项便是不同”气味“的组成部分,而这个”气味”采集方式的边界值 又分为三部分 。
Java层就是IPC协议 ,因为IPC协议是当前进程可以操作的最后一个方法 。剩下的就是服务端给喂数据了。
Native层就是SVC拦截 ,因为SVC是Linux进入内核的最后一条指令。
还有一种是读文件 ,这块区分成两部分
进程文件,也就是/proc/下面的
系统文件,系统提供的一些文件可供读取。
好 ,根据上面的总结,只要我们在上面的三个边界值进行拦截理论上就是最完美的方案 。
“边界值”拦截技术实现: 下面的架子是珍惜大佬的沙箱的设计模式 ,这块也是分享一下对应的“架构” 。
IPC协议拦截: 先说IPC,IPC的话很简单,我在上面也说了可以动态代理,也可以直接去用Hook框架Hook binder
里面的交互方法 。当发现触发指定的IPC协议的时候,直接模拟服务端往里面写入即可。
这块还有个细节点,为了防止程序直接通过cache
获取,因为有的字段初始化以后可能被保存到cache
里面 ,如果不存在的话再通过ipc去获取。Apk在启动一瞬间就进行了初始化,cache会被保存 。很多IPC代理人会这么设计,所以需要清理掉cache ,这个cache可以是Parcel的cache也可以是IPC代理人里面的cache 。比如Parcel里面的mCreators 或者sPairedCreators 都需要清空 。如果是IPC代理人的话也可以看代码看具体实现,看看是否包含cache,有的话清掉即可
SVC拦截: 主要用的是ptrace+seccomp做的架子
https://bbs.kanxue.com/thread-273160.htm
文件读取: 这块分为两部分,proc
下的文件我会使用fuse
对整个proc
进行模拟 ,这是完美方案 。proot
代码写好现成的,迁移到android 上直接用就好了。
如果是读取系统文件的话,可以直接使用 IO重定向配合SVC拦截即可,SVC都可以拦截了,任何文件读取你都可以随便修改 。
因为不管如何,最终都会调用到系统内核去读取文件,都会被转换成SVC指令 。
第四篇:https://bbs.kanxue.com/thread-281889-1.htm