1. 项目概述一次与HTTPS证书校验的“正面交锋”最近在做一个Android应用的逆向分析项目目标应用与服务端通信采用了严格的HTTPS证书校验。这意味着我无法像往常那样简单地通过配置系统代理比如Burp Suite或Fiddler来拦截和查看网络请求因为应用会校验服务端证书的有效性一旦发现证书不是它信任的连接就会立刻中断。这就像一扇加了双重锁的门第一把锁是HTTPS加密第二把锁就是证书校验。我的任务就是找到打开第二把锁的方法在不修改应用本身代码的前提下让流量能够顺利通过我的代理工具。这不仅仅是“抓个包”那么简单。在移动安全测试、协议分析、甚至是某些合法的自动化测试场景下绕过证书校验都是一个非常核心且基础的技术点。它考验的是你对Android网络层、安全机制以及系统运行原理的理解深度。整个过程就像一场精密的“外科手术”你需要在不“杀死”应用即不导致应用崩溃或功能异常的前提下精准地修改其安全策略。接下来我将详细记录这次从理论分析到实践成功的完整过程其中涉及到的工具、思路和踩过的坑希望能给遇到类似问题的朋友提供一个清晰的参考路径。2. 核心原理与方案选型为什么常规代理会失效在深入动手之前我们必须先搞清楚为什么一个配置了Burp Suite CA证书的Android设备仍然无法拦截某些应用的HTTPS流量核心原因就在于应用实现了“证书锁定”Certificate Pinning或自定义的信任管理策略。2.1 HTTPS与证书校验的基本流程当一个标准的Android应用使用HttpsURLConnection或OkHttp等库且未做特殊配置发起HTTPS请求时其校验流程大致如下TCP连接建立与应用指定的服务器建立TCP连接。TLS/SSL握手客户端与服务器开始TLS握手服务器会发送它的数字证书链。系统信任库校验Android系统更具体地说是底层的网络安全库如Conscrypt会检查服务器证书的颁发者CA是否存在于系统预置的信任CA证书库中。这个库包含了全球公认的根证书颁发机构如DigiCert, GlobalSign, Let‘s Encrypt等。主机名验证系统会检查证书中的Common Name (CN)或Subject Alternative Name (SAN)字段是否与请求的目标主机名匹配。连接建立如果以上校验全部通过TLS连接建立成功后续的应用层数据HTTP报文才会在加密通道中传输。当你把Burp Suite或Fiddler的CA证书安装到设备的“用户凭据”中时这个证书会被加入到系统信任库。对于遵循标准流程的应用代理工具此时扮演中间人用自己的证书由你安装的Burp CA签发与客户端通信时客户端会校验通过因为签发者Burp CA已在信任列表中。2.2 证书校验被强化的几种方式目标应用之所以能防御这种中间人攻击是因为它没有完全依赖系统的默认校验机制。常见的手段有自定义TrustManager应用自己实现X509TrustManager接口完全接管证书校验逻辑。它可能只信任自己硬编码在应用内的特定证书或证书公钥而忽略系统的信任库。这是最彻底、也最难绕过的方式之一。证书固定Pinning这是更现代和推荐的做法。应用在代码中预先存储“固定”了它期望的服务端证书的公钥哈希值或整个证书。在建立连接时它会计算收到的服务器证书的公钥哈希并与预存的值比对不一致则拒绝连接。OkHttp库原生支持此功能。网络安全配置Network Security Configuration从Android 7.0 (API 24) 开始应用可以通过一个XML配置文件来定制网络安全策略例如指定只信任某些特定的自定义CA证书而不信任用户安装的CA证书。这直接导致用户安装的Burp证书失效。SSL/TLS库替换应用可能使用非标准的网络库如Cronet、或者自己编译的OpenSSL这些库有独立的证书信任链管理方式。2.3 本次绕过的核心思路面对这些防御我们的绕过思路需要分层次、有针对性地进行。粗暴地修改APK反编译后的AndroidManifest.xml或Smali代码是一种方法但过程繁琐且容易出错。我选择的是一种更“运行时”的动态方案核心思路是在应用进程启动后通过注入代码Hook的方式修改其证书校验相关的关键函数的行为使其总是返回“信任”或“验证成功”的结果。这个方案的优势在于无需修改原始APK保持了应用的完整性避免了签名校验可能带来的问题。动态生效可以随时开启或关闭。针对性强可以精准定位到负责校验的类和方法。要实现这个思路我们需要一个强大的动态插桩框架。在Android平台上Frida无疑是当前最流行、最强大的选择。它允许你将自己的JavaScript代码注入到目标应用的进程中拦截和修改函数调用。我们的作战计划就此明确使用Frida Hook掉证书校验的关键函数。3. 环境准备与工具链搭建工欲善其事必先利其器。一次成功的Hook操作离不开稳定可靠的环境。3.1 所需工具清单一部已Root的Android设备或模拟器这是硬性要求因为Frida的注入通常需要高权限。我使用的是Android Studio自带的x86_64系统镜像的模拟器API 30并已获取Root权限。使用真机也可以但务必确保已解锁Bootloader并刷入Magisk等Root方案。Frida包括服务端运行在设备上和客户端运行在电脑上。Frida Server需要下载与设备CPU架构对应的版本如frida-server-16.1.4-android-x86_64.xz推送到设备并运行。Frida Client (Python包)在电脑上通过pip install frida-tools安装。目标应用你需要获取到待分析应用的APK文件。可以通过adb pull从已安装的设备中提取或从其他渠道获取。反编译工具可选但强烈推荐用于静态分析目标应用定位关键类和方法。Jadx-GUI强大的Java反编译器图形化界面友好能直接将APK/DEX文件反编译成可读的Java代码。这是分析逻辑、寻找Hook点的首选。Apktool用于反编译APK资源文件查看AndroidManifest.xml和res目录对于分析网络安全配置特别有用。ADB (Android Debug Bridge)Android SDK的一部分用于与设备通信推送文件、执行命令。3.2 关键环境配置步骤步骤一在模拟器/真机上开启Root和调试对于模拟器启动时选择带Google APIs或Google Play的系统镜像这类镜像通常自带Root。启动后在设置中连续点击“版本号”开启开发者选项再开启“USB调试”。 对于真机过程更复杂需要解锁、刷入自定义Recovery、刷入Magisk。这里不展开网上有大量教程。步骤二安装并运行Frida Server首先从Frida的GitHub Release页面下载对应版本的Server。解压后得到可执行文件如frida-server-16.1.4-android-x86_64。# 将frida-server推送到设备 adb push frida-server-16.1.4-android-x86_64 /data/local/tmp/frida-server # 连接到设备的shell adb shell # 进入推送目录赋予执行权限以后台方式运行 cd /data/local/tmp chmod 755 frida-server ./frida-server 运行后ps | grep frida应该能看到进程。注意设备重启后需要重新运行。步骤三电脑端验证连接新开一个电脑终端运行frida-ps -U如果列出设备上正在运行的进程列表说明Frida环境搭建成功。-U参数表示连接到USB设备。步骤四静态分析目标应用使用Jadx-GUI打开目标APK。我们的首要任务是找到证书校验发生的地方。搜索关键词是突破口Java层搜索在Jadx中全局搜索X509TrustManager,checkServerTrusted,CertificatePinner,TrustManager,SSLContext,SSLSocketFactory,HostnameVerifier。网络安全配置用Apktool反编译APK查看res/xml目录下是否有network_security_config.xml文件。同时查看AndroidManifest.xml的application标签是否包含android:networkSecurityConfig属性。第三方库注意应用是否使用了OkHttp。如果使用了搜索CertificatePinner或OkHttpClient.Builder()调用处。实操心得很多时候应用不会自己从头实现TrustManager而是使用OkHttp等库的证书固定功能。因此优先搜索CertificatePinner和OkHttpClient的构建代码往往能更快定位。如果找到了类似certificatePinner(new CertificatePinner.Builder().add(example.com, sha256/AAAAAAAA...).build())的代码那就是确凿的证书固定证据。4. 定位与Hook精准打击校验逻辑经过静态分析我发现了目标应用使用了OkHttp库并且在初始化OkHttpClient时配置了CertificatePinner。这就是我们要攻克的核心堡垒。4.1 分析关键代码在Jadx中我找到了类似下面的代码片段经过脱敏和简化import okhttp3.CertificatePinner; import okhttp3.OkHttpClient; public class NetworkManager { private OkHttpClient createHttpClient() { CertificatePinner pinner new CertificatePinner.Builder() .add(api.targetapp.com, sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg) .add(api.targetapp.com, sha256/Vjs8r4z80wjNcr1YKepWQboSIRi63WsWXhIMNeWys) .build(); return new OkHttpClient.Builder() .certificatePinner(pinner) .connectTimeout(30, TimeUnit.SECONDS) .build(); } }这段代码意味着应用只信任api.targetapp.com域名的两个特定证书公钥哈希SHA-256 Pin。任何其他证书包括Burp签发的、哈希值不匹配的证书都会被拒绝。4.2 设计Frida Hook脚本我们的目标是让CertificatePinner的校验函数失效。查看OkHttp的源码或通过反编译的代码分析核心的校验逻辑发生在CertificatePinner.check()方法中。一个直接的想法是Hook这个方法让它什么都不做直接返回。我编写了如下Frida JavaScript脚本bypass_pin.jsJava.perform(function () { console.log([*] Starting OkHttp Certificate Pinner Bypass...); // 定位CertificatePinner类 var CertificatePinner Java.use(okhttp3.CertificatePinner); // Hook check方法使其不执行任何操作 CertificatePinner.check.overload(java.lang.String, java.util.List).implementation function(hostname, pinsToCheck) { console.log([] Bypassing CertificatePinner.check for host: hostname); // 原方法会抛出异常我们直接不调用原方法就等于跳过了校验 // 也可以选择调用原方法但捕获异常这里选择直接跳过 return; // check方法返回void }; // 有时候应用会使用带回调的check方法我们也一并Hook CertificatePinner.check.overload(java.lang.String, kotlin.jvm.functions.Function0).implementation function(hostname, pinLookup) { console.log([] Bypassing CertificatePinner.check (with callback) for host: hostname); return; // 同样直接返回 }; console.log([*] CertificatePinner Hook installed successfully.); });脚本逻辑解释Java.perform确保我们的代码在Java虚拟机上下文中执行。使用Java.use获取到okhttp3.CertificatePinner类的引用。找到check方法。通过查看反编译代码我发现有两个重载方法一个接受List参数另一个接受一个Kotlin函数参数可能是为了兼容不同版本的OkHttp或异步调用。我们使用overload来指定要Hook的具体方法签名。在implementation中我们替换了原方法的实现。这里简单地打印一条日志然后直接返回因为原check方法返回void。这意味着当应用调用证书检查时我们的空函数被执行真正的检查逻辑被跳过了。4.3 执行Hook脚本首先确保目标应用已经关闭。然后通过Frida启动应用并注入脚本frida -U -f com.target.app.package -l bypass_pin.js --no-pause-U: 使用USB设备。-f com.target.app.package: 以spawn方式启动应用-f指定包名。--no-pause表示立即启动不暂停。-l bypass_pin.js: 加载我们编写的脚本。如果应用已经在运行我们可以附加attach到进程frida -U -n “Target App Name” -l bypass_pin.js执行命令后Frida会输出启动日志我们的脚本日志[*] Starting...和[*] ...successfully.应该会出现。此时尝试在应用内触发网络请求。4.4 验证Hook效果打开Burp Suite确保代理设置正确并且设备的Wi-Fi或全局代理已指向Burp。在应用内进行操作观察Burp的Proxy - HTTP history标签页。如果之前请求失败现在成功出现了HTTPS请求的明文内容并且Proxy - Intercept可以拦截和修改那么恭喜你证书固定已被成功绕过注意事项这种直接让check方法空返回的Hook方式对于OkHttp 3.x和4.x的大部分版本是有效的。但是有些高度定制或混淆过的应用可能会把证书校验逻辑藏在更深的地方或者使用其他方法如自定义TrustManager。如果上述方法无效我们需要调整策略。5. 进阶与备用方案应对更复杂的情况如果HookCertificatePinner.check无效说明应用可能采用了其他校验机制。我们需要像侦探一样层层深入。5.1 Hook 自定义TrustManager如果应用自己实现了X509TrustManager我们需要找到这个类。在Jadx中搜索implements X509TrustManager或extends了某个TrustManager的类。找到后Hook其checkServerTrusted方法这个方法负责决定是否信任服务器证书链。Java.perform(function () { console.log([*] Looking for custom X509TrustManager...); // 假设我们通过分析找到了自定义TrustManager的类路径 var CustomTrustManager Java.use(com.target.app.security.MyCustomTrustManager); CustomTrustManager.checkServerTrusted.overload([Ljava.security.cert.X509Certificate;, java.lang.String).implementation function(chain, authType) { console.log([] Bypassing custom TrustManager.checkServerTrusted.); // 原方法可能抛出CertificateException我们直接不抛异常就等于信任所有证书 // 可以选择打印一下证书信息用于调试 for (var i 0; i chain.length; i) { console.log( Cert[ i ] Subject: chain[i].getSubjectDN().getName()); } return; // 方法返回void不抛异常即表示信任 }; });5.2 Hook SSLContext.init 或 SSLSocketFactory有时应用会通过SSLContext.getInstance(“TLS”).init(null, trustAllCerts, null)来初始化一个信任所有证书的上下文但trustAllCerts这个TrustManager[]数组可能被后续代码替换或包装。我们可以尝试HookSSLContext.init方法看看传入的TrustManager是什么。 更直接的方法是HookOkHttpClient.Builder的sslSocketFactory方法如果应用调用了它来设置自定义的SSLSocketFactory我们可以替换掉它。Java.perform(function () { var OkHttpClient_Builder Java.use(okhttp3.OkHttpClient$Builder); OkHttpClient_Builder.sslSocketFactory.overload(javax.net.ssl.SSLSocketFactory).implementation function(socketFactory) { console.log([] OkHttpClient.Builder.sslSocketFactory() called. Attempting to bypass...); // 这里可以尝试返回一个我们自定义的、信任所有证书的SSLSocketFactory // 但更简单的方法是不让它设置任何自定义的Factory或者设置一个我们Hook过的Factory // 由于需要构造复杂的Java对象这里展示思路实际脚本更复杂 // 通常如果走到这里说明上一步的CertificatePinner Hook可能没覆盖到需要结合使用。 return this.sslSocketFactory(socketFactory); // 暂时先原样调用避免崩溃 }; });5.3 使用通用型“核武器”脚本当不确定具体Hook点时可以尝试一些社区流传的、覆盖面更广的通用脚本。这些脚本会尝试Hook多个常见的安全校验类和方法。例如一个经典的“信任所有证书”的Frida脚本会HookTrustManagerFactory,SSLContext以及HostnameVerifier。但使用这类脚本要格外小心可能会造成应用不稳定。这里提供一个经过简化的示例Java.perform(function () { // 1. 让所有HostnameVerifier都返回true var HostnameVerifier Java.use(“javax.net.ssl.HostnameVerifier”); HostnameVerifier.verify.overload(‘java.lang.String’, ‘javax.net.ssl.SSLSession’).implementation function(hostname, session) { console.log(“[*] Bypassing HostnameVerifier for: “ hostname); return true; }; // 2. 创建一个信任所有证书的TrustManager var TrustAllCerts Java.registerClass({ name: ‘com.bypass.TrustAllCerts’, implements: [Java.use(‘javax.net.ssl.X509TrustManager’)], methods: { checkClientTrusted: function(chain, authType) {}, checkServerTrusted: function(chain, authType) {}, getAcceptedIssuers: function() { return []; } } }); // 3. 尝试用这个TrustAllCerts替换掉SSLContext初始化的TrustManager // 这里逻辑比较复杂通常需要枚举和替换已存在的SSLContext实例风险较高。 console.log(“[*] Generic bypass hooks installed. May not work for all cases.”); });5.4 处理网络安全性配置NSC如果应用在AndroidManifest.xml中配置了android:networkSecurityConfig并且其中设置了trust-anchors只信任系统证书certificates src“system”/而不信任用户证书certificates src“user”/那么即使Hook了代码系统库层面也不会信任用户安装的Burp证书。解决方案修改network_security_config.xml文件将src“system”改为src“system|user”或者直接删除整个trust-anchors配置。但这需要反编译APK、修改XML、重新打包并签名。对于签名校验严格的应用此路不通。动态Hook方案的优势在此凸显——我们无需修改安装包。6. 问题排查与实战心得在实际操作中几乎不可能一帆风顺。下面是我遇到的一些典型问题及解决方法。6.1 常见问题速查表问题现象可能原因排查步骤与解决方案frida-ps -U无输出或报错1. Frida Server未运行或已退出。2. 设备未Root或ADB权限不足。3. 客户端与Server版本不匹配。1.adb shell进入设备ps | grep frida确认进程存在用./frida-server 重新运行。2.adb root尝试获取Root权限真机需确认Magisk等环境正常。3.pip list | grep frida和Server版本号是否一致。注入脚本后应用闪退1. Hook的类或方法不存在或签名错误。2. 脚本逻辑有误导致Java异常。3. 应用有反调试或反Frida检测。1. 使用Java.available和Java.enumerateLoadedClasses()确认类已加载。仔细核对类名和方法签名注意混淆后的名称。2. 在Frida命令后加—runtimev8使用V8引擎可能更稳定。使用try-catch包裹Hook代码。3. 尝试在非主线程注入、延迟Hook、或使用Frida的隐身模式frida -U -f … —enable-early-instrumentation。Burp能收到请求但响应被应用拒绝或显示网络错误1. 证书固定Pinning未完全绕过可能还有第二处校验。2. 应用校验了证书链的完整性或有效期。3. 应用使用了证书透明度CT等扩展校验。1. 在Jadx中再次搜索pin,cert,ssl等关键词检查是否有多个网络客户端实例或深层调用。2. Hook更底层的X509TrustManager方法或尝试HookMessageDigest等用于计算Pin值的类。3. 这种情况较少见可能需要更全面的Hook或静态修改。Frida脚本日志显示Hook成功但Burp仍无流量1. 应用可能使用了WebView或非OkHttp/HttpURLConnection的库如Cronet, Volley自定义栈。2. 流量走了其他协议或端口如WebSocket, gRPC。3. 代理设置未生效特别是Android高版本对明文流量限制。1. 使用tcpdump或Wireshark在设备上抓包确认流量是否真的经过代理IP和端口。2. 检查应用是否在代码中设置了代理Proxy.NO_PROXY或使用了ProxySelector。3. 对于WebView需要单独设置WebView.setWebContentsDebuggingEnabled(true)并在Chrome中调试。高版本AndroidAPI 24用户证书不生效应用配置了网络安全配置NSC默认不信任用户安装的证书。1. 动态Hook方案通常不受NSC影响因为我们在Java层绕过了校验。确保Hook点正确。2. 如果必须安装Burp证书可尝试将其安装到系统证书目录需Root但这通常很麻烦。Hook是更优解。6.2 独家避坑技巧与心得静态分析先行动态验证辅助不要一上来就写Frida脚本。花足够的时间用Jadx仔细阅读代码理清网络请求的初始化流程、使用的库、以及安全校验的具体实现位置。画个简单的调用关系图会非常有帮助。由浅入深逐步Hook先从最明显的入口点如CertificatePinner.check开始尝试。如果无效再逐步向底层深入TrustManager,SSLContext。同时使用console.log大量打印信息了解代码的执行路径。利用Frida的枚举功能当不确定类名时可以用Java.enumerateLoadedClasses()打印出所有已加载的类然后过滤包含cert,ssl,trust,pin等关键词的类。也可以用Java.choose()在堆上查找已有对象实例。处理混淆面对混淆过的代码类名和方法名可能变成a.a,b.c这种。关键在于寻找不变的东西比如方法参数和返回值类型。搜索字符串常量如域名api.targetapp.com是定位关键代码的黄金法则。找到引用该字符串的代码顺藤摸瓜就能找到校验逻辑。保持环境干净每次测试前尽量杀掉目标应用进程和Frida Server然后重新启动避免残留状态影响。使用frida -U -f自动生成spawn模式通常比附加attach模式更稳定。备份与回滚修改Frida脚本时做好版本备份。如果某次注入导致应用持续崩溃可能需要清除应用数据或重新安装。这次绕过Android服务端证书校验的实践本质上是一场对应用安全机制的深度探索。它不仅仅是为了“抓包”更是理解现代移动应用如何构建防御体系的过程。从最初的束手无策到静态分析找到关键点再到编写Frida脚本动态突破每一步都需要耐心和细致的观察。最终当Burp Suite的界面中终于出现那些被加密的请求明文时那种成就感是对技术钻研者的最佳奖赏。记住这种技术应当仅用于安全研究、授权测试和个人学习切勿用于任何非法用途。