1. 项目概述为什么我们要跟SSL Pinning“斗智斗勇”做移动安全测试或者逆向分析的朋友肯定都遇到过这个让人头疼的“拦路虎”——SSL Pinning中文常叫“证书绑定”。简单来说它就像App给自己家的后门上了一把特制的锁这把锁只认它自己预先埋好的那把钥匙也就是内置的特定证书或公钥。当你试图用Burp Suite、Charles这类抓包工具把自己的“万能钥匙”代理工具的CA证书插进去时App会立刻警觉并拒绝连接让你啥也抓不到。这招对于保护用户数据在传输过程中的安全防止中间人攻击确实非常有效。但对于我们这些需要分析App通信逻辑、排查接口问题或者进行安全评估的人来说它就成了必须跨过去的一道坎。我最早接触这个问题是为了分析一个金融类App的加密协议。当时用Burpsuite死活抓不到包一开代理App就网络错误折腾了半天才发现是SSL Pinning在作祟。网上搜解决方案“JustTrustMe”这个Xposed模块几乎是必提的“神器”。它通过Hook钩子Android系统底层的一些关键类让App无条件信任所有证书从而绕过校验。这方法在早些年Android 7.0以下系统上几乎是“一招鲜吃遍天”。但随着系统安全机制的加强特别是Android 7.0引入了网络安全配置以及App开始采用更复杂的自定义校验逻辑单纯依赖JustTrustMe越来越力不从心。有时候你会发现装了模块重启了手机抓包工具里依然是一片空白。所以今天我们不只讲怎么用现成的模块更要深入一步手把手带你理解背后的原理并教你如何自己动手用Xposed框架编写Hook代码精准地搞定那些“顽固”的证书验证逻辑。这不仅能解决你手头的问题更能让你掌握一套通用的、可定制的分析方法以后遇到任何新的校验方式你都知道从哪里入手去破解。无论你是安全研究员、逆向工程师还是对移动端技术有浓厚兴趣的开发者这套思路和实操方法都会让你受益匪浅。2. SSL Pinning的核心原理与常见实现方式要绕过它首先得知道它是怎么工作的。SSL Pinning的本质是App在代码层面硬编码了它认为合法的证书信息并在建立TLS/SSL连接时将服务器返回的证书与本地预置的信息进行比对。如果匹配失败就果断终止连接。它的实现层次可以大致分为三类理解这三类就等于拿到了破解地图。2.1 系统默认信任管理器TrustManager的定制化这是最经典也是最常见的方式。在Android中javax.net.ssl.TrustManager是负责决定是否信任对方证书的核心接口。App可以通过实现自定义的X509TrustManager在checkClientTrusted或checkServerTrusted方法中加入严格的校验逻辑。比如它可能不是简单地检查证书是否由可信CA签发而是直接比对证书的指纹SHA-1或SHA-256、公钥甚至整个证书链。一个简单的自定义TrustManager伪代码逻辑可能是这样的public class MyPinningTrustManager implements X509TrustManager { private static final String PINNED_CERT_SHA256 A1:B2:C3...; Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { // 1. 先执行标准的CA验证可选但常见 // 2. 关键提取终端实体证书chain[0]的SHA-256指纹 String certFingerprint calculateSha256(chain[0].getEncoded()); // 3. 与预置的指纹比对 if (!PINNED_CERT_SHA256.equals(certFingerprint)) { throw new CertificateException(证书指纹不匹配Pinning失败); } } // ... 其他方法 }这种方式的Hook点非常明确就是checkServerTrusted这个方法。我们的目标就是让这个方法不抛异常或者直接返回假装校验通过。2.2 OkHttp等网络库的证书绑定机制如今绝大多数Android App使用OkHttp或Retrofit基于OkHttp作为网络库。OkHttp原生提供了简洁强大的证书绑定Certificate Pinning支持。开发者只需要在OkHttpClient.Builder中配置一下即可val client OkHttpClient.Builder() .certificatePinner( CertificatePinner.Builder() .add(api.example.com, sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) .build() ) .build()当使用这个client发起请求时OkHttp会在后台自动完成证书指纹的校验。这种方式比自定义TrustManager更优雅也更常见。绕过它的思路要么是HookCertificatePinner类的check方法要么更直接地HookOkHttpClient.Builder的certificatePinner方法让其返回一个不做任何校验的“空”Pinner实例。2.3 原生代码Native Code实现一些对安全要求极高的App可能会将核心的校验逻辑放在Native层C/C代码通过JNI调用。这大大增加了分析的难度因为你需要反编译并阅读so库文件。对付这种方式静态分析IDA Pro, Ghidra和动态调试Frida, GDB的能力就变得至关重要。思路可能是Hook Native层用于比较证书数据的函数或者更上层地Hook Java层调用Native方法的地方直接伪造一个成功的返回值。注意在实际分析中一个App可能同时采用多种方式形成多层防御。例如先用OkHttp Pinning再在自定义TrustManager里加一道校验。所以我们的绕过策略也需要灵活组合。3. 工具准备与环境搭建工欲善其事必先利其器。在开始Hook之前我们需要一个合适的实验环境。这里我强烈推荐使用Android模拟器而不是真机因为我们需要频繁修改系统、安装Xposed框架模拟器更容易重置和快照。3.1 模拟器与系统镜像选择我个人的选择是Android Studio自带的AVDAndroid Virtual Device管理器搭配一个x86架构的Android 7.1API 25系统镜像。为什么是7.1对Xposed框架兼容性好虽然更高版本也有办法但7.1是经过大量实践验证最稳定的版本之一。规避部分新限制Android 8.0以上对非系统应用安装CA证书有更多限制7.1下操作更简单。性能与兼容性平衡x86镜像在电脑上运行速度远快于ARM且大多数App都提供x86兼容库。创建AVD时建议选择不带Google Play服务的镜像如“Android 7.1 Nougat”以减少不必要的后台干扰和网络流量。将模拟器的存储空间设置得大一些如2GB以便安装多个测试App和工具。3.2 Xposed框架的安装与模块管理Xposed框架是我们实现Hook的基石。在模拟器上安装步骤比真机简单很多。在电脑上下载对应Android 7.1 x86的Xposed框架安装包通常是一个ZIP文件如xposed-v89-sdk25-x86.zip。启动模拟器并确保已开启Root权限。在AVD的启动参数中可以加上-writable-system以便以可写系统分区的方式启动。将下载的Xposed框架ZIP包拖入模拟器窗口进行安装。更可靠的方式是使用ADB命令刷入adb root adb remount adb push xposed-v89-sdk25-x86.zip /sdcard/ adb shell twrp install /sdcard/xposed-v89-sdk25-x86.zip假设你已通过某种方式在模拟器内安装了TWRP等Recovery。对于AVD更简单的方法是直接刷入已集成Xposed的系统镜像。重启模拟器后安装“Xposed Installer”APK。打开后如果显示“Xposed框架版本 XX 已激活”则说明安装成功。接下来你需要一个模块来承载我们的Hook代码。你可以直接使用现成的“JustTrustMe”模块在Xposed Installer的仓库中搜索下载它已经集成了对多种常见证书校验方式的Hook。但我们的目标是学习所以我更建议你自己创建一个简单的Xposed模块项目。3.3 抓包代理工具配置Burp SuiteBurp Suite是我们的“照妖镜”用来验证Hook是否成功。在电脑上运行Burp Suite在Proxy - Options中确保代理监听在0.0.0.0:8080这样模拟器能访问到。关键一步导出Burp的CA证书。在Burp中访问http://burp点击“CA Certificate”下载cacert.der文件。将其转换为PEM格式Android需要openssl x509 -inform DER -in cacert.der -out cacert.pem将cacert.pem文件重命名为cacert.cerAndroid识别.cer后缀然后推送到模拟器adb push cacert.cer /sdcard/在模拟器的设置 - 安全 - 从SD卡安装证书中找到并安装这个证书。安装时会要求你设置一个证书名称如“PortSwigger CA”。最后在模拟器的Wi-Fi设置中长按当前网络选择“修改网络”展开高级选项设置代理为手动主机名填写你电脑的局域网IP不是127.0.0.1端口填写8080。至此基础环境就搭建好了。如果一切正常未做任何Hook的情况下打开一个普通HTTP网站应该能在Burp中看到流量。但对于启用了SSL Pinning的HTTPS App此时连接会失败。4. 从使用JustTrustMe到理解其原理在动手写代码前我们先做个实验看看现成的“神器”是怎么工作的。在Xposed Installer中下载激活JustTrustMe模块并重启模拟器。然后打开一个你怀疑启用了SSL Pinning的App比如某些银行的测试版或小众App操作一下触发网络请求。此时再观察Burp Suite你很可能会惊喜地发现之前抓不到的HTTPS请求现在都出现了。但是JustTrustMe不是万能的。我遇到过好几次失效的情况App使用了自定义的Native校验JustTrustMe主要Hook Java层对Native层无能为力。App使用了非标准的网络库或自研协议JustTrustMe的Hook目标列表可能没有覆盖。Android版本过高系统API发生变化Hook的类或方法可能已不存在或行为改变。这时候我们就需要打开JustTrustMe的源码项目通常在GitHub上开源看看它到底做了什么。以其中一个经典版本为例它的核心代码是Hook了几个关键类// Hook 1: 让TrustManager对所有证书都放行 findAndHookMethod(org.apache.http.conn.ssl.SSLSocketFactory, lpparam.classLoader, setHostnameVerifier, HostnameVerifier.class, new XC_MethodHook() { Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { param.setResult(new HostnameVerifier() { Override public boolean verify(String hostname, SSLSession session) { return true; // 总是返回true验证主机名 } }); } }); // Hook 2: 让自定义的X509TrustManager不做任何检查 findAndHookMethod(com.example.MyPinningTrustManager, lpparam.classLoader, checkServerTrusted, X509Certificate[].class, String.class, new XC_MethodHook() { Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // 什么也不做让方法直接通过不抛异常 param.setResult(null); } }); // Hook 3: 针对OkHttp的CertificatePinner findAndHookMethod(okhttp3.CertificatePinner, lpparam.classLoader, check, String.class, List.class, new XC_MethodHook() { Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // 直接返回跳过检查 param.setResult(null); } });看到没原理其实非常直观找到执行校验的那个关键方法然后通过Hook让它“失灵”。要么是让它直接返回不执行原有逻辑要么是让它返回一个永远为“真”的结果。实操心得JustTrustMe模块本质上是一个“广谱”的Hook列表。它预先猜测了开发者可能使用的各种证书校验类和方法并全部进行Hook。这带来了便利也带来了问题一是可能产生不必要的Hook影响其他App或系统稳定性二是无法应对未知的、新的校验方式。因此掌握针对性Hook的方法才是治本之策。5. 手把手编写自定义Xposed Hook模块当JustTrustMe失效时我们就需要自己动手针对特定的App进行“外科手术式”的Hook。这个过程分为四步定位、分析、编写、测试。5.1 第一步定位证书校验代码位置首先你需要目标App的APK文件。使用反编译工具如Jadx-GUI或Apktool jd-gui将其打开。我们的目标是搜索那些“可疑”的关键词。在Jadx中使用全局搜索快捷键通常是CtrlShiftF搜索类名TrustManager,X509TrustManager,checkServerTrusted,checkClientTrusted。搜索网络库相关OkHttpClient,CertificatePinner,pin。搜索字符串sha256/,pin,certificate,SSL。搜索方法调用setCertificatePinner,setHostnameVerifier。例如搜索checkServerTrusted你可能会找到类似下面的代码片段public class SecurityHelper { public static void verifyCertificate(X509Certificate[] chain) { // ... 一些计算指纹的代码 ... String pinnedHash sha256/AAAAAAAA...; String certHash calculateHash(chain[0]); if (!pinnedHash.equals(certHash)) { throw new SSLException(Certificate pinning failure); } } }或者你可能会发现一个自定义的TrustManager类。记下这个类的完整名称比如com.targetapp.security.MyCustomTrustManager。5.2 第二步分析校验逻辑与确定Hook点找到代码后不要急着Hook。先花几分钟读懂它的逻辑。它校验的是什么是整个证书的DER编码的SHA-256还是公钥的SHA-256或者是Subject字段校验失败后如何反应是抛出CertificateException还是SSLException或者只是记录日志但允许通过后者比较少见这个校验方法被谁调用是在一个单例的TrustManager里还是在每次请求时动态创建确定Hook点的原则是选择最底层、最核心的那个校验方法。通常这就是checkServerTrusted方法。我们的目标就是阻止它抛出异常。5.3 第三步编写Xposed模块代码现在创建一个新的Android Studio项目选择“No Activity”。然后添加Xposed Bridge API的依赖。最简单的方式是下载XposedBridgeApi-XX.jar版本与你模拟器的Xposed框架匹配放到项目的libs目录并在build.gradle中添加dependencies { compileOnly files(libs/XposedBridgeApi-XX.jar) }接下来创建你的Hook主类例如PinningBypassHookpackage com.example.pinningbypass; import de.robv.android.xposed.IXposedHookLoadPackage; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XposedHelpers; import de.robv.android.xposed.callbacks.XC_LoadPackage; public class PinningBypassHook implements IXposedHookLoadPackage { Override public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { // 只处理我们的目标App if (!lpparam.packageName.equals(com.targetapp.package)) { return; } // 案例1: Hook 自定义TrustManager的checkServerTrusted try { Class? trustManagerClass XposedHelpers.findClass( com.targetapp.security.MyCustomTrustManager, lpparam.classLoader ); XposedHelpers.findAndHookMethod( trustManagerClass, checkServerTrusted, X509Certificate[].class, String.class, new XC_MethodHook() { Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // 关键什么都不做让方法静默通过不执行原校验逻辑也不抛异常。 // 如果原方法有返回值可能需要设置一个合适的返回值这里通常是void。 // param.setResult(null); // 如果方法非void可能需要这行 } } ); Log.d(Xposed, 成功Hook MyCustomTrustManager); } catch (XposedHelpers.ClassNotFoundError | NoSuchMethodError e) { Log.d(Xposed, 未找到MyCustomTrustManager尝试其他Hook点); } // 案例2: Hook OkHttp的CertificatePinner.check try { Class? certPinnerClass XposedHelpers.findClass( okhttp3.CertificatePinner, lpparam.classLoader ); XposedHelpers.findAndHookMethod( certPinnerClass, check, String.class, List.class, new XC_MethodHook() { Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // 直接返回跳过指纹检查 param.setResult(null); } } ); Log.d(Xposed, 成功Hook OkHttp CertificatePinner); } catch (XposedHelpers.ClassNotFoundError | NoSuchMethodError e) { // 可能使用的不是OkHttp或者版本不同 } // 案例3: Hook SSLSocketFactory的setHostnameVerifier (更通用的备用方案) try { Class? sslSocketFactoryClass XposedHelpers.findClass( org.apache.http.conn.ssl.SSLSocketFactory, lpparam.classLoader ); XposedHelpers.findAndHookMethod( sslSocketFactoryClass, setHostnameVerifier, org.apache.http.conn.ssl.X509HostnameVerifier.class, new XC_MethodHook() { Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // 替换为一个全部放行的验证器 param.setResult(new org.apache.http.conn.ssl.X509HostnameVerifier() { Override public boolean verify(String host, SSLSession session) { return true; } Override public void verify(String host, SSLSocket ssl) throws IOException {} Override public void verify(String host, X509Certificate cert) throws SSLException {} Override public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException {} }); } } ); } catch (Exception e) { // 忽略可能App未使用Apache HTTP Client } } }代码中的关键点在于beforeHookedMethod方法。我们通过param.setResult(null)或直接让方法体为空来“劫持”原方法的执行流使其失效。5.4 第四步配置模块声明与安装测试在assets目录下创建xposed_init文件里面写上你的Hook主类全名com.example.pinningbypass.PinningBypassHook在AndroidManifest.xml的application标签内添加Xposed模块元数据meta-data android:namexposedmodule android:valuetrue / meta-data android:namexposeddescription android:valueBypass SSL Pinning for Target App / meta-data androidxposedminversion android:value82 /编译生成APK安装到模拟器。在Xposed Installer的“模块”页面勾选你的模块然后重启模拟器。重启后打开目标App并触发网络请求。此时再观察Burp Suite如果之前抓不到的包现在出现了恭喜你你的自定义Hook成功了6. 进阶技巧与疑难问题排查掌握了基础Hook后我们来看看那些更棘手的情况和提升效率的技巧。6.1 应对Native层校验如果Java层的Hook都无效问题可能出在Native层。这时你需要使用Frida这样的动态插桩工具。Frida可以在运行时注入JavaScript代码来Hook Native函数。基本思路是使用objc.choose()或Module.findExportByName()定位到负责校验的Native函数这需要一定的逆向分析基础可能要通过字符串搜索、交叉引用等方式在so库中找到关键函数。编写Frida脚本替换该函数的返回值。例如如果一个叫native_verify_cert的函数返回1表示成功0表示失败你可以这样HookInterceptor.attach(Module.findExportByName(libsecurity.so, native_verify_cert), { onLeave: function(retval) { // 无论原逻辑如何都强制返回成功 (1) retval.replace(ptr(0x1)); } });将Frida脚本注入到目标App进程。这通常需要在已Root的设备上通过frida -U -f com.targetapp.package -l your_script.js命令完成。6.2 处理代码混淆与加固商业App普遍会进行代码混淆ProGuard甚至加固如梆梆、腾讯御安全。这会让类名和方法名变成a.a,b.b这种无意义的字符增加定位难度。应对策略字符串常量证书指纹、错误提示语等字符串通常不会被混淆。在Jadx中搜索pin,certif,sha256等字符串然后查看其所在的类和方法。调用栈分析在运行时触发SSL Pinning错误比如故意装一个错误的CA证书让App崩溃从崩溃日志的调用栈中寻找线索。或者使用Android Studio的调试器附加进程在证书验证相关API如X509TrustManager方法上设置断点。特征行为Hook即使类名混淆了但系统API如javax.net.ssl.*下的类是无法被混淆的。你可以尝试Hook所有实现了X509TrustManager接口的类。这虽然粗暴但有时有效。// 遍历所有已加载的类寻找目标 XposedHelpers.findAndHookMethod(ClassLoader.class, loadClass, String.class, new XC_MethodHook() { Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { Class? clazz (Class?) param.getResult(); if (clazz ! null X509TrustManager.class.isAssignableFrom(clazz)) { // 找到疑似TrustManager的类Hook其checkServerTrusted方法 XposedBridge.hookAllMethods(clazz, checkServerTrusted, new XC_MethodHook() { Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // 绕过逻辑 } }); } } });6.3 通用型Hook模块的编写思路如果你不想每次都针对一个App写代码可以尝试编写一个更通用的模块。思路是同时Hook多个已知的、常见的证书校验入口点就像JustTrustMe做的那样但你可以根据自己的经验维护一个更精准的列表。你可以创建一个配置界面让用户输入App包名然后动态启用针对该App的特定Hook集。6.4 常见问题排查清单当你按照步骤操作却依然失败时可以按以下清单排查问题现象可能原因排查步骤Xposed模块未激活模块未勾选或未重启1. 检查Xposed Installer中模块是否已勾选。2. 确保已执行软重启或完全重启。3. 查看Xposed Installer日志是否有模块加载错误。Hook代码未执行包名过滤错误类名/方法名不对1. 在Hook代码开始处加Log确认是否进入目标App处理逻辑。2. 检查目标App的真实包名用adb shell dumpsys package | grep -i targetapp。3. 确认反编译得到的类名和方法签名参数列表完全正确。抓包工具无流量代理设置错误或App禁用代理1. 确认模拟器Wi-Fi代理设置正确主机IP和端口。2. 尝试用浏览器访问http://burp看是否能下载CA证书验证代理连通性。3. App可能使用了NetworkSecurityPolicy或代码设置了Proxy.NO_PROXY需要额外Hook。HTTPS请求仍失败CA证书未安装或系统不信任1. 确认Burp的CA证书已正确安装到“系统信任的凭据”中Android 7需Root后放入系统证书目录。2. 对于Android 7App可能只信任系统预置证书需将Burp证书放入/system/etc/security/cacerts/并设置正确权限644。3. App可能使用了证书透明度CT等额外校验。仅部分请求可抓App混合了多种网络库或校验方式1. 分析可抓和不可抓的请求看域名、路径是否有规律。2. 可能只有部分接口使用了SSL Pinning或者不同模块使用了不同的网络库如既有OkHttp又有HttpURLConnection。需要补充Hook点。App崩溃或闪退Hook逻辑有误或影响了其他正常逻辑1. 检查Hook方法中是否错误地修改了参数或返回值导致后续流程出错。2. 尝试将beforeHookedMethod改为afterHookedMethod看看是否原方法执行后崩溃。3. 使用XposedBridge.log输出详细日志定位崩溃点。实操心得最耗时的往往不是写Hook代码而是定位正确的Hook点。一个高效的技巧是先使用JustTrustMe这类通用模块测试。如果能成功说明是已知的校验方式然后用Xposed的日志功能或写一个简单的日志模块打印出JustTrustMe具体Hook了哪些类和方法这能为你提供最直接的线索。如果JustTrustMe也失败那就要做好深入逆向分析的心理准备了从字符串、网络库初始化代码、异常堆栈等信息一点点摸清它的防御体系。这个过程虽然繁琐但每一次成功破解都是对移动应用安全机制理解的一次深化。