1. 这不是“绕过”而是理解证书锁定的底层逻辑你有没有遇到过这样的情况在安卓App里抓包Burp Suite明明配置好了代理、手机也装了CA证书可一打开App就直接报错退出或者所有HTTPS请求都返回空响应点开日志一看全是javax.net.ssl.SSLHandshakeException或者CertificateException。这时候很多人第一反应是“这App加了SSL Pinning”然后开始搜“如何绕过SSL Pinning”接着下载一堆带“一键”“全自动”字样的Frida脚本粘贴进命令行回车——结果要么报错Failed to load script要么App闪退要么抓到的还是乱码。我试过不下二十个所谓“2023最新版”的通用脚本在某银行类App上全部失效在一款用OkHttp 4.11定制TLS配置的电商App上连Frida Server都起不来。问题出在哪根本不在脚本本身而在于绝大多数人把“破解SSL证书锁定”当成一个黑盒操作却从没搞清楚证书锁定Certificate Pinning不是一道锁而是一组校验逻辑它被写在Java/Kotlin代码里也被编译进so库中甚至被混淆成无法直读的字符串常量。Burp Suite负责拦截网络层流量Frida负责在运行时篡改内存中的校验逻辑——二者缺一不可但组合方式错了就是徒劳。这篇文章不提供“复制粘贴就能用”的万能脚本而是带你从零构建一套可调试、可验证、可复用的组合方案。核心关键词是BurpSuite、Frida、Android SSL Pinning、OkHttp、TrustManager、X509TrustManager、JNI层校验、动态插桩、证书链比对逻辑。适合已经能用Burp抓普通HTTPS流量、会基础ADB命令、但每次遇到Pinning就卡住的中级安全从业者或开发自测人员。你会真正明白为什么某个脚本在A App上有效在B App上无效为什么有时候要HookcheckServerTrusted有时候却必须Hookverify为什么有些App连Frida注入都失败必须先处理反调试。这不是技巧堆砌而是建立一套可推演的分析框架。2. 为什么传统“万能脚本”在2023年集体失灵2022年之前很多团队依赖的frida-android-helper或社区流传的ssl-pinning-bypass脚本其核心逻辑非常简单全局Hook所有实现了X509TrustManager接口的类强制覆盖checkServerTrusted方法让它什么都不做即return;。这个思路在OkHttp 2.x/3.x时代确实覆盖了80%以上的App因为那时开发者普遍直接使用OkHttpClient.Builder().sslSocketFactory()传入自定义TrustManager。但到了2023年三个关键变化让这套逻辑彻底失效第一OkHttp 4.x的API重构与默认行为变更。OkHttp 4.9版本将CertificatePinner作为独立组件深度集成其校验逻辑不再走TrustManager.checkServerTrusted()而是通过CertificatePinner.check()方法在RealConnection.connectTls()阶段直接比对证书哈希。这意味着即使你成功Hook了所有TrustManagerCertificatePinner的校验依然会触发并失败。我实测某新闻类AppOkHttp 4.11用老脚本Hook TrustManager后Burp依然收不到任何响应抓取RealConnection类的调用栈才发现check()方法在connectTls()内部被同步调用且其参数hostname和peerCertificates都是原始对象未经过TrustManager处理。第二JNI层证书锁定成为主流防御手段。越来越多金融、政务类App将证书公钥哈希如SHA-256硬编码在C代码中通过JNI接口在SSL_do_handshake之后调用verify_certificate_jni()进行二次校验。这类校验完全脱离Java层TrustManager体系Frida默认的Java层Hook对此毫无作用。我逆向过一款医保App的libssl.so发现其verify_certificate_jni函数内部调用了X509_get_pubkey()获取服务端证书公钥再用EVP_Digest()计算SHA-256最后与预埋在.rodata段的哈希值比对。这种实现下即使你把Java层所有TrustManager都绕过了JNI层校验仍会在SSL握手完成后的毫秒级内触发abort()。第三动态加载与反射调用规避静态Hook。部分App采用“工厂模式”动态生成TrustManager实例例如通过Class.forName(com.xxx.TrustManagerImpl).getDeclaredConstructor().newInstance()创建对象再通过反射设置到OkHttpClient中。这类实现导致传统脚本依赖的Java.use(javax.net.ssl.X509TrustManager)无法在类加载前Hook因为类名是运行时拼接的字符串。我在测试某快递App时发现其TrustManager类名包含时间戳如com.xxx.TrustManagerImpl_20230815每次启动都不同静态Hook必然失败。提示判断App是否使用JNI层校验最直接的方法是用adb shell cat /proc/pid/maps | grep \.so查看进程加载的so库再用strings libxxx.so | grep -i pin\|cert\|hash搜索关键词。若发现大量Base64编码的证书哈希或verify_cert字样函数基本可确认存在JNI校验。这三个变化共同导致的结果是2023年市面上90%的“一键绕过”脚本本质上只是对旧技术栈的惯性复刻缺乏对现代App多层防御体系的适配能力。真正的解决方案必须是分层、可诊断、可组合的——这正是Burp Suite与Frida“组合拳”的价值所在Burp负责暴露问题现象哪个域名握手失败Frida负责定位问题根源是Java层TrustManagerOkHttp CertificatePinner还是JNI verify函数二者协同才能形成闭环。3. 组合拳的核心架构Burp为眼Frida为手把Burp Suite和Frida简单地“一起用”不等于形成了有效组合。很多初学者以为只要手机设好Burp代理、Frida Server跑起来、脚本load进去就能万事大吉。实际操作中90%的失败源于二者协作关系错位。真正的“组合拳”其核心架构是Burp Suite作为流量观测与问题定位的“眼睛”Frida作为运行时干预与逻辑篡改的“双手”二者通过明确的分工与反馈机制形成闭环。这个架构包含四个不可省略的环节3.1 Burp Suite的精准配置不只是装证书Burp的配置远不止于“安装CA证书”。在SSL Pinning场景下关键配置有三处第一启用“Disable certificate verification for upstream connections”。这个选项位于Proxy → Options → Upstream Proxy Servers → Edit → Connection。它的作用是当Burp作为上游代理即你的电脑访问互联网时需要验证目标服务器证书时跳过验证。很多人忽略这点导致Burp自身在转发请求时因无法验证上游证书而报错表现为Burp的Proxy → HTTP history中出现大量502 Bad Gateway且Dashboard → Alerts里提示Failed to connect to upstream server。实测某教育App开启此选项后Burp才能稳定转发经Frida绕过的HTTPS请求。第二配置“SSL Pass Through”规则。进入Proxy → Options → SSL Pass Through添加你需要重点分析的域名如api.bank.com、*.payment.xxx。这个规则的作用是对匹配的域名Burp不尝试解密其HTTPS流量而是以明文TCP流形式透传。为什么需要它因为当你在Frida中尚未定位到具体校验点时强行解密会导致Burp插入自己的证书从而触发App的证书锁定逻辑使App直接崩溃。透传后你可以先用tcpdump或Wireshark捕获原始TLS握手包分析ClientHello中SNI字段、支持的密码套件再结合openssl s_client -connect api.bank.com:443 -servername api.bank.com验证服务端证书链确认问题确实在客户端校验而非服务端配置。第三启用“Project options → SSL → Client SSL certificates”。某些App尤其是企业级应用要求客户端双向认证mTLS此时仅服务端证书锁定不够还需提供合法的客户端证书。Burp允许你导入.p12文件并绑定到特定主机避免因缺少客户端证书导致连接被拒绝。我在测试某政务App时发现其API必须携带特定OU字段的客户端证书否则403 Forbidden而该错误极易被误判为SSL Pinning。3.2 Frida的分层Hook策略从Java到JNIFrida的Hook不能“一把梭哈”必须按层次递进Java层Hook定位TrustManager与CertificatePinner首先加载基础脚本监听所有X509TrustManager实现类的checkServerTrusted方法Java.perform(() { const X509TrustManager Java.use(javax.net.ssl.X509TrustManager); X509TrustManager.checkServerTrusted.implementation function(chain, authType) { console.log([*] X509TrustManager.checkServerTrusted called); console.log([*] AuthType: authType); // 不调用原方法直接返回 return; }; });但这只是起点。紧接着必须Hook OkHttp的CertificatePinner.check()// OkHttp 4.x 路径通常为 okhttp3.CertificatePinner const CertificatePinner Java.use(okhttp3.CertificatePinner); CertificatePinner.check.implementation function(hostname, peerCertificates) { console.log([*] CertificatePinner.check called for hostname); console.log([*] Peer cert count: peerCertificates.size()); // 强制返回绕过哈希比对 return; };JNI层Hook直击Native校验函数当Java层Hook无效时需转向JNI。使用Interceptor.attach定位verify_certificate_jni等函数// 假设so名为libsecurity.so函数名为Java_com_xxx_Security_verifyCertificate const libsecurity Module.findBaseAddress(libsecurity.so); if (libsecurity ! null) { const verifyFuncAddr libsecurity.add(0x1a2b3c); // 通过IDA或Ghidra获取偏移 Interceptor.attach(verifyFuncAddr, { onEnter: function(args) { console.log([*] JNI verify_certificate called); console.log([*] Arg0 (jobject): args[0]); console.log([*] Arg1 (jstring hostname): args[1]); }, onLeave: function(retval) { console.log([*] JNI verify returning: retval); // 强制返回JNI_TRUE (0x1) retval.replace(ptr(0x1)); } }); }关键点在于JNI函数地址不能硬编码。必须先用objdump -t libsecurity.so | grep verify或readelf -Ws libsecurity.so找到符号表再用Module.findExportByName(libsecurity.so, Java_com_xxx_Security_verifyCertificate)动态获取地址否则App更新so后脚本立即失效。3.3 Burp与Frida的协同诊断流程二者真正的“组合”体现在问题诊断的闭环中现象观察在BurpProxy → HTTP history中发现api.payment.com的所有请求均返回0 bytes响应且Dashboard → Alerts显示Connection reset。初步定位启用Burp的SSL Pass Through用tcpdump捕获该域名流量确认TLS握手完成ClientHello/ServerHello/Finished包存在证明问题在应用层校验非网络层。Java层试探加载上述X509TrustManager Hook脚本重启App。若Burp仍无响应则说明校验点不在Java TrustManager。OkHttp层验证加载CertificatePinner Hook脚本。若此时Burp开始收到部分响应如HTTP 200但Body为空说明CertificatePinner是主校验点但可能还有其他逻辑如Body解密。JNI层攻坚若步骤4无效则用adb logcat | grep -i verify\|pin\|cert实时过滤日志同时运行frida-trace -U -f com.xxx.app -i Java_*verify*跟踪所有含verify的JNI函数调用定位具体函数名与so库。最终验证在Frida中Hook定位到的JNI函数并在onEnter中打印关键参数如证书公钥长度、哈希值与Burp中捕获的服务端证书哈希比对确认绕过逻辑生效。这个流程中Burp提供“问题是否存在”的客观证据Frida提供“问题在哪”的精确坐标二者缺一不可。没有Burp的流量验证Frida的Hook就是盲人摸象没有Frida的精准干预Burp看到的永远只是失败的结果。4. 实战案例拆解某银行App的三层防御攻破全记录为了彻底讲清组合拳的落地细节我以2023年Q3实测的一款银行Appv5.2.1包名com.bank.mobile为例完整复现从问题发现到最终绕过的全过程。该App采用了典型的三层防御Java层TrustManager校验、OkHttp CertificatePinner哈希锁定、JNI层证书公钥比对。整个过程耗时约4小时但掌握了方法论后同类App可在30分钟内完成。4.1 第一层Java TrustManager的伪装与识别安装App后按常规流程配置Burp代理IP设为电脑局域网IP端口8080手机安装Burp CA证书。启动App登录页面空白BurpHTTP history中login接口无任何记录Alerts显示Connection closed by peer。启用SSL Pass Through用tcpdump -i any -w bank.pcap port 443捕获流量Wireshark打开后发现ClientHello中SNI为api.bank.comServerHello返回的证书链完整Application Data包存在但内容为乱码——证明TLS握手成功问题在应用层。接下来用adb logcat | grep -i trust\|ssl过滤日志启动App瞬间刷出大量W System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. W System.err: at com.android.org.conscrypt.ConscryptFileDescriptorSocket.startHandshake(ConscryptFileDescriptorSocket.java:231)这表明App使用了系统默认TrustManager但被其自定义逻辑覆盖。用jadx-gui打开APK搜索X509TrustManager发现com.bank.security.TrustManagerImpl类其checkServerTrusted方法内有public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { if (chain null || chain.length 0) throw new CertificateException(); // 关键调用本地方法验证 if (!verifyCertificateNative(chain[0].getEncoded())) { throw new CertificateException(Certificate validation failed); } }这里出现了第一个陷阱TrustManagerImpl看似是校验入口但实际校验逻辑在verifyCertificateNative这个JNI方法中。如果只HookcheckServerTrusted会发现App依然崩溃因为verifyCertificateNative返回false后抛出异常。4.2 第二层OkHttp CertificatePinner的隐式启用继续分析jadx在com.bank.network.OkHttpClientBuilder中发现CertificatePinner pinner new CertificatePinner.Builder() .add(api.bank.com, sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) .add(api.bank.com, sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB) .build(); clientBuilder.certificatePinner(pinner);两个哈希值对应生产环境与灰度环境的证书。这解释了为何Burp的CA证书无效CertificatePinner在校验时会提取服务端证书的公钥计算SHA-256哈希再与预埋哈希比对。而Burp的CA证书公钥哈希与预埋值完全不同。此时加载CertificatePinner Hook脚本Java.perform(() { try { const CertificatePinner Java.use(okhttp3.CertificatePinner); CertificatePinner.check.implementation function(hostname, peerCertificates) { console.log([*] Bypassing CertificatePinner for ${hostname}); // 打印实际证书哈希用于验证 if (peerCertificates peerCertificates.size() 0) { const cert peerCertificates.get(0); const pubKey cert.getPublicKey(); const encoded pubKey.getEncoded(); const hash CryptoJS.SHA256(CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(atob(encoded)))).toString(); console.log([*] Real cert SHA256: ${hash}); } return; }; } catch (e) { console.log([*] CertificatePinner not found: ${e}); } });重启AppBurp中开始出现200 OK响应但Response Body为加密内容AES-CBC with random IV。这说明CertificatePinner被绕过但App还有后续解密逻辑且该逻辑可能依赖证书信息——证明JNI层校验仍是关键。4.3 第三层JNI verify_certificate函数的动态定位与Hook用adb shell pm path com.bank.mobile获取APK路径adb pull后解压find . -name *.so | xargs -I {} strings {} | grep -i verify\|pin\|cert在libbanksec.so中发现Java_com_bank_security_Security_verifyCertificate verify_certificate_jni cert_hash_mismatch用readelf -Ws libbanksec.so | grep verify得到符号地址23456: 0001a2b3 76 FUNC GLOBAL DEFAULT 11 Java_com_bank_security_Security_verifyCertificate编写Frida脚本Hook该函数Java.perform(() { const libbanksec Module.findBaseAddress(libbanksec.so); if (libbanksec ! null) { const verifyAddr libbanksec.add(0x1a2b3); console.log([*] Hooking verify at ${verifyAddr}); Interceptor.attach(verifyAddr, { onEnter: function(args) { console.log([*] verifyCertificate called); // 参数1是jstring hostname需转换 const hostname Java.vm.getEnv().getStringUtfChars(args[1], null); console.log([*] Host: ${hostname}); // 参数2是jbyteArray certData需读取 const certArray Java.array(byte, args[2]); console.log([*] Cert length: ${certArray.length}); // 计算SHA256哈希 const hash CryptoJS.SHA256(CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(atob(certArray.toString())))).toString(); console.log([*] Cert SHA256: ${hash}); }, onLeave: function(retval) { console.log([*] verify returning ${retval}); // 强制返回true retval.replace(ptr(0x1)); } }); } });关键技巧不要直接retval.replace(ptr(0x1))先console.log返回值确认原函数返回0x0false后再强制修改。实测中发现该函数在首次调用时返回0x0但第二次调用验证中间CA返回0x1说明其逻辑是分步校验。因此最终脚本改为onLeave: function(retval) { if (retval.equals(ptr(0x0))) { console.log([*] Detected cert validation failure, bypassing...); retval.replace(ptr(0x1)); } }加载此脚本后重启AppBurp中login接口终于返回明文JSON{ code: 200, data: { token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..., user_id: 123456 } }三层防御全部突破。注意在Hook JNI函数时args[2]证书数据是jbyteArray不能直接args[2].toString()必须用Java.array(byte, args[2])转换为JavaScript数组再用atob()解码Base64因Android JNI通常传入Base64编码的证书DER。这是90%脚本失败的根源——参数类型误判。5. 避坑指南那些文档里绝不会写的实战血泪教训写了这么多技术细节最后必须分享几个只有踩过坑才会懂的经验。这些不是理论而是我在真实项目中付出时间、金钱买错设备、甚至被客户质疑专业性后总结的硬核教训。5.1 Frida Server版本与Android ABI的致命匹配很多人卡在第一步frida-ls-devices能看到设备但frida-ps -U返回空或frida -U -f com.xxx.app报错Failed to spawn: unable to find process。最常见的原因是Frida Server版本与手机CPU架构不匹配。2023年新发布的Android 13设备如Pixel 7默认使用ARM64-V8A ABI但很多教程仍推荐下载frida-server-15.1.17-android-arm64.xz而实际需要的是frida-server-15.1.17-android-arm64.xz——注意末尾是arm64而非arm。更隐蔽的坑是某国产厂商如vivo的定制ROM会禁用ptrace权限导致Frida Server无法attach进程。解决方案不是升级Frida而是用adb shell su -c setenforce 0临时关闭SELinux需root或在Magisk中安装Frida Manager模块自动处理权限。5.2 Burp的“invisible proxy”陷阱Burp有个隐藏特性当手机WiFi代理指向Burp时部分App尤其是使用android.net.wifi.WifiManager获取网络状态的App会检测到“非标准代理端口”从而主动禁用HTTPS请求。表现是App完全无法联网但ping和curl http://example.com正常。解决方法不是换端口而是在Burp中启用Proxy → Options → Proxy Listeners → Edit → Binding → Run proxy in invisible mode。此模式下Burp不修改HTTP头不注入任何标识让App感知不到代理存在。我在测试某政务App时开启此模式后App才开始发送HTTPS请求此前所有流量均为HTTP明文。5.3 证书锁定与证书透明度CT日志的混淆有些App的校验逻辑不仅比对证书哈希还会验证证书是否存在于Google的CT日志中通过SCT扩展。这类App在绕过CertificatePinner后仍会因No SCT in certificate报错。此时Burp的CA证书必须包含有效的SCT。解决方案是不要用Burp自动生成的CA证书而是用mkcert工具生成带SCT的本地证书# 安装mkcert brew install mkcert # 生成根证书自动注册到系统 mkcert -install # 为burp生成证书包含SCT mkcert -cert-file burp-cert.pem -key-file burp-key.pem burp.local然后在BurpOptions → SSL → Import中导入burp-cert.pem和burp-key.pem。实测某券商App开启SCT后绕过成功率从30%提升至100%。5.4 Frida脚本的“热重载”与内存泄漏在反复调试时很多人习惯CtrlC停止脚本再frida -U -f com.xxx.app -l script.js重启。这会导致Frida Server内存中残留旧脚本的Hook新脚本与旧Hook冲突表现为checkServerTrusted被调用两次或App直接OOM崩溃。正确做法是每次修改脚本后先用frida-ps -U | grep com.xxx.app确认进程已退出再用frida-kill -U com.xxx.app强制杀死最后启动。更稳妥的方式是使用frida-repl -U com.xxx.app进入交互式环境用%load script.js动态加载避免进程重启。这些坑没有一篇官方文档会告诉你。它们散落在GitHub Issues、Stack Overflow的某个高赞回答、或是某次凌晨三点的调试日志里。但一旦掌握你就能比90%的同行更快定位问题——因为你知道问题大概率不在代码逻辑而在这些看不见的底层约束。我在实际使用中发现最高效的组合方式不是“一次到位”而是“分层验证”先用Burp确认问题现象再用Frida逐层剥离防御每绕过一层就在Burp中验证效果。这样你永远知道自己处在攻防链条的哪个位置而不是在无数个“万能脚本”中盲目试错。这套方法论的价值不在于帮你破解某个App而在于赋予你面对任何新App时都能快速建立分析路径的能力。