hooker中keystore_dump.js 安卓 APP 进行 HTTPS 双向认证时,自动 HOOK 并导出 APP 内置的客户端证书理解byDoubao

hooker/js/keystore_dump.js at master · CreditTone/hooker

//在https双向认证的情况下,dump客户端证书为p12. 证书密码: hooker
var password = "hooker";



function dateFormat(fmt, date) {
    let ret;
    const opt = {
        "Y+": date.getFullYear().toString(),
        // 年
        "m+": (date.getMonth() + 1).toString(),
        // 月
        "d+": date.getDate().toString(),
        // 日
        "H+": date.getHours().toString(),
        // 时
        "M+": date.getMinutes().toString(),
        // 分
        "S+": date.getSeconds().toString() // 秒
    };
    for (let k in opt) {
        ret = new RegExp("(" + k + ")").exec(fmt);
        if (ret) {
            fmt = fmt.replace(ret[1], (ret[1].length == 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0")))
        };
    };
    return fmt;
}

function random(min, max) {
    return Math.floor(Math.random() * (max - min)) + min;
}

function getNowTime() {
    return dateFormat("YYYY_mm_dd_HH_MM_SS", new Date()) + "_" + random(1, 100);
}

function getPackageName() {
    var currentApplication = Java.use('android.app.ActivityThread').currentApplication();
    var context = currentApplication.getApplicationContext();
    return context.getPackageName();
};

function newMethodBeat(text, executor) {
    var threadClz = Java.use("java.lang.Thread");
    var androidLogClz = Java.use("android.util.Log");
    var exceptionClz = Java.use("java.lang.Exception");
    var processClz = Java.use("android.os.Process");
    var currentThread = threadClz.currentThread();
    var beat = new Object();
    beat.invokeId = Math.random().toString(36).slice( - 8);
    beat.executor = executor;
    beat.myPid = processClz.myPid();
    beat.threadId = currentThread.getId();
    beat.threadName = currentThread.getName();
    beat.text = text;
    beat.startTime = new Date().getTime();
    beat.stackInfo = androidLogClz.getStackTraceString(exceptionClz.$new()).substring(20);
    return beat;
};

function printBeat(beat) {
    var str = ("------------pid:" + beat.myPid + ",startFlag:" + beat.invokeId + ",objectHash:"+beat.executor+",thread(id:" + beat.threadId +",name:" + beat.threadName + "),timestamp:" + beat.startTime+"---------------\n");
    str += beat.text + "\n";
    str += beat.stackInfo;
    str += ("------------endFlag:" + beat.invokeId + ",usedtime:" + (new Date().getTime() - beat.startTime) +"---------------\n");
    console.log(str);
};

function dump2sdcard(pri, p7, filePath) {
    console.log("dump:" + filePath);
    var X509CertificateClass = Java.use("java.security.cert.X509Certificate");
    var myX509 = Java.cast(p7, X509CertificateClass);
    var chain = Java.array("java.security.cert.X509Certificate", [myX509]);
    var ks = Java.use("java.security.KeyStore").getInstance("PKCS12", "BC");
    ks.load(null, null);
    ks.setKeyEntry("client", pri, Java.use('java.lang.String').$new(password).toCharArray(), chain);
    try {
        var out = Java.use("java.io.FileOutputStream").$new(filePath);
        ks.store(out, Java.use('java.lang.String').$new(password).toCharArray());
    } catch(error) {
        console.log(error);
    }
}



Java.perform(function() {
    var packageName = getPackageName();
    console.log("在https双向认证的情况下,dump客户端证书为p12. 存储位置:/data/user/0/"+packageName+"/client_keystore_{nowtime}.p12 证书密码: hooker");
    Java.use("java.security.KeyStore$PrivateKeyEntry").getPrivateKey.implementation = function() {
         var executor = this.hashCode();
        var beatText = 'public java.security.cert.Certificate java.security.KeyStore$PrivateKeyEntry.getPrivateKey()';
        var beat = newMethodBeat(beatText, executor);
        var result = this.getPrivateKey();
        let filePath = '/data/user/0/' + packageName + "/client_keystore_" + "_" + getNowTime() + '.p12';
        dump2sdcard(this.getPrivateKey(), this.getCertificate(), filePath);
        printBeat(beat);
        return result;
    }
    Java.use("java.security.KeyStore$PrivateKeyEntry").getCertificateChain.implementation = function() {
    var executor = this.hashCode();
        var beatText = 'public java.security.cert.Certificate java.security.KeyStore$PrivateKeyEntry.getCertificate()';
        var beat = newMethodBeat(beatText, executor);
        var result = this.getCertificateChain();
        let filePath = '/data/user/0/' + packageName + "/client_keystore_" + getNowTime() + '.p12';
        dump2sdcard(this.getPrivateKey(), this.getCertificate(), filePath);
        return result;
    }
})

代码整体理解

这是一段Frida脚本,核心作用是:在安卓APP进行HTTPS双向认证时,自动HOOK并导出APP内置的客户端证书,保存为带密码的p12格式文件,方便抓包/逆向分析HTTPS双向认证通信。

简单说:APP要和服务器双向验证身份,自带了客户端证书,这段代码把证书偷出来存成p12文件,密码是hooker


一、代码分模块拆解(从易到难)

1. 基础工具函数(通用辅助)

var password = "hooker";  // 导出的p12证书固定密码
  • 定义导出证书的固定密码,后续打开p12文件必须用这个。

① 日期格式化 + 随机数

dateFormat()   // 格式化时间:2026_03_02_12_30_45
random()       // 生成1-99随机数
getNowTime()   // 组合:时间_随机数,避免文件名重复
  • 用途:给导出的证书文件生成唯一文件名,防止覆盖。

② 获取APP包名

getPackageName()
  • 用途:拿到当前APP的包名,用来拼接证书保存路径
    /data/user/0/包名/client_keystore_xxx.p12

③ 日志打印工具

newMethodBeat() / printBeat()
  • 用途:HOOK时打印日志(进程ID、线程、调用堆栈、耗时),方便调试、查看是否成功触发。

2. 核心导出函数:dump2sdcard(最关键)

function dump2sdcard(pri, p7, filePath) {
    // 1. 拿到 私钥 + 证书
    // 2. 创建 PKCS12 格式密钥库
    // 3. 把私钥+证书放进去,设置密码 hooker
    // 4. 写入文件保存为 .p12
}

这个函数做了4件事:

  1. 接收两个核心参数:
    • pri:客户端证书私钥(最重要)
    • p7:客户端证书本身
  2. 创建PKCS12格式容器(就是p12文件)
  3. 私钥+证书存入容器,设置密码:hooker
  4. 写入文件到APP沙盒目录,完成导出。

3. HOOK核心:劫持证书获取逻辑

Java.perform(function() {
    // HOOK两个系统方法:
    // 1. 获取私钥
    Java.use("java.security.KeyStore$PrivateKeyEntry").getPrivateKey.implementation
    // 2. 获取证书链
    Java.use("java.security.KeyStore$PrivateKeyEntry").getCertificateChain.implementation
})

为什么HOOK这两个方法?

  • HTTPS双向认证时,APP一定会调用这两个系统方法
    • 私钥 → 签名认证
    • 证书 → 发给服务器验证
  • 代码在这两个方法被调用时插入自己的逻辑
    调用dump2sdcard(),把证书+私钥导出保存。

二、完整执行流程

  1. 脚本注入APP → 拿到包名
  2. 监听系统获取证书/私钥的方法
  3. APP发起HTTPS双向认证
  4. 系统调用getPrivateKey()/getCertificateChain()
  5. 脚本触发 → 拿到私钥+证书
  6. 打包成p12,密码hooker
  7. 保存到:/data/user/0/包名/client_keystore_xxx.p12
  8. 打印日志,提示导出成功

三、代码用途与使用场景

用途

破解HTTPS双向认证,导出客户端证书

  • 普通HTTPS只验证服务器;
  • 双向认证:服务器还要验证APP的客户端证书;
  • 没有这个证书,你就算抓包也无法建立连接;
  • 导出p12后,你可以把证书导入抓包工具(Charles/Fiddler),成功解密通信数据。

使用条件

  • 环境:安卓 + Frida
  • 目标:启用了HTTPS双向认证的APP
  • 输出:带密码hooker的p12证书文件

四、代码优缺点与小问题

优点

  • 自动HOOK系统方法,无需知道APP的证书存储逻辑
  • 自动生成唯一文件名,不会覆盖
  • 直接导出标准p12,可直接导入抓包工具

存在的小问题(不影响使用)

  1. 两个HOOK方法都会触发导出,同一个证书会导出两次
  2. 日志打印稍冗余
  3. 没有做重复导出去重

五、总结(最核心的3点)

  1. 这是Frida脚本,用于安卓HTTPS双向认证场景下导出客户端证书
  2. 导出文件格式.p12固定密码hooker
  3. 保存路径:APP沙盒目录 /data/user/0/包名/xxx.p12

你只要运行这段脚本,打开APP触发HTTPS请求,就能拿到可直接用于抓包的客户端证书。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容