1. 项目概述为什么Frida是安卓逆向与抓包的“瑞士军刀”在移动安全测试和逆向工程领域安卓平台因其开放性而成为研究的焦点。传统的静态分析工具如Jadx、Ghidra能帮我们看清代码的“骨架”但当面对动态加载、运行时加密、代码混淆或复杂的网络协议校验时静态分析往往力不从心。这时我们就需要一个能在应用“活着”的时候实时干预其行为的工具——这就是Frida。Frida的核心价值在于其“动态插桩”能力。它像一位技艺高超的外科医生能在目标应用无论是Java层还是Native层运行时精准地注入自己的代码片段我们称之为“脚本”从而实现对函数调用、参数修改、返回值替换、甚至内存读写等操作的完全控制。对于APP渗透和抓包而言这种能力直接击穿了传统防御的软肋。很多应用会使用SSL Pinning证书绑定来防止中间人攻击或者对请求参数进行复杂的签名加密。单纯使用Burp Suite或Charles配置代理往往会遇到“网络错误”或“证书不被信任”的提示。而Frida可以让我们直接“说服”应用忽略证书校验、输出加密前的明文参数、甚至直接调用其内部的签名函数为我们生成合法数据。我之所以称它为“瑞士军刀”是因为其应用场景远不止抓包。从绕过Root检测、破解本地验证逻辑、分析加密算法到自动化执行特定功能Frida几乎无所不能。本次实战我们将聚焦于其在APP渗透测试中最经典、也最棘手的两个高级应用场景对抗SSL Pinning实现完美抓包以及Hook关键函数获取核心业务数据。无论你是安全研究员、渗透测试工程师还是对安卓底层机制充满好奇的开发者掌握Frida的这些高级技巧都将极大提升你的问题解决能力。2. 环境搭建与核心工具链配置工欲善其事必先利其器。一个稳定、高效的Frida工作环境是后续所有操作的基础。这里我分享一套经过大量实战检验的配置方案兼顾了便捷性与可控性。2.1 Frida服务端与客户端的版本协同Frida的架构分为服务端frida-server和客户端frida-tools等。最大的一个坑就是版本不匹配。服务端运行在目标设备手机或模拟器上客户端运行在你的分析主机上两者必须保持主版本号一致。操作步骤确定Python环境建议使用Python 3.8-3.10版本在虚拟环境如venv或conda中操作避免包冲突。安装Frida客户端工具在主机命令行执行pip install frida-tools。这会同时安装frida核心库和frida-ps、frida-ls-devices等命令行工具。安装后可以通过frida --version查看客户端版本例如16.1.4。下载匹配的Frida服务端前往Frida的GitHub Releases页面找到与客户端版本号相同的发布包。对于安卓设备需要根据设备CPU架构下载对应的frida-server。大多数现代手机是arm64模拟器如雷电可能是x86_64。文件通常命名为frida-server-16.1.4-android-arm64.xz。推送并启动服务端# 将下载的.xz文件解压得到可执行文件frida-server-16.1.4-android-arm64 adb push frida-server-16.1.4-android-arm64 /data/local/tmp/frida-server adb shell # 进入adb shell后 su # 获取root权限这是Frida发挥全部功能的前提 cd /data/local/tmp chmod 755 frida-server # 赋予执行权限 ./frida-server # 后台运行验证连接退出adb shell在主机上运行frida-ps -U。如果能看到设备上运行的进程列表恭喜你环境通了。注意很多人在雷电模拟器等环境下遇到抓不到包或Frida连接失败的问题根源之一就是架构不匹配。确保模拟器开启Root权限并使用正确的frida-server版本。如果frida-ps -U报错检查adb devices连接是否正常以及是否在adb shell中正确启动了服务端。2.2 动态分析与抓包工具的组合配置Frida不是孤军奋战的它需要和抓包工具协同工作形成“侦查抓包- 分析逆向- 干预Frida”的闭环。抓包代理工具Burp Suite/Charles负责拦截和查看HTTP/HTTPS流量。确保你的主机代理如Burp监听8080端口已设置好。安卓设备代理配置将安卓设备的Wi-Fi代理设置为你的主机IP和抓包工具端口。这一步是流量导流的基础。安装抓包工具证书到系统信任区这是拦截HTTPS流量的关键。通常将Burp或Charles的CA证书下载后从“用户证书”移动到“系统证书”存储区需要Root权限。你可以使用adb push将证书文件放到系统证书目录/system/etc/security/cacerts/并修改其权限和文件名需要哈希值。这是一个精细活操作失误可能导致系统问题建议先在网上搜索当前设备型号的具体教程。核心组合工作流设备流量 - 经过主机代理 - 被Burp/Charles拦截 - 发现加密或校验问题 - 使用Frida脚本在设备端“解决”该问题 - 实现明文抓包。2.3 开发与调试环境VS Code与关键插件对于编写复杂的Frida脚本一个好用的IDE至关重要。我强烈推荐VS Code及其插件。JavaScript/TypeScript支持Frida脚本主要用JavaScript编写。安装相关插件可以获得代码提示、语法高亮。Frida插件搜索并安装如Frida Snippets等插件它们能提供Frida API的自动补全比如Java.perform、Interceptor.attach等极大提升编码效率。文件同步配置VS Code的SFTP或使用adb push/pull来同步脚本到测试设备方便快速迭代。配置好这个环境意味着你拥有了一个强大的移动端动态分析实验室可以应对绝大多数复杂的应用安全分析场景。3. 核心原理Frida如何实现运行时干预要玩转Frida不能只停留在“脚本小子”的层面理解其底层原理能让你在遇到问题时更快地定位和解决。Frida的魔力主要建立在两大支柱上在Java虚拟机JVM层面的Hook以及在原生层Native的Hook。3.1 Java层Hook与JVM共舞安卓应用的大部分业务逻辑由Java或Kotlin编写运行在ARTAndroid Runtime或Dalvik虚拟机上。Frida通过其JavaAPI与JVM交互实现“上帝视角”的观察与修改。关键API解析Java.perform(fn)这是一个包裹函数所有对Java层的操作代码都必须放在这个回调函数内部执行。它确保Frida的代码在正确的线程和ClassLoader上下文中运行。Java.use(className)这是最常用的函数用于获取一个Java类的“包装器”。通过它你可以创建类的实例或者更重要的——替换其方法实现。Java.perform(function() { // 获取目标类的引用 var TargetClass Java.use(com.example.secret.EncryptUtils); // 替换其encrypt方法 TargetClass.encrypt.overload(java.lang.String).implementation function(input) { console.log([*] encrypt() called with input: input); var originalResult this.encrypt(input); // 调用原方法获取结果 console.log([*] Original result: originalResult); return originalResult; // 可以返回原结果也可以返回我们篡改的结果 }; });Java.choose(className, callbacks)用于在堆上枚举已存在的类实例并对每个实例进行操作。这在需要与特定对象交互时非常有用比如修改某个Activity实例的字段值。原理浅析Frida在Java层本质上利用了JVM的“反射”和“动态代理”机制但比标准反射更底层、更强大。它通过修改方法对应的内存指针将其指向Frida注入的代理代码从而在方法被调用时率先获得控制权。3.2 Native层Hook深入C/C的世界许多核心算法如加密、音视频编码或安全校验逻辑为了性能和保密会用C/C实现并编译为so库。Hook Native层是逆向中更具挑战性的一环。关键API解析Module对象用于操作加载到内存中的so库。Module.findBaseAddress(libnative.so)可以找到库的基地址。Interceptor.attach(address, callbacks)这是Native Hook的核心。通过传入目标函数的绝对地址或通过模式匹配找到的地址可以附加一个拦截器。Interceptor.attach(Module.findExportByName(libcrypto.so, SHA256_Init), { onEnter: function(args) { // args[0] 是SHA256_CTX结构体指针 console.log([*] SHA256_Init called. Context ptr: args[0]); this.contextPtr args[0]; // 保存下来供onLeave使用 }, onLeave: function(retval) { // 函数执行完毕可以读取或修改retval console.log([*] SHA256_Init finished.); } });Memory.read/write系列API用于直接读写目标进程的内存这是分析数据结构、打补丁Patch的基础。实操心得Native Hook的难点在于确定函数签名和参数结构。你需要一定的逆向基础使用IDA Pro或Ghidra反编译so库分析出函数的参数类型和个数以便在onEnter中正确解析args数组。对于C的类成员函数还需要考虑this指针。3.3 脚本注入机制与稳定性考量Frida通过ptrace系统调用或Zygote注入等方式将包含JavaScript引擎的库注入到目标进程。脚本在其中运行并通过IPC与外部客户端通信。稳定性技巧异常处理务必在脚本中使用try-catch包裹关键代码避免因未找到类、方法签名错误等问题导致整个脚本崩溃甚至影响目标进程。延迟Hook有些类可能不是在应用启动时就加载的。使用setImmediate或Java.perform内部的setTimeout来延迟执行Hook代码确保目标类已可用。资源释放虽然Frida会管理大部分资源但在长时间附着时注意避免在回调函数中执行阻塞操作或内存泄漏。理解这些原理你就知道Frida脚本不是魔法而是建立在操作系统和运行时提供的强大机制之上。这能让你在编写脚本时更有把握在调试时更有方向。4. 高级实战一彻底击穿SSL Pinning实现抓包SSL Pinning证书钉扎是应用开发者防止中间人攻击的常用手段。应用内置了服务端证书或公钥的哈希值在建立TLS连接时会比对服务器返回的证书即使你的Burp证书被系统信任但因为哈希值不匹配连接也会被拒绝。下面我们分步拆解如何用Frida解决这个问题。4.1 定位证书校验的关键代码首先我们需要知道应用在哪里、用什么方式做了校验。常见的有以下几种OkHttp3通过CertificatePinner类。Apache HttpClient自定义SSLSocketFactory。X509TrustManager自定义实现checkServerTrusted方法。网络库如Cronet或WebView可能有自己的校验逻辑。定位方法字符串搜索使用Jadx打开APK搜索关键词如“pin”、“CertificatePinner”、“X509TrustManager”、“checkServerTrusted”、“SSLContext”、“TrustManager”。堆栈跟踪法在Burp中看到SSL错误后尝试在Frida中Hook一些通用的SSL相关类打印调用堆栈。例如Hookjavax.net.ssl.HttpsURLConnection的setSSLSocketFactory或setHostnameVerifier方法。Java.perform(function() { var HttpsURLConnection Java.use(javax.net.ssl.HttpsURLConnection); HttpsURLConnection.setSSLSocketFactory.implementation function(factory) { console.log([*] setSSLSocketFactory called!); console.log(Java.use(android.util.Log).getStackTraceString(Java.use(java.lang.Exception).$new())); return this.setSSLSocketFactory(factory); // 继续执行原方法 }; });通过堆栈信息可以快速定位到应用自定义校验代码的位置。4.2 通用型SSL Pinning绕过脚本解析对于大多数使用标准库如OkHttp的应用我们可以使用“暴力破解”的方式——直接替换掉负责校验的核心类。这里提供一个经典的、针对多种情况的通用脚本框架。Java.perform(function() { console.log([*] Starting SSL Pinning Bypass...); // 1. 绕过 TrustManager var TrustManagerImpl; try { TrustManagerImpl Java.use(com.android.org.conscrypt.TrustManagerImpl); TrustManagerImpl.verifyChain.implementation function(untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) { console.log([*] Bypassing TrustManagerImpl.verifyChain for: host); return; // 直接返回不执行验证 }; } catch (e) { console.log([!] TrustManagerImpl not found: e.message); } // 2. 绕过 OkHttp 的 CertificatePinner var CertificatePinner; try { CertificatePinner Java.use(okhttp3.CertificatePinner); CertificatePinner.check.overload(java.lang.String, java.util.List).implementation function(hostname, pins) { console.log([*] Bypassing CertificatePinner.check for: hostname); // 什么都不做直接放过 }; // 另一个重载方法 CertificatePinner.check.overload(java.lang.String, [Ljava.security.cert.Certificate;).implementation function(hostname, certificates) { console.log([*] Bypassing CertificatePinner.check (array) for: hostname); }; } catch (e) { console.log([!] CertificatePinner not found: e.message); } // 3. 绕过 X509TrustManager (最常见) var X509TrustManager Java.use(javax.net.ssl.X509TrustManager); var TrustManager Java.registerClass({ name: com.bypass.TrustManager, implements: [X509TrustManager], methods: { checkClientTrusted: function(chain, authType) { console.log([*] checkClientTrusted bypassed); }, checkServerTrusted: function(chain, authType) { console.log([*] checkServerTrusted bypassed for: authType); }, getAcceptedIssuers: function() { return []; } } }); // 找到应用的 SSLContext 并替换其 TrustManager var SSLContext Java.use(javax.net.ssl.SSLContext); SSLContext.init.overload([Ljavax.net.ssl.KeyManager;, [Ljavax.net.ssl.TrustManager;, java.security.SecureRandom).implementation function(keyManagers, trustManagers, secureRandom) { console.log([*] SSLContext.init() hooked. Replacing TrustManager...); // 创建一个我们自己的、什么都不校验的TrustManager数组 var myTrustManagerArray [TrustManager.$new()]; return this.init(keyManagers, myTrustManagerArray, secureRandom); }; console.log([*] SSL Pinning Bypass hooks installed.); });这个脚本尝试了三种最常见的绕过方式。它的工作原理是“偷梁换柱”当应用初始化SSL上下文或进行证书检查时我们用自己实现的、不做任何实质性校验的类替换掉原来的校验逻辑。4.3 针对特定库如OkHttp3、WebView的定制化方案如果通用脚本无效说明应用可能使用了更冷门的库或自研方案。这时就需要定制化Hook。案例Hook OkHttp3的OkHttpClient.Builder如果应用在构建OkHttpClient时设置了CertificatePinner我们可以直接让这个设置失效。var OkHttpClient_Builder Java.use(okhttp3.OkHttpClient$Builder); OkHttpClient_Builder.certificatePinner.implementation function(certificatePinner) { console.log([*] OkHttpClient.Builder.certificatePinner() called, will return a dummy pinner.); // 返回一个空的、不执行任何检查的CertificatePinner实例 var CertificatePinner Java.use(okhttp3.CertificatePinner); var DummyPinner CertificatePinner.builder().build(); return this.certificatePinner(DummyPinner); };案例处理WebView的证书校验对于混合开发应用其WebView部分也可能有独立的校验。// Hook WebViewClient 的 onReceivedSslError 方法直接让错误处理失效 var WebViewClient Java.use(android.webkit.WebViewClient); WebViewClient.onReceivedSslError.implementation function(webView, handler, error) { console.log([*] WebView SSL Error intercepted: error.toString()); // 关键调用 handler.proceed() 来忽略SSL错误 handler.proceed(); };实操心得绕过SSL Pinning后务必在Burp Suite中确认是否已能成功拦截和解密HTTPS流量。有时应用可能有多重校验需要结合多个Hook点。这个过程是“猫鼠游戏”需要耐心和细致的观察。5. 高级实战二Hook关键函数获取与篡改业务数据绕过网络层的防御后我们进入应用的核心腹地业务逻辑。很多应用会对请求参数进行加密、签名或对返回数据进行解密。直接抓到的包是一堆乱码。此时我们需要Hook负责这些操作的函数获取明文或篡改数据。5.1 定位加密/签名函数的方法论找到这些函数是成功的第一步。以下是几种行之有效的方法网络请求参数回溯法在抓包工具中找到一个典型的、带有加密参数如signxxxxxx或dataENCRYPTED_BASE64_STRING的请求。然后在Jadx中全局搜索这个参数名如sign。搜索可能用于生成签名的常量字符串如MD5、SHA256、HmacSHA256。搜索网络请求库如OkHttp的InterceptorRetrofit的Converter的调用这些地方往往是参数处理的最后一站。堆栈跟踪法进阶Hook网络库发送请求的最终方法如okhttp3.RealCall#execute或HttpURLConnection#getInputStream在调用时打印堆栈。从堆栈中向上查找很容易找到对参数进行处理的业务逻辑代码。密码学API监控法Hook标准的JCAJava Cryptography Architecture类如MessageDigest.getInstance,Mac.getInstance,Cipher.getInstance。当这些方法被调用时打印堆栈和参数可以快速定位到应用使用加密算法的地方。var Cipher Java.use(javax.crypto.Cipher); Cipher.getInstance.overload(java.lang.String).implementation function(transformation) { console.log([*] Cipher.getInstance called: transformation); console.log(Java.use(android.util.Log).getStackTraceString(Java.use(java.lang.Exception).$new())); return this.getInstance(transformation); };5.2 参数拦截、明文输出与动态篡改找到函数后我们就可以进行拦截和操作了。假设我们定位到一个签名函数String generateSign(String param1, String param2)。基础Hook打印输入输出var SignUtils Java.use(com.target.app.utils.SignUtils); SignUtils.generateSign.overload(java.lang.String, java.lang.String).implementation function(param1, param2) { console.log([*] generateSign called!); console.log( Param1: param1); console.log( Param2: param2); // 调用原方法获取签名结果 var originalSign this.generateSign(param1, param2); console.log( Original Sign: originalSign); // 返回原结果不影响应用运行 return originalSign; };运行脚本并发起一个网络请求你就能在Frida控制台看到明文的参数和计算出的签名值。这本身就是巨大的胜利——你知道了签名的算法和输入。高级操作动态篡改返回值或参数篡改签名如果你想测试服务端对错误签名的处理可以直接返回一个假签名。return fake_sign_123456;篡改请求参数在签名函数内部修改传入的参数从而影响最终发送的请求。但要注意如果参数在传入前已被其他地方使用可能需要更早地进行Hook。// 假设我们想把某个固定参数值改掉 var newParam1 hacked_value; var originalSign this.generateSign(newParam1, param2); // 用篡改后的参数计算签名 console.log( Modified Sign: originalSign); return originalSign;Hook请求构建过程更彻底的方式是直接Hook构建请求体的地方比如OkHttp的RequestBody。你可以创建一个新的RequestBody并返回。5.3 处理复杂对象与加解密流程现实中的应用往往更复杂参数可能是JSON对象加密可能涉及多层嵌套。案例拦截一个被AES加密的JSON请求体假设应用先将数据转为JSON然后用AES加密最后Base64编码发送。定位加密函数用前述方法找到Cipher的doFinal方法调用并向上回溯找到传入doFinal的明文数据来源。Hook加密前的明文var SomeEncryptor Java.use(com.target.app.security.AESEncryptor); SomeEncryptor.encrypt.overload([B).implementation function(plainBytes) { // 将字节数组转为字符串假设是UTF-8编码的JSON var plainText Java.array(byte, plainBytes); // 注意这里需要正确转换 // 简便方法如果类内部有将byte[]转String的方法可以调用 // 或者更常见的Hook那个生成JSON字符串的函数 console.log([*] AES encrypt input (as String): String.fromCharCode.apply(String, plainBytes)); return this.encrypt(plainBytes); };实际上更聪明的做法是直接Hook生成JSON字符串的地方比如Gson.toJson()或JSONObject.toString()。var JSONObject Java.use(org.json.JSONObject); JSONObject.toString.implementation function() { var result this.toString(); console.log([*] JSONObject being converted to string: result); // 你可以在这里篡改result // var hackedResult result.replace(...); // return hackedResult; return result; };完整流程还原通过多个Hook点你可以拼凑出完整的“明文JSON - (可能压缩) - AES加密 - Base64编码”的流程。一旦掌握了这个流程你不仅可以窥探数据甚至可以编写外部脚本模拟应用生成合法的加密请求实现自动化测试或爬虫。注意事项多线程网络请求和加密操作可能在子线程进行。Frida的Hook是全局的但打印日志时要注意线程信息可以用Process.getCurrentThreadId()来区分。性能影响在implementation函数中执行复杂的操作如大量日志、网络请求会拖慢目标应用可能引发超时或行为异常。生产脚本应尽量精简。反调试与反Hook一些加固的应用会检测Frida。这涉及到更高级的对抗如隐藏Frida特征、使用frida-gum的Stalker进行跟踪等属于另一个深水区。通过本章的实战你已掌握了从网络层到业务层进行深度干预的核心技能。Frida将不再是黑盒工具而成为你手中一把可以随意解剖应用的手术刀。记住能力越大责任越大这些技术请务必在合法授权的范围内使用。