在Android上抓HTTPS包,我们会通常使用不限于charles, fiddler等工具作为中间人:
在上面这些抓包工具的实现原理中,伪造证书这一点就是他们毕生无法绕过的坎,基于这个破绽就可以有下面的检测方式:
判断在请求的时候获取到的服务端证书链中的leaf证书公钥指纹 和 真实的服务端leaf证书的公钥指纹(发版时候硬编码到app) 是否吻合来确定是不是伪造来判断是否有中间人,这种方式就是我们所说的ssl pinning
。
而mtls则是更严格的校验,服务端也要验客户端的证书(硬编码在app),就意味着你必须要用拥有服务端自签发的信任证书的客户端发起的访问,服务端才受理。既然这样,在中间人伪造了证书在没有配置信任客户端证书的情况下,请求会被拦在跟服务端握手时候的证书校验上。
对于常规的java层证书校验方式,如TrustKit-Android
依赖的是Android框架层的Java TLS API(HttpsURLConnection、SSLSocket、X509TrustManager等),所以针对于这些证书校验,一些工具譬如JustTrustMe
和objection
,就是hook掉所有涉及到的常规证书校验的java层校验类来强制验证通过。
即便这些工具在java层已经hook了这么多方法,但是依然存在着会使用native层来进行证书校验的方式,存在魔改的http或者自定义协议的通信的方式,这些对于http协议的抓包工具来说都是束手无策的。
当然,其实前面说这么多,并不是为了贬低中间人代理类抓包工具,也不是为了争个高低对错,我只是想表达的是中间人代理这类抓包软件作为首选永远不会错,但是当你感到迷茫的时候,请不要忘记还有wireshark站在你的背后。
好了,屁话有点多了。进入正题,下面我会基于从目标出发,来说明如何在不使用中间人代理的方式让wireshark实时抓包并且自动解密tls。
文章会分为上下两篇:
wireshark是支持对用户提供的sslkey.log
来解密对应匹配的tls流量,持续的写入sslkey.log,wireshark会使用类似于tail的方式读取新sslkey.
wireshark需要的sslkey.log包含的信息会是类似于下面这样:
每一行可以分成三段:
label
: 通常会有CLIENT_RANDOM
, CLIENT_HANDSHAKE_TRAFFIC_SECRET
, EXPORTER_SECRET
等label,其中最关键的是 CLIENT_RANDOM
。
Client Random
: 32字节(64个hex字符),握手阶段(Cient Hello)客户端发起Hello时候的随机数,这个值也是wireshark用来定位要解密tls报文的ID
Master Secret
: 48字节(96个hex字符),TLS 握手计算出来的主密钥。简单的可以理解成这是用于生成后续客户端和服务端通信的加密数据的密钥的重要参数。( 可以理解成gen_data_secret(master_secret)
)
图1
图2
有了前面这些认识,再次确认我们需要完成的目标:通过任何可行的方式,让要抓包的app上tls流量的sslkey实时写出到pc日志文件上,以让wireshark能够读取解密tls流量。
在Android 7之后,aosp的ssl就从openssl换到了boringssl,所以这里我们只会从boringssl项目来研究(虽然我们需要关注的地方大概率并没有区别)。
boringssl项目: 529K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Y4L8$3!0Y4L8r3g2Q4x3V1k6T1L8%4u0A6L8X3N6K6M7$3H3`.
在aosp中boringssl会生成libssl.so
,是java层SSLSocket / SSLContext等的native支持。这是一句没有提供任何依据的结论描述。对于没有了解过java层是怎么和native关联起来的人来说,这其实很依赖死记硬背,虽然了解这些并这不是必要的,但如果感兴趣的可以看:下面我会以一个不知道这个事实的角度来,从一个简单的例子中跟踪为什么java层定位到libssl.so。(如果已经了解或并不在意,可省略该部分跳转)
为什么是这个方法?
下面的代码段来自 boringssl项目的src/ssl/ssl_lib.cc
文件
确定要hook的函数的时候,最先想到的可能会是:
为什么呢?最快速确定就是通过readelf来确定符号名称是否存在,或者使用hex编辑器来搜索字符串来确定存在并且在.symtab段(事实上如果是使用ida pro的话,他能显示并不能反映符号的存在,因为他会依赖其他的信息手段来确定函数符号名);elf是否能够保留STB_LOCAL
函数符号究其根本在编译的时候就确定了,通过external/boringssl/Android.bp
的配置也能确定这些事实。
SSL_CTX_set_keylog_callback
作为导出符号,自然获取其符号地址起来也是非常的简单,在这之前,我们需要拿到要注册回调函数的SSL_CTX *ctx
才能够继续,这个要怎么办呢?
前面我们在跟踪java层到native层中,可以看到建立连接,首先需要的是 SSL_new
。
所以我们的策略是,通过attach符号SSL_new
,使用参数SSL_CTX* ctx
来使用符号SSL_CTX_set_keylog_callback
进行注册回调函数。
到这里我们完成了对libssl.so
中tls的密钥打印。但这就是所有了吗?不,还没有,wireshark还拿不到这些数据,我们还需要使用rpc来接管所有的sslkey。
frida提供了一套rpc的通信实现:允许脚本中使用rpc.exports来注册导出函数定义,并且使用send和recv来通信:(frida-analykit实现了这一套,按照文档配置使用即可)
frida-analykit的导出函数注册位于 frida-analykit/script/rpc.ts
frida-analykit的python的rpc接受的数据代理实现位于 frida-analykit/agent/rpc/resolver.py
这一部分我只列举了实现原理依据,其余都是代码开发、项目组织层面的,这些我就不过多说了(真想理解看代码自己消化更高效)。实现细节可以跳转阅读frida-analykit
光是第一眼看到有这么多文字,耐心就足以被削去一半。说到这些,其实我是更倾向于直接把演示测试这一段放到前头(先通过图片看看怎么个事,再决定要不要细看,毕竟大家时间都很宝贵),但是想了想这会使得文章的行文组织过于跳脱、突兀,所以作罢,仅留下一把跳伞用于定位。
下面的资源都可以跳转工具下载,其中下面的测试资源
都是相对于该目录下的
图3
图3可以看到app的大概情况,接下来我们会演示验证下面两点:
验证frida-analykit + wireshark是否能规避类似于ssl pinning的证书校验。(虽然从原理上是显而易见的,但是没有比能看到事实更让人感到安心的)
wireshark能够实时抓包解密tls到并且展示http协议。
在win上使用softAP方式开热点的通常会自动创建Wi-Fi Direct
网卡,在wireshark 捕获选项
中选择该网卡(通过任务管理器可以确定名称)
推荐使用这种方式来让设备进行捕获,不然会混杂其他设备过多的流量还需要写过滤规则
图4
./ptpython_spawn.sh
来启动脚本index.ts
点击"发起请求"按钮,发起https请求,
sslkey.log文件只会在有数据进来的时候才会创建,所以一般会在发起一次请求后才去按图1配置,当然你手动创建也不是不行
图5
图6
图7
图5可以看出报文已经成功解密,其中标注了三个框:上框=过滤规则(只保留http协议相关);中框=关联报文(关联的请求和响应);下框=报文内容。
从wireshark不是针对http协议的,所以用起来是没有charles,fiddler直观
图8
图9
图10
这篇主要是借用libssl.so
这个有明显符号导出的boringssl进行hook,一个简单的例子来说明这一套流量抓包的流程和原理。
但是实战中会能这么简单如愿的hook到ssl_log_secret
吗?答案是否定的,现实情况是,即便是常用的webview,用的都不是动态库libssl.so
,而是自己静态链接了boringssl的,这就意味着除了常规的java层原生实现的证书校验外,其余的tls流量都无法解密,如果仅仅只是到这种程度,当然算不上可用。所以下篇会针对性的讨论我们该如何根据特征来对静态链接了boringssl(譬如flutter和webview),但没有导出符号的ssl_log_secret函数进行hook(实际上也适用于libssl.so)。
URL url =
new
URL(
"e69K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6W2P5r3q4E0M7r3I4W2i4K6u0W2j5$3!0E0"
);
URLConnection conn = url.openConnection();
conn.connect()
java.net.URL.openConnection()
v
abstract
URLStreamHandler
├── com.android.okhttp.HttpsHandler.openConnection()
├── com.android.okhttp.HttpHandler.openConnection()
├── com.android.okhttp.HttpHandler.newOkUrlFactory()
├── com.android.okhttp.OkUrlFactory.open()
├── com.squareup.okhttp.internal.huc.HttpsURLConnectionImpl()
├── com.squareup.okhttp.internal.huc.DelegatingHttpsURLConnection()
├── com.squareup.okhttp.internal.huc.HttpURLConnectionImpl()
v
abstract
URLConnection
v
com.squareup.okhttp.internal.huc.HttpURLConnectionImpl.connect()
v
com.squareup.okhttp.internal.huc.HttpURLConnectionImpl.execute()
v
com.squareup.okhttp.internal.http.HttpEngine.sendRequest()
v
com.squareup.okhttp.internal.http.HttpEngine.connect()
v
com.squareup.okhttp.internal.http.StreamAllocation.newStream()
v
com.squareup.okhttp.internal.http.StreamAllocation.findHealthyConnection()
v
com.squareup.okhttp.internal.http.StreamAllocation.findConnection()
v
com.squareup.okhttp.internal.io.RealConnection()
v
com.squareup.okhttp.internal.io.RealConnection.connect()
v
com.squareup.okhttp.internal.io.RealConnection.connectSocket()
v
com.squareup.okhttp.internal.io.RealConnection.connectTls()
v
abstract
SSLSocketFactory
├── org.conscrypt.OpenSSLSocketFactoryImpl.createSocket()
├── org.conscrypt.Platform.createFileDescriptorSocket()
├── org.conscrypt.ConscryptFileDescriptorSocket()
├── org.conscrypt.newSsl()
├── org.conscrypt.NativeSsl.newInstance()
├── org.conscrypt.NativeCrypto.SSL_new()
v
static
native
long
SSL_new(
long
ssl_ctx, AbstractSessionContext holder)
throws
SSLException;
v
v
com.android.org.conscrypt.NativeCryptoJni.init()
v
System.loadLibrary(
"javacrypto"
)
├──
| cc_defaults {
| name:
"libjavacrypto-defaults"
,
|
| cflags: [
|
"-Wall"
,
|
"-Wextra"
,
|
"-Werror"
,
|
"-Wunused"
,
|
"-fvisibility=hidden"
,
| ],
|
| srcs: [
"common/src/jni/main/cpp/**/*.cc"
],
| local_include_dirs: [
"common/src/jni/main/include"
],
| }
|
| cc_library_shared {
| name:
"libjavacrypto"
,
|
| shared_libs: [
|
"libcrypto"
,
|
"liblog"
,
|
"libssl"
,
| ],
|
| apex_available: [
|
"com.android.conscrypt"
,
|
"test_com.android.conscrypt"
,
| ],
|
| }
|
├──
| jint libconscrypt_JNI_OnLoad(JavaVM* vm,
void
*)
|
├──
| NativeCrypto::registerNativeMethods(env);
| ├── ...
| ├── CONSCRYPT_NATIVE_METHOD(SSL_new,
"(J"
REF_SSL_CTX
")J"
)
| ├── ...
|
static
jlong NativeCrypto_SSL_new(JNIEnv* env, jclass, jlong ssl_ctx_address, CONSCRYPT_UNUSED jobject holder)
| ├── SSL_new()
|
├──
|
void
jniRegisterNativeMethods(JNIEnv* env,
const
char
* className,
const
JNINativeMethod* gMethods,
int
numMethods)
| env->RegisterNatives()
v
static
native
long
SSL_new(
long
ssl_ctx, AbstractSessionContext holder)
throws
SSLException;
v
........
(end)
URL url =
new
URL(
"e69K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6W2P5r3q4E0M7r3I4W2i4K6u0W2j5$3!0E0"
);
URLConnection conn = url.openConnection();
conn.connect()
java.net.URL.openConnection()
v
abstract
URLStreamHandler
├── com.android.okhttp.HttpsHandler.openConnection()
├── com.android.okhttp.HttpHandler.openConnection()
├── com.android.okhttp.HttpHandler.newOkUrlFactory()
├── com.android.okhttp.OkUrlFactory.open()
├── com.squareup.okhttp.internal.huc.HttpsURLConnectionImpl()
├── com.squareup.okhttp.internal.huc.DelegatingHttpsURLConnection()
├── com.squareup.okhttp.internal.huc.HttpURLConnectionImpl()
v
abstract
URLConnection
v
com.squareup.okhttp.internal.huc.HttpURLConnectionImpl.connect()
v
com.squareup.okhttp.internal.huc.HttpURLConnectionImpl.execute()
v
com.squareup.okhttp.internal.http.HttpEngine.sendRequest()
v
com.squareup.okhttp.internal.http.HttpEngine.connect()
v
com.squareup.okhttp.internal.http.StreamAllocation.newStream()
v
com.squareup.okhttp.internal.http.StreamAllocation.findHealthyConnection()
v
com.squareup.okhttp.internal.http.StreamAllocation.findConnection()
v
com.squareup.okhttp.internal.io.RealConnection()
v
com.squareup.okhttp.internal.io.RealConnection.connect()
v
com.squareup.okhttp.internal.io.RealConnection.connectSocket()
v
com.squareup.okhttp.internal.io.RealConnection.connectTls()
v
abstract
SSLSocketFactory
├── org.conscrypt.OpenSSLSocketFactoryImpl.createSocket()
├── org.conscrypt.Platform.createFileDescriptorSocket()
├── org.conscrypt.ConscryptFileDescriptorSocket()
├── org.conscrypt.newSsl()
├── org.conscrypt.NativeSsl.newInstance()
├── org.conscrypt.NativeCrypto.SSL_new()
v
static
native
long
SSL_new(
long
ssl_ctx, AbstractSessionContext holder)
throws
SSLException;
v
v
com.android.org.conscrypt.NativeCryptoJni.init()
v
System.loadLibrary(
"javacrypto"
)
├──
| cc_defaults {
| name:
"libjavacrypto-defaults"
,
|
| cflags: [
|
"-Wall"
,
|
"-Wextra"
,
|
"-Werror"
,
|
"-Wunused"
,
|
"-fvisibility=hidden"
,
| ],
|
| srcs: [
"common/src/jni/main/cpp/**/*.cc"
],
| local_include_dirs: [
"common/src/jni/main/include"
],
| }
|
| cc_library_shared {
| name:
"libjavacrypto"
,
|
| shared_libs: [
|
"libcrypto"
,
|
"liblog"
,
|
"libssl"
,
| ],
|
| apex_available: [
|
"com.android.conscrypt"
,
|
"test_com.android.conscrypt"
,
| ],
|
| }
|
├──
| jint libconscrypt_JNI_OnLoad(JavaVM* vm,
void
*)
|
├──
| NativeCrypto::registerNativeMethods(env);
| ├── ...
| ├── CONSCRYPT_NATIVE_METHOD(SSL_new,
"(J"
REF_SSL_CTX
")J"
)
| ├── ...
|
static
jlong NativeCrypto_SSL_new(JNIEnv* env, jclass, jlong ssl_ctx_address, CONSCRYPT_UNUSED jobject holder)
| ├── SSL_new()
|
├──
|
void
jniRegisterNativeMethods(JNIEnv* env,
const
char
* className,
const
JNINativeMethod* gMethods,
int
numMethods)
| env->RegisterNatives()
v
static
native
long
SSL_new(
long
ssl_ctx, AbstractSessionContext holder)
throws
SSLException;
v
........
(end)
int
ssl_log_secret(
const
SSL *ssl,
const
char
*label,
const
uint8_t *secret,
size_t
secret_len) {
if
(ssl->ctx->keylog_callback == NULL) {
return
1;
}
ScopedCBB cbb;
uint8_t *out;
size_t
out_len;
if
(!CBB_init(cbb.get(),
strlen
(label) + 1 + SSL3_RANDOM_SIZE * 2 + 1 +
secret_len * 2 + 1) ||
!CBB_add_bytes(cbb.get(), (
const
uint8_t *)label,
strlen
(label)) ||
!CBB_add_bytes(cbb.get(), (
const
uint8_t *)
" "
, 1) ||
!cbb_add_hex(cbb.get(), ssl->s3->client_random, SSL3_RANDOM_SIZE) ||
!CBB_add_bytes(cbb.get(), (
const
uint8_t *)
" "
, 1) ||
!cbb_add_hex(cbb.get(), secret, secret_len) ||
!CBB_add_u8(cbb.get(), 0
) ||
!CBB_finish(cbb.get(), &out, &out_len)) {
return
0;
}
ssl->ctx->keylog_callback(ssl, (
const
char
*)out);
OPENSSL_free(out);
return
1;
}
struct
ssl_ctx_st {
void
(*keylog_callback)(
const
SSL *ssl,
const
char
*line) = nullptr;
}
int
ssl_log_secret(
const
SSL *ssl,
const
char
*label,
const
uint8_t *secret,
size_t
secret_len) {
if
(ssl->ctx->keylog_callback == NULL) {
return
1;
}
ScopedCBB cbb;
uint8_t *out;
size_t
out_len;
if
(!CBB_init(cbb.get(),
strlen
(label) + 1 + SSL3_RANDOM_SIZE * 2 + 1 +
secret_len * 2 + 1) ||
!CBB_add_bytes(cbb.get(), (
const
uint8_t *)label,
strlen
(label)) ||
!CBB_add_bytes(cbb.get(), (
const
uint8_t *)
" "
, 1) ||
!cbb_add_hex(cbb.get(), ssl->s3->client_random, SSL3_RANDOM_SIZE) ||
!CBB_add_bytes(cbb.get(), (
const
uint8_t *)
" "
, 1) ||
!cbb_add_hex(cbb.get(), secret, secret_len) ||
!CBB_add_u8(cbb.get(), 0
) ||
!CBB_finish(cbb.get(), &out, &out_len)) {
return
0;
}
ssl->ctx->keylog_callback(ssl, (
const
char
*)out);
OPENSSL_free(out);
return
1;
}
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2025-4-25 18:30
被zsa233编辑
,原因: 回填下篇地址,改成点赞回复可见