1. 项目概述为什么我们还在和SSL Pinning“斗智斗勇”搞Android安全测试或者逆向分析的朋友对“SSL Pinning”这个词肯定不陌生甚至有点“又爱又恨”。爱的是它作为一项重要的安全加固措施能有效防止中间人攻击保护用户数据不被窃听或篡改这是现代App安全架构的基石。恨的是当我们作为安全研究员、逆向工程师或者只是想深入了解一下某个App的网络通信逻辑时它就成了横在面前的一堵高墙。你抓包工具像Charles、Fiddler配置得再好系统证书装得再全一打开目标App网络请求要么直接失败要么返回一堆乱码日志里赫然写着“Certificate verification failed”。这就是SSL Pinning在起作用App不再完全信任操作系统或用户提供的根证书链而是将服务端证书的特定信息如公钥哈希硬编码或动态验证在代码里只有匹配的证书才能建立连接。所以“绕过SSL Pinning”就成了移动安全领域一个经典且持久的课题。市面上方法很多从早期简单的Xposed模块到后来基于各种注入框架的Hook方案。而Frida凭借其动态注入、脚本化、跨平台的强大特性成为了当前最主流、最灵活的解决方案之一。今天这篇实战我们就聚焦于使用Frida进行SSL Pinning绕过但不止步于“能用”我们要深入“为什么能用”以及“怎么用得稳”。网上很多教程只给脚本不讲原理遇到环境差异或App更新就抓瞎。我将结合多次实战踩坑的经验带你从原理到实践从通用方法到定制化对抗彻底掌握这套“破壁”之术。无论你是安全测试人员、逆向爱好者还是想了解App安全机制的开发者这篇长文都能给你带来实实在在的干货。2. SSL Pinning的核心原理与Frida的对抗哲学在动手之前我们必须先搞清楚对手的“武功路数”。SSL Pinning的实现并非铁板一块它在Android上有多种常见的“招式”我们的Hook策略也需要对症下药。2.1 SSL Pinning的常见实现方式Android应用实现证书绑定的地方主要在网络库和HTTP客户端层。以下是几种主流方式OkHttp的CertificatePinner这是目前最主流、最规范的方式。OkHttp库提供了CertificatePinner类开发者可以很方便地配置一个或多个主机名与预期证书公钥的SHA-256哈希的绑定关系。这是“官方推荐”的姿势。Apache HttpClient的自定义TrustManager一些老项目或底层库可能使用Apache HttpClient。开发者会实现一个自定义的X509TrustManager在checkServerTrusted方法中加入对特定证书的校验逻辑。自定义SSLSocketFactory或TrustManager更为底层和定制的做法。App可能完全自己实现SSLSocketFactory或者在初始化时替换掉默认的TrustManager从而完全掌控证书验证流程。Native层的验证C/C为了增加逆向难度核心校验逻辑可能放在Native层so库中。通过JNI调用在C/C代码里完成证书的解析与比对这给动态Hook带来了新的挑战。第三方SDK或网络库的集成像腾讯的MMTLS、阿里云的HTTPDNSSSL等它们可能内置了自己的证书校验逻辑形态各异。2.2 Frida的对抗策略总览Frida的核心能力在于动态插桩Instrumentation。我们不需要修改App的安装包APK而是在App运行时将我们的JavaScript代码注入到目标进程的内存空间中去修改关键函数的行为。对于SSL Pinning我们的核心攻击面就是那些负责证书验证的类和方法。策略可以归结为两类“使其信任一切”策略这是最粗暴也最通用的方法。我们Hook住证书验证链条中最关键的那些方法如TrustManager.checkServerTrusted,X509TrustManager.checkServerTrusted让它们不做任何校验直接通过或者返回一个空的异常列表。相当于给验证机制“开了后门”。“偷梁换柱”策略这种方法更为精细。我们依然让验证流程执行但在执行过程中将待验证的证书即我们抓包工具提供的证书的关键信息如公钥哈希替换成App预期中的那个值。或者直接Hook哈希计算函数让它始终返回App预期的哈希值。这种方法对抗一些会检测验证流程是否被短路的应用可能更有效。在本教程中我们将重点讲解第一种策略因为它适用性最广脚本最成熟。同时我们也会探讨如何应对第二种策略中的一些复杂情况以及当遇到Native层验证时该如何拓展思路。3. 实战环境搭建与前置检查工欲善其事必先利其器。一个稳定的Frida环境是成功的一半。很多新手卡在第一步问题都出在环境上。3.1 Frida环境部署详解你需要准备两部分客户端你的电脑和服务器端Android设备/模拟器。客户端PC/Mac# 使用Python的pip安装强烈建议在虚拟环境中进行 pip install frida-tools # 安装完成后验证版本 frida --version注意frida-tools版本最好与服务器端的frida-server版本保持一致或接近大版本号如16.x一致为佳可以避免很多RPC协议不兼容的问题。服务器端Android确定架构在设备上执行adb shell getprop ro.product.cpu.abi常见输出有arm64-v8a,armeabi-v7a,x86_64等。下载对应版本的frida-server前往Frida的GitHub Releases页面下载与你设备架构和客户端frida版本匹配的frida-server-*.xz文件。例如对于Android arm64和设备端应下载frida-server-16.1.4-android-arm64.xz。推送并启动# 解压下载的.xz文件得到frida-server文件 xz -d frida-server-*.xz # 推送至设备 adb push frida-server /data/local/tmp/ # 进入adb shell赋予执行权限以后台方式运行 adb shell cd /data/local/tmp chmod 755 frida-server ./frida-server 验证连接在电脑终端执行frida-ps -U如果能看到设备上运行的进程列表恭喜你环境通了。3.2 目标App与抓包工具准备目标App选择一个你熟悉的、已知启用了SSL Pinning的应用进行测试。切勿在未经授权的生产环境应用上进行测试。抓包工具推荐使用Charles Proxy或mitmproxy。确保你的电脑和手机处于同一局域网并在手机上配置好代理同时将抓包工具的根证书安装到手机的系统证书目录Android 7.0以上需要将证书安装到系统分区或对目标App进行额外配置以信任用户证书。关键前置检查 在尝试Hook之前先不做任何处理直接用配置好代理的手机打开目标App观察抓包工具。如果所有网络请求都失败或证书错误基本确认SSL Pinning存在。这是我们的基准状态。4. 核心Hook脚本解析与逐行精讲网上流传着很多“万能”的Frida SSL Pinning绕过脚本。直接拿来用可能偶尔成功但更可能失败。理解每一行代码的作用才能灵活调整应对变化。下面我们拆解一个功能强大、层次分明的脚本。4.1 脚本骨架与通用Hook点我们先看一个综合性的脚本框架它尝试了多个常见的Hook点Java.perform(function () { console.log([*] Starting SSL Pinning Bypass...); // 1. 信任所有证书的万能TrustManager var TrustManager Java.use(javax.net.ssl.X509TrustManager); var TrustManagerImpl Java.registerClass({ name: com.bypass.TrustManagerImpl, implements: [TrustManager], methods: { checkClientTrusted: function (chain, authType) {}, checkServerTrusted: function (chain, authType) {}, getAcceptedIssuers: function () { return []; } } }); 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!); // 替换传入的TrustManager为我们自己实现的、什么都不校验的那个 var myTrustManager TrustManagerImpl.$new(); this.init(keyManagers, [myTrustManager], secureRandom); }; // 2. 针对OkHttp的CertificatePinner try { var CertificatePinner Java.use(okhttp3.CertificatePinner); CertificatePinner.check.overload(java.lang.String, java.util.List).implementation function (hostname, pins) { console.log([] Bypassing OkHttp CertificatePinner for: hostname); // 直接不执行任何检查让验证通过 return; }; // 另一个常见方法findMatchingPins CertificatePinner.findMatchingPins.overload(java.lang.String).implementation function (hostname) { console.log([] Bypassing OkHttp CertificatePinner.findMatchingPins for: hostname); return Java.use(java.util.Collections).emptyList(); }; } catch (e) { console.log([-] OkHttp CertificatePinner not found: e.message); } // 3. 针对Apache HttpClient的TrustManager try { var X509TrustManager Java.use(org.apache.http.conn.ssl.X509TrustManager); X509TrustManager.checkServerTrusted.implementation function (chain, authType) { console.log([] Bypassing Apache HttpClient X509TrustManager); // 空实现等同于信任所有 }; } catch (e) { console.log([-] Apache HttpClient X509TrustManager not found: e.message); } // 4. Hook常见的证书验证方法更底层 var X509Certificate Java.use(java.security.cert.X509Certificate); // 可以Hook verify等方法但通常不需要因为上面已经釜底抽薪 console.log([*] SSL Pinning Bypass hooks installed.); });4.2 关键代码段深度剖析1. 自定义TrustManager (Java.registerClass) 这是脚本中最核心、最彻底的一环。它没有去修改App原有的TrustManager而是重新创建了一个新的类这个类实现了X509TrustManager接口但它的checkServerTrusted和checkClientTrusted方法都是空的。当Hook到SSLContext.init方法时这是SSL上下文初始化的必经之路我们用这个“傀儡”TrustManager替换掉App原本想要使用的那个。这样一来后续所有的证书验证都会走到我们这个空方法里自然全部通过。实操心得这种方法非常强大但有些加固或检测手段会校验TrustManager的类名或实例。如果遇到Hook后仍失败的情况可以尝试不替换而是直接修改原TrustManager实例的方法实现使用.implementation但前提是你能找到那个具体的实例。2. OkHttpCertificatePinner的Hook OkHttp的绑定逻辑主要在check方法里。我们直接让它的实现为空验证就跳过了。findMatchingPins方法返回一个空列表也能达到类似效果。这里用了try...catch包裹因为不是所有App都用OkHttp找不到类很正常避免脚本因异常而终止。注意事项有些App可能会在代码里缓存CertificatePinner的检查结果或者使用CertificatePinner.Builder的build方法返回一个不可修改的实例。如果Hook了check方法仍无效可以尝试HookCertificatePinner.Builder的build方法返回一个我们自定义的、被Hook过的CertificatePinner实例。3. 方法重载Overload的处理 注意CertificatePinner.check.overload(java.lang.String, java.util.List)。在Java中方法可能有多个重载版本参数不同。Frida需要明确知道你要Hook哪一个。使用.overload并指定参数类型的签名来精确匹配。如何知道签名可以通过Frida的-j参数先枚举类的方法或者查看反编译的代码。4. 执行脚本 将上述代码保存为bypass_ssl.js然后使用Frida命令注入到目标进程# 附加到已运行的应用 frida -U -l bypass_ssl.js -f com.example.targetapp --no-pause # 或者以spawn方式启动应用 frida -U -l bypass_ssl.js -f com.example.targetapp注入成功后再操作App观察抓包工具应该能看到之前失败的HTTPS请求现在可以正常捕获和解密了。5. 进阶对抗与疑难问题排查如果你的通用脚本不起作用那么真正的挑战才刚刚开始。下面是一些进阶场景和排查思路。5.1 应对App的Anti-Frida检测越来越多的安全敏感型App会检测Frida的存在。常见检测点检测frida-server端口默认端口27042。检测进程名/内存映射查找包含“frida”字样的线程或内存模块。检测异常指令Frida的插桩会修改代码可能被检测。对抗措施修改Frida Server端口启动时指定非默认端口./frida-server -l 0.0.0.0:8080连接时使用frida -H 192.168.x.x:8080 ...。使用隐蔽模式Frida的Gadget模式可以将Frida库打包进App而不是外部注入但需要修改APK。动态Hook对抗代码写Frida脚本去Hook App内部的检测函数让它们总是返回“未检测到”的结果。这需要你先逆向分析出检测逻辑在哪里。使用其他工具辅助对于纯检测端口的可以尝试先用iptables重定向端口。5.2 处理Native层C/C的SSL Pinning当证书校验发生在so库里时就需要用到Frida的Interceptor来Hook Native函数。思路定位关键函数使用objdump、IDA Pro或Ghidra静态分析so库寻找与SSL相关的函数如SSL_CTX_set_cert_verify_callback,X509_verify_cert等来自OpenSSL库或自定义的JNI_OnLoad、Java_*函数。使用Frida的Native HookInterceptor.attach(Module.findExportByName(libtarget.so, SSL_verify_cert), { onEnter: function(args) { console.log([Native] SSL_verify_cert called); }, onLeave: function(retval) { // 强制让验证成功例如让函数返回1成功 retval.replace(1); console.log([Native] SSL_verify_cert bypassed, retval - 1); } });寻找Java到Native的桥梁App通常通过JNI调用Native函数。可以先在Java层找到加载so库System.loadLibrary和声明native方法的地方然后跟踪到具体的Native实现。5.3 脚本注入成功但抓包仍失败的排查清单按照以下步骤系统性排查步骤检查项可能原因与解决方案1. 基础连接frida-ps -U能否列出进程Frida-server未运行、版本不匹配、USB调试未开启、设备未授权。2. 脚本注入控制台是否有[*] Starting SSL Pinning Bypass...和[*] ... hooks installed.输出脚本语法错误、目标类不存在try-catch已处理、注入时机过早/过晚。尝试用-fspawn方式或确保App在主页后再附加。3. Hook生效当触发网络请求时控制台是否有[] Bypassing ...等日志输出Hook点不对。App可能使用了你没Hook的网络库如Cronet、腾讯MMTLS。需反编译分析其网络栈。4. 证书信任Hook已触发但Charles仍显示SSL handshake failed。我们的TrustManager可能没被调用到或者存在多层SSL上下文。尝试Hook更底层的TrustManagerFactory或所有X509TrustManager的实现类。5. 代理设置非SSL请求能抓HTTPS不行。手机代理设置正确吗Charles/mitmproxy的根证书安装并信任了吗Android 7 需注意系统级证书6. App自身行为抓包成功一次后后续请求又失败。App可能实现了证书锁定公钥轮换或每次连接都重新初始化SSL上下文并校验。需要Hook更初始化的地方或脚本实现更持久化的绕过。7. 加固混淆类名和方法名是混淆的如a.a, b.c。通用脚本失效。需要静态分析找到混淆后的对应类和方法名更新到脚本中。关注SSLContext、X509TrustManager、CertificatePinner等关键词的混淆映射。5.4 使用Objection进行快速测试如果你想要一个更快捷、无需写脚本的初步测试可以使用基于Frida的自动化工具Objection。# 安装 pip install objection # 连接设备探索目标App objection -g com.example.targetapp explore # 在Objection REPL中运行SSL Pinning绕过命令 android sslpinning disableObjection会自动尝试多种常见的绕过方法。它的优点是快缺点是黑盒、不够灵活对于定制化强的Pinning可能无效。但它是一个很好的初步诊断工具。如果Objection能成功说明是通用Pinning你可以再用Frida脚本细化如果Objection失败那你很可能需要走上文提到的深度分析之路。6. 实战案例逆向分析定制化Pinning并编写针对性脚本假设我们遇到一个App通用脚本无效Objection也失败了。我们需要进行手动逆向分析。步骤一静态分析找线索使用jadx-gui或APKTooldex2jar反编译APK。在代码中搜索关键词CertificatePinner,X509TrustManager,SSLContext,checkServerTrusted,TrustManager。重点关注网络请求初始化相关的类或者应用自定义的Application或NetworkSecurityConfig。步骤二动态追踪定目标如果静态分析找不到明显线索可能被混淆使用Frida进行动态追踪。写一个脚本跟踪所有X509TrustManager实现类的checkServerTrusted方法调用Java.perform(function() { // 枚举所有已加载的类 Java.enumerateLoadedClasses({ onMatch: function(className) { if (className.includes(TrustManager) || className.includes(X509)) { console.log([*] Found class: className); // 尝试Hook这个类的checkServerTrusted try { var clazz Java.use(className); var methods clazz.class.getDeclaredMethods(); for (var i in methods) { if (methods[i].getName().indexOf(checkServerTrusted) ! -1) { console.log([!] Potential target: className . methods[i].getName()); // 这里可以进一步尝试Hook但注意重载 } } } catch (e) {} } }, onComplete: function() {} }); });运行App并触发网络请求观察控制台输出找到被调用的那个具体的、混淆后的类和方法。步骤三编写针对性Hook脚本假设通过动态追踪我们发现实际负责校验的类是一个混淆后的com.sec.a.b.c其中有一个方法a(String, List)被调用。Java.perform(function () { var TargetClass Java.use(com.sec.a.b.c); TargetClass.a.overload(java.lang.String, java.util.List).implementation function(host, certs) { console.log([] Bypassing custom pinning in com.sec.a.b.c.a() for host: host); // 什么也不做直接返回 return; }; console.log([*] Custom pinning hook installed.); });这个脚本就是针对这个特定App的“特效药”。7. 总结与安全研究伦理通过以上从原理到实践从通用到定制的讲解你应该对使用Frida绕过Android SSL Pinning有了一个全面且深入的理解。关键在于理解原理、灵活运用、耐心排查。没有一成不变的脚本只有对机制的理解和解决问题的思路。最后必须强调安全研究的伦理与法律边界。本文所有技术仅限用于对自己拥有合法权限的移动应用进行安全评估。学习、研究移动安全技术。在CTF比赛等合法授权场景中使用。严禁将这些技术用于任何未经授权的网络攻击、数据窃取、隐私侵犯或商业破坏活动。在测试任何应用前请务必确保你已获得明确授权或在完全隔离的、属于自己的测试环境中进行。技术的力量来源于人也取决于人如何使用它。希望你能利用这些知识为构建更安全的移动生态贡献力量而非破坏它。在探索技术深度的同时永远不要忘记法律的准绳和道德的底线。