
1. 项目概述当App抓包工具集体“失明”作为一名常年和移动应用数据打交道的开发者我遇到过最让人头疼的场景之一就是当你信心满满地打开抓包工具准备分析某个App的网络请求时却发现流量列表一片空白或者干脆提示“证书不被信任”。这十有八九是遇到了SSL Pinning证书绑定。这就像App给自家的服务器大门上了一把特制的锁而你手里只有通用的万能钥匙系统信任的CA证书自然就吃了个闭门羹。SSL Pinning是一种安全增强技术App在开发阶段就将服务端的公钥或证书哈希值“硬编码”到客户端。当建立HTTPS连接时App会比对服务器返回的证书与内置的“真品”是否一致。如果不一致即使这个证书被操作系统信任比如你安装的抓包工具根证书连接也会被立刻终止。这就是为什么Charles、Fiddler、mitmproxy等抓包工具在面对今日头条、抖音、小红书、支付宝等大量应用时“集体失效”的根本原因。那么如何绕过这道防线手动逆向、修改App代码固然可行但门槛高、效率低。而Frida的出现为我们提供了一把动态的、精准的“万能钥匙”。它通过注入JavaScript脚本到目标App的运行时内存中可以实时地Hook钩住关键函数修改其逻辑从而让SSL Pinning验证“形同虚设”。本文将从原理入手带你一步步拆解SSL Pinning的实现并手把手教你编写和运行Frida脚本一站式解决这个困扰无数开发者和安全研究员的抓包难题。无论你是想进行安全审计、竞品分析还是单纯想研究某个App的API这套方法都能让你重获“透视”网络流量的能力。2. SSL Pinning原理深度拆解锁是如何工作的要绕过一把锁首先得了解它的构造。SSL Pinning并非铁板一块根据“锁芯”的不同主要有以下几种实现方式理解它们对后续编写Hook脚本至关重要。2.1 证书固定Certificate Pinning这是最直接的方式。开发者将服务器证书或其中间CA证书的整个二进制内容或者其公钥直接打包进App的资产Assets或资源目录。在建立SSL连接时App的网络库如OkHttp、Apache HttpClient会调用特定的验证回调。核心验证逻辑通常发生在X509TrustManager接口的实现类中。在Android上系统默认的TrustManager会检查证书链是否被设备信任的CA签名。而实现了Pinning的App会自定义一个TrustManager在其中加入额外的校验将服务器返回的证书与本地预置的证书进行逐字节比对或计算其指纹如SHA-256哈希进行比对。例如一个自定义的TrustManager的checkServerTrusted方法可能包含如下逻辑概念代码Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { // 1. 先执行标准的证书链验证可选有时会跳过 // defaultTrustManager.checkServerTrusted(chain, authType); // 2. Pinning 核心校验 for (X509Certificate cert : chain) { byte[] pubKey cert.getPublicKey().getEncoded(); String pin calculateSha256(pubKey); // 计算公钥指纹 if (!isValidPin(pin)) { // 与预置的PIN列表比对 throw new CertificateException(SSL Pinning verification failed); } } }我们的Frida脚本目标就是找到这个关键的校验方法并让它“永远返回成功”。2.2 公钥固定Public Key Pinning这种方式比证书固定更灵活。证书会过期、会轮换但服务器的公钥可以保持不变。因此App只存储公钥的指纹PIN。在TLS握手时验证服务器证书中的公钥指纹是否与预置的匹配。HTTP公钥固定HPKP曾是标准但已废弃不过在移动端开发者仍可在代码中自行实现类似的逻辑。其Hook点与证书固定类似都集中在证书验证环节。2.3 框架级实现以OkHttp为例现代Android开发中OkHttp是绝对主流的网络库它内置了对SSL Pinning的优雅支持通过CertificatePinner类实现。配置示例如下val client OkHttpClient.Builder() .certificatePinner( CertificatePinner.Builder() .add(api.example.com, sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) .build() ) .build()当使用这个client发起请求时OkHttp会在后台自动进行指纹校验。Frida脚本要Hook的目标就非常明确了okhttp3.CertificatePinner类的check方法。让这个方法静默失败或直接返回是绕过OkHttp系App Pinning的最有效途径。2.4 原生层NativePinning一些对安全要求极高的App如金融、大型社交App会将校验逻辑放在Native层C/C通过JNI调用。它们可能使用OpenSSL、BoringSSL等库直接进行证书验证。这增加了逆向和Hook的难度因为你需要分析so库文件。但Frida同样支持Native Hook我们可以定位到如SSL_CTX_set_cert_verify_callback或自定义的验证函数进行拦截。注意不同App可能混合使用多种方式甚至存在多级校验。成功的绕过往往需要组合拳逐一解除这些校验点。3. Frida工具链与环境搭建工欲善其事必先利其器。在编写脚本前我们需要一个可用的Frida环境。3.1 核心组件解析Frida Server (运行在目标设备)这是一个守护进程注入到目标进程并与Frida客户端通信。它是所有魔法的发生地。Frida CLI / Frida-tools (运行在分析机)命令行工具用于连接Server、列出进程、注入脚本等。我们常用的frida、frida-ps命令就来自这里。Frida Python Bindings如果你喜欢用Python脚本来自动化Frida操作强烈推荐就需要安装这个包pip install frida-tools通常已包含。3.2 环境搭建详细步骤分析机通常是你的电脑环境# 安装 Frida-tools它会自动安装合适版本的frida Python包 pip install frida-tools # 安装完成后检查版本 frida --version目标设备Android手机/模拟器环境这是关键且容易出错的一步。获取Frida Server访问Frida官方GitHub的Release页面。根据你目标设备的CPU架构下载对应的Server文件。现代手机大多是arm64模拟器可能是x86_64。文件命名类似frida-server-xx.x.x-android-arm64.xz。使用adb shell getprop ro.product.cpu.abi命令可以准确查询架构。推送与运行Server# 解压下载的.xz文件得到可执行文件frida-server # 将文件推送到设备的临时目录并赋予执行权限 adb push frida-server /data/local/tmp/ adb shell chmod 755 /data/local/tmp/frida-server # 切换到root用户需要已root的设备或模拟器 adb shell su -c /data/local/tmp/frida-server # 或者如果是在模拟器或已root的设备上也可以直接运行 adb shell cd /data/local/tmp ./frida-server 实操心得保持这个shell窗口不要关闭或者使用nohup让它在后台运行。你可以另开一个终端进行后续操作。端口转发可选但推荐默认情况下Frida Server监听在设备的27042端口。为了方便我们将其转发到本地adb forward tcp:27042 tcp:27042 adb forward tcp:27043 tcp:270433.3 基础连接测试在新终端中执行以下命令验证环境是否就绪# 列出设备上正在运行的进程 frida-ps -U # 或者使用Python交互模式 python -c import frida; print(frida.get_usb_device())如果能看到设备信息和进程列表恭喜你Frida的舞台已经搭好。4. Frida脚本编写实战解剖与Hook关键函数现在进入核心环节编写能绕过SSL Pinning的Frida脚本。我们的策略是“擒贼先擒王”找到负责证书验证的那个函数然后“偷梁换柱”。4.1 通用型脚本攻击TrustManager对于使用自定义X509TrustManager的App我们可以尝试Hook所有checkServerTrusted方法让它们统统“放行”。// disable_ssl_pinning_trustmanager.js Java.perform(function () { console.log([*] 开始尝试禁用SSL Pinning (TrustManager)...); // 1. 定位并Hook X509TrustManager接口的checkServerTrusted方法 var X509TrustManager Java.use(javax.net.ssl.X509TrustManager); // 这里有三个重载方法我们全部Hook var overloads X509TrustManager.checkServerTrusted.overloads; for (var i 0; i overloads.length; i) { overloads[i].implementation function (chain, authType) { console.log([] Bypassing TrustManager.checkServerTrusted for: authType); // 什么都不做直接通过验证 // 如果需要可以在这里打印证书信息用于调试 // for (var j 0; j chain.length; j) { // console.log(chain[j].getSubjectDN()); // } return; } } // 2. 有些App会实现自己的TrustManager类我们也可以尝试Hook常见的类名 var commonTrustManagers [ com.example.app.CustomTrustManager, okhttp3.internal.tls.OkHostnameVerifier, // OkHttp的Hostname验证器有时也参与 org.apache.http.conn.ssl.AbstractVerifier ]; commonTrustManagers.forEach(function (className) { try { var clazz Java.use(className); var method clazz.checkServerTrusted || clazz.verify; if (method) { method.implementation function () { console.log([] Bypassing custom verifier: className); return; } } } catch (e) { // 类不存在忽略 } }); console.log([*] TrustManager Hook 完成。); });脚本解析Java.perform确保代码在Java虚拟机上下文中执行。Java.use获取对目标类的引用。overloads一个方法可能有多个重载参数不同我们需要处理所有情况。implementation替换原方法的实现。这里我们用一个空函数覆盖相当于让验证逻辑失效。4.2 精准打击针对OkHttp的CertificatePinner如果目标App使用OkHttp那么针对CertificatePinner的Hook成功率更高且副作用更小。// disable_ssl_pinning_okhttp.js Java.perform(function () { console.log([*] 瞄准OkHttp 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 OkHttp CertificatePinner.check for host: hostname); // 原方法会抛出异常我们直接让它静默返回 return; }; // 另一个可能的重载 CertificatePinner.check.overload(java.lang.String, [Ljava.security.cert.Certificate;).implementation function (hostname, certificates) { console.log([] Bypassing OkHttp CertificatePinner.check (array) for host: hostname); return; }; console.log([*] OkHttp CertificatePinner Hook 完成。); });4.3 进阶挑战Hook Native层验证当校验逻辑在so库中时我们需要使用Frida的Interceptor来Hook Native函数。这需要先确定目标函数通常通过逆向分析so库获得。假设我们通过分析发现libnative-lib.so中有一个关键函数Java_com_example_app_NativeHelper_verifyCertificate。// disable_ssl_pinning_native.js Java.perform(function () { console.log([*] 尝试Hook Native层验证函数...); // 首先通过Module.findExportByName找到函数地址 var verifyFuncAddr Module.findExportByName(libnative-lib.so, Java_com_example_app_NativeHelper_verifyCertificate); if (verifyFuncAddr) { console.log([] 找到Native验证函数地址: verifyFuncAddr); // 使用Interceptor.attach进行Hook Interceptor.attach(verifyFuncAddr, { onEnter: function (args) { console.log([] Native证书验证函数被调用。); // args[0]是JNIEnv*, args[1]是jobject, args[2]...是参数 // 我们可以在这里打印或修改参数 }, onLeave: function (retval) { console.log([] 强制让Native验证返回成功 (true/1)。); // 修改返回值假设原函数返回jboolean (true) retval.replace(ptr(0x1)); // 替换为true } }); } else { console.log([-] 未找到指定的Native函数。); } });重要提示Native Hook需要对ARM/ARM64汇编和函数调用约定有一定了解。更常见的做法是Hook SSL库的通用函数如SSL_CTX_set_verify或SSL_get_verify_result但这需要对OpenSSL等库有更深理解。5. 脚本注入与实战抓包全流程有了脚本我们如何将它送入“战场”并验证成果5.1 连接与注入多种姿势方式一命令行直接注入最快捷# -U 表示USB设备-f 以spawn方式启动App-l 加载脚本 frida -U -f com.example.targetapp -l disable_ssl_pinning_okhttp.js --no-pause-f会重启App确保脚本在App启动最早阶段注入适合对付启动时就初始化网络库的App。--no-pause让App在注入后立即运行而不是挂起。方式二附加到已运行进程# 先启动App然后获取其进程名或PID frida-ps -U | grep targetapp # 假设进程名是 com.example.targetapp frida -U -n com.example.targetapp -l disable_ssl_pinning_okhttp.js方式三使用Python脚本自动化推荐用于复杂任务# automate_frida.py import frida import sys def on_message(message, data): if message[type] send: print(f[*] {message[payload]}) else: print(message) # 连接设备 device frida.get_usb_device() # 附加到进程 pid device.spawn([com.example.targetapp]) # 或者使用attach session device.attach(pid) # 加载JS脚本 with open(disable_ssl_pinning_okhttp.js, r) as f: jscode f.read() script session.create_script(jscode) script.on(message, on_message) script.load() # 如果用了spawn需要恢复进程运行 device.resume(pid) # 保持脚本运行 sys.stdin.read()运行python automate_frida.py控制权会保持你可以看到脚本输出的日志。5.2 配合抓包工具完成最后一击配置抓包工具代理确保你的抓包工具如Charles代理已开启例如监听0.0.0.0:8888。配置设备代理将手机或模拟器的Wi-Fi代理设置为电脑的IP和抓包工具的端口。安装抓包工具根证书在设备浏览器访问chls.pro/ssl(Charles) 或类似地址下载并安装证书。务必将其安装到“受信任的凭据”或“用户凭据”中具体位置因Android版本而异。启动Frida并注入脚本使用上述任一方法将绕过SSL Pinning的脚本注入目标App。触发网络请求在手机上操作目标App使其产生网络流量。验证成果此时你应该能在抓包工具中看到清晰的HTTPS请求和响应内容而不再是之前的CONNECT隧道或证书错误。5.3 实战案例以某资讯类App为例假设我们面对一个使用了OkHttp且启用了SSL Pinning的资讯App进程名com.example.news。初步探测先尝试通用TrustManager脚本发现抓包仍失败但Frida日志显示Hook到了系统TrustManager说明App可能没使用自定义的。调整策略换用OkHttp专用脚本。注入后Frida日志输出[] Bypassing OkHttp CertificatePinner.check for host: api.news.com这表明成功Hook。抓包验证再次触发App刷新Charles中立刻出现了api.news.com下的明文API请求成功。6. 常见问题、反调试与排查技巧实录实战从来不会一帆风顺。下面是我在无数次“攻防”中积累的常见问题与解决方案。6.1 Frida连接与注入失败问题现象可能原因排查步骤与解决方案frida-ps -U无输出或报错1. Frida Server未运行或已崩溃。2. 设备未Root/ADB未授权。3. 端口转发问题。1.adb shell进入设备ps | grep frida检查进程。若无重新运行Server。2. 确认设备已Rootadb shell su -c whoami返回root。3. 尝试直接使用frida-ps -U不转发端口或检查防火墙是否阻挡了27042端口。Failed to spawn: unable to find application包名错误。使用frida-ps -U | grep -i keyword精确查找进程名。注入后App秒退或闪退1. 脚本存在语法错误或运行时异常。2. App检测到Frida并触发反调试。1. 检查Frida输出的红色错误信息修正JS脚本。2. 见下方反调试对抗部分。6.2 脚本Hook成功但抓包仍失败问题现象可能原因排查步骤与解决方案抓包工具显示Tunnel to ...或CONNECT1. 设备代理设置错误。2. App使用了证书透明CT或HTTP/3(QUIC)。3. Hook点不对还有更深层的验证。1. 确认手机代理IP和端口正确且抓包工具监听在所有接口0.0.0.0。2. QUIC协议UDP 443不走代理抓包工具需额外支持。可尝试在App设置或系统网络设置中强制使用HTTP/2。3. 尝试组合脚本同时Hook TrustManager和OkHttp或使用更激进的脚本如禁用所有SSL验证。抓包工具提示证书错误非Pinning抓包工具的根证书未正确安装或不被信任。在Android设置中找到“加密与凭据”-“用户凭据”或“安全”-“更多安全设置”确认抓包工具证书已存在且状态为“已启用”。Android 7需要将证书安装到系统分区才能被所有App信任需Root或使用Magisk模块“Move Certificates”。只有部分请求被抓到App可能使用了多域名且只对部分域名做了Pinning。或者部分请求走的是图片、日志等非关键域名未做严格校验。观察Frida日志看Hook被触发的域名。确保脚本对所有可能的验证点都生效。6.3 对抗App的反调试与Frida检测高安全等级的App会尝试检测Frida的存在常见手段包括检测端口扫描27042等默认端口。检测进程/文件查找frida-server、gum-js等进程或文件。检测内存特征查找Frida注入的线程名、内存映射等。应对策略修改Frida Server名称和端口# 重命名server文件 mv frida-server frida_helper # 运行在非默认端口 ./frida_helper -l 0.0.0.0:8080在连接时指定端口frida -H 192.168.x.x:8080 ...使用定制版或隐藏性更好的工具如objection基于Frida的anti-anti-frida插件或使用ptrace等底层技术进行隐藏。Hook App自身的检测函数如果App通过JNI调用getprocs等函数来检测进程我们可以用Frida Hook这些检测函数使其返回“干净”的结果列表。// 示例Hook readdir 来隐藏frida相关文件Native层 var readdir Module.findExportByName(null, readdir); Interceptor.attach(readdir, { onLeave: function(retval) { var dirEntry retval; if (!dirEntry.isNull()) { var cName dirEntry.add(/* offset of d_name */).readCString(); if (cName cName.includes(frida)) { // 如果发现frida相关条目跳过它模拟读取结束 // 这是一个复杂操作需要深入理解结构体此处仅为思路 } } } });实操心得反调试与反反调试是猫鼠游戏。对于大多数App修改端口和名称已足够。对于强对抗App需要具体分析其检测点进行针对性Hook。有时使用较新或较旧版本的Frida也可能绕过检测。6.4 脚本调试技巧善用console.log在脚本关键位置打印变量、参数、堆栈这是最直接的调试方式。使用Frida Trace进行动态追踪在不写脚本的情况下先摸清App的网络调用路径。# 追踪所有包含“ssl”或“cert”的类方法 frida-trace -U -i *ssl* -i *cert* com.example.targetapp处理重载方法使用overloads遍历或使用overload(...)精确匹配避免因参数类型不对而Hook失败。绕过SSL Pinning只是移动安全分析的起点。掌握了Frida的动态注入能力你就拥有了在运行时观察和修改App行为的“上帝视角”。从Hook加密函数到修改业务逻辑可能性大大增加。我个人在实际操作中最大的体会是耐心和细致是关键。每个App的防御策略都像一座独特的迷宫需要你仔细分析日志、尝试不同的Hook点并做好与反调试机制周旋的准备。最后请务必在合法授权的范围内使用这些技术它们是你理解系统、提升技能、进行安全防御的利器。