Frida实战:绕过Android应用SSL Pinning实现HTTPS抓包

发布时间:2026/7/4 16:27:11
Frida实战:绕过Android应用SSL Pinning实现HTTPS抓包 1. 项目概述为什么SSL Pinning是移动安全测试的“硬骨头”在移动应用安全测试和逆向分析领域HTTPS抓包是获取应用与服务器交互数据、分析业务逻辑的基石。常规操作很简单在测试设备上安装一个受信的根证书比如Burp Suite或Charles的证书然后配置代理大部分应用的HTTPS流量就能被我们轻松解密和查看。这就像拿到了邮局的万能钥匙可以打开所有寄往这个地址的信封。但现实往往更复杂。越来越多的应用尤其是金融、社交、支付等对安全性要求极高的应用会采用一种叫做**SSL Pinning证书绑定**的防御机制。它让上面那把“万能钥匙”瞬间失效。简单来说SSL Pinning就是应用在代码里“记住”了它只信任的特定证书或公钥。当应用发起HTTPS连接时它不仅会校验证书链是否由系统信任的CA签发这是标准SSL/TLS流程还会额外检查服务器返回的证书是否和它“记住”的那个证书一模一样。如果不一样即便这个证书是你安装的、被系统信任的Burp证书应用也会直接断开连接拒绝通信。这就好比应用不再信任“邮局”这个机构而是只认它熟悉的那个“邮递员”的工牌。你伪造的工牌哪怕再逼真只要不是它认识的那一个它就不会把信交给你。对于安全测试人员来说这意味着常规的抓包方法彻底失灵我们无法窥探应用内部的网络请求分析工作也就无从谈起。因此绕过SSL Pinning成为了移动安全特别是Android逆向工程中一项必备且核心的高级技能。而Frida作为一款强大的动态代码插桩工具正是攻克这道防线的最锋利武器。它允许我们在应用运行时动态地修改其内存和逻辑让应用“忘记”或“忽略”证书绑定的检查。本系列教程就将深入实战手把手带你用Frida这把“手术刀”精准地“切除”应用中的SSL Pinning验证逻辑。2. SSL Pinning原理深度解析与常见实现方式要绕过它必须先彻底理解它。SSL Pinning并非一个单一的技术点而是开发者为了增强TLS通信安全性而采取的一系列策略的统称。其核心思想是将服务器证书的特定信息“硬编码”或预置在客户端应用中在建立连接时进行比对。2.1 证书绑定的核心校验点应用进行Pinning校验时通常不直接比对整个证书文件那样太笨重而是比对从证书中提取出的“指纹”。主要有以下几种证书公钥Public KeyPinning这是最常见和推荐的方式。应用存储服务器证书的公钥哈希如SHA-256。在校验时它计算当前连接所用证书的公钥哈希与预存的哈希进行比对。即使服务器证书到期续期只要公私钥对没有更换公钥哈希就不会变应用无需更新。这种方式对运维最友好。证书指纹Certificate FingerprintPinning应用存储整个证书的哈希值如SHA-1或SHA-256。一旦服务器证书更新即使是同一个CA重新签发指纹就会改变导致连接失败必须更新客户端应用。这种方式非常严格但维护成本高。颁发者公钥Issuer Public KeyPinning存储证书颁发者CA的公钥哈希。它信任某个特定的CA而不是具体的终端证书。比公钥Pinning宽松一些但依然比完全信任系统根证书库要严格。2.2 Android中SSL Pinning的常见实现库在Android开发中实现SSL Pinning通常不直接操作底层的javax.net.ssl而是借助一些成熟的安全库这决定了我们Hook的切入点OkHttpSquare公司出品目前Android生态中最主流的网络库。它通过CertificatePinner类非常方便地实现Pinning。开发者可以这样写OkHttpClient client new OkHttpClient.Builder() .certificatePinner(new CertificatePinner.Builder() .add(api.example.com, sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) .build()) .build();我们的Hook目标就是让CertificatePinner的检查方法总是返回成功。Apache HttpClient已废弃但仍有老应用使用通过自定义SSLSocketFactory和TrustManager来实现。Xamarin / .NET MAUI使用ServicePointManager或HttpClientHandler的ServerCertificateValidationCallback。React Native / Flutter这些跨平台框架的网络请求最终会调用原生Android/iOS的网络库。对于Android侧仍然是OkHttp或系统API。因此Hook点通常在原生层。自定义TrustManager一些应用会自己实现X509TrustManager接口在checkServerTrusted方法中加入自定义的证书校验逻辑。这是我们Hook的经典位置。底层SSL库如OpenSSL/ BoringSSL一些对性能和安全有极致要求的应用如大型社交、支付App可能直接使用C/C编写的原生SSL库并在JNIJava Native Interface层进行绑定校验。这需要我们具备Native层的Hook能力。了解这些实现方式就像拿到了建筑物的结构图。我们的Frida脚本就是根据这张图找到承重墙校验函数的关键节点进行精准的“干预”。3. Frida环境搭建与基础Hook流程回顾在开始手术前必须确保手术刀Frida是锋利且顺手的。虽然Frida的安装不是本篇核心但一个稳定高效的环境是成功的前提。这里快速回顾并强调几个实战中容易踩坑的点。3.1 服务端与客户端配置要点Frida Server版本匹配这是最常见的问题。你PC上安装的frida-tools客户端版本必须与推送到Android设备上的frida-server服务端版本严格一致。最好都使用同一版本号的最新稳定版。用frida --version和手机shell中./frida-server --version检查。设备架构下载frida-server时务必对应你的设备架构arm,arm64,x86,x86_64。现代手机基本都是arm64。用adb shell getprop ro.product.cpu.abi命令确认。运行权限将frida-server推送到设备后需要赋予可执行权限并以root权限运行。adb push frida-server /data/local/tmp/ adb shell su cd /data/local/tmp chmod 755 frida-server ./frida-server 注意有些深度定制的ROM如某些国产手机的系统即使root了在/data/local/tmp路径下也可能无法正常运行frida-server。可以尝试将其推到/sdcard目录或者直接推送到/system/bin需remount system分区为可写风险较高。更稳妥的做法是使用Magisk模块来安装和启动Frida Server。3.2 基础Hook脚本结构与调试技巧一个典型的Frida JavaScript脚本结构如下我们以Hook一个简单函数为例Java.perform(function () { // 定位要Hook的类 var TargetClass Java.use(com.example.app.TrustManagerImpl); // Hook类中的特定方法 TargetClass.checkServerTrusted.implementation function(chain, authType) { console.log([*] checkServerTrusted called!); // 打印调用栈对于定位问题至关重要 console.log(Java.use(android.util.Log).getStackTraceString(Java.use(java.lang.Exception).$new())); // 打印传入的证书链 for (var i 0; i chain.length; i) { console.log(Cert i : chain[i].getSubjectDN()); } // 原方法本应抛出异常现在我们什么都不做直接静默通过 // 或者可以选择调用原方法但不处理异常 // try { this.checkServerTrusted(chain, authType); } catch(e) {} console.log([*] SSL Pinning check bypassed.); }; });实战调试技巧console.log是你的眼睛在关键位置打印参数、返回值、调用栈这是理解应用逻辑最直接的方法。使用frida -U -f com.example.app -l script.js --no-pause-U连接USB设备-f启动应用-l加载脚本--no-pause立即启动主线程。这对于需要从应用启动就开始Hook的场景非常有用。setTimeout解决时机问题有时Hook代码执行时目标类可能还未被加载。可以用setTimeout将Hook逻辑包裹延迟执行。setTimeout(function() { Java.perform(function () { // Hook代码 }); }, 1000);环境就绪原理清晰接下来我们就进入最核心的实战环节如何找到并Hook那些执行SSL Pinning校验的关键函数。4. 定位SSL Pinning关键校验函数静态分析与动态探索面对一个开启了SSL Pinning的未知应用第一步也是最重要的一步是找到它在代码中的哪个位置进行了校验。这是一个“侦探”工作结合静态分析和动态追踪。4.1 静态分析寻找线索即使没有应用的源代码我们也可以通过反编译工具如JADX-GUI、GDA查看其Smali或Java代码搜索关键字符串和类名。搜索关键词CertificatePinner(OkHttp)checkServerTrusted(TrustManager)X509TrustManagerSSLContextTrustManagerFactorypinsha256/特定证书的哈希字符串在反编译的代码中可能以常量形式存在分析网络库查看build.gradle或依赖文件看它使用了OkHttp、Retrofit底层是OkHttp、Volley还是HttpURLConnection。这能帮你快速缩小Hook的范围。4.2 动态Hook探索枚举与试错当静态分析找不到明显线索或者代码被混淆时动态Hook就派上用场了。我们可以写一个“侦察兵”脚本广泛地Hook一些常见类观察日志输出。Java.perform(function () { // 1. Hook 所有实现 X509TrustManager 接口的类的 checkServerTrusted 方法 var X509TrustManager Java.use(javax.net.ssl.X509TrustManager); var trustManagers Java.enumerateMethods(javax.net.ssl.X509TrustManager* checkServerTrusted); // ... 遍历并Hook // 2. Hook OkHttp 的 CertificatePinner.check try { var CertificatePinner Java.use(okhttp3.CertificatePinner); CertificatePinner.check.overload(java.lang.String, java.util.List).implementation function(p0, p1) { console.log([*] OkHttp CertificatePinner.check called for host: p0); // 直接通过不执行原方法 // this.check(p0, p1); // 如果调用原方法就会抛出异常 }; } catch(e) { console.log(OkHttp not used or class not found: e); } // 3. Hook SSLContext.init 方法看它使用了哪些 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 called.); if (trustManagers) { for (var i 0; i trustManagers.length; i) { console.log( TrustManager[ i ]: trustManagers[i].$className); } } // 继续调用原方法不影响正常初始化 return this.init(keyManagers, trustManagers, secureRandom); }; });运行这个脚本然后操作应用触发网络请求。观察控制台输出哪个Hook点被触发了并且触发了之后网络请求就失败了那这个点就极有可能是SSL Pinning的关键校验处。4.3 利用异常堆栈定位一个更粗暴有效的方法是先让应用崩溃一次。不进行任何Hook直接配置代理和安装Burp证书然后触发网络请求。应用会因为证书校验失败而抛出异常如CertificateException,SSLHandshakeException。在Android Studio的Logcat中过滤这个异常。异常的调用栈Stack Trace会清晰地指出是哪个类、哪个方法、哪一行代码抛出了这个异常。这个抛出点就是你需要Hook的“七寸”。你可以根据调用栈中的类名和方法名编写精准的Frida Hook脚本。5. 实战Hook案例一通用型TrustManager绕过这是最经典、兼容性最广的绕过方法。其原理是无论应用使用何种网络库或自定义逻辑最终在Java层SSL证书的校验都会落到一个实现了X509TrustManager接口的对象上。我们Hook这个接口的核心方法使其失去校验能力。5.1 HookcheckServerTrusted方法X509TrustManager接口有三个方法其中checkServerTrusted是校验服务器证书链的核心。我们的目标是让这个方法“闭嘴”无论传入什么证书都表示信任。Java.perform(function () { // 首先尝试Hook系统默认的TrustManager实现适用于使用系统默认SSLContext的应用 var X509TrustManager Java.use(javax.net.ssl.X509TrustManager); var SSLContext Java.use(javax.net.ssl.SSLContext); // Hook SSLContext的getDefault方法替换其返回的TrustManager SSLContext.getDefault.implementation function() { var originalContext this.getDefault(); console.log([*] SSLContext.getDefault() called.); // 创建一个我们自定义的、什么都不做的TrustManager var MyTrustManager Java.registerClass({ name: com.bypass.MyTrustManager, implements: [X509TrustManager], methods: { checkClientTrusted: function(chain, authType) { console.log([*] MyTrustManager: checkClientTrusted (bypassed)); }, checkServerTrusted: function(chain, authType) { console.log([*] MyTrustManager: checkServerTrusted called for authType: authType); // 关键不执行任何校验直接放行 // 原方法会在这里抛出CertificateException我们什么都不做 for(var i0; ichain.length; i) { console.log( |- Cert[ i ] Subject: chain[i].getSubjectDN()); } }, getAcceptedIssuers: function() { return []; } } }); // 获取原Context的SocketFactory并替换其TrustManager此处略复杂需要反射 // 更通用的做法是直接Hook所有X509TrustManager实例的checkServerTrusted方法 return originalContext; }; // 更暴力通用的方法替换所有已加载类中X509TrustManager接口方法的实现 // 注意这会影响到整个进程内所有的SSL校验包括WebView等需谨慎。 Java.enumerateLoadedClasses({ onMatch: function(className) { if (className.includes(X509TrustManager)) { console.log([] Found X509TrustManager implementation: className); try { var TrustManagerClass Java.use(className); // Hook checkServerTrusted var checkServerTrustedOverloads TrustManagerClass.checkServerTrusted.overloads; for (var i 0; i checkServerTrustedOverloads.length; i) { checkServerTrustedOverloads[i].implementation function(chain, authType) { console.log([*] Bypassing checkServerTrusted in: this.$className); // 静默通过不调用原方法 }; } // 也可以选择性地Hook checkClientTrusted } catch (e) { // 忽略无法Hook的类如抽象类、接口 } } }, onComplete: function() { console.log([*] X509TrustManager enumeration complete.); } }); });注意事项作用范围这种全局Hook会影响应用内所有HTTPS请求可能导致一些依赖严格校验的内部组件行为异常但通常对抓包目标无影响。混淆处理如果类名被混淆如a.a,b.cenumerateLoadedClasses并过滤X509TrustManager可能找不到。此时需要结合动态调试在证书校验失败时查看堆栈找到具体的混淆类名进行精准Hook。WebView应用内的WebView可能使用独立的网络栈此方法不一定对其生效。WebView的SSL Pinning绕过是另一个话题通常需要Hookandroid.webkit.WebViewClient的onReceivedSslError方法。5.2 针对自定义TrustManager的Hook很多应用不会直接实现X509TrustManager而是继承自已有的实现类如X509ExtendedTrustManager或者包装一层。我们同样可以通过堆栈信息找到这个自定义类。假设通过日志我们发现校验发生在com.secure.app.CustomTrustManager类中Java.perform(function () { var CustomTrustManager Java.use(com.secure.app.CustomTrustManager); // 需要先确定方法签名。可能不止一个checkServerTrusted方法。 // 方法1Hook所有重载 CustomTrustManager.checkServerTrusted.overloads.forEach(function(overload) { overload.implementation function() { console.log([*] CustomTrustManager.checkServerTrusted bypassed.); // 直接返回不执行任何操作也不调用原方法。 // 如果原方法有返回值void除外可能需要返回一个空数组或null具体看方法签名。 }; }); // 方法2如果知道具体签名例如参数是X509Certificate[]和String CustomTrustManager.checkServerTrusted.overload([Ljava.security.cert.X509Certificate;, java.lang.String).implementation function(chain, authType) { console.log([*] Bypassing custom pinning for authType: authType); // 静默通过 }; });6. 实战Hook案例二OkHttp CertificatePinner绕过如果应用使用OkHttp那么绕过将非常直接和优雅。OkHttp的证书绑定逻辑封装在CertificatePinner类中我们只需要让它的check方法失效即可。6.1 直接禁用CertificatePinner.check这是最有效的方法。找到构建OkHttpClient的地方将其CertificatePinner替换成一个什么都不做的对象或者直接Hookcheck方法。Java.perform(function () { // 方法AHook CertificatePinner类的check方法 var CertificatePinner Java.use(okhttp3.CertificatePinner); CertificatePinner.check.overload(java.lang.String, java.util.List).implementation function(hostname, pins) { console.log([*] OkHttp CertificatePinner.check intercepted for host: hostname); console.log( Pins configured: pins); // 关键不执行原方法直接返回void相当于跳过了校验。 // this.check(hostname, pins); // 千万不要调用这行 }; // 方法B更彻底替换整个CertificatePinner对象如果应用在运行时构建Client // Hook OkHttpClient.Builder的build方法或certificatePinner方法 var Builder Java.use(okhttp3.OkHttpClient$Builder); Builder.certificatePinner.implementation function(certificatePinner) { console.log([*] OkHttpClient.Builder.certificatePinner() called, returning a no-op pinner.); // 返回一个空的、不进行任何检查的CertificatePinner实例 var NoOpPinner CertificatePinner.$new(); return this.certificatePinner(NoOpPinner); // 或者直接返回NoOpPinner }; });6.2 处理OkHttp的混淆在混淆后的应用中okhttp3.CertificatePinner类名可能保持不变如果用了ProGuard规则保护了OkHttp但方法名可能被混淆。此时我们需要通过动态分析来找到正确的方法签名。先Hookokhttp3.OkHttpClient的构造或发送请求的方法如newCall。在请求发生时打印出OkHttpClient实例的certificatePinner属性。或者枚举CertificatePinner类的所有方法然后Hook那些参数为String和List的方法进行尝试。Java.perform(function() { var CertificatePinner Java.use(okhttp3.CertificatePinner); var methods CertificatePinner.class.getDeclaredMethods(); // 在Frida中需要通过Java.use(java.lang.Class)的反射API来操作这里是一个思路。 // 更简单的方法是直接通过堆栈定位被调用的方法名。 });7. 实战Hook案例三针对特定JNI/Native层的校验当校验逻辑下沉到C/C层时我们就需要用到Frida的Native Hook功能Interceptor。这难度陡增但思路相通找到校验函数修改其返回值。7.1 定位Native函数从Java层追踪在Java层证书校验的堆栈中如果看到native方法调用或者类名包含JNI、Native等字样说明进入了Native层。搜索so库在应用的lib目录下查找包含ssl、crypto、pinning等关键词的.so文件如libnative-ssl.so。使用frida-trace进行追踪这是一个强大的动态追踪工具。frida-trace -U -i SSL_* -i *verify* -i *pinning* 应用包名这条命令会追踪所有包含SSL_、verify、pinning的函数调用并生成Hook脚本模板。7.2 Hook Native函数示例假设我们通过分析发现校验函数在libnative-lib.so中函数名是native_verify_cert。Java.perform(function () { // 首先确保so库已加载 var libnative Module.findBaseAddress(libnative-lib.so); if (libnative) { console.log([] libnative-lib.so base: libnative); // 假设我们知道了函数的相对地址或通过导出符号找到了它 // 方法1通过导出符号名如果函数是JNIEXPORT的 var verifyCertAddr Module.findExportByName(libnative-lib.so, Java_com_example_app_NativeHelper_verifyCert); // 方法2通过偏移量需要IDA等静态分析工具 // var verifyCertAddr libnative.add(0x1234); if (verifyCertAddr) { console.log([] Found verifyCert at: verifyCertAddr); // 使用Interceptor进行Hook Interceptor.attach(verifyCertAddr, { onEnter: function(args) { console.log([] native_verify_cert called!); // args[0], args[1]... 是参数根据函数签名分析 // 例如打印第一个参数可能是JNIEnv* }, onLeave: function(retval) { console.log([] native_verify_cert returning.); // 修改返回值假设原函数返回1表示成功0表示失败。 // 我们强制让它返回1成功。 retval.replace(ptr(1)); // 注意需根据实际返回值类型int, bool等调整 console.log([] Return value replaced to 1 (SUCCESS).); } }); } else { console.log([-] Could not find verifyCert function.); } } else { console.log([-] libnative-lib.so not loaded.); } });Native Hook的挑战函数定位需要一定的逆向工程基础使用IDA Pro、Ghidra等工具分析so文件找到校验函数的确切地址或符号。参数与返回值分析需要理解函数的调用约定ARM/ARM64、参数类型和返回值类型才能正确读取和修改。稳定性错误的Hook可能导致应用崩溃。8. 常见问题排查与脚本优化技巧在实际操作中你几乎一定会遇到各种问题。这里汇总了一些典型的“坑”和解决方案。8.1 问题排查清单问题现象可能原因排查步骤与解决方案Frida连接被拒绝1. Frida Server未运行或已退出。2. 设备未Root或ADB未获得Root权限。3. 端口冲突默认27042。4. 设备存在反调试/反Frida机制。1.adb shell进入ps | grep frida检查进程重启frida-server。2. 确认adb root成功或使用su后启动server。3. 尝试用frida -U -p PID连接特定进程。4. 尝试使用隐藏Frida的脚本或工具如frida-server改名。应用启动崩溃1. Hook的时机不对在类未加载时进行Hook导致异常。2. Hook的函数签名错误或修改了不该修改的内存。3. 脚本存在语法错误。1. 使用setTimeout延迟Hook或HookApplication.onCreate()等早期生命周期。2. 仔细核对函数签名参数、返回值类型使用overloads遍历所有重载。3. 使用frida -l script.js检查脚本语法。Hook成功但抓包仍失败1. Hook点不正确未命中真正的校验逻辑。2. 存在多线程或异步校验Hook未覆盖所有线程。3. 证书绑定发生在Native层Java层Hook无效。4. 应用使用了证书双向验证mTLS。1. 加强动态分析通过异常堆栈精确定位。2. 确保Hook是全局的或尝试Hook更底层的类如TrustManagerFactory。3. 使用frida-trace追踪SSL相关Native函数。4. mTLS需要同时处理客户端证书情况更复杂需另寻方法。脚本执行后应用卡死或无响应1. 在Hook函数中进行了阻塞操作如网络请求。2. 死循环或递归调用。3. 修改了关键的系统对象导致其他线程异常。1. 确保Hook函数快速执行完毕避免复杂逻辑。2. 检查脚本逻辑避免this.method()调用原方法时产生循环。3. 尽量采用“静默通过”策略而非修改复杂对象。8.2 脚本优化与高级技巧条件Hook与精准打击不要无差别地Hook所有可能类这会影响性能和稳定性。先侦察再针对性地编写脚本。// 只Hook特定包名下的类 Java.enumerateLoadedClasses({ onMatch: function(className) { if (className.startsWith(com.target.app.security.)) { // 只Hook这个包下的类 } }, onComplete: function() {} });使用Java.choose()Hook已存在对象对于已经实例化的对象Java.use修改的是类蓝图对所有实例生效。而Java.choose可以遍历堆上的现有对象对其单独操作。Java.choose(okhttp3.OkHttpClient, { onMatch: function(instance) { console.log([*] Found OkHttpClient instance: instance); var pinner instance.certificatePinner(); if (pinner ! null) { console.log( It has a CertificatePinner.); // 可以尝试替换这个实例的pinner属性需要反射 } }, onComplete: function() {} });脚本模块化与复用将通用的Hook逻辑如通用TrustManager绕过写成一个函数或独立模块方便在不同项目中快速导入和使用。应对代码混淆对于重度混淆的应用类名和方法名都变成了a,b,c。此时动态堆栈分析是你的唯一可靠朋友。触发证书错误捕获异常堆栈记录下那个关键的a.a.a()调用位置。然后根据这个混淆后的类名和方法名进行Hook。有时方法参数的类型信息也能帮你定位正确的重载。绕过SSL Pinning是一场与应用开发者的攻防博弈。没有一成不变的银弹脚本。核心在于理解原理掌握动态分析的工具和方法Frida, Logcat, 堆栈跟踪并具备耐心和细心去定位那个关键的校验点。一旦找到用Frida“解除”它往往就是几行代码的事。