服务端证书校验

  • 客户端校验

    1
    2
    - 在客户端中预设证书信息
    - 客户端向服务端发送请求,将服务端返回的证书信息(公钥)和客户端预设证书信息进行校验
  • 服务端校验

    1
    2
    - 在客户端预设证书(p12/bks)
    - 客户端向服务端发送请求时,携带证书信息,在服务端会校验客户端携带过来证书的合法性

1.服务端校验逻辑

服务端证书的校验逻辑:

  • 在apk打包时,将证书 bks 或 p12 格式的证书保存在 assets 或 raw 等目录。

  • 安卓代码,发送请求时 【读取证书文件内容】+ 【证书密码】

    image-20230207114936007

    image-20240517193439831

  • 服务端接收到请求后,会进行校验。

逆向时,需要实现:

  • 获取 bks 或 p12证书 文件
  • 获取证书相关密码
  • 将证书导入到charles,可以实现抓包(bks格式需要转换p12格式)
  • 用requests发送请求时,携带证书去发送请求

2.一波逆向案例

2.1 泡泡聊天

版本:v1.7.4

下载:https://www.wandoujia.com/apps/8280413

image-20240517193448216

2.1.1 抓包

image-20240517193459501

2.1.2 Hook密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java.perform(function () {
var KeyStore = Java.use("java.security.KeyStore");

KeyStore.load.overload('java.io.InputStream', '[C').implementation = function (v1, v2) {
var pwd = Java.use("java.lang.String").$new(v2);
console.log('\n------------')
console.log("类型:" + this.getType());
console.log("密码:" + pwd);
console.log(JSON.stringify(v1));
//console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
var res = this.load(v1, v2);
return res;
};
});
// frida -U -f com.paopaotalk.im -l 1.hook_password.js
// 111111

2.1.3 Hook证书文件

在开发时,是将证书文件加载到 InputStream 对象中,后续发送请求是会携带。。。

而我们想要获取证书可有两种方式:

  • 定位代码,找到加载证书的文件路径,然后去apk中寻找。

  • 直接Hook证书加载位置,将证书的内容从InputStream写入到自定义文件,实现自动导出【更加通用,甚至都不需要任何逆向】。

    注意:手机要对当前APP开启本地硬盘操作权限。

2.1.3.1.定位代码

  • Hook KeyStore.load 输出调用栈,寻找证书的位置。

    1
    ...
  • app是有壳,需要先试用 frida-dexdump进行脱壳,然后反编译。

    1
    frida-dexdump  -U -f com.paopaotalk.im
  • 定位代码位置

    image-20240517193513135

    image-20240517193525472

    image-20240517193533302
    image-20240517193548989

所以,证书的位置 在apk的assets目录下 client.bks 且密码阿是 111111

image-20240517193603036

2.1.3.2.导出证书

注意:在手机上一定要先给当前app开启可以操作硬盘的权限,否则无法导出证书文件。

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
Java.perform(function () {
var KeyStore = Java.use("java.security.KeyStore");
var String = Java.use("java.lang.String");


KeyStore.load.overload('java.io.InputStream', '[C').implementation = function (inputStream, v2) {
var pwd = String.$new(v2);
console.log('\n------------')
console.log("密码:" + pwd, this.getType());

if (this.getType() === "BKS") {
var myArray = new Array(1024);
for (var i = 0; i < myArray.length; i++) {
myArray[i] = 0x0;
}
var buffer = Java.array('byte', myArray);

var file = Java.use("java.io.File").$new("/sdcard/Download/paopao-" + new Date().getTime() + ".bks");
var out = Java.use("java.io.FileOutputStream").$new(file);
var r;
while ((r = inputStream.read(buffer)) > 0) {
out.write(buffer, 0, r);
}
console.log("save success!")
out.close()
}

var res = this.load(inputStream, v2);
return res;
};

});

// frida -U -f com.paopaotalk.im -l 2.hook_save.js

2.1.4 转换bks到p12

charles不支持导入bks格式的证书,如果逆向过程中得到了bks格式证书,需要使用 portecle 将bks证书转化弄成p12格式,然后再处理。

提示:此工具依赖jdk,请务必先在自己电脑上安装jdk。

打开portecle,并导入bks证书。

image-20240517194730962

image-20240517194741220

image-20240517194749465

image-20240517194759864

image-20240517194809295

image-20240517194819603

image-20240517194826522

image-20240517194837759

image-20240517194847140

提示:也可以使用 https://keystore-explorer.org/downloads.html 来做证书的转换(我的mac不太好用)。

2.1.5 charles导入证书

将p12证书导入charles,然后再转抓包就可以了。

image-20240517194857506

image-20240517194905194

image-20240517194912423

再次抓包,成功了。

image-20240517194919977

2.1.6 Python请求

如果是服务端证书校验,需要携带证书才能访问。

2.1.6.1.requests_pkcs12

1
pip install requests-pkcs12
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
from requests_pkcs12 import get, post

res = post(
url='https://8.218.94.100:48837/userservices/v2/user/login',
json={
"device_type": "app",
"username": "008615131255555",
"password": "a2c62dffccea2f1d638aa3945be8770c",
"device_id": "1d40d48517776215104989bd5949fb91",
"device_name": "Xiaomi M2007J17C",
"device_model": "M2007J17C"
},
headers={
"bundle_id": "com.paopaotalk.im",
"version": "1.7.4",
"timestamp": "1600",
"sign": "94703ac3c05a8a5380010cc90890c72b",
"app_id": "qiyunxin",
"Accept-Language": "zh-CN",
"package": "com.paopaotalk.im",
},
pkcs12_filename='Client1.p12',
pkcs12_password='111111',
verify=False
)
print(res.text)

2.6.1.2.requests

默认requests不支持直接使用p12格式的证书,所以需要将p12转换成pem才可以。

image-20240517194931577

1
>>>openssl pkcs12 -in paopao-client.p12 -out demo.pem -nodes -passin "pass:111111"
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
from requests import post

res = post(
url='https://8.218.94.100:48837/userservices/v2/user/login',
json={
"device_type": "app",
"username": "008615131255555",
"password": "a2c62dffccea2f1d638aa3945be8770c",
"device_id": "1d40d48517776215104989bd5949fb91",
"device_name": "Xiaomi M2007J17C",
"device_model": "M2007J17C"
},
headers={
"bundle_id": "com.paopaotalk.im",
"version": "1.7.4",
"timestamp": "1600",
"sign": "94703ac3c05a8a5380010cc90890c72b",
"app_id": "qiyunxin",
"Accept-Language": "zh-CN",
"package": "com.paopaotalk.im",
},
cert='demo.pem',
verify=False
)
print(res.text)

2.2 美之图

版本:v3.5.6 (apk文件见课件)

注意:请不要晚上逆向这个app,有点伤身体(无不良引导,实在是案例少)。

image-20240517194941566

2.2.1 校验逻辑

服务端证书的校验逻辑:

  • 在apk打包时,将证书 bks 或 p12 格式的证书保存在 assets 或 raw 等目录。

  • 安卓代码,发送请求时 【读取证书文件内容】+ 【证书密码】

    image-20240517194948934

    image-20240517194955914

逆向时,需要实现:

  • 获取 bks 或 p12证书 文件
  • 获取证书相关密码
  • 将证书导入到charles,可以实现抓包(bks格式需要转换p12格式)
  • 用requests发送请求时,携带证书去发送请求

2.2.2 Hook密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java.perform(function () {
var KeyStore = Java.use("java.security.KeyStore");

KeyStore.load.overload('java.io.InputStream', '[C').implementation = function (v1, v2) {
var pwd = Java.use("java.lang.String").$new(v2);
console.log('\n------------')
console.log("类型:" + this.getType());
console.log("密码:" + pwd);
console.log(JSON.stringify(v1));
//console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
var res = this.load(v1, v2);
return res;
};
});
// frida -U -f com.mmzztt.app -l 1.hook_password.js
// uX39!dd$#rr_XIyb%

2.2.3 Hook证书文件

在开发时,是将证书文件加载到 InputStream 对象中,后续发送请求是会携带。。。

而我们想要获取证书可有两种方式:

  • 定位代码,找到加载证书的文件路径,然后去apk中寻找。

  • 直接Hook证书加载位置,将证书的内容从InputStream写入到自定义文件,实现自动导出【更加通用,甚至都不需要任何逆向】。

    注意:手机要对当前APP开启本地硬盘操作权限。

2.2.3.1.定位代码

  • Hook KeyStore.load 输出调用栈,寻找证书的位置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    java.lang.Throwable
    at java.security.KeyStore.load(Native Method)
    at com.deepe.c.j.c.b.b(Unknown Source:32)
    at com.deepe.c.j.c.b.a(Unknown Source:39)
    at com.deepe.c.j.e.d.getSslSocketFactory(Unknown Source:20)
    at com.deepe.c.j.d.g.a(Unknown Source:42)
    at com.deepe.c.j.d.g.a(Unknown Source:120)
    at com.deepe.c.j.d.a.a(Unknown Source:33)
    at com.deepe.c.j.h.run(Unknown Source:69)
  • app是有壳,需要先试用 frida-dexdump进行脱壳,然后反编译。

    1
    frida-dexdump  -U -f com.mmzztt.app
  • 定位代码位置
    image-20240517195009117

    image-20230207142132121

    image-20240517195047633

    再结合Hook脚本,就能确定其实是读取本地的 config文件,其实就是p12证书。
    注意:因为有壳,所以Hook相关代码时需要延迟下。

    image-20240517195059226

    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
    Java.perform(function () {
    var KeyStore = Java.use("java.security.KeyStore");
    var String = Java.use("java.lang.String");


    KeyStore.getInstance.overload('java.lang.String').implementation = function (name) {
    //console.log("秘钥格式:", name);
    return this.getInstance(name);
    }
    KeyStore.load.overload('java.io.InputStream', '[C').implementation = function (v1, v2) {
    var pwd = String.$new(v2);
    console.log("密码:" + pwd);
    // console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
    var res = this.load(v1, v2);
    return res;
    };

    setTimeout(function (){

    var b = Java.use("com.deepe.c.j.c.b");
    b.b.implementation = function (str,str2) {
    console.log(str,str2);
    return this.b(str,str2);
    }

    var UZUtility = Java.use("com.uzmap.pkg.uzkit.UZUtility");
    UZUtility.guessInputStream.implementation = function (str) {
    console.log(str);
    return this.guessInputStream(str);
    }

    var f = Java.use("com.deepe.c.i.f");
    f.a.overload('java.lang.String', 'java.io.InputStream').implementation = function (str,stream) {
    console.log(str,stream);
    return this.a(str,stream);
    }
    },1000);

    });

    // frida -U -f com.mmzztt.app -l 2.hook.js
    // uX39!dd$#rr_XIyb%

2.2.3.2.导出证书

注意:在手机上一定要先给当前app开启可以操作硬盘的权限,否则无法导出证书文件。

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
Java.perform(function () {
var KeyStore = Java.use("java.security.KeyStore");
var String = Java.use("java.lang.String");


KeyStore.load.overload('java.io.InputStream', '[C').implementation = function (inputStream, v2) {
var pwd = String.$new(v2);
console.log('\n------------')
console.log("密码:" + pwd, this.getType());

if (this.getType() === "PKCS12") {
var myArray = new Array(1024);
for (var i = 0; i < myArray.length; i++) {
myArray[i] = 0x0;
}
var buffer = Java.array('byte', myArray);

var file = Java.use("java.io.File").$new("/sdcard/Download/meizhitu-" + new Date().getTime() + ".bks");
var out = Java.use("java.io.FileOutputStream").$new(file);
var r;
while ((r = inputStream.read(buffer)) > 0) {
out.write(buffer, 0, r);
}
console.log("save success!")
out.close()
}

var res = this.load(inputStream, v2);
return res;
};

});

// frida -U -f com.mmzztt.app -l 2.hook_save.js

2.2.4 charles导入证书

同上

2.2.5 python请求

同上

3 笔记

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
抓包-服务端证书校验

今日概要:
- 客户端校验补充
- 服务端证书校验

1.客户端校验补充

1.1 客户端证书校验逻辑
安卓开发者,在安卓app中集成代码,校验:集成证书 vs 真正请求证书是否一致。【手机端校验】

1.2 三处客户端证书校验
- 公钥校验,安卓开发:证书->Base64编码->字符串,手机APP发送请求:字符串+真正请求的字符。
案例:
- Net01,无证书校验
- Net02,客户端证书校验
- 证书校验,安卓开发:PEM证书,手机APP发送请求:读取证书+真正请求获取证书
案例:
- Net03,证书校验(baidu_com.pem)
- Host校验,
案例:Net04

扩展:关于【公钥校验】【证书校验】基于配置文件的形式进行开发。

1.3 关于混淆的问题
非通用的实现:frida的Hook脚本 + xposed
代码混淆:
- 系统代码,永远不会被混淆
- 第三方包+自定义包,会被混淆。

解决思路:以系统包Hook+调用栈分析 => 绕过

2.服务端证书校验

2.1 校验逻辑
- 安卓开发工程需要将:证书文件(bks/p12)+ 密码 + 代码 => 集成手机APP中;每次发送网络请求:证书+密码,携带到后端API
- 后端开发工程师:项目开发和部署要开发对应的校验过程。

2.2 想要绕过服务端证书校验
核心:找到集成在APP中 【xx.bks或xx.p12】+【密码】

方法1:学会编写Java代码去集成【xx.bks或xx.p12】+【密码】,反编译APK,关键字找对应的代码。
方法2:编写Hook脚本 KeyStore.load 函数(KeyStore属于系统包 -> 不会被混淆)
java.security.KeyStore.load 函数
通用方案,适用于所有的APP中只要有服务端证书校验。

2.3 观看Hook脚本
- 获取密码
- 导出证书文件

2.4 案例:泡泡聊天

1.现象
- 手机直接访问 ok
- 手机+Charles代理 error

2.基于通用的Hook脚本测试
密码:111111
证书:paopao-1711028290196.bks

3.集成Charles中,实现正常抓包
- 将手机中的bks证书获取到电脑
- 将bks格式转换为p12格式【工具】
- p12集成charles

4.基于Python发送请求
- 基于requests-pkcs12模块
- 基于requests模块+PEM证书(pkcs12->pem)
- 基于openssl
>>>openssl pkcs12 -in paopao-client.p12 -out paopao.pem -nodes -passin "pass:111111"
- 发送请求

2.5 案例:美之图

__END__