移动端 API 通信安全:证书绑定 (Pinning) 的多种绕过与中间人攻击 (MITM) 实战
前言
-
技术背景:在移动应用的攻防体系中,API 通信是信息窃取、逻辑篡改的核心攻击面。开发者为了防止常规的中间人攻击(MITM),引入了 SSL/TLS 证书绑定(Certificate Pinning) 作为一道重要的防线。它不再盲目信任设备系统的所有根证书,而是强制要求客户端只与预设的、特定的服务器证书进行通信。因此,绕过证书绑定是进行移动端应用安全测试、渗透测试和漏洞挖掘的前置关键步骤。没有绕过它,就无法抓取和分析绝大部分核心业务的 HTTPS 流量。
-
学习价值:掌握证书绑定的绕过技术,意味着你将能够:
- 解决核心抓包问题:对绝大多数启用了证书绑定的 App 进行 HTTPS 流量的解密、查看与修改。
- 深入分析业务逻辑:通过分析解密后的 API 请求,理解应用的核心业务流程、数据结构和安全缺陷。
- 挖掘高价值漏洞:在解密的流量中寻找越权、信息泄露、支付逻辑漏洞等严重安全问题。
- 构建自动化测试能力:编写脚本自动化地对 API 进行模糊测试、重放攻击和安全扫描。
-
使用场景:证书绑定绕过技术广泛应用于以下实际工作场景:
- 授权渗透测试:在客户授权下,对移动应用进行全面的安全评估。
- 应用安全审计:作为甲方安全工程师,审查自身产品的安全防护强度。
- 漏洞悬赏(Bug Bounty):在合法的漏洞悬赏平台上挖掘并报告安全漏洞。
- 应急响应与取证:分析恶意软件的网络行为或对安全事件进行溯源。
一、SSL/TLS 证书绑定是什么
1. 精确定义
SSL/TLS 证书绑定(SSL/TLS Certificate Pinning) 是一种客户端安全机制,它强制移动应用在与服务器建立 HTTPS 连接时,只信任一个或一组预先定义好的服务器证书或公钥。客户端会忽略设备操作系统信任证书库中的其他证书,直接比对服务器提供的证书是否与应用内“钉死”(Pin)的证书信息一致。如果不一致,即使该证书是由系统信任的 CA 签发的,客户端也会主动断开连接。
2. 一个通俗类比
想象一下,你去一个需要严格身份验证的秘密俱乐部。
-
常规 HTTPS:就像俱乐部门口的保安只检查你的身份证(由公安局这一“权威机构”颁发)是否真实有效。只要是真的身份证,不管你是张三还是李四,保安都认为你是合法公民,允许你进入。这里的“权威机构”就是操作系统的根证书颁发机构(CA)。
-
证书绑定 (Pinning):俱乐部老板给了你一张特制的、带有秘密标记的会员卡。现在,保安不仅要检查你的身份证,还必须核对你出示的会员卡是否与老板给他的样本完全一致。如果你只出示了身份证,或者出示了一张伪造的会员卡(即使它看起来很像真的),保安都会拒绝你进入。这张“特制会员卡”就是被“钉死”在应用里的服务器证书或公钥。
3. 实际用途
证书绑定的核心用途是抵御中间人攻击(Man-in-the-Middle, MITM)。在标准的 HTTPS 通信中,攻击者可以通过在用户设备上安装一个恶意的根证书(例如 Burp Suite 或 Charles 的证书),来解密和篡改所有 HTTPS 流量。因为这个恶意证书被系统“信任”了,所以应用也会信任它。而证书绑定则彻底打破了这种信任链,因为即使攻击者的证书被系统信任,它也无法匹配应用内预设的那个唯一证书,从而保护了通信内容不被窃取。
4. 技术本质说明
要理解证书绑定的本质,我们首先需要回顾标准的 TLS 握手流程,然后看 Pinning 是如何改变它的。
标准 TLS 握手流程
加入证书绑定后的流程
证书绑定在客户端验证服务器证书的环节加入了额外的校验逻辑。
这张图清晰地展示了,证书绑定通过在应用内部建立一个独立的、更高优先级的信任锚点,使得依赖于操作系统信任库的传统中间人攻击方法失效。这就是 SSL Pinning 原理。
二、环境准备
进行证书绑定绕过实战,需要一套标准的移动安全测试环境。
警告:所有工具和操作仅限在授权的测试环境中使用。未经授权的测试属于违法行为。
| 工具/组件 | 版本 (建议) | 下载方式/说明 | 核心配置命令/步骤 |
|---|---|---|---|
| Frida | 16.x 或更高 | pip install frida-tools |
frida --version 验证安装 |
| frida-server | 与 Frida 匹配 | 从 Frida GitHub Releases 下载对应 CPU 架构的版本 (如 arm64-v8a) |
adb push frida-server /data/local/tmp/adb shell "chmod 755 /data/local/tmp/frida-server" |
| Objection | 1.11.0 或更高 | pip install objection |
objection --version 验证安装 |
| Burp Suite | Professional 2023.x+ | PortSwigger 官网 | 设置代理监听 127.0.0.1:8080,并导出 CA 证书 |
| Android 设备 | Android 8.0 - 12.0 | 推荐使用已 Root 的物理机或模拟器 (如 Genymotion, MuMu) | 1. 安装 Magisk 获取 Root 权限。 2. 安装 Magisk 模块 MagiskTrustUserCerts 自动将用户证书移至系统区。 |
| ADB 工具 | 最新版 | Android SDK Platform Tools | adb devices 确认设备连接 |
可运行环境启动流程
这是一个典型的启动命令序列,用于准备好进行 Hook 操作。
# ---------------------------------------------------
# 警告: 仅限在授权的测试环境中使用此脚本
# ---------------------------------------------------
# 步骤 1: 启动 frida-server (在新的终端窗口执行)
# 确保你已经通过 adb 连接了你的安卓设备
echo "[+] 正在启动 frida-server..."
adb shell "su -c '/data/local/tmp/frida-server'"
# 步骤 2: 验证 frida-server 是否运行成功 (在另一个终端窗口)
# 这条命令应该能列出设备上正在运行的进程
echo "[+] 正在检查 frida 连接状态..."
frida-ps -Ua
# 如果能看到应用列表,说明环境已准备就绪
echo "[+] 环境准备完毕,可以开始使用 Objection 或 Frida 脚本进行 Hook。"
三、核心实战:使用 Objection 一键绕过
对于许多常见的证书绑定实现,Objection 提供了高度自动化的绕过方案,是新人入门和快速测试的首选工具。它集成了大量现成的 Frida 脚本。
1. 攻击步骤
步骤 1:启动目标应用并附加 Objection
首先,确保目标应用已经安装在你的测试设备上。然后,找到应用的包名(例如 com.example.android)。
# ---------------------------------------------------
# 警告: 仅限在授权的测试环境中使用此脚本
# ---------------------------------------------------
#
# 参数:
# -g, --gadget: 指定要附加的应用包名
#
# 示例:
# objection -g com.example.android explore
# 启动 Objection 并附加到目标应用
# 请将 com.example.android 替换为你的目标应用包名
objection -g com.example.android explore
目的:此命令会启动应用(如果尚未运行),并将 Frida 的 gadget 注入到应用进程中,建立一个交互式的 Hooking 会话。
输出结果:
...
Using gadget 'com.example.android'
Agent injected and responds ok!
_ _ _ _
___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_| _| _| | . | |
|___|___|_|___|___|_| |_|___|_|_|
|___|(object)inject(ion) v1.11.0
...
com.example.android on (Android: 11) [usb] #
出现 ... # 提示符,表示已成功进入 Objection 的交互环境。
步骤 2:执行 SSL Pinning 绕过命令
在 Objection 的交互环境中,执行其内置的绕过插件。
# 在 Objection 的交互式 shell 中输入
android sslpinning disable
目的:该命令会尝试在当前应用进程中搜索并 Hook 常见的 SSL Pinning 实现代码,例如 OkHttp3, TrustManager, Cronet 等,并将其验证逻辑禁用。
请求/响应/输出结果:
com.example.android on (Android: 11) [usb] # android sslpinning disable
(agent) Custom TrustManager found, attempting to patch...
(agent) Found okhttp3.CertificatePinner.Builder, patching...
(agent) Found app.security.cert.Pinning, patching...
(agent) SSL Pinning disabled.
Job: 2f3a7b1c-d8e9-4f0a-8b9c-5d6e4f1a2b3c - Running
看到 SSL Pinning disabled 和 Running 的状态,意味着绕过脚本已经成功加载并持续运行。
步骤 3:验证绕过效果
此时,保持 Objection 和 frida-server 运行。在你的电脑上打开 Burp Suite,并确保手机的 WIFI 代理已正确设置为 Burp Suite 的监听地址和端口。
重新操作 App,触发网络请求。现在,你应该可以在 Burp Suite 的 HTTP history 标签页中看到之前无法抓取到的 HTTPS 流量了。
(这是一个示例图片链接,实际应替换为真实截图)
2. 自动化脚本示例
虽然 Objection 的交互模式很方便,但在批量测试或需要快速验证时,编写一个自动化脚本会更高效。
以下是一个使用 Python subprocess 模块调用 Objection 命令行的自动化 SSL Pinning 绕过脚本。
# -*- coding: utf-8 -*-
#
# 文件名: auto_unpin.py
# 描述: 自动化使用 Objection 绕过 SSL Pinning 的脚本
#
# 警告: 本脚本仅可用于经授权的渗透测试和安全研究。
# 未经授权使用可能违反法律法规。
#
import subprocess
import time
import sys
def run_objection_unpin(package_name: str, timeout: int = 10):
"""
使用 subprocess 启动 Objection 并执行 SSL Pinning 绕过。
:param package_name: 目标应用的包名 (例如 'com.example.android')
:param timeout: 等待 Objection 启动并注入的超时时间 (秒)
"""
if not package_name:
print("[!] 错误: 请提供目标应用的包名。")
print(" 用法: python auto_unpin.py <package_name>")
return
print(f"[+] 目标应用: {package_name}")
print("[+] 准备启动 Objection 并执行 'android sslpinning disable'...")
# 构建 Objection 命令
# 使用 --startup-command 来在启动后立即执行命令
command = [
'objection',
'-g',
package_name,
'explore',
'--startup-command',
'android sslpinning disable'
]
try:
# 使用 Popen 启动一个子进程,这样主脚本不会被阻塞
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8')
print(f"[+] Objection 进程已启动 (PID: {process.pid})。")
print(f"[+] 等待 {timeout} 秒让绕过脚本生效...")
print("[+] 现在可以打开 Burp Suite 并操作 App 进行抓包了。")
print("[+] 按 Ctrl+C 退出此脚本并终止 Objection 进程。")
# 持续打印 Objection 的输出,方便调试
while True:
line = process.stdout.readline()
if not line and process.poll() is not None:
break
if line:
print(f"[Objection]: {line.strip()}")
time.sleep(0.1)
except FileNotFoundError:
print("[!] 错误: 'objection' 命令未找到。")
print(" 请确保你已经通过 'pip install objection' 安装了它,并且它在你的系统 PATH 中。")
except KeyboardInterrupt:
print("\n[+] 收到退出信号,正在终止 Objection 进程...")
process.terminate()
# 等待一小段时间确保进程被终止
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
print("[!] 进程未能正常终止,强制杀死。")
process.kill()
print("[+] 脚本已退出。")
except Exception as e:
print(f"[!] 发生未知错误: {e}")
if 'process' in locals() and process.poll() is None:
process.kill()
if __name__ == '__main__':
# 从命令行参数获取包名
target_package = sys.argv[1] if len(sys.argv) > 1 else ""
run_objection_unpin(target_package)
使用方法:
- 保存以上代码为
auto_unpin.py。 - 在终端中运行:
python auto_unpin.py com.target.app.package。 - 脚本会自动启动 Objection 并执行绕过命令,之后你就可以直接去 Burp Suite 查看流量了。
四、进阶技巧:手动 Hook 与通用脚本
Objection 并非万能。当遇到以下情况时,你需要更深入的 Frida Hooking 技巧:
- 应用使用了非常规的、自定义的证书校验逻辑。
- 应用集成了 C/C++ 层的网络库(如
libcurl)进行通信和校验。 - 应用具备反调试、反 Hook 机制,导致 Objection 注入失败。
这时,我们需要编写自己的 Frida 脚本,实现更精准的 SSL Pinning 绕过教程。
1. 常见错误与解决方案
-
错误:
android sslpinning disable后依然无法抓包。- 原因分析: 很可能是应用使用了 Objection 脚本库未覆盖的 pinning 实现。
- 解决方案: 反编译 APK(使用
jadx-gui),搜索关键词如CertificatePinner,TrustManager,verify,checkServerTrusted,定位自定义的校验代码,然后针对性地编写 Frida 脚本进行 Hook。
-
错误: Objection 注入时应用闪退。
- 原因分析: 应用可能存在反 Frida 检测。常见的检测方式是扫描内存中是否存在
frida-agent或gum-js-loop等特征字符串。 - 解决方案:
- 使用
frida-ps -Ua查看应用是否在启动瞬间就消失,确认是启动时检测。 - 尝试使用
spawn模式而非attach模式启动 Frida (frida -U -f com.package.name --no-pause -l script.js),在应用代码执行前注入。 - 寻找并使用“魔改版”或“反反调试版”的 frida-server,这些版本修改了默认的特征。
- 使用
- 原因分析: 应用可能存在反 Frida 检测。常见的检测方式是扫描内存中是否存在
2. Frida 通用绕过脚本实战
社区大神们已经总结了覆盖绝大多数 Java/Kotlin 层证书绑定实现的通用脚本。frida-multiple-unpinning 是其中一个非常经典和有效的例子。
以下是一个精简和注释版的通用脚本,解释了其原理和使用方法。
/*
* 文件名: universal-unpin.js
* 描述: 一个 Frida 通用 SSL Pinning 绕过脚本,覆盖多种常见框架。
*
* 警告: 本脚本仅可用于经授权的渗透测试和安全研究。
* 未经授权使用可能违反法律法规。
*
* 使用方法:
* frida -U -f com.target.app.package -l universal-unpin.js --no-pause
*/
console.log("[+] 开始执行通用 SSL Pinning 绕过脚本...");
Java.perform(function() {
console.log("[+] 已进入 Java VM 环境。");
// --- 绕过 TrustManager (Android 7.0+ 网络安全配置) ---
// 目标: 覆盖 checkServerTrusted 方法,使其直接返回,不抛出异常。
try {
var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl');
TrustManagerImpl.verifyChain.implementation = function(untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) {
console.log("[*] Hooked TrustManagerImpl.verifyChain - > 绕过证书链验证");
// 直接返回最受信任的证书链,欺骗调用者
return trustAnchorChain;
};
} catch (e) {
console.log("[!] 绕过 TrustManagerImpl 失败: " + e.message);
}
// --- 绕过 OkHttp3 (v3.x - v4.x) ---
// 目标: 覆盖 CertificatePinner 的 check 方法。
try {
var CertificatePinner = Java.use('okhttp3.CertificatePinner');
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function(hostname, peerCertificates) {
console.log("[*] Hooked OkHttp3 CertificatePinner.check -> 主机名: " + hostname);
// 直接返回,不执行任何检查
return;
};
console.log("[+] OkHttp3 CertificatePinner.check 已 Hook。");
} catch (e) {
console.log("[!] 绕过 OkHttp3 v3.x+ 失败: " + e.message);
}
// --- 绕过 TrustKit ---
// 目标: 针对流行的 TrustKit 库。
try {
var TrustKit = Java.use("com.datatheorem.android.trustkit.pinning.PinningTrustManager");
TrustKit.checkServerTrusted.implementation = function(chain, authType) {
console.log("[*] Hooked TrustKit.checkServerTrusted -> 绕过验证");
// TrustKit 希望我们什么都不做
};
} catch (e) {
console.log("[!] 绕过 TrustKit 失败: " + e.message);
}
// --- 通用 SSLContext 绕过 (捕获漏网之鱼) ---
// 目标: 很多自定义实现最终会调用 SSLContext.init,我们替换掉它的 TrustManager。
try {
var SSLContext = Java.use('javax.net.ssl.SSLContext');
var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
// 创建一个永远信任所有证书的 TrustManager
var TrustAllManager = Java.registerClass({
name: 'com.example.TrustAllManager',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function(chain, authType) {},
checkServerTrusted: function(chain, authType) {},
getAcceptedIssuers: function() {
return [];
}
}
});
var trustAllManagers = [TrustAllManager.$new()];
SSLContext.init.implementation = function(keyManagers, trustManagers, secureRandom) {
console.log("[*] Hooked SSLContext.init -> 替换为自定义的 TrustAllManager");
// 无论原始的 TrustManager 是什么,都用我们自己的“万能”TrustManager 替换掉
this.init(keyManagers, trustAllManagers, secureRandom);
};
} catch (e) {
console.log("[!] 绕过 SSLContext.init 失败: " + e.message);
}
console.log("[+] 通用 SSL Pinning 绕过脚本加载完毕。");
});
使用此脚本:
# ---------------------------------------------------
# 警告: 仅限在授权的测试环境中使用此脚本
# ---------------------------------------------------
#
# 参数:
# -U: 连接到 USB 设备
# -f: 指定包名并以 spawn 模式启动 (在应用启动前注入)
# -l: 加载指定的 Frida 脚本
# --no-pause: 注入后立即恢复应用主线程
#
# 示例:
# frida -U -f com.example.android -l universal-unpin.js --no-pause
# 请将 com.example.android 替换为你的目标应用包名
frida -U -f com.example.android -l universal-unpin.js --no-pause
3. 实战经验总结
- 先易后难:始终先尝试 Objection。如果失败,再动用通用脚本。如果还失败,再反编译 APK 进行定向分析。
- 关注日志:Frida 脚本中的
console.log输出是调试的关键。仔细观察哪个 Hook 成功了,哪个失败了,可以帮你定位问题。 - 版本差异:注意库的版本。OkHttp v2 和 v3 的 pinning 实现类名和方法都不同。脚本需要适配。
- 原生层 Hook:如果 Java 层绕过全部失效,流量依然不通,就要怀疑是 C/C++ 层实现的 Pinning(例如,游戏引擎、金融类 App)。这时需要 Hook
libssl.so中的SSL_CTX_set_custom_verify或SSL_get_verify_result等函数,这属于更高级的主题。
五、注意事项与防御
作为一名合格的安全工程师,不仅要懂得攻击,更要精通防御。
1. 错误写法 vs 正确写法 (开发侧)
错误/脆弱的实现:
// 错误示例: 仅依赖默认的 TrustManager
// 这种写法完全没有 Pinning,容易被中间人攻击。
URL url = new URL("https://api.example.com/");
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
urlConnection.connect();
正确/健壮的实现 (以 OkHttp 为例):
// 正确示例: 使用 OkHttp 的 CertificatePinner
//
// 1. 获取服务器证书的公钥哈希 (SPKI Hash)
// openssl s_client -connect api.example.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
// 输出: sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
String hostname = "api.example.com";
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // 替换为你的真实哈希
.add(hostname, "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // 备份证书哈希
.build();
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build();
Request request = new Request.Builder()
.url("https://" + hostname)
.build();
// 使用此 client 发起的请求将强制执行 Pinning
client.newCall(request).execute();
2. 风险提示
- 证书过期风险:如果 Pinning 的是证书本身,一旦证书过期更换,所有旧版 App 将无法联网。最佳实践是 Pin 公钥,因为更换证书时可以保持公钥不变。
- 备份 Pin 很重要:务必至少 Pin 两个或以上的公钥哈希。一个是当前正在使用的,另一个是备用证书的。这样在主证书出现问题(如泄露、吊销)时,可以平滑切换到备用证书,而不需要强制用户更新 App。
- 动态 Pinning 的复杂性:一些方案尝试从服务器动态获取最新的 Pinning 列表,但这引入了“先有鸡还是先有蛋”的问题,首次获取列表的请求本身可能被劫持。实现必须非常谨慎。
3. 开发侧安全代码范式
- 使用成熟的第三方库:如 OkHttp、TrustKit,它们提供了健壮且易于使用的 Pinning API。
- 不要在代码中硬编码证书文件:将证书哈希作为字符串配置在代码或资源文件中更安全、更灵活。
- 实现双向校验:除了客户端校验服务器,有条件的服务器也应该实现客户端证书校验(mTLS),进一步提升安全性。
- 代码混淆与加固:使用 ProGuard/R8 对实现 Pinning 的代码进行深度混淆,增加逆向分析和 Hook 的难度。使用商业加固产品可以进一步保护原生库和关键校验逻辑。
4. 运维侧加固方案
- 证书透明度 (Certificate Transparency):监控是否有未经授权的证书为你的域名签发。
- 严格的 HSTS 策略:部署 HTTP Strict Transport Security (HSTS) 头部,强制浏览器和部分客户端只能通过 HTTPS 访问。
- 定期更换证书:即使公钥不变,也应定期轮换证书,缩短攻击窗口。
5. 日志检测线索 (如何发现自己被攻击)
- 客户端遥测:在 Pinning 验证失败时,向你的监控服务器上报一条遥测日志。日志应包含设备信息、网络环境、以及对方提供的证书信息。
- 服务器端分析:如果在短时间内,来自同一 IP 或同一用户的大量请求都出现了 TLS 握手失败,且失败阶段在证书验证环节,这可能是一个攻击信号。
- 异常流量模式:正常用户的 API 请求模式是多样的,而攻击者在抓包分析时,可能会频繁重放同一个请求。监控 API 调用频率和模式的异常可以作为辅助检测手段。
总结
- 核心知识:SSL Pinning 是通过在客户端预置服务器证书信息,来绕过系统信任链,从而抵御中间人攻击的一种关键防御技术。
- 使用场景:绕过 Pinning 是移动端渗透测试、安全审计和漏洞挖掘的必备前置技能,是解密和分析 HTTPS 流量的基础。
- 防御要点:有效的防御需要 Pin 公钥而非证书、设置备份 Pin、使用成熟库实现,并配合代码混淆与应用加固来提升攻击门槛。
- 知识体系连接:掌握 Pinning 绕过是通往更高级移动安全主题的桥梁,如 API 逻辑漏洞挖掘、协议分析、SO 层 Hook、反调试与反混淆等。
- 进阶方向:深入研究 C/C++ 原生层网络库的 Hooking 技术、Frida 的 Stalker 等高级功能用于反混淆,以及学习 Swift/Objective-C 在 iOS 平台上的 Pinning 绕过方法。
自检清单
- 是否说明技术价值?
- 是否给出学习目标?
- 是否有 Mermaid 核心机制图?
- 是否有可运行代码?
- 是否有防御示例?
- 是否连接知识体系?
- 是否避免模糊术语?
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)