Frida过某黑盒frida检测及hkey逆向及post请求体分析(已完结,文末有签到脚本)
本文最后更新于 239 天前,其中的信息可能已经有所发展或是发生改变。

前言

本文章仅做移动安全学习交流用途,严禁作其他用途,如果侵犯您的权益请联系我删除。
目标版本是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,因此这三者是对应的,也就是说encode得到的结果与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]的值


还可以发现其中有RSA关键字和一串字符串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机制

签到及日常任务脚本

Github

转载请注明:
作者:syy,出处:https://www.94i.top/index.php/2024/09/03/frida过某黑盒frida检测/

评论

  1. 皮皮熊
    1 年前
    2024-9-11 14:52:14

    楼主请问有没有完整的hkey 算法

    • 博主
      皮皮熊
      1 年前
      2024-9-12 15:57:39

      暂时还没研究出来

  2. 12 月前
    2024-9-25 16:34:33

    大佬,能方便看看你的 hook RegisterNatives不分的代码吗

    • 博主
      yytkkn
      12 月前
      2024-9-26 10:57:15
      • syy
        12 月前
        2024-9-26 14:27:02

        大佬的小黑盒研究出来吗,我到那个get_vx那 发现是根据一个字符串生成一个字符串

  3. 11 月前
    2024-10-18 13:25:05

    太牛逼了 大佬

  4. 11 月前
    2024-10-18 18:22:09

    大佬解析出来nonce的算法了吗

    • 博主
      yytkkn
      11 月前
      2024-10-22 14:50:27

      没研究这个,我传的固定值

  5. 11 月前
    2024-10-25 14:41:35

    博主能带带hkey吗

  6. ltkami
    7 月前
    2025-2-26 11:34:20

    谢谢大佬

  7. ltkami
    7 月前
    2025-2-27 21:52:24

    看一看

  8. ltkami
    7 月前
    2025-2-27 21:53:41

    复现代码怎么看不到

  9. Garcke
    4 月前
    2025-5-05 19:52:51

    有复现代码吗,大佬

  10. 不吃鱼
    4 月前
    2025-5-05 23:18:43

    那个sha512的密钥如何获取

  11. 非酋
    3 月前
    2025-6-17 19:01:20

    大佬解析出来nonce的算法了吗

  12. kk
    3 周前
    2025-8-24 21:18:43

    旧版本hkey似乎无法签到了,新版本hkey生成也不一样>﹏<

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇