背景
在开发/迭代过程中, 会遇到一些业务功能 之前都是好好的, 突然不行了
的奇怪的问题, 特别是在季度版本发布的时候, 由于涉及了多个网元联合发布解决方案版本, 范围大责任远, 难免会出一些意料之外的情况.
然而这种问题定位之后大多都是由于双方接口发生了变化, 没能适配导致的. 再往后就是抓日志看日志, 这日志看一次两次还好, 但似乎这种情况就没断绝过. (最可气的是因为客户直面终端, 所以即便自己没有问题终端也总是要拉过去替别人干活啊有没有~)
那就是定位数据问题:
- 说对方发出的数据包问题吧, 拿不出证据;
- 抓个包看看吧, 连接是 TLS 的啥也看不出来;
- 说自己加打点吧, 破坏了版本现场;
- 好不容易谨慎加了打点吧, 发现问题不在这个模块, 白折腾;
- 辛辛苦苦给各个模块加打点吧, 这版本谁还敢信啊;
综合来看, 对于我们系统应用开发的场景, 需要的是一种 侵入性小的, 作用范围广的, 还原性强的 方案来解决这些问题.
- 侵入性小: 能够避免或少量修改现有现场
- 作用范围广: 因为终端系统细分为很多模块, 尽量多的作用于各个模块
- 还原性强: 和侵入性小类似, 需要避免或尽量少的影响干预后的结果
我们公司的服务现阶段几乎都是 TLS
的服务, 解决数据分析问题, 主要集中在 TLS
数据包分析上, 市面上类似的方案也有, 但多多少少有些水土不服:
- 各种基于中间人攻击的代理
Fidder
/Charles
/MITM
, 这种需要客户端信任他们的CA
, 但是目前Android
已经对这些方式加了限制, 我们的应用更彻底, 只信任自己的证书. 所以基本已经失灵了. - 基于
Frida
的 r0capture: 很强很暴力, 但是这种方法侵入性太强, 间接修改了所有系统的TLS
上层组件, 不校验证书也不校验域名, 虽然能够抓出明文包, 但是已经破坏了案发现场. 用来逆向系统可以, 但是做问题分析还是不太妥当. - 基于
Linux
内核功能的 ecapture: 很强且安全, 但可惜对内核要求太高, 我们的操作系统现在无法支持.
刚好最近在捣腾 Wireshark
解析 TLS
报文, 灵光闪现之后感觉可以搞点事情.
原理
灵光闪现的源头是了解到 OpenSSL
有一个输出 Pre-Master-Secret
的可选能力, 这个能力在 Windows/Linux
等系统下可以轻松打开.
而 Android
默认使用的 TLS
组件是 Google
基于 OpenSSL
分支的 BoringSSL
, 它和 OpenSSL
同属一脉.
Android
的 BoringSSL
在工作场景下虽然不直接支持输出 Pre-Master-Secret
, 但保留了相关的功能接口, 只需要稍稍改造, 即可让使用这个 BoringSSL
组件的程序记录下 Pre-Master-Secret
, 最终达到解析原始 TLS
数据包的效果.
Android TLS 组件关系
正常情况下 Android
开发时用到的 TLS
组件都是一些抽象接口和抽象类, 例如 SSLContext.getDefault()
, 应用程序在调用时, 系统内部首先通过
./libcore/luni/src/main/java/java/security/security.properties
配置文件找到提供者:
...
# Android's provider of OpenSSL backed implementations
security.provider.1=com.android.org.conscrypt.OpenSSLProvider
# Android's version of the CertPathValidator and CertPathBuilder
security.provider.2=sun.security.provider.CertPathProvider
# Android's stripped down BouncyCastle provider
security.provider.3=com.android.org.bouncycastle.jce.provider.BouncyCastleProvider
# Android's provider of OpenSSL backed implementations
security.provider.4=com.android.org.conscrypt.JSSEProvider
...
可以看到默认指定了 4 个, 其中第 1 个存在且默认生效.
再通过 OpenSSLProvider
指向 SSLContext
的最终实现类 DefaultSSLContextImpl
, DefaultSSLContextImpl
继承自 OpenSSLContextImpl
, OpenSSLContextImpl
初始化时构造的 ClientSessionContext
和 ServerSessionContext
的父类 AbstractSessionContext
则就是这一切在 Java
层的源头, 他是 JNI
层 NativeXXX
这一系列 JNI
类的一个包装, 对外提供服务.
在 Native
层面, NativeCrypto
是 JNI
的入口, 通过它, 才终于来到了 BoringSSL
的地界.
关系结构建议结合源码看, 后面有空了补个图.
通过观察 NativeCrypto
和 BoringSSL
的源码发现, BoringSSL
确实和 OpenSSL
一样, 天然可以支持打印 Pre-Master-Secret
, 只不过默认是关闭的:
/*
* public static native int SSL_CTX_new();
*/
static jlong NativeCrypto_SSL_CTX_new(JNIEnv* env, jclass) {
...
SSL_CTX_set_info_callback(sslCtx.get(), info_callback);
SSL_CTX_set_cert_cb(sslCtx.get(), cert_cb, nullptr);
if (conscrypt::trace::kWithJniTraceKeys) {
SSL_CTX_set_keylog_callback(sslCtx.get(), debug_print_session_key);
}
...
JNI_TRACE("NativeCrypto_SSL_CTX_new => %p", sslCtx.get());
return (jlong)sslCtx.release();
}
所以, 只需要稍稍改造一下这个库, 就可以实现目标了.
记录一下源码的路径:
# security.properties 源码路径:
./libcore/luni/src/main/java/java/security/security.properties
# com.android.org.conscrypt.OpenSSLProvider 源码路径:
./external/conscrypt/repackaged/common/src/main/java/com/android/org/conscrypt/OpenSSLProvider.java
# DefaultSSLContextImpl / OpenSSLContextImpl / AbstractSessionContext 等源码路径:
./external/conscrypt/repackaged/common/src/main/java/com/android/org/conscrypt/
# NativeCrypto` JNI 源码路径:
./external/conscrypt/common/src/jni/main/cpp/conscrypt/native_crypto.cc
# BoringSSL` 源码路径:
./external/boringssl
Conscrypt 修改
修改方式有两种, 一种是修改 JNI 层面的入口 native_crypto.cc
, 好处是已经有模板代码, 拷贝即可, 但就只能在 Java
层生效.
另外一种是修改 BoringSSL
的主类文件 lib_ssl.cc
, 相对 native_crypto.cc
而言需要自己写代码, 但好处是能够做到全系统生效(Java
+ Linux
).
native_crypto.cc
(二选其一)
static void debug_print_session_key(const SSL* ssl, const char* line) {
JNI_TRACE_KEYS("ssl=%p KEY_LINE: %s", ssl, line);
}
+ static FILE *g_keylog_file = nullptr;
+ static void print_session_key(const SSL*, const char* line) {
+ if (g_keylog_file == nullptr) {
+ g_keylog_file = fopen("/data/local/tmp/PreMasterSecret.log", "a");
+ }
+ if (g_keylog_file != nullptr) {
+ fprintf(g_keylog_file, "%s\n", line);
+ fflush(g_keylog_file);
+ }
+ }
+
static void debug_print_packet_data(const SSL* ssl, char direction, const char* data, size_t len) {
SSL_CTX_set_info_callback(sslCtx.get(), info_callback);
SSL_CTX_set_cert_cb(sslCtx.get(), cert_cb, nullptr);
if (conscrypt::trace::kWithJniTraceKeys) {
SSL_CTX_set_keylog_callback(sslCtx.get(), debug_print_session_key);
}
+ SSL_CTX_set_keylog_callback(sslCtx.get(), print_session_key);
// By default BoringSSL will cache in server mode, but we want to get
// notified of new sessions being created in client mode. We set
// SSL_SESS_CACHE_BOTH in order to get the callback in client mode, but
// ignore it in server mode in favor of the internal cache.
SSL_CTX_set_session_cache_mode(sslCtx.get(), SSL_SESS_CACHE_BOTH);
此方法编译使用 make -j8 libjavacrypto
命令, 编译后生成 libjavacrypto.so
, 替换 /system/lib(?|32|64)/libjavacrypto.so
即可.
lib_ssl.cc
(二选其一)
static CRYPTO_EX_DATA_CLASS g_ex_data_class_ssl =
CRYPTO_EX_DATA_CLASS_INIT_WITH_APP_DATA;
static CRYPTO_EX_DATA_CLASS g_ex_data_class_ssl_ctx =
CRYPTO_EX_DATA_CLASS_INIT_WITH_APP_DATA;
+ static FILE *g_keylog_file = nullptr;
+ static void print_session_key(const SSL*, const char* line) {
+ if (g_keylog_file == nullptr) {
+ g_keylog_file = fopen("/data/local/tmp/PreMasterSecret.log", "a");
+ }
+ if (g_keylog_file != nullptr) {
+ fprintf(g_keylog_file, "%s\n", line);
+ fflush(g_keylog_file);
+ }
+ }
+
bool CBBFinishArray(CBB *cbb, Array<uint8_t> *out) {
uint8_t *ptr;
size_t len;
if (!CBB_finish(cbb, &ptr, &len)) {
OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
return false;
}
out->Reset(ptr, len);
return true;
}
SSL_CTX *SSL_CTX_new(const SSL_METHOD *method) {
if (method == NULL) {
OPENSSL_PUT_ERROR(SSL, SSL_R_NULL_SSL_METHOD_PASSED);
return nullptr;
}
UniquePtr<SSL_CTX> ret = MakeUnique<SSL_CTX>(method);
if (!ret) {
return nullptr;
}
ret->cert = MakeUnique<CERT>(method->x509_method);
ret->sessions = lh_SSL_SESSION_new(ssl_session_hash, ssl_session_cmp);
ret->client_CA.reset(sk_CRYPTO_BUFFER_new_null());
if (ret->cert == nullptr ||
ret->sessions == nullptr ||
ret->client_CA == nullptr ||
!ret->x509_method->ssl_ctx_new(ret.get())) {
return nullptr;
}
if (!SSL_CTX_set_strict_cipher_list(ret.get(), SSL_DEFAULT_CIPHER_LIST) ||
// Lock the SSL_CTX to the specified version, for compatibility with
// legacy uses of SSL_METHOD.
!SSL_CTX_set_max_proto_version(ret.get(), method->version) ||
!SSL_CTX_set_min_proto_version(ret.get(), method->version)) {
OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
return nullptr;
}
+ ret.get()->keylog_callback = print_session_key;
+
return ret.release();
}
此方法编译使用 make -j8 libssl
命令, 编译后生成 libssl.so
, 替换 /system/lib(?|32|64)/libssl.so
即可.
APEX & APEX Conscrypt
在 Android 10
上面, 替换 /system/lib(?|32|64)/libjavacrypto.so
或者 /system/lib(?|32|64)/libssl.so
之后并不能生效, 因为实际生效的路径是 /apex/com.android.conscrypt/lib(?|32|64)/
, 这个路径是虚拟文件系统且只读, 不得不引入一个新的东西 APEX
.
什么是 APEX
Android Pony EXpress (APEX)
是 Android 10
中引入的一种容器格式,用于较低级别系统模块的安装流程中。此格式可帮助更新不适用于标准 Android
应用模型的系统组件。一些示例组件包括原生服务和原生库、硬件抽象层 (HAL)
)、运行时 (ART)
以及类库。
简单来说就是 Google
设计了一套能够轻量化更新 Android
系统核心组件的一种技术, 就是 APEX
, 对应的升级包文件也是 apex
后缀.
编译 APEX Conscrypt
APEX
升级包和 APK
类似, 也有版本号的概念, 所以需要先修改 APEX Conscrypt
的版本号:
{
"name": "com.android.conscrypt",
- "version": 291601510
+ "version": 500000000
}
源码文件: ./external/conscrypt/apex/apex_manifest.json
APEX Conscrypt
编译使用 make -j8 com.android.conscrypt
命令编译, 编译后生成 com.android.conscrypt.apex
升级包
安装/卸载 APEX Conscrypt
和 APK
类似, APEX
也是用 adb install xxx.apex
进行安装, 不同的是 APEX
安装后重启才能生效.
adb install com.android.conscrypt.apex
卸载的话使用 APEX
包名, 比如 APEX Conscrypt
就是 com.android.conscrypt
, 同样的卸载后重启生效:
adb uninstall com.android.conscrypt
效果
上面的一顿输出, 目前可以实现通过替换一个系统功能组件, 开启 TLS
的 Pre-Master-Secret
打印, 配合 tcpdump
抓包之后, 就可以在 Wireshark
查看完整的明文数据报文.
对于前面提到的3点诉求, 基本都给做到了:
- 侵入性小: 仅替换系统一个原生库, 不修改任何上层业务代码;
- 作用范围广: 修改全系统生效, 各个模块无需单独适配;
- 还原性强: 不修改任何数据, 支持观察系统原始行为;