Frida动态注入技术:绕过安卓APP抓包检测的三种实战方案

📅 2026/7/27 8:14:36
Frida动态注入技术:绕过安卓APP抓包检测的三种实战方案
1. 项目概述当抓包工具遇上“隐身”的APP搞安卓逆向和渗透测试的朋友估计都遇到过这种让人头疼的情况你兴冲冲地打开Burp Suite或者Charles配置好代理和证书准备对目标APP来一波流量分析结果APP一启动就弹窗“网络异常”或者直接闪退数据包一个都抓不到。这背后就是APP内置的各种抓包检测机制在作祟。它们像警觉的哨兵一旦发现系统代理被设置、发现陌生的CA证书被安装就立刻启动防御切断网络或者进入“伪装模式”。今天要聊的就是如何用Frida这把“瑞士军刀”以动态注入的方式从运行时层面巧妙地“骗过”这些检测。这不是简单的Hook一两个函数而是针对不同检测原理的三种实战思路。我会附上完整的、可运行的Frida脚本代码你可以直接拿来修改适配你的目标。无论你是安全研究员、爬虫工程师还是对安卓底层机制感兴趣的开发者这套“组合拳”都能帮你打开一扇新的大门。2. 核心思路理解检测与绕过的攻防本质在动手之前我们必须先搞清楚一个APP究竟是如何知道自己在被“偷听”的。知其然更要知其所以然这样才能对症下药。2.1 APP抓包检测的常见“哨兵”现代安卓APP尤其是金融、社交、电商类应用普遍会集成以下一种或多种检测手段系统代理检测这是最基础的。APP会读取系统的全局代理设置通常通过System.getProperty(“http.proxyHost”)等如果发现设置了代理且代理地址不是它信任的内网地址则判定为异常环境。证书绑定SSL Pinning这是中级防御。APP在代码中“硬编码”了它信任的服务端证书公钥或哈希值。即使你在设备上安装了Burp等工具的CA证书APP在SSL握手时也会对比证书发现不是它预埋的那个就拒绝连接。这又分为网络层Pinning如OkHttp的CertificatePinner和应用层Pinning在Native层验证。双向证书验证mTLS高级防御。不仅客户端要验证服务器证书服务器也要验证客户端证书。APP会内置一个客户端证书抓包工具没有对应的私钥根本无法完成握手。环境检测综合性防御。检测设备是否Root、是否安装了Xposed/Frida等动态分析框架、是否运行在模拟器中甚至检测是否有调试端口被打开。一旦发现可疑直接退出或进入“沙箱模式”。2.2 Frida的破局思路从“外部对抗”到“内部修改”传统绕过方法比如用Postern做透明代理、用VirtualXposed做环境隔离都属于“外部对抗”操作复杂且可能不通用。Frida的思路是“内部修改”直接注入到APP的进程内存中在它执行检测代码的瞬间修改其逻辑或返回值。我们的核心武器是Frida的Interceptor.attachAPI。它可以挂钩Hook任意函数在函数执行前onEnter或执行后onLeave插入我们的代码。通过Hook那些负责检测的关键函数我们可以让它“看”不到代理Hook读取系统属性的函数返回空值或假值。让它“信”我们的证书Hook证书验证逻辑强制返回验证成功。让它“认”不出异常环境Hook环境检测函数返回一个“清白”的结果。接下来我们就针对前三种最常见的检测展开三种具体的“骚操作”。3. 实战操作一Hook系统属性让代理“隐身”这种方法的原理是拦截APP获取系统网络配置的API调用让它们返回我们期望的值从而“欺骗”APP使其认为没有设置任何代理。3.1 关键API与Hook点分析在Java层APP通常通过以下方式获取代理System.getProperty(“http.proxyHost”)/System.getProperty(“http.proxyPort”)ProxySelector相关的类更高层的网络库如OkHttp的代理配置方法但其底层最终也会调用系统API。在Native层C/C一些加固或高安全等级的APP可能会通过libc库的函数如getenv或直接读取/proc/net等系统文件来检测。我们的策略是双管齐下同时Hook Java层的System.getProperty和Native层的getenv函数确保覆盖更广。3.2 完整Frida脚本代码与逐行解析Java.perform(function () { console.log(“[] 开始Hook系统代理检测...”); // 骚操作1Hook Java层的 System.getProperty var SystemClass Java.use(“java.lang.System”); SystemClass.getProperty.overload(‘java.lang.String’).implementation function (key) { var result this.getProperty(key); // 先调用原方法获取真实值 // 如果查询的是代理相关属性则返回空欺骗APP if (key “http.proxyHost” || key “https.proxyHost” || key “http.proxyPort” || key “https.proxyPort”) { console.log(“[Java Hook] 检测到获取代理属性: ” key “, 真实值: ” result “, 返回: null”); return null; // 或者返回 “” 空字符串 } // 其他属性正常返回 return result; }; // 骚操作2Hook一些网络库自带的代理检查方法以OkHttp为例 // 注意类名可能被混淆需要根据实际情况定位 try { var OkHttpClientBuilder Java.use(“okhttp3.OkHttpClient$Builder”); OkHttpClientBuilder.proxy.overload(‘java.net.Proxy’).implementation function (proxy) { console.log(“[Java Hook] OkHttpClient.Builder.proxy() 被调用传入代理: ”, proxy); // 可以选择不设置代理或者设置一个无害的代理如null // 这里我们选择调用原方法但实际可以注释掉下一行来强制不设置代理 return this.proxy(proxy); }; } catch (e) { console.log(“[-] 未找到OkHttp相关类可能目标未使用或类名已混淆: ” e.message); } }); // 骚操作3Hook Native层的 getenv 函数 (如果APP在so库里检测) Interceptor.attach(Module.findExportByName(null, “getenv”), { onEnter: function (args) { this.envName Memory.readCString(args[0]); // 读取参数即环境变量名 // console.log(“[Native Hook] getenv called for: ” this.envName); }, onLeave: function (retval) { // 如果检测到是HTTP代理相关的环境变量篡改返回值 var name this.envName; if (name (name.indexOf(“_PROXY”) ! -1 || name “HTTP_PROXY” || name “HTTPS_PROXY”)) { console.log(“[Native Hook] 篡改getenv返回值环境变量: ” name “, 原值: ” Memory.readCString(retval)); // 返回一个空指针(NULL)表示环境变量未设置 retval.replace(ptr(“0x0”)); } // 其他环境变量正常返回 } });脚本使用与注意事项保存脚本将上述代码保存为bypass_proxy.js。启动Frida确保手机或模拟器已安装frida-server并运行。使用命令frida -U -f com.target.app –no-pause -l bypass_proxy.js启动APP并注入脚本。关键点类名混淆实战中OkHttpClient$Builder这类类名很可能被混淆成a$a等形式。你需要先用frida-trace或Objection等工具枚举类和方法或者反编译APP后分析代码找到正确的类名进行Hook。覆盖不全有些APP会使用更冷门的API或自定义JNI方法检测。如果此脚本无效你需要用frida-trace -U -i “*proxy*” com.target.app来跟踪所有包含“proxy”字样的函数调用找到真正的检测点。返回值选择返回null还是“”取决于目标APP的判空逻辑。有些APP用String.isEmpty()判断null会导致空指针异常。可以都试试。4. 实战操作二击穿SSL Pinning让证书验证“形同虚设”这是最常遇到的“硬骨头”。我们的目标是找到APP进行证书验证的那个关键函数然后让它永远返回“验证成功”。4.1 SSL Pinning的实现层级与Hook策略网络框架层如OkHttp通过CertificatePinner类实现。Hook它的check方法。TrustManager层这是Java SSL信任链的核心。APP可能会自定义X509TrustManager我们需要Hook其checkClientTrusted或checkServerTrusted方法让它们什么都不做空实现。HostnameVerifier层用于验证主机名是否匹配证书。HookHostnameVerifier.verify方法让它直接返回true。Native层SSLSocket/OpenSSL在so库中实现性能更高更难绕过。需要Hook如SSL_CTX_set_cert_verify_callback这样的C函数。最暴力且通用的方法是直接替换掉整个TrustManager。下面提供两种主流方案的代码。4.2 方案A通用型TrustManager“大替换”此方案尝试定位并替换APP使用的SSLContext的TrustManager适用于大多数Java层实现的Pinning。Java.perform(function () { console.log(“[] 开始爆破SSL Pinning (通用TrustManager替换法)...); // 首先尝试禁用Java层的默认主机名验证部分旧版本或自定义HTTP库有效 var HttpsURLConnectionClass Java.use(“javax.net.ssl.HttpsURLConnection”); HttpsURLConnectionClass.setDefaultHostnameVerifier(Java.use(“javax.net.ssl.HostnameVerifier”).$new({ verify: function (hostname, session) { console.log(“[HostnameVerifier] 验证主机名: ” hostname “, 直接返回true”); return true; } })); // 核心创建一个“人畜无害”的TrustManager它信任所有证书 var TrustAllManager Java.registerClass({ name: ‘com.bypass.TrustAllManager’, implements: [Java.use(‘javax.net.ssl.X509TrustManager’)], methods: { checkClientTrusted: function (chain, authType) { console.log(“[TrustAllManager] checkClientTrusted called, 自动通过”); }, checkServerTrusted: function (chain, authType) { console.log(“[TrustAllManager] checkServerTrusted called, 自动通过”); }, getAcceptedIssuers: function () { return []; } } }); // 关键步骤遍历所有初始化过的SSLContext将其TrustManager替换成我们的 var SSLContextClass Java.use(“javax.net.ssl.SSLContext”); SSLContextClass.init.overload(‘[Ljavax.net.ssl.KeyManager;’, ‘[Ljavax.net.ssl.TrustManager;’, ‘java.security.SecureRandom’).implementation function (keyManagers, trustManagers, secureRandom) { console.log(“[SSLContext Hook] 检测到SSLContext.init()被调用原TrustManagers: ”, trustManagers); // 替换为我们的TrustAllManager数组 var newTrustManagers [TrustAllManager.$new()]; return this.init(keyManagers, newTrustManagers, secureRandom); }; // 针对OkHttp的CertificatePinner进行Hook try { var CertificatePinnerClass Java.use(“okhttp3.CertificatePinner”); CertificatePinnerClass.check.overload(‘java.lang.String’, ‘java.util.List’).implementation function (hostname, pins) { console.log(“[OkHttp Pin Bypass] 证书锁定检查被调用主机名: ” hostname “, 已绕过”); // 什么都不做直接放过 }; } catch (e) { console.log(“[-] 未找到OkHttp CertificatePinner可能未使用或已混淆”); } });4.3 方案B针对Native层Pinning的Hook以OpenSSL为例如果APP将关键验证逻辑放在Native层就需要在so库里寻找函数符号。Java.perform(function () { // ... (上述Java层的Hook可以保留) ... // 重点Hook Native层的证书验证回调函数 // 常见的函数名可能因编译器和版本而异 // - SSL_CTX_set_cert_verify_callback // - SSL_verify_cb (回调函数本身) // - 自定义的JNI函数 // 方法使用Module.enumerateExports遍历so库的导出函数寻找关键词 // 这里以libssl.so为例 var libssl Module.findBaseAddress(“libssl.so”); if (libssl) { console.log(“[] 找到libssl.so基址: ” libssl); // 尝试Hook一个常见的函数注意这不是唯一或绝对正确的方法需要根据实际情况调整 var sslVerifyCallback Module.findExportByName(“libssl.so”, “SSL_CTX_set_verify”); if (sslVerifyCallback) { Interceptor.attach(sslVerifyCallback, { onEnter: function (args) { console.log(“[Native SSL Hook] SSL_CTX_set_verify 被调用”); // 这个函数用于设置验证回调。我们可以尝试修改其参数但更直接的是Hook它设置的那个回调函数。 // 更常见的做法是Hook实际执行验证的函数这需要逆向分析so库。 } }); } } // 一个更暴力的“穷举”思路Hook libc的getpeercert相关函数不这太底层且不稳定。 // 更实用的方法是使用 objection 的 android sslpinning disable 命令它集成了多种 bypass 方法。 // 或者使用 Frida 脚本扫描所有可能的验证函数。这里提供一个模式搜索的示例需谨慎可能误杀 /* var modules Process.enumerateModules(); for (var i0; imodules.length; i) { var mod modules[i]; if (mod.name.indexOf(‘ssl’) ! -1 || mod.name.indexOf(‘crypto’) ! -1) { console.log(‘扫描模块: ’ mod.name); mod.enumerateExports().forEach(function(exp) { if (exp.type ‘function’ (exp.name.indexOf(‘verify’) ! -1 || exp.name.indexOf(‘cert’) ! -1)) { console.log(‘ - 可疑导出: ’ exp.name ‘ at ’ exp.address); // 可以尝试attach这个地址但最好先通过反汇编确认其功能 } }); } } */ });重要提醒Native层Hook难度大稳定性要求高。对于生产环境测试强烈建议优先使用成熟的工具链如Objection命令android sslpinning disable它自动尝试多种方法。Frida脚本集如frida-multiple-unpinning等开源项目集合了针对数十个常见网络库和银行APP的绕过脚本。我们的自定义脚本更适合作为学习原理和针对特定、已知检测点的精准打击。5. 实战操作三综合环境伪装打造“清白”的运行沙箱有些APP的检测非常全面它不仅查代理、查证书还查你的设备干不干净。这一步我们就要伪造一个“合规”的运行环境。5.1 常见环境检测点及对抗方法检测点常见API/方法我们的Hook/伪造策略Root检测检查/su、/system/xbin/su等文件检查which su命令检查Build.TAGS是否包含test-keys。Hook文件访问API如File.exists,File.canRead对于su相关路径返回false。HookRuntime.exec对执行su的命令返回错误。模拟器检测检查Build类的特定字段如MODEL,MANUFACTURER,PRODUCT是否包含google_sdk,sdk,emulator等检查特定设备文件如/dev/socket/qemud。HookBuild类的getter方法返回真实手机的值。Hook文件检测API。调试器检测检查android:debuggable属性检查TracerPid/proc/self/status。Hookandroid.os.Debug.isDebuggerConnected()返回false。Hook读取/proc/self/status的文件操作过滤或修改TracerPid字段。Frida检测检测frida-server默认端口27042是否开放检测内存中是否存在frida相关字符串或特征库。修改frida-server启动端口-l 0.0.0.0:8080。使用-f参数spawn启动而非attach。使用定制化的frida-gadget或对抗检测的脚本。Xposed检测检查已安装应用列表检查XposedBridge类是否存在。HookPackageManager.getInstalledApplications等方法过滤掉Xposed安装器。5.2 一体化环境伪装脚本示例这个脚本展示了如何同时对抗多种基础检测。Java.perform(function () { console.log(“[] 启动综合环境伪装...”); // —————— 1. 反Root检测 —————— var FileClass Java.use(“java.io.File”); FileClass.exists.implementation function () { var path this.getAbsolutePath(); // 屏蔽对常见su路径的检测 var blockedPaths [“/su”, “/sbin/su”, “/system/bin/su”, “/system/xbin/su”, “/data/local/xbin/su”, “/data/local/bin/su”, “/system/sd/xbin/su”, “/system/bin/failsafe/su”, “/data/local/su”]; for (var i 0; i blockedPaths.length; i) { if (path.indexOf(blockedPaths[i]) ! -1) { console.log(“[反Root] 拦截对su文件的检测: ” path “, 返回false”); return false; } } return this.exists(); }; // —————— 2. 反模拟器检测 —————— var BuildClass Java.use(“android.os.Build”); // Hook BUILD类的一些字段获取注意直接修改final字段很困难通常Hook获取方法或替换整个类 // 更常见的是Hook应用里调用Build.MODEL等的地方。这里提供一个思路替换系统服务返回的字段 // 一个取巧的方法Hook检查模拟器的自定义函数。这里以检测Build.MODEL为例 // 我们需要找到APP里读取Build.MODEL进行判断的代码然后Hook那个判断函数。 // 下面是一个更通用的“欺骗”方法但并非万能 try { // 假设APP有一个工具类方法 isEmulator() var EmulatorDetectorClass Java.use(“com.target.app.security.EmulatorDetector”); // 类名需要替换 if (EmulatorDetectorClass) { EmulatorDetectorClass.isEmulator.implementation function () { console.log(“[反模拟器] 绕过isEmulator检测返回false”); return false; }; } } catch (e) { /* 类可能不存在或名字不对 */ } // —————— 3. 反调试检测 —————— var DebugClass Java.use(“android.os.Debug”); DebugClass.isDebuggerConnected.implementation function () { console.log(“[反调试] 调用isDebuggerConnected返回false”); return false; }; // —————— 4. 应用列表隐藏反Xposed等框架检测 —————— var PackageManagerClass Java.use(“android.content.pm.PackageManager”); // Hook getInstalledApplications 或 getInstalledPackages PackageManagerClass.getInstalledApplications.overload(‘int’).implementation function (flags) { var originalList this.getInstalledApplications(flags); // 过滤掉包含“xposed”, “edxposed”, “lsposed”等关键词的包名 var filteredList Java.use(“java.util.ArrayList”).$new(); for (var i 0; i originalList.size(); i) { var appInfo originalList.get(i); var packageName appInfo.packageName.value; var lowerPkg packageName.toLowerCase(); if (!(lowerPkg.indexOf(“xposed”) ! -1 || lowerPkg.indexOf(“lsposed”) ! -1 || lowerPkg.indexOf(“edxposed”) ! -1)) { filteredList.add(appInfo); } else { console.log(“[应用列表过滤] 隐藏包: ” packageName); } } return filteredList; }; }); // —————— 5. Native层反Frida检测示例 —————— // 检测Frida的常见方法是扫描端口或内存特征。我们可以尝试隐藏。 // 方法A如果Frida-server运行在非默认端口检测脚本可能扫不到。 // 方法BHook connect 系统调用如果目标端口是27042返回错误。 Interceptor.attach(Module.findExportByName(null, “connect”), { onEnter: function (args) { var fd args[0].toInt32(); var sockaddr args[1]; var addrlen args[2].toInt32(); // 解析sockaddr结构体比较麻烦这里是一个简化示例思路 // 实际需要根据内存结构读取端口号 // if (port 27042) { console.log(“[反Frida检测] 拦截对27042端口的连接”); this.context.r0 -1; } // 设置返回值错误 }, onLeave: function (retval) { } });环境伪装的核心难点检测与反检测是持续对抗的。高强度的APP会使用多维度、多层次的检测并且检测点会更新和混淆。上述脚本是一个起点真实场景中你需要动态分析定位使用frida-trace跟踪所有文件访问、系统属性读取、包管理查询等API。静态分析辅助反编译APP搜索root、emulator、debug、xposed等关键词找到检测代码的具体位置和类名。组合拳将代理绕过、证书绕过、环境伪装脚本合并使用形成一个完整的“隐身”方案。6. 完整流程、问题排查与实战心得将以上三种操作的脚本整合到一个主脚本中按顺序执行可以构建一个相对完整的抓包环境准备流程。6.1 完整实战操作流程环境准备一台已Root的安卓测试机或模拟器推荐真机如Google Pixel系列。安装并配置好Frida环境PC端frida-tools手机端frida-server。安装目标APP并配置好抓包工具Burp Suite/Charles的代理和证书即使装了后续也会被绕过。逆向分析可选但推荐使用jadx-gui或apktool反编译目标APP快速搜索proxy、pin、certificate、verify、root、emulator、debug等关键词初步了解其防护手段。动态探测使用frida-ps -U确认目标APP进程名。使用objection -g com.target.app explore连接并运行android hooking list classes等命令初步探索。使用frida-trace进行针对性跟踪例如frida-trace -U -i “*getprop*” com.target.app # 跟踪属性获取 frida-trace -U -j “*System.getProperty*” com.target.app # 跟踪特定Java方法脚本注入与测试将我们编写的bypass_proxy.js、bypass_ssl_pinning.js、bypass_env_detect.js中的核心函数合并到一个脚本比如ultimate_bypass.js。使用frida -U -f com.target.app –no-pause -l ultimate_bypass.js启动APP并注入。观察控制台输出看Hook是否成功是否有错误。尝试在APP内进行网络操作同时在抓包工具中查看流量是否成功捕获。6.2 常见问题排查速查表问题现象可能原因排查思路与解决方案注入后APP闪退1. Hook的函数不正确或参数处理错误导致崩溃。2. 脚本存在语法错误或逻辑错误。3. APP有强大的反调试/反注入机制检测到Frida后主动崩溃。1. 检查Frida输出日志看是否有JavaScript报错。2. 逐段注释脚本定位导致崩溃的Hook点。3. 尝试使用-fspawn模式启动而不是attach。4. 尝试使用frida-gadget以嵌入方式启动或使用对抗更强检测的Frida版本/配置。Hook成功但抓包仍失败1. 检测点没有Hook全存在遗漏。2. 检测发生在Native层而脚本只Hook了Java层。3. 证书绑定非常底层如网络框架自行实现TLS通用脚本无效。4. 流量走了其他通道如WebSocket、纯TCP Socket、或使用了HTTP/3。1. 使用frida-trace进行更广泛的跟踪查漏补缺。2. 检查是否有so库文件尝试Hook常见的C函数如SSL_CTX_set_verify。3. 尝试使用objection的android sslpinning disable命令它集成更多方法。4. 使用Wireshark等底层抓包工具确认是否有TCP连接建立判断流量是否真的经过代理。控制台无输出或输出不全1.console.log可能被APP或系统重定向/屏蔽。2. 脚本没有成功执行到Hook部分。1. 将日志写入文件var log_file new File(“/sdcard/frida_log.txt”, “a”); log_file.write(“[] Hooked\n”);2. 在Java.perform开头加一个简单的console.log确认脚本是否被加载。部分功能异常Hook了不该Hook的函数影响了APP正常业务逻辑。在Hook函数的实现里更精确地判断调用上下文this、参数值只有符合检测特征时才进行篡改其他情况正常调用原函数return this.originalMethod.apply(this, arguments)。6.3 踩坑心得与高阶技巧先动后静动静结合不要一上来就埋头看反编译的代码。先用Frida进行动态探索和测试找到关键函数和流程再有针对性地去静态代码中分析效率更高。Hook的粒度要细不要一上来就Hook非常底层的通用函数如libc的open这可能导致系统不稳定或APP大量崩溃。应该从高层Java网络库API向底层JNI、Native逐步深入。善用工具链Objection、Frida-scripts仓库、Jadx、Ghidra/IDA是你的好朋友。不要重复造轮子先看看有没有现成的脚本或模式。保持耐心与迭代绕过检测是一个迭代过程。一次注入可能不成功需要根据现象闪退、无流量、有流量但乱码不断调整Hook点和篡改逻辑。注意性能与稳定性在生产环境或长时间测试中过于频繁的Hook或复杂的Hook逻辑可能影响APP性能甚至导致崩溃。确保你的脚本尽可能高效、精准。法律与道德边界所有这些技术仅用于安全研究、授权测试和个人学习。未经授权对他人APP进行逆向和攻击是非法行为。请务必在合法合规的环境下使用这些技能。