在开始逆向分析B站Sign算法之前,我们需要做好充分的准备工作。首先需要明确的是,逆向分析本质上是一个"黑盒测试"的过程,我们需要通过各种工具和技术手段,逐步揭开目标系统的内部实现逻辑。
进行Native层逆向分析需要准备以下工具链:
我建议初学者可以先从Frida和JADX这两个工具入手,它们的学习曲线相对平缓,而且社区资源丰富。在实际工作中,我经常遇到一些开发者问"为什么我的Frida脚本不生效",90%的情况都是因为环境配置问题,所以一定要确保工具链配置正确。
在逆向工程中,定位目标函数是关键的第一步。对于B站的Sign算法,我们可以采用"由外而内"的分析策略:
在实际操作中,我通常会先用Objection进行快速的函数Hook,这样可以快速缩小分析范围。比如对于B站的案例,我们可以先用以下命令快速定位:
bash复制objection -g tv.danmaku.bili explore
android hooking watch class_method com.bilibili.nativelibrary.LibBili.s --dump-args --dump-backtrace --dump-return
通过分析网络请求,我们可以发现B站的API请求中都会携带一个sign参数。通过Hook网络请求库,我们可以追溯到签名生成的入口点。在我的分析过程中,发现签名生成最终调用了com.bilibili.nativelibrary.LibBili.s方法。
这个方法的特点是:
为了更详细地观察方法的输入输出,我编写了以下Frida Hook脚本:
javascript复制function hookSign() {
Java.perform(function() {
var LibBili = Java.use("com.bilibili.nativelibrary.LibBili");
LibBili.s.implementation = function(map) {
console.log("\n[+] Input Map Contents:");
var entries = map.entrySet().toArray();
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
console.log(entry.getKey() + " => " + entry.getValue());
}
var result = this.s(map);
console.log("\n[+] Return Value: " + result);
return result;
};
});
}
这个脚本不仅能打印输入参数,还能输出返回值。在实际调试中,我发现B站的签名算法对参数顺序敏感,这也是为什么参数类型是SortedMap而不是普通的Map。
定位Native函数有两种主要方式:静态注册和动态注册。通过实践发现B站使用的是动态注册方式,这需要我们使用特殊的技巧来定位。
我推荐使用改进版的RegisterNatives Hook脚本:
javascript复制function hook_RegisterNatives() {
var symbols = Module.enumerateSymbolsSync("libart.so");
var addrRegisterNatives = null;
// 查找RegisterNatives函数地址
symbols.forEach(function(symbol) {
if (symbol.name.includes("RegisterNatives") &&
!symbol.name.includes("CheckJNI")) {
addrRegisterNatives = symbol.address;
console.log("RegisterNatives found at:", symbol.address);
}
});
if (addrRegisterNatives) {
Interceptor.attach(addrRegisterNatives, {
onEnter: function(args) {
var className = Java.vm.tryGetEnv().getClassName(args[1]);
if (className === "com.bilibili.nativelibrary.LibBili") {
var methodCount = args[3].toInt32();
var methodsPtr = args[2];
for (var i = 0; i < methodCount; i++) {
var namePtr = Memory.readPointer(methodsPtr.add(i * Process.pointerSize * 3));
var sigPtr = Memory.readPointer(methodsPtr.add(i * Process.pointerSize * 3 + Process.pointerSize));
var fnPtr = Memory.readPointer(methodsPtr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));
var name = Memory.readCString(namePtr);
var sig = Memory.readCString(sigPtr);
var module = Process.findModuleByAddress(fnPtr);
console.log(`[RegisterNatives] ${name} ${sig} at ${fnPtr} in ${module.name}`);
}
}
}
});
}
}
这个脚本可以帮助我们准确找到目标Native函数的地址和所在的so库。
JNItrace是一个强大的工具,但在实际使用中需要注意以下几点:
一个实用的JNItrace命令如下:
bash复制jnitrace -l libbili.so -m spawn tv.danmaku.bili
在实际分析中,我发现JNItrace的输出非常详细,但需要结合上下文理解。比如当看到GetStringUTFChars调用时,需要关注它操作的是哪个Java字符串,这通常可以通过指针地址回溯来确定。
拿到so文件后,我习惯先用IDA进行初步分析。对于B站的libbili.so,分析过程有几个关键点:
在IDA中,我通常会进行以下操作:
通过动态和静态分析的结合,我逐步还原出了B站的签名算法流程:
关键代码逻辑如下:
c复制// 伪代码表示签名生成过程
void generate_sign(JNIEnv *env, jobject map) {
// 1. 从map中获取参数并排序拼接
string sorted_params = sort_and_concat_params(map);
// 2. 追加固定密钥
string sign_input = sorted_params + "560c52ccd288fed045859ed18bffd973";
// 3. 计算MD5
unsigned char md5_result[16];
MD5((unsigned char*)sign_input.c_str(), sign_input.length(), md5_result);
// 4. 转换为16进制字符串
char sign[33];
for(int i = 0; i < 16; i++) {
sprintf(sign + i*2, "%02x", md5_result[i]);
}
// 返回结果
return env->NewStringUTF(sign);
}
在实际验证过程中,我编写了一个Python脚本来模拟这个签名过程,确认能够生成与服务端一致的签名。
在逆向工程中,动态分析和静态分析应该交替进行。我的典型工作流程是:
这种方法可以避免陷入静态分析的细节泥潭,也能防止动态分析的片面性。
在实际逆向过程中,我遇到过各种问题,以下是几个典型场景及解决方法:
问题1:Frida脚本无法注入
问题2:IDA无法识别JNI函数参数
问题3:算法识别困难
逆向分析往往需要反复尝试,以下是一些提高效率的建议:
我在实际项目中发现,良好的工程实践可以显著提高逆向效率。比如为每个分析项目创建独立的工作目录,使用版本控制管理分析脚本等。