前言
本文章仅做移动安全学习交流用途,严禁作其他用途,如果侵犯您的权益请联系我删除。
目标版本是1.3.332
所用工具: IDA Pro, frida, jadx
分析
通过下面代码可以看到,在pthread_create中对frida进行了检测
function hook_dlsym() {
var count = 0
console.log("=== HOOKING dlsym ===")
var interceptor = Interceptor.attach(Module.findExportByName(null, "dlsym"),
{
onEnter: function (args) {
const name = ptr(args[1]).readCString()
// const module = Process.findModuleByAddress(ptr(this.returnAddress))
console.log("[dlsym]", name)
if (name == "pthread_create") {
count++
}
}
}
)
return Interceptor
}
function hook_dlopen() {
var interceptor = 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();
console.log("[LOAD]", path)
if (path.indexOf("libmsaoaidsec.so") > -1) {
hook_dlsym()
}
}
},
}
)
return interceptor
}
var dlopen_interceptor = hook_dlopen()
过检测代码
function create_fake_pthread_create() {
const fake_pthread_create = Memory.alloc(4096)
Memory.protect(fake_pthread_create, 4096, "rwx")
Memory.patchCode(fake_pthread_create, 4096, code => {
const cw = new Arm64Writer(code, { pc: ptr(fake_pthread_create) })
cw.putRet()
})
return fake_pthread_create
}
function hook_dlsym() {
var count = 0
console.log("=== HOOKING dlsym ===")
var interceptor = Interceptor.attach(Module.findExportByName(null, "dlsym"),
{
onEnter: function (args) {
const name = ptr(args[1]).readCString()
console.log("[dlsym]", name)
if (name == "pthread_create") {
count++
}
},
onLeave: function(retval) {
if (count == 1) {
retval.replace(fake_pthread_create)
}
else if (count == 2) {
retval.replace(fake_pthread_create)
// 完成2次替换, 停止hook dlsym
interceptor.detach()
}
}
}
)
return Interceptor
}
function hook_dlopen() {
var interceptor = 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();
console.log("[LOAD]", path)
if (path.indexOf("libmsaoaidsec.so") > -1) {
hook_dlsym()
}
}
}
}
)
return interceptor
}
// 创建虚假pthread_create
var fake_pthread_create = create_fake_pthread_create()
var dlopen_interceptor = hook_dlopen()
逆向
jadx中分析
通过jadx搜索字符串heybox_id
定位
发现这里调用了encode函数
参考了别人的思路知道用到了NDKTools,发现头文件引用中确实有
并且NDKTools的encode的调用在libnative-lib.so文件中
用jadx生成的frida代码hook一下encode函数得到了这些结果,发现第一个参数是一个java类,第二个是接口的字符串,第三个是时间戳,第四个是请求中的
nonce
参数,而hook的结果则是类似hkey
的字符串。实际上,与请求包所对应的
hkey
是8位,而encode得到的结果为7位,但是如图所示hook的这三个地方都有相同的值TyUPTy0qTlwj1f6cMeqKSGbfUZLkdFzI
,因此这三者是对应的,也就是说nonce
经过getVA处理后得到了最终结果hkey
getVA是一个native函数,在libhbsecurity.so文件中
那么现在的核心任务就是分析libnative-lib.so和libhbsecurity.so文件了。
通过hook getVA函数发现,当传入的str值为固定值时,返回的结果也是固定的,也就是说,
hkey
仅与nonce
有关。则现在的目标是分析nonce
的生成和getVA在so中的算法。
通过hook RegisterNatives方法得到了,动态注册函数的偏移:
[RegisterNatives] java_class: com.max.security.SecurityTool name: setKA sig: (Ljava/lang/String;)V fnPtr: 0x7ed0d635c0 module_name: libhbsecurity.so module_base: 0x7ed0cc0000 offset: 0xa35c0
[RegisterNatives] java_class: com.max.security.SecurityTool name: setKB sig: (Ljava/lang/String;Ljava/lang/String;)V fnPtr: 0x7ed0d63c34 module_name: libhbsecurity.so module_base: 0x7ed0cc0000 offset: 0xa3c34
[RegisterNatives] java_class: com.max.security.SecurityTool name: setKM sig: (Ljava/lang/String;Ljava/lang/String;)V fnPtr: 0x7ed0d63d80 module_name: libhbsecurity.so module_base: 0x7ed0cc0000 offset: 0xa3d80
[RegisterNatives] java_class: com.max.security.SecurityTool name: setKT sig: (Ljava/lang/String;Ljava/lang/String;)V fnPtr: 0x7ed0d63ecc module_name: libhbsecurity.so module_base: 0x7ed0cc0000 offset: 0xa3ecc
[RegisterNatives] java_class: com.max.security.SecurityTool name: setKN sig: (Ljava/lang/String;Ljava/lang/String;)V fnPtr: 0x7ed0d63fd8 module_name: libhbsecurity.so module_base: 0x7ed0cc0000 offset: 0xa3fd8
[RegisterNatives] java_class: com.max.security.SecurityTool name: setKD sig: (Ljava/lang/String;Ljava/lang/String;)V fnPtr: 0x7ed0d640e0 module_name: libhbsecurity.so module_base: 0x7ed0cc0000 offset: 0xa40e0
[RegisterNatives] java_class: com.max.security.SecurityTool name: setKC sig: (Ljava/lang/String;Ljava/lang/String;)V fnPtr: 0x7ed0d642d8 module_name: libhbsecurity.so module_base: 0x7ed0cc0000 offset: 0xa42d8
[RegisterNatives] java_class: com.max.security.SecurityTool name: getVX sig: (Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String; fnPtr: 0x7ed0d645d0 module_name: libhbsecurity.so module_base: 0x7ed0cc0000 offset: 0xa45d0
[RegisterNatives] java_class: com.max.security.SecurityTool name: getVA sig: (Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String; fnPtr: 0x7ed0d64834 module_name: libhbsecurity.so module_base: 0x7ed0cc0000 offset: 0xa4834
[RegisterNatives] java_class: com.max.security.SecurityTool name: getVB sig: (I)I fnPtr: 0x7ed0d65954 module_name: libhbsecurity.so module_base: 0x7ed0cc0000 offset: 0xa5954
[RegisterNatives] java_class: com.max.security.SecurityTool name: getVC sig: (Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String; fnPtr: 0x7ed0d65a58 module_name: libhbsecurity.so module_base: 0x7ed0cc0000 offset: 0xa5a58
[RegisterNatives] java_class: com.max.security.SecurityTool name: getVD sig: (Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String; fnPtr: 0x7ed0d65e44 module_name: libhbsecurity.so module_base: 0x7ed0cc0000 offset: 0xa5e44
[RegisterNatives] java_class: com.max.security.SecurityTool name: resetVA sig: ()V fnPtr: 0x7ed0d66428 module_name: libhbsecurity.so module_base: 0x7ed0cc0000 offset: 0xa6428
然后可以用hook函数对这些函数进行hook
hook函数模板:
function hook_sub_AC990(){
Java.perform(function () {
var a45d = Module.findBaseAddress("libhbsecurity.so")
a45d = a45d.add(0xA35C0)
Interceptor.attach(a45d,{
onEnter:function(arg){
//console.log(hexdump(arg[1]))
//console.log(hexdump(arg[2]));
//console.log("参数0:",arg[0].readInt())
console.log("参数0:",arg[0].readInt())
console.log("参数1:",arg[1].readInt())
console.log("参数2:",arg[2].readInt())
console.log("参数3:",arg[3].readInt())
},
onLeave:function(ret){
console.log("返回值;",ret.toString())
}
})
});
}
hook了so层中的函数sub_A6620,它的作用是获取一个字符串参数,实际上获取到的就是nonce
的值
ida中分析
通过分析流程发现getVA似乎仅与sub_A4834函数有关,推测getVA函数在so中应该为sub_A4834函数-2024-09-05 17:17
2024-09-08
通过了解RegisterNatives函数,找到了getva对应在so中的方法为sub_A4834,该方法有四个参数,第一个为env
,第二个为java类,第三个为context
,第四个为nonce
,用frida去hook该函数,发现打印出来的这些参数的字符串都是地址,使用frida的readCstring也无法正确打印,今日偶然发现这个函数可以将地址正确转换为字符串,同时发现context
也可以打印出来,同样的,返回值也可以打印出来。
//string
function jstring2Str(jstring) { //从frida_common_funs.js中copy出来
var ret;
Java.perform(function() {
var String = Java.use("java.lang.String");
ret = Java.cast(jstring, String);//jstring->String
});
return ret;
}
//context
function jcontext2context(jcontext){
var ret;
Java.perform(function() {
var context = Java.use("android.content.Context");
ret = Java.cast(jcontext, context);//jstring->String
});
return ret;
}
整体hook代码
function hook_sub_AC990() {
Java.perform(function () {
var baseAddr = Module.findBaseAddress("libhbsecurity.so");
if (baseAddr === null) {
console.log("无法找到模块基地址");
return;
}
var funcPtr = baseAddr.add(0xA4834);
Interceptor.attach(funcPtr, {
onEnter: function (args) {
// 假设参数是指针,但具体类型未知
console.log("参数3(指针):", args[1]);
// 将JNI指针转换为Java对象
var ContextClass = Java.use("android.content.Context");
var StringClass = Java.use("java.lang.String");
// 打印参数
console.log("Context: " + jcontext2context(args[2])); // 注意:这通常不会按预期工作,因为需要适当的转换
console.log("String: " + jstring2Str(args[3])); // 这同样需要正确的转换
// 或者,如果参数是整数或结构体的指针,你可能需要其他方式来解析它们
},
onLeave: function (retval) {
// 检查返回值是否看起来像一个有效的地址
console.log("返回值: " + jstring2Str(retval)); // 这同样需要正确的转换
}
});
});
}
因此,可以得出结论hkey
的生成逻辑就在函数sub_A4834中,那么下面的任务就是分析函数sub_A4834
2024-09-12
hook此函数可以得到如下结果
这两个值是一对应的。参数2对应jmethodID的地址,参数3对应传入该方法参数的地址,可以看到两次hook对应的jmethodID是一样的,只有参数不同。
hook代码
function hook_sub_func() {
Java.perform(function () {
var baseAddr = Module.findBaseAddress("libhbsecurity.so");
if (baseAddr === null) {
console.log("无法找到模块基地址");
return;
}
var funcPtr = baseAddr.add(0xA3268);
//var ptrv156 = baseAddr.add(0x168);
//console.log("v156:",hexdump(ptrv156))
Interceptor.attach(funcPtr, {
onEnter: function (args) {
// 这里可以添加一些进入函数前的处理
console.log("========开始=========");
console.log("参数1:",jclass2class(args[1])); //jclass
console.log("参数2:",(args[2])); //jmethodID
console.log("参数3:",(args[3])); //应该是方法参数
console.log("参数4:",jstring2Str(args[4])); // 应该是编码格式
//console.log("参数1:",args[1].readCString());
},
onLeave: function (retval) {
console.log("返回值:",jstring2Str(ptr(retval)));
console.log("========结束=========");
}
});
});
}
2024-09-14
参数3的类型为[B,确认是否为 Java 引用地址:在 Frida 的 Java 环境中,地址可能并不是直接的物理内存地址,而是 JVM 引用的某种形式。考虑如何在Java层中对其进行处理
此代码将一些java类型进行转换,不再需要用jstring2string
function objectToString(obj) {
var result;
Java.perform(function() {
result = Java.cast(obj, Java.use("java.lang.Object")).toString(); // 转换为 String 表示
});
return result;
}
2024-09-24
var javaclass = Java.vm.tryGetEnv().getByteArrayElements(args[3]);
console.log("byte_ptr: ",hexdump(ptr(javaclass)));
使用此方法可以打印出args[3]的值
HPPDCEAENEHBFHPASRDCAMNHJLAAPF
,猜测可能和RSA算法有关2024-09-28
hkey的生成方式为:请求的接口+秒级时间戳+imei+heybox_id进行hmacsha512加密后得到的结果再进行标准crc32计算即可得到hkey,至于hmacsha512加密的密钥就自己找吧
Python实现
此内容已隐藏
总结
2024-09-28
hkey算法,支持版本(1.3.332, 1.3.336)
补充 (关于data_report的post请求中的请求体)2024-10-06
请求体中有三个参数:data,key,sid
data参数
加密方式:AES/CBC/PKCS7Padding
使用随机16位长度字符串来生成AES密钥,初始化向量为abcdefghijklmnop
。有此两个值即可进行AES加密。
明文:
数据类似为
将此数据进行GZIP压缩,再转为字节数组。
key参数
加密方式:RSA/ECB/PKCS1Padding
随机16位长度字符串转字节数组后进行RSA加密的结果进行base64格式编码。
公钥为
-----BEGIN RSA PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZgjVwAiKTjZ55nG+mW6r3TSU4
ECvNYqDMIS/bhCj2QaH5GI/KZb2TBp+CBvUj9SLFnmJQ0kzHzHoGZCQ88VevCffF
7JePGF9cmKQqotlfTKbV4oxV5iLz7JSG6b/Vg7AXtrTolNtWsa8HiB0tI0YClYaQ
lOXm4UxLeSxQwSFETwIDAQAB
-----END RSA PUBLIC KEY-----
sid参数
加密方式:MD5
对两个字符串进行MD5加密,然后拼接起来
第一段为参数key加上时间戳然后MD5,第二段为参数data直接MD5,因此如果有了参数data和参数key的生成,则sid的生成也就有了。
注:16位随机数为data参数加密时生成AES密钥的值,同时是key参数的明文字符串
Python复现
注意:由于base64的编码格式问题,java层中的base64编码会对字符串每76个字符后加一个换行符,如果要在python中复现,需要手动去处理。处理base64换行符的代码如下:
def insert_newline_every_76_chars(input_str):
# 用列表来保存结果
result = []
# 循环遍历字符串,每76个字符切片
for i in range(0, len(input_str), 76):
result.append(input_str[i:i+76]) # 每次取76个字符
# 将切片后的部分用 '%0A' 连接
return '\n'.join(result)
还要注意的是,参数data在完成每76个字符换行后,结尾加一个\n再去进行MD5计算,而参数key完成换行符添加后,结尾加一个\n再加时间戳再进行MD5计算。
注:在测试请求时,请求失败报两种错误,第一种是请求失败了,第二种是非法请求。当时间戳距离当前时间戳较远时,会报请求失败了,而时间戳距离较近时,报错非法请求。
复现代码
此内容已隐藏
参考:绕过最新版bilibili app反frida机制
楼主请问有没有完整的hkey 算法
暂时还没研究出来
大佬,能方便看看你的 hook RegisterNatives不分的代码吗
第二种
大佬的小黑盒研究出来吗,我到那个get_vx那 发现是根据一个字符串生成一个字符串
太牛逼了 大佬
大佬解析出来nonce的算法了吗
没研究这个,我传的固定值
博主能带带hkey吗
谢谢大佬
看一看
复现代码怎么看不到
有复现代码吗,大佬
那个sha512的密钥如何获取
大佬解析出来nonce的算法了吗
旧版本hkey似乎无法签到了,新版本hkey生成也不一样>﹏<