三层Hook覆盖系统/OkHttp/OpenSSL全校验链路,附通用脚本、踩坑清单与请求提取方案
做APP接口分析和数据采集的同学,大概率都遇到过这个死局:手机装了抓包证书、代理也配好了,Charles里要么全是unknown,要么直接报SSL handshake failed,APP干脆直接断网。你以为是证书没装对,折腾半天系统证书、用户证书全装了一遍,结果还是不行——这就是**SSL Pinning(证书钉扎)**在起作用。
网上能搜到的绕过方案大多停留在三四年前,要么只能绕过系统默认校验,遇到OkHttp自定义Pinning直接失效;要么就是让你重打包、改smali,一遇到加固、签名校验就闪退。更不用说现在很多APP还叠了双向认证、TLS指纹校验,普通的Hook脚本根本碰不到校验点。
本文从TLS校验的全链路出发,用Frida实现从系统层、框架层到底层SSL库的三层Hook绕过,同时完成HTTPS请求体、加密参数的完整提取。所有方案均经过数十款主流APP实测,覆盖安卓7~14,附可直接复用的脚本和完整踩坑清单。
一、先搞懂:SSL Pinning到底卡在哪一步
HTTPS抓包的本质是中间人攻击:代理用自己的证书冒充服务器,客户端信任代理证书后,流量就能被解密。而SSL Pinning的作用,就是从根源上废掉这种中间人。
正常的TLS握手流程中,客户端只需要验证服务器证书由受信任的CA签发即可;而开启Pinning后,APP会在代码里内置服务器证书的公钥哈希、证书指纹或完整证书链,只信任指定的证书。哪怕你把抓包证书安装到系统根目录,APP自己的校验逻辑也会拒绝握手,直接抛出证书验证异常。
从实现层级上看,Pinning通常分布在三个层面,越往下越难绕过:
- 系统证书校验层:基于Android系统
X509TrustManager完成证书链校验,安卓7+默认不信任用户证书,这是最基础的拦截点,也是很多新手卡壳的地方。 - 应用框架层:最常见的是OkHttp的
CertificatePinner,Retrofit、FastAndroidNetworking等绝大多数网络框架都基于它实现,直接在代码里写死公钥哈希。 - Native原生层:C/C++实现的网络库直接调用OpenSSL/BoringSSL完成校验,Java层Hook完全无效,多见于加固应用、游戏、自研网络库。
更进阶的对抗还包括双向客户端证书认证、TLS指纹校验、证书链完整性校验、动态下发证书等,这也是单一脚本无法通杀所有APP的根本原因。
二、整体绕过架构与Hook点选型
Frida的核心优势是动态注入,无需重打包、不用修改APP,既能Hook Java层也能Hook Native层,同时可以在内存中直接拦截请求数据。
完整的绕过与提取链路如下图所示,我们按照“从上到下、逐层排查”的思路定位校验点,优先Hook Java层,无效再下探到Native层,最后配合请求拦截完成数据提取。
三层Hook的适用场景:
- 仅系统校验拦截:只需要Hook
X509TrustManager即可,适用于原生网络请求、WebView场景。 - OkHttp自定义Pinning:需要Hook
CertificatePinner,这是绝大多数APP的实现方式。 - Native层校验:需要Hook OpenSSL/BoringSSL的底层校验函数,适用于加固、自研网络库的场景。
三、前置环境准备
在开始Hook之前,必须先把基础环境搭好,否则再强的脚本也跑不起来。
1. 基础环境
- 运行环境:Root手机或安卓模拟器,推荐安卓10~13,兼容性最好;安卓14需要配合Magisk和最新版Frida。
- Frida环境:PC端安装
frida-tools,手机端安装对应架构、对应版本的frida-server,并运行在后台。 - 抓包工具:Charles或mitmproxy,提前导出根证书。
- 系统证书安装:安卓7+默认不信任用户证书,必须将抓包证书移动到
/system/etc/security/cacerts/目录,格式为哈希.0。 - 目标APP:确认包名、CPU架构(32/64位)、是否加固,加固应用建议使用Spawn模式启动注入。
2. 快速定位校验层级
不用上来就堆全套脚本,可以先快速排查:
- 装完系统证书后,APP仍无法联网、报SSL错误,说明存在Pinning。
- 用Frida枚举类名,搜索
CertificatePinner、X509TrustManager,能搜到说明是Java层校验。 - Java层Hook后仍握手失败,说明存在Native层校验,需要下探到so层。
四、第一层:系统证书校验绕过
这是最基础的一层,核心是Hook安卓系统的X509TrustManager,让它信任所有证书,同时绕过主机名校验。
实现原理
安卓所有HTTPS请求的证书校验,最终都会调用X509TrustManager的checkServerTrusted方法,校验失败就抛出CertificateException。我们直接Hook该方法,空实现不抛出异常,就等于让系统信任所有证书。
同时还要HookHostnameVerifier,强制返回true,避免主机名不匹配的问题。
核心脚本
Java.perform(function () { // 绕过X509TrustManager证书校验 var TrustManagerImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl"); TrustManagerImpl.checkServerTrusted.overload('[Ljava.security.cert.X509Certificate;', 'java.lang.String').implementation = function (chain, authType) { // 直接返回,不抛出异常即视为校验通过 return; }; // 绕过主机名校验 var HostnameVerifier = Java.use("javax.net.ssl.HostnameVerifier"); var OkHostnameVerifier = Java.use("okhttp3.internal.tls.OkHostnameVerifier"); OkHostnameVerifier.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function (hostname, session) { return true; }; // 兼容HttpURLConnection的主机名校验 var DefaultHostnameVerifier = Java.use("org.apache.http.conn.ssl.DefaultHostnameVerifier"); DefaultHostnameVerifier.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function (hostname, session) { return true; }; });注意:不同安卓版本的
TrustManagerImpl类名可能不同,部分ROM使用android.net.http.X509TrustManagerExtensions,如果Hook失败可以用Java.enumerateClassLoaders遍历查找。
这一层只能绕过系统默认的证书校验,无法绕过OkHttp自定义Pinning。如果Hook完APP还是无法联网,继续往下走。
五、第二层:OkHttp CertificatePinner 绕过
这是目前最常见的Pinning实现方式,绝大多数使用OkHttp/Retrofit的APP都采用这种方案。
实现原理
OkHttp通过CertificatePinner类实现证书钉扎,在check方法中对比服务器证书的公钥哈希和代码中内置的哈希,不匹配就抛出SSLPeerUnverifiedException。
最稳定的绕过方式有两种:
- 直接Hook
check方法,空实现,不抛出异常; - Hook
CertificatePinner.Builder的add方法,把内置的公钥哈希替换成抓包证书的哈希。
第一种最简单,兼容性也最好,优先使用。
核心脚本
Java.perform(function () { try { var CertificatePinner = Java.use("okhttp3.CertificatePinner"); CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function (hostname, peerCertificates) { // 直接返回,不做校验 return; }; // 兼容混淆后的类名,如果上面的类找不到,可以尝试下面的方式 // 遍历所有类,搜索包含CertificatePinner特征的类 Java.enumerateLoadedClasses({ onMatch: function (className) { if (className.indexOf("CertificatePinner") !== -1) { console.log("找到CertificatePinner类: " + className); } }, onComplete: function () {} }); } catch (e) { console.log("OkHttp CertificatePinner Hook失败: " + e.message); } });混淆与适配
如果APP做了代码混淆,okhttp3.CertificatePinner的类名会被改成a.b.c,这时候不要硬写类名,通过字符串特征或者方法签名去匹配:
- 搜索
Certificate pinning failure异常字符串,定位抛出异常的类; - 枚举所有实现了
check方法、参数包含List<Certificate>的类; - 用Frida的
trace命令跟踪SSL相关的Java调用,定位校验点。
到这一步,90%以上的普通APP都可以正常抓包了。如果还是不行,说明校验逻辑在Native层。
六、第三层:Native层OpenSSL/BoringSSL 绕过
很多加固应用、游戏、自研网络库不会走Java层的证书校验,而是直接在Native层调用OpenSSL/BoringSSL完成TLS握手和证书校验,这时候Java层的Hook完全无效。
实现原理
Native层的证书校验核心是两个函数:
SSL_get_verify_result:返回证书校验结果,返回0表示成功,非0表示失败;X509_verify_cert:执行证书链校验,返回1表示成功。
我们用Frida的Native Hook拦截这两个函数,强制返回成功,就能绕过Native层的Pinning。
核心脚本
// 等待so库加载完成后执行 setTimeout(function () { var libssl = null; var libs = ["libssl.so", "libboringssl.so", "libssl.so.1.1", "libssl.so.3"]; for (var i = 0; i < libs.length; i++) { try { libssl = Module.findBaseAddress(libs[i]); if (libssl) { console.log("找到SSL库: " + libs[i]); break; } } catch (e) {} } if (!libssl) { console.log("未找到SSL库,尝试全局搜索"); return; } // Hook SSL_get_verify_result,强制返回0(校验成功) var SSL_get_verify_result = Module.findExportByName(libs[i], "SSL_get_verify_result"); if (SSL_get_verify_result) { Interceptor.attach(SSL_get_verify_result, { onLeave: function (retval) { retval.replace(0); } }); console.log("SSL_get_verify_result Hook成功"); } // Hook X509_verify_cert,强制返回1(校验成功) var X509_verify_cert = Module.findExportByName(libs[i], "X509_verify_cert"); if (X509_verify_cert) { Interceptor.attach(X509_verify_cert, { onLeave: function (retval) { retval.replace(1); } }); console.log("X509_verify_cert Hook成功"); } }, 2000);注意事项
- 不同APP使用的SSL库不同,除了系统的
libssl.so,还可能是APP自带的libcurl.so、libmbedtls.so,需要先枚举模块确认。 - 部分APP会自定义证书校验函数,不是标准的OpenSSL接口,这时候需要通过字符串、导入表或者堆栈定位自定义校验函数。
- 加固APP要注意Hook时机,必须等壳加载完成、so库加载后再注入,建议使用Spawn模式启动。
七、HTTPS请求与加密参数完整提取
绕过Pinning只是第一步,很多时候我们需要提取请求URL、Header、Body、加密参数,甚至响应数据。这里提供两种方案,覆盖不同场景。
方案一:抓包工具直接提取
绕过Pinning后,Charles或mitmproxy可以直接解密HTTPS流量,查看完整的请求和响应,适合绝大多数场景。如果APP不走系统代理,可以用iptables转发流量,或者使用VPN模式的抓包工具。
方案二:Frida内存直接提取
针对有代理检测、走私有通道、或者流量加密的APP,直接在内存中Hook网络框架的请求方法,提取明文数据,完全不需要走代理。
以OkHttp为例,HookRealCall的execute和enqueue方法,打印请求信息:
Java.perform(function () { try { var RealCall = Java.use("okhttp3.RealCall"); RealCall.execute.overload().implementation = function () { var request = this.request(); var url = request.url().toString(); var method = request.method(); var body = request.body(); console.log("=== 请求 ==="); console.log("Method: " + method); console.log("URL: " + url); console.log("Headers: " + request.headers().toString()); // 打印请求体 if (body != null) { var buffer = Java.use("okio.Buffer").$new(); body.writeTo(buffer); console.log("Body: " + buffer.readUtf8()); } var response = this.execute(); console.log("响应码: " + response.code()); return response; }; } catch (e) { console.log("OkHttp请求Hook失败: " + e.message); } });进阶用法是直接Hook加密、签名函数,在参数加密前提取明文,在签名计算前提取原始参数,省去逆向算法的时间。
八、进阶对抗与必踩的坑
实际场景中,很多APP不止有Pinning,还叠加了各种对抗手段,这里整理了最常见的8个坑和解决方案。
- 双向客户端证书认证
APP要求客户端携带证书才能完成握手,这时候光绕过服务端校验没用。需要从APK或内存中提取客户端证书和私钥,HookKeyManager的chooseClientAlias方法,指定使用抓包工具的客户端证书,或者直接导入证书到系统。 - 代理检测
APP检测系统代理后直接不走代理,甚至断网。可以HookProxySelector强制返回NO_PROXY,或者用iptables将流量转发到抓包端口,也可以使用VPN模式的抓包工具。 - TLS指纹校验
服务器通过JA3/JA3S指纹识别抓包工具,即使证书校验通过也会返回异常。可以用Frida修改底层TLS扩展、密码套件顺序,或者使用支持指纹定制的代理工具。 - Frida与Root检测
APP检测Frida、检测Root、检测调试,会直接闪退或返回假数据。需要先过反调试:使用Frida隐身脚本、修改frida-server端口和包名、配合Magisk Hide隐藏Root,必要时使用Frida的Spawn模式提前注入。 - 多进程与多Dex
很多APP有多个进程,网络请求可能在子进程中,需要用--enable-jit并注入所有进程;加固APP的Dex会动态加载,需要等Dex加载完成后再Hook。 - 安卓14兼容性
安卓14加强了SELinux限制和命名空间隔离,旧版Frida会注入失败。建议使用Frida 16.2以上版本,配合Magisk的frida-magisk模块,关闭SELinux的严格模式。 - Hook时机错误导致崩溃
如果在APP启动早期就Hook还没加载的类,会导致崩溃。建议使用延迟Hook、类加载监听,或者用Spawn模式启动APP,在合适的时机执行Hook。 - 证书链与公钥校验
部分APP不校验完整证书,只校验公钥哈希、证书序列号或者证书链长度,这时候需要针对性Hook对应的校验函数,而不是只Hook标准接口。
九、写在最后
SSL Pinning的绕过从来没有万能脚本,本质是找到校验点,强制返回成功。不同APP的校验层级、实现方式都不一样,需要按照“系统层→框架层→Native层”的顺序逐层排查,而不是上来就堆一堆脚本。
Frida的价值在于动态调试能力,它可以让我们在不重打包、不修改APP的情况下,快速定位校验点、验证绕过方案,同时直接在内存中提取数据。但也要清楚,绕过证书校验只是接口分析的第一步,后面还有签名算法、参数加密、设备指纹、风控对抗,每一步都需要对应的逆向和工程能力。
最后提醒:所有技术方案仅用于合法的接口分析、安全测试与学习,请遵守相关法律法规,不要用于非法数据采集。