Android应用SSL Pinning绕过实战:Frida动态Hook与HTTPS流量解密

发布时间:2026/7/4 11:27:11
Android应用SSL Pinning绕过实战:Frida动态Hook与HTTPS流量解密 1. 项目概述与核心价值最近在分析一个Android应用时遇到了一个典型的安全加固场景应用启用了SSL Pinning证书绑定。这意味着即使我在测试设备上安装了自签名的根证书试图用Wireshark这类抓包工具来解密HTTPS流量也会因为应用只信任它自己“绑定”的特定证书而失败抓到的全是加密的乱码。这就像一扇门不仅上了锁HTTPS加密还把唯一的钥匙焊死在了门框上SSL Pinning常规的中间人攻击手段瞬间失效。对于安全研究、逆向分析或者单纯的故障排查来说这无疑是个棘手的障碍。这个实战项目的目标就是解决这个“钥匙被焊死”的问题。我们的核心思路是不跟应用在证书验证的逻辑上硬碰硬而是利用Frida这个强大的动态代码插桩框架在应用运行时“劫持”其SSL/TLS相关的验证函数让验证逻辑失效或返回我们期望的结果从而绕过SSL Pinning。一旦绕过应用就会接受我们中间人代理比如Burp Suite或Charles的证书HTTPS流量就能被Wireshark清晰解密和查看。整个过程融合了逆向工程、动态调试和网络分析是移动安全分析中一项非常实用且核心的技能。这篇文章我将以一个真实的Android应用具体名称不便透露但方法通用为例手把手带你走通从环境搭建、脚本编写、注入调试到最终成功抓包的完整流程。无论你是安全研究员、应用开发者想测试自己的网络库还是对移动端逆向感兴趣的爱好者这套组合拳都能为你打开一扇透视HTTPS通信内容的大门。我会重点分享我在实战中遇到的坑和解决技巧确保你能复现并理解每一步背后的原理。2. 环境与工具链的精准配置工欲善其事必先利其器。一个稳定、版本匹配的工具链是成功的第一步。这里我强烈建议使用物理Android手机进行测试虽然模拟器如雷电、夜神在某些场景下更方便但许多应用会检测运行环境在模拟器上可能无法正常运行或触发额外的反调试机制增加不必要的复杂度。我的主力测试机是一台已获取Root权限的Android 10设备。2.1 Frida生态搭建服务端与客户端的版本协同Frida的架构分为两部分运行在目标设备上的frida-server服务端和运行在你电脑上的frida-tools客户端包含frida、frida-ps等命令。版本必须严格一致否则会出现连接失败或无法注入等诡异问题。1. 电脑端Client安装我习惯使用Python的pip进行安装这样可以方便地管理版本。建议在虚拟环境中操作。# 创建并激活虚拟环境可选但推荐 python -m venv frida_env source frida_env/bin/activate # Linux/macOS # 或 frida_env\Scripts\activate # Windows # 安装指定版本的frida-tools它会自动安装对应版本的frida核心库 pip install frida-tools16.1.4这里我选择了16.1.4版本这是一个经过大量实践验证的稳定版本。安装完成后可以通过frida --version和frida-ps --version来确认。2. 手机端Server部署这是关键一步。你需要根据你手机的处理器架构ARM, ARM64, x86等下载对应的frida-server可执行文件。通过adb shell连接手机后执行getprop ro.product.cpu.abi可以查看架构现代手机大多是arm64-v8a。前往Frida的GitHub Releases页面找到与你客户端版本如16.1.4匹配的发布包。下载对应架构的frida-server-16.1.4-android-arm64.xz文件。解压得到frida-server-16.1.4-android-arm64文件。通过ADB推送到手机并赋予执行权限adb push frida-server-16.1.4-android-arm64 /data/local/tmp/frida-server adb shell chmod 755 /data/local/tmp/frida-server3. 启动与连接测试在手机端以后台方式启动serveradb shell /data/local/tmp/frida-server 在电脑端可以列出手机上的进程来测试连接是否成功frida-ps -U如果能看到一长串进程列表恭喜你Frida通道已经打通。这里有个重要技巧很多教程会教你用adb shell su -c来以root权限启动这当然可以。但我更推荐一个更稳定的方法——将frida-server复制到/system/bin或/system/xbin需要Remount系统分区为可写这样它就有了系统级权限并且开机可以自启通过init.rc或Magisk模块避免了每次重启都要重新推送和启动的麻烦。2.2 Wireshark与中间人代理的定位Wireshark是我们的“眼睛”负责捕获和展示网络数据包。直接从官网下载安装最新版即可。但Wireshark本身不直接进行HTTPS解密它需要配合中间人攻击MitM来获取解密密钥。常见的MitM工具有Burp Suite和Charles。在这个方案中Wireshark的角色更偏向于“底层流量监控和最终展示”。我们通常的流程是设置手机Wi-Fi代理指向电脑上运行的Burp Suite例如192.168.1.100:8080。在手机安装Burp Suite的CA证书。使用Frida绕过目标应用的SSL Pinning使其信任Burp的证书。Burp Suite成功代理并解密流量。同时我们在电脑上使用Wireshark监听相同的网络接口如Wi-Fi网卡。为了能让Wireshark也解密流量我们需要将Burp Suite与Wireshark联动。Burp Suite支持将每个会话的TLS主密钥Master Secret以SSLKEYLOGFILE格式导出。Wireshark可以配置读取这个文件从而解密捕获到的TLS流量。因此你需要确保Burp Suite已正确配置并运行。在Burp Suite的Proxy - Options - Proxy Listeners中确保你的代理监听器是启动状态并且Support invisible proxying选项根据情况勾选这有助于处理一些非标准端口或直连IP的流量。2.3 目标应用的准备与初步分析在开始Hook之前需要对目标应用有个基本了解。使用frida-ps -U找到它的包名例如com.example.targetapp。然后可以先用Frida附加上去看看它加载了哪些库这对于后续寻找Hook点很有帮助。frida -U -f com.example.targetapp --no-pause在Frida的交互式命令行中可以执行如Process.enumerateModules()来枚举模块。特别关注那些与SSL/TLS相关的库比如libssl.soOpenSSL、libcrypto.so、或者Android系统自带的conscrypt相关库。不同的应用使用的网络库可能不同OkHttp, Volley, 系统HttpURLConnection或自定义的Native库这决定了我们Hook的具体函数。注意直接启动应用-f可能会触发应用的反调试检测。如果应用一启动就崩溃可以尝试先启动应用然后使用frida -U -n com.example.targetapp附加到已运行的进程上。如果附加后立刻崩溃说明有很强的反Frida机制这就需要先进行反反调试这本身就是一个更大的话题可能涉及绕过ptrace检测、文件检测、端口检测等本篇暂不深入展开。3. SSL Pinning原理与Frida绕过策略剖析要绕过它必须先理解它。SSL Pinning的核心思想是“我只认我认识的证书”。具体实现通常有两种方式证书锁定Certificate Pinning在应用代码或资源文件中硬编码或通过其他方式存储服务器证书的公钥或整个证书的哈希值如SHA-256指纹。当建立TLS连接时应用会将服务器返回的证书与本地存储的指纹进行比对不一致则拒绝连接。这是最常见的方式。公钥锁定Public Key Pinning与证书锁定类似但只比对公钥部分。这样即使证书到期续签只要公钥不变连接仍可继续。在Android中实现SSL Pinning的代码可能存在于Java/Kotlin层使用OkHttp的CertificatePinner类或自定义的X509TrustManager接口实现。Native层C/C使用OpenSSL库通过SSL_CTX_set_cert_verify_callback自定义验证回调函数或直接HookSSL_verify_cert_chain等函数。我们的绕过策略就是针对这些验证点进行“欺骗”。3.1 Java层通用Hook脚本解析对于使用OkHttp等高级网络库的应用在Java层Hook通常是最直接有效的。Frida提供了强大的Java API可以枚举类、方法并进行拦截。下面是一个功能强大且通用的Java层SSL Pinning绕过脚本的核心思路Java.perform(function() { console.log([*] Starting Java SSL Pinning Bypass...); // 1. 攻击 CertificatePinner (OkHttp) var CertificatePinnerClass Java.use(okhttp3.CertificatePinner); CertificatePinnerClass.check.overload(java.lang.String, [Ljava.security.cert.Certificate;).implementation function(pin, certs) { console.log([] Bypassing CertificatePinner.check for: pin); // 直接不执行任何验证逻辑或者返回null表示成功 return; }; // 2. 攻击 TrustManager (最根本的方法) var X509TrustManagerClass Java.use(javax.net.ssl.X509TrustManager); var TrustManagerImpls []; // 遍历所有已加载的类寻找X509TrustManager的实现类 Java.enumerateLoadedClasses({ onMatch: function(className) { if (className.includes(TrustManager) || className.includes(X509)) { // 这里需要小心避免Hook到系统关键类导致不稳定 try { var clazz Java.use(className); // Hook checkClientTrusted 和 checkServerTrusted 方法 if (clazz.checkServerTrusted) { clazz.checkServerTrusted.implementation function(chain, authType) { console.log([] Bypassing checkServerTrusted in: className); return; }; TrustManagerImpls.push(className); } if (clazz.checkClientTrusted) { clazz.checkClientTrusted.implementation function(chain, authType) { console.log([] Bypassing checkClientTrusted in: className); return; }; } } catch (e) { // 忽略无法Hook的类 } } }, onComplete: function() { console.log([*] TrustManager enumeration complete. Hooked: TrustManagerImpls.join(, )); } }); // 3. 攻击 SSLContext.init 方法替换掉TrustManager var SSLContextClass Java.use(javax.net.ssl.SSLContext); SSLContextClass.init.overload([Ljavax.net.ssl.KeyManager;, [Ljavax.net.ssl.TrustManager;, java.security.SecureRandom).implementation function(keyManagers, trustManagers, secureRandom) { console.log([] Intercepted SSLContext.init); // 创建一个什么都不做信任所有证书的TrustManager var TrustAllManager Java.registerClass({ name: com.bypass.TrustAllManager, implements: [X509TrustManagerClass], methods: { checkClientTrusted: function(chain, authType) {}, checkServerTrusted: function(chain, authType) {}, getAcceptedIssuers: function() { return []; } } }); var dummyTrustManager [TrustAllManager.$new()]; // 用我们自定义的TrustManager调用原始方法 return this.init(keyManagers, dummyTrustManager, secureRandom); }; console.log([*] Java层SSL Pinning绕过脚本加载完成。); });这个脚本做了三件事针对OkHttp的CertificatePinner直接让它的检查方法空跑。暴力枚举所有可能是X509TrustManager的实现类并Hook其核心验证方法让它们直接通过。更底层的HookSSLContext.init方法在SSL上下文初始化时就用一个我们自定义的、信任所有证书的TrustAllManager替换掉应用原本的TrustManager。实操心得方法2枚举Hook看似暴力但非常有效尤其对付那些自定义了TrustManager的应用。不过它可能会Hook到一些系统类或第三方库的类有时会导致应用不稳定。如果遇到崩溃可以尝试在onMatch回调中加入更精确的类名过滤或者优先使用方法1和方法3。3.2 Native层OpenSSLHook深入有些应用特别是对安全要求极高或核心通信逻辑在Native层实现的会在C/C代码中直接调用OpenSSL库进行证书验证。这时就需要在Native层进行Hook。这要求你对目标应用的Native库有所了解。首先需要找到关键的验证函数。常见的目标函数有SSL_CTX_set_cert_verify_callbackSSL_get_verify_resultX509_verify_cert我们可以使用Frida的Interceptor来附加到这些函数上。以下是一个HookSSL_CTX_set_cert_verify_callback的示例这个函数用于设置自定义的证书验证回调很多Pinning实现就在这里Java.perform(function() { // 首先确保目标so库已加载例如libssl.so var libssl Module.findBaseAddress(libssl.so); if (libssl) { console.log([] libssl.so base address: libssl); // 使用Frida的DebugSymbol来查找函数地址更准确 var SSL_CTX_set_cert_verify_callback DebugSymbol.getFunctionByName(SSL_CTX_set_cert_verify_callback); // 或者如果符号被剥离可能需要计算偏移量或使用模式搜索 // var SSL_CTX_set_cert_verify_callback libssl.add(0x12345); // 假设的偏移 if (SSL_CTX_set_cert_verify_callback) { console.log([] Found SSL_CTX_set_cert_verify_callback at: SSL_CTX_set_cert_verify_callback); Interceptor.attach(SSL_CTX_set_cert_verify_callback, { onEnter: function(args) { // args[0] 是 SSL_CTX* // args[1] 是回调函数指针 // args[2] 是用户自定义数据 console.log([] SSL_CTX_set_cert_verify_callback called.); console.log( Callback function address: args[1]); // 我们的目标替换这个回调函数或者修改其行为 // 但直接修改函数指针比较危险。更常见的做法是Hook应用设置的回调函数本身。 // 这里我们只是打印信息证明我们拦截到了。 }, onLeave: function(retval) { // 函数执行后 } }); } else { console.log([-] Could not find SSL_CTX_set_cert_verify_callback symbol.); } } else { console.log([-] libssl.so not loaded yet.); // 可以监听模块加载事件 Module.load(libssl.so, { onLoad: function(module) { console.log([] libssl.so loaded dynamically.); // 在这里执行Hook逻辑 } }); } });对于SSL_get_verify_result它的返回值是验证结果X509_V_OK表示成功。我们可以强制让它返回成功var SSL_get_verify_result DebugSymbol.getFunctionByName(SSL_get_verify_result); Interceptor.attach(SSL_get_verify_result, { onLeave: function(retval) { console.log([] SSL_get_verify_result original return: retval); // 强制返回验证成功 (X509_V_OK 通常是 0) retval.replace(0); } });注意事项Native Hook的难度和风险远高于Java层。首先函数符号可能被剥离Stripped你需要通过偏移量或特征码来定位函数。其次错误的Hook可能导致进程崩溃。务必在测试应用或沙盒环境中先行尝试。使用Module.findExportByName或DebugSymbol来查找函数是更安全的方式。4. 完整实战流程从注入到解密抓包理论说得再多不如一次完整的实战。假设我们的目标应用com.example.targetapp使用了OkHttp并启用了证书锁定。4.1 第一步启动环境与代理设置启动服务确保手机上的frida-server正在运行adb shell后ps | grep frida查看。配置代理将手机的Wi-Fi代理设置为你的电脑IP和Burp Suite监听端口如192.168.1.100:8080。安装CA证书用手机浏览器访问http://burp下载并安装Burp Suite的CA证书。在Android高版本7.0中用户安装的证书默认不被信任需要将证书移动到系统证书目录需要Root或者将应用配置为信任用户证书android:networkSecurityConfig。对于我们的测试最简单的方法是使用已Root的手机并将Burp的CA证书.der格式通过ADB推送到/system/etc/security/cacerts/目录并重命名为特定的哈希名可用openssl计算然后重启手机。这是关键一步否则系统层面不信任Burp的证书。配置Wireshark密钥日志在Burp Suite中找到设置TLS主密钥导出的选项不同版本位置可能不同通常在Proxy - SSL/TLS Pass Through附近或全局搜索TLS Master Secrets。启用并指定一个文件路径如C:\burp_secrets.log。然后在Wireshark中打开编辑 - 首选项 - Protocols - TLS在(Pre)-Master-Secret log filename中填入同一个文件路径。4.2 第二步编写并加载Frida脚本将前面章节的Java层绕过脚本保存为一个文件例如ssl_bypass.js。我们可以使用Frida的命令行工具来注入脚本并启动应用frida -U -f com.example.targetapp -l ssl_bypass.js --no-pause-U: 连接到USB设备。-f com.example.targetapp: 启动目标应用。-l ssl_bypass.js: 加载我们的脚本。--no-pause: 启动后立即继续执行不暂停。如果应用已经在运行我们可以附加到进程frida -U -n Target App Name -l ssl_bypass.js或者使用包名附加frida -U -n com.example.targetapp -l ssl_bypass.js当Frida命令行出现并且打印出类似[*] Starting Java SSL Pinning Bypass...和[*] Java层SSL Pinning绕过脚本加载完成。的日志时说明脚本注入成功。4.3 第三步触发网络请求并验证回到手机操作目标应用触发需要分析的HTTPS网络请求比如登录、刷新列表等。此时你应该能在Burp Suite的Proxy - HTTP history中看到清晰的HTTP/HTTPS请求和响应而不是一堆TLSv1.2 Application Data的密文。这是SSL Pinning已被绕过的直接证据。同时观察Frida的命令行输出你应该能看到类似[] Bypassing CertificatePinner.check for: sha256/...或[] Bypassing checkServerTrusted in: com.example.targetapp.CustomTrustManager的日志这告诉我们Hook具体在哪个环节起了作用。4.4 第四步Wireshark捕获与解密开始捕获打开Wireshark选择你电脑上连接手机同一局域网的网络接口通常是Wi-Fi网卡开始抓包。过滤流量为了清晰可以在过滤栏输入ip.addr 你的手机IP来只显示与手机相关的流量。触发请求再次在手机上操作应用触发网络活动。查看解密数据如果一切配置正确特别是TLS密钥日志文件路径正确且Burp成功导出了密钥Wireshark会自动解密TLS流量。对于已解密的TLS数据包在协议列你会看到TLSv1.2后面跟着具体的应用层协议如HTTP、JSON等。右键数据包 -Follow - TLS Stream你就能看到完整的、明文的HTTP会话内容。一个关键技巧Wireshark可能不会自动将HTTPS端口443的流量识别为HTTP。你可以通过右键数据包 -Decode As...- 将TCP port为443的流量解码为HTTP协议这样能更方便地查看HTTP请求头和响应头。5. 疑难杂症与进阶排查指南实战中不可能一帆风顺。下面是我踩过的一些坑以及对应的解决方案。5.1 常见问题速查表问题现象可能原因排查步骤与解决方案Frida无法连接设备1.frida-server未运行或版本不匹配。2. ADB连接不稳定。3. 手机未开启USB调试。1. adb shell ps注入脚本后应用闪退1. 脚本Hook了不稳定的类或方法。2. 应用有反调试/反Frida机制。3. Native Hook导致崩溃。1. 简化脚本逐一注释Hook点定位问题函数。2. 尝试先启动应用再附加-n而非启动时注入-f。3. 使用setImmediate包裹Java.perform或调整注入时机。4. 检查Native Hook的函数地址是否正确避免空指针。Burp Suite能抓到包但Wireshark全是TLS密文1. Wireshark未正确配置TLS密钥日志文件。2. Burp Suite未成功导出密钥。3. 抓包接口选错。1. 确认Wireshark的TLS设置中密钥文件路径正确且文件有更新查看修改时间。2. 确认Burp Suite的TLS Master Secrets导出功能已开启并且路径可写。3. 在Wireshark中过滤tls查看是否有Client Hello和Server Hello包确认抓包接口正确。应用网络请求失败无任何抓包1. SSL Pinning绕过不完全。2. 应用使用了证书双向验证mTLS。3. 应用使用了自定义Socket或非HTTP协议。1. 检查Frida脚本日志确认关键验证函数被Hook。2. 尝试更全面的Hook脚本如同时Hook Java和Native层。3. 使用tcpdump或Wireshark直接抓取原始流量看是否有TCP连接建立判断请求是否发出。高版本AndroidAndroid 7系统不信任用户安装的CA证书应用默认只信任系统证书。Root方案将Burp CA证书转换为.pem计算哈希重命名后推送到/system/etc/security/cacerts/重启。非Root方案修改应用APK在其AndroidManifest.xml中配置android:networkSecurityConfig指向一个允许用户证书的配置文件然后重打包签名。这属于另一个逆向工程范畴。5.2 对抗反Frida检测越来越多的应用会检测Frida的存在。常见检测手段包括检测端口默认Frida-server监听27042端口。应用会尝试连接或扫描这个端口。检测进程/文件检查/proc/self/maps或/proc/self/task/.../status中是否包含frida字符串或检查是否存在frida-server、gum-js-loop等特征进程。检测内存特征在内存中搜索Frida相关字符串或代码片段。应对策略修改Frida-server端口启动server时指定非默认端口./frida-server -l 0.0.0.0:8080。然后在电脑端连接时使用frida -H 192.168.1.5:8080 ...。重命名Frida-server将可执行文件改名为一个常见的系统服务名如/system/bin/surfaceflinger需谨慎避免冲突。使用Patch工具使用如objection基于Frida等工具它内置了一些反反调试功能或者寻找针对特定反Frida机制的Patch脚本。动态对抗编写Frida脚本反过来Hook应用的检测函数使其永远返回“未检测到”的结果。这需要逆向分析应用的检测逻辑。5.3 处理非标准网络库与协议不是所有应用都用OkHttp或HttpURLConnection。你可能遇到CronetChromium网络栈Google推出的高性能网络库常用于追求极致性能的应用。其SSL验证逻辑在Native层需要Hook Chromium/Blink相关的C代码难度较大。自定义TCP/UDP Socket应用直接使用Socket编程自己实现应用层协议。这种情况下SSL Pinning可能不存在但流量可能仍然是加密的自定义加密。此时Frida Hook的重点在于加解密函数本身需要逆向分析其算法。Wireshark抓到的将是原始的TCP/UDP流。WebSocket/QUIC对于WebSocket over TLS我们的方法同样适用因为底层仍是TLS。对于QUICHTTP/3其加密机制不同目前Frida和Wireshark对它的支持还在完善中可能需要更专门的工具和方法。面对这些情况核心思路不变定位负责连接建立和验证的关键函数然后用Frida去改变其行为。只是目标的函数名和位置变了需要更多的静态分析和动态探索。我个人在实际操作中的体会是SSL Pinning绕过就像一场攻防博弈。作为分析方我们的优势在于可以完全控制运行环境Root手机和动态修改进程内存。成功的秘诀在于耐心和细致从Java层最常见的点开始尝试逐步深入Native层仔细阅读Frida的日志和应用的崩溃信息善用Objection这样的自动化工具进行初步探索objection explore它能够自动执行很多常见的Hook给出有价值的线索。最后记得将你成功的脚本和配置保存下来它们会成为你个人工具库中宝贵的资产。这个实战过程不仅能帮你抓到想要的包更能让你深刻理解Android应用的安全机制和防御手段无论是对于安全研究还是开发加固都大有裨益。