双向证书效验

0-前言

​ 工作下的uat测试环境,采用了TLCP的双向认证,因为是测试环境可以webview+iptables取巧的办法绕过了双向认证,还是得学一下常见的生产环境下怎么获得客户端的证书之类的操作比较好。

开源项目SSL 实现-google

所用到的工具:

jadxfridabp/charles/mitm/yakitwiresharkWSL2

学习文章抓包之双向证书校验HTTPS 双向认证抓包指南frida-analykit wireshark 流量抓包(上)frida-analykit wireshark 流量抓包(下)

主要记录大佬frida-analykit的查看方法,更为详细的步骤可以跳转到大佬的项目观看

操作准备(WSL2):

1
2
3
4
#记得开放对应端口防火墙然后再映射对应adb的端口---------无线调试	//要改源代码才行因为源代码默认只支持-U
adb tcpip 5555 #连接USB然后设定无限调试的端口
#或者使用 adb pair 192.168.xx.xx:xxx [匹配码] #匹配码匹配
netsh interface portproxy add v4tov4 listenaddress=<ip> listenport=<port> connectaddress=<ip> connectport=<port>

如果觉得太过于麻烦而且不安全你可以下载usbipd-win

1
2
3
4
#直接把usb接口转发给wsl2--记得先关闭adb---------------USB调试
usbipd list
usbipd bind --busid=1-3
usbipd attach --wsl --busid=1-3

1-双向认证 (mTLS)

普通 HTTPS: 通常只有单向认证

②双向认证常用协议

Ⅰ.ISCP/gRPC/MQTT/VPN/CoAP

协议 传输层 核心用途 为什么用双向认证
MQTT TCP 物联网设备通信 防止恶意设备接入云端
gRPC HTTP/2 内部微服务调用 零信任架构,防止内网渗透
CoAP UDP 资源受限的硬件 轻量化,同时保证身份合法
IPsec/VPN 网络层 企业远程接入 确保只有公司授权的电脑能连内网
特性 MQTT / gRPC / IPsec ISCP (TCLP常见)
开放性 公开标准,谁都能实现 私有/半私有,通常随 SDK 分发
主要负载 JSON / Protobuf / 报文 音视频流 + 实时控制指令
认证严苛度 只要证书对就行 证书可能二次加密,且与 AppID 强绑定
实时性 较好 极高(针对音视频同步做了算法优化)

2-私钥的存储

①常规的java层证书校验

依赖的是Android框架层的Java TLS API(HttpsURLConnection、SSLSocket、X509TrustManager等)

方法:

hook掉所有涉及到的常规证书校验的java层校验类来强制验证通过,或者java-hook自吐密钥或请求

工具:

JustTrustMeobjectionssl_Pinningr0capture自吐密钥

②so层双向认证难点

Ⅰ.native

(特征值检查、汇编混淆、so自检)

1>so层证书/密码提取困难

2>so层绕过服务器证书认证困难

Ⅱ.魔改/自定义协议通信

(处理异常、解析异常)

1>代理服务器工具对ISCP/gRPC/MQTT/VPN/CoAP/TLCP这些五花八门的协议不太支持


私钥存储一般(ai回复):

  • 存在 Android Keystore
  • 或存于 硬件安全模块(TEE
  • 或存在 国密SDK内部结构体

可以查看send() 栈里直接输出libxxxsec.so!0x12345等非常见的so层,自研层的可能需要用到unidbgida.pro,处理so要是entropy还高得离谱像这样那就真是有福了

注:Xref作用

Ⅰ.pdf的地图
Ⅱ.ida.pro搜索引用该字符串的指令

3-操作

大佬提供了一个解决方案:
就是直接监听内存的密钥,然后用wireshark查看请求

frida-analykit wireshark 流量抓包

Ⅰ.原理

SSL 实现-google里面排查ssl_log_secret可得:

CLIENT_RANDOM 这个常量字符串将作为我们定位ssl_log_secret的入口

Ⅱ.注意

com.frida_analykit.ssl_log_secret测试app如果安装不了,可以看自己的手机架构

build.gradleandroid参数里增加

1
2
3
ndk {
abiFilters.add("arm64-v8a")
}
1
gradlew.bat clean assembleDebug

②提取sslkey

1
2
#常规so
script.jsh('SSLTools').attachBoringsslKeylogFunc({ 'libname': 'libssl.so' })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# ---------------------------------------------------------
# 第一阶段:建立连接与对象代理
# ---------------------------------------------------------

# script.jsh 是该框架特有的方法,用于在 Python 中“映射” JS 层的全局对象
# 就像是在 Python 里建立了一个通往手机 App 内存的“遥控器”
proc = script.jsh('Process') # 遥控 App 里的【进程与模块】
ssltools = script.jsh('SSLTools') # 遥控 App 里的【SSL 调试工具】

# ---------------------------------------------------------
# 第二阶段:目标排查(防止“空挂钩”)
# ---------------------------------------------------------

# 我们不能盲目 Hook,必须先确认这个库(.so 文件)是否已经加载到 App 的内存里了
# 场景:如果是 Flutter 应用,SSL 逻辑通常在 libflutter.so 里
libflutter = proc.findModuleByName('libflutter.so')

# 【重要】查看模块信息
# 如果输出为 None 或者报错,说明 App 还没运行到相关功能,你需要去点点 App 界面
print(f"libflutter 模块详情: {libflutter.value}")

# ---------------------------------------------------------
# 第三阶段:实施挂钩(三种常见方案)
# ---------------------------------------------------------

### 方案 A:针对系统/标准库(最基础)
# 适用于大部分使用系统 WebView 或标准 Http 连接的 App
# 指向 libssl.so,脚本会自动在里面搜索 ssl_log_secret 函数
print("[*] 正在尝试挂钩 libssl.so...")
ssltools.attachBoringsslKeylogFunc({ 'libname': 'libssl.so' })


### 方案 B:针对 Flutter 应用(精准定位)
# 如果 libflutter.value 有值,说明找到了文件。
# 我们直接把这个“模块对象”传给 Hook 函数,这比传名字更稳,不容易出错。
if libflutter.value:
print("[*] 发现 Flutter 库,正在通过对象指针实施 Hook...")
ssltools.attachBoringsslKeylogFunc({ 'mod': libflutter })


### 方案 C:针对延迟加载的库(如 WebView)
# 注意:像 libmonochrome_64.so 这种库,只有在你点击“打开网页”后才会出现
# 所以操作顺序是:手机点击打开网页 -> 执行下面这行代码
print("[*] 正在尝试挂钩 WebView 核心库...")
ssltools.attachBoringsslKeylogFunc({ 'libname': 'libmonochrome_64.so' })

注意:

如果 App 根本不用 OpenSSL/BoringSSL,而是自研了一套加密协议,仍然需要从头开始分析

启动脚本

1
2
3
4
#开头要运行
npm run watch
python frida-analykit/main.py bootup-server
./ptpython_spawn.sh

命令太多了所以整合成了一个启动的脚本:

1
2
3
4
./service.sh start     # 启动(含前台 ptpython)
./service.sh stop # 停止后台服务
./service.sh status # 查看状态
./service.sh restart # 重启
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#!/bin/bash

set -e

LOG_DIR="logs"
PID_DIR="pids"

mkdir -p "$LOG_DIR" "$PID_DIR"

# ========= 通用函数 =========

is_running() {
local pid=$1
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
return 0
fi
return 1
}

start_daemon() {
local name=$1
local cmd=$2
local log_file="$LOG_DIR/$name.log"
local pid_file="$PID_DIR/$name.pid"

if [ -f "$pid_file" ]; then
old_pid=$(cat "$pid_file")
if is_running "$old_pid"; then
echo "[!] $name already running (PID $old_pid)"
return
else
echo "[!] removing stale PID file for $name"
rm -f "$pid_file"
fi
fi

echo "[+] starting $name ..."

nohup bash -c "
while true; do
echo '[*] $(date) starting $name'
$cmd
echo '[!] $(date) $name crashed, restarting in 2s...'
sleep 2
done
" > "$log_file" 2>&1 &

echo $! > "$pid_file"
echo "[+] $name started (PID $(cat $pid_file))"
}

stop_daemon() {
local name=$1
local pid_file="$PID_DIR/$name.pid"

if [ ! -f "$pid_file" ]; then
echo "[!] $name not running (no pid file)"
return
fi

pid=$(cat "$pid_file")

if is_running "$pid"; then
kill "$pid"
echo "[+] stopped $name (PID $pid)"
else
echo "[!] $name already stopped"
fi

rm -f "$pid_file"
}

status_daemon() {
local name=$1
local pid_file="$PID_DIR/$name.pid"

if [ -f "$pid_file" ]; then
pid=$(cat "$pid_file")
if is_running "$pid"; then
echo "[RUNNING] $name (PID $pid)"
else
echo "[DEAD] $name (stale PID $pid)"
fi
else
echo "[STOPPED] $name"
fi
}

# ========= 业务控制 =========

start() {
echo "========== START =========="

start_daemon "npm_watch" "npm run watch"
start_daemon "frida" "python frida-analykit/main.py bootup-server"

echo ""
echo "[*] 启动 ptpython(前台)..."
echo "[*] 退出 ptpython 后不会影响后台服务"
echo "-----------------------------------"

./ptpython_spawn.sh
}

stop() {
echo "========== STOP =========="

stop_daemon "npm_watch"
stop_daemon "frida"
}

status() {
echo "========== STATUS =========="

status_daemon "npm_watch"
status_daemon "frida"
}

restart() {
stop
sleep 1
start
}

# ========= 入口 =========

case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac