yinsel 发表于 2024-8-11 17:08

记录一次绕过 Android 服务端的证书校验的详细过程

## 前言

**在过程中还有些许问题需要大家一起来探讨,就是关于Java层Hook很多都Hook不到,即使尝试延迟注入也一样,并且在调用堆栈看也有调用,谢谢各位支持!**

本来想挑一个 APP 抓包练练手,因为基础不是很好,想提升实战水平,结果一不小心挑了个不寻常的(对于我而言,大佬勿喷),但因为自己比较固执,不死心,花了几天时间总算搞定了,但还是有些问题,希望有懂行大佬指点一下。

该 APP 使用了 `org.conscrypt` 库,据了解,这一个封装基于 `OpenSSL` 的库,在 `Github` 上也有 `1.3k Star` 但是网上并没有相关的文章,很少,最终还是通过翻阅源码找到一个关键的 `So` 层函数作为 `Hook` 点将私钥导出。

本文章 `Hook` 脚本均参考了网上的文章以及借助 `ChatGPT` 所编写,并且经过许多次调试,因为自己不是特别熟悉 `frida JS API`,还需要多练,多实战,因此写了这篇文章记录自己的过程,以分享自己的思路,给有需要的人一些参考,避免踩坑。
## 详细过程

### 设置代理

目前手机已 `root`,已安装 `Burp` 证书至系统,当然用 `JustTrustMe` 也可以干掉客户端的证书校验,比较简单也没检测 `VPN`,随后开启热点,使用安卓端 `proxifier` 开启 VPN 让指定 APP 走 `Burp`:

设置 `Burp` 代理:



指定 `APP`:



### 发现服务端的证书校验

进入 `APP`,发现一切正常:



在输入框中随便输入,点击加入,服务端返回 400,并且是 `No required SLL certifucate was sent`:





### 解包寻找 APK 中的证书

使用 `Jadx-gui` 对 `APK` 进行反编译,发现资源部分有几个关键证书:





但这些 `grp_sp.bks` 、`hmsincas.bks` 、`hmsrootcas.bks` 都是 `SDK` 相关的证书,而 `trust.crt` 有点可疑,使用 `XCA` 对 `trust.crt` 进行查看:



`trust.crt` 包含多个公钥证书,还有一些 CA 证书,应该是证书信任链,不是客户端证书,因此解包寻找证书无果。

### 尝试大佬的 frida 自吐脚本和 r0capture

#### 自吐脚本

于是我参考了这位大佬的文章:`https://xz.aliyun.com/t/12993`,使用其中的 frida 自吐脚本尝试 `Hook` 看有没有发现,脚本链接:`https://github.com/WithSecureLabs/android-keystore-audit/blob/master/frida-scripts/tracer-keystore.js`

这里我用的是 `JsHook`,使用 `frida` 还是非常方便的:



开启 `frida-server` 服务及端口转发,复制自吐脚本,并以 `spawn` 模式启动 `App`:

```python
# 端口转发
adb forward tcp:28042 tcp:28042

# 以spawn启动
frida -H 127.0.0.1:28042 -f <包名> -l hook.js
```

发现啥也没有:



尝试手动抛出异常打印堆栈:

```python
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
```



也没发现,不过有一行看起来跟证书相关的类,这里先记一下:

```python
ak.im.module.AkeyChatX509PrivateCA.clientBootstrapCertInfo(AkeyChatX509PrivateCA.java:8)
```

#### r0capture 通杀脚本

一句话启动:

```python
python r0capture.py -H 127.0.0.1:28042 -f <包名> -v
```



还是无果,内容还是加密的,证书也未导出,看调用堆栈可知,使用了 `org.conscrypt` 库,搜了一下发现是 `Google` 开发的一个基于 `OpenSSL` 封装的 `SSL/TLS` 加密库,有点用,先记着。
### 反编译 Hook 证书

用了大佬们的脚本都无果,其实到这有点想放弃了,不过还没仔细看代码,根据上文的堆栈信息查找与证书有关的类名:



发现有个函数返回了证书,尝试 `hook` 它试试,`jadx-gui` 很方便,可以右键直接复制 `Hook` 代码:

```js
function hook() {
    Java.perform(function () {
      let AkeyChatX509PrivateCA = Java.use("ak.im.module.AkeyChatX509PrivateCA");
      AkeyChatX509PrivateCA["clientBootstrapCertInfo"].implementation = function () {
            console.log(`AkeyChatX509PrivateCA.clientBootstrapCertInfo is called`);
            let result = this["clientBootstrapCertInfo"]();
            console.log(`AkeyChatX509PrivateCA.clientBootstrapCertInfo result=${result}`);
            return result;
      };
    });
}
```

结果打印了一个证书,从 `X509 Extend Usage` 中的 `Web Client Authentication` 看以及关键字眼 ` android `,可以判断这个就是客户端证书:
·


将其 dump 出来:

```js
function hook() {
    Java.perform(function () {
      let AkeyChatX509PrivateCA = Java.use("ak.im.module.AkeyChatX509PrivateCA");
      
      AkeyChatX509PrivateCA["clientBootstrapCertInfo"].implementation = function () {
            console.log("AkeyChatX509PrivateCA.clientBootstrapCertInfo is called");
            // 调用原始方法,获取返回的 X509Certificate 对象
            let result = this.clientBootstrapCertInfo();
            console.log("AkeyChatX509PrivateCA.clientBootstrapCertInfo result:", result)
            // 获取 DER 编码的字节数组
            let cert = result.getEncoded();
            let bytes = Memory.readByteArray(cert,cert.length)
            const file = new File("/sdcard/Download/clent.crt", "wb");
            file.write(bytes);
            // 返回原始结果
            return result;
      };
    });
}

hook();
```



**有个问题,这证书从哪来的?** 只能先抛开不谈,现在证书有了,私钥呢?常规 APP 一般不都打包成 `bks`、`p12`、`jks` 这类的文件然后设置个密码,但这 `APP` 不走寻找路,于是我尝试了各种 `Hook`,如下图,涉及私钥和证书的 `Java` 层方法都尝试过:



以及 `javax.net.ssl.*` `org.conscrypt` 库的一些 `Java` 层的关键方法,要么 `Hook` 不到(这里可能是 Hook 时机不对或者没调用,很多都没 Hook 到),要么为 `null`,心态崩了,但我还是不想放弃,于是去找 `native` 层。
### 寻找 So 层 Hook 点

下载 `org.conscrypt` 的源码:https://github.com/google/conscrypt,使用 `VScode` 打开寻找关键词,尝试 `Hook`,最终找到了一处:



其中 `keyJavaBytes` 根据语义以及函数名可以判断这个跟私钥有关,**但是我发现这个函数在 `Java` 层有被调用,尝试 `Hook` 他们却没反应**:



先暂时不管,直接尝试 `Hook` 这个函数,打印 `jbyteArray` 的内存数据,找到 `APP` 加载的 `So` 文件名为:`libconscrypt_jni.so`,`frida` 脚本如下:

```js
function hookFunc(funcAddr, name) {
    Interceptor.attach(funcAddr, {
      onEnter: function (args) {
            const key = args
            const dump = hexdump(key, {
                offset: 0,
                length: 0x1000,
               headers: true,
               ansi: true
                })
            console.log(hex);

      },
      onLeave: function (retval) {

      },
    });
}

function hook() {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
      onEnter: function (args) {
            var pathptr = args;
            if (pathptr !== undefined && pathptr != null) {
                var path = ptr(pathptr).readCString();
                if (path.indexOf("libconscrypt_jni.so") !== -1) {
                  console.log("dlopen: " + path);
                  this.path = path;
                }
            }
      },

      onLeave: function (retval) {
            if (this.path !== undefined) {
                // 获取模块的 base 地址
                var baseAddress = Module.findBaseAddress(this.path);
                if (baseAddress !== null) {
                  console.log("Module base address: " + baseAddress);

                  // 遍历导出表
                  console.log("Listing exports in " + this.path);
                  Module.enumerateExports(this.path, {
                        onMatch: function (symbol) {
                            if (symbol.name.indexOf("EVP_parse_private_key") !== -1) {
                              console.log(symbol.name + "---" + symbol.address.toString());
                              hookFunc(symbol.address, symbol.name)
                            }
                        },
                        onComplete: function () {

                        }
                  });
                }
            }
      }
    });
}

hook();
```



这里我尝试用很多中办法都没法得到 `jbyteArray` 的长度,调用 `JNI` 的 `GetArrayLength` 方法会导致闪退,具体原因未知,只能预先 dump 大小为 0x1000,有需要也可以更大,将其 dump 至 `/sdcard/Download` 下,替换上方的 `hook` 函数:

```js
function hook(funcAddr, name) {
    Interceptor.attach(funcAddr, {
      onEnter: function (args) {
            console.log(name + " enter");
            const bytes = Memory.readByteArray(args, 0x1000);
            const file = new File("/sdcard/Download/private.pem", "wb");
            file.write(bytes);
      },
      onLeave: function (retval) {

      },
    });
}
```

私钥如下:



导入 `XCA`,查看之前的证书,可以发现私钥对应上了:



使用 `XCA` 直接导出为 `p12`:



导入至 `burp`:



发包,成功:



不知道这种方法能不能对使用了 `org.conscrypt` 库的 `App` 通杀,后续研究一下。

## 参考链接

https://xz.aliyun.com/t/12993

https://bbs.kanxue.com/thread-280089.htm

https://bbs.kanxue.com/thread-281584.htm

linix 发表于 2024-8-15 12:14

本帖最后由 linix 于 2024-8-15 12:15 编辑

E:\Temp\1.png

楼主大大,为什么我用burp抓包没有出现那个校验证书的数据包?是版本不对还是哪里设置问题?我的burp版本为V2020.12
用charles抓包出现了那个包,但是没有url地址,确实不方便,想分析无从下手。

debug_cat 发表于 2024-8-12 09:17

思路和分析过程都很详细!{:1_921:}

terwer 发表于 2024-8-12 10:11

感谢分享,学习了

yinsel 发表于 2024-8-12 10:14

debug_cat 发表于 2024-8-12 09:17
思路和分析过程都很详细!

感谢支持

liruibei572 发表于 2024-8-12 17:18

感谢分享,学习学习

Cleopatra 发表于 2024-8-12 17:47

6的又学习到了一招了

SunnyLee 发表于 2024-8-12 17:54

学习了学习了

musiccard 发表于 2024-8-12 17:58

厉害了大神

anennzxq 发表于 2024-8-12 18:06

不明觉厉,学习了

hh0123 发表于 2024-8-12 18:42

感谢分享,学习了
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 记录一次绕过 Android 服务端的证书校验的详细过程