逆向方法

从 base.apk(build 8911100)的 38 个 DEX 文件中提取字符串和类名,结合 Frida OkHttp hook 捕获真实上传请求,还原完整上传流程。

核心四步流程

Step 1: Preupload 探测    获取 upos_uri / X-Upos-Auth / biz_id
Step 2: 初始化上传        POST /{upos_path}?uploads
Step 3: 分片上传          PUT  /{upos_path}?partNumber=N&...
Step 4: 合并完成          POST /{upos_path}?output=json&uploadId=...
Step 5: 投稿提交          POST /x/vu/app/add

Step 1:Preupload 探测

def preupload_probe():
    r = session.get(
        'https://member.bilibili.com/preupload',
        params={
            'r': 'probe',        # 带宽探测,选最优节点
            'profile': 'ugcupos/st-android',
            'build': '8911100',
            'mobi_app': 'android',
        }
    )
    return r.json()  # 返回 endpoint_list,选最优

def preupload(filename, filesize, upcdn='bos'):
    r = session.get(
        'https://member.bilibili.com/preupload',
        params={
            'name': filename,
            'size': filesize,
            'r': 'upos',
            'profile': 'ugcupos/st-android',
            'upcdn': upcdn,
            'build': '8911100',
        }
    )
    data = r.json()
    return {
        'upload_id': data['upload_id'],
        'upos_uri': data['upos_uri'],  # upos://xxx
        'endpoint': data['endpoint'],
        'auth': data['X-Upos-Auth'],
        'biz_id': data['biz_id'],
        'chunk_size': data['chunk_size'],
    }

Step 2-4:分片上传

def upload_video(filepath, upload_info):
    endpoint = upload_info['endpoint']
    upos_path = upload_info['upos_uri'].removeprefix('upos:/')
    auth = upload_info['auth']
    upload_id = upload_info['upload_id']
    chunk_size = upload_info['chunk_size']
    total = os.path.getsize(filepath)

    # Step 2: 初始化
    session.post(
        f'https://{endpoint}{upos_path}',
        params={'uploads': '', 'output': 'json'},
        headers={'X-Upos-Auth': auth},
    )

    # Step 3: 分片上传
    with open(filepath, 'rb') as f:
        chunks = math.ceil(total / chunk_size)
        for i, chunk in enumerate(iter(lambda: f.read(chunk_size), b'')):
            session.put(
                f'https://{endpoint}{upos_path}',
                params={
                    'partNumber': i + 1,
                    'uploadId': upload_id,
                    'chunk': i,
                    'chunks': chunks,
                    'size': len(chunk),
                    'start': i * chunk_size,
                    'end': i * chunk_size + len(chunk),
                    'total': total,
                },
                data=chunk,
                headers={'X-Upos-Auth': auth, 'Content-Type': 'application/octet-stream'},
            )
            print(f'[BiliUpload] Upload chunk {i+1}/{chunks} success')

    # Step 4: 合并
    session.post(
        f'https://{endpoint}{upos_path}',
        params={
            'output': 'json',
            'name': os.path.basename(filepath),
            'profile': 'ugcupos/st-android',
            'uploadId': upload_id,
            'biz_id': upload_info['biz_id'],
        },
        headers={'X-Upos-Auth': auth},
    )

Step 5:投稿提交

def submit_archive(biz_id, cover_url, title, desc, tag, tid=17):
    params = sign_app({
        'access_key': access_token,
        'videos': json.dumps([{
            'filename': filename_without_ext,
            'title': title,
            'cid': 0,
            'type': 'normal',
        }]),
        'cover': cover_url,
        'title': title,
        'desc': desc,
        'tag': tag,
        'tid': tid,
        'biz_id': biz_id,
    }, APP_KEY, APP_SECRET)

    r = session.post(
        'https://member.bilibili.com/x/vu/app/add',
        data=params
    )
    return r.json()  # {'code': 0, 'data': {'aid': ..., 'bvid': ...}}

关键 DEX 字符串

从 38 个 DEX 文件扫描中定位上传流程的日志字符串,帮助快速确认类名和方法:

[BiliUpload] Start to upload chunk=
[BiliUpload] Upload chunk chunkSize=
[BiliUpload] Upload all chunk success!!!
ugcupos/st-android
X-Upos-Auth

CDN 节点选择

preupload probe 返回多个 endpoint,选择延迟最低的节点:

upos-sz-upcdncos.bilivideo.com  (COS/腾讯云)
upos-sz-upcdnbos.bilivideo.com  (BOS/百度云)
upos-cs-upcdngcs.bilivideo.com  (GCS/谷歌云)
upos-sz-upcdnkodo.bilivideo.com (Kodo/七牛云)