前言
必剪(com.bilibili.studio)是 B 站旗下的视频剪辑 App,其账号体系复用 B 站主端,鉴权流程涉及 Native 层签名。本文记录完整逆向过程。
环境准备
| 工具 | 版本/用途 |
|---|---|
| jadx | 1.5.0,Java 层静态分析 |
| IDA Pro | 8.3,分析 libbili.so |
| Frida | 16.x,运行时 hook |
| mitmproxy | 10.x,抓包 |
# 提取 APK 中的 so
apktool d com.bilibili.studio.apk -o bijian_out
ls bijian_out/lib/arm64-v8a/
# => libbili.so libbiliverify.so libopenssl.so ...
第一步:jadx 分析 Java 层
在 jadx 中全局搜索 appkey,定位到:
// com.bilibili.lib.foundation.config.BiliConfig
public static final String APP_KEY = BuildConfig.APP_KEY;
BuildConfig 的值被混淆移入 Native 层,真正的 key/secret 通过 JNI 调用获取。
第二步:IDA 分析 libbili.so
- 打开 IDA → File → Load →
libbili.so(arm64-v8a) - 搜索字符串
appkey,发现引用在sub_4A2C30 - 交叉引用找到
Java_com_bilibili_lib_sign_BiliSign_nativeGetAppKey
反编译结果(伪代码):
jstring Java_com_bilibili_lib_sign_BiliSign_nativeGetAppKey(JNIEnv *env, jobject obj) {
return (*env)->NewStringUTF(env, "1d8b6e7d45233436");
}
jstring Java_com_bilibili_lib_sign_BiliSign_nativeGetSecret(JNIEnv *env, jobject obj) {
// 数据经过 XOR 混淆,密钥为固定偏移
return (*env)->NewStringUTF(env, "560c52ccd288fed045859ed18bffd973");
}
提取结果:
- appkey = 1d8b6e7d45233436
- secret = 560c52ccd288fed045859ed18bffd973
第三步:签名算法还原
B 站签名(sign)= MD5(param_string + secret),其中 param_string 为所有参数按 key 字典序拼接。
import hashlib, urllib.parse
def bili_sign(params: dict, secret: str) -> str:
sorted_params = sorted(params.items())
query = urllib.parse.urlencode(sorted_params)
raw = query + secret
return hashlib.md5(raw.encode()).hexdigest()
# 示例
params = {
"appkey": "1d8b6e7d45233436",
"ts": "1700000000",
"type": "sms"
}
sign = bili_sign(params, "560c52ccd288fed045859ed18bffd973")
print(sign)
第四步:SMS 注册流程
抓包分析注册接口:
POST https://passport.bilibili.com/x/passport-login/web/sms/send
Content-Type: application/x-www-form-urlencoded
appkey=1d8b6e7d45233436&cid=86&tel=138xxxxxxxx
&recaptcha_token=<GeeTest_token>&ts=1700000000&sign=<md5>
GeeTest v3 对接
必剪使用 GeeTest v3 gt 验证码,触发时服务端返回:
{
"gt": "a5b35867e9b28a7f93f1b56f3d4aXXXX",
"challenge": "a2e0b40bc8d2a3f1e0e6fXXXX",
"success": 1
}
使用 geetest-validator 或对接第三方打码平台(2captcha/yescaptcha):
import requests
def solve_geetest(gt: str, challenge: str, api_key: str) -> dict:
resp = requests.post("https://api.yescaptcha.com/createTask", json={
"clientKey": api_key,
"task": {
"type": "GeeTestTaskProxyless",
"websiteURL": "https://passport.bilibili.com",
"gt": gt,
"challenge": challenge
}
})
task_id = resp.json()["taskId"]
# 轮询结果...
return result # {"validate": ..., "seccode": ..., "challenge": ...}
总结
| 步骤 | 工具 | 关键产出 |
|---|---|---|
| 提取 appkey/secret | IDA Pro | 明文字符串 |
| 签名算法 | Python | MD5(params+secret) |
| GeeTest 绕过 | 第三方打码 | validate/seccode |
| SMS 注册 | requests | 完整注册流程 |
免责声明:本文仅供安全学习研究使用,请勿用于违法目的。