1355 lines
42 KiB
Markdown
1355 lines
42 KiB
Markdown
# 抢购/抢兑逻辑静态分析记录
|
||
|
||
## 分析范围
|
||
|
||
- 项目目录:`/Users/yml/Downloads/多功能计算器2.7.unpacked.exe_export_for_ai`
|
||
- 样本形态:Windows EXE 的逆向导出结果,不是源码仓库。
|
||
- 主要依据:
|
||
- `strings.txt`
|
||
- `memory/00586000--005CD000.txt`
|
||
- `pointers.txt`
|
||
- `function_index.txt`
|
||
- `decompile/*.c`
|
||
- `disassembly/42FC3C.asm`
|
||
- 追加依据:
|
||
- `imgs/1.png`
|
||
- `imgs/2.png`
|
||
- `imgs/3.png`
|
||
- `/Users/yml/codes/douyu_login_py/core/huya/http_client.py`
|
||
- `/Users/yml/codes/douyu_login_py/core/huya/wss_client.py`
|
||
- `/Users/yml/codes/douyu_login_py/core/huya/wup_protocol.py`
|
||
- `/Users/yml/codes/douyu_login_py/core/huya/taf_protocol.py`
|
||
- `/Users/yml/codes/douyu_login_py/core/huya/activity_structs.py`
|
||
- 本记录为静态分析,没有运行程序,也没有打开浏览器。
|
||
|
||
## 总体结论
|
||
|
||
当前样本里确实存在“抢购/抢兑/自动兑换”相关逻辑。
|
||
|
||
这套逻辑不是普通计算器功能,而是一组虎牙活动自动监控与积分兑换流程:
|
||
|
||
1. 登录/连接后维护 WUP/WebSocketCommand 通道。
|
||
2. 周期性查询活动奖品列表。
|
||
3. 根据界面配置的目标奖品或自定义 pid 匹配奖品。
|
||
4. 命中后构造 `scoreExchangePrize(cookie, pid, sid)` 兑换请求。
|
||
5. 通过通用 HTTP 封装发送编码后的 WUP/WebSocketCommand 数据。
|
||
6. 根据返回的成功、频控、下架等状态继续重试、记录或重启监控。
|
||
|
||
## 核心接口
|
||
|
||
嵌入 JS/WUP 协议代码在 `strings.txt` 中,关键函数如下:
|
||
|
||
- `scoreExchangePrize(cookie, pid, sid)`
|
||
- 位置:`strings.txt:617`
|
||
- servant/func:`webActUI` / `scoreExchangePrize`
|
||
- 请求体:`ExchangePrizeReq`
|
||
- 关键字段:
|
||
- `pid`
|
||
- `sid`
|
||
- `userId.lUid`
|
||
- `userId.sCookie`
|
||
- `userId.sHuYaUA = "web&1.0&huya"`
|
||
|
||
- `getActPrizeList(cookie, sid)`
|
||
- 位置:`strings.txt:622`
|
||
- servant/func:`webActUI` / `getActPrizeList`
|
||
- 用于查询活动奖品列表。
|
||
|
||
- `getUserPrizeRecords(cookie, sid)`
|
||
- 位置:`strings.txt:622`
|
||
- servant/func:`webActUI` / `getUserPrizeRecords`
|
||
- 用于查询用户兑换/中奖记录。
|
||
|
||
- `getUserScore(cookie, sid)`
|
||
- 位置:`strings.txt:617`
|
||
- 用于查询剩余积分。
|
||
|
||
- `checkUserBindGameAccount(cookie, outerActId)`
|
||
- 位置:`strings.txt:625`
|
||
- 用于检查绑定角色/区服信息。
|
||
|
||
## WUP 封装逻辑
|
||
|
||
`_sendWup` 在 JS 中构造 `w.Wup`:
|
||
|
||
- 设置 servant:`setServant(t)`
|
||
- 设置 func:`setFunc(e)`
|
||
- 设置 request id:`setRequestId(r)`
|
||
- 写入请求结构:`writeStruct("tReq", i)`
|
||
- 外层再包成 `E.WebSocketCommand`
|
||
- `iCmdType = EWSCmd_WupReq`
|
||
- 最后编码成 hex/base64 样式字符串返回给 C 层发送。
|
||
|
||
响应解码通过 `decodeTaf`:
|
||
|
||
- 先解 `WebSocketCommand`
|
||
- 如果是 `EWSCmd_WupRsp`,再解 WUP
|
||
- 根据 `sFuncName` 查 `WupMapping`
|
||
- 映射包括:
|
||
- `scoreExchangePrize -> ExchangePrizeResp`
|
||
- `getActPrizeList -> GetActPrizeListResp`
|
||
- `getUserScore -> GetUserScoreResp`
|
||
- `checkUserBindGameAccount -> CheckUserBindGameAccountResp`
|
||
- `getUserPrizeRecords -> GetUserPrizeRecordsResp`
|
||
|
||
## 自动监控入口
|
||
|
||
### `sub_43597D`
|
||
|
||
位置:`decompile/43597D.c`
|
||
|
||
这是自动监控的外层调度器:
|
||
|
||
- 初始化账号/任务列表相关容器。
|
||
- 等待 `dword_69EE3C` 连接对象可用:
|
||
- `decompile/43597D.c:133`
|
||
- 打印/记录线程创建成功:
|
||
- `decompile/43597D.c:152`
|
||
- 设置自动监控标志:
|
||
- `dword_69EE38 = 1`
|
||
- `decompile/43597D.c:156`
|
||
- 遍历账号和活动/任务组合。
|
||
- 通过连接对象的 `+24` 方法投递任务:
|
||
- `decompile/43597D.c:221`
|
||
- 投递入口为:
|
||
- `sub_436737 -> sub_4363A4`
|
||
- 循环间隔走 `sub_4276C9`:
|
||
- `decompile/43597D.c:243`
|
||
- 结束时清理并关闭监控:
|
||
- `dword_69EE38 = 0`
|
||
- `decompile/43597D.c:248`
|
||
|
||
### `sub_436756`
|
||
|
||
位置:`decompile/436756.c`
|
||
|
||
这是继续监控线程入口:
|
||
|
||
```c
|
||
int sub_436756()
|
||
{
|
||
return sub_43597D();
|
||
}
|
||
```
|
||
|
||
汇编中可见在未命中奖品后会投递该入口继续监控:
|
||
|
||
- `disassembly/42FC3C.asm:2872`
|
||
|
||
## 请求构造函数对应关系
|
||
|
||
这些函数本质上都是从 ScriptControl/JScript 调用指定函数名,拿到编码后的 WUP/WebSocketCommand 数据,再转成二进制/缓冲区。
|
||
|
||
从 `pointers.txt` 可确认:
|
||
|
||
- `sub_435001`
|
||
- 字符串指针:`getActPrizeList`
|
||
- `pointers.txt:2376`
|
||
- 用于构造奖品列表查询请求。
|
||
|
||
- `sub_436610`
|
||
- 字符串指针:`scoreExchangePrize`
|
||
- `pointers.txt:2479`
|
||
- 用于构造兑换请求。
|
||
|
||
- `sub_4362B2`
|
||
- 字符串指针:`getUserPrizeRecords`
|
||
- `pointers.txt:2463`
|
||
- 用于构造兑换记录查询请求。
|
||
|
||
- `sub_42FA78`
|
||
- 多个账号/槽位回调中使用。
|
||
- 具体函数名需结合 `sub_4968A7(5)` 的运行时字符串表确认。
|
||
|
||
- `sub_42FB54`
|
||
- 多个账号/槽位回调中使用。
|
||
- 具体函数名需结合 `sub_4968A7(5)` 的运行时字符串表确认。
|
||
|
||
## 核心响应处理
|
||
|
||
### `sub_43E81F`
|
||
|
||
位置:`decompile/43E81F.c`
|
||
|
||
这是一个槽位回调模板,其他如 `436B5A.c`、`4394F1.c`、`43BE88.c`、`443B4D.c`、`458805.c`、`4604CA.c` 等逻辑高度同构,只是开头取不同账号/任务槽位。
|
||
|
||
关键解析字段:
|
||
|
||
- `status`
|
||
- `decompile/43E81F.c:213`
|
||
- `msg`
|
||
- 实际变量名由反编译器表现为 `off_58DDE9`
|
||
- `decompile/43E81F.c:221`
|
||
- `iRequestId`
|
||
- `decompile/43E81F.c:230`
|
||
- `score`
|
||
- `decompile/43E81F.c:239`
|
||
- `roleName`
|
||
- `decompile/43E81F.c:248`
|
||
- `areaName`
|
||
- `decompile/43E81F.c:260`
|
||
- `platName`
|
||
- `decompile/43E81F.c:272`
|
||
|
||
## iRequestId 分支
|
||
|
||
已确认的分支含义:
|
||
|
||
- `iRequestId == "1"`
|
||
- 奖品列表/兑换响应主分支。
|
||
- C 侧在该分支解析 `prizeList.value`,也处理 `scoreExchangePrize` 返回。
|
||
- 位置:`decompile/43E81F.c:330`
|
||
|
||
- `iRequestId == "2"`
|
||
- `getUserScore` 积分分支。
|
||
- 位置:`decompile/43E81F.c:288`
|
||
|
||
- `iRequestId == "6"`
|
||
- `getUserPrizeRecords` 兑换记录分支。
|
||
- 位置:`decompile/43E81F.c:286` 和 `decompile/43E81F.c:673`
|
||
|
||
- `iRequestId == "17"`
|
||
- `checkUserBindGameAccount` 绑定信息分支。
|
||
- 位置:`decompile/43E81F.c:307`
|
||
|
||
- `iRequestId == "-1"`
|
||
- `doLaunch/wsLaunch`,读取 `sGuid`。
|
||
- 位置:`decompile/43E81F.c:766`
|
||
|
||
## 奖品列表解析
|
||
|
||
`iRequestId == "1"` 时会解析:
|
||
|
||
- `prizeList.value`
|
||
- `decompile/43E81F.c:337`
|
||
- `prizeList.value[i].id`
|
||
- 汇编确认:`disassembly/42FC3C.asm:2051`
|
||
- `prizeList.value[i].name`
|
||
- 汇编确认:`disassembly/42FC3C.asm:2106`
|
||
- `prizeList.value[i].num`
|
||
- 汇编确认:`disassembly/42FC3C.asm:2161`
|
||
- `prizeList.value[i].percent`
|
||
- 汇编确认:`disassembly/42FC3C.asm:2226`
|
||
- `prizeList.value[i].newScore`
|
||
- 汇编确认:`disassembly/42FC3C.asm:2298`
|
||
|
||
同时有 `num * percent / 100` 一类计算,用于展示或估算剩余可兑数量:
|
||
|
||
- `disassembly/42FC3C.asm:2276`
|
||
- `disassembly/42FC3C.asm:2350`
|
||
|
||
## 命中策略
|
||
|
||
命中逻辑有两类信号:
|
||
|
||
1. 自定义 pid
|
||
- 常量:`自定义pid`
|
||
- 地址:`0x5A479F`
|
||
- 汇编引用:`disassembly/42FC3C.asm:1950`
|
||
|
||
2. 自动选择奖品
|
||
- 常量:`自动选择奖品:`
|
||
- 地址:`0x5A47EB`
|
||
- 汇编引用:`disassembly/42FC3C.asm:2775`
|
||
|
||
当没有在 `prizeList.value` 中查到目标时:
|
||
|
||
- 输出:`未查询到所需物品,继续监控`
|
||
- 地址:`0x5A47FD`
|
||
- 汇编引用:`disassembly/42FC3C.asm:2886`
|
||
- 会等待约 100ms:
|
||
- `sub_4276C9(100, 1, 0, 0)`
|
||
- `disassembly/42FC3C.asm:2847`
|
||
- 再投递 `sub_436756` 继续监控:
|
||
- `disassembly/42FC3C.asm:2872`
|
||
|
||
## 兑换请求发送
|
||
|
||
兑换请求由 `scoreExchangePrize(cookie, pid, sid)` 生成 WUP/WebSocketCommand 数据。
|
||
|
||
C 层最终通过 `sub_428C41` 发送:
|
||
|
||
- 第一处发送:
|
||
- `decompile/43E81F.c:512`
|
||
- 汇编:`disassembly/42FC3C.asm:1376`
|
||
- 第二处发送:
|
||
- `decompile/43E81F.c:620`
|
||
- 汇编:`disassembly/42FC3C.asm:1749`
|
||
|
||
这两处发送均使用:
|
||
|
||
- URL/目标地址:`unk_69EE18`
|
||
- 方法参数:`a2 = 1`
|
||
- 在 `sub_428C41` 中 `a2 = 1` 对应第 2 个方法,即 `POST`
|
||
- `decompile/428C41.c:159`
|
||
|
||
## HTTP 封装函数
|
||
|
||
### `sub_428C41`
|
||
|
||
位置:`decompile/428C41.c`
|
||
|
||
这是通用 HTTP 请求封装,不是业务函数。
|
||
|
||
功能特征:
|
||
|
||
- 支持多种 HTTP 方法:
|
||
- GET
|
||
- POST
|
||
- HEAD
|
||
- PUT
|
||
- OPTIONS
|
||
- DELETE
|
||
- TRACE
|
||
- CONNECT
|
||
- 方法分支位置:
|
||
- `decompile/428C41.c:159`
|
||
- 检测 `https://`:
|
||
- `decompile/428C41.c:212`
|
||
- 默认 User-Agent:
|
||
- `Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)`
|
||
- `decompile/428C41.c:225`
|
||
- 默认超时约 5000ms:
|
||
- `decompile/428C41.c:524`
|
||
|
||
## 响应处理
|
||
|
||
### 请求成功/兑换成功
|
||
|
||
相关常量:
|
||
|
||
- `请求成功`
|
||
- 地址:`0x5A4756`
|
||
- 在 `decompile/43E81F.c:453` 比较。
|
||
- `:兑换成功`
|
||
- 地址:`0x5A475F`
|
||
- 汇编引用:`disassembly/42FC3C.asm:1092`
|
||
|
||
逻辑含义:
|
||
|
||
- 当 `status == "200"` 且 `msg == "请求成功"` 时,会进入成功处理路径。
|
||
- 成功输出中会把当前奖品名与 `:兑换成功` 拼接。
|
||
|
||
注意:
|
||
|
||
- `byte_5A476B` 不是“兑换成功”,而是“操作频繁,请稍后再试”。
|
||
- `:兑换成功` 实际从 `0x5A475F` 开始。
|
||
|
||
### 操作频繁
|
||
|
||
相关常量:
|
||
|
||
- `操作频繁,请稍后再试`
|
||
- 地址:`0x5A476B`
|
||
- 比较位置:`decompile/43E81F.c:569`
|
||
|
||
- `操作频繁数据#`
|
||
- 地址:`0x5A477F`
|
||
- 汇编引用:`disassembly/42FC3C.asm:1561`
|
||
|
||
逻辑含义:
|
||
|
||
- 如果返回频控提示,会记录频控数据。
|
||
- 之后仍会进入后续请求/重试相关路径。
|
||
|
||
### 奖品已下架
|
||
|
||
相关常量:
|
||
|
||
- `奖品已下架`
|
||
- 地址:`0x5A4827`
|
||
- 汇编引用:`disassembly/42FC3C.asm:3888`
|
||
|
||
关联逻辑:
|
||
|
||
- 检测到下架后,会调用连接对象停止/关闭方法:
|
||
- `disassembly/42FC3C.asm:3908`
|
||
- 打印 `线程销毁`
|
||
- 地址:`0x5A4832`
|
||
- 汇编引用:`disassembly/42FC3C.asm:3914`
|
||
- 清除监控标志:
|
||
- `dword_69EE38 = 0`
|
||
- `disassembly/42FC3C.asm:3933`
|
||
- 短暂等待后打印 `执行自动监控`
|
||
- 地址:`0x5A483B`
|
||
- 汇编引用:`disassembly/42FC3C.asm:3943`
|
||
- 调用 `sub_4349E2` 尝试重启/恢复监控:
|
||
- `disassembly/42FC3C.asm:3954`
|
||
|
||
## 已解码关键中文/字符串常量
|
||
|
||
从 `memory/00586000--005CD000.txt` GBK 解码:
|
||
|
||
| 地址 | 内容 |
|
||
| --- | --- |
|
||
| `0x5A3C72` | `1` |
|
||
| `0x5A471F` | `2` |
|
||
| `0x5A472E` | `17` |
|
||
| `0x5A4756` | `请求成功` |
|
||
| `0x5A475F` | `:兑换成功` |
|
||
| `0x5A476B` | `操作频繁,请稍后再试` |
|
||
| `0x5A477F` | `操作频繁数据#` |
|
||
| `0x5A478F` | `prizeList.value` |
|
||
| `0x5A479F` | `自定义pid` |
|
||
| `0x5A47A9` | `].id` |
|
||
| `0x5A47AE` | `prizeList.value[` |
|
||
| `0x5A47BF` | `].num` |
|
||
| `0x5A47C5` | `].percent` |
|
||
| `0x5A47D7` | `].newScore` |
|
||
| `0x5A47E2` | `开始监控` |
|
||
| `0x5A47EB` | `自动选择奖品:` |
|
||
| `0x5A47FD` | `未查询到所需物品,继续监控` |
|
||
| `0x5A4818` | `-1` |
|
||
| `0x5A481B` | `sGuid` |
|
||
| `0x5A4821` | `guid=` |
|
||
| `0x5A4827` | `奖品已下架` |
|
||
| `0x5A4832` | `线程销毁` |
|
||
| `0x5A483B` | `执行自动监控` |
|
||
| `0x5A4C14` | `线程创建成功...` |
|
||
| `0x5A4C24` | `getUserPrizeRecords` |
|
||
| `0x5A4C38` | `scoreExchangePrize` |
|
||
| `0x5A4C5F` | `prize:` |
|
||
|
||
## 同构槽位回调
|
||
|
||
下面这些文件中的主体逻辑基本相同,差异主要是开头从 `dword_69EDD8 + 4 * slot` 读取不同账号/任务槽位:
|
||
|
||
- `decompile/436B5A.c`
|
||
- `decompile/4394F1.c`
|
||
- `decompile/43BE88.c`
|
||
- `decompile/43E81F.c`
|
||
- `decompile/4411B6.c`
|
||
- `decompile/443B4D.c`
|
||
- `decompile/4464E4.c`
|
||
- `decompile/448E7B.c`
|
||
- `decompile/44B812.c`
|
||
- `decompile/44E1A9.c`
|
||
- `decompile/4534D7.c`
|
||
- `decompile/455E6E.c`
|
||
- `decompile/458805.c`
|
||
- `decompile/45B19C.c`
|
||
- `decompile/45DB33.c`
|
||
- `decompile/4604CA.c`
|
||
- `decompile/462E61.c`
|
||
- `decompile/4657F8.c`
|
||
|
||
这些函数均引用同一批常量:
|
||
|
||
- `byte_5A3C72`
|
||
- `unk_5A471F`
|
||
- `unk_5A472E`
|
||
- `byte_5A4756`
|
||
- `byte_5A476B`
|
||
- `byte_5A4818`
|
||
- `unk_5A4C5F`
|
||
|
||
可见它们是一套多账号/多槽位并发模板。
|
||
|
||
## 流程伪代码
|
||
|
||
```text
|
||
启动自动监控:
|
||
初始化账号列表和活动/任务列表
|
||
等待连接对象 dword_69EE3C 可用
|
||
dword_69EE38 = 1
|
||
|
||
while 未停止 且 未超过循环次数:
|
||
for 每个账号:
|
||
for 每个活动/任务:
|
||
投递请求任务 sub_436737 -> sub_4363A4
|
||
sleep(配置间隔)
|
||
|
||
收到 WUP 响应:
|
||
json = decodeTaf(response)
|
||
status = json.status
|
||
msg = json.msg
|
||
requestId = json.iRequestId
|
||
|
||
if requestId == 2:
|
||
读取 score 并输出剩余积分
|
||
|
||
if requestId == 17:
|
||
读取 roleName/areaName/platName
|
||
输出绑定信息或未获取到绑定信息
|
||
|
||
if requestId == 1:
|
||
if 响应里无目标 pid/目标奖品:
|
||
输出 未查询到所需物品,继续监控
|
||
sleep(约 100ms)
|
||
投递 sub_436756 继续监控
|
||
else:
|
||
遍历 prizeList.value
|
||
读取 id/name/num/percent/newScore
|
||
记录/展示奖品信息
|
||
构造 scoreExchangePrize(cookie, pid, sid)
|
||
通过 sub_428C41 POST 发出
|
||
|
||
if status == 200 且 msg == 请求成功:
|
||
输出 当前奖品 + :兑换成功
|
||
|
||
if msg == 操作频繁,请稍后再试:
|
||
记录操作频繁数据
|
||
进入后续重试/刷新路径
|
||
|
||
if 奖品已下架:
|
||
停止连接/销毁线程
|
||
dword_69EE38 = 0
|
||
再次执行自动监控
|
||
|
||
if requestId == 6:
|
||
遍历 userPrizeList.value
|
||
输出/记录兑换记录
|
||
|
||
if requestId == -1:
|
||
读取 sGuid
|
||
```
|
||
|
||
## 待继续确认点
|
||
|
||
1. `unk_69EE18` 的运行时 URL 来源还未完全定位。
|
||
- 静态字符串里没有直接看到固定初始化。
|
||
- 更像是连接对象、配置或运行时脚本中保存的地址。
|
||
|
||
2. `sub_4968A7(index)` 的运行时字符串表需要进一步命名。
|
||
- 目前已通过 `pointers.txt` 和上下文确认一部分业务函数。
|
||
- 仍有若干 `sub_4968A7(5/6/7/9)` 取出的值需要结合更多调用点还原。
|
||
|
||
3. GUI 控件字段可以继续命名。
|
||
- 已知存在自定义 pid、自动选择奖品、循环/间隔相关配置。
|
||
- 但每个控件 ID 到语义字段的映射还可以继续细分。
|
||
|
||
4. `getActPrizeList` 的 request id 在 JS 中传入 `null`,但 C 侧奖品列表解析落在 `iRequestId == "1"` 分支。
|
||
- 可能是 WUP/脚本层默认值或反编译导出表现导致。
|
||
- 这一点如需完全确认,需要继续追 `setRequestId(null)` 的编码结果或运行时响应样本。
|
||
|
||
## 一句话总结
|
||
|
||
程序的抢兑链路是:
|
||
|
||
```text
|
||
查活动奖品列表 -> 匹配自定义 pid/目标奖品 -> 选中奖品 -> 构造 scoreExchangePrize(cookie,pid,sid)
|
||
-> POST 发送 WUP/WebSocketCommand -> 根据成功/频控/下架结果继续记录、重试或重启监控
|
||
```
|
||
|
||
## 2026-07-06 追加:截图与参考项目对照
|
||
|
||
### 截图里的运行态信息
|
||
|
||
`imgs/1.png`、`imgs/2.png`、`imgs/3.png` 能把 GUI 字段和逆向逻辑对齐:
|
||
|
||
- 主页面按钮:
|
||
- `查询`
|
||
- `积分查询`
|
||
- `绑定查询`
|
||
- `兑换记录`
|
||
- `兑换1次`
|
||
- `停止兑换`
|
||
- `取消定时`
|
||
- 设置页面参数:
|
||
- `线程 = 5`
|
||
- `次数 = 300`
|
||
- `延时 = 150`
|
||
- `Cookie`
|
||
- `网页登录`
|
||
- 定时时间选择器
|
||
- 已登录账号页面:
|
||
- 多账号表列:`num / cookie / name / 状态 / 首个兑换`
|
||
- 每个账号可勾选。
|
||
- 下方共用同一份奖品表。
|
||
- 奖品表列:
|
||
- `num`
|
||
- `name`
|
||
- `剩余`
|
||
- `总量`
|
||
- `价格`
|
||
- 日志示例:
|
||
- `登录成功`
|
||
- `你选择了:套装-朝萌小黄鸭`
|
||
|
||
与反编译对应关系:
|
||
|
||
- `次数` 基本对应 `sub_43597D` 中的 `v51`,循环条件为 `v46 < v51`:
|
||
- `decompile/43597D.c:73-95`
|
||
- `decompile/43597D.c:243-245`
|
||
- `延时` 基本对应 `sub_43597D` 中的 `v50`,外层循环 sleep:
|
||
- `decompile/43597D.c:86-90`
|
||
- `decompile/43597D.c:243`
|
||
- `线程` 与连接对象/worker 数量相关,`sub_43597D` 会通过连接对象方法读取可用数量并投递任务:
|
||
- `decompile/43597D.c:160`
|
||
- `decompile/43597D.c:221`
|
||
- 具体 UI 控件到线程池大小的赋值点还未完全命名。
|
||
- 多账号、多目标组合会形成笛卡尔积投递:
|
||
- 外层账号列表:`v53`
|
||
- 内层目标/活动列表:`v52`
|
||
- 投递入口:`sub_436737 -> sub_4363A4`
|
||
- `decompile/43597D.c:163-227`
|
||
|
||
### JS 请求构造的精确字段
|
||
|
||
从 `strings.txt` 里抽出的 JS 可以确认:
|
||
|
||
- `_sendWup(servant, func, req, requestId, ..., options)`
|
||
- 构造 `w.Wup`
|
||
- `setServant(servant)`
|
||
- `setFunc(func)`
|
||
- `setRequestId(requestId)`
|
||
- `writeStruct("tReq", req)`
|
||
- 再包进 `E.WebSocketCommand`
|
||
- `iCmdType = EWSCmd_WupReq`
|
||
- `traceId = hex16:hex16:0:0`
|
||
- 返回 base64 编码后的 `WebSocketCommand` 字节。
|
||
|
||
- `scoreExchangePrize(cookie, pid, sid)`
|
||
- `ExchangePrizeReq.pid = pid`
|
||
- `ExchangePrizeReq.sid = sid`
|
||
- `userId.iTokenType = 0`
|
||
- `userId.lUid = parseInt(cookie["yyuid"])`
|
||
- `userId.sCookie = cookie`
|
||
- `userId.sHuYaUA = "web&1.0&huya"`
|
||
- `requestId = 1`
|
||
|
||
- `getActPrizeList(cookie, sid)`
|
||
- `GetActPrizeListReq.userId.lUid = yyuid`
|
||
- `GetActPrizeListReq.userId.sCookie = cookie`
|
||
- `GetActPrizeListReq.userId.sHuYaUA = "web&1.0&huya"`
|
||
- `GetActPrizeListReq.sid = sid`
|
||
- JS 里 `requestId = null`
|
||
- 但 C 侧奖品列表和兑换响应都落在 `iRequestId == "1"` 分支,需要按实际响应确认。
|
||
|
||
- `getUserPrizeRecords(cookie, sid)`
|
||
- `GetUserPrizeRecordsReq.sid = sid`
|
||
- `userId.lUid = yyuid`
|
||
- `userId.sGuid = guid`
|
||
- `userId.sCookie = cookie`
|
||
- `userId.sHuYaUA = "webh5&1.0.0&huya"`
|
||
- `requestId = 6`
|
||
|
||
- `getUserScore(cookie, sid)`
|
||
- `requestId = 2`
|
||
- `userId.sHuYaUA = "web&1.0&huya"`
|
||
|
||
- `checkUserBindGameAccount(cookie, outerActId)`
|
||
- `requestId = 17`
|
||
- `userId.sHuYaUA = "pc&1.0.0&huya"`
|
||
- `gid = "3203"`
|
||
|
||
- `doLaunch(cookie)`
|
||
- servant/func:`launch` / `wsLaunch`
|
||
- `requestId = -1`
|
||
- `sUA = "webh5&0.0.1&activity&other"`
|
||
- `sAppSrc = "HUYA&ZH&2052"`
|
||
|
||
### 奖品结构字段
|
||
|
||
样本内 JS 定义的 `TActPrizeInfo` 字段:
|
||
|
||
| tag | 字段 | 类型/含义 |
|
||
| --- | --- | --- |
|
||
| 0 | `id` | 奖品 pid |
|
||
| 1 | `sid` | 活动 sid |
|
||
| 2 | `name` | 奖品名 |
|
||
| 3 | `score` | 旧积分价格 |
|
||
| 4 | `type` | 类型 |
|
||
| 5 | `img` | 图片 |
|
||
| 6 | `sort` | 排序 |
|
||
| 7 | `frequency` | 频次 |
|
||
| 8 | `frequencyLimit` | 频次限制 |
|
||
| 9 | `createTime` | 创建时间 |
|
||
| 10 | `num` | 总量 |
|
||
| 11 | `status` | 状态 |
|
||
| 12 | `percent` | 剩余百分比文本 |
|
||
| 13 | `newScore` | 新积分价格 |
|
||
| 14 | `tags` | 标签/分类 |
|
||
| 15 | `leftNum` | 剩余数量字段 |
|
||
| 16 | `isShowNum` | 是否展示数量 |
|
||
|
||
当前 C 回调实际重点读取:
|
||
|
||
- `id`
|
||
- `name`
|
||
- `num`
|
||
- `percent`
|
||
- `newScore`
|
||
|
||
截图中的展示大概率为:
|
||
|
||
- `总量 = num`
|
||
- `价格 = newScore`
|
||
- `剩余 = floor(num * percent / 100)` 或近似取整
|
||
|
||
例子:截图里 `套装-朝萌小黄鸭` 行显示 `剩余 6000 / 总量 24001 / 价格 988`,和 `num * percent / 100` 这类计算路径吻合:
|
||
|
||
- `decompile/43E81F.c:401-419`
|
||
- `disassembly/42FC3C.asm:2276`
|
||
- `disassembly/42FC3C.asm:2350`
|
||
|
||
## 参考项目可复用结论
|
||
|
||
参考项目:`/Users/yml/codes/douyu_login_py`
|
||
|
||
### HTTP RPC 通道
|
||
|
||
文件:`core/huya/http_client.py`
|
||
|
||
该项目已经实现了一条可直接迁移到 Go 的 HTTP POST RPC 链路:
|
||
|
||
- URL:
|
||
- `https://cdnws.api.huya.com/?baseinfo=<base64url(WSConnectParaInfo)>`
|
||
- Body:
|
||
- 完整 WUP 包,带 4 字节大端长度前缀。
|
||
- Header:
|
||
- `Content-Type: application/octet-stream`
|
||
- `Origin: https://zt.huya.com`
|
||
- `Referer: https://zt.huya.com/`
|
||
- PC Chrome UA
|
||
- `WSConnectParaInfo` baseinfo 字段:
|
||
|
||
| tag | 字段 | 说明 |
|
||
| --- | --- | --- |
|
||
| 0 | `lUid` | yyuid |
|
||
| 1 | `sGuid` | guid,可为空 |
|
||
| 2 | `sUA` | `webh5&0.0.1&websocket&&diypc_52775` |
|
||
| 3 | `sAppSrc` | `HUYA&ZH&2052` |
|
||
| 4 | `sMid` | 空 |
|
||
| 5 | `sExp` | 空 |
|
||
| 6 | `iTokenType` | 0 |
|
||
| 7 | `sToken` | 空 |
|
||
| 8 | `sCookie` | 完整 Cookie |
|
||
| 9 | `sTraceId` | `hex16:hex16:0:0` |
|
||
| 10 | `mCustomHeaders` | 空 Map |
|
||
|
||
便捷方法:
|
||
|
||
- `query_user_score`
|
||
- `get_act_prize_list`
|
||
- `get_user_prize_records`
|
||
- `score_exchange_prize`
|
||
- `check_user_bind_game_account`
|
||
|
||
注意:
|
||
|
||
- 参考项目的 HTTP `call_rpc` 统一 `setRequestId(1)`。
|
||
- 这和样本 JS 的 `getUserScore=2`、`getUserPrizeRecords=6`、`check=17` 不完全一致。
|
||
- 对 HTTP cdnws 通道来说,服务端主要靠 servant/func 和 `tReq` 解析,requestId 更像客户端响应关联字段。
|
||
- 如果目标是“尽量复刻样本”,Go 版应允许每个接口显式指定 requestId。
|
||
|
||
### TAF/WUP 编解码
|
||
|
||
文件:
|
||
|
||
- `core/huya/taf_protocol.py`
|
||
- `core/huya/wup_protocol.py`
|
||
|
||
可直接翻译为 Go:
|
||
|
||
- TAF type:
|
||
- `INT8=0`
|
||
- `INT16=1`
|
||
- `INT32=2`
|
||
- `INT64=3`
|
||
- `FLOAT=4`
|
||
- `DOUBLE=5`
|
||
- `STRING1=6`
|
||
- `STRING4=7`
|
||
- `MAP=8`
|
||
- `LIST=9`
|
||
- `STRUCT_BEGIN=10`
|
||
- `STRUCT_END=11`
|
||
- `ZERO=12`
|
||
- `SIMPLE_LIST=13`
|
||
- 头部:
|
||
- `tag < 15`:单字节 `(tag << 4) | type`
|
||
- `tag >= 15`:两字节 `0xF0 | type, tag`
|
||
- 整数写入要做压缩:
|
||
- 0 写 `ZERO`
|
||
- int8 范围写 `INT8`
|
||
- int16 范围写 `INT16`
|
||
- int32 范围写 `INT32`
|
||
- 否则写 `INT64`
|
||
- WUP v3:
|
||
- tag1 `iVersion`
|
||
- tag2 `cPacketType`
|
||
- tag3 `iMessageType`
|
||
- tag4 `iRequestId`
|
||
- tag5 `sServantName`
|
||
- tag6 `sFuncName`
|
||
- tag7 `sBuffer`
|
||
- tag8 `iTimeout`
|
||
- tag9 `context`
|
||
- tag10 `status`
|
||
- `sBuffer = Map<string, bytes>{"tReq": encodedStruct}`
|
||
- 最外层加 4 字节大端总长度。
|
||
|
||
### 活动结构体
|
||
|
||
文件:`core/huya/activity_structs.py`
|
||
|
||
Go 版最小需要先实现:
|
||
|
||
- `ActivityUserId`
|
||
- tag0 `lUid`
|
||
- tag1 `sGuid`
|
||
- tag2 `sToken`
|
||
- tag3 `sHuYaUA`
|
||
- tag4 `sCookie`
|
||
- tag5 `iTokenType`
|
||
- tag6 `sDeviceInfo`
|
||
|
||
- `GetActPrizeListReq`
|
||
- tag0 `userId`
|
||
- tag1 `sid`
|
||
|
||
- `GetActPrizeListResp`
|
||
- tag0 `status`
|
||
- tag1 `msg`
|
||
- tag2 `prizeList`
|
||
- tag4 `tagList`
|
||
|
||
- `ScoreExchangePrizeReq`
|
||
- tag0 `userId`
|
||
- tag1 `sid`
|
||
- tag2 `pid`
|
||
- tag3 `ip`
|
||
- tag4 `clientEnv`
|
||
- tag5 `source`
|
||
- tag6 `isRole`
|
||
|
||
- `ScoreExchangePrizeResp`
|
||
- tag0 `status`
|
||
- tag1 `msg`
|
||
- tag3 `orderId`
|
||
- tag4 `exchangeInfo`
|
||
- tag5 `actPreCondition`
|
||
|
||
- `GetUserPrizeRecordsReq/Resp`
|
||
- `GetUserScoreReq/Resp`
|
||
|
||
### WSS 参考项目和当前样本的差异
|
||
|
||
文件:`core/huya/wss_client.py`
|
||
|
||
参考项目的 WSS 是商城通道:
|
||
|
||
- host:`77bc035c-ws.va.huya.com`
|
||
- frame header:`>BBI`
|
||
- 1B version
|
||
- 1B command
|
||
- 4B sequence
|
||
- 初始化:
|
||
- heartbeat
|
||
- `launch.wsLaunch`
|
||
- `mobileui.getConfig`
|
||
- register
|
||
- confirm
|
||
- business RPC
|
||
- 响应匹配:
|
||
- 服务端响应 seq 不一定等于请求 seq
|
||
- 项目用 FIFO 队列匹配。
|
||
|
||
当前样本 JS 的业务外层是 `E.WebSocketCommand` TAF 结构:
|
||
|
||
- tag0 `iCmdType`
|
||
- tag1 `vData`
|
||
- tag2 `lRequestId`
|
||
- tag3 `traceId`
|
||
- tag4 `iEncryptType`
|
||
- tag5 `lTime`
|
||
- tag6 `sMD5`
|
||
|
||
样本里明文 WSS host:
|
||
|
||
- `wss://65cecb21-ws.va.huya.com/`
|
||
- 位置:
|
||
- `strings.txt:702`
|
||
- `decompile/46DEE1.c:147`
|
||
|
||
因此 Go 复刻时应把通道拆成两个可替换实现:
|
||
|
||
1. `cdnws HTTP POST WUP`
|
||
- 参考项目已有完整实现。
|
||
- 最适合先做最小可用版本。
|
||
2. `va.huya WSS + WebSocketCommand`
|
||
- 更贴近样本 JS 的外层 `WebSocketCommand`。
|
||
- 需要继续确认握手、verifyCookie、registerGroup、heartbeat 与业务发送封装。
|
||
|
||
## Go 复刻建议蓝图
|
||
|
||
### 包结构
|
||
|
||
```text
|
||
huya-go/
|
||
taf/
|
||
type.go
|
||
writer.go
|
||
reader.go
|
||
wup/
|
||
request.go
|
||
response.go
|
||
huya/
|
||
cookie.go
|
||
baseinfo.go
|
||
activity/
|
||
structs.go
|
||
client.go
|
||
transport/
|
||
http_rpc.go
|
||
wss_rpc.go
|
||
sniper/
|
||
config.go
|
||
scheduler.go
|
||
matcher.go
|
||
result.go
|
||
```
|
||
|
||
### 最小可用链路
|
||
|
||
先做 HTTP cdnws 版本:
|
||
|
||
```text
|
||
读取账号 Cookie
|
||
-> 解析 yyuid/guid
|
||
-> 构造 ActivityUserId
|
||
-> 构造 GetActPrizeListReq
|
||
-> WUP encode
|
||
-> POST cdnws
|
||
-> WUP decode GetActPrizeListResp
|
||
-> 匹配目标 pid/name
|
||
-> 构造 ScoreExchangePrizeReq
|
||
-> POST cdnws
|
||
-> WUP decode ScoreExchangePrizeResp
|
||
-> 记录 status/msg/orderId
|
||
```
|
||
|
||
### 抢购调度逻辑
|
||
|
||
建议按样本行为复刻,但加上明确的停止条件和退避:
|
||
|
||
```text
|
||
config:
|
||
accounts: []Account
|
||
targets: []Target
|
||
sid: int
|
||
maxLoops: int # 截图里的 次数
|
||
delayMs: int # 截图里的 延时
|
||
concurrency: int # 截图里的 线程
|
||
scheduledAt: time
|
||
stopAfterFirstSuccess: bool
|
||
|
||
run:
|
||
等到 scheduledAt
|
||
for loop in 1..maxLoops:
|
||
并发遍历勾选账号
|
||
查询奖品列表
|
||
过滤目标:
|
||
自定义 pid 优先
|
||
否则按 name/选中行匹配
|
||
如果未命中:
|
||
记录 未查询到所需物品,继续监控
|
||
return
|
||
如果命中:
|
||
如果剩余估算 <= 0:
|
||
可选择跳过或继续尝试
|
||
调 scoreExchangePrize
|
||
status == 200 且 msg == 请求成功:
|
||
记录兑换成功
|
||
如果 stopAfterFirstSuccess: 标记账号停止
|
||
msg == 操作频繁:
|
||
记录频控,按账号退避
|
||
msg == 奖品已下架:
|
||
刷新列表/重启监控
|
||
sleep(delayMs)
|
||
```
|
||
|
||
### Go 实现时的关键细节
|
||
|
||
- Cookie:
|
||
- 去重保留最后一次。
|
||
- 从 `yyuid` 或 `udb_uid` 解析 uid。
|
||
- `guid` 可从 Cookie 取;HTTP cdnws 可为空,但 WSS/launch 分支需要处理返回 `sGuid`。
|
||
- UA:
|
||
- 样本活动兑换/列表:`web&1.0&huya`
|
||
- 样本兑换记录:`webh5&1.0.0&huya`
|
||
- 样本绑定查询:`pc&1.0.0&huya`
|
||
- 参考 HTTP 客户端:`webh5&0.0.1&websocket&&diypc_52775`
|
||
- 建议 Go 结构允许按接口覆盖,不要写死一个全局值。
|
||
- requestId:
|
||
- 样本兑换:`1`
|
||
- 样本积分:`2`
|
||
- 样本记录:`6`
|
||
- 样本绑定:`17`
|
||
- 样本 launch:`-1`
|
||
- HTTP cdnws 可先统一 `1`,但保留参数。
|
||
- 奖品剩余:
|
||
- 样本 C 侧使用 `num`、`percent`、`newScore` 展示。
|
||
- 如果响应有 `leftNum`,Go 版可以同时记录直接字段和估算字段,避免误判。
|
||
- 频控:
|
||
- 样本识别 `操作频繁,请稍后再试` 后仍会继续后续重试路径。
|
||
- Go 版建议记录频控并按账号退避,避免无意义高频请求。
|
||
|
||
## 新增待确认点
|
||
|
||
1. `unk_69EE18` 仍未定位到静态写入点。
|
||
- 目前只看到它作为 `sub_428C41` 的 URL 参数被读取:
|
||
- `decompile/43E81F.c:513`
|
||
- `decompile/43E81F.c:621`
|
||
- 明文有 `wss://65cecb21-ws.va.huya.com/`:
|
||
- `strings.txt:702`
|
||
- `decompile/46DEE1.c:147`
|
||
- 但参考项目 HTTP 版使用 `https://cdnws.api.huya.com/?baseinfo=...`。
|
||
- 需要实际抓一次样本运行请求,或继续追连接对象初始化,才能最终确认业务发送 URL。
|
||
|
||
2. `线程` 控件的赋值点还未完全映射。
|
||
- 样本有 18 个同构槽位回调。
|
||
- 截图中设置为 5。
|
||
- `sub_43597D` 通过连接对象方法读取可用数量和投递任务,但 UI 控件到连接池容量的路径还需继续找。
|
||
|
||
3. `getActPrizeList` 的 requestId。
|
||
- JS 传 `null`。
|
||
- `Wup.prototype.setRequestId=function(t){this.iRequestId=t||++this.iRequestId}`。
|
||
- 所以 `null` 会触发自增,初始值为 0 时会变成 1。
|
||
- 这解释了 C 侧奖品列表落入 `iRequestId == "1"` 分支。
|
||
|
||
4. HTTP cdnws 与样本 WSS 外层差异。
|
||
- 用 Go 先做 HTTP 版本可更快验证结构体和兑换接口。
|
||
- 如果要完全复刻样本,需要再补 `WebSocketCommand` 外层、verifyCookie、doLaunch、registerGroup、heartbeat。
|
||
|
||
## 2026-07-06 运行日志与 HAR 新证据
|
||
|
||
### 输入文件
|
||
|
||
- `/Users/yml/Desktop/抓包/hy/抢购/7.6-软件抓包.har`
|
||
- `/Users/yml/Desktop/抓包/hy/抢购/软件日志.txt`
|
||
- `/Users/yml/Desktop/抓包/hy/抢购/软件日志2.txt`
|
||
|
||
注意:HAR 中包含真实账号 Cookie、uid、guid、token 类字段。下面只记录结构化结论,不保存明文敏感值。
|
||
|
||
### HAR 总览
|
||
|
||
- HAR 共 702 条请求。
|
||
- 其中 661 条是 `HEAD http://www.baidu.com/`,应为软件或代理的联网检测/保活噪声。
|
||
- 真正与本软件相关的主要请求:
|
||
- `api3.1wxyun.com`:5 条软件授权/登录相关请求,`type=1/11/8/24/21`。
|
||
- `129.204.148.26:7381/huya/heping.html`:1 条远端配置/公告页。
|
||
- `i.huya.com`:3 条账号页面加载。
|
||
- `65cecb21-ws.va.huya.com`:3 条 WebSocket 握手,状态码 `101`。
|
||
- `diy-assets.msstatic.com/cqhfolder/NewDefault.png`:3 条默认头像/资源图加载。
|
||
- `82.156.166.142:1822/upload`:3 条兑换成功后的远端上报。
|
||
|
||
HAR 只保存了 WebSocket 握手,没有保存 WebSocket 帧内容,所以无法直接从这份 HAR 还原 `scoreExchangePrize` 的二进制 WUP 请求体。它能确认真实运行使用 WSS 通道,但请求体仍需要依赖反编译和参考项目里的 WUP/TAF 编码继续复刻。
|
||
|
||
### 远端配置页
|
||
|
||
`http://129.204.148.26:7381/huya/heping.html` 返回了软件配置:
|
||
|
||
- 版本号:`2.7`
|
||
- 软件开关:`1`
|
||
- 兑换代码:`2203`
|
||
- 子奖品代码 A:`10329`
|
||
- 子奖品代码 B:`10329`
|
||
- livelink 活动 id:`17096`
|
||
- 上传地址:
|
||
- `http://129.204.148.26:1822/upload`
|
||
- `http://115.159.215.151:1822/upload`
|
||
- `http://82.156.166.142:1822/upload`
|
||
- 登录开关、cookie 开关、切换活动开关、任务开关均为 `1`。
|
||
|
||
这说明软件并不只依赖本地静态常量,还会启动时拉取作者远端配置。对 Go 复刻来说,`兑换代码=2203`、`子奖品代码=10329`、`活动 id=17096` 是这次“套装-潮萌小黄鸭”的直接运行参数;远端开关/上传地址不需要复刻。
|
||
|
||
### WSS 连接时序
|
||
|
||
HAR 时间换算成本地时间后:
|
||
|
||
- `16:53:58.071`:3 条 `GET https://i.huya.com/`
|
||
- `16:53:58.109` 到 `16:53:58.114`:3 条 `GET wss://65cecb21-ws.va.huya.com/` 握手成功
|
||
- `16:53:59`:日志显示 3 个账号登录成功
|
||
- `16:59:57`:日志显示线程创建成功并开始兑换
|
||
|
||
这和前面的反编译结论可以对上:软件在登录阶段为每个账号建立或准备连接,抢购触发时复用已有连接,而不是到点后再创建 WSS。Go 版如果要贴近样本,应改成“账号初始化 -> 建立持久 WSS -> launch/register/heartbeat -> 到点并发发送兑换 WUP”。
|
||
|
||
WebSocket 握手头特征:
|
||
|
||
- Host:`65cecb21-ws.va.huya.com`
|
||
- User-Agent:`WebSocket++/0.8.2`
|
||
- Upgrade:`websocket`
|
||
- 未看到浏览器常见的 `Origin`。
|
||
|
||
### 成功上报行为
|
||
|
||
HAR 中 3 条 `POST http://82.156.166.142:1822/upload` 与 `软件日志.txt` 的 3 个成功账号对应:
|
||
|
||
- 本地时间约 `17:00:01.946`、`17:00:04.751`、`17:00:05.508` 上报。
|
||
- 上报体使用 GBK 文本拼接,字段以 `#` 分隔。
|
||
- 上报内容包括账号昵称、成功时间、奖品名、完整 Cookie、活动/软件标识、程序名 `多功能计算器2.7.vmp.exe` 等。
|
||
|
||
这是明显的数据外传逻辑。Go 复刻不应实现这个上传接口;如果保留日志,只应写本地脱敏日志。
|
||
|
||
### `软件日志.txt` 统计
|
||
|
||
该日志对应 3 个账号,和 HAR 中的 3 条 `i.huya.com`、3 条 WSS、3 条上传最匹配。
|
||
|
||
- 登录账号数:3
|
||
- 选择奖品:`套装-潮萌小黄鸭`
|
||
- 兑换动作日志:270 条
|
||
- 成功:3 条
|
||
- 操作频繁:4 条
|
||
- 兑换货币不足:263 条
|
||
- 成功时间:全部在 `16:59:57`
|
||
- 成功后查询到剩余积分:3 个账号均为 `2`
|
||
|
||
首批结果:
|
||
|
||
| 时间 | 账号结果 |
|
||
| --- | --- |
|
||
| 16:59:57 | 堂上不仁也:兑换成功 |
|
||
| 16:59:57 | 里村路老:兑换成功 |
|
||
| 16:59:57 | 颁赏胡同兰博:先操作频繁,随后同秒兑换成功 |
|
||
|
||
后续从 `17:00:01` 起继续大量重试,但返回基本变成“兑换货币不足”。这说明样本成功后没有立刻按账号停止兑换循环,或停止标记只影响了部分路径;复刻时应加“账号成功后停止该奖品重试”,否则会制造无意义请求并触发频控。
|
||
|
||
### `软件日志2.txt` 统计
|
||
|
||
该日志对应 10 个账号,HAR 未捕获到对应的 10 条 WSS/上传,因此它应是另一轮运行日志或抓包缺失了这部分流量。
|
||
|
||
- 登录账号数:10
|
||
- 选择奖品:`套装-潮萌小黄鸭`
|
||
- 兑换动作日志:1190 条
|
||
- 奖品暂未开放兑换:70 条
|
||
- 成功:10 条
|
||
- 操作频繁:6 条
|
||
- 兑换货币不足:1104 条
|
||
- 成功时间:全部在 `17:00:00`
|
||
- 成功后查询到剩余积分:10 个账号均为 `2`
|
||
|
||
关键节奏:
|
||
|
||
| 秒级时间 | 请求结果分布 |
|
||
| --- | --- |
|
||
| 16:59:57 | 13 条,全部“奖品暂未开放兑换” |
|
||
| 16:59:58 | 37 条,全部“奖品暂未开放兑换” |
|
||
| 16:59:59 | 20 条,全部“奖品暂未开放兑换” |
|
||
| 17:00:00 | 10 条,全部“兑换成功” |
|
||
| 17:00:02 起 | 大量“兑换货币不足”,夹杂少量“操作频繁” |
|
||
|
||
这说明软件会在开放前约 3 秒开始高频探测,不等官方开放时间才发第一波。开放瞬间,每个账号至少成功提交一次;成功之后仍继续跑循环,造成 1100+ 条无效的“兑换货币不足”。
|
||
|
||
### 对抢购逻辑的更新判断
|
||
|
||
1. 远端配置确认了这次奖品参数:
|
||
- 活动 id:`17096`
|
||
- 兑换代码/父奖品或活动奖品标识:`2203`
|
||
- 子奖品 id:`10329`
|
||
|
||
2. 实际样本运行确认使用 WSS:
|
||
- 静态字符串里的 `wss://65cecb21-ws.va.huya.com/` 不是备用字符串。
|
||
- 登录阶段按账号建立 WSS,抢购阶段复用连接发请求。
|
||
|
||
3. HAR 未保存 WSS 帧:
|
||
- 不能从当前 HAR 直接提取 `WebSocketCommand`、`iRequestId` 或兑换请求 WUP body。
|
||
- 仍需从反编译、JS/WUP 定义、参考 Python 项目或重新抓 WebSocket frames 来补齐。
|
||
|
||
4. 样本抢购策略:
|
||
- 提前约 3 秒开始轮询/尝试。
|
||
- 多账号并发。
|
||
- 成功后继续重试,直到后续查询/循环结束。
|
||
- 频控文案不是致命错误,样本会继续尝试。
|
||
|
||
5. Go 复刻建议:
|
||
- 账号维度维护持久 WSS 连接,而不是每次兑换新建 HTTP 请求。
|
||
- 抢购开始时间可设置为官方时间前 2-3 秒,允许“暂未开放”继续重试。
|
||
- 每个账号成功一次后立即停止该账号对同一奖品的兑换重试。
|
||
- “操作频繁”做短退避;“兑换货币不足”可视为该账号已成功或积分不足,停止重试。
|
||
- 禁止实现样本的远端 `upload` 上报;只保留本地脱敏日志。
|
||
|
||
### 仍需补证
|
||
|
||
- 重新抓包时需要确认工具能保存 WebSocket binary frames;当前 HAR 只有 WSS handshake。
|
||
- 如果抓不到帧,可以在 Go 版用已实现的 WUP 编码对照参考项目,先构造:
|
||
- `webActUI.getActPrizeList`
|
||
- `webActUI.getUserScore`
|
||
- `webActUI.scoreExchangePrize`
|
||
- 然后补样本 WSS 外层:
|
||
- `WebSocketCommand`
|
||
- `verifyCookie`
|
||
- `doLaunch`
|
||
- `registerGroup`
|
||
- heartbeat
|
||
|
||
## 2026-07-06 浏览器手动抢购小黄鸭 HAR 新证据
|
||
|
||
### 输入文件
|
||
|
||
- `/Users/yml/Desktop/抓包/hy/抢购/7.6-手动抢购小黄鸭`
|
||
|
||
该文件是 Charles Proxy 5.2 导出的 HAR,文件无扩展名,实际是单行 JSON。HAR 内含真实 Cookie、uid、guid、token、账号名等敏感字段;这里只记录脱敏后的协议结论。
|
||
|
||
### HAR 总览
|
||
|
||
- 条目数:228
|
||
- 主要 host:
|
||
- `cdnws.api.huya.com`:12 条 HTTP WUP 请求。
|
||
- `77bc035c-ws.va.huya.com`:3 条 WebSocket 握手,状态码 `101`。
|
||
- `zt.huya.com`:活动页 `b02faae1/pc/index.html`。
|
||
- `dataservice.cdn.huya.com`、`l.huya.com`:配置/登录态检查。
|
||
- `metric.huya.com`、`ylog.huya.com`、`statwup.huya.com`:埋点/统计噪声。
|
||
- 活动页 URL:
|
||
- `https://zt.huya.com/b02faae1/pc/index.html?sourceId=yellowcarlist&pid=1199650619883&anchorUid=1199650619883&roomid=30596253`
|
||
|
||
这份 HAR 比软件 HAR 更完整:Charles 保存了 WebSocket binary frames,能还原手动浏览器实际发送的 WUP 请求。
|
||
|
||
### Charles HAR 编码特征
|
||
|
||
Charles 在 HAR 中不是直接保存二进制 body,而是保存 base64 文本:
|
||
|
||
1. `cdnws.api.huya.com` 的 `request.postData.text` 和 `response.content.text` 是 WUP 二进制包的 base64。
|
||
2. `wss://77bc035c-ws.va.huya.com/` 的 `request.postData.text` / `response.content.text` 是原始 WebSocket wire bytes 的 base64。
|
||
3. WebSocket client -> server 帧:
|
||
- `opcode=2`
|
||
- `masked=1`
|
||
- `RSV1=1`
|
||
- 启用了 `permessage-deflate`
|
||
4. WebSocket 解码顺序:
|
||
- base64 decode
|
||
- 解析 WebSocket frame
|
||
- client 帧按 mask key XOR 解 mask
|
||
- 若 `RSV1=1`,使用 raw deflate 解压;Python 可用 `zlib.decompressobj(wbits=-15)`,并在 payload 后补 `00 00 ff ff`
|
||
- 解压后 payload 中从 offset `6` 或 `7` 开始是 WUP packet
|
||
- WUP packet 后还有尾部追踪字段:client 侧约 44 bytes,server 侧约 25 bytes
|
||
|
||
Go 复刻 WSS 时需要支持:
|
||
|
||
- WebSocket binary frame
|
||
- permessage-deflate
|
||
- client mask 由 WebSocket 库自动处理
|
||
- 从 `WebSocketCommand` 外层取出/填入内部 WUP packet
|
||
|
||
### HTTP cdnws 初始化请求
|
||
|
||
`cdnws.api.huya.com` 中可见的主要 WUP:
|
||
|
||
| 时间 | servant.func | 作用 |
|
||
| --- | --- | --- |
|
||
| 16:59:45.467 | `launch.wsLaunch` | HTTP 版启动/获取连接信息 |
|
||
| 16:59:45.467 | `launch.pingSignal` | 信令 ping |
|
||
| 16:59:45.518 | `webActUI.checkUserBindGameAccount` | 查询游戏账号绑定 |
|
||
| 16:59:45.518 | `webActUI.getLiveLinkParam` | 获取活动参数 |
|
||
| 16:59:45.519 | `huyauserui.getUserProfileBatch` | 查询用户资料 |
|
||
| 16:59:45.551 / 16:59:45.553 | `shopMiddleUI.getGoodsInfoV5` | 黄车商品信息 |
|
||
| 16:59:45.670/672/679 | `launch.queryHttpDns` | 查询可用 WSS host |
|
||
| 16:59:46.064 | `webLiveUI.getWebUserBaseInfo` | 直播用户信息 |
|
||
| 16:59:47.079 | `launch.pingSignal` | 后续 ping |
|
||
|
||
`webActUI.getLiveLinkParam` 回包中确认:
|
||
|
||
- `actId=17096`
|
||
- `gameIdList=cjm`
|
||
- `livePlatId=huya`
|
||
- `code`、`sig` 为动态签名字段,不在文档中保存明文。
|
||
|
||
`launch.queryHttpDns` 返回多个可用 WSS host,其中本次实际连接的是:
|
||
|
||
- `77bc035c-ws.va.huya.com`
|
||
|
||
这说明 `65cecb21-ws.va.huya.com` 不是唯一固定地址;Go 版不要写死单个 WSS host,应该先走 `launch.queryHttpDns`,或至少允许配置/轮换 host。
|
||
|
||
### 手动 WSS 抢购函数顺序
|
||
|
||
主要业务发生在 `77bc035c-ws.va.huya.com` 的第 2 条 WSS 连接中。解 mask + inflate 后,client -> server 函数顺序如下:
|
||
|
||
| frame | requestId | servant.func | 关键参数 |
|
||
| --- | ---: | --- | --- |
|
||
| 6 | 7 | `webActUI.getActPrizeList` | `sid=2203` |
|
||
| 7 | 8 | `webActUI.getActInfo` | 活动信息 |
|
||
| 8 | 9 | `webActUI.getActTaskDetail` | 任务列表 |
|
||
| 9 | 10 | `webActUI.getActUserTaskDetail` | 用户任务状态 |
|
||
| 10 | 11 | `webActUI.getUserScore` | `sid=2203` |
|
||
| 12 | 12 | `webActUI.getActPrizeDetail` | `sid=2203` |
|
||
| 13 | 13 | `webActUI.getActPrizeDetail` | `sid=2203` |
|
||
| 14 | 14 | `webActUI.scoreExchangePrize` | `sid=2203`, `pid=12889`, `ip=""`, `clientEnv={}`, `source=""`, `isRole=1` |
|
||
| 15 | 15 | `webActUI.getActPrizeList` | `sid=2203` |
|
||
| 16 | 16 | `webActUI.getUserScore` | `sid=2203` |
|
||
|
||
这条链路和之前逆向出的 `webActUI.scoreExchangePrize(cookie,pid,sid)` 完全对上。这里可以把 `sid=2203` 理解为活动/兑换场次 id,把 `pid=12889` 理解为本次“套装-潮萌小黄鸭”的奖品 id。
|
||
|
||
### 手动兑换成功回包
|
||
|
||
`scoreExchangePrize` 的 server -> client 回包:
|
||
|
||
- `requestId=14`
|
||
- `servant=webActUI`
|
||
- `func=scoreExchangePrize`
|
||
- `status=200`
|
||
- `msg=请求成功`
|
||
- `orderId=sOrderId_2203_12889_<uid>_1783328404_957`
|
||
- `exchangeInfo.pid=12889`
|
||
- `exchangeInfo.exchangeScore=988`
|
||
- `exchangeInfo.desc=兑换奖品:套装-潮萌小黄鸭`
|
||
|
||
`1783328404` 转成本地时间为 `2026-07-06 17:00:04`,说明这次浏览器手动兑换成功发生在 17:00:04 左右。
|
||
|
||
该回包进一步确认:
|
||
|
||
- 本次手动小黄鸭兑换参数是 `sid=2203`、`pid=12889`。
|
||
- 小黄鸭套装消耗积分是 `988`。
|
||
- 兑换成功后页面会继续请求 `getActPrizeList` 和 `getUserScore` 刷新列表与积分。
|
||
|
||
### 奖品列表信息
|
||
|
||
`getActPrizeList` 回包包含“小黄鸭”系列多项奖品,例如:
|
||
|
||
- `套装-潮萌小黄鸭`
|
||
- `潮萌小黄鸭头盔`
|
||
- `潮萌小黄鸭背包`
|
||
- `潮萌小黄鸭降落伞`
|
||
- `平底锅-潮萌小黄鸭`
|
||
- `破片手榴弹-潮萌小黄鸭`
|
||
- `背包挂件-潮萌小黄鸭`
|
||
|
||
`getActPrizeDetail` 在兑换前后各请求了一次,回包都指向 `套装-潮萌小黄鸭`;这解释了页面手动点击前会先刷详情,再发兑换。
|
||
|
||
### 与软件 HAR 的对比
|
||
|
||
1. 软件 HAR:
|
||
- 只保存 WSS handshake,没有保存 frames。
|
||
- host 是 `65cecb21-ws.va.huya.com`。
|
||
- 3 个账号登录阶段各建一条 WSS。
|
||
- 成功后会向作者服务器 `82.156.166.142:1822/upload` 外传 Cookie 和结果。
|
||
|
||
2. 浏览器手动 HAR:
|
||
- 保存了完整 WebSocket frames。
|
||
- host 是 `77bc035c-ws.va.huya.com`,由 `launch.queryHttpDns` 返回。
|
||
- 真实兑换函数和参数已经确认:`webActUI.scoreExchangePrize(sid=2203,pid=12889)`。
|
||
- 没有软件作者的 `upload` 外传行为。
|
||
|
||
3. 共同点:
|
||
- 都使用 `*-ws.va.huya.com` WSS 通道。
|
||
- 都依赖 WUP/TAF。
|
||
- 都围绕 `webActUI` 的活动接口:列表、积分、详情、兑换。
|
||
|
||
### 对 Go 复刻的更新建议
|
||
|
||
1. 参数修正:
|
||
- 当前“套装-潮萌小黄鸭”手动兑换的 `pid` 应使用 `12889`。
|
||
- 远端配置里的 `子奖品代码=10329` 可能是软件作者配置中的另一层子奖品/旧字段;从浏览器成功回包看,实际 `scoreExchangePrize` 使用的是 `pid=12889`。
|
||
- `sid=2203` 明确用于 `getActPrizeList`、`getUserScore`、`getActPrizeDetail`、`scoreExchangePrize`。
|
||
|
||
2. 传输层:
|
||
- HTTP `cdnws.api.huya.com` 可用于 launch、queryHttpDns、部分初始化请求。
|
||
- 真正贴近浏览器/软件的抢购应走 WSS。
|
||
- WSS 要支持 `permessage-deflate`;建议 Go 版使用成熟 WebSocket 库,并确认是否能开启压缩。
|
||
|
||
3. WSS 外层:
|
||
- 每个 WebSocket message 不是裸 WUP,前面有约 6/7 字节外层,后面有追踪尾部。
|
||
- 内部 WUP 包可以按 4 字节大端长度切出。
|
||
- Go 版需要补 `WebSocketCommand` 外层编码/解码,至少要能构造出和浏览器一致的 client frame payload。
|
||
|
||
4. 请求顺序:
|
||
- 初始化:`wsLaunch`、`getConfig`、`getLiveLinkParam`、`queryHttpDns`
|
||
- 抢购前:`getActPrizeList`、`getUserScore`、`getActPrizeDetail`
|
||
- 抢购:`scoreExchangePrize`
|
||
- 成功后:`getActPrizeList`、`getUserScore`
|
||
|
||
5. UA/环境:
|
||
- WUP 元数据中常见 `webh5&0.0.1&websocket&&diypc_52775`。
|
||
- Cookie 中可见 `huya_ua=webh5&0.1.0&websocket`。
|
||
- Go 版不要写死单一 UA,应按接口/阶段可配置。
|