ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

App Frida检测绕过实战:从特征隐藏到Hook注入

App Frida检测绕过实战:从特征隐藏到Hook注入 自己花了三个晚上才把“某青看点”的Frida检测给绕过去整个过程踩的坑比想象中多得多。今天把这套完整思路和可复现的操作步骤整理出来希望对正在搞Android逆向、尤其是遇到App反调试拦截的朋友有所帮助。先说明一点这篇只讨论技术对抗思路和常规的安全调试手段请确保你是在合法授权的前提下对目标应用进行分析。“某青看点”本身是一款资讯类App但它的客户端做了比较强的安全防护其中就包含对Frida框架的检测。稍微懂点逆向的都知道Frida是我们在Android平台上做动态分析最顺手的工具一旦被App检测到进程直接闪退或者拒绝运行整个调试就无法继续。文章面向的读者是刚接触Android逆向的新手以及已经能熟练使用Frida但被反调试卡住、需要绕过检测的中级开发者。1. 整体思路先搞清楚它是怎么把你拦下来的1.1 Frida检测的常见手段在动手绕之前先要知道对面用了什么招式。Android应用检测Frida常见的方式无非以下几种扫描端口Frida默认监听端口是27042App会去检查这个端口是否处于监听状态。最简单也最暴力所以很多App第一道检测就是这个。检查/proc/self/mapsFrida注入进程后会在内存映射里留下frida相关的so文件名比如frida-agent-64.so、linjector、gum-js-loop等。App只需要遍历maps文件匹配这些特征字符串就能判定当前进程被注入了。检测D-Bus协议流量Frida在通信时使用D-Bus认证机制App可以检测连接的socket是否包含D-Bus字符串这也是很常用的一招。调用系统API检查有些App会用Runtime.exec执行ps -A过滤进程列表里的frida-server或者通过ServerSocket连接特定端口探测响应甚至直接加载so层动态检测在native层做轮询。名字特征检测很多人会把frida-server改名但App检测时会遍历所有进程名同时搜索frida关键字甚至匹配已注入的agent路径。“某青看点”从实际表现来看它把maps扫描 端口探测 进程名匹配这几样都用上了。第一次直接跑frida -U附加上去不到两秒进程就退了logcat里没有任何明显异常纯靠闪退来拦截。1.2 破解思路别想着一次搞定分步绕过面对这种多层次的检测我建议的分步思路是先解决最明显的特征暴露点端口、进程名、默认socket。再处理内存映射痕迹让Frida注入后的so名字看起来无害或者用更隐蔽的加载方式。找到App的检测函数用Frida本身去hook并抹掉检测逻辑——这看起来有点绕但一旦能注入进去后续就能持续干预。第一步和第二步是保证“能进去”第三步是保证“进去之后不被踢出来”。很多人一上来就想着hook检测函数但注入这一步就被拦了Hook根本无从谈起所以顺序不能乱。2. 环境准备工具选型和验证基准2.1 真机与模拟器的选择我是在雷电模拟器上做的第一轮测试后来又转移到一台红米Note 12T Pro真机上跑了完整流程。结论很明确模拟器更容易通过但真机的检测行为更接近实际对抗情况。先说模拟器。雷电模拟器Android 9镜像下“某青看点”的检测没有完全生效可能是因为模拟器本身缺少部分硬件特征应用做了兼容性取舍。所以我第一轮就直接上了模拟器先把Frida的脚本逻辑调通再转到真机上处理检测问题。再说真机。红米Note 12T Pro跑Android 13系统比较新Frida server建议用对应的frida-server-16.x.x-android-arm64版本。真机上App的检测逻辑完整执行所以这里才是真正的战场。2.2 Frida版本与Python环境Frida的版本选择很重要。太老的版本在Android 13上频繁出现段错误太新的版本在某些加固App面前又纹丝不动。我这里用的是fridaPython包16.3.3frida-tools12.5.0frida-server16.3.3android-arm64安装过程不复杂一条pip install frida-tools就能搞定。但要注意PC端frida版本和手机端frida-server版本必须一致否则连上去会报错误。我第一次就是PC端更新到了16.4.1手机端还是15.2.2结果握手阶段就报版本不匹配白白浪费了半小时。2.3 辅助工具清单除了Frida本身我还用到了这些工具建议都装好jadx-gui静态分析Java层代码快速定位检测点。虽然SuperApp有so层混淆但Java层调Native的入口还是要靠它来梳理。GDA处理jadx不太顺利的dex文件对混淆代码的还原能力强一些。objection基于Frida的运行时探索工具可以快速查看类、方法、内存模块信息。但注意objection默认特征明显如果目标检测严格先把它放一边直接写自定义脚本更安全。Frida gadget如果要走“注入lib库”路线gadget可以作为依赖级库被应用加载适合在无法直接root的场景使用。下一篇可以展开讲。工具本身没什么特殊的关键是后面操作时的细节处理。3. 实操攻坚一步步绕过“某青看点”的Frida检测3.1 先确认检测点不盲猜用排除法拿到App后我没急着直接绕先做了个简单实验启动App确认正常运行。用adb forward tcp:27042 tcp:27042转发端口什么都不注入只是开启端口监听。再重启App观察是否闪退。结果App直接退出。这说明端口检测是生效的。没有注入任何代码仅仅是开了个端口就被判定这个检测逻辑很简单也很实诚。接着我把端口转发关掉用默认的frida-server启动并注入同样闪退。这就说明还有额外的手段在起作用——要么是maps扫描要么是进程特征匹配。为了确认是怎么回事我用adb shell进去手动cat /proc/pid/maps | grep frida发现App在运行时确实有frida-agent-64.so出现。而且ps -A | grep frida也能看到frida-server进程。这几个特征全暴露了。接下来就逐个去堵。3.2 端口与进程名的隐藏两个实用小技巧第一个操作是把frida-server改名并修改监听端口。修改端口的方式是启动时加参数./fs -l 0.0.0.0:45678注意改了端口之后PC端连接时也要带上端口frida -H 127.0.0.1:45678 -f com.example.app但如果走的是USB连接默认方式frida -U会尝试连接27042端口所以这里必须用-H指定或者使用adb forward把手机上的45678转发到本地对应端口。关于frida-server改名有人直接mv frida-server fs但这样还不够因为Frida注入到App进程里的agent模块名是写死在库里的不会因为server改名而改变。真正要处理的是后面讲到的映射特征。端口和进程名这两步做完后我重新启动App发现还是闪退但退出时间从之前的2秒拉长到了5秒左右。这说明端口检测已经过了但maps检测还在生效。3.3 处理内存映射特征使用改名的Frida-agent要让注入后的agent文件名不带frida字样最直接的办法是修改frida-server里的字符串资源重新编译。但这对新手来说门槛偏高涉及二进制patch还要处理签名校验。我这里有一个更轻量的做法用Frida的rename机制加载一个自定义名字的agent库。具体来说就是自己编译一份Frida agent的so文件把默认的frida-agent-64.so替换成libhelper.so这样的无害名字再通过注入器加载它。这个过程需要用到NDK和Frida源码编译环境不复杂但第一次编容易踩坑。不过如果你只是想过检测、拿结果有个更快的办法先在静态层用jadx定位App的native检测函数然后用Frida自身去hook这些函数。一旦端口和进程名隐藏成功Frida能注入进去了后续就靠hook来让检测函数失效即使maps里还有特征App内部也不会再继续深究了。3.4 定位并Hook检测函数核心步骤这一步是整个绕过的关键。用jadx打开“某青看点”的apk在Java层搜索以下关键词maps27042fridaexecRuntimedlopenstrstr很快就能看到一个类似SecurityCheck.java的类里面有几个native方法其中有一个叫checkFrida()。它返回的是int值0表示安全非0表示检测到Frida。这里要注意很多App包括这一款会把检测函数放到so层Java层只是通过JNI调用。所以即使你在Java层看到checkFrida()具体实现还是在libjiagu.so这类加固.so里面。这种情况下单纯hook Java层的函数是不够的因为JNI的返回值在native层已经被处理过了。我这边采取了两条线路线路一hook Java层函数直接让它永远返回0。线路二hook native层的strstr、fopen、open等常见文件/字符串操作函数让它们遇到“frida”“gum”等关键字时自动返回空值。两条线路一起用效果最稳。下面这段脚本就是我当时用的通用检测绕过脚本Java.perform(function() { // 路线1: 设置Java层检查结果 try { var SecurityCheck Java.use(com.xxx.security.SecurityCheck); SecurityCheck.checkFrida.implement function() { console.log([*] Java层checkFrida被hook, return 0); return 0; }; SecurityCheck.checkDebug.implement function() { console.log([*] Java层checkDebug被hook, return 0); return 0; }; } catch(e) { console.log([-] Java hook失败: e); } // 路线2: Hook native层字符串匹配 var strstr Module.findExportByName(libc.so, strstr); if (strstr) { Interceptor.attach(strstr, { onEnter: function(args) { this.haystack Memory.readCString(args[0]); this.needle Memory.readCString(args[1]); if (this.needle (this.needle.indexOf(frida) ! -1 || this.needle.indexOf(gum-js-loop) ! -1 || this.needle.indexOf(linjector) ! -1)) { console.log([*] 拦截strstr: haystack this.haystack , needle this.needle); this.needle not_found_match; } }, onLeave: function(retval) { if (this.needle not_found_match) { retval.replace(0); } } }); } });写这个脚本时有几个关键注意点implement必须要写在Java.use之后而且要try-catch包住因为不同版本App里类名可能不同找不到类会直接抛异常导致脚本崩溃。native层hookstrstr时不能随便把所有匹配frida的都返回0因为有些App会把frida字符串本身用作别的重要业务参数。我这里的处理就用了替换needle的方式属于取巧实际对抗时可能还需要根据App逻辑微调。返回值的处理要用retval.replace(0)而不是直接return 0这是Frida 16.x的API用法老版本可能是直接return。3.5 完整绕过流程的串联按顺序操作一遍实际流程如下启动frida-server并改名、修改监听端口adb push fs /data/local/tmp/ adb shell chmod 755 /data/local/tmp/fs adb shell /data/local/tmp/fs -l 0.0.0.0:45678 转发端口adb forward tcp:45678 tcp:45678启动App并注入绕过脚本frida -H 127.0.0.1:45678 -f com.xxx.app -l bypass_frida.js --no-pause这里有个细节-f参数会冷启动App配合--no-pause可以在App启动之前就注入脚本。如果App的检测逻辑在Application的onCreate里执行普通注入时机就晚了一定要用冷启动模式。4. 踩坑实录这些问题我猜你会遇到4.1 闪退问题注入成功但App立即退出这种情况多半是注入时机太晚或者是某些so库在JNI_OnLoad里也做了检测。我的处理方法是确认脚本里hook的类名和方法名是否正确jadx里看到的类和实际运行时可能会有混淆建议先hookClassLoader来打印加载的所有类。检查是否有多处检测函数比如除了checkFrida()还有checkNativeDebug()、checkPort()等。把注入时机提到最早用--no-pause冷启动方式。4.2 端口改了但frida -U找不到设备这个问题纯属操作失误。-U参数默认连接的是USB设备并且固定走27042端口。如果你把server端口改成了45678PC端必须对应修改frida -H 127.0.0.1:45678 -f com.xxx.app或者用adb forward tcp:27042 tcp:45678来兼容默认端口。我推荐直接用-H更直观也不会混淆。4.3 server启动正常但App一运行就消失排查步骤按顺序来先ls -l /data/local/tmp/fs确认权限是-rwxr-xr-x很多人push完忘记chmod。adb shell su -c /data/local/tmp/fs -l 0.0.0.0:45678时注意su的权限是否授予了adb shell。确认手机上没有残留的旧版frida-server在跑两个进程抢同一个端口也会导致问题。检查/proc/net/tcp里是否有45678端口在监听。4.4 注入后无法调用Java API这个问题常见于Android 13和更高版本系统对Java层的访问限制更多。遇到这种情况换成hook native层函数或者改用Java.performNow代替Java.perform有时候能缓解。但根本解决办法是让Frida的agent以更底层的方式运行也就是前面提到的gadget方案。4.5 常见问题速查表现象可能原因解决方法App启动后立即退出maps检测或端口检测改端口、改名、注入hook脚本frida-server启动报segfault版本不匹配或系统内核问题更换frida-server版本推荐16.3.3端口转发成功但连接超时手机端server未启动/未监听确认监听IP是0.0.0.0而非127.0.0.1App运行无异常但注入的hook不生效类名/方法名错误用Java.enumerateLoadedClasses确认准确类名hook native函数导致App崩溃拦截了业务正常调用的函数增加字符串过滤条件减少误伤5. 小结与心得这个绕过思路能扩展到哪里“某青看点”的Frida检测在同类产品里不算最难的它没有做内核级的反调试也没有用eBPF做进程行为监控主要靠常规特征扫描。这几位组合拳看起来唬人但只要理解了每层检测的原理逐个拆解并不复杂。我个人在实际操作中的体会是static分析先行动态hook兜底。先花时间在jadx里把App的调用链理清楚比盲猜hook点高效得多。很多人一上来就开Frida试注入半天没进展就烦躁其实只要多看一眼Java层代码思路立刻明朗。这个绕过方法后续的扩展方向有两个一个是把Frida换成Gadget模式让应用主动加载agent库适合没有root权限的检测场景另一个是把检测函数的hook逻辑放进so层用inline hook的方式把检测点打补丁这样即便App做了进程完整性校验也能避免直接在Java层留下痕迹。最后再分享一个小技巧写绕过脚本时把日志输出做得完整一点比如每个hook触发时打印调用栈。真机环境跑一次哪些函数被高频调用一目了然。如果发现某函数的触发频率异常高且每次都跟检测相关那它很可能就是App在定时轮询这时候不要只在它return时hook可以把它内部的执行流程打印出来能发现更多隐藏逻辑。
返回列表