脱敏说明:本文涉及的应用域名、认证 Token、设备标识符(IMEI、MAC 地址、Android ID 等)均已脱敏处理,以 xxx 或 [MASKED] 代替,不包含任何可追溯至真实设备或账号的信息。文中截图仅用于技术原理说明。
一、以歌曲搜索协议为例
在上一篇中,我们借助 Postern 全局代理成功让 Charles 捕获了目标音乐APP 的所有流量。本文以「歌曲搜索」接口为例,深入分析协议结构,梳理出需要逆向破解的关键参数。
1.1 Charles 抓包查看协议详情
打开 Charles,在目标音乐APP 中触发一次歌曲搜索操作,即可在 Charles 中看到对应的请求:
Charles 中歌曲搜索协议详情1.2 复制为 Curl 形式
Charles 支持将请求导出为 Curl 命令,便于在终端重现请求或作为代码编写的参照。以下是脱敏后的完整 Curl 请求:
curl \
-H 'Host: interface3.music.xxx.com' \
-H 'Cookie: EVNSM=1.0.0; NMCID=xxxxxx.1667355455436.01.4; versioncode=8008050; buildver=221010200836; resolution=2392x1440; deviceIdYD=[MASKED]; ntes_kaola_ad=1; mobilename=Nexus6P; __csrf=[MASKED]; osver=8.1.0; deviceIdZX=[MASKED]; os=android; channel=huawei1; MUSIC_A=[MASKED]; deviceId=[MASKED]; appver=8.8.50; NMDI=[MASKED]; NMTID=[MASKED]; packageType=release' \
-H 'user-agent: MusicApp/8.8.50 (Android 8.1.0; Nexus 6P)' \
-H 'cmpageid: SearchActivity' \
-H 'mconfig-info: {"IuRPVVmc3WWul9fT":{"version":"2893824","appver":"8.8.50"},"tPJJnts2H31BZXmp":{"version":"1153024","appver":"3.25.00"}}' \
-H 'x-mam-custommark: cronet' \
-H 'content-type: application/x-www-form-urlencoded' \
--data-binary "params=74A595527B7A1647174ADDB4F261E92FF2AFA5F42E4694960F9C5A3746841C9BA4DFA4F09AC5266109659469E031F06AE91ECFE7DB48EB4E9EFC1ECCD7562F7836FF6A0218FA861FA3B69A37A27D61C2D60AB011FF10E2DFD68262EE9744A31640FB1FC650D5679D5B2350FB9EC4B73249F33DAD34FA177056C73768AF0885D281..." \
--compressed \
'https://interface3.music.xxx.com/eapi/search/song/page'
注意:上述 Curl 中的 Cookie 已全部脱敏,params 值为实际加密密文的截断示例,不影响对协议结构的分析。
二、请求结构分析
完整的请求结构可归纳如下图:
请求结构与逆向工作分解图2.1 接口信息
| |
|---|
| POST |
| /eapi/search/song/page |
| interface3.music.xxx.com |
| application/x-www-form-urlencoded |
接口路径中的 /eapi/ 前缀是该应用 Android 客户端专用的 API 命名空间,区别于网页端使用的 /api/ 接口,通常具有更强的加密和鉴权机制。
2.2 请求体:加密参数 params
POST Body 中只有一个字段:
params=74A595527B7A1647174ADDB4F261E92FF2AFA5F42E4694960F9C5A3746841C9B...
params 的值是一串十六进制大写字符串,是整个搜索请求的核心加密载荷。其明文内容猜测为搜索关键词、分页信息、客户端版本等构成的 JSON 结构,经过某种对称加密算法(AES 或自定义 XOR)处理后转为十六进制表示。
后续逆向工作:需在 App 反编译代码中找到 params 的构造与加密流程,提取加密密钥(key/iv)及算法,才能自主构造任意搜索请求。
2.3 请求头分析
请求头中主要包含以下几类信息:
| | |
|---|
Host | | |
user-agent | | |
cmpageid | 当前页面标识(此处为 SearchActivity) | |
mconfig-info | | |
x-mam-custommark | | |
content-type | | |
mconfig-info 字段包含若干模块的版本号,推测用于服务端灰度下发配置,可直接沿用抓包中的值作为固定参数传入。
2.4 Cookie 参数逐项解析
Cookie 是本次协议中信息量最大、需要逆向工作最多的部分,共包含约 14 个字段。按性质分为四类:
类型一:固定参数(可直接硬编码)
| | |
|---|
EVNSM | 1.0.0 | |
versioncode | 8008050 | |
buildver | 221010200836 | |
appver | 8.8.50 | |
os | android | |
ntes_kaola_ad | 1 | |
packageType | release | |
channel | huawei1 | |
类型二:设备信息(依赖真实设备或可伪造)
| | |
|---|
resolution | 2392x1440 | |
mobilename | Nexus6P | |
osver | 8.1.0 | |
这三个字段均为纯设备信息,可以直接读取设备属性后填入,或伪造为任意合法的设备参数。
类型三:设备 ID(结构已知,部分 Native 生成)
**deviceId**(最关键的设备 ID 字段)
其值为 Base64 编码字符串,解码后的明文结构为多字段 Tab 分隔拼接:
<IMEI(15位)>\t<MAC地址>\t<android_id(16位hex)>\t<native_id(16位hex)>
| | |
|---|
| | 系统 API / TelephonyManager |
| 网卡物理地址,格式 xx:xx:xx:xx:xx:xx | |
| | Settings.Secure.ANDROID_ID |
| 由 App 的 Native 层 JNI 函数生成 | |
逆向结论:deviceId 的结构已基本清晰,native_id 对应的 Native 函数(位于 com.example.music.deviceid.factory.JNIFactory 中的 JNI 方法)经反编译代码追踪,可以传入空字符串或 null 而不影响请求被接受,后续如有需要可进一步分析其生成逻辑。
NMCID
格式为 {6位随机字符串}.{13位毫秒时间戳}.01.4,猜测是客户端会话 ID,其中随机字符串部分需进一步确认是否有特定字符集或校验规则。
deviceIdYD、deviceIdZX
均为设备 ID 的另一种形式,采用 Base64 或 URL 编码的 JSON 格式封装,具体的生成逻辑尚未确认,需后续逆向分析。
类型四:认证 Token(需逆向破解)
这四个字段是整个 Cookie 中防护强度最高的部分,直接关系到请求能否被服务端认证通过。如果构造爬虫时直接复用抓包获取的真实 Token,存在被风控检测到账号异常的风险;更稳健的方案是逆向分析其生成算法,使每次请求都使用动态生成的 Token。
三、响应数据
值得注意的是,服务端返回的响应体同样是加密的,并非明文 JSON。这意味着即便成功构造出合法的请求,还需要额外实现响应解密才能获取有效数据。
# 响应体示例(密文,非明文 JSON)
{
"data": "<encrypted_payload>..."
}
响应解密的逆向思路与 params 加密类似:在 App 代码中追踪 HTTP 响应的处理链路,找到解密函数,提取算法和密钥。
四、逆向工作总结
经过协议分析,完整实现该歌曲搜索接口的爬虫程序,需要解决以下三类逆向任务:
Task 1:请求参数 params 加密破解
- 目标:确定
params 由什么明文内容组成,经过什么算法、使用什么密钥加密 - 方向:在反编译代码中搜索
params 赋值点,逆向加密函数(可能涉及 AES-ECB/CBC 或自定义算法)
Task 2:Cookie 加密字段生成破解
需要破解的字段共 7 个,优先级建议如下:
deviceId(结构已知,仅需确认 native_id 是否可为空)deviceIdYD、deviceIdZX(Base64/JSON 封装的设备 ID,优先级次之)NMCID(格式规律明显,需确认随机字符串的生成规则)__csrf、MUSIC_A、NMDI、NMTID(核心认证字段,难度最高,最后攻克)
Task 3:响应数据解密
- 目标:解密服务端返回的加密响应,获取歌曲搜索结果的明文 JSON
- 方向:追踪响应处理回调,找到解密入口函数,分析算法与密钥
五、Python 模拟请求框架(待补全)
在逆向工作完成前,可先搭建请求框架,待各加密字段逆向完成后逐步填充:
import requests
import hashlib
import base64
import time
import random
import string
# --- 设备信息(可自定义) ---
DEVICE_INFO = {
"resolution": "2392x1440",
"mobilename": "Nexus6P",
"osver": "8.1.0",
"os": "android",
"channel": "huawei1",
"appver": "8.8.50",
"versioncode": "8008050",
"buildver": "221010200836",
}
defbuild_nmcid() -> str:
"""
构造 NMCID 字段:{6位随机字母数字}.{13位毫秒时间戳}.01.4
格式规律已从抓包中归纳,随机串的字符集待逆向确认
"""
random_part = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
timestamp_ms = int(time.time() * 1000)
returnf"{random_part}.{timestamp_ms}.01.4"
defbuild_device_id(imei: str, mac: str, android_id: str, native_id: str = "") -> str:
"""
构造 deviceId 字段(Base64 编码)
明文格式:{IMEI}\t{MAC}\t{android_id}\t{native_id}
native_id 可置为空字符串(经反编译代码验证)
"""
raw = f"{imei}\t{mac}\t{android_id}\t{native_id}"
return base64.b64encode(raw.encode()).decode()
defencrypt_params(payload: dict) -> str:
"""
TODO: 加密请求体 params 字段
待逆向分析后填充具体算法(AES or 自定义)
:param payload: 搜索参数明文 dict,如 {"s": "关键词", "offset": 0, "limit": 30}
:return: 加密后的十六进制字符串
"""
raise NotImplementedError("params 加密算法尚未逆向,待补全")
defbuild_cookie(device_id: str, nmcid: str) -> str:
"""
拼装 Cookie 字符串
TODO: __csrf / MUSIC_A / NMDI / NMTID 等认证字段待逆向破解后补全
"""
cookie_parts = [
"EVNSM=1.0.0",
f"NMCID={nmcid}",
f"versioncode={DEVICE_INFO['versioncode']}",
f"buildver={DEVICE_INFO['buildver']}",
f"resolution={DEVICE_INFO['resolution']}",
"ntes_kaola_ad=1",
f"mobilename={DEVICE_INFO['mobilename']}",
"__csrf=TODO_REVERSE", # 待逆向
f"osver={DEVICE_INFO['osver']}",
f"os={DEVICE_INFO['os']}",
f"channel={DEVICE_INFO['channel']}",
"MUSIC_A=TODO_REVERSE", # 待逆向
f"deviceId={device_id}",
f"appver={DEVICE_INFO['appver']}",
"NMDI=TODO_REVERSE", # 待逆向
"NMTID=TODO_REVERSE", # 待逆向
"packageType=release",
]
return"; ".join(cookie_parts)
defsearch_song(keyword: str, offset: int = 0, limit: int = 30) -> dict:
"""
歌曲搜索接口主函数
:param keyword: 搜索关键词
:param offset: 分页偏移
:param limit: 每页数量
:return: 解密后的响应 JSON(TODO: 响应解密待补全)
"""
# 构造 deviceId(示例值,实际需传入真实设备信息)
device_id = build_device_id(
imei="000000000000000",
mac="00:00:00:00:00:00",
android_id="0000000000000000",
)
nmcid = build_nmcid()
# 构造请求 payload(待加密)
payload = {
"s": keyword,
"type": 1, # 1 = 歌曲搜索
"offset": offset,
"limit": limit,
"total": True,
}
# TODO: 加密 params(逆向完成后替换)
# encrypted_params = encrypt_params(payload)
encrypted_params = "TODO_ENCRYPT"
url = "https://interface3.music.xxx.com/eapi/search/song/page"
headers = {
"Host": "interface3.music.xxx.com",
"user-agent": f"MusicApp/{DEVICE_INFO['appver']} (Android {DEVICE_INFO['osver']}; {DEVICE_INFO['mobilename']})",
"cmpageid": "SearchActivity",
"x-mam-custommark": "cronet",
"content-type": "application/x-www-form-urlencoded",
"Cookie": build_cookie(device_id, nmcid),
}
data = {"params": encrypted_params}
resp = requests.post(url, headers=headers, data=data)
resp.raise_for_status()
# TODO: 响应解密(逆向完成后替换)
# return decrypt_response(resp.content)
return resp.json() # 当前返回原始响应,待解密逻辑补全后修改
说明:上述框架已搭好整体结构,encrypt_params()、响应解密逻辑、以及 __csrf / MUSIC_A / NMDI / NMTID 等认证字段的生成函数,将在后续逆向分析完成后逐步补全。