当前位置: 首页 > news >正文

混淆与SSL Pinning双重防御下,如何通过动静结合技术实现HTTPS抓包

1. 项目概述:当混淆与SSL Pinning联手,抓包如何破局?

在移动应用安全测试和逆向分析领域,抓包是获取应用与服务器交互数据、分析业务逻辑、发现潜在漏洞的基础操作。然而,随着开发者安全意识的提升,两道“铁壁”正变得越来越常见:代码混淆SSL证书绑定。前者让逆向分析者难以阅读和理解核心逻辑,后者则直接阻断了我们使用像Charles、Fiddler这样的中间人代理工具进行抓包的可能。当这两者结合在一个应用上时,问题就变得尤为棘手——你费尽心思绕过了混淆,定位到了关键的SSL验证代码,却发现代码被混淆得面目全非,传统的Hook或Patch方法无从下手。这正是标题所描述的困境:一个经过混淆的App,其SSL Pinning机制难以被常规方法绕过。

这篇文章,我将从一个资深移动安全研究员的角度,分享一套面对“混淆+SSL Pinning”组合拳时的系统性解决方案。这不仅仅是介绍一两个工具,而是梳理出一条从环境准备、静态分析、动态调试到最终实现稳定抓包的完整路径。无论你是安全测试人员、逆向工程师,还是对移动应用通信机制感兴趣的研究者,这套方法都能为你提供清晰的思路和可实操的步骤。我们将从理解对手开始,逐步拆解防御,最终实现可控的中间人攻击。

2. 核心防御机制拆解:混淆与SSL Pinning如何协同工作

要解决问题,必须先透彻理解问题。混淆和SSL Pinning并非独立运作,它们的结合往往会产生“1+1>2”的防御效果。

2.1 代码混淆:不仅仅是“乱码”

代码混淆的目的并非让程序无法运行,而是增加人工阅读和逆向分析的难度。常见的混淆技术包括:

  • 标识符重命名:将有意义的类名、方法名、变量名改为a, b, c, aa, ab等无意义字符。这是最基础的混淆,主要影响静态分析的效率。
  • 控制流扁平化:将原本清晰的if-else、switch-case、循环结构,打散成一个巨大的switch语句或通过状态机跳转,使得执行流程难以追踪。
  • 字符串加密:将代码中的明文字符串(如API地址、密钥、证书哈希)在编译时加密存储,运行时动态解密。这直接保护了关键常量,包括用于SSL Pinning比对的证书指纹。
  • 指令替换和垃圾代码插入:用等价的、更复杂的指令序列替换简单指令,并插入永远不执行或无关紧要的代码块,干扰反编译器和分析者的视线。

在SSL Pinning的语境下,混淆的威力被放大。原本,我们可以通过搜索“X509TrustManager”、“checkServerTrusted”、“CertificatePinner”等关键词快速定位验证逻辑。但混淆后,这些类名和方法名可能变成了a.ab.b(),而关键的证书公钥或哈希值,可能被加密后分散在多个地方,通过复杂的解密函数在运行时还原。

2.2 SSL Pinning:信任的“白名单”

SSL Pinning的本质是放弃操作系统或浏览器的默认证书信任链,由应用程序自己决定信任哪些证书。具体实现方式主要有两种:

  1. 证书锁定:在App中预置服务器证书(或它的公钥)。在SSL握手时,将服务器返回的证书与预置的进行比较,完全一致才放行。
  2. 公钥锁定:在App中预置证书的公钥哈希(如SHA-256)。比较时只比对公钥指纹,即使证书到期续签,只要公钥不变,连接依然有效。这种方式更灵活,是目前的主流。

当中间人代理(如Charles)介入时,它会向客户端(App)出示自己生成的、由用户安装到设备信任库的CA证书。对于未做Pinning的App,它会信任系统信任库,从而接受Charles的证书。但对于做了Pinning的App,它会发现Charles证书的公钥指纹与自己预置的白名单不匹配,随即终止连接,抛出SSLHandshakeException等异常。

2.3 组合防御的挑战

混淆让定位Pinning逻辑和关键数据(证书哈希)变得困难;而强化的Pinning逻辑本身又增加了绕过的复杂度。你可能遇到的情况包括:

  • 搜索常见关键词一无所获。
  • 找到疑似验证方法,但调用链路因控制流混淆而支离破碎。
  • 找到了存储证书哈希的变量或资源,但值是加密的,且解密函数被混淆。
  • 应用采用了非标准或自定义的证书验证库,进一步增加了分析难度。

3. 解决方案总览:分层击破的策略

面对复合型防御,单一工具或方法很难奏效。我采用的是一套分层递进的策略,如下图所示(策略流程,非图表):

第一层:环境与工具准备。这是基础,确保拥有一个可控的、支持高级动态分析的环境。第二层:静态探查与信息收集。在不运行App的情况下,尽可能多地收集信息,为动态分析提供线索。第三层:动态调试与行为分析。让App运行起来,在关键节点进行干预和观察,这是破解混淆代码的核心。第四层:定制化绕过与持久化。根据分析结果,编写脚本或修改二进制文件,实现稳定、可重复的绕过。

这个流程不是线性的,而是一个循环。动态分析中获取的新信息,常常需要返回到静态分析中重新审视。接下来,我们深入每一层的具体操作。

4. 实操环境搭建与工具链选型

工欲善其事,必先利其器。一个高效的工具链能事半功倍。以下是我在实战中打磨出的组合,兼顾了Android和iOS平台。

4.1 核心抓包与代理工具

  • Charles / Fiddler:经典的GUI抓包工具,配置简单,可视化好,用于初步测试和常规HTTP/HTTPS流量捕获。它们是触发SSL Pinning警报的“试金石”。
  • Burp Suite:安全测试的瑞士军刀。除了抓包,其Repeater、Intruder、Decoder模块在分析混淆后的请求参数、尝试爆破时极其有用。Professional版的移动端辅助工具(如burp-mobile-assistant)有时能简化证书安装。
  • mitmproxy:命令行驱动的中间人代理,强大之处在于其可脚本化(Python)。你可以编写脚本,自动修改请求/响应,甚至模拟SSL Pinning验证,这对于自动化测试和复杂场景至关重要。

注意:确保电脑和测试手机在同一局域网,并在手机上正确安装并信任代理工具的CA证书。对于Android 7.0及以上版本,系统不再信任用户安装的CA证书,需要将CA证书移至系统证书目录,这通常需要Root权限。或者,可以修改App的网络安全配置文件(network_security_config.xml),但这本身就需要对App进行逆向修改。

4.2 静态分析工具

  • Android
    • apktool:反编译APK,获取smali汇编代码、资源文件和AndroidManifest.xmlsmali/baksmali是理解和修改DEX文件的基础。
    • jadx-gui/Ghidra:将DEX或APK反编译为可读性更高的Java代码。jadx-gui速度快,适合快速浏览和搜索。Ghidra是NSA开源的逆向框架,功能强大,支持反编译和深度分析,对于混淆严重的代码,其反编译器有时能产生比jadx更优的结果,并且支持脚本自动化。
    • Bytecode Viewer:集成了多种反编译器(CFR, FernFlower, Procyon),可以对比查看不同反编译器的输出,有助于理解混淆代码的真实意图。
  • iOS
    • otool/MachOView:查看Mach-O文件头、加载命令和段信息。
    • Hopper Disassembler/IDA Pro:静态反汇编和反编译利器。Hopper性价比高,IDA是行业标准,支持高级脚本和插件,对于分析经过LLVM混淆的代码非常有效。
    • class-dump:用于dump未加密或已脱壳的Objective-C二进制文件的头文件,快速获取类和方法信息。

4.3 动态分析与调试工具

  • Frida这是破解SSL Pinning的核武器。它是一个动态插桩框架,通过注入JavaScript代码到目标进程,可以实时Hook任何函数、修改内存、调用原生方法。完全不受代码混淆的影响,因为你Hook的是函数在内存中的地址或符号(如果符号未剥离)。objection是基于Frida的命令行工具,可以快速执行诸如android sslpinning disable之类的命令,但对于自定义或深度混淆的Pinning,仍需手写Frida脚本。
  • Xposed/LSPosed:Android平台另一个强大的Hook框架,需要在系统层面安装模块。它更稳定,但需要重启,且针对单个App的模块编写和调试流程比Frida稍复杂。在Frida不稳定的某些场景下(如某些加固环境),Xposed模块是备选方案。
  • LLDB/GDB:标准的原生代码调试器。对于iOS或Android的Native层(C/C++)实现的SSL Pinning,可能需要使用LLDB进行动态调试和断点。
  • adb logcat:Android设备日志输出。配置正确的过滤标签(如SSL),可以捕获App抛出的SSL相关异常信息,这是定位问题起点的关键。

4.4 辅助工具

  • justtrustme/SSLUnpinning:这是基于Xposed或Frida的“一键禁用SSL Pinning”的模块/脚本。它们对很多通用库(如OkHttp, Retrofit, Apache HttpClient)有效,但对于自定义或深度混淆的Pinning往往失效。因此,它们更适合作为初步测试工具,如果失效,就说明我们遇到了“硬骨头”,需要下面的手动方法。
  • frida-server:运行在目标设备上的Frida服务端,必须与电脑端的Frida版本匹配。
  • 模拟器/真机:推荐使用可Root的Android模拟器(如Android Studio AVD with Root)或越狱的iOS设备进行测试。生产环境测试需使用真机,并注意法律风险。

5. 静态分析:在混沌中寻找秩序

justtrustme失效后,我们就需要手动出击。第一步是从静态的二进制文件中寻找蛛丝马迹。

5.1 初步侦察与关键词搜索

即使代码被混淆,一些关键的系统API和字符串常量(如果未加密或加密较弱)仍有迹可循。

  1. 使用jadx-gui打开APK或IPA
  2. 进行全局搜索
    • 类/方法名:搜索X509TrustManager,checkServerTrusted,CertificatePinner,SSLSocketFactory,TrustManager。混淆后,类名可能变,但方法签名中的参数类型(如java.security.cert.X509Certificate[])和异常类型(CertificateException)相对难以完全改变。可以尝试搜索CertificateException
    • 字符串:搜索pin,ssl,cert,publickey,sha256,sha1。开发者可能在日志、配置或资源文件中留下线索。特别注意一些第三方库的标识字符串,如OkHttp
    • 网络配置文件:在资源文件中查找network_security_config.xml,这里可能定义了证书Pinning的原始配置。

5.2 分析证书/公钥存储位置

找到的证书或哈希值可能存储在:

  • Java/Kotlin代码中:以字节数组或字符串形式硬编码。
  • 资源文件:如.cer,.pem文件,或存储在rawassets目录下的文本文件。
  • Native层:通过JNI调用,在C/C++代码中存储和验证,这大大增加了分析难度。
  • 从服务器动态获取:首次启动或定期从服务器拉取Pinning配置,这需要先捕获一次未加密的通信(可能通过旧版本App或其它漏洞)。

jadx中,如果你看到一个长的、看似随机的字符串或字节数组,被传入一个名称可疑的方法(如a.a(String str)),那很可能就是加密后的证书数据。你需要找到调用它的地方,并分析那个解密方法。

5.3 理解控制流与定位入口点

对于控制流扁平化,不要试图在反编译的Java代码中完全理清。我们的目标是找到验证函数的入口点。可以关注:

  • 网络库的初始化代码:寻找OkHttpClient.Builder()Retrofit.Builder()等调用链,通常Pinning的配置会在这里添加。
  • 证书验证回调:查找实现了X509TrustManager接口的类,重写其checkServerTrusted方法。即使类名是a,只要它实现了这个接口,就是我们的目标。
  • 使用Ghidra进行辅助分析:将关键的DEX或SO文件导入Ghidra,利用其数据流分析和交叉引用功能,追踪证书数据的来源和使用位置,有时能发现反编译器遗漏的线索。

6. 动态分析与Frida实战:穿透混淆的利刃

静态分析给我们提供了可能的目标和线索,但真正的决战在动态运行时。Frida的强大在于它能无视混淆,直接与内存中的函数对话。

6.1 基础Frida脚本Hook

假设我们通过静态分析,怀疑一个名为com.example.obfuscated.a的类中的b方法负责证书验证。

// ssl_bypass.js Java.perform(function () { var targetClass = Java.use("com.example.obfuscated.a"); targetClass.b.overload('[Ljava.security.cert.X509Certificate;', 'java.lang.String').implementation = function(certs, authType) { console.log("[*] SSL Pinning check intercepted!"); console.log(" Auth Type: " + authType); for (var i = 0; i < certs.length; i++) { console.log(" Cert[" + i + "]: " + certs[i].getSubjectDN()); } // 关键:什么都不做,直接返回,相当于信任所有证书 // 或者,可以在这里打印出服务器证书的SHA256,与我们已知的比对 // var sha256 = computeSha256(certs[0].getEncoded()); // console.log(" Server Cert SHA256: " + sha256); }; });

使用命令frida -U -f com.example.app -l ssl_bypass.js --no-pause注入脚本。如果成功Hook并看到日志输出,说明找到了关键函数。通过打印出的证书信息,我们可以确认验证逻辑。

6.2 处理未知方法与枚举

如果静态分析毫无头绪,可以采用“盲Hook”策略,枚举所有可能的方法。

// enumerate_ssl.js Java.perform(function () { Java.enumerateLoadedClasses({ onMatch: function (className) { if (className.toLowerCase().indexOf("ssl") !== -1 || className.toLowerCase().indexOf("cert") !== -1 || className.toLowerCase().indexOf("trust") !== -1 || className.toLowerCase().indexOf("pin") !== -1) { console.log("[*] Found potential class: " + className); try { var clazz = Java.use(className); var methods = clazz.class.getDeclaredMethods(); for (var i in methods) { console.log(" Method: " + methods[i].getName()); } } catch (e) { // 忽略无法使用的类 } } }, onComplete: function () { console.log("[*] Class enumeration complete."); } }); });

这个脚本会列出所有类名中包含特定关键词的类及其方法,帮助你缩小目标范围。

6.3 Hook底层SSL库函数

对于Native层实现的Pinning,或者Java层最终调用到Native库(如OpenSSL)的情况,需要Hook C函数。

// hook_openssl.js Interceptor.attach(Module.findExportByName("libssl.so", "SSL_CTX_set_cert_verify_callback"), { onEnter: function (args) { console.log("[*] SSL_CTX_set_cert_verify_callback called!"); // 可以在这里替换回调函数,使其始终返回验证成功 } }); Interceptor.attach(Module.findExportByName("libcrypto.so", "X509_verify_cert"), { onEnter: function (args) { console.log("[*] X509_verify_cert called!"); }, onLeave: function (retval) { // 强制返回1(验证成功) retval.replace(1); } });

这需要你对OpenSSL API有一定的了解。使用Module.enumerateImports()Module.enumerateExports()可以探索目标模块的所有函数。

6.4 绕过自定义验证逻辑

有时,开发者会自己实现证书比对。我们的策略是找到比对结果所在,并强制让其返回“真”。

  1. 定位比对函数:在动态调试时,在可能进行比对的函数(如那些接收两个字符串或字节数组的函数)入口打印参数。
  2. Hook并修改返回值:一旦找到,确保其返回值始终为true0(表示成功)。
var resultClass = Java.use("com.example.validator.d"); resultClass.a.overload('[B', '[B').implementation = function (key1, key2) { console.log("[*] Custom comparator called. Forcing true."); return true; // 或返回 0,取决于函数定义 };

7. 高级技巧与问题排查实录

在实际操作中,你会遇到各种“坑”。以下是一些常见问题及我的解决心得。

7.1 反调试与反Hook检测

一些加固或高安全级别的App会检测Frida、Xposed等工具的存在。

  • 检测Frida:检查/proc/self/maps中是否包含frida-agent字符串,检查端口(默认27042)是否开放。
  • 绕过方法
    • 重命名Frida Server:将frida-server文件改名为其他名字启动。
    • 修改端口:使用frida-server -l 0.0.0.0:8080指定非默认端口。
    • 使用定制版或隐藏技术:如frida-server的某些修改版,或使用ptrace附加等更底层的方法。
    • Patch检测代码:使用Frida Hook掉这些检测函数,使其永远返回“未检测到”。

7.2 证书哈希动态获取或更新

如果证书哈希是从服务器动态获取的,你需要:

  1. 首先在未开启SSL Pinning的环境下(如旧版App、通过其他漏洞)捕获到这次通信。
  2. 或者,Hook网络请求函数,在App获取到Pinning配置后,在内存中将其修改为你可控的哈希值(即代理工具的证书哈希)。

7.3 双向认证与客户端证书

有些App不仅验证服务器,还要求客户端提供证书。这通常表现为在SSL握手时,服务器向客户端索要证书。

  • 现象:即使绕过服务器证书Pinning,连接依然失败,抓包工具显示Alert: certificate_required
  • 解决方案:你需要从App中提取客户端证书(通常是一个.p12.bks文件及其密码)。这通常需要逆向资源文件或解密相关代码。提取后,在Burp Suite或mitmproxy中配置客户端证书,才能完成完整握手。

7.4 Frida脚本注入失败或不稳定

  • Java.perform错误:确保脚本在Java.perform函数内执行,并且目标App的Java虚拟机已完全启动。使用-f参数在App启动时注入,或使用spawn模式。
  • 应用崩溃:可能是Hook了不正确的函数或参数类型不匹配。仔细核对方法签名(overload)。使用try-catch包裹可能不稳定的Hook操作。
  • 使用setImmediate:对于需要在App生命周期早期执行的Hook,可以将代码包裹在setImmediate中,确保Java环境就绪。

7.5 常见问题速查表

问题现象可能原因排查思路与解决方案
Charles/Burp显示SSL handshake failed1. 未安装/信任代理CA证书
2. SSL Pinning生效
1. 确认证书已正确安装到系统信任库(Android 7+需Root或修改App)。
2. 使用justtrustme测试,若无效则进入手动分析流程。
Frida连接被拒绝1.frida-server未运行
2. 设备未USB调试/网络连接
3. 端口被占用或防火墙阻止
1.adb shell检查进程,ps | grep frida
2.adb devices确认设备连接。
3. 尝试frida -U(USB)或frida -H IP:port(网络)。
注入Frida后App闪退1. App有反调试/反Frida检测
2. Frida脚本Hook了错误函数导致崩溃
1. 检查logcat崩溃日志,寻找检测线索。尝试隐藏Frida。
2. 注释掉部分Hook代码,二分法定位问题Hook点。
找到验证函数但Hook后仍失败1. 验证逻辑有多处
2. Native层还有验证
3. 比对值被加密
1. 扩大搜索和Hook范围。
2. 使用Frida Hooklibssl.so等Native库函数。
3. 动态跟踪解密函数,在解密后获取明文哈希。
抓包成功但请求体乱码/加密应用层额外加密1. 分析请求头,寻找加密算法线索(如encryptcipher)。
2. 搜索常量如AESDESRSA关键词。
3. Hook应用层的加密函数,获取密钥或直接输出解密后明文。

8. 案例实战:一个虚构混淆App的完整绕过流程

假设我们有一个目标App:com.hardapp.obfuscated,使用Charles抓包失败。

第一步:初步测试与信息收集

  1. 安装App,配置Charles代理,确认HTTPS请求失败,提示SSL错误。
  2. 在已Root的设备上安装JustTrustMeXposed模块,重启后测试,抓包依然失败。确认是“硬骨头”。

第二步:静态分析寻找线索

  1. 使用apktool d解包APK。
  2. 使用jadx-gui打开APK,全局搜索CertificateException。发现一个名为c.f.a.g的类,其a方法抛出了此异常。
  3. 查看该方法的调用者,追溯到com.hardapp.network.o类的b方法,其中调用了c.f.a.g.a(byte[] bArr, byte[] bArr2)
  4. 分析com.hardapp.network.o,发现它在初始化一个网络客户端。两个byte[]参数,一个来自服务器证书,另一个来自一个名为e的静态字段。e字段的值由一个名为d的类的c方法返回。
  5. 查看d.c(),发现它从一个资源文件raw/encrypted_pin读取字节,然后调用b.a(byte[])进行解密。b类看起来是AES解密。

第三步:动态验证与Hook

  1. 编写Frida脚本,Hook关键点:
    Java.perform(function(){ // Hook 解密函数,获取明文PIN var decryptClass = Java.use("com.hardapp.obfuscated.b"); decryptClass.a.overload('[B').implementation = function(encrypted){ var result = this.a(encrypted); // 调用原方法 console.log("[*] Decrypted PIN (hex): " + bytesToHex(result)); send(result); // 可以发送到Python端保存 return result; }; // Hook 比较函数,使其直接返回true var compareClass = Java.use("c.f.a.g"); compareClass.a.overload('[B', '[B').implementation = function(pin1, pin2){ console.log("[*] Bypassing certificate comparison."); return true; // 强制验证通过 }; }); function bytesToHex(bytes) { ... } // 辅助函数
  2. 运行脚本,启动App。在日志中看到解密的SHA256哈希值(例如A1B2C3...),并确认比较函数被绕过。
  3. 此时,Charles成功捕获到HTTPS流量。

第四步:持久化方案(可选)如果希望长期有效,可以修改APK:

  1. smali代码中找到c/f/a/g.smali中的a方法。
  2. 将其比较逻辑删除,直接让它return-void或者在最后设置一个成功的返回条件。
  3. 使用apktool b重新打包,签名并安装。

通过这个流程,我们完成了从探测、分析、动态破解到静态修补的全过程。核心思想是动静结合:静态分析提供地图,动态调试提供实时导航,而Frida则是我们穿越混淆迷雾的越野车。记住,每个App都是独特的,但解决问题的思路和工具链是相通的。保持耐心,细心观察,你总能找到那条通往流量的路。

http://www.jsqmd.com/news/1100911/

相关文章:

  • HDFS常用的命令(40个)
  • 别再手动删历史了!用BFG Repo-Cleaner一键清理Git提交里的密码和密钥(附Java环境配置)
  • ESP32做SPI从机,和STM32通信速度上不去?手把手教你排查DMA缓冲区与时钟同步问题
  • YOLOv10模型改进-卷积层改进-第13篇:YOLOv10改进策略【卷积层】| GhostNet幽灵卷积
  • 别再死记硬背了!用Python+NumPy手把手模拟量子叠加态与纠缠态(附代码)
  • ArcGIS 10.8 模型构建器:不用写代码,三步搞定批量要素转栅格(附工具分享)
  • Twitch掉落挖矿终极指南:如何零流量自动获取游戏奖励
  • 手把手教你配置台达DVP08TC-H3温控模块:从K型热电偶接线到PLC程序读取温度值
  • AI搜索时代的品牌生存法则:不被AI看见,就等于不被客户看见
  • 不到2块钱的国产RISC-V单片机CH32V003,用它做个USB转串口工具真香
  • DETR目标检测实战:从YOLO格式数据转换到模型训练与评估
  • 5分钟快速掌握LRCGET:批量歌词下载与智能同步音乐管理完整指南
  • 【HarmonyOS闯关习题】——从简单的页面开始
  • 微信消息防撤回技术解析:从网络协议分析到逆向工程实践
  • [Android] Tapet几何壁纸-解锁-算法无限生成壁纸,都是独一无二
  • 技术解析:APK Installer的Windows平台Android应用安装架构解密
  • AI 时代下的企业数字化:如何利用 API 接口进行 GEO(生成式引擎优化)与内容标准建设
  • Android自动化实战:AutoTask完整系统使用指南
  • 终极免费窗口强制调整工具:3步解决Windows顽固窗口大小问题
  • 计算机毕业设计之基于卷积神经网络的金融新闻情感分析系统设计与实现
  • 阿里云ACA大模型认证V2.4更新:从“会用”到“驾驭”
  • 告别串口线!用CH552单片机实现USB-CDC虚拟串口打印调试信息(Keil工程详解)
  • IT爱学堂-2026 尚硅谷Java全栈+Python智能体双语言技术栈与Agent项目落地教程
  • 某军事院校全栈式智能运维体系建设案例
  • 国茂 ZLYJ 减速机拆解更换齿轮配件完整实操流程正文
  • 3D点云处理实战:从核心算法到工程落地的系统性指南
  • 为什么92%的技术团队在关键项目中弃用ChatGPT改用Claude?——源自23家头部企业的生产环境日志分析(含真实错误率与响应延迟数据)
  • 如何快速掌握开源PCB查看器:硬件工程师的终极免费工具指南
  • 告别串口线!用CH552单片机实现USB CDC虚拟串口,5分钟搞定调试信息输出
  • Keycloak~infinispan中MergedUpdate中lifespanMs和maxIdleTimeMs