Android Native逆向实战:Frida+JNItrace定位B站Sign算法
1. 项目概述从零到一逆向B站Sign算法的完整旅程最近在折腾Android Native层的逆向分析目标直指B站客户端的Sign签名算法。对于很多刚接触Native逆向的朋友来说这就像面对一堵高墙Java层的逆向工具和思路相对成熟一旦逻辑下沉到so库用C/C编写传统的静态分析工具就显得力不从心动态调试的门槛也陡然增高。这次实战我选择用Frida和JNItrace这两件“神器”组合出击完整地走通了定位、分析、还原算法的全过程。整个过程充满了“踩坑”与“顿悟”非常适合像我一样从Java层逆向转向Native层探索的新手参考。如果你也好奇B站App的请求是如何被加上那一串神秘签名的或者想学习一套通用的Native层Hook与分析方法那么这篇笔记或许能给你带来不少启发。简单来说我们的目标就是搞清楚B站App在发起网络请求时那个关键的sign参数是如何生成的。这个参数通常用于验证请求的合法性防止篡改和重放是客户端与服务器通信的重要“暗号”。算法核心逻辑往往被编译进so动态链接库中以提高安全性。因此这次逆向的核心战场就在这些so文件里我们需要一套能够深入Native层函数调用、监控JNI交互的方法。2. 逆向环境与工具链的精心搭建工欲善其事必先利其器。一个稳定、高效的逆向环境是成功的一半。这次实战主要依赖两大工具Frida和JNItrace。前者是动态插桩的瑞士军刀后者则是透视JNI调用的“X光机”。它们的组合能让我们在不太深入汇编指令的情况下依然清晰地看到函数调用流和数据变化。2.1 Frida环境部署从PC到手机的贯通Frida的安装看似简单但版本兼容性是个大坑。我的环境是Windows 11主机一部已Root的Android 10测试机以及目标B站App版本号7.xx.xx具体版本建议选择稍旧一些的稳定版新版本可能加固更强。PC端安装直接在命令行使用pip安装是最快的方式。但务必注意frida和frida-tools的版本需要匹配且最好与手机端运行的frida-server版本一致。pip install frida15.2.2 frida-tools12.0.0这里我选择了15.2.2这个相对稳定的版本。安装完成后用frida --version验证。手机端部署根据手机CPU架构通常是arm64-v8a去Frida的GitHub Releases页面下载对应版本的frida-server-15.2.2-android-arm64.xz。解压得到二进制文件通过adb推送到手机并赋予执行权限adb push frida-server-15.2.2-android-arm64 /data/local/tmp/ adb shell su cd /data/local/tmp chmod 755 frida-server-15.2.2-android-arm64在手机端后台运行frida-server./frida-server-15.2.2-android-arm64 。更稳妥的方法是使用nohup或创建一个init.d脚本确保进程稳定。连接测试在PC端执行frida-ps -U如果能看到手机上的进程列表恭喜你环境打通了。注意很多加固或风控方案会检测Frida。如果遇到App闪退或frida-ps看不到目标进程可能是触发了反调试。此时可以尝试更换Frida版本如使用较旧的14.x版本、使用定制化的frida-server修改默认端口、名称、或者借助其他工具如objection先绕过一些简单的检测。本次分析的B站版本在常规环境下可直接附加。2.2 JNItrace工具集成照亮JNI调用的迷雾JNItrace是一个基于Frida的脚本它能打印出所有JNI函数的调用及其参数、返回值对于定位Java与Native代码的交互点至关重要。安装非常简单pip install jnitrace安装后我们就可以通过jnitrace命令来启动对目标App的监控。它的输出日志非常详细是后续分析的关键入口。2.3 辅助工具准备IDA Pro/Ghidra用于so文件的静态分析。当通过动态追踪定位到关键函数后需要反编译查看其伪代码逻辑。adb 抓包工具Charles/Fiddler用于捕获B站App的网络请求确认sign参数的存在和格式。抓包需要配置好手机代理和SSL证书解密B站用了SSL Pinning可能需要额外工具如JustTrustMe模块配合Frida绕过。一台Root过的Android测试机这是运行frida-server和进行深度Hook的前提。模拟器如雷电也可以但部分反调试方案在模拟器上行为可能不同且Frida对模拟器的支持有时需要特殊配置。3. 核心思路与逆向策略拆解面对一个庞大的App直接扎进海量的so文件里无疑是盲人摸象。我的逆向策略遵循了“由外而内、由动至静”的原则层层递进。3.1 策略总览从网络抓包到代码定位行为观察抓包首先通过抓包工具清晰地看到哪个API请求携带了sign参数它的值长什么样通常是32位或64位的十六进制字符串以及它和哪些请求参数如时间戳、请求体可能有关联。记录下几个典型的请求。入口定位JNI监控使用JNItrace大面积扫描寻找所有计算或生成sign字符串可能的JNI调用路径。Sign的生成最终很可能由Java层某个方法触发调用Native方法完成。关键函数HookFrida根据JNItrace的线索用Frida编写精确的Hook脚本针对特定的Native函数或Java方法打印其输入、输出及关键中间变量。逻辑分析与还原静态分析将Hook得到的核心函数地址在IDA Pro中定位阅读其反编译的C/C伪代码结合动态观察到的数据流理解并最终还原出算法逻辑。算法复现与验证使用Python或Java将逆向出来的算法逻辑重新实现并用抓包到的原始数据测试看能否生成一致的sign值。3.2 为什么选择FridaJNItrace组合对于Native逆向新手这个组合的优势在于降低了起步门槛。Frida提供了在运行时动态修改内存、拦截函数的能力。你不需要一开始就理解复杂的汇编指令可以先通过Hook观察函数行为。JNItraceJNIJava Native Interface是Java代码和Native代码通信的桥梁。Sign算法的入口极有可能是一个native声明的Java方法。JNItrace能自动追踪所有这些桥梁上的“车辆”调用告诉我们什么时候、哪个Java方法调用了哪个so里的哪个函数传递了什么参数。这相当于给了我们一张清晰的“地图”直接标出了可疑地点避免了在数百万条汇编指令中大海捞针。4. 实战第一步捕获目标与定位入口理论说得再多不如动手操作。我们正式开始。4.1 网络请求抓包与特征分析配置好抓包工具和SSL绕过后打开B站App进行一些能触发网络请求的操作比如刷新首页、搜索视频。在Charles中你会发现类似api.bilibili.com域名的请求其URL或Form Data里包含一个名为sign的参数。例如一个搜索请求可能看起来像https://api.bilibili.com/xxx/search?keywordtestts1648888888signabcdef1234567890abcdef1234567890初步观察点sign值通常是32位或64位的十六进制字符串。除了sign请求中通常还有ts时间戳等参数。尝试重复发送相同参数的请求sign值会变化说明它可能和ts甚至一个随机数有关。尝试修改keyword再发送sign也变了说明它和请求参数内容有关。这个阶段我们假设sign是对“请求参数密钥时间戳”等元素通过某种哈希算法如MD5、SHA256或HMAC计算得出的。我们的任务就是找到这个计算发生的地方。4.2 启动JNItrace进行全景扫描这是最关键的一步。我们需要在App启动时就开始监控因为Sign计算可能发生在App初始化或早期。jnitrace -l libbili*.so -m com.bilibili.app com.bilibili.app.in-l libbili*.so只跟踪B站相关so库的JNI调用减少无关日志。你可以先不加这个参数跑一遍看看有哪些so再针对性过滤。-m指定启动的Activity。com.bilibili.app是主进程有时核心逻辑在子进程如com.bilibili.app.in需要分别尝试。命令执行后JNItrace会启动B站App并输出海量的日志。这时我们在手机上操作触发之前抓包看到的那个带sign的搜索请求。4.3 在日志海洋中寻找“Sign”的蛛丝马迹JNItrace的日志输出到控制台。我们需要仔细搜索。重点关注以下几类调用FindClass/GetMethodID/CallStaticObjectMethod等这些可能是在Native层主动调用Java类方法。也许Native算法需要获取一些Android系统参数如设备ID。NewStringUTFNative层创建了一个Java字符串。一个计算好的sign最终很可能要通过NewStringUTF返回给Java层。涉及加密哈希相关的函数名虽然JNI函数名本身不直接显示业务逻辑但我们可以搜索日志中的字符串字面量。在触发请求的时间点附近用文本编辑器的查找功能搜索“sign”、“md5”、“sha”、“hmac”、“encrypt”等关键词。一个典型的发现过程可能是 在触发搜索请求的瞬间日志中突然出现一连串密集的JNI调用。你可能会看到类似下面的片段这是简化后的示意[] Call Stack 0: com.bilibili.lib.security.SignUtils - nativeGenerateSign (Ljava/lang/String; Ljava/lang/String; J)Ljava/lang/String; libbilibili_crypto.so!0x7d6c (Java_com_bilibili_lib_security_SignUtils_nativeGenerateSign)这行日志告诉我们Java类com.bilibili.lib.security.SignUtils中的nativeGenerateSign方法被调用了它实现在libbilibili_crypto.so中对应的Native函数地址是0x7d6c偏移量。这就是我们梦寐以求的入口点此外在调用nativeGenerateSign之前或之后可能会看到GetStringUTFChars获取Java字符串参数和NewStringUTF返回结果的调用这进一步验证了我们的猜想。5. 深入核心Frida精准Hook与参数追踪找到疑似入口后需要用Frida进行更精确的侦查和验证。我们将编写一个Frida脚本。5.1 编写Frida Hook脚本创建一个名为hook_sign.js的文件内容如下Java.perform(function () { // 1. Hook Java层的入口方法如果确认了的话 var SignUtils Java.use(com.bilibili.lib.security.SignUtils); SignUtils.nativeGenerateSign.implementation function (param1, param2, timestamp) { console.log(\n[] Java层 nativeGenerateSign 被调用); console.log( param1: param1); console.log( param2: param2); console.log( timestamp: timestamp); var result this.nativeGenerateSign(param1, param2, timestamp); // 调用原函数 console.log( [] 返回值: result); return result; }; // 2. Hook Native层的函数通过地址 Interceptor.attach(Module.findBaseAddress(libbilibili_crypto.so).add(0x7d6c), { onEnter: function (args) { console.log(\n[] Native函数 Java_com_bilibili_..._nativeGenerateSign 进入); // args[0] 是JNIEnv*, args[1] 是jclass/jobject, args[2], args[3]... 是参数 // 读取Java字符串参数需要调用JNI函数这里简化通常更复杂 console.log( 函数地址: this.returnAddress); // 可以在这里打印内存但需要更多操作 }, onLeave: function (retval) { console.log([] Native函数离开); // retval 是返回的jstring可以尝试读取 // var jniEnv Java.vm.getEnv(); // var resultStr jniEnv.getStringUtfChars(retval, null); // console.log( 返回的字符串: resultStr.readCString()); } }); // 3. Hook通用的加密函数如果知道用了什么库 // 例如Hook OpenSSL的MD5_Init/Update/Final var md5_init_addr Module.findExportByName(libcrypto.so, MD5_Init); if (md5_init_addr) { Interceptor.attach(md5_init_addr, { onEnter: function (args) { console.log(\n[] MD5_Init 被调用上下文: this.context.pc); } }); } });这个脚本做了三件事Hook Java层的Native方法声明处打印传入的参数和最终结果。直接Hook我们通过JNItrace找到的Native函数地址尝试在进入和离开时打印信息。尝试Hook底层的加密库函数如OpenSSL作为辅助验证。5.2 运行脚本并分析输出通过Frida加载脚本frida -U -f com.bilibili.app -l hook_sign.js --no-pause再次在App中触发搜索请求。观察控制台输出。理想情况下你会看到清晰的调用链[] Java层 nativeGenerateSign 被调用 param1: “keywordtestts1648888888” param2: “another_fixed_string” timestamp: 1648888888000 [] Native函数 Java_com_bilibili_..._nativeGenerateSign 进入 [] MD5_Init 被调用上下文: 0x7a1b3c4d [] MD5_Update 被调用... [] MD5_Final 被调用... [] Native函数离开 [] 返回值: “abcdef1234567890abcdef1234567890”通过对比抓包得到的原始请求参数和这里param1的内容你就能确定Native函数接收的输入是否就是构建签名的原始字符串。返回值与网络请求中的sign一致则完全证实了这个函数就是我们要找的目标。5.3 参数构造逻辑的逆向现在我们知道nativeGenerateSign接收三个参数。但网络请求有很多参数它们是如何被拼接成param1的呢这可能需要我们向上追溯。我们可以Hook调用nativeGenerateSign的Java方法可能是一个叫getSign的Java方法看它内部是如何收集请求参数如keyword,ts,其它固定参数并按照什么规则如按字母排序、URL键值对拼接组装成字符串然后传递给Native层的。这部分工作可能需要反复尝试和猜测。例如你可能会发现最终的参数字符串是“keywordtestts1648888888appkeyxxxxxxappsecyyyyyy”其中appkey和appsec可能是内置的固定值。而param2可能是一个盐值salt或版本标识。6. 静态分析与算法还原有了明确的函数地址libbilibili_crypto.so0x7d6c和输入输出样本就可以进行静态分析了。6.1 使用IDA Pro加载so文件将手机中的libbilibili_crypto.so文件pull到电脑上用IDA Pro打开。等待自动分析完成后按G键跳转到地址0x7d6c注意这是文件偏移在IDA中可能需要根据so加载基址换算。更简单的方法是在Hex View中搜索函数名Java_com_bilibili_lib_security_SignUtils_nativeGenerateSign的字符串直接定位函数。6.2 分析伪代码逻辑定位到函数后按F5反编译成伪代码。你会看到类似下面的结构极度简化和抽象jstring __fastcall Java_com_bilibili_lib_security_SignUtils_nativeGenerateSign(JNIEnv *env, jclass clazz, jstring param1, jstring param2, jlong timestamp) { const char *v5; // 输入字符串param1 const char *v6; // 输入字符串param2 char concatenated_str[512]; char timestamp_str[32]; unsigned char md5_result[16]; char final_sign[33]; v5 (*env)-GetStringUTFChars(env, param1, 0); v6 (*env)-GetStringUTFChars(env, param2, 0); // 1. 将时间戳转换为字符串 sprintf(timestamp_str, %lld, timestamp); // 2. 拼接字符串 param1 param2 timestamp_str (或其它顺序) strcpy(concatenated_str, v5); strcat(concatenated_str, v6); strcat(concatenated_str, timestamp_str); // 3. 计算MD5 MD5_Init(ctx); MD5_Update(ctx, concatenated_str, strlen(concatenated_str)); MD5_Final(md5_result, ctx); // 4. 将16字节MD5结果转为32位十六进制字符串 for ( i 0; i 16; i ) sprintf(final_sign[2 * i], %02x, (unsigned int)md5_result[i]); final_sign[32] 0; // 5. 可能还有二次处理如取部分字符、大小写转换等 // to_upper(final_sign); // 6. 创建Java字符串并返回 result (*env)-NewStringUTF(env, final_sign); (*env)-ReleaseStringUTFChars(env, param1, v5); (*env)-ReleaseStringUTFChars(env, param2, v6); return result; }通过阅读伪代码算法的核心步骤就清晰了字符串拼接 - MD5哈希 - 十六进制格式化。你需要仔细核对拼接的顺序、是否包含其他固定字符串、时间戳的格式等细节。这些细节必须与动态Hook时观察到的数据完全吻合。6.3 算法复现与验证最后一步用Python将算法还原import hashlib import time def generate_sign(param1, param2, timestamp): # 1. 拼接字符串 (根据静态分析确定的顺序) raw_str param1 param2 str(timestamp) # 2. 计算MD5 m hashlib.md5() m.update(raw_str.encode(utf-8)) md5_bytes m.digest() # 3. 转为十六进制字符串 sign md5_bytes.hex() # 4. 可能的后续处理如转大写 sign sign.upper() return sign # 使用抓包和Hook得到的数据进行测试 test_param1 keywordtestts1648888888 test_param2 a_fixed_salt test_ts 1648888888000 calculated_sign generate_sign(test_param1, test_param2, test_ts) print(f计算得到的sign: {calculated_sign}) # 与你抓包中真实的sign对比如果计算结果与真实请求中的sign一致那么恭喜你逆向成功7. 常见问题、踩坑记录与进阶技巧逆向过程很少一帆风顺以下是我在这次和以往项目中总结的一些典型问题和解决思路。7.1 Frida相关的问题与排查问题1Frida无法附加进程提示“Failed to attach: process not found”或进程崩溃。可能原因App有反Frida检测。排查与解决检查进程名使用frida-ps -U确认进程名是否正确。Android App可能有主进程、子进程、多开进程等。尝试延迟注入使用-fspawn模式启动App但加上--no-pause然后尽快在脚本中用setTimeout延迟Hook关键函数避免在启动初期就被检测。更换Frida版本/配置尝试老版本Frida如12.x。修改frida-server的文件名和端口通过-l参数指定。使用绕过工具结合使用objection的android anti-root-detection disable等命令或使用其他隐藏Frida的脚本。问题2Hook Native函数时Module.findBaseAddress返回null或地址不对。可能原因so文件尚未加载或者加载的基址每次运行都不同ASLR。解决// 等待so加载 function hookAfterSoLoad() { var baseAddr Module.findBaseAddress(libbilibili_crypto.so); if (baseAddr) { var targetAddr baseAddr.add(0x7d6c); Interceptor.attach(targetAddr, { ... }); } else { setTimeout(hookAfterSoLoad, 500); // 递归等待 } } setTimeout(hookAfterSoLoad, 0);7.2 JNItrace使用技巧与日志分析问题JNItrace日志太多刷屏太快找不到有用信息。解决严格过滤使用-l参数只跟踪目标so使用-i参数只跟踪特定函数如NewStringUTF。输出到文件使用-o trace.log将日志输出到文件然后用文本编辑器如VSCode的强大搜索功能支持正则表达式进行分析。搜索sign、md5、encrypt等关键词。关注调用栈JNItrace会打印调用栈Call Stack这是理解函数调用关系的黄金信息。重点关注那些在触发网络请求时刻出现的、栈底是Java用户代码的JNI调用。7.3 静态分析中的难点问题IDA Pro反编译的伪代码可读性差有很多混淆的变量名和间接调用。解决重命名变量根据上下文和动态Hook时获得的信息给关键变量起有意义的名称如v5改为input_param1_str。识别加密常量MD5、SHA256等算法的初始化向量IV是固定的常量。在IDA的Hex View中搜索这些常量如MD5的A、B、C、D初始值可以快速定位加密函数。动态调试辅助如果条件允许可以使用IDA Pro或Ghidra的远程调试功能配合adb和android_server在真机或模拟器上进行源码级调试单步跟踪验证猜想。7.4 算法还原的验证与边界情况问题自己实现的算法大部分情况正确但偶尔生成的sign对不上。排查编码问题确保拼接字符串时的编码与Native层一致通常是UTF-8。特别是中文字符需要确认。隐藏参数是否有一些隐式的参数被加入了计算比如设备信息、版本号可能在Native层通过JNI调用Java方法获取然后参与计算。回顾JNItrace日志看Native函数是否在计算前调用了CallObjectMethod等获取了其他数据。时间戳精度Native层使用的时间戳精度秒、毫秒、微秒是否与你的生成一致算法变种是否是标准MD5有没有自定义的变换如循环左移、额外异或仔细对比你的MD5结果和Hook到的Native函数中间变量如果能在MD5_Final之前Hook到ctx状态并导出将是终极验证手段。逆向工程就像侦探破案需要耐心、细致的观察和合理的推理。Frida和JNItrace的组合为Android Native逆向提供了一套强大的动态分析工具链极大地降低了入门难度。这次对B站Sign算法的逆向不仅让我获得了一个可用的算法更重要的是熟悉了从动态追踪到静态分析再到算法还原的完整方法论。记住每个App的防护强度不同思路可以复用但具体的绕过和定位技巧需要随机应变。希望这篇详细的实战笔记能为你打开Native逆向世界的大门。