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件事:
- 接收两个核心参数:
-
pri:客户端证书私钥(最重要) -
p7:客户端证书本身
-
- 创建
PKCS12格式容器(就是p12文件) - 把私钥+证书存入容器,设置密码:
hooker - 写入文件到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(),把证书+私钥导出保存。
二、完整执行流程
- 脚本注入APP → 拿到包名
- 监听系统获取证书/私钥的方法
- APP发起HTTPS双向认证
- 系统调用
getPrivateKey()/getCertificateChain() - 脚本触发 → 拿到私钥+证书
- 打包成p12,密码
hooker - 保存到:
/data/user/0/包名/client_keystore_xxx.p12 - 打印日志,提示导出成功
三、代码用途与使用场景
用途
破解HTTPS双向认证,导出客户端证书
- 普通HTTPS只验证服务器;
- 双向认证:服务器还要验证APP的客户端证书;
- 没有这个证书,你就算抓包也无法建立连接;
- 导出p12后,你可以把证书导入抓包工具(Charles/Fiddler),成功解密通信数据。
使用条件
- 环境:安卓 + Frida
- 目标:启用了HTTPS双向认证的APP
- 输出:带密码
hooker的p12证书文件
四、代码优缺点与小问题
优点
- 自动HOOK系统方法,无需知道APP的证书存储逻辑
- 自动生成唯一文件名,不会覆盖
- 直接导出标准p12,可直接导入抓包工具
存在的小问题(不影响使用)
- 两个HOOK方法都会触发导出,同一个证书会导出两次
- 日志打印稍冗余
- 没有做重复导出去重
五、总结(最核心的3点)
- 这是Frida脚本,用于安卓HTTPS双向认证场景下导出客户端证书
-
导出文件格式:
.p12,固定密码:hooker -
保存路径:APP沙盒目录
/data/user/0/包名/xxx.p12
你只要运行这段脚本,打开APP触发HTTPS请求,就能拿到可直接用于抓包的客户端证书。