某某语音auth值生成分析
北海 发表于 广东 历史精选 617浏览 · 2024-12-21 09:59

本文仅用于分享交流

抓包发现校验的参数auth

下面来分析这个auth的生成

用jadx反编译apk

经过尝试,发现搜索关键字"auth"能找到相关位置

定位到这个拦截器的位置

进入函数m11706a

进入函数m11704a

进入函数m7257b

进入函数m7246b

最后发现native_newmakeUrl是jni函数

将库makeurl3.3.0拖入ida,并没有在导出表中发现相关函数名,说明可能是动态加载

这时候就要祭出hook脚本来定位native_newmakeUrl的位置
在Android系统中,与JNI相关的核心实现大多集中在libart.so这个动态共享库中。其中RegisterNatives函数负责动态注册,所以通过hook打印出其调用时的关键参数信息,就能找到动态加载的so库函数的相关信息
代码如下

function hook_RegisterNatives() {
    var symbols = Module.enumerateSymbolsSync("libart.so");
    var addrRegisterNatives = null;
    for (var i = 0; i < symbols.length; i++) {
        var symbol = symbols[i];

        //_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi
        if (symbol.name.indexOf("art") >= 0 &&
                symbol.name.indexOf("JNI") >= 0 &&
                symbol.name.indexOf("RegisterNatives") >= 0 &&
                symbol.name.indexOf("CheckJNI") < 0) {
            addrRegisterNatives = symbol.address;
            console.log("RegisterNatives is at ", symbol.address, symbol.name);
        }
    }

    if (addrRegisterNatives != null) {
        Interceptor.attach(addrRegisterNatives, {
            onEnter: function (args) {
                console.log("[RegisterNatives] method_count:", args[3]);
                var env = args[0];
                var java_class = args[1];
                var class_name = Java.vm.tryGetEnv().getClassName(java_class);
                //console.log(class_name);

                var methods_ptr = ptr(args[2]);

                var method_count = parseInt(args[3]);
                for (var i = 0; i < method_count; i++) {
                    var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
                    var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
                    var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));

                    var name = Memory.readCString(name_ptr);
                    var sig = Memory.readCString(sig_ptr);
                    var find_module = Process.findModuleByAddress(fnPtr_ptr);
                    console.log("[RegisterNatives] java_class:", class_name, "name:", name, "sig:", sig, "fnPtr:", fnPtr_ptr, "module_name:", find_module.name, "module_base:", find_module.base, "offset:", ptr(fnPtr_ptr).sub(find_module.base));

                }
            }
        });
    }
}

setImmediate(hook_RegisterNatives);

但是发现了存在frida反调试,估计是有特征检测和进程附加

绕过的办法很多,这里选择使用florida这个去除部分特征的server
https://github.com/Ylarod/Florida

运行hook nativeregister的js脚本,发现能绕过,而且打印出了native_makeUrl的地址

在ida中跳转到0x6168,这里的sub_0005就是native_makeUrl的逻辑了

下面来hook一下这个native_makeUrl,来确定定位的对不对

Java.perform(() => {
    // 获取包含目标原生函数的Java类对象
    var JniMakeUrlClass = Java.use("com.douyu.lib.http.JniMakeUrl");
    if (JniMakeUrlClass) {
        // 替换目标原生函数的实现
        JniMakeUrlClass.native_newmakeUrl.implementation = function (context, str, strArr, strArr2, strArr3, strArr4, i, i2) {
            // 在这里可以添加代码来打印传入的参数信息
            console.log("Context 参数: ", context);
            console.log("字符串参数: ", str);
            console.log("字符串数组参数(长度: " + (strArr && strArr.length || 0) + "):");
            if (strArr && Array.isArray(strArr)) {
                for (var j = 0; j < strArr.length; j++) {
                    console.log("  元素 " + j + ": " + strArr[j]);
                }
            }
            // 同理处理其他字符串数组参数strArr2、strArr3、strArr4
            console.log("字符串数组参数2(长度: " + (strArr2 && strArr2.length || 0) + "):");
            if (strArr2 && Array.isArray(strArr2)) {
                for (var k = 0; k < strArr2.length; k++) {
                    console.log("  元素 " + k + ": " + strArr2[k]);
                }
            }
            console.log("字符串数组参数3(长度: " + (strArr3 && strArr3.length || 0) + "):");
            if (strArr3 && Array.isArray(strArr3)) {
                for (var l = 0; l < strArr3.length; l++) {
                    console.log("  元素 " + l + ": " + strArr3[k]);
                }
            }
            console.log("字符串数组参数4(长度: " + (strArr4 && strArr4.length || 0) + "):");
            if (strArr4 && Array.isArray(strArr4)) {
                for (var m = 0; m < strArr4.length; m++) {
                    console.log("  元素 " + m + ": " + strArr4[m]);
                }
            }
            console.log("整数参数1: ", i);
            console.log("整数参数2: ", i2);

            // 调用原函数获取返回值
            var retval = this.native_newmakeUrl(context, str, strArr, strArr2, strArr3, strArr4, i, i2);
            // 可以在这里打印返回值相关信息,比如
            console.log("函数返回值: ", retval);
            return retval;
        };
    } else {
        console.log("未能找到 com.douyu.lib.http.JniMakeUrl 类");
    }
});

看着没啥毛病,估计调用链定位得没问题

总结调用链

com.dyheart.lib.dylog.network.RequestInterceptor.a(Request request, String str)——>
com.dyheart.lib.dylog.network.RequestInterceptor.a(Context context, String str, Map<String, String> map, Map<String, String> map2, String str2)——>
com.douyu.lib.http.MakeUrlClient.b(Context context, String str, String[] strArr, String[] strArr2, String[] strArr3, String[] strArr4, int i, int i2)——>
com.douyu.lib.http.MakeUrlClient.b(Context context, String str, String[] strArr, String[] strArr2, String[] strArr3, String[] strArr4, int i, int i2)——>
com.douyu.lib.http.JniMakeUrl.native_newmakeUrl

下面具体分析auth的生成
有时候感觉不必每个函数的看得清清楚楚,找到关键点可能会有四两拨千斤的效果
这里的关键的就是native_newmakeUrl函数
通过hook,可以看到native_newmakeUrl的参数是多数都是些固定值,除了字符串数组2的元素3,目测是一个时间戳


于是下面从变成从前面调用链中的许多的函数中找到这个元素3是怎么生成的就好了
先定位到前一个函数的strArr2


接着回溯


最终发现这个元素3是在调用链的最开始传进来的,是时间戳无疑了,System.currentTimeMillis()/1000一眼钉帧了

这样子用于生成auth的所有参数都搞定了

接着看看auth是怎么生成的,我们来到偏移0x6168,将第一个参数类型改成JNIEnv*。这里起手是拉到返回值出逆着看。
很快就发现这个返回值是将v29转变成java字符串而成的,而v29可能与v33有关


接着网上看发现v33被sub_10898赋值了


于是hook一下sub_10898

function hook_sub_10898(){
    var addr = Module.findBaseAddress("libmakeurl3.3.0.so");
    console.log("libmakeurl3.3.0.so base address: " + addr);
    var funcAddr = addr.add(0x10898);
    console.log("makeurl3.3.0.so makeurl address: " + funcAddr);
    Interceptor.attach(funcAddr, {
        onEnter: function (args) {
            this.args8 = args[8];
            console.log("args0: " + args[0]);
            console.log("args1: " + args[1].toInt32());
            console.log("args2: " + args[2]);
            console.log("args3: " + args[3]);
            console.log("args4: " + Memory.readUtf8String(args[4]));
            console.log("args5: " + (args[5].readPointer()));
            console.log("args6: " + args[6].toInt32());
            console.log("args7: " + args[7].toInt32());
            console.log("args8: " + hexdump(args[8]));
        },
        onLeave: function (retval){
            // console.log("retval:" + retval);
            console.log("retval:" + Memory.readUtf8String(retval));
            console.log("args8:" + hexdump(this.args8));
        }
    })
}

这里的args8也就是v33,它经过了这个函数确实变成了auth的值

于是键入sub_10898,还是起手拉最后,看返回值
emm...... 发现result是a6和a9拼接的,但是并没有直接看到a9是怎么来的qaq

太菜了wuwuwu,还是先hook一手离result最近的sub_CD68

function hook_sub_CD68(){
    var addr = Module.findBaseAddress("libmakeurl3.3.0.so");
    console.log("libmakeurl3.3.0.so base address: " + addr);
    var funcAddr = addr.add(0xCD68);
    Interceptor.attach(funcAddr, {
        onEnter: function (args) {
            this.args1 = args[1];
            console.log("args0: " + args[0].readUtf8String());
            console.log("args1: " + args[1]);
            console.log("args2: " + hexdump(args[2]));
        },
        onLeave: function (retval){
            console.log("retval:" + retval);
        }
    })
}

经过观察,后面拼接的"vOXGo3ad7hLHyTw4Zgu2blCjEBQDcx6z"貌似是不会变的

观察一下前面的sub_CE10,这个貌似是规范输出的


观察一下sub_CC34,这个长得有点像MD5

于是对比一下,发现确实是MD5

于是分析到此就可以知道auth是如何生成的了。

2 条评论
某人
表情
可输入 255
目录