1. 逆向分析前的准备工作
在开始逆向分析B站Sign算法之前,我们需要做好充分的准备工作。首先需要明确的是,逆向分析本质上是一个"黑盒测试"的过程,我们需要通过各种工具和技术手段,逐步揭开目标系统的内部实现逻辑。
1.1 工具准备清单
进行Native层逆向分析需要准备以下工具链:
- 反编译工具:JADX或JEB用于Java层分析
- 动态调试工具:Frida框架及其配套工具
- 静态分析工具:IDA Pro或Ghidra
- 辅助工具:JNItrace、Objection等
- 开发环境:Python环境、Android SDK等
我建议初学者可以先从Frida和JADX这两个工具入手,它们的学习曲线相对平缓,而且社区资源丰富。在实际工作中,我经常遇到一些开发者问"为什么我的Frida脚本不生效",90%的情况都是因为环境配置问题,所以一定要确保工具链配置正确。
1.2 目标定位策略
在逆向工程中,定位目标函数是关键的第一步。对于B站的Sign算法,我们可以采用"由外而内"的分析策略:
- 首先通过抓包分析确定需要逆向的API接口
- 在Java层定位到生成签名的入口方法
- 通过调用栈分析找到Native层的实现
在实际操作中,我通常会先用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
2. Java层分析与定位
2.1 入口方法定位
通过分析网络请求,我们可以发现B站的API请求中都会携带一个sign参数。通过Hook网络请求库,我们可以追溯到签名生成的入口点。在我的分析过程中,发现签名生成最终调用了com.bilibili.nativelibrary.LibBili.s方法。
这个方法的特点是:
- 接收一个SortedMap对象作为参数
- 返回一个SignedQuery对象
- 是一个native方法,实现在so库中
2.2 Frida Hook脚本编写
为了更详细地观察方法的输入输出,我编写了以下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。
3. Native层动态分析
3.1 JNI函数定位技巧
定位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库。
3.2 JNItrace实战技巧
JNItrace是一个强大的工具,但在实际使用中需要注意以下几点:
- 使用spawn模式而不是attach模式,后者在某些情况下会出现问题
- 配合Frida主动调用可以获得更清晰的调用轨迹
- 需要过滤无关的JNI调用,避免信息过载
一个实用的JNItrace命令如下:
bash复制jnitrace -l libbili.so -m spawn tv.danmaku.bili
在实际分析中,我发现JNItrace的输出非常详细,但需要结合上下文理解。比如当看到GetStringUTFChars调用时,需要关注它操作的是哪个Java字符串,这通常可以通过指针地址回溯来确定。
4. 静态分析与算法还原
4.1 IDA静态分析要点
拿到so文件后,我习惯先用IDA进行初步分析。对于B站的libbili.so,分析过程有几个关键点:
- 首先定位到目标函数的偏移地址(通过动态分析获得)
- 修复JNIEnv参数类型,这有助于IDA生成更准确的伪代码
- 识别关键的字符串操作和加密函数
在IDA中,我通常会进行以下操作:
- 重命名关键函数和变量
- 添加详细的注释
- 分析函数的控制流程图
- 识别加密算法的特征常量
4.2 签名算法解析
通过动态和静态分析的结合,我逐步还原出了B站的签名算法流程:
- 将输入参数排序后拼接成字符串
- 在字符串末尾追加一个固定密钥
- 对拼接后的字符串进行MD5哈希
- 将MD5结果转换为16进制字符串作为最终签名
关键代码逻辑如下:
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脚本来模拟这个签名过程,确认能够生成与服务端一致的签名。
5. 逆向技巧与经验分享
5.1 动态分析与静态分析的结合
在逆向工程中,动态分析和静态分析应该交替进行。我的典型工作流程是:
- 先用动态分析工具快速定位关键点
- 通过静态分析理解整体逻辑
- 再回到动态分析验证假设
- 不断迭代直到完全理解算法
这种方法可以避免陷入静态分析的细节泥潭,也能防止动态分析的片面性。
5.2 常见问题与解决方案
在实际逆向过程中,我遇到过各种问题,以下是几个典型场景及解决方法:
问题1:Frida脚本无法注入
- 检查设备是否root
- 确认frida-server正在运行
- 尝试更换注入模式(spawn/attach)
问题2:IDA无法识别JNI函数参数
- 手动修正函数原型
- 参考JNI文档添加正确的参数类型
- 通过动态分析确定参数的实际用途
问题3:算法识别困难
- 查找加密算法的特征常量
- 对比已知算法实现
- 使用自动化工具如FindCrypt
5.3 性能优化建议
逆向分析往往需要反复尝试,以下是一些提高效率的建议:
- 编写可复用的Frida脚本
- 建立自己的代码片段库
- 使用Python自动化常见任务
- 对关键函数添加书签和注释
- 保持有组织的分析记录
我在实际项目中发现,良好的工程实践可以显著提高逆向效率。比如为每个分析项目创建独立的工作目录,使用版本控制管理分析脚本等。