客户端校验

image-20220928212448549

SSL PINNING是Google官方推荐的校验方式,原理是在客户端(安卓APP)中预先设置好证书信息,握手时与服务端返回的证书进行比较。

如果有这种客户端校验,那么charles等抓包工具,就无法抓包了。因为charles作为中间人返回给客户端的证书信息与原客户端预先设置的不一致,所以,客户端检测到,就拒绝发送请求了。

image-20240517191811016

1.公钥校验(Pinner)

开发一个安卓应用,了解下这个验证到底是怎么实现的。例如:百度APP。

1.1 基于代码

1.1.1 获取sha256公钥

http://slproweb.com/products/Win32OpenSSL.html

image-20240517191824393

1
>>>openssl s_client -connect www.baidu.com:443 -servername www.baidu.com | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
1
Zhv4cvwdHmEmE0edWEcIdmLfwsqxrrOmp+vbngwNnrU=
1
>>>openssl x509 -in baidu.pem -pubkey  -noout  | openssl rsa  -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
1
下载PEM证书 => 转换公钥 => SHA256加密 => Base64编码

image-20240517191837030

也可以同代码生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
info = """MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqi/MQY0lroPp9CfEALM5
bw6YKlV9B+WASYL609OFmLXfe2+7At3teOQMByueHoZL9mqGWNdXbyFZEdhvlm7S
3jYo9rTjzpUyKQDBZY5psAD+Ujf0iD+LbQ+78OzFwDHvrbUMBmatvtxDE8RmsF3P
VlPi0ZaCHAa7m1/tYI3S7fPSUO67zbI2l8jOe9JLt1y0iMo3bovO+Zb9tPVHtSB3
u/yonYGybPjHCWrdIm6DP6dT3/HaLylrIsPpHWXoxaC6E04WPwOT8KVZihqA6Cd9
SSPf0flLl7cBxBn18cX/kTPQoXTG7tTP9jgM7b1eqkT7iPd7mXB2NFV+VdIPnr+U
kwIDAQAB"""

import base64
import hashlib

v1 = base64.b64decode(info)

obj = hashlib.sha256(v1)
res = obj.digest()

v2 = base64.b64encode(res)
print(v2) # b'Zhv4cvwdHmEmE0edWEcIdmLfwsqxrrOmp+vbngwNnrU='
1
>>>openssl s_client -connect www.baidu.com:443 -servername www.baidu.com | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha1 -binary | openssl enc -base64
1
ogF5HvFnVP8hLIG2b0JNw3ajJAE=

1.1.2 配置

在安卓中开启网络请求的权限。

1
2
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
dependencies {

// implementation 'androidx.appcompat:appcompat:1.6.1'
// implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.6.0'

implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

implementation "com.squareup.okhttp3:okhttp:3.14.9"
}

image-20240517191850675

image-20240517191857998

1
implementation "com.squareup.okhttp3:okhttp:3.14.9"

1.1.3 发送请求

image-20240517191907590

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
private void doRequest() {
new Thread() {
@Override
public void run() {

final String CA_PUBLIC_KEY = "sha256/Zhv4cvwdHmEmE0edWEcIdmLfwsqxrrOmp+vbngwNnrU=";
final String CA_DOMAIN = "www.baidu.com";

//校验公钥
CertificatePinner pinner = new CertificatePinner.Builder().add(CA_DOMAIN, CA_PUBLIC_KEY).build();

OkHttpClient client = new OkHttpClient.Builder().certificatePinner(pinner).build();

Request req = new Request.Builder().url("https://www.baidu.com/?q=defaultCerts").build();
Call call = client.newCall(req);
try {
Response res = call.execute();
Log.e("请求成功", "状态码:" + res.code());

} catch (IOException ex) {
Log.e("请求失败", "异常" + ex);
}
}
}.start();
}

1.2 基于配置

详见示例:NetDemo2.zip

1.2.1 获取sha256公钥

image-20240517191917443

1
>>>openssl s_client -connect www.baidu.com:443 -servername www.baidu.com | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
1
Zhv4cvwdHmEmE0edWEcIdmLfwsqxrrOmp+vbngwNnrU=

1.2.2 配置

image-20240517191925258

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!--公钥校验-->
<domain-config>
<domain includeSubdomains="true">baidu.com</domain>
<pin-set>
<pin digest="SHA-256">Zhv4cvwdHmEmE0edWEcIdmLfwsqxrrOmp+vbngwNnrU=</pin>
</pin-set>
</domain-config>
</network-security-config>

1.2.3 发送请求

image-20240517191940999

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
package com.nb.netdemo2;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import java.io.IOException;

import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity {
private Button btn1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

btn1 = findViewById(R.id.btn1);
btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doRequest();
}
});
}

private void doRequest() {
new Thread() {
@Override
public void run() {
OkHttpClient client = new OkHttpClient.Builder().build();
Request req = new Request.Builder().url("https://www.baidu.com/?q=defaultCerts").build();
Call call = client.newCall(req);
try {
Response res = call.execute();
Log.e("请求发送成功", "状态码:" + res.code());

} catch (IOException ex) {
Log.e("Main", "网络请求异常" + ex);
}
}
}.start();
}

}

2.证书校验

2.1 基于代码

详见示例:NetDemo3.zip

2.1.1 获取PEM证书

image-20240517191955388

image-20240517192002522

image-20240517192010833

image-20240517192016887

2.1.2 配置

image-20240517192023883

2.1.3 发送请求

image-20240517192033705

image-20240517192042609

2.2 基于配置

详见示例:NetDemo4.zip

2.2.1 获取PEM证书

image-20240517192054490

image-20240517192104485

image-20240517192112731

image-20240517192120931

2.2.2 配置

image-20240517192127666

2.2.3 发送请求

image-20240517192134870

3.Host校验

详见示例:NetDemo5.zip

3.1 配置

image-20240517192143492

3.2 发送请求

image-20240517192151163

验证,请求的地址必须是指定域名,verify方法返回:

  • true,校验通过
  • false,校验失败

此处,你可能会有疑问,我的请求已经www.baidu.com 了,何必再在HostnameVerifier中的verfy再校验一次呢?

1
2
3
4
5
一般Host校验会跟证书校验结合,在发送请求时,先校验证书、再校验Host。

以百度为例,很多域名共用一个证书,例如:[baidu.com, baifubao.com, www.baidu.cn, www.baidu.com.cn, mct.y.nuomi.com, apollo.auto, dwz.cn, *.baidu.com, *.baifubao.com, *.baidustatic.com, *.bdstatic.com, *.bdimg.com, *.hao123.com, *.nuomi.com等。
那么,当预设证书后无论访问 baidu.com 或 hao123.com均能通过校验。
如果再结合 HostnameVerifier的verfy再次校验,就能在客户端对请求进一步认证。

4.证书+Host校验

详见示例:NetDemo6.zip

image-20240517192234073

image-20240517192242210

5.如何过客户端校验?

想要解决客户端的校验,本质上就是通过Hook的机制将原本校验的位置替换成自定义逻辑(直接不校验)。

5.1 frida hook脚本

  • 证书校验
    image-20240517192251747

  • Host校验

    image-20240517192259179

  • pinner校验
    image-20240517192306201

上述是以okhttp为例的截图,真正开发时开发会使用其他的模块来发送网络请求,frida中提供了常见网络库相关绕过客户端校验的Hook脚本,例如:

1
2
3
4
5
6
7
/*
* This script combines, fixes & extends a long list of other scripts, most notably including:
*
* - https://codeshare.frida.re/@akabe1/frida-multiple-unpinning/
* - https://codeshare.frida.re/@avltree9798/universal-android-ssl-pinning-bypass/
* - https://pastebin.com/TVJD63uM
*/

详见:demo/frida_multiple_unpinning.js

1
2
3
4
Run with:

frida -U -f [APP_ID] -l frida_multiple_unpinning.js 重启app
frida -UF -l frida_multiple_unpinning.js 不重启

image-20240517192317833

image-20240517192325356

5.2 JustTrustMe

本质上是xposed的Hook脚本,只不过可以打包安装在手机直接运行。

  • 安装 Magisk 面具(手机root)
  • 在面具中刷入 LSPosed框架
  • 安装 JustTrustMe
  • 在LSPosed框架中配置并启动 JustTrustMe

5.2.1 Magisk面具

请根据自己手机的机型去root并安装面具,参考链接:

1
2
3
https://www.bilibili.com/video/BV1Ly4y1u7YE/
https://www.bilibili.com/video/BV1er4y1C7wU
https://magiskcn.com/

提示:后续的操作均使用的Magisk面具 24.0 版本。

5.2.2 刷入LSPosed

  • Riru-LSPosed

    1
    2
    - 先刷Riru            https://github.com/RikkaApps/Riru/releases
    - 在刷Riru-LSPosed https://github.com/LSPosed/LSPosed/releases
  • Zygisk-LSPosed(推荐)

    1
    - 刷Zygisk-LSPosed   https://github.com/LSPosed/LSPosed/releases

注意:在面具中可以根据是否开启 Zygisk,来切换和生效。

image-20240517192340808

示例1:红米8A

image-20240517192347367

image-20240517192356040

问题:LSPosed无图标

刷入成功后,就可以看到 LSPosed 的图标,如果没有出现的话,就去手机的 /data/adb/lspd/目录下找apk包,然后再点击安装即可。

image-20240517192405982

问题:LSPosed未激活

正常安装完LSPosed会直接激活,如果LSPosed显示未激活,请点击 图1 中Magisk安装,根据步骤点击安装,之后LSPosed就可以激活了。

image-20240517192418617

示例2:红米Note 9Pro

image-20240517192428238

5.2.3 安装并启动JustTrustMe

JustTrustMePlus.apk 安装到手机(跟安装其他app一样)。

安装成功后,在LSPosed的模块列表中可以看到 JustTrustMe.

image-20240517192436891

image-20240517192446010

image-20240517192454261

6.实战案例

6.1 安居客

版本:v16.13.2

https://www.wandoujia.com/apps/280945/history_v322000

  • JustTrustMe 【记得重启APP】

  • frida_multiple_unpinning.js

    1
    2
    frida -U -f com.anjuke.android.app -l frida_multiple_unpinning.js --no-pause
    frida -U -f com.anjuke.android.app -l frida_multiple_unpinning.js

    image-20240517192503976

    注意:存在反调试,请先删除 libmsaoaidsec.so

7 笔记

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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
抓包专题

1.为什么要做抓包专题?
- 防护,防止你抓包
- 实现抓包

2.今日概要:客户端证书校验(安居客)
- 什么是客户端证书校验?
- 如何绕过?【傻瓜式】
- 校验的原理 & 绕过的机制

3.什么是客户端证书校验?
- http请求,没有任何的校验(很少有人http)
- https请求
- 唯品会APP,安卓开发者没有在代码中编写验证证书的逻辑。
- 安居客APP,安卓开发者有在代码中编写验证证书的逻辑。
- 现象:唯品会APP vs 安居客APP
- 绕过客户端证书校验,本质:通过Hook机制实现
- frida + js代码 -> Hook绕过 【方法1】
- xposed + java代码 -> Hook绕过
xposed + justtrustme组件 -> Hook绕过 【方法2】

3.1 Frida的Hook绕过客户端证书校验
问题:
你知道客户端证书校验的代码是怎么实现的?
如果知道,你就可以了解他是执行的那个方法进行校验。
直接找到响应的包和方法Hook就能绕过。

1.去课件中找到 frida_multiple_unpinning.js
2.frida执行进行hook
3.运行环境
Python3.7.9 + frida==16.0.1 + frida-tools==12.0.1

>>>frida -U -f com.anjuke.android.app -l frida_multiple_unpinning.js

注意:安居客APP里面有frida检测,先解决frida检测,删除 libmsaoaidsec.so

总结经验:
SSL handshake with client failed: An unknown issue occurred processing the certificate (certificate_unknown)
`尝试`使用frida_multiple_unpinning.js进行Hook绕过

3.2 xposed + java代码(justtrustme组件)

1.安装xposed(LSPosed)
- Magisk面具+ROOT
- LSPosed是面具的模块(zip)
第1步:获取LSPosed的zip文件(课件+https://github.com/LSPosed/LSPosed/releases)
第2步:将zip文件上传到手机
adb push 本地目录/xxx.zip /sdcard/Download/LSPosed-v1.8.4-6609-zygisk-release.zip\
第3步:在Magisk面具中刷入LSPosed模块
- 打开Magisk面具
- 点击模块
- 本地安装 + 刷入
- 手机重启,手机上就会出现lsposed的图标

注意:如果没有lsposed图片,就去 `/data/adb/lspd/` 【mt管理器 or np管理】,手动安装
注意:如果是未激活

2.JustTrustMe组件(java代码 -> apk) -> 实现绕过客户端证书校验
- 安装JustTrustMe组件
adb install JustTrustMePlus.apk

- 看到LSPosed组件
- 打开LSPosed组件中的JustTrustMe组件
- 配置JustTrustMe组件去Hook指定的APP(重启手机生效)

4.校验的原理 & 绕过的机制
- 公钥校验(Pinner)
- 证书校验
- Host校验

4.1 公钥校验(Pinner)

安卓开发工程师:
- 获取后台的证书PEM文件(后端项目Https部署)
- PEM文件内容处理
下载PEM证书 => 转换公钥 => SHA256加密 => Base64编码
"Zhv4cvwdHmEmE0edWEcIdmLfwsqxrrOmp+vbngwNnrU=""
- 发送网络请求
- 以前的网络请求发送
OkHttpClient client = new OkHttpClient.Builder().build();
Request req = new Request.Builder().url("https://www.baidu.com/?q=defaultCerts").build();
Call call = client.newCall(req);
try {
Response res = call.execute();
Log.e("请求成功", "状态码:" + res.code());

} catch (IOException ex) {
Log.e("请求失败", "异常" + ex);
}

- 客户端校验
final String CA_PUBLIC_KEY = "sha256/Zhv4cvwdHmEmE0edWEcIdmLfwsqxrrOmp+vbngwNnrU=";
final String CA_DOMAIN = "www.baidu.com";

//校验公钥
// 1.找到CertificatePinner对象
// 2.执行CertificatePinner中的check方法【 证书内容 vs 动态生成 】
CertificatePinner pinner = new CertificatePinner.Builder().add(CA_DOMAIN, CA_PUBLIC_KEY).build();

OkHttpClient client = new OkHttpClient.Builder().certificatePinner(pinner).build();
Request req = new Request.Builder().url("https://www.baidu.com/?q=defaultCerts").build();
Call call = client.newCall(req);
try {
Response res = call.execute();
Log.e("请求成功", "状态码:" + res.code());

} catch (IOException ex) {
Log.e("请求失败", "异常" + ex);


}

完整体验:
1.【Net01】创建安卓项目 + 发送https网络请求(无客户端证书校验)
2.【Net02】创建安卓项目 + 发送https网络请求(客户端证书校验)【百度工程师】
- 下载PEM证书 => 转换公钥 => SHA256加密 => Base64编码 -> Q6TCQAWqP4t+eq41xnKaUgJdrPWqyG5L+Ni2YzMhqdY=
- 安装APP运行

3.运行
- 未抓包(未使用charlse抓包)
- Net01 OK
- Net02 OK

- 抓包 + 分析 + 逆向(使用charlse抓包)
- Net01 OK -> 例如 唯品会案例
- Net02 异常 -> 例如 安居客

4.猜想:关于安居客APP
- 安卓工程师
- 提前自己网址-> 下载PEM证书 => 转换公钥 => SHA256加密 => Base64编码
- 集成项目

5.如何绕过?
# 集成到APP中的公钥
final String CA_PUBLIC_KEY = "sha256/Q6TCQAWqP4t+eq41xnKaUgJdrPWqyG5L+Ni2YzMhqdY=";
final String CA_DOMAIN = "www.baidu.com";

# 进行校验 CertificatePinner.check 方法
# - 读取本地集成公钥(sha256+base64)
# - 获取请求地址证书(sha256+base64)
CertificatePinner pinner = new CertificatePinner.Builder().add(CA_DOMAIN, CA_PUBLIC_KEY).build();
OkHttpClient client = new OkHttpClient.Builder().certificatePinner(pinner).build();
Request req = new Request.Builder().url("https://www.baidu.com/?q=defaultCerts").build();

如果要Hook:
okhttp3.CertificatePinner.check,如果让check方法不报错。

再看frida脚本:
永远让check不报错。

答疑:
1.关于https请求?
2.抓包不是密文?中间人
3.我可以拿到任何网站的公钥?
- 你任职于bilibili公司
- 开发自己公司APP -> 【APP】 + 【后端API】
- 根据后端域名 -> 公钥->Base64编码
- 集成到APP中
- 我的APP能正常向我的后端发送请求
- 如果有中间人抓包,一定会报错

4.2 证书校验
安卓开发工程师:
- 下载PEM证书 & 放入到APP中
- ...

如果绕过?
...

4.3 Host校验

安卓开发工程师:
...

问题:为什么frida_multiple_unpinning.js有700多行代码?
- 发送网络请求 okhttp3(几乎)
- 其他的网络请求 ...

5.感觉:
如果遇到客户端证书校验?【通杀】
- Frida的hook脚本
- xposed+justtrustme组件

代码混淆问题无法解决。
APP未混淆:
okhttp3.CertificatePinner

var okhttp3_Activity_1 = Java.use('okhttp3.CertificatePinner');
okhttp3_Activity_1.check.overload('java.lang.String', 'java.util.List').implementation = function(a, b) {
console.log('[+] Bypassing OkHTTPv3 {1}: ' + a);
return;
};
APP混淆
a.c
b.e
d.b

如果想要绕过:
var okhttp3_Activity_1 = Java.use('a.c');
okhttp3_Activity_1.bbb.overload('java.lang.String', 'java.util.List').implementation = function(a, b) {
console.log('[+] Bypassing OkHTTPv3 {1}: ' + a);
return;
};

__END__