- 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辅助函数 - 新增证据金标准 + 协议文档
187 lines
6.7 KiB
Markdown
187 lines
6.7 KiB
Markdown
# 虎牙 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 值。
|