1. 项目概述:当SSL握手成为拦路虎
在移动安全测试和逆向工程领域,我们经常遇到一个棘手的场景:目标应用使用了双向SSL/TLS证书认证。这意味着,除了客户端需要验证服务器的证书(单向认证),服务器也会要求客户端出示一个受信任的证书。这就像你去一个高级会所,不仅要检查会所的会员资质(服务器证书),对方还要你出示一张特定的VIP卡(客户端证书)。没有这张卡,门都进不去。
传统的抓包工具(如Burp Suite、Charles)在单向认证时,可以通过在设备上安装一个由工具签发的根证书来充当“中间人”,解密HTTPS流量。但在双向认证面前,这套方法就失效了,因为服务器会拒绝没有携带正确客户端证书的连接请求。这时候,很多人的第一反应是去逆向APK,寻找硬编码的证书和私钥。但更常见的情况是,证书和私钥并非静态存储,而是由程序在运行时动态构建或从安全元件中获取。
“Frida Hook进阶:动态修改SSLContext实现双向证书绕过”这个项目,就是针对这种动态、运行时构建SSL上下文的场景。它的核心思路不是去“偷”那张VIP卡,而是直接“欺骗”会所的安检系统,让它以为我们已经出示了正确的卡,或者干脆让它放弃检查。通过Frida这个强大的动态插桩工具,我们Hook住应用创建SSLContext(SSL上下文)的关键方法,在运行时修改其行为,注入我们自己的信任管理器或密钥管理器,从而绕过客户端的证书校验,实现流量的拦截与解密。
这个方法的价值在于其通用性和动态性。它不依赖于特定的证书存储方式,无论是从文件读取、从网络获取,还是通过JNI从原生代码生成,只要最终在Java/Android层构建了
javax.net.ssl.SSLContext
或
okhttp3.OkHttpClient
等对象,我们就有机会介入并修改。对于安全研究人员、渗透测试工程师和逆向爱好者来说,掌握这项技术意味着能攻克更多加固严密、通信安全的应用。
2. 核心原理与方案选型
要理解如何绕过,首先得明白双向认证在Android(Java)中是如何建立的。整个过程的核心是
SSLContext
类。
2.1 SSLContext与双向认证流程
SSLContext
是一个工厂类,用于创建
SSLSocketFactory
和
SSLServerSocketFactory
。在配置双向认证时,关键是通过其
init
方法传入两个管理器:
- TrustManager[] :信任管理器,决定是否信任远程服务器的证书链(验证服务器)。
- KeyManager[] :密钥管理器,负责提供客户端的证书和私钥(向服务器证明自己)。
一个典型的双向认证初始化代码如下(以Java标准库为例):
// 1. 加载客户端证书和私钥
KeyStore clientKeyStore = KeyStore.getInstance("PKCS12");
clientKeyStore.load(new FileInputStream("client.p12"), "password".toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(clientKeyStore, "password".toCharArray());
// 2. 加载受信任的CA证书(用于验证服务器)
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(new FileInputStream("truststore.jks"), "trustpass".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
// 3. 初始化SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
// 4. 应用于HTTP客户端,例如OkHttp
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager)tmf.getTrustManagers()[0])
.build();
服务器验证客户端的逻辑,就藏在
KeyManager
提供的证书里。如果我们的Hook能替换掉这个
KeyManager
,或者让
SSLContext.init
方法接受一个空的
KeyManager
,那么双向认证的客户端部分就被绕过了。
2.2 为什么选择Hook SSLContext.init?
面对双向认证,我们有几种常见的思路:
- 静态逆向 :反编译APK,寻找证书文件(.p12, .bks)或硬编码的证书字节码和密钥。这种方法最直接,但遇到代码混淆、证书动态下载或来自SO库时,难度剧增。
-
Hook 网络库
:针对特定网络库(如OkHttp的
CertificatePinner)进行Hook。这种方法精准,但通用性差,换一个库或自定义实现就失效了。 -
Hook SSLContext.init
:这是相对通用的一层。无论应用使用何种网络库(HttpURLConnection, OkHttp, Retrofit),无论证书来源多么隐蔽,只要它最终要在Java层建立安全的SSL连接,几乎必然要调用
SSLContext.getInstance()和sslContext.init()。在此处拦截,等于抓住了“七寸”。
方案优势 :
-
通用性强
:覆盖标准Java
HttpsURLConnection、Apache HttpClient、OkHttp等多种客户端。 - 位于合适抽象层 :比Hook底层Socket更简单,比Hook高层应用逻辑更通用。
- 动态生效 :无需修改应用安装包,运行时注入,适合快速测试和分析。
我们的核心目标
:编写Frida脚本,Hook
javax.net.ssl.SSLContext
的
init
方法,将其传入的
KeyManager[]
参数替换为我们自定义的、能提供合法证书(或直接置空)的
KeyManager
,从而骗过服务器的验证。
注意 :此方法主要目的是 安全测试与授权分析 。在实际测试中,请确保你拥有测试该应用的法律权限,遵守相关法律法规。绕过安全机制仅用于评估其强度,而非用于非法目的。
3. 环境准备与Frida基础
工欲善其事,必先利其器。在开始编写复杂的Hook脚本之前,确保你的基础环境是稳固的。
3.1 Frida环境搭建
你需要准备两部分:桌面端的Frida工具和运行在目标设备上的Frida-server。
-
安装桌面端Frida :
pip install frida-tools安装后,可以使用
frida --version和frida-ps --version验证。 -
部署Frida-server到设备 :
-
根据你的目标设备架构(
arm,arm64,x86,x86_64)从Frida的GitHub Releases页面下载对应的frida-server二进制文件。 - 将设备通过USB连接电脑,并开启USB调试模式。
- 使用ADB将frida-server推送到设备,赋予执行权限,并在后台运行:
adb push frida-server /data/local/tmp/ adb shell "chmod 755 /data/local/tmp/frida-server" adb shell "/data/local/tmp/frida-server &"-
验证连接:在电脑上执行
frida-ps -U,应能列出设备上的进程列表。
-
根据你的目标设备架构(
3.2 目标应用与测试环境
选择一个用于测试的应用。理想的目标是已知使用了双向认证的应用(例如一些银行的Demo应用或自己编写的测试应用)。如果没有,可以自己编写一个简单的Android应用,使用OkHttp配置双向认证的客户端。
关键准备步骤 :
- 启动应用 :在设备上启动目标应用。
-
附加进程
:使用Frida附加到目标进程。你可以先使用
frida -U -f com.example.targetapp来启动并附加,或者附加到已运行的进程frida -U com.example.targetapp。 -
基础Hook测试
:编写一个简单的脚本,测试是否能Hook到目标类和方法。例如,Hook
java.lang.String的构造函数来验证环境。
通过// test_hook.js Java.perform(function() { var StringClass = Java.use("java.lang.String"); StringClass.$init.overload('java.lang.String').implementation = function(str) { console.log("String created: " + str); return this.$init(str); }; });frida -U -l test_hook.js com.example.targetapp运行,观察日志输出。
3.3 定位关键方法
在Hook
SSLContext.init
之前,我们需要确认应用确实使用了它,并了解其具体签名。可以使用Frida的枚举功能来辅助定位。
脚本:枚举SSLContext的所有方法
Java.perform(function() {
var SSLContext = Java.use("javax.net.ssl.SSLContext");
console.log("=== SSLContext Methods ===");
var methods = SSLContext.class.getDeclaredMethods();
methods.forEach(function(method) {
console.log(method.toString());
});
});
运行这个脚本,你会看到
SSLContext
的所有方法,其中应该包含
init(KeyManager[], TrustManager[], SecureRandom)
。记下它的完整签名,这在后续重载(overload)选择时至关重要。
实操心得 :在实际测试中,你可能会遇到应用使用Android系统或自定义的
SSLContext子类。因此,更稳妥的做法是先HookSSLContext.getInstance()方法,打印出其返回的具体类名,然后再去Hook那个具体类的init方法。这样可以避免Hook不到的情况。
4. Frida Hook脚本核心实现
这是本项目的核心部分。我们将一步步构建一个功能完整的Hook脚本。
4.1 Hook SSLContext.init 方法
我们的首要目标是拦截
init
方法,并控制其参数。
init
方法有多个重载,最常见的是三个参数的那个。
Java.perform(function() {
// 使用Java.use获取SSLContext类的引用
var SSLContext = Java.use("javax.net.ssl.SSLContext");
// 找到三个参数的重载:init(KeyManager[], TrustManager[], SecureRandom)
SSLContext.init.overload('[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom').implementation = function(keyManagers, trustManagers, secureRandom) {
console.log("\n[+] SSLContext.init Hooked!");
// 打印原始传入的参数信息
console.log(" Original KeyManagers length: " + (keyManagers ? keyManagers.length : 0));
console.log(" Original TrustManagers length: " + (trustManagers ? trustManagers.length : 0));
// 核心操作:替换KeyManagers
// 方案1:直接置空,适用于某些不严格校验的服务器(可能失败)
// var newKeyManagers = [];
// 方案2:提供一个“傀儡”KeyManager,能生成一个自签名证书(更通用)
// 我们需要先实现一个自定义的KeyManager
console.log(" Attempting to replace KeyManagers with custom ones...");
// 调用原方法,但传入修改后的参数
// this.init(newKeyManagers, trustManagers, secureRandom);
// 注意:这里我们先注释掉实际调用,接下来实现自定义KeyManager
};
});
现在,脚本能拦截到调用并打印信息,但还没有实际修改功能。直接置空
KeyManager
数组在某些情况下会导致SSL握手失败(服务器要求必须有证书)。因此,我们需要实现方案二:提供一个能动态生成或返回有效证书的自定义
KeyManager
。
4.2 实现自定义的X509KeyManager
我们需要在Frida的JavaScript环境中,用Java的接口实现一个
X509KeyManager
。这需要用到
Java.registerClass
方法。
// 在Java.perform内部定义
// 1. 首先,获取需要用到的Java类引用
var X509KeyManager = Java.use("javax.net.ssl.X509KeyManager");
var X509ExtendedKeyManager = null;
try {
X509ExtendedKeyManager = Java.use("javax.net.ssl.X509ExtendedKeyManager"); // Android中常用的是这个扩展类
} catch(e) {
console.log("X509ExtendedKeyManager not found, using X509KeyManager");
}
var KeyStore = Java.use("java.security.KeyStore");
var KeyFactory = Java.use("java.security.KeyFactory");
var CertificateFactory = Java.use("java.security.cert.CertificateFactory");
var ByteArrayInputStream = Java.use("java.io.ByteArrayInputStream");
// 2. 创建一个自定义的KeyManager类
var MyKeyManager = null;
if (X509ExtendedKeyManager) {
// 实现更通用的X509ExtendedKeyManager
MyKeyManager = Java.registerClass({
name: 'com.example.frida.MyX509ExtendedKeyManager',
implements: [X509ExtendedKeyManager],
methods: {
// 必须实现的方法
chooseClientAlias: function(keyType, issuers, socket) {
console.log("[MyKeyManager] chooseClientAlias called for keyType: " + JSON.stringify(keyType));
// 返回一个别名,这里我们随便返回一个,例如 "frida-client"
return "frida-client";
},
getClientAliases: function(keyType, issuers) {
console.log("[MyKeyManager] getClientAliases called");
return ["frida-client"];
},
chooseServerAlias: function(keyType, issuers, socket) { return null; },
getServerAliases: function(keyType, issuers) { return null; },
// 最关键的方法:返回客户端证书链
getCertificateChain: function(alias) {
console.log("[MyKeyManager] getCertificateChain called for alias: " + alias);
if (alias === "frida-client") {
try {
// 这里需要返回一个X509Certificate[]。
// 为了演示,我们尝试加载一个预设的证书,或者动态生成一个。
// 动态生成比较复杂,这里先演示一个返回空数组(可能失败)或占位符的思路。
// 更实用的做法是:从Hook到的原始KeyManager里“偷”一个证书链出来,或者预先准备好一个证书文件。
console.warn(" Returning empty certificate chain. This may cause handshake failure if server strictly requires a valid cert.");
return [];
} catch(e) {
console.error(" Error in getCertificateChain: " + e);
}
}
return null;
},
// 最关键的方法:返回私钥
getPrivateKey: function(alias) {
console.log("[MyKeyManager] getPrivateKey called for alias: " + alias);
if (alias === "frida-client") {
// 同理,这里需要返回一个PrivateKey对象。
// 我们可以返回null,或者尝试生成/获取一个。
console.warn(" Returning null private key.");
return null;
}
return null;
},
// X509ExtendedKeyManager的额外方法,保持默认实现
chooseEngineClientAlias: function(keyType, issuers, engine) { return this.chooseClientAlias(keyType, issuers, null); },
chooseEngineServerAlias: function(keyType, issuers, engine) { return null; }
}
});
} else {
// 实现基础的X509KeyManager (逻辑类似,略)
}
这个自定义的
MyKeyManager
目前只是一个“空壳”,它的
getCertificateChain
和
getPrivateKey
返回的是空值或null,这在实际双向认证中很可能失败。为了让Hook真正成功,我们需要一个能提供有效证书和私钥的KeyManager。
4.3 高级技巧:窃取或伪造有效证书链
提供有效证书有两种主要策略:
策略A:窃取应用原有的证书
在Hook到原始的
init
方法时,我们可以拿到原始的
keyManagers
数组。我们可以从中提取出有效的证书链和私钥,存储起来,然后在我们自定义的KeyManager中返回它们。这要求原始KeyManager在Hook时是可用的。
修改
init
的Hook部分:
var stolenCertificateChain = null;
var stolenPrivateKey = null;
SSLContext.init.overload('[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom').implementation = function(keyManagers, trustManagers, secureRandom) {
console.log("\n[+] SSLContext.init Hooked!");
if (keyManagers && keyManagers.length > 0) {
console.log(" Original KeyManagers found, attempting to extract cert & key...");
var originalKeyManager = keyManagers[0];
// 尝试调用原始KeyManager的方法获取信息(注意:需要在主线程进行)
// 这里只是一个思路示例,实际调用可能因线程问题而复杂
// var alias = originalKeyManager.chooseClientAlias(null, null, null);
// stolenCertificateChain = originalKeyManager.getCertificateChain(alias);
// stolenPrivateKey = originalKeyManager.getPrivateKey(alias);
console.log(" (In a real scenario, you would store the original cert/key here)");
}
// 即使窃取,我们也用自定义的KeyManager替换
var myKeyManagerInstance = MyKeyManager.$new();
var newKeyManagers = [myKeyManagerInstance];
console.log(" Replacing with custom MyKeyManager.");
// 调用原init,但使用我们的KeyManager
this.init(newKeyManagers, trustManagers, secureRandom);
};
然后,修改
MyKeyManager
的
getCertificateChain
和
getPrivateKey
方法,返回之前存储的
stolenCertificateChain
和
stolenPrivateKey
。
策略B:动态生成自签名证书(更通用但可能被服务器CA校验拒绝) 使用BouncyCastle或Java的API在内存中生成一个自签名的X.509证书和RSA密钥对。这需要引入额外的库,在Frida环境中操作较为复杂,通常需要将编译好的类注入进去。对于大多数测试场景,如果服务器只校验客户端是否有证书,而不校验证书是否由特定CA签发,那么一个自签名证书可能就足够了。但更常见的是服务器会校验客户端证书的颁发者。
策略C:完全绕过客户端认证(终极方案) 如果我们的目的仅仅是解密流量,而不是建立完整的双向认证连接,我们可以尝试一个更激进的方法: 同时修改TrustManager,让它信任所有的服务器证书 。这样,结合一个空的或伪造的KeyManager,我们就能建立一个“单向”的SSL连接,而服务器端可能因为配置不严格而接受(或者配合服务端测试时,我们可以控制服务器不验证客户端证书)。
修改
init
方法,同时注入一个“信任所有”的TrustManager:
// 创建一个接受所有证书的TrustManager
var TrustAllManager = Java.registerClass({
name: 'com.example.frida.TrustAllManager',
implements: [Java.use("javax.net.ssl.X509TrustManager")],
methods: {
checkClientTrusted: function(chain, authType) {},
checkServerTrusted: function(chain, authType) {},
getAcceptedIssuers: function() { return []; }
}
});
SSLContext.init.overload('[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom').implementation = function(keyManagers, trustManagers, secureRandom) {
console.log("\n[+] SSLContext.init Hooked - Using TrustAll + DummyKey Manager");
var myKeyManagerInstance = MyKeyManager.$new();
var trustAllManagerInstance = TrustAllManager.$new();
// 替换KeyManager和TrustManager
this.init([myKeyManagerInstance], [trustAllManagerInstance], secureRandom);
};
这种“双管齐下”的方法——用空KeyManager应付客户端认证,用TrustAllManager跳过服务器证书验证——在非严格的生产环境中,有时能奇迹般地让流量通过,从而被我们的中间人代理(如Burp Suite)成功拦截和解密。 这是实际测试中成功率较高的一个实用技巧。
5. 针对不同网络库的适配与实战
现代Android应用很少直接使用原始的
HttpURLConnection
,更多是使用OkHttp或Retrofit。这些库对
SSLContext
的封装方式不同,我们的Hook策略也需要微调。
5.1 针对OkHttp的Hook
OkHttp通常通过
OkHttpClient.Builder
的
sslSocketFactory
方法传入自定义的
SSLSocketFactory
。这个Factory就是从
SSLContext
获取的。因此,Hook
SSLContext.init
仍然有效。但OkHttp还有一个特性叫
CertificatePinner
(证书锁定),它会进一步校验服务器证书的公钥指纹。如果应用使用了这个,即使SSL握手成功,请求也会在证书锁定校验时失败。
我们需要额外Hook CertificatePinner :
Java.perform(function() {
var CertificatePinner = Java.use("okhttp3.CertificatePinner");
// Hook build方法,返回一个“空”的CertificatePinner
CertificatePinner.Builder.$new().build.implementation = function() {
console.log("[+] Bypassing OkHttp CertificatePinner.");
// 返回一个不进行任何校验的CertificatePinner实例
var builder = CertificatePinner.Builder.$new();
// 可以调用builder.add("example.com", "sha256/AAAAAAAA...")来添加伪造的指纹,但更简单的是直接返回builder.build()一个空规则集。
// 实际上,build()方法本身不接收参数。我们需要的是替换整个CertificatePinner对象。
// 更直接的方法:Hook OkHttpClient.Builder的build方法,并设置一个空的CertificatePinner。
return this.build(); // 这里返回了原始的,需要更精细的Hook
};
// 更有效的方法是Hook OkHttpClient.Builder的certificatePinner setter
var OkHttpClientBuilder = Java.use("okhttp3.OkHttpClient$Builder");
OkHttpClientBuilder.certificatePinner.overload('okhttp3.CertificatePinner').implementation = function(pinner) {
console.log("[+] Nullifying CertificatePinner in OkHttpClient Builder.");
// 调用原方法,但传入一个空的CertificatePinner
var dummyPinner = CertificatePinner.Builder.$new().build();
return this.certificatePinner(dummyPinner);
};
});
5.2 实战脚本整合与使用
将上述所有技巧整合成一个完整的、针对性强且健壮的脚本。
// frida_ssl_bypass_complete.js
Java.perform(function() {
console.log("[*] Starting comprehensive SSL bypass script...");
// 1. 定义 TrustAllManager
var X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
var TrustAllManager = Java.registerClass({
name: 'com.frida.TrustAllManager',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function(chain, authType) {
console.log("[TrustAllManager] Blindly trusting client cert: " + authType);
},
checkServerTrusted: function(chain, authType) {
console.log("[TrustAllManager] Blindly trusting server cert: " + authType);
},
getAcceptedIssuers: function() {
return [];
}
}
});
// 2. 定义 DummyKeyManager (简化版,只返回空)
var X509KeyManager = Java.use("javax.net.ssl.X509KeyManager");
var DummyKeyManager = Java.registerClass({
name: 'com.frida.DummyKeyManager',
implements: [X509KeyManager],
methods: {
chooseClientAlias: function(keyType, issuers, socket) {
console.log("[DummyKeyManager] Client alias requested for: " + JSON.stringify(keyType));
return "frida-dummy-alias";
},
getClientAliases: function(keyType, issuers) { return ["frida-dummy-alias"]; },
chooseServerAlias: function(keyType, issuers, socket) { return null; },
getServerAliases: function(keyType, issuers) { return null; },
getCertificateChain: function(alias) {
console.log("[DummyKeyManager] Returning empty cert chain for alias: " + alias);
return [];
},
getPrivateKey: function(alias) {
console.log("[DummyKeyManager] Returning null private key for alias: " + alias);
return null;
}
}
});
// 3. Hook SSLContext.init (核心)
var SSLContext = Java.use("javax.net.ssl.SSLContext");
var initOverload = SSLContext.init.overload('[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom');
initOverload.implementation = function(keyManagers, trustManagers, secureRandom) {
console.log("\n[+] Hooking SSLContext.init (3-args)");
console.log(" Original KM count: " + (keyManagers ? keyManagers.length : 0) + ", TM count: " + (trustManagers ? trustManagers.length : 0));
console.log(" Replacing with DummyKeyManager and TrustAllManager.");
var dummyKeyManager = DummyKeyManager.$new();
var trustAllManager = TrustAllManager.$new();
// 调用原方法,但使用我们自己的Manager
this.init([dummyKeyManager], [trustAllManager], secureRandom);
};
// 4. (可选) Hook OkHttpClient.Builder 以禁用 CertificatePinner
try {
var OkHttpClientBuilder = Java.use("okhttp3.OkHttpClient$Builder");
OkHttpClientBuilder.certificatePinner.overload('okhttp3.CertificatePinner').implementation = function(pinner) {
console.log("[+] Intercepted OkHttpClient.Builder.certificatePinner() - Nullifying.");
var CertificatePinner = Java.use("okhttp3.CertificatePinner");
var dummyPinner = CertificatePinner.Builder.$new().build();
return this.certificatePinner(dummyPinner);
};
console.log("[*] OkHttp CertificatePinner hook installed.");
} catch (e) {
console.log("[!] OkHttp not found or hook failed: " + e.message);
}
// 5. (可选) Hook TrustManagerFactory 以防万一
var TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory");
TrustManagerFactory.init.overload('java.security.KeyStore').implementation = function(ks) {
console.log("[+] Hooking TrustManagerFactory.init(KeyStore)");
// 调用原方法,但之后我们可以替换getTrustManagers的返回值吗?更直接的是Hook SSLContext。
// 这里只是打印信息,证明它被调用了。
return this.init(ks);
};
console.log("[*] SSL bypass hooks installation complete.");
});
使用脚本 :
-
将上述脚本保存为
ssl_bypass.js。 -
启动目标应用,或重启应用以Frida注入模式启动:
frida -U -f com.example.targetapp -l ssl_bypass.js --no-pause -
触发应用中的网络请求。观察Frida控制台输出,应该能看到
[+] Hooking SSLContext.init和Manager被调用的日志。 - 此时,配置你的抓包工具(Burp Suite/Charles)的代理,并确保设备已安装抓包工具的根证书。理论上,应用的双向认证已被绕过,HTTPS流量应该可以被成功解密。
6. 常见问题、排查技巧与进阶思考
在实际操作中,你几乎一定会遇到各种问题。下面是一些常见的情况和解决思路。
6.1 问题排查清单
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| Hook不生效,无日志输出 |
1. 目标类/方法名错误。
2. 应用使用了自定义类加载器或加固,类未被正常加载。 3. Frida脚本注入时机太晚。 |
1.
确认类名
:使用
frida -U -f com.example.app -j
进入REPL,用
Java.enumerateLoadedClasses({onMatch: function(c){if(c.includes(\"SSLContext\")) console.log(c)}, onComplete: function(){}})
枚举已加载的类,确认完整类名。
2. 检查加固 :如果应用加固,可能需要先脱壳或寻找合适的时机(如
Java.choose
)来Hook。可以尝试Hook
java.lang.ClassLoader
的
loadClass
方法,在目标类被加载时再执行Hook。
3. 提前注入 :使用
-f
参数在应用启动时即注入脚本,或Hook
Application.onCreate()
等早期生命周期。
|
| SSL握手失败 (Handshake Failure) |
1. 自定义的
KeyManager
返回的证书/私钥无效或格式不对。
2. 服务器严格校验客户端证书,不接受空或自签名证书。 3. 应用使用了证书锁定(如OkHttp的
CertificatePinner
)且未被绕过。
|
1.
检查KeyManager
:确保
getCertificateChain
返回的是
X509Certificate[]
,
getPrivateKey
返回有效的
PrivateKey
。尝试使用
策略A(窃取)
。
2. 查看服务器日志 :如果可能,查看服务器端的SSL握手错误日志,确认是证书未知、过期还是CA不信任。 3. 确认证书锁定 :检查脚本中OkHttp
CertificatePinner
的Hook是否生效。可以搜索代码中是否有
CertificatePinner.Builder()
。
|
| 流量仍无法被Burp解密 |
1. 设备的系统证书库未安装Burp的CA证书。
2. 应用使用了 SSL Pinning(证书固定) ,且我们的
TrustAllManager
未能生效,或者固定在了更高层(如Native层)。
3. 应用可能使用了非标准的HTTP库或直接使用Socket。 |
1.
安装CA证书
:确保Burp的CA证书已安装到设备的系统信任证书库(Android 7+需要将证书安装到系统分区,或修改App的网络安全配置)。
2. 对抗SSL Pinning :除了Hook Java层的
TrustManager
,还需要检查是否有Native库(如
libssl.so
,
libcrypto.so
)在验证证书。需要使用Frida的
Interceptor
来Hook Native函数(如
SSL_CTX_set_cert_verify_callback
)。这是一个更高级的话题。
3. 全局代理检测 :有些应用会检测是否设置了系统代理,并拒绝通过代理发送流量。需要Hook相关检测方法(如
System.getProperty(“http.proxyHost”)
)或使用透明代理工具(如
r0capture
)。
|
| 应用崩溃或行为异常 |
1. Hook的函数实现有bug,导致参数或返回值类型错误。
2. 线程问题:在非UI线程执行了某些需要主线程的操作。 3. 内存冲突或重复Hook。 |
1.
精简脚本
:注释掉部分Hook,逐步排查是哪个方法导致崩溃。仔细检查
implementation
函数内的逻辑,确保调用原方法时参数正确。
2. 使用
Java.perform
:确保所有Java操作都在
Java.perform
的回调中执行。
3. 避免重复注入 :如果多次注入同一脚本,可能导致重复Hook和冲突。重启应用或使用
frida -U --attach
重新附加。
|
6.2 进阶:对抗Native层SSL验证
如果应用将SSL验证逻辑放在Native代码(C/C++)中,上述纯Java层的Hook将完全失效。你需要将战场转移到Native层。
-
定位Native库
:使用
frida-ps -Uai查看应用包含的so文件,常见的有libssl.so、libcrypto.so(OpenSSL/BoringSSL)或应用自定义的so。 -
Hook Native函数
:使用Frida的
Interceptor来Hook如SSL_CTX_set_verify、SSL_get_verify_result等函数,修改其回调或返回值。Interceptor.attach(Module.findExportByName("libssl.so", "SSL_CTX_set_verify"), { onEnter: function(args) { // args[1]是验证模式,可以尝试修改它 console.log("SSL_CTX_set_verify called, mode: " + args[1]); // 例如,强制设置为SSL_VERIFY_NONE (0) args[1] = ptr(0); } }); -
工具辅助
:可以使用如
objection(基于Frida的命令行工具)的android sslpinning disable命令,它尝试自动禁用常见的证书固定方法,包括一些Native层的。
6.3 个人实操体会与建议
经过多次实战,我总结出几点心得:
- 由浅入深 :不要一开始就想着写一个“万能”脚本。先从一个简单的、已知使用了双向认证的测试应用开始,验证基础Hook(如打印日志)是否生效。
-
日志是你的眼睛
:在脚本中大量使用
console.log(),打印出函数调用栈(Java.use(“android.util.Log”).getStackTraceString(Java.use(“java.lang.Exception”).$new()))、参数值、返回值。这能帮你精准定位问题。 - 组合拳 :很少有应用只使用一种防护。成功拦截流量往往是 Java层SSLContext Hook + TrustManager绕过 + 证书锁定禁用 + 系统CA证书安装 组合作用的结果。
- 理解业务逻辑 :有时候,绕过技术问题后,你会发现应用在业务层还有额外的签名校验或Token验证。安全测试是一个系统工程,SSL绕过只是打开了通信的大门,里面的房间可能还有别的锁。
- 合法合规是底线 :再次强调,所有这些技术都应在你拥有明确测试授权的范围内使用。用于学习研究时,请在自己的实验环境中进行。
最后,这项技术的魅力在于它的动态性和创造性。每一个应用都可能是一个新的谜题,而Frida给了我们一套强大的工具去解开它。从Hook一个简单的
init
方法开始,你可能会深入到JNI、Native Hook、系统内核,甚至RASP对抗的领域。保持好奇,耐心调试,你会发现在移动安全的深水区,别有洞天。

481

被折叠的 条评论
为什么被折叠?



