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

前言

本来想挑一个 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:

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

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

发现啥也没有:

图片

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

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

图片

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

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

r0capture 通杀脚本

一句话启动:

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

图片

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

反编译 Hook 证书

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

图片

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

代码:

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 出来:

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 一般不都打包成 bksp12jks 这类的文件然后设置个密码,但这 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.sofrida 脚本代码如下:

function hookFunc(funcAddr, name) {
    Interceptor.attach(funcAddr, {
        onEnter: function (args) {
            const key = args[1]
            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[0];
            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 函数:

function hook(funcAddr, name) {
    Interceptor.attach(funcAddr, {
        onEnter: function (args) {
            console.log(name + " enter");
            const bytes = Memory.readByteArray(args[1], 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

<think>嗯,用户想知道如何绕过APK的签名校验机制。首先,我得回忆一下APK签名的作用。APK签名主要是为了验证应用在发布后没有被篡改,确保应用的完整性。如果用户想绕过这个机制,可能有几种方法,比如修改APK后重新签名,但系统会检测到签名不一致,导致安装失败或者运行时校验失败。 根据提供的引用内容,引用[1]提到verifiedEntries找不到新添加的文件,导致返回null。这可能涉及到在签名后添加文件,从而绕过签名检查。另外,引用[2]和[3]提到了替换public_key的方法,可能是在反编译后的代码中找到签名相关的部分,替换正确的公钥,或者通过抓包修改网络请求中的验证信息。引用[4]和[5]则涉及重打包和修改返回值,比如hook方法返回特定的路径,欺骗签名检查。 首先,我需要整理这些方法。可能的方法包括:重打包时处理签名校验逻辑、修改验证相关的代码或资源、网络验证的中间人攻击、动态Hook技术,以及通过注入文件来绕过校验。每种方法的具体步骤需要详细说明,比如反编译APK、定位签名校验代码、修改smali代码或资源文件,然后重新打包签名。同时,需要注意这些方法的实现细节,比如如何找到关键代码位置,如何处理签名后的文件添加,或者如何Hook系统方法返回伪造的信息。 另外,用户可能需要了解这些方法的原理和潜在风险,比如可能违反法律法规,或导致应用不稳定。因此,在回答中需要强调这些注意事项,并建议仅用于合法授权的研究。同时,要确保步骤清晰,比如分步骤说明如何反编译、修改、重打包,以及可能用到的工具如apktool、Frida等。 还需要考虑用户的实际操作中可能遇到的问题,比如如何定位签名校验的代码位置,如何正确替换公钥,或者如何配置抓包工具来拦截和修改网络请求。引用中的例子提供了具体的代码修改点,比如修改getPackageCodePath的返回值,这需要用户具备一定的逆向工程和编程能力。 总结起来,回答需要分步骤介绍不同方法,结合引用中的具体案例,说明每种方法的操作流程,并提醒用户注意法律和安全问题。同时,生成的相关问题要围绕这些方法的应用、检测和防御展开,帮助用户进一步理解相关技术。</think>### 绕过APK签名校验的常见方法 #### 1. **修改签名校验逻辑(代码层绕过)** - **原理**:APK签名校验通常通过对比当前APK的签名与预置的合法签名是否一致来实现。若直接修改校验逻辑(例如强制返回`true`),可绕过检测。 - **步骤**: 1. 使用反编译工具(如`apktool`)解包APK,定位签名校验代码。通常搜索关键词如`signatures`、`getPackageInfo`、`verify`等[^2]。 2. 修改`smali`代码或`Java`逻辑,使校验函数直接返回合法值。例如: ```smali # 原代码:if-eqz v0, :cond_0 (检查签名是否为空) # 修改为:if-nez v0, :cond_0 (反向条件) ``` - **风险**:部分应用会在多个位置校验签名,需全面覆盖。 #### 2. **注入合法公钥(资源替换)** - **原理**:若应用使用预埋的公钥验证签名,可通过反编译替换合法公钥。 - **步骤**: 1. 反编译APK后,搜索`public_key`或`signature`相关资源文件。 2. 将合法APK的公钥替换到目标APK中,确保验证逻辑匹配[^3]。 - **案例**:通过抓包对比正常APK与篡改APK的`public_key`差异,替换后绕过网络验证[^3]。 #### 3. **动态Hook绕过(运行时拦截)** - **原理**:使用`Frida`或`Xposed`等工具,在运行时修改关键函数返回值。 - **步骤**: 1. Hook签名校验函数(如`getPackageCodePath`),强制返回合法APK路径: ```javascript // Frida脚本示例[^5] Java.perform(function () { Java.use("android.content.ContextWrapper").getPackageCodePath.implementation = function () { return "/data/app/合法APK路径/base.apk"; }; }); ``` 2. 绕过校验后加载篡改后的APK。 #### 4. **签名后注入文件(Jar漏洞利用)** - **原理**:Android的`JarVerifier`在校验时依赖`verifiedEntries`列表。若在签名后添加新文件,该校验会失败但可能被忽略[^1]。 - **步骤**: 1. 对已签名APK追加新文件(如`assets/`目录下的资源)。 2. 修改代码逻辑,使应用加载新增文件时不触发校验。 #### 5. **网络验证中间人攻击** - **原理**:若签名校验依赖服务端验证,可通过抓包篡改响应。 - **步骤**: 1. 使用抓包工具(如`Charles`)拦截验证请求。 2. 替换响应中的签名验证结果字段(如`"valid": true`)[^3]。 --- ### 注意事项 1. **法律风险**:绕过签名校验可能违反《计算机软件保护条例》,仅限合法授权场景使用。 2. **兼容性问题**:修改后的APK可能因签名失效导致部分功能异常(如支付、地图SDK)。 3. **对抗加固**:若APK被混淆或加固(如360加固),需先脱壳再修改。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值