记录一次 Android 服务端的证书校验的详细过程
字数 1591 2025-08-22 12:23:18
Android 服务端证书校验分析与绕过实战指南
前言
本教程详细记录了对一个使用 org.conscrypt 库进行服务端证书校验的 Android 应用的逆向分析过程。通过该案例,您将学习到如何识别、分析和绕过复杂的证书校验机制,特别是针对使用 OpenSSL 封装库的应用。
准备工作
所需工具
- 逆向工具:Jadx-gui (用于反编译 APK)
- 抓包工具:Burp Suite (用于拦截和分析网络请求)
- Hook 工具:Frida (用于动态分析应用行为)
- 证书工具:XCA (用于证书管理和分析)
- 脚本工具:r0capture (用于自动化抓包)
- 代理工具:Proxifier (用于强制应用流量通过代理)
环境配置
- 已 root 的 Android 设备
- Burp 证书已安装至系统信任区
- Frida-server 运行在设备上
初步分析
1. 设置代理
- 使用 Proxifier 开启 VPN 让目标应用流量走 Burp 代理
- 配置 Burp 代理监听端口
2. 识别证书校验
- 应用请求返回
400 No required SSL certificate was sent错误 - 表明服务端要求客户端提供有效的证书
静态分析
1. 解包 APK
使用 Jadx-gui 反编译 APK,重点关注以下文件:
grp_sp.bkshmsincas.bkshmsrootcas.bkstrust.crt
2. 分析证书文件
- 使用 XCA 查看
trust.crt内容 - 发现包含多个公钥证书和 CA 证书
- 确认这些是证书信任链,非客户端证书
动态分析
1. 尝试 Frida 自吐脚本
使用 android-keystore-audit 中的 tracer-keystore.js 脚本:
adb forward tcp:28042 tcp:28042
frida -H 127.0.0.1:28042 -f <包名> -l hook.js
2. 手动抛出异常
通过打印堆栈发现关键类:
ak.im.module.AkeyChatX509PrivateCA.clientBootstrapCertInfo
3. 尝试 r0capture
python r0capture.py -H 127.0.0.1:28042 -f <包名> -v
发现数据仍加密,证书未导出
深入分析
1. Hook 证书相关方法
针对 AkeyChatX509PrivateCA.clientBootstrapCertInfo 方法编写 Hook 脚本:
function hook() {
Java.perform(function() {
let AkeyChatX509PrivateCA = Java.use("ak.im.module.AkeyChatX509PrivateCA");
AkeyChatX509PrivateCA["clientBootstrapCertInfo"].implementation = function() {
console.log(`AkeyChatX509PrivateCA.clientBootstrapCertInfo is called`);
let result = this["clientBootstrapCertInfo"]();
console.log(`AkeyChatX509PrivateCA.clientBootstrapCertInfo result=${result}`);
return result;
};
});
}
2. 导出证书
成功获取 X509 证书,包含 "Web Client Authentication" 用途标识:
let cert = result.getEncoded();
let bytes = Memory.readByteArray(cert, cert.length);
const file = new File("/sdcard/Download/private.pem", "wb");
file.write(bytes);
获取私钥
1. 分析 org.conscrypt 库
- 下载源码:https://github.com/google/conscrypt
- 搜索关键词发现关键函数:
EVP_parse_private_key
2. Hook So 层函数
针对 libconscrypt_jni.so 编写 Hook 脚本:
function hookFunc(funcAddr, name) {
Interceptor.attach(funcAddr, {
onEnter: function(args) {
console.log(name + " enter");
const bytes = Memory.readByteArray(args[1], 0x1000);
const file = new File("/sdcard/Download/private.pem", "wb");
file.write(bytes);
}
});
}
function hook() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function(args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
if (path.indexOf("libconscrypt_jni.so") !== -1) {
console.log("dlopen: " + path);
this.path = path;
}
}
},
onLeave: function(retval) {
if (this.path !== undefined) {
var baseAddress = Module.findBaseAddress(this.path);
if (baseAddress !== null) {
Module.enumerateExports(this.path, {
onMatch: function(symbol) {
if (symbol.name.indexOf("EVP_parse_private_key") !== -1) {
console.log(symbol.name + "---" + symbol.address.toString());
hookFunc(symbol.address, symbol.name)
}
}
});
}
}
}
});
}
hook();
证书与私钥配对
- 使用 XCA 导入导出的私钥
- 验证私钥与之前获取的证书是否匹配
- 在 XCA 中将证书和私钥导出为 PKCS#12 (.p12) 格式
配置 Burp Suite
- 将导出的 PKCS#12 证书导入 Burp
- 配置 Burp 使用该客户端证书
- 重新发送请求,成功绕过证书校验
总结
关键点
- 证书定位:通过堆栈分析找到关键证书类
- Hook 技巧:Java 层和 Native 层结合 Hook
- 私钥提取:针对 org.conscrypt 库的特定函数进行 Hook
- 证书配对:确保证书和私钥匹配
适用性
该方法可能适用于其他使用 org.conscrypt 库的应用,但需要根据具体实现调整 Hook 点。
后续研究方向
- 自动化识别 org.conscrypt 的关键函数
- 研究其他 OpenSSL 封装库的类似方法
- 探索不依赖 root 的证书提取方法
通过本教程,您应该掌握了分析复杂证书校验机制的基本方法,特别是针对使用 OpenSSL 封装库的应用。记住在实际应用中要遵守相关法律法规,仅将此技术用于合法授权的安全测试。