前言

必剪(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

  1. 打开 IDA → File → Load → libbili.so(arm64-v8a)
  2. 搜索字符串 appkey,发现引用在 sub_4A2C30
  3. 交叉引用找到 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 完整注册流程

免责声明:本文仅供安全学习研究使用,请勿用于违法目的。