虎牙App密码登录: 逆向WUP协议并实现纯Python登录编码器
- hook WUP序列化层抓取TLS加密前明文,确认密码登录走 servant=huyaudbwebui / func=hypasswordLogin (msgType=0x1001) - 逆向JCE RequestPacket帧格式(1015字节TAF二进制),定稿三层结构: WUP header(tag1-10) -> sBuffer Map -> _wup_data业务struct - 澄清两个疑点字节: 0x66/0x7d 是TAF字段头(非分隔符) - 实现 tools/huya_wup_encoder.py,逐字节复现金标准 - 实测纯Python动态构造登录成功(返回uid+token),TLS指纹不阻断 - userAction随机坐标/时间戳同样通过,内置make_user_action辅助函数 - 新增证据金标准 + 协议文档
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
# 虎牙 App 密码登录 — WUP 明文抓包实证(最终)
|
||||
|
||||
> 状态:**密码登录完整 WUP 请求已抓到(TLS 加密前明文)**
|
||||
> 证据:`/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/evidence/derived/huya-wup-full3.jsonl`
|
||||
> 方法:hook `createWupRequestData<AppLgnPasswordLoginReq>` onLeave 拿到序列化后的 WUP 字节
|
||||
|
||||
---
|
||||
|
||||
## 一、密码登录 WUP 请求(核心结论)
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| **servant** | `huyaudbwebui` |
|
||||
| **func** | `hypasswordLogin` |
|
||||
| **协议** | WUP / TAF(JCE) 二进制序列化 |
|
||||
| **msgType** | 4097 (0x1001) = `MsgLogin.mMsgId` |
|
||||
| **请求大小** | 1356 字节 |
|
||||
| **密码** | SHA1(明文) hex,无盐(`772ed992b0e161276f44ec63671e60155c506294`) |
|
||||
| **name** | `hy_300023887`(`hy_` + 虎牙号) |
|
||||
|
||||
---
|
||||
|
||||
## 二、WUP 帧完整结构(已 100% 逆向定稿)
|
||||
|
||||
> 金标准文件:`evidence/wup_passwordlogin_taf.bin`(1015 字节 TAF 二进制)+ `evidence/wup_passwordlogin_base64.bin`(1356 字节 base64,即 HTTP body)
|
||||
> HTTP:`POST https://udblgn.huya.com/open/hy/passwordLogin`,`Content-Type: application/x-wup`
|
||||
|
||||
### 2.1 外层:WUP RequestPacket(标准 JCE/TAF 序列化)
|
||||
|
||||
```
|
||||
[0:4] uint32 BE 总长 = 1015
|
||||
|
||||
JCE RequestPacket 字段(head = (tag<<4)|type):
|
||||
tag1 iVersion = 3 (INT8: 10 03)
|
||||
tag2 cPacketType = 0 (ZERO: 2c)
|
||||
tag3 iMessageType = 0 (ZERO: 3c)
|
||||
tag4 iRequestId = session(3251699) (INT32: 42 00 31 9d f3)
|
||||
tag5 sServantName = "huyaudbwebui" (STR1: 56 0c + 12B)
|
||||
tag6 sFuncName = "hypasswordLogin" (STR1: 66 0f + 15B)
|
||||
tag7 sBuffer = MAP{...} (SIMPLE_LIST: 7d 00 01 03 c1 + 961B)
|
||||
tag8 iTimeout = 0 (ZERO: 8c)
|
||||
tag9 context = {} (空map) (MAP: 98 0c)
|
||||
tag10 status = {} (空map) (MAP: a8 0c)
|
||||
```
|
||||
|
||||
> 关键澄清:之前误认为 `'f'(0x66)` 和 `'}' (0x7d)` 是分隔符,实为 JCE 字段头:
|
||||
> - `0x56` = 字段5(sServantName)+STRING1
|
||||
> - `0x66` = 字段6(sFuncName)+STRING1
|
||||
> - `0x7d` = 字段7(sBuffer)+SIMPLE_LIST
|
||||
|
||||
### 2.2 中层:sBuffer(961 字节)是一个 MAP
|
||||
|
||||
```
|
||||
MAP (2条目):
|
||||
"_wup_data" → 915 字节 (内层登录请求 struct)
|
||||
"wupudbrequest_v0" → 5 字节 = 02 00 31 9d f3 (版本2 + session INT32)
|
||||
```
|
||||
|
||||
### 2.3 内层:_wup_data(915 字节)登录请求 struct(完整字段映射)
|
||||
|
||||
```
|
||||
_wup_data = STRUCT {
|
||||
field0 = STRUCT { # 元数据+风控
|
||||
tag0 = 0
|
||||
tag1 = "1.0" # 协议版本
|
||||
tag2 = 207B JSON (associationId/funcName/group/id/session/step/stillLogin/traceId/type/uid/userContext)
|
||||
tag3 = "5008" # bizAppid
|
||||
tag4 = 3 (INT8) # = type
|
||||
tag5 = 180B safedeviceid # 设备指纹(base64)
|
||||
tag6 = ""
|
||||
tag7 = ""
|
||||
tag8 = 210B userAction JSON # 风控行为轨迹
|
||||
tag9 = ""
|
||||
}
|
||||
field1 = STRUCT { # 设备基础信息
|
||||
tag0 = "ed0db8334cadd236c00cadf7e11ab5a5" # hdid (32 hex)
|
||||
tag1 = "13.4.22" # App版本
|
||||
tag2 = "1.0.80138" # SDK版本
|
||||
tag3 = ""
|
||||
tag4 = "127.0.0.1" # 占位
|
||||
tag5 = "xiaomi" # 厂商
|
||||
tag6 = ""
|
||||
}
|
||||
field2 = STRUCT { # 设备详情
|
||||
tag0 = 1 (INT8)
|
||||
tag1 = "M2102J2SC" # 型号 thyme
|
||||
tag2 = "02df398797432eadefcc12767119ad5e80999389" # 设备唯一ID(40 hex)
|
||||
tag3 = "android"
|
||||
tag4 = "M2102J2SC,30,11" # 型号,SDK,版本
|
||||
tag6 = "1080" # 屏宽 (tag5缺失)
|
||||
tag7 = "2120" # 屏高
|
||||
tag8 = "7c5387e0539c023c31c4ff0e807e7256117385ee" # 另一设备ID(40 hex)
|
||||
}
|
||||
field3 = STRUCT { # 登录凭证
|
||||
tag3 = "hy_300023887" # name (hy_虎牙号)
|
||||
tag4 = "772ed992b0e161276f44ec63671e60155c506294" # password = SHA1(明文)
|
||||
tag5 = LIST["5008"] # appid列表
|
||||
tag6 = 1 (INT8)
|
||||
tag7 = {} (空map)
|
||||
tag8 = [] (空simplelist)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 原始可读字符串(完整)
|
||||
|
||||
TAF 序列化,可读字符串全量提取如下(关键字段用换行标出):
|
||||
|
||||
```
|
||||
AAAD9xADLDxCADGd81YMaHV5YXVkYndlYnVpZg9oeXBhc3N3b3JkTG9naW59... ← WUP 头
|
||||
└─ "huyaudbwebui" ← servant
|
||||
└─ "hypasswordLogin" ← func
|
||||
|
||||
_wup_data 字段(关联元数据 JSON):
|
||||
{"associationId":8193,
|
||||
"funcName":"hypasswordLogin",
|
||||
"group":1,
|
||||
"id":4097, ← msgType
|
||||
"session":3251699,
|
||||
"step":1,
|
||||
"stillLogin":false,
|
||||
"traceId":"0b8f098ff64a5bdc-23473-94783833787595172451",
|
||||
"type":3,
|
||||
"uid":0,
|
||||
"userContext":""}
|
||||
|
||||
业务数据:
|
||||
5008 ← bizAppid (appId)
|
||||
PQwemAN9NHkZKoMqVT... ← safedeviceid (设备指纹, base64)
|
||||
|
||||
userAction JSON:
|
||||
{"curl":"登录页",
|
||||
"furl":"我的",
|
||||
"latitude":"-1.0",
|
||||
"longitude":"-1.0",
|
||||
"ssid":"",
|
||||
"user_action":[
|
||||
{"id":"24","time":"1787595171950","x":"277","y":"1057"},
|
||||
{"id":"11","time":"1787595172444","x":"296","y":"952"}]}
|
||||
|
||||
设备信息字段:
|
||||
ed0db8334cadd236c00cadf7e11ab5a5 ← hdid (硬件ID)
|
||||
13.4.22 ← App 版本
|
||||
1.0.801386 ← 某版本号
|
||||
127.0.0.1 ← 回环地址(占位)
|
||||
xiaomi ← 厂商
|
||||
M2102J2SC ← 设备型号 (thyme)
|
||||
M2102J2SC,30,11 ← 型号,SDK版本,版本
|
||||
android ← 系统
|
||||
1080 / 2120 ← 分辨率
|
||||
7c5387e0539c023c31c4ff0e807e7256117385ee ← 另一个设备ID
|
||||
|
||||
登录凭证:
|
||||
hy_300023887 ← name
|
||||
772ed992b0e161276f44ec63671e60155c506294 ← password (SHA1)
|
||||
|
||||
结尾:
|
||||
wupudbrequest_v0 ← WUP 请求类型标记
|
||||
AAFAgAxnfOMmAyoDA== ← base64 尾巴(签名/校验)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、WUP 请求的三层结构
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ 第1层: WUP/TAF 外层 │
|
||||
│ servant = "huyaudbwebui" │
|
||||
│ func = "hypasswordLogin" │
|
||||
│ _wup_data = { associationId, funcName, group, id, │
|
||||
│ session, step, stillLogin, traceId, │
|
||||
│ type, uid, userContext } │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ 第2层: 业务数据 (AppLgnPasswordLoginReq) │
|
||||
│ bizAppid = 5008 │
|
||||
│ name = "hy_300023887" │
|
||||
│ password = SHA1(明文) │
|
||||
│ userAction = 风控行为 JSON │
|
||||
│ safedeviceid / hdid / 设备信息 │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ 第3层: 网络传输 │
|
||||
│ TLS 1.3 → 116.162.209.x:443 (udblgn.huya.com) │
|
||||
│ (自定义 BoringSSL / hyssl) │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、网络层(实证)
|
||||
|
||||
- **密码登录走 TLS 443**(不是之前看到的 80 端口图片 CDN)
|
||||
- 目标 IP:`116.162.209.83-86`(`udblgn.huya.com`,kunluncan CDN)
|
||||
- TLS 层:libhycrypto.so = 定制 BoringSSL(hyssl),符号已 strip
|
||||
- 连接池:App 启动时预建立,登录时复用
|
||||
- **HTTP body = 原始 TAF 二进制(1015字节),NOT base64**(base64 是 hook 层返回值的表示形式,发送前已解码)
|
||||
|
||||
---
|
||||
|
||||
## 四·五、纯 Python 重放验证(✅ 成功)
|
||||
|
||||
**关键结论:纯 Python 可以复刻密码登录请求,服务端返回完整登录响应。**
|
||||
|
||||
```
|
||||
POST https://udblgn.huya.com/open/hy/passwordLogin
|
||||
Content-Type: application/x-wup
|
||||
Body: 原始 TAF 二进制 (1015字节, 非base64)
|
||||
```
|
||||
|
||||
重放金标准请求(`wup_passwordlogin_taf.bin`)后,服务端返回 **1514 字节登录响应**:
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| **uid** | `1199666914671` |
|
||||
| **name** | `hy_300023887` |
|
||||
| **手机号掩码** | `001******7524` |
|
||||
| **token** | 346 字节 base64(已存 `evidence/huya_token.txt`) |
|
||||
| **uc_states** | `1`(成功标记) |
|
||||
|
||||
这证明:
|
||||
1. ✅ 请求格式 100% 正确(服务端无 BAD_REQUEST)
|
||||
2. ✅ 设备指纹(safedeviceid/hdid)可复用(重放旧值仍通过)
|
||||
3. ✅ userAction 可复用(旧轨迹未被风控拒绝)
|
||||
4. ✅ TLS 指纹不阻断
|
||||
|
||||
> 注:此验证是「重放」,即复用已抓到的完整请求。要批量登录新账号,还需编码器动态构造新请求(替换 name/password/session/traceId/userAction)。
|
||||
|
||||
---
|
||||
|
||||
## 四·六、编码器 + 动态构造验证(✅ 完整成功)
|
||||
|
||||
**编码器已实现,逐字节复现金标准,且能动态构造全新请求。**
|
||||
|
||||
1. **`tools/huya_wup_encoder.py`** — 自检通过:用已知字段构造,输出与金标准逐字节一致(1015 字节)
|
||||
2. **动态构造测试** — 用新 session(3251799)、新 traceId 构造全新请求(1009 字节),发送后返回:
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| **uid** | `1199666914671` |
|
||||
| **name** | `hy_300023887` |
|
||||
| **手机号掩码** | `001******7524` |
|
||||
| **token** | 全新 token(`XAQBRJ3H...`) |
|
||||
|
||||
这证明 **纯 Python 可以完整复刻虎牙密码登录**:
|
||||
1. ✅ 帧格式(JCE/WUP)逆向定稿
|
||||
2. ✅ 编码器逐字节复现
|
||||
3. ✅ session/traceId 可自由生成(服务端接受新值)
|
||||
4. ✅ 设备指纹可复用
|
||||
5. ✅ 动态构造请求返回有效登录响应(uid + token)
|
||||
|
||||
### userAction 随机化(✅ 已实测)
|
||||
|
||||
`userAction` 只是请求里的一个普通 JSON 字符串字段,编码器已暴露 `user_action_json` 参数。实测**随机坐标 + 随机时间戳**同样能通过登录(返回 uid + token),服务端对该字段不做严格校验。
|
||||
|
||||
编码器提供辅助函数:
|
||||
- `make_user_action(now_ms=None)` — 生成随机 userAction JSON(坐标/时间戳/点击间隔随机)
|
||||
- `make_trace_id(pid=0)` — 生成 traceId
|
||||
|
||||
> 未实测定论:批量高频登录时,服务端是否按**账号/设备/IP 维度**限流或风控。这需要实测才能确认,与 userAction 无关。
|
||||
|
||||
---
|
||||
|
||||
## 五、对「纯 Python 复刻」的启示(已实证更新)
|
||||
|
||||
**TLS 指纹已实测:不阻断。** 普通 Python `urllib` 直连 `udblgn.huya.com` 能正常收到业务响应。
|
||||
|
||||
要脱离 App 用 Python 发密码登录请求,需要:
|
||||
|
||||
1. ✅ **密码**:`sha1(明文)` —— 已破解,纯 Python 可做
|
||||
2. ✅ **name**:`hy_虎牙号` —— 已知
|
||||
3. ✅ **servant/func**:`huyaudbwebui` / `hypasswordLogin` —— 已知
|
||||
4. ✅ **WUP 帧格式**:标准 JCE RequestPacket,已 100% 定稿(见第二节)
|
||||
5. ✅ **Content-Type**:`application/x-wup`,body = 原始 TAF 二进制
|
||||
6. ✅ **重放验证**:服务端返回 uid + token(纯 Python 可行)
|
||||
7. ✅ **TAF/JCE 编码器**:`tools/huya_wup_encoder.py`(自检通过,逐字节复现)
|
||||
8. ✅ **_wup_data 元数据**:`session`/`traceId` 可自由生成(服务端接受新值)
|
||||
9. ✅ **设备指纹(safedeviceid/hdid)**:可复用旧值(缓存一次即可)
|
||||
10. ✅ **userAction**:随机坐标/时间戳即可通过(编码器内置 `make_user_action()`)
|
||||
|
||||
> 唯一待实测项:批量高频登录时的账号/设备/IP 维度限流(与 userAction 无关)。
|
||||
|
||||
---
|
||||
|
||||
## 六、关联证据文件
|
||||
|
||||
- `huya-wup-plain.jsonl` — 第一次明文抓包(sendMsg JSON 层,305 事件)
|
||||
- `huya-wup-full3.jsonl` — 完整 WUP 序列化(含本次密码登录 1356 字节)
|
||||
- `wup_passwordlogin_taf.bin` — 金标准:1015 字节 TAF 二进制(= HTTP body,编码器自检依赖)
|
||||
- `wup_passwordlogin_base64.bin` — 金标准:1356 字节 base64(hook 返回值)
|
||||
- `wup_response_badreq.bin` — 服务端响应帧(383 字节,returnCode=104)
|
||||
- `tools/huya_wup_encoder.py` — WUP 帧编码器(自检通过,逐字节复现金标准,含 `make_user_action`/`make_trace_id`)
|
||||
- hook 脚本:`hook_huya_wup_full.js`
|
||||
- runner:`run_huya_netcapture.py`
|
||||
|
||||
> 含有效登录 token 的响应文件(`wup_response_login.bin`、`wup_response_new_session.bin`、`huya_token.txt`)已加入 `.gitignore`,不提交。
|
||||
|
||||
---
|
||||
|
||||
## 七、结论与下一步
|
||||
|
||||
已完成并验证:
|
||||
1. ✅ **TLS 指纹**:实测不阻断(Python urllib 正常收到业务响应)
|
||||
2. ✅ **TAF 编码器**:`tools/huya_wup_encoder.py`,逐字节复现金标准
|
||||
3. ✅ **纯 Python 登录**:重放 + 动态构造均返回 uid + token
|
||||
4. ✅ **userAction 随机化**:随机坐标/时间戳通过
|
||||
|
||||
待实测(唯一未定论项):
|
||||
1. **批量高频登录的限流/风控**:需用编码器连续发请求,观察账号/设备/IP 维度是否触发限制(错误码、频率阈值)
|
||||
2. **集成进 `core/huya/login.py`**:把 APP 密码登录(`/open/hy/passwordLogin`)作为 Web 登录被风控阻断时的替代路径
|
||||
@@ -0,0 +1,131 @@
|
||||
# 虎牙 App 密码登录协议 — Frida 抓包实证
|
||||
|
||||
> 状态:**密码登录完整链路已抓包实证**
|
||||
> 方法:spawn + libmsaoaidsec 绕过 + hook sendMsg/jbyteArray2str/str2jbyteArray(TLS 加密前的明文层)
|
||||
> 证据文件:`/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/evidence/derived/huya-wup-plain.jsonl`
|
||||
> Java 反编译:`/tmp/hy_jadx5/sources/com/huyaudbunify/`
|
||||
|
||||
---
|
||||
|
||||
## 一、密码登录请求(msgType = 0x1001 = 4097)
|
||||
|
||||
`MsgLogin.mMsgId = 4097 (0x1001)`,Java 层 `HuyaAuth.login()` → `sendMessage(4097, json)` → native `sendMsg(JNIEnv, jclass, jlong, jbyteArray)`。
|
||||
|
||||
**请求 JSON**(TLS 加密前明文,抓包实证):
|
||||
```json
|
||||
{
|
||||
"bizAppids": [],
|
||||
"isAuthLogin": false,
|
||||
"lgnExtParam": {},
|
||||
"name": "hy_300023887",
|
||||
"password": "772ed992b0e161276f44ec63671e60155c506294",
|
||||
"userAction": "{\"curl\":\"登录页\",\"furl\":\"我的\",\"latitude\":\"-1.0\",\"longitude\":\"-1.0\",\"ssid\":\"\",\"user_action\":[{\"id\":\"24\",\"time\":\"1787593636012\",\"x\":\"253\",\"y\":\"1573\"},...]}"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 值/规则 | 说明 |
|
||||
|---|---|---|
|
||||
| `name` | `"hy_" + 虎牙号` | 虎牙号 = 纯数字账号 |
|
||||
| `password` | **标准 SHA1(明文密码) hex,无盐** | 40 字符小写 hex |
|
||||
| `isAuthLogin` | false | 普通密码登录 |
|
||||
| `bizAppids` | [] | 目标业务 appId 列表(空=主 app) |
|
||||
| `lgnExtParam` | {} | 扩展参数 |
|
||||
| `userAction` | 风控行为 JSON | 点击轨迹、时间戳、坐标、curl/furl、经纬度 |
|
||||
|
||||
**密码哈希**(用户确认 + 抓包实证):
|
||||
```
|
||||
password = sha1(明文密码).hexdigest()
|
||||
例如 sha1("某密码") = "772ed992b0e161276f44ec63671e60155c506294"
|
||||
```
|
||||
> 无盐、无 HMAC、无 RSA,就是裸 SHA1。
|
||||
|
||||
---
|
||||
|
||||
## 二、登录响应(msgType=0x1001 返回)
|
||||
|
||||
```json
|
||||
{
|
||||
"lastLgnTime": 1787593640035,
|
||||
"token": "9QDzAAEDAAAAb2W4URcBAAABAGNI4jSgAQQANTAwOCgAMDJkZjM5ODc5NzQzMmVhZGVmY2MxMjc2NzExOWFkNWU4MDk5OTM4OQQANTAwOKIAAhKJiMHWd/m1LAKSxxpg1cjDxJEIG+ooj8BGSC5NBXb23Trpcz85MOqpZxhLmSeQ9c2MGzBk0oU4iR2PR0Ak6/t50Ej9sdzHjDLa2wcDiRofNyEZzuU1KcIQqrGDwwwfouEwuagTPI0ZrP5vAFJe8uccW5BKRVoqDDsXc09nIW7HX5Yp+9cT0nr8t/8+/3IdTXhFOZYmDN6gHaQI+ToQivXIAAAAAA==",
|
||||
"tokenType": 2,
|
||||
"uid": 1199666914671
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `token` | Base64,内含 TAF 二进制(`9QDzAAED...` 头),是后续凭据登录/接口调用的 token |
|
||||
| `tokenType` | 2(密码登录产生的 token 类型) |
|
||||
| `uid` | 1199666914671(该账号 uid) |
|
||||
| `lastLgnTime` | 上次登录时间戳(ms) |
|
||||
|
||||
---
|
||||
|
||||
## 三、完整登录调用链(Java → native → 网络)
|
||||
|
||||
```
|
||||
Java: HuyaAuth.login(name, password, isAuthLogin, bizAppids, callback)
|
||||
└─ MsgLogin (mMsgId=4097=0x1001)
|
||||
└─ ReqLogin.toString() → JSON(含 SHA1 密码)
|
||||
└─ HuyaAuthMgr.sendMessage(4097, jsonBytes)
|
||||
└─ HuyaAuthCore.sendMsg(jlong, byte[]) [native JNI]
|
||||
└─ sendMsg(JNIEnv, jclass, jlong msgType, jbyteArray)
|
||||
└─ jbyteArray2str → 明文 JSON
|
||||
└─ UdbHandlerMgr.getPlatFormMsgHandler() 处理
|
||||
└─ TAF/WUP 包装(servant + func)
|
||||
└─ TLS (libhycrypto 自带 BoringSSL)
|
||||
└─ 目标: udblgn.huya.com / udb3lgn.huya.com (443)
|
||||
```
|
||||
|
||||
**关键符号地址**(libudbauthunify.so):
|
||||
| 函数 | 地址 | 作用 |
|
||||
|---|---|---|
|
||||
| `sendMsg` | 0x26244c | Java→native 消息入口(msgType + jbyteArray) |
|
||||
| `jbyteArray2str` | 0x262b00 | jbyteArray→std::string(请求明文) |
|
||||
| `str2jbyteArray` | 0x262be8 | std::string→jbyteArray(响应明文) |
|
||||
| `hyudb_otp_encrypt` | 0x32fa24 | OTP/dckey 加密(XXTEA+AES) |
|
||||
|
||||
---
|
||||
|
||||
## 四、msgType 观察(本次抓包 305 事件)
|
||||
|
||||
| msgType | 次数 | 含义 |
|
||||
|---|---|---|
|
||||
| `0xb000003` | 92 | getLoginData(读取登录态/token),高频轮询 |
|
||||
| `0xb000021` | 4 | setDeviceInfo(hdid + safedeviceid) |
|
||||
| **`0x1001` (4097)** | 1 | **密码登录**(MsgLogin) |
|
||||
| `0xb000001` | 1 | 登录后 getLoginData |
|
||||
| `0xb000024` | 1 | 登录后获取数据 |
|
||||
| `0xb000013` | 1 | getConfig(TAF 二进制,servant=mobileuif) |
|
||||
|
||||
---
|
||||
|
||||
## 五、与 OTP 加密链的关系
|
||||
|
||||
密码登录时序(抓包时间线):
|
||||
```
|
||||
1787593639831 sendMsg msgType=0x1001 ← 密码登录 JSON 提交
|
||||
1787593639988 crypto-otp b=2 ← 紧接着触发 OTP/dckey 加密(AESkeyMgr b=2)
|
||||
1787593639996 sendMsg msgType=0xb000001 ← 登录成功后的凭据流程
|
||||
```
|
||||
|
||||
即:**密码登录本身用 SHA1 密码 + WUP 明文 JSON;登录成功后,native 层用 OTP 加密链(XXTEA+AES)生成 dckey 凭据,供后续业务接口(token/cred)使用**。
|
||||
|
||||
之前破解的 OTP 加密链(`tools/huya_app_crypto.py`)对应的是**登录后的凭据续期/设备绑定**,不是密码本身。
|
||||
|
||||
---
|
||||
|
||||
## 六、servant / URL
|
||||
|
||||
- 密码登录 CGI:`/{servantName}/hypasswordLogin`
|
||||
- URL:`https://udblgn.huya.com/open/hy/passwordLogin`(国内版)
|
||||
- servant 名运行时传入,登录类走 `huyaudb`/`huyaudbwebui`
|
||||
- 设备信息:`getCgi()` → `/{servantName}/setDeviceInfo`(hdid + safedeviceid)
|
||||
|
||||
---
|
||||
|
||||
## 七、待确认项(剩余,影响较小)
|
||||
|
||||
1. **userAction 是否必填/可复用**:风控行为采集(点击轨迹),批量登录时能否固定/复用同一份,还是每次都要真实模拟。
|
||||
2. **登录接口是否校验 OTP dckey**:密码登录请求本身是否需要先拿到 dckey(本次抓包 OTP 在密码登录之后触发,说明密码登录可能不依赖 dckey)。
|
||||
3. **token 的 TAF 结构解析**:响应 token 的 Base64 内是 TAF 二进制(`9QDzAAED...`),需解析出 uid/appid/时间戳等字段。
|
||||
@@ -0,0 +1,194 @@
|
||||
# 虎牙 App 登录 — APK 静态逆向分析(libudbauthunify.so)
|
||||
|
||||
> 目标:`apks/huya-13.4.22-115315-live.apk`(158MB,versionCode=115315)
|
||||
> 分析对象:`lib/arm64-v8a/libudbauthunify.so`(4.7MB,UDB 认证统一库)
|
||||
> 方法:`nm -D` 导出符号 + `objdump` 反汇编 + `strings` 字符串提取(纯静态,无需 hook)
|
||||
> 结论:**App 登录 = WUP 长连接 + XXTEA/AES 双层加密**,全部加密函数与 servant 已定位。
|
||||
|
||||
---
|
||||
|
||||
## 一、关键结论(一句话)
|
||||
|
||||
虎牙 App 登录的**设备风控加密**由 `libudbauthunify.so` 实现,算法链是:
|
||||
|
||||
```
|
||||
明文 → cred_pack 打包 → XXTEA 加密 → cred_pack 拼装 → MD5 派生 key → AES-128-ECB 加密 → 密文
|
||||
```
|
||||
|
||||
`dckey` = **Login Credential(登录凭据)**,通过 `MsgGetCred` / `MsgRequestLgnCred` 获取,加密后经 `udbdf.huya.com/dckey/check` 上传。
|
||||
|
||||
---
|
||||
|
||||
## 二、关键 native 库定位
|
||||
|
||||
| so 库 | 大小 | 作用 |
|
||||
|---|---|---|
|
||||
| **`libudbauthunify.so`** | 4.7MB | **UDB 认证统一库(核心)**,含全部登录/加密逻辑 |
|
||||
| `libhycrypto.so` | 1.9MB | BoringSSL(AES/RSA 标准实现) |
|
||||
| `libhydeviceid.so` | 4.1MB | 设备 ID 生成 |
|
||||
| `libtscsdk.so` | 1.5MB | 数美(TrustKernel)设备指纹 SDK(对应 zztfly 请求) |
|
||||
| `libtea.so` | 409KB | XXTEA 算法 |
|
||||
| `libmtpencrypt.so` | 809KB | MTP 加密 |
|
||||
|
||||
---
|
||||
|
||||
## 三、加密体系(已 100% 破解,来自反汇编)
|
||||
|
||||
### 3.1 加密函数清单
|
||||
|
||||
| 函数 | 地址 | 说明 |
|
||||
|---|---|---|
|
||||
| `hyudb_crypt_util::xxtea_encrypt` | 0x32e83c | **XXTEA 加密**(第一层) |
|
||||
| `hyudb_crypt_util::xxtea_decrypt` | 0x32ece8 | XXTEA 解密 |
|
||||
| `hyudb_crypt_util::md5_char16` | 0x32e71c | **MD5 派生 16 字节 key** |
|
||||
| `hyudb_crypt_util::md5_char32` | 0x32e7ac | MD5 派生 32 字节 |
|
||||
| `UdbAESUtil::encrypt` | 0x250038 | **AES-128-ECB 加密**(第二层) |
|
||||
| `UdbAESUtil::decrypt` | 0x2501a4 | AES 解密 |
|
||||
| `AESkeyMgr::getkey` | 0x26871c | **AES key 缓存管理**(map<int,map<int,string>>) |
|
||||
| `hyudb_packet_util::cred_pack` | 0x45xxxx | **cred(dckey) 二进制打包** |
|
||||
| `hyudb_packet_util::cred_unpack` | — | cred 解包 |
|
||||
| `encode_xxtea` / `decode_xxtea` | 0x330058/0x3306cc | XXTEA 编解码封装 |
|
||||
|
||||
### 3.2 加密模式确认(反汇编证据)
|
||||
|
||||
**AES 是 ECB 模式 + ZeroPadding**,证据来自 `UdbAESUtil::encrypt`(0x250038)反汇编:
|
||||
|
||||
```asm
|
||||
; 计算长度 & 0xf,非 16 倍数则 push_back(0) 补齐(ZeroPadding)
|
||||
and w11, w11, #0xf
|
||||
...
|
||||
bl push_back(0) ; 补零
|
||||
; 每 16 字节独立调用 _encrypt(单块),无 IV 参与 → ECB 模式
|
||||
loop:
|
||||
bl UdbAESUtil::_encrypt(ptr16, out) ; 逐块 AES
|
||||
add x24, x24, #0x10 ; +16
|
||||
```
|
||||
|
||||
### 3.3 完整加密调用链(`hyudb_otp_encrypt` 0x32fa24 还原)
|
||||
|
||||
```
|
||||
hyudb_otp_encrypt(x0=key, w1=2, w2=len, x3..x5=字段, w6=4, x7=buf, out)
|
||||
├─ cred_pack(sp+0xb0, x7) # 打包第一个字段
|
||||
├─ xxtea_encrypt(sp+0xc8, sp+0xe0, x24) # XXTEA,key = x0 参数
|
||||
├─ cred_pack(sp+0x98, <header字段>) # 打包 header
|
||||
├─ cred_pack(sp+0x98, sp+0xc8) # 打包 XXTEA 密文
|
||||
├─ cred_pack(sp+0x98, x23) # 打包第5参数字段
|
||||
├─ AESkeyMgr::getkey(i, i) # 取 AES key 字符串
|
||||
├─ string::insert(...) # key 拼接
|
||||
├─ md5_char16(key_str) # MD5 派生 16 字节 key
|
||||
├─ UdbAESUtil::C1(md5key) # 用 MD5 结果构造 AES
|
||||
└─ UdbAESUtil::encrypt(packed, md5key) # AES-128-ECB 加密
|
||||
```
|
||||
|
||||
调用方:`BusinessCfg::getOtp(uid, otp, ...)`(0x268bbc~0x268ca0)→ `hyudb_otp_encrypt(...)`。
|
||||
|
||||
---
|
||||
|
||||
## 四、硬编码 key 候选(rodata 段)
|
||||
|
||||
| 字符串 | 长度 | 备注 |
|
||||
|---|---|---|
|
||||
| **`HuyaUdb1928374650qwertyuiop`** | 27B | **最强候选**("HuyaUdb" + 数字 + 字母键盘序) |
|
||||
| `275d0ff676c0d65114acdefbd2ad87a3` | 16B | 32 hex,正好 AES-128 key 长度 |
|
||||
| `0123456789ABCDEF` / `0123456789abcdef` | 16B | 常见默认 key |
|
||||
|
||||
> XXTEA key 与 AES key 源需 hook 最终确认(hook `xxtea_encrypt` 第3参、`md5_char16` 输入)。
|
||||
|
||||
---
|
||||
|
||||
## 五、dckey = Login Credential
|
||||
|
||||
符号证据链:
|
||||
|
||||
```
|
||||
MsgGetCred / MsgRequestLgnCred / MsgResponseLgnCred
|
||||
HandlerGetCred / HandlerRequestLoginCred / HandlerResponseLoginCred
|
||||
AutoCredLogin(自动凭据登录)
|
||||
BusinessCfg::getCred / getCredEx / setSafeDeviceId
|
||||
AUTO_CRED_LOGIN_TIMER
|
||||
```
|
||||
|
||||
即:**dckey 就是「登录凭据(credential)」**,App 启动后用设备指纹换取,登录时作为风控凭证提交。
|
||||
|
||||
---
|
||||
|
||||
## 六、完整登录 servant 清单(TAF/WUP 结构名)
|
||||
|
||||
从 so 的 rodata 提取全部 `wup.*` 结构名:
|
||||
|
||||
### 登录类(核心)
|
||||
| servant/结构 | 用途 |
|
||||
|---|---|
|
||||
| **`AppLgnPasswordLoginReq/Resp`** | **App 密码登录** |
|
||||
| **`AppLgnOtpLoginReq/Resp`** | **App 一键(OTP)登录** |
|
||||
| **`AppLgnCredentialLoginReq/Resp`** | **dckey 凭据登录** |
|
||||
| **`AppLgnSmscodeLoginReq/Resp`** | 短信验证码登录 |
|
||||
| `AppLgnMobileSendSmsReq/Resp` | 手机号发短信 |
|
||||
| `AppLgnSessionSendSmsReq/Resp` | 会话发短信 |
|
||||
| `AppLgnSecondAuthLoginReq/Resp` | 二次认证 |
|
||||
| `AppLgnAntiViolentLoginReq/Resp` | 反暴力登录 |
|
||||
| `AppLgnRefreshPicReq/Resp` | 刷新验证码图 |
|
||||
| `AppLgnThirdLoginReq/Resp` | 第三方登录 |
|
||||
|
||||
### 密码/注册类
|
||||
```
|
||||
AppPasswordLoginReq/Resp # 密码登录(另一组)
|
||||
AppCredentialLoginReq # 凭据登录
|
||||
AppAnonymousLoginReq/Resp # 匿名登录
|
||||
AppMobileRegisterReq # 手机注册
|
||||
AppRegSendSms / AppRegCheckMobile / AppRegCheckUser
|
||||
AppRegSmscodeRegister / AppRegSmscodeModifyPassword / AppRegSmscodeResetPassword
|
||||
AppRegTokenRegister / AppRegTokenModifyPassword / AppRegTokenResetPassword
|
||||
AppRegUidVerifySmscode
|
||||
AppBindLoginMobile / AppBindNewMobile(绑定手机)
|
||||
AppBypassRouteReq/Resp # 旁路路由
|
||||
```
|
||||
|
||||
### servant 名(TAF 服务名)
|
||||
```
|
||||
huyaudb # UDB 主服务
|
||||
huyaudbwebui # 抓包里 wsapi.huya.com 的 servant
|
||||
huyaudbnimoui # NIM UI 服务
|
||||
passport # 护照服务
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、其他关键 token/字段
|
||||
|
||||
```
|
||||
violentToken / sessionToken # 反暴力 token / 会话 token
|
||||
enpack_header / depack_header # header 打包/解包(含 NEWKEY)
|
||||
key_version[%d] / key_id[%d] # key 版本/ID 管理
|
||||
lgnExtParam / lgnProtect # 登录扩展参数 / 登录保护
|
||||
loginAppId / loginAppName / loginDeviceName / loginStrategy / loginType
|
||||
```
|
||||
|
||||
`enpack_header` + `NEWKEY` 解释了抓包里 dckey body 的 **100 字节固定头**:那是打包的 header(key 版本、key_id、固定字段),加密只作用于变长数据体。
|
||||
|
||||
---
|
||||
|
||||
## 八、给 hook 的精确落点(静态分析直接给出)
|
||||
|
||||
| 目标函数 | hook 什么 | 得到什么 |
|
||||
|---|---|---|
|
||||
| `hyudb_crypt_util::xxtea_encrypt` | 第3参(XXTEA key) + 第2参(明文) + 返回(密文) | **XXTEA key + 明文结构** |
|
||||
| `hyudb_crypt_util::md5_char16` | 输入 string + 输出 16B | **AES key 源**(MD5 前明文) |
|
||||
| `AESkeyMgr::getkey` | 返回值 string | **AES key 字符串** |
|
||||
| `UdbAESUtil::encrypt` | 入参(明文) + 返回(密文) | 验证 AES 模式/填充 |
|
||||
| `hyudb_packet_util::cred_pack` | 各字段 | **dckey 二进制结构** |
|
||||
| `BusinessCfg::getOtp` | 入参 | OTP 生成逻辑 |
|
||||
| `MsgRequestLgnCred` 序列化前 | WUP 结构 | **登录凭据请求明文** |
|
||||
| OkHttp `CertificatePinner.check` | — | SSL unpin,解 cdn.wup 长连接 |
|
||||
|
||||
---
|
||||
|
||||
## 九、抓包 ↔ so 对应关系(验证闭环)
|
||||
|
||||
| 抓包现象 | so 证据 |
|
||||
|---|---|
|
||||
| dckey body 前 100B 固定 | `enpack_header`/`cred_pack` 打包的固定 header |
|
||||
| dckey body 后段变长 | XXTEA+AES 加密的数据体 |
|
||||
| dfpReport t2 魔数 `571882cf664bb39401ee` | cred_pack 的固定标识 |
|
||||
| selectOperator.t5 / dfpReport.t2 同前缀 13B | 同一固定 header(非 RSA,是打包结构头) |
|
||||
| 登录走 cdn.wup.huya.com 抓不到 | App 登录 servant 走 WUP 长连接 + SSL Pinning |
|
||||
@@ -0,0 +1,186 @@
|
||||
# 虎牙 App 登录 — 加密算法动态验证(Frida 最终结论)
|
||||
|
||||
> 状态:**算法链已 100% 破解并双向验证;arg7/appKey/XXTEA key 源已确认**
|
||||
> 实现:`tools/huya_app_crypto.py`(自检通过)
|
||||
> 方法:Frida spawn + libmsaoaidsec 绕过 + hook libudbauthunify.so 导出符号
|
||||
> 样本:com.duowan.kiwi 13.4.22(versionCode 115315)
|
||||
|
||||
---
|
||||
|
||||
## 一、最终算法链(一句话)
|
||||
|
||||
```
|
||||
OTP 加密 = AES-128-ECB( cred_pack([0x02][xxtea密文][设备指纹]) , key )
|
||||
其中 key = md5("865a4924a40897ac1fcfe6b4c2cbb0e3" + AESKEYMGR_KEYS[b])[4:12].hex()
|
||||
xxtea密文 = XXTEA( arg7(8字节) + 4字节长度 , key=md5(uid)[:16]hex )
|
||||
```
|
||||
|
||||
两层加密,均已用 hook 到的「明文↔密文」样本双向验证通过。
|
||||
|
||||
---
|
||||
|
||||
## 二、AES 层(已验证)
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 模式 | AES-128-ECB + ZeroPadding |
|
||||
| key 派生 | `md5(otp_input).digest()[4:12].hex()` → 16 个 ASCII hex 字符 |
|
||||
| otp_input | `sessHex32 + AESKEYMGR_KEYS[b]` |
|
||||
|
||||
验证样本:
|
||||
```
|
||||
otp_input = "865a4924a40897ac1fcfe6b4c2cbb0e3" + "MKDKeridjing7avnsasdSDHI"
|
||||
aes_key = "312334e88d8c35cb"
|
||||
明文 131 字节 → 补 13 字节零 → 144 字节密文,加密/解密双向一致。
|
||||
```
|
||||
|
||||
> 关键坑:`md5_char16` 内部是 `HuyaMd5::toString16()`,输出的是 digest **第 5~12 字节**(跳过前 4 字节)转 hex,不是前 16 字符,也不是 16 字节二进制。
|
||||
|
||||
---
|
||||
|
||||
## 三、XXTEA 层(已验证)
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 算法 | 标准 XXTEA(DELTA=0x9e3779b9,rounds=6+52/n) |
|
||||
| key | `md5(uid字符串).hexdigest()[:16]` 的 16 个 ASCII 字符(4 个小端 u32) |
|
||||
| 输入 | `arg7(8字节小端) + struct.pack("<I", len(arg7))`(追加原始长度) |
|
||||
|
||||
验证样本:
|
||||
```
|
||||
uid = "1471238907296"
|
||||
xxtea_key = "4d20d5c7222d5858" (md5(uid) 前 16 hex 字符)
|
||||
输入 = 0000ca5ead34a001 + 08000000 (arg7=0x01a034ad5eca0000, 长度8)
|
||||
输出 = d24e6493d0ee53c395790d67
|
||||
```
|
||||
|
||||
> 关键坑:XXTEA key 是 md5 hex 字符串**前 16 个 ASCII 字符**,不是 16 字节 digest。反汇编证据:`ldr q0` 只读 16 字节,无 hex 解码。
|
||||
> padding:不是简单补 0,而是「补 0 到 4 对齐 + 追加 4 字节原始长度」。
|
||||
|
||||
---
|
||||
|
||||
## 四、AESkeyMgr 硬编码 key 表(rodata)
|
||||
|
||||
`AESkeyMgr::getkey(a=1, b)` 返回的 key,b=2..5 已 hook 动态确认:
|
||||
|
||||
| b | key | 确认方式 |
|
||||
|---|---|---|
|
||||
| 1 | `owNMiaCgcHmqoTr3iRamFuHj` | rodata 推断 |
|
||||
| **2** | `MKDKeridjing7avnsasdSDHI` | ✅ hook |
|
||||
| **3** | `nskdI7MDGKSDJsnadjdoonvs` | ✅ hook |
|
||||
| **4** | `whdFNGFdfJHMJbdfg6jhdsFG` | ✅ hook |
|
||||
| **5** | `SHBfgytjtoikooruhogji7ER` | ✅ hook |
|
||||
| 6 | `KNSDNjfohweeromnmkladj3g` | rodata 推断 |
|
||||
| 7 | `KJNzmZaxUd9kGLoAbtefNzCv` | rodata 推断 |
|
||||
| 8 | `YnzbiMPNXaGuxxuoNRd9Lzyj` | rodata 推断 |
|
||||
|
||||
`sessHex32 = "865a4924a40897ac1fcfe6b4c2cbb0e3"` 固定(多次 hook 一致,非 so 内字符串,运行时生成)。
|
||||
|
||||
---
|
||||
|
||||
## 五、OTP 数据结构(cred_pack)
|
||||
|
||||
`cred_pack` 打包格式(反汇编 `push_varstr`/`cred_packlsE*` 确认):
|
||||
|
||||
| 类型 | 编码 |
|
||||
|---|---|
|
||||
| uchar | 1 字节 |
|
||||
| ulong | 8 字节(小端) |
|
||||
| string | `[u16_le 长度][数据]` |
|
||||
|
||||
`hyudb_otp_encrypt` 输出的 AES 明文结构:
|
||||
```
|
||||
[0x02] # 固定 header(arg1=2)
|
||||
[u16_le len][xxtea密文] # arg7 加密结果(8字节→12字节)
|
||||
[u16_le len][设备指纹] # arg5(114字节左右)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、函数参数映射(hook 实测)
|
||||
|
||||
`hyudb_otp_encrypt(x0..x7, 栈上out)`:
|
||||
|
||||
| 参数 | 含义 | 实测值 |
|
||||
|---|---|---|
|
||||
| x0 (string) | XXTEA key 源 = **uid 字符串** | "1471238907296" |
|
||||
| x1 (uchar) | 固定 header | 2 |
|
||||
| x2 (uchar) | **AESkeyMgr b 索引** | 2/3/4/5 |
|
||||
| x3 (string) | 空 | "" |
|
||||
| x4 (string) | **sessHex32** | "865a4924a40897ac1fcfe6b4c2cbb0e3" |
|
||||
| x5 (string) | **设备指纹**(二进制) | 0a80... |
|
||||
| x6 (uchar) | 固定 | 4 |
|
||||
| x7 (ulong) | **XXTEA 输入值** | 0x01a034ad5eca0000 |
|
||||
|
||||
调用方:`BusinessCfg::getOtp(uid, ...)` → `hyudb_otp_encrypt`。
|
||||
|
||||
---
|
||||
|
||||
## 七、Hook 落点(已验证可用)
|
||||
|
||||
| 函数 | 符号 needle | 地址偏移 |
|
||||
|---|---|---|
|
||||
| OTP 加密 | `hyudb_otp_encrypt` | 0x32fa24 |
|
||||
| XXTEA 包装 | `crypt_util13xxtea_encrypt` | 0x32e83c |
|
||||
| XXTEA 核心 | `hyudbxxt13xxtea_encryptEPjj` | 0x252700 |
|
||||
| MD5 派生 | `md5_char16` | 0x32e71c |
|
||||
| AES 加密 | `UdbAESUtil7encrypt` | 0x250038 |
|
||||
| AES key 管理 | `AESkeyMgr6getkey` | 0x26871c |
|
||||
| OTP 入口 | `BusinessCfg6getOtp` | 0x26916c |
|
||||
|
||||
Hook 脚本:`/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/evidence/scripts/hook_huya_crypto.js`
|
||||
Runner:`/Users/yml/codes/Reverse-Engineering-Agent-Universal-v3.0/scripts/run_huya_crypto.py`
|
||||
|
||||
---
|
||||
|
||||
## 八、sessHex32 = appKey(已确认)
|
||||
|
||||
`sessHex32 = "865a4924a40897ac1fcfe6b4c2cbb0e3"` 就是 **appId=5008 的 appKey**。
|
||||
|
||||
验证(appSign 算法 `md5(f"{appId}{versionCode}{appKey}")[:8]`):
|
||||
```
|
||||
md5("5008" + "115315" + "865a4924a40897ac1fcfe6b4c2cbb0e3")[:8] = "60576904" ✓
|
||||
```
|
||||
与已知 appSign(5008) 完全一致。该值不在 so/dex 硬编码,是运行时由上层传入(App 密钥)。
|
||||
|
||||
---
|
||||
|
||||
## 九、arg7 = 时间戳 << 16(已确认)
|
||||
|
||||
`arg7` 是 XXTEA 的输入(8 字节 ulong),规律:
|
||||
```
|
||||
arg7 = System.currentTimeMillis() << 16 # 低 16 位恒为 0
|
||||
```
|
||||
Frida 实测 5 个样本,`arg7 >> 16` 与事件时间戳差值 <600ms(hook 传播延迟)。
|
||||
|
||||
---
|
||||
|
||||
## 十、XXTEA key 源 = uid 字符串(已确认)
|
||||
|
||||
`hyudb_otp_encrypt` 的 x0(XXTEA key 源)= **uid 十进制字符串**:
|
||||
- 登录后:`"1471238907296"`(用户 uid)→ `md5(uid)[:16]hex` 作 XXTEA key
|
||||
- 匿名(未登录):空字符串 → `md5("")[:16]hex`
|
||||
|
||||
---
|
||||
|
||||
## 十一、设备指纹(arg5)= 数美 SDK 黑盒(需缓存/设备获取)
|
||||
|
||||
设备指纹是数美(TrustKernel)SDK 生成的黑盒二进制(非标准 JCE),同一设备跨调用稳定:
|
||||
|
||||
| b(OTP 类型) | 指纹开头 | 长度 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 2, 4 | `0a80ee5642...` | 114B | 缓存同值(设备指纹 A) |
|
||||
| 3 | 空 | 0 | 无设备指纹 |
|
||||
| 5 | `0a005e3748...` | 114B | 另一指纹类型(设备指纹 B) |
|
||||
|
||||
**无法在纯 Python 复刻**(依赖设备硬件信息 + 数美算法)。两种可行方案:
|
||||
1. **缓存**:对同一设备,hook 一次拿到指纹后持久化复用;
|
||||
2. **设备获取**:在设备上通过 hook `createWupDeviceInfo` / 数美 SDK 实时生成。
|
||||
|
||||
---
|
||||
|
||||
## 十二、待确认项(剩余)
|
||||
|
||||
1. **b=1,6,7,8 的 key 映射**:rodata 推断,需 hook 对应场景确认(影响较小,登录主流程走 b=2)。
|
||||
2. **设备指纹的稳定周期**:同一设备指纹是否长期不变,还是含时间戳会刷新(跨调用已稳定,跨天待验证)。
|
||||
3. **App 登录主流程走哪个 b**:观察到的 b=2/3/4/5 是启动期匿名 getOtp 的不同类型,真正的密码登录提交需在登录动作时确认 b 值。
|
||||
@@ -0,0 +1,212 @@
|
||||
# 虎牙 App 登录逆向笔记
|
||||
|
||||
> 抓包来源:`/Users/yml/Desktop/抓包/hy/8.23/`(登录-1.har / 登录-5.har)
|
||||
> 分析工具:`tools/taf_decode.py`(TAF/JCE 纯字节流解码)、`tools/hy_har_parse.py`(HAR 批量解析)
|
||||
> 状态:App 登录**前置风控链路**已完整解明;**登录提交(最后一跳)走 cdn.wup.huya.com 长连接 + SSL Pinning,尚未解出明文**。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论速览
|
||||
|
||||
1. **App 登录 ≠ Web 登录**,是两套完全不同的协议栈:
|
||||
- **Web 登录(旧,appId=5002)**:`udblgn.huya.com/web/v2/passwordLogin` + `udbrtt.huya.com/auth/pt/*` 滑块 + `df.huya.com/web/df/token|collect`。已在 `core/huya/login.py`、`e语言/huya/登录/虎牙操作.txt` 实现。
|
||||
- **App 登录(新,appId=5008)**:`wsapi.huya.com`(servant=`huyaudbwebui`)走 `getDfpConfig → selectOperator → dfpReport` 设备指纹,`udbdf.huya.com/dckey/check` 拿 dckey,运营商取号,最后登录提交进 `cdn.wup.huya.com` 长连接。
|
||||
|
||||
2. **Web 登录"风控加强"的本质**:Web 纯脚本无法伪造 App 侧的**设备态**(dckey + dfp 指纹 + appId=5008),被风控拦下。登录接口本身没变,是**前置设备校验变严**。
|
||||
|
||||
3. **抓包抓不到登录的原因**(两层锁):
|
||||
- TLS 层:`cdn.wup.huya.com` 与裸 IP(`211.91.243.102` 等)有 SSL Pinning,Reqable 中间人证书被拒,只留 CONNECT 隧道。
|
||||
- 应用层:`statwup.huya.com` 的部分 body 是 `877de5c4...` 纯二进制(应用层加密),即使破 TLS 也还要再解一层。
|
||||
|
||||
---
|
||||
|
||||
## 二、App 登录完整链路(已解明的普通 HTTP 部分)
|
||||
|
||||
按时间顺序(登录-5.har):
|
||||
|
||||
```
|
||||
① 数美安全 SDK 初始化(App 启动)
|
||||
cfgc.zztfly.com/v6/gcf · cdn-api-auth.zztfly.com/api/initSecCdn · upc.flysleep.cn/v6/gcl
|
||||
② 设备指纹配置
|
||||
wsapi.huya.com huyaudbwebui.getDfpConfig ← 只传 appId=5008,返回 80 字节加密配置
|
||||
③ 选运营商(一键登录前置)
|
||||
wsapi.huya.com huyaudbwebui.selectOperator ← 传设备信息 + RSA 加密段,返回运营商索引
|
||||
④ 设备指纹上报 ×4(循环)
|
||||
wsapi.huya.com huyaudbwebui.dfpReport ← 上报加密设备指纹,返回 32 位指纹 + action
|
||||
udbdf.huya.com/dckey/check ← 加密 body,返回 dckey
|
||||
⑤ 运营商取号(联通)
|
||||
auth.wosms.cn/dro/netm/v1.0/qc
|
||||
nisportal.10010.com:9001/api?appid=2074469246116
|
||||
⑥ 登录提交(❌ 未抓取)
|
||||
→ 进入 cdn.wup.huya.com / 裸 IP 长连接(被 SSL Pinning 挡住)
|
||||
```
|
||||
|
||||
`wup.huya.com` 上看到的 `mobileui.getDynamicConfigHotFixUpdateInfo` / `reportMobileUpdateResult` 只是 **App 热更配置**,与登录无关。`statwup.huya.com` 上 `metric.reportDetailV2` 是埋点。
|
||||
|
||||
---
|
||||
|
||||
## 三、各接口字段级结构
|
||||
|
||||
### 1. getDfpConfig(wsapi,servant=huyaudbwebui)
|
||||
|
||||
- **请求 tReq**:`{"t0": {"t0": "5008"}}` —— 只传 appId。
|
||||
- **响应 tRsp**:`{"t0": {"t0": {"5008": "<base64>"}}}` —— 按 appId 返回配置。
|
||||
- **base64 配置解码后 = 80 字节**(64+16 结构,疑似密钥材料):
|
||||
|
||||
```
|
||||
hex: 34b946b716f1481d8584980b79cf756b962e79c999d752016904c1fc07d689
|
||||
694a148ef872206826745498088e61be3f062f7d8d1a0276e29c0048062ac8e2
|
||||
b58fe0d1ee376c412472d8b9124d0bbf7d
|
||||
```
|
||||
|
||||
### 2. selectOperator(wsapi,servant=huyaudbwebui)
|
||||
|
||||
请求 key 是 `_wup_data`(注意不是 tReq),三层嵌套结构:
|
||||
|
||||
```
|
||||
_wup_data.t0.t0 = { # 会话上下文
|
||||
t0: 0,
|
||||
t1: "1.0", # 协议版本
|
||||
t2: "hyudb_1787583730402", # 会话 id = "hyudb_" + 毫秒时间戳
|
||||
t3: "5008", # appId
|
||||
t4: 0,
|
||||
t5: "PQwemAN9NHkZKoMqV...", # RSA 公钥加密段(与 dfpReport 响应 t2 同前缀)
|
||||
t6:"", t7:"", t8:"", t9:""
|
||||
}
|
||||
_wup_data.t1 = { # SDK 信息
|
||||
t0:"", t1:"13.4.22"(App版本), t2:"2.0.8"(SDK版本), t3:"",t4:"",t5:"",t6:""
|
||||
}
|
||||
_wup_data.t2 = { # 设备信息
|
||||
t0: 1,
|
||||
t1: "M2102J2SC", # 机型
|
||||
t2: "02df398797432eadefcc12767119ad5e80999389", # 40 hex = SHA1 设备指纹
|
||||
t3: "android",
|
||||
t4: "M2102J2SC,30,11", # 机型,Android版本,SDK版本
|
||||
t6:"", t7:""
|
||||
}
|
||||
_wup_data.t3 = [0, 1, 3] # 运营商列表
|
||||
```
|
||||
|
||||
- **响应**:`_wup_data.t0.t1 = 1`(选中运营商索引 1)。
|
||||
|
||||
### 3. dfpReport(wsapi,servant=huyaudbwebui)
|
||||
|
||||
- **请求 tReq**:
|
||||
|
||||
```
|
||||
{t0: {t0:"", t1:"android", t2:"<加密体>", t3:"", t4:12}}
|
||||
```
|
||||
|
||||
- `t2` 加密体结构:**10 字节固定魔数 + 变长加密数据**
|
||||
- 魔数(跨 6 次请求完全一致):`571882cf664bb39401ee`
|
||||
- 加密数据长度 3300~4100 字节(每次递增,说明是**累积式采集上报**,每次追加新采集数据)
|
||||
|
||||
- **响应 tRsp**:
|
||||
|
||||
```
|
||||
{t0: {
|
||||
t0: "10",
|
||||
t1: "865a4924a40897ac1fcfe6b4c2cbb045", # 32 hex = 稳定设备指纹(4 次响应一致)
|
||||
t2: "PQwemAN9NHkZKoMqV...", # base64 = RSA 公钥加密的 action(每次变)
|
||||
t3: 43200, # 过期时间(秒)=12h
|
||||
t4: "action",
|
||||
t5: "7c5387e0539c023c31c4ff0e807e7256117385ee" # 16 hex = 稳定 session hash(4 次一致)
|
||||
}}
|
||||
```
|
||||
|
||||
### 4. dckey/check(udbdf.huya.com,okhttp/3.14.9,application/octet-stream)
|
||||
|
||||
- **请求 body 结构**:**100 字节固定头 + 变长加密体**(总长 543B 或 547B,两种长度差 4 字节)
|
||||
- 100 字节固定头(跨 5 个样本、两个会话完全一致):
|
||||
|
||||
```
|
||||
32f1990eb8f46c503c3b5db81be58ae84dd3e961b991dd9e9c593aba8c78881c
|
||||
f1281c555198710a1009f1ec897c46be8b26d81e8eab98f27242de3444315cab8
|
||||
d617eae189d597e296099f0e5bc817a7554456c1af99515254ab7665a2a3f861a
|
||||
7d941b
|
||||
```
|
||||
|
||||
- 变长加密体(543/547B 减 100B = 443/447B)每次变化。
|
||||
- 同一长度下,末尾 32 字节可复现(同设备+同 session 重复调用时)。
|
||||
|
||||
- **响应**:`{"dckey":"AAAAAMC1eP4iV43WYoI57ZOu0...","status":"success"}`
|
||||
- dckey 固定前缀 `AAAAAMC1eP4iV43WYoI57ZOu0`(base64 前 16 字节),后续每次变。
|
||||
|
||||
### 5. 运营商取号(联通一键登录)
|
||||
|
||||
- `POST auth.wosms.cn/dro/netm/v1.0/qc`
|
||||
- `POST nisportal.10010.com:9001/api?appid=2074469246116&ret_url=<base64>`
|
||||
|
||||
---
|
||||
|
||||
## 四、加密体分析汇总
|
||||
|
||||
| 加密体 | 位置 | 结构 | 猜测算法 |
|
||||
|---|---|---|---|
|
||||
| dfpReport t2 | wsapi | 10B 魔数 `571882cf664bb39401ee` + 变长加密 | 自定义格式,魔数疑似协议版本/类型标识 |
|
||||
| dckey/check body | udbdf | 100B 固定头 + 443/447B 变长 | 固定头疑似 RSA/ECDH 公钥或设备态加密快照 |
|
||||
| getDfpConfig 配置 | wsapi 响应 | 80B = 64B+16B | 疑似 dfpReport 加密密钥 + IV/nonce |
|
||||
| selectOperator t5 / dfpReport t2(action) | wsapi | base64,同前缀 `PQwemAN9NHkZKoMqV` | **RSA 公钥加密**(同公钥 → 密文前缀相同) |
|
||||
| statwup #7 | statwup | 400B 纯二进制 | 应用层加密(session-key) |
|
||||
|
||||
**关键判断**:`selectOperator.t5` 和 `dfpReport 响应 t2` 前缀相同 → **同一个 RSA 公钥**加密,明文大概率是「设备信息 + 会话 key」的协商包。拿到这个公钥(APK 内嵌)就能解密。
|
||||
|
||||
---
|
||||
|
||||
## 五、稳定字段清单(可直接复用的常量)
|
||||
|
||||
| 字段 | 值 | 用途 |
|
||||
|---|---|---|
|
||||
| appId(App 场景) | `5008` | 替代 web 的 5002 |
|
||||
| App 版本 | `13.4.22` | versionCode=115315 |
|
||||
| SDK 版本 | `2.0.8` | selectOperator.t1.t2 |
|
||||
| dfpReport 魔数 | `571882cf664bb39401ee` | t2 加密体前缀 |
|
||||
| dckey 固定头 | 100B(见上) | dckey/check body 前缀 |
|
||||
| dckey 响应前缀 | `AAAAAMC1eP4iV43WYoI57ZOu0` | dckey 前 16B |
|
||||
| 设备指纹(selectOperator) | `02df398797432eadefcc12767119ad5e80999389` | 40 hex = SHA1 |
|
||||
| 设备指纹(dfpReport) | `865a4924a40897ac1fcfe6b4c2cbb045` | 32 hex,服务端下发 |
|
||||
| session hash(dfpReport) | `7c5387e0539c023c31c4ff0e807e7256117385ee` | t5 |
|
||||
| 会话 id 格式 | `hyudb_<毫秒时间戳>` | selectOperator.t2 |
|
||||
| appSign | `60576904`(md5 取前 8,见 login.py 注释) | App 场景签名 |
|
||||
|
||||
---
|
||||
|
||||
## 六、与现有 Web 登录(appId=5002)的差异
|
||||
|
||||
| 维度 | Web(5002) | App(5008) |
|
||||
|---|---|---|
|
||||
| 登录入口 | udblgn.huya.com/web/v2/passwordLogin | cdn.wup.huya.com 长连接 |
|
||||
| 设备指纹 | df.huya.com/web/df/token+collect | wsapi getDfpConfig+dfpReport + dckey/check |
|
||||
| 验证码 | udbrtt pt/get3+verify3(滑块/点选) | 运营商取号 + (未解明) |
|
||||
| 传输 | JSON over HTTP | TAF/WUP + 部分应用层加密 |
|
||||
|
||||
---
|
||||
|
||||
## 七、待破解项(按优先级)
|
||||
|
||||
1. **[最硬] dckey/check 的加密算法**:100B 固定头 + 变长体。需 hook APK 里 dckey 生成方法。
|
||||
2. **[核心] dfpReport t2 加密体**:魔数 + 累积式采集数据。需 hook dfp 采集模块。
|
||||
3. **[核心] 登录提交的 WUP servant/func + 参数**:被 SSL Pinning 挡住,需 Frida unpin 或 hook 登录方法。
|
||||
4. **[可解] RSA 公钥**:APK 内嵌,解出 selectOperator.t5 / dfpReport action 明文。
|
||||
5. **[可解] getDfpConfig 80B 配置**:确定是否 dfpReport 加密密钥。
|
||||
|
||||
---
|
||||
|
||||
## 八、Frida hook 点建议(供 hook 阶段参考)
|
||||
|
||||
1. **dckey 生成**:hook `udbdf` 请求发出前,dump 加密前明文。搜 `com/duowan/kiwi` 下 dckey / dcKey 相关类。
|
||||
2. **dfp 采集**:hook dfpReport 的 t2 字节流写入点(`writeTo`/`toByteArray`)。
|
||||
3. **登录提交**:hook `com.duowan.kiwi.base.login.UdbNetHelper`(appSign 已逆向自此类),以及登录 servant 的 `writeStruct("tReq", ...)` 前的参数。
|
||||
4. **SSL unpin**:hook OkHttp `CertificatePinner.check` + conscrypt `TrustManagerImpl.verifyChain`,让 Reqable 能解 cdn.wup.huya.com。
|
||||
|
||||
---
|
||||
|
||||
## 九、工具使用
|
||||
|
||||
```bash
|
||||
# 解码某个 HAR 里的所有 WUP(TAF) 请求/响应字段
|
||||
.venv/bin/python tools/hy_har_parse.py "/Users/yml/Desktop/抓包/hy/8.23/登录-5.har"
|
||||
|
||||
# 底层 TAF 解码器(可单独 import)
|
||||
from tools.taf_decode import Taf, decode_struct_bytes
|
||||
```
|
||||
Reference in New Issue
Block a user