0%

通过修改系统TLS组件实现对TLS数据包解析

  1. 背景
  2. 原理
    1. Android TLS 组件关系
    2. Conscrypt 修改
      1. native_crypto.cc (二选其一)
      2. lib_ssl.cc (二选其一)
    3. APEX & APEX Conscrypt
      1. 什么是 APEX
      2. 编译 APEX Conscrypt
      3. 安装/卸载 APEX Conscrypt
  3. 效果

背景

在开发/迭代过程中, 会遇到一些业务功能 之前都是好好的, 突然不行了 的奇怪的问题, 特别是在季度版本发布的时候, 由于涉及了多个网元联合发布解决方案版本, 范围大责任远, 难免会出一些意料之外的情况.

然而这种问题定位之后大多都是由于双方接口发生了变化, 没能适配导致的. 再往后就是抓日志看日志, 这日志看一次两次还好, 但似乎这种情况就没断绝过. (最可气的是因为客户直面终端, 所以即便自己没有问题终端也总是要拉过去替别人干活啊有没有~)

那就是定位数据问题:

  • 说对方发出的数据包问题吧, 拿不出证据;
  • 抓个包看看吧, 连接是 TLS 的啥也看不出来;
  • 说自己加打点吧, 破坏了版本现场;
  • 好不容易谨慎加了打点吧, 发现问题不在这个模块, 白折腾;
  • 辛辛苦苦给各个模块加打点吧, 这版本谁还敢信啊;

综合来看, 对于我们系统应用开发的场景, 需要的是一种 侵入性小的, 作用范围广的, 还原性强的 方案来解决这些问题.

  • 侵入性小: 能够避免或少量修改现有现场
  • 作用范围广: 因为终端系统细分为很多模块, 尽量多的作用于各个模块
  • 还原性强: 和侵入性小类似, 需要避免或尽量少的影响干预后的结果

我们公司的服务现阶段几乎都是 TLS 的服务, 解决数据分析问题, 主要集中在 TLS 数据包分析上, 市面上类似的方案也有, 但多多少少有些水土不服:

  • 各种基于中间人攻击的代理 Fidder / Charles / MITM, 这种需要客户端信任他们的 CA, 但是目前 Android 已经对这些方式加了限制, 我们的应用更彻底, 只信任自己的证书. 所以基本已经失灵了.
  • 基于 Fridar0capture: 很强很暴力, 但是这种方法侵入性太强, 间接修改了所有系统的 TLS 上层组件, 不校验证书也不校验域名, 虽然能够抓出明文包, 但是已经破坏了案发现场. 用来逆向系统可以, 但是做问题分析还是不太妥当.
  • 基于 Linux 内核功能的 ecapture: 很强且安全, 但可惜对内核要求太高, 我们的操作系统现在无法支持.

刚好最近在捣腾 Wireshark 解析 TLS 报文, 灵光闪现之后感觉可以搞点事情.

原理

灵光闪现的源头是了解到 OpenSSL 有一个输出 Pre-Master-Secret 的可选能力, 这个能力在 Windows/Linux 等系统下可以轻松打开.

Android 默认使用的 TLS 组件是 Google 基于 OpenSSL 分支的 BoringSSL, 它和 OpenSSL 同属一脉.

AndroidBoringSSL 在工作场景下虽然不直接支持输出 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 初始化时构造的 ClientSessionContextServerSessionContext 的父类 AbstractSessionContext 则就是这一切在 Java 层的源头, 他是 JNINativeXXX 这一系列 JNI 类的一个包装, 对外提供服务.

Native 层面, NativeCryptoJNI 的入口, 通过它, 才终于来到了 BoringSSL 的地界.

关系结构建议结合源码看, 后面有空了补个图.

通过观察 NativeCryptoBoringSSL 的源码发现, 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

效果

上面的一顿输出, 目前可以实现通过替换一个系统功能组件, 开启 TLSPre-Master-Secret 打印, 配合 tcpdump 抓包之后, 就可以在 Wireshark 查看完整的明文数据报文.

对于前面提到的3点诉求, 基本都给做到了:

  • 侵入性小: 仅替换系统一个原生库, 不修改任何上层业务代码;
  • 作用范围广: 修改全系统生效, 各个模块无需单独适配;
  • 还原性强: 不修改任何数据, 支持观察系统原始行为;
  • 本文作者: 6x
  • 本文链接: https://6xyun.cn/article/174
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-ND 许可协议。转载请注明出处!