前言
最近一段時間,我對B站的App接口進行了深入分析,特別是關注了認證機制和私信功能的實現。通過逆向工程和網絡抓包,發現了B站移動端API的底層工作原理,包括設備標識生成機制、認證流程和消息傳輸協議。本文將分享這些研究成果,希望能對API安全設計和防護提供一些思考。
B站接口的基本架構
B站的移動端API采用了gRPC作為主要通信協議,這與普通的REST API有較大區別。gRPC使用Protocol Buffers(protobuf)進行數據序列化,具有更高的效率和更嚴格的類型檢查。
gRPC請求結構
B站的API請求通常包含以下幾個關鍵部分:
[壓縮標志(1字節)][消息長度(4字節)][Protobuf消息體]
- 壓縮標志:通常是0(不壓縮)或1(gzip壓縮)
- 消息長度:大端序的4字節整數,表示消息體的長度
- 消息體:使用protobuf編碼的實際數據
例如,一個典型的請求可能是:
00 00 00 00 52 0A 2A 08 88 F3 EC 8A 01 10 01 18 E4 C7 A0 85 01 20 E9 8D FF E7 07 28 01 32 0F 7B 22 63 6F 6E 74 65 6E 74 22 3A 22 32 22 7D 80 01 01 2A 24 65 65 62 35 35 63 38 37 2D 66 32 36 34 2D 34 39 64 33 2D 61 61 39 62 2D 63 37 36 34 39 66 36 31 39 34 33 38
其中:
00
- 不使用壓縮00 00 00 52
- 消息長度為82字節- 剩余部分 - protobuf編碼的消息體
設備標識機制深度解析
BUVID生成機制
BUVID (Bilibili Unique Video ID) 是B站用來唯一標識設備的關鍵值,其生成過程如下:
def generate_buvid(device_id, buvid_type):"""生成BUVID (B站設備唯一標識)參數:device_id: 設備ID (根據buvid_type的不同可以是AndroidID、MAC等)buvid_type: BUVID類型 (XX代表Android ID, XY代表MAC地址)"""# 預處理device_idif buvid_type == BUVIDType.MAC:device_id = device_id.replace(":", "")# 計算device_id的MD5值并轉為大寫id_md5 = hashlib.md5(device_id.encode('utf-8')).hexdigest().upper()# ..............(敏感信息脫敏處理)# 最終BUVID: 類型前綴 + ID_E + MD5buvid = buvid_type + id_e + id_md5return buvid
BUVID的格式為:XX/XY
+ 3位特征碼
+ 32位MD5
,共37位字符。
設備指紋(fp_local)生成與BUVID的關系
設備指紋(fp_local)與BUVID密切相關,通常用于二次驗證設備身份。fp_local的生成依賴于BUVID,同時也考慮了設備型號和無線電版本信息:
def generate_fp(buvid, device_model, device_radio):"""根據BUVID生成設備指紋"""# 1. 構建基礎字符串并計算MD5base_str = buvid + device_model + device_radiomd5_hex = hashlib.md5(base_str.encode('utf-8')).hexdigest()# 2. 添加時間戳time_str = datetime.now().strftime("%Y%m%d%H%M%S")# 3. 添加隨機字符串(16位十六進制)random_hex = ''.join(random.choice('0123456789abcdef') for _ in range(16))# 4. 合并所有部分fp_base = md5_hex + time_str + random_hex# 5. 計算校驗和,這里是關鍵部分。所以隨機生成并不能通過驗證calculate_checksum就不公布了checksum = calculate_checksum(fp_base)# 6. 返回完整指紋return fp_base + checksum
關鍵點:
- fp_local的首32位是由BUVID、設備型號和無線電版本共同決定的MD5值
- 接下來14位是生成時的時間戳
- 再加上16位隨機數據
- 最后2位是校驗和
BUVID與fp_local的關系驗證:
def verify_fp(buvid, fp, device_model, device_radio):"""驗證FP是否與BUVID匹配"""# 1. 提取FP中的MD5部分(前32個字符)fp_md5 = fp[:32]# 2. 計算預期的MD5base_str = buvid + device_model + device_radioexpected_md5 = hashlib.md5(base_str.encode('utf-8')).hexdigest()# 3. 驗證MD5部分是否匹配if fp_md5 != expected_md5:return False# 4. 驗證校驗和fp_base = fp[:-2]expected_checksum = calculate_checksum(fp_base)actual_checksum = fp[-2:]# 5. 返回校驗和是否匹配return expected_checksum == actual_checksum
這種設計確保了:
- 設備更換后無法保持相同的指紋
- 服務器可以驗證設備指紋的真實性
- 可以檢測設備信息的篡改
Ticket認證機制分析
JWT格式的Ticket結構
Ticket采用JWT(JSON Web Token)格式,由三部分組成:Header、Payload和Signature。
典型的B站Ticket如下:
eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDQ2NTYxNjcsImlhdCI6MTc0NDYyNzA2NywiYnV2aWQiOiJYVUY4MjRFRkQ3NjUyNTE5NzhCOEIyNzlEMDYyODNFQkQ1OTZFIn0.flJFBus6TltpIlD_byS2bM0kzXyQe0o-5ndOnrtuTOc
解析后:
- Header:
{"alg":"HS256","kid":"s03","typ":"JWT"}
- Payload:
{"exp":1744656167,"iat":1744627067,"buvid":"XUF824EFD765251978B8B279D06283EBD596E"}
- Signature: HMAC-SHA256簽名
Ticket獲取請求的構造
獲取Ticket的請求較為復雜,涉及多層嵌套的protobuf消息和簽名計算:
def create_ticket_request(self, device_bin):"""創建Ticket請求數據"""# 創建請求數據request_data = bytearray()# 創建x-fingerprint的值fingerprint_value = self.create_fingerprint_value()# 添加字段1: context (x-fingerprint)# 添加字段1: context (x-exbadbasket)# 將兩個context條目添加到請求中# 添加字段2: key_id = "ec01"# 添加字段3: sign# 壓縮數據compressed_data = gzip.compress(bytes(request_data))# 構造gRPC消息# ----------- 此處省略return bytes(grpc_message)
簽名計算的安全性
簽名計算是認證系統的核心安全機制:
def calculate_app_sign(self, device_bin, fingerprint_value, exbadbasket_value=b''):"""計算應用簽名"""# 簽名密鑰key = b'E2lc5tgtl' # 注:此處為示例密鑰,非真實值# 將base64編碼的device_bin解碼device_bin_data = base64.b64decode(device_bin)# 創建HMAC對象h = hmac.new(key, digestmod=hashlib.sha256)# 按特定順序更新數據# 此處脫敏處理,省略# 獲取十六進制摘要return h.hexdigest()
Protobuf消息的構造與解析
protobuf字段類型
protobuf編碼中,每個字段都由標簽和值組成:
- 標簽:
(field_number << 3) | wire_type
- 值:根據wire_type決定編碼方式
常見的wire_type:
- 0: Varint (int32, int64, bool等)
- 1: 64-bit (fixed64, double等)
- 2: Length-delimited (string, bytes, embedded messages等)
- 5: 32-bit (fixed32, float等)
手動構造protobuf消息
沒有.proto定義文件時,我們需要手動構造protobuf消息,當然你也可以用現成的庫去處理:
def append_tag(self, buf, field_number, wire_type):"""添加字段標簽"""tag = (field_number << 3) | wire_typeself.append_varint(buf, tag)def append_varint(self, buf, value):"""添加varint類型的值"""while value >= 0x80:buf.append((value & 0x7f) | 0x80)value >>= 7buf.append(value & 0x7f)def append_string(self, buf, value):"""添加字符串類型字段"""encoded = value.encode('utf-8')self.append_varint(buf, len(encoded)) # 長度前綴buf.extend(encoded) # 字符串內容
解析protobuf消息
從二進制數據解析protobuf消息也很關鍵:
def parse_protobuf(data):"""解析protobuf格式數據"""result = {}try:offset = 0while offset < len(data):# 獲取字段編號和類型field_and_type = data[offset]field_num = field_and_type >> 3wire_type = field_and_type & 0x07offset += 1# 基于wire_type處理不同類型的數據if wire_type == 0: # Varintvalue = 0shift = 0while True:b = data[offset]offset += 1value |= ((b & 0x7f) << shift)if not (b & 0x80):breakshift += 7result[str(field_num)] = valueelif wire_type == 2: # Length-delimitedlength = 0shift = 0while True:b = data[offset]offset += 1length |= ((b & 0x7f) << shift)if not (b & 0x80):breakshift += 7# 嘗試解析為字符串、JSON或嵌套消息try:string_value = data[offset:offset+length].decode('utf-8')if string_value.startswith('{') and string_value.endswith('}'):try:json_value = json.loads(string_value)result[str(field_num)] = string_valueexcept:result[str(field_num)] = string_valueelse:result[str(field_num)] = string_valueexcept:# 嵌套消息或二進制數據nested_result = parse_protobuf(data[offset:offset+length])if nested_result:result[str(field_num)] = nested_resultelse:result[str(field_num)] = data[offset:offset+length].hex()offset += length# 處理其他wire_type...except Exception as e:print(f"Protobuf解析錯誤: {e}")return result
私信發送功能詳解
私信消息的protobuf結構
私信消息的protobuf結構如下:
消息 {field1: { // 內部消息field1: 接收者UID (VarInt)field2: 消息類型 (VarInt)field3: 序列號 (VarInt)field4: 設備標識 (VarInt)field5: 標志位 (VarInt)field6: 消息內容JSON (String)field16: 標志位 (VarInt)}field5: 消息ID (String)
}
構造和發送私信
def create_and_send_message(self, content, receiver_uid, message_id=None):"""構造并發送B站私信消息"""# 生成消息IDif message_id is None:message_id = str(uuid.uuid4())# 創建消息數據message_data = bytearray()# 添加內部消息inner_msg = bytearray()# 添加接收者UID# 添加消息類型# 添加序列號# 添加設備標識# 添加標志位# 添加消息內容(JSON格式)# 添加標志位self.append_tag(inner_msg, 16, 0)self.append_varint(inner_msg, 1)# 將內部消息添加到外層消息self.append_tag(message_data, 1, 2)self.append_varint(message_data, len(inner_msg))message_data.extend(inner_msg)# 添加消息IDself.append_tag(message_data, 5, 2)self.append_string(message_data, message_id)# 創建gRPC消息# 準備請求頭headers = {"APP-KEY": "android64","Content-Type": "application/grpc","User-Agent": "...","buvid": self.buvid,"x-bili-device-bin": self.device_bin(),"x-bili-fawkes-req-bin": self.fawkes_req_bin(),"x-bili-locale-bin": "...","x-bili-metadata-bin": self.generate_bili_metadata(),"authorization": f"identify_v1 {self.access_key}","x-bili-ticket": self.ticket# 其他必要的頭信息...}# 發送請求url = "https://app.bilibili.com/bilibili.im.interface.v1.ImInterface/SendMsg"response = requests.post(url, headers=headers, data=bytes(grpc_message))return response
解析Base64編碼的錯誤消息
B站的錯誤響應常常是Base64編碼的protobuf消息:
def parse_base64_protobuf(base64_str):"""解析Base64編碼的protobuf數據"""try:# 補全paddingpadding = len(base64_str) % 4if padding:base64_str += '=' * (4 - padding)# 解碼Base64decoded_data = base64.b64decode(base64_str)# 解析protobufreturn resultexcept Exception as e:print(f"解析Base64 protobuf數據失敗: {e}")return None
例如,錯誤消息CAISBTIxMDI2GlAKJ3R5cGUuZ29vZ2xlYXBpcy5jb20vYmlsaWJpbGkucnBjLlN0YXR1cxIlCKKkARIf5LiN6IO957uZ6Ieq5bex5Y+R6YCB5raI5oGv5ZOmfg
解析后可能包含"不允許給自己發送消息"的提示。
技術要點分析
1. 多層認證機制
B站采用了多層認證策略:
- BUVID:設備唯一標識
- fp_local:基于BUVID的設備指紋
- Ticket:JWT格式的臨時憑證
- access_key:長期授權令牌
這種多層機制增加了偽造身份的難度。
2. 簽名驗證
請求過程中,多處使用了HMAC簽名來確保數據完整性和來源可靠性。例如:
- Ticket請求中對device_bin和fingerprint_value進行簽名
- JWT格式的Ticket本身也包含簽名
3. 設備指紋算法
設備指紋(fp_local)的生成算法具有以下特點:
- 基于設備特征(BUVID、型號、無線電版本)
- 包含時間戳,允許追蹤指紋生成時間
- 添加校驗和,驗證數據完整性
- 部分隨機化,防止完全預測
4. protobuf格式的優勢利用
B站充分利用了protobuf的優勢:
- 高效的序列化/反序列化
- 嚴格的類型檢查
- 二進制格式難以直接分析和修改
- 版本兼容性好
5. 協議頭中各個值的算法
Ascii 類
user-agent
客戶端 UA, 必需.
device_model
設備 Model
device_build
設備 Build
app_ver
APP 版本號
mobi_app
APP 包類型
app_build
APP 版本號
app_build_inner
APP 版本號(內部)
x-bili-gaia-vtoken
暫時留空.
x-bili-aurora-eid
未登錄留空. 必需.
x-bili-mid
用戶 UID, 未登錄默認為 0. 必需.
x-bili-aurora-zone
留空. 必需.
x-bili-trace-id
如 16e903399574695df75be114ff63ac64:f75be114ff63ac64:0:0. 需要算法處理. 必需.
authorization
鑒權, 登錄時設定為 identify_v1 {access_key}, 未登錄時無需此項.
buvid
設備唯一標識, 有自己的一套算法.
bili-http-engine
cronet
te
trailers
Binary 類
x-bili-fawkes-req-bin
設備 Fawkes 信息,加密信息,必須
x-bili-metadata-bin
使用 Metadata 生成. 加密信息,必須
x-bili-device-bin
設備信息, 加密信息,必須
x-bili-network-bin
設備網絡信息, 加密信息,必須
x-bili-restriction-bin
限制信息, 加密信息,必須
x-bili-locale-bin
設備區域信息,加密信息,必須
x-bili-exps-bin
加密信息,必須
安全漏洞與防范建議
通過分析,我發現了幾個潛在的安全問題及其防范措施:
1. 設備模擬防范
問題:通過分析可以偽造設備信息,包括BUVID和指紋。
防范措施:
- 增加設備特征采集,如硬件序列號、系統指紋等
- 引入設備行為特征分析,識別異常行為模式
- 實施設備綁定策略,限制賬號可用設備數量
- 定期更新設備指紋算法,增加逆向難度
2. 簽名密鑰保護
問題:客戶端存儲的簽名密鑰可能被提取。
防范措施:
- 使用白盒加密技術保護客戶端密鑰
- 實施密鑰分散存儲和動態派生
- 定期輪換密鑰
- 服務端增加額外驗證,不完全依賴客戶端簽名
3. Protobuf結構保護
問題:通過逆向工程可以分析出protobuf結構。
防范措施:
- 混淆字段名和字段編號
- 定期更新協議結構
- 添加冗余或誘餌字段
- 對關鍵字段進行額外加密
4. JWT安全加固
問題:JWT可能面臨重放攻擊或篡改。
防范措施:
- 縮短Token有效期
- 實施Token綁定(將Token與特定會話或設備綁定)
- 使用更強的簽名算法(如ES256而非HS256)
- 服務端維護已吊銷Token列表
5. 請求頻率限制
問題:可能出現API濫用。
防范措施:
- 實施基于賬號和設備的請求頻率限制
- 為敏感操作增加驗證碼或其他人機驗證
- 監控異常請求模式并實施自動封禁
- 對同一設備的多賬號操作進行關聯分析
實踐經驗與優化方向
代碼優化
在研究過程中,我發現以下編碼實踐更有效:
# 更清晰的protobuf構造
def create_message(receiver_id, content):msg = bytearray()# 使用輔助函數增加可讀性add_varint_field(msg, 1, receiver_id)add_string_field(msg, 6, json.dumps({"content": content}))return msg
調試技巧
分析和調試protobuf消息:
def debug_protobuf(hex_data):"""將十六進制數據解析為人類可讀的形式"""data = binascii.unhexlify(hex_data.replace(" ", ""))result = parse_protobuf(data)# 美化輸出return json.dumps(result, indent=2, ensure_ascii=False)
結語
通過對B站App接口的深入分析,我們不僅理解了其身份驗證和消息發送機制,也掌握了protobuf消息的構造和解析技術。這些知識不僅適用于B站,也可以應用到其他使用類似技術棧的應用分析中。
對于API開發者,本文揭示了現代移動應用API安全設計的多層次防護策略,以及可能的改進方向。對于研究者,則提供了一個深入理解gRPC和protobuf在實際應用中如何使用的案例。
希望本文對你了解現代移動應用的API實現有所幫助。記住,技術研究應當遵守倫理和法律邊界,尊重平臺和用戶的權益。
本文內容僅供學習和研究使用,請勿用于任何非法或違反相關服務條款的目的。使用本文中的技術進行未授權訪問或攻擊行為可能違反《網絡安全法》等相關法律法規,責任自負,如有需要聯系作者可通過 dGcgQGludm9rZXlvdQ==
(base64decode后查看聯系方式) 聯系我。