42 KiB
抢购/抢兑逻辑静态分析记录
分析范围
- 项目目录:
/Users/yml/Downloads/多功能计算器2.7.unpacked.exe_export_for_ai - 样本形态:Windows EXE 的逆向导出结果,不是源码仓库。
- 主要依据:
strings.txtmemory/00586000--005CD000.txtpointers.txtfunction_index.txtdecompile/*.cdisassembly/42FC3C.asm
- 追加依据:
imgs/1.pngimgs/2.pngimgs/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
- 本记录为静态分析,没有运行程序,也没有打开浏览器。
总体结论
当前样本里确实存在“抢购/抢兑/自动兑换”相关逻辑。
这套逻辑不是普通计算器功能,而是一组虎牙活动自动监控与积分兑换流程:
- 登录/连接后维护 WUP/WebSocketCommand 通道。
- 周期性查询活动奖品列表。
- 根据界面配置的目标奖品或自定义 pid 匹配奖品。
- 命中后构造
scoreExchangePrize(cookie, pid, sid)兑换请求。 - 通过通用 HTTP 封装发送编码后的 WUP/WebSocketCommand 数据。
- 根据返回的成功、频控、下架等状态继续重试、记录或重启监控。
核心接口
嵌入 JS/WUP 协议代码在 strings.txt 中,关键函数如下:
-
scoreExchangePrize(cookie, pid, sid)- 位置:
strings.txt:617 - servant/func:
webActUI/scoreExchangePrize - 请求体:
ExchangePrizeReq - 关键字段:
pidsiduserId.lUiduserId.sCookieuserId.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 -> ExchangePrizeRespgetActPrizeList -> GetActPrizeListRespgetUserScore -> GetUserScoreRespcheckUserBindGameAccount -> CheckUserBindGameAccountRespgetUserPrizeRecords -> GetUserPrizeRecordsResp
自动监控入口
sub_43597D
位置:decompile/43597D.c
这是自动监控的外层调度器:
- 初始化账号/任务列表相关容器。
- 等待
dword_69EE3C连接对象可用:decompile/43597D.c:133
- 打印/记录线程创建成功:
decompile/43597D.c:152
- 设置自动监控标志:
dword_69EE38 = 1decompile/43597D.c:156
- 遍历账号和活动/任务组合。
- 通过连接对象的
+24方法投递任务:decompile/43597D.c:221
- 投递入口为:
sub_436737 -> sub_4363A4
- 循环间隔走
sub_4276C9:decompile/43597D.c:243
- 结束时清理并关闭监控:
dword_69EE38 = 0decompile/43597D.c:248
sub_436756
位置:decompile/436756.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 等逻辑高度同构,只是开头取不同账号/任务槽位。
关键解析字段:
statusdecompile/43E81F.c:213
msg- 实际变量名由反编译器表现为
off_58DDE9 decompile/43E81F.c:221
- 实际变量名由反编译器表现为
iRequestIddecompile/43E81F.c:230
scoredecompile/43E81F.c:239
roleNamedecompile/43E81F.c:248
areaNamedecompile/43E81F.c:260
platNamedecompile/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.valuedecompile/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:2276disassembly/42FC3C.asm:2350
命中策略
命中逻辑有两类信号:
-
自定义 pid
- 常量:
自定义pid - 地址:
0x5A479F - 汇编引用:
disassembly/42FC3C.asm:1950
- 常量:
-
自动选择奖品
- 常量:
自动选择奖品: - 地址:
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 个方法,即POSTdecompile/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 = 0disassembly/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.cdecompile/4394F1.cdecompile/43BE88.cdecompile/43E81F.cdecompile/4411B6.cdecompile/443B4D.cdecompile/4464E4.cdecompile/448E7B.cdecompile/44B812.cdecompile/44E1A9.cdecompile/4534D7.cdecompile/455E6E.cdecompile/458805.cdecompile/45B19C.cdecompile/45DB33.cdecompile/4604CA.cdecompile/462E61.cdecompile/4657F8.c
这些函数均引用同一批常量:
byte_5A3C72unk_5A471Funk_5A472Ebyte_5A4756byte_5A476Bbyte_5A4818unk_5A4C5F
可见它们是一套多账号/多槽位并发模板。
流程伪代码
启动自动监控:
初始化账号列表和活动/任务列表
等待连接对象 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
待继续确认点
-
unk_69EE18的运行时 URL 来源还未完全定位。- 静态字符串里没有直接看到固定初始化。
- 更像是连接对象、配置或运行时脚本中保存的地址。
-
sub_4968A7(index)的运行时字符串表需要进一步命名。- 目前已通过
pointers.txt和上下文确认一部分业务函数。 - 仍有若干
sub_4968A7(5/6/7/9)取出的值需要结合更多调用点还原。
- 目前已通过
-
GUI 控件字段可以继续命名。
- 已知存在自定义 pid、自动选择奖品、循环/间隔相关配置。
- 但每个控件 ID 到语义字段的映射还可以继续细分。
-
getActPrizeList的 request id 在 JS 中传入null,但 C 侧奖品列表解析落在iRequestId == "1"分支。- 可能是 WUP/脚本层默认值或反编译导出表现导致。
- 这一点如需完全确认,需要继续追
setRequestId(null)的编码结果或运行时响应样本。
一句话总结
程序的抢兑链路是:
查活动奖品列表 -> 匹配自定义 pid/目标奖品 -> 选中奖品 -> 构造 scoreExchangePrize(cookie,pid,sid)
-> POST 发送 WUP/WebSocketCommand -> 根据成功/频控/下架结果继续记录、重试或重启监控
2026-07-06 追加:截图与参考项目对照
截图里的运行态信息
imgs/1.png、imgs/2.png、imgs/3.png 能把 GUI 字段和逆向逻辑对齐:
- 主页面按钮:
查询积分查询绑定查询兑换记录兑换1次停止兑换取消定时
- 设置页面参数:
线程 = 5次数 = 300延时 = 150Cookie网页登录- 定时时间选择器
- 已登录账号页面:
- 多账号表列:
num / cookie / name / 状态 / 首个兑换 - 每个账号可勾选。
- 下方共用同一份奖品表。
- 多账号表列:
- 奖品表列:
numname剩余总量价格
- 日志示例:
登录成功你选择了:套装-朝萌小黄鸭
与反编译对应关系:
次数基本对应sub_43597D中的v51,循环条件为v46 < v51:decompile/43597D.c:73-95decompile/43597D.c:243-245
延时基本对应sub_43597D中的v50,外层循环 sleep:decompile/43597D.c:86-90decompile/43597D.c:243
线程与连接对象/worker 数量相关,sub_43597D会通过连接对象方法读取可用数量并投递任务:decompile/43597D.c:160decompile/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_WupReqtraceId = hex16:hex16:0:0- 返回 base64 编码后的
WebSocketCommand字节。
- 构造
-
scoreExchangePrize(cookie, pid, sid)ExchangePrizeReq.pid = pidExchangePrizeReq.sid = siduserId.iTokenType = 0userId.lUid = parseInt(cookie["yyuid"])userId.sCookie = cookieuserId.sHuYaUA = "web&1.0&huya"requestId = 1
-
getActPrizeList(cookie, sid)GetActPrizeListReq.userId.lUid = yyuidGetActPrizeListReq.userId.sCookie = cookieGetActPrizeListReq.userId.sHuYaUA = "web&1.0&huya"GetActPrizeListReq.sid = sid- JS 里
requestId = null - 但 C 侧奖品列表和兑换响应都落在
iRequestId == "1"分支,需要按实际响应确认。
-
getUserPrizeRecords(cookie, sid)GetUserPrizeRecordsReq.sid = siduserId.lUid = yyuiduserId.sGuid = guiduserId.sCookie = cookieuserId.sHuYaUA = "webh5&1.0.0&huya"requestId = 6
-
getUserScore(cookie, sid)requestId = 2userId.sHuYaUA = "web&1.0&huya"
-
checkUserBindGameAccount(cookie, outerActId)requestId = 17userId.sHuYaUA = "pc&1.0.0&huya"gid = "3203"
-
doLaunch(cookie)- servant/func:
launch/wsLaunch requestId = -1sUA = "webh5&0.0.1&activity&other"sAppSrc = "HUYA&ZH&2052"
- servant/func:
奖品结构字段
样本内 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 回调实际重点读取:
idnamenumpercentnewScore
截图中的展示大概率为:
总量 = num价格 = newScore剩余 = floor(num * percent / 100)或近似取整
例子:截图里 套装-朝萌小黄鸭 行显示 剩余 6000 / 总量 24001 / 价格 988,和 num * percent / 100 这类计算路径吻合:
decompile/43E81F.c:401-419disassembly/42FC3C.asm:2276disassembly/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-streamOrigin: https://zt.huya.comReferer: https://zt.huya.com/- PC Chrome UA
WSConnectParaInfobaseinfo 字段:
| 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_scoreget_act_prize_listget_user_prize_recordsscore_exchange_prizecheck_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.pycore/huya/wup_protocol.py
可直接翻译为 Go:
- TAF type:
INT8=0INT16=1INT32=2INT64=3FLOAT=4DOUBLE=5STRING1=6STRING4=7MAP=8LIST=9STRUCT_BEGIN=10STRUCT_END=11ZERO=12SIMPLE_LIST=13
- 头部:
tag < 15:单字节(tag << 4) | typetag >= 15:两字节0xF0 | type, tag
- 整数写入要做压缩:
- 0 写
ZERO - int8 范围写
INT8 - int16 范围写
INT16 - int32 范围写
INT32 - 否则写
INT64
- 0 写
- 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 字节大端总长度。
- tag1
活动结构体
文件:core/huya/activity_structs.py
Go 版最小需要先实现:
-
ActivityUserId- tag0
lUid - tag1
sGuid - tag2
sToken - tag3
sHuYaUA - tag4
sCookie - tag5
iTokenType - tag6
sDeviceInfo
- tag0
-
GetActPrizeListReq- tag0
userId - tag1
sid
- tag0
-
GetActPrizeListResp- tag0
status - tag1
msg - tag2
prizeList - tag4
tagList
- tag0
-
ScoreExchangePrizeReq- tag0
userId - tag1
sid - tag2
pid - tag3
ip - tag4
clientEnv - tag5
source - tag6
isRole
- tag0
-
ScoreExchangePrizeResp- tag0
status - tag1
msg - tag3
orderId - tag4
exchangeInfo - tag5
actPreCondition
- tag0
-
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.wsLaunchmobileui.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:702decompile/46DEE1.c:147
因此 Go 复刻时应把通道拆成两个可替换实现:
cdnws HTTP POST WUP- 参考项目已有完整实现。
- 最适合先做最小可用版本。
va.huya WSS + WebSocketCommand- 更贴近样本 JS 的外层
WebSocketCommand。 - 需要继续确认握手、verifyCookie、registerGroup、heartbeat 与业务发送封装。
- 更贴近样本 JS 的外层
Go 复刻建议蓝图
包结构
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 版本:
读取账号 Cookie
-> 解析 yyuid/guid
-> 构造 ActivityUserId
-> 构造 GetActPrizeListReq
-> WUP encode
-> POST cdnws
-> WUP decode GetActPrizeListResp
-> 匹配目标 pid/name
-> 构造 ScoreExchangePrizeReq
-> POST cdnws
-> WUP decode ScoreExchangePrizeResp
-> 记录 status/msg/orderId
抢购调度逻辑
建议按样本行为复刻,但加上明确的停止条件和退避:
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 版可以同时记录直接字段和估算字段,避免误判。
- 样本 C 侧使用
- 频控:
- 样本识别
操作频繁,请稍后再试后仍会继续后续重试路径。 - Go 版建议记录频控并按账号退避,避免无意义高频请求。
- 样本识别
新增待确认点
-
unk_69EE18仍未定位到静态写入点。- 目前只看到它作为
sub_428C41的 URL 参数被读取:decompile/43E81F.c:513decompile/43E81F.c:621
- 明文有
wss://65cecb21-ws.va.huya.com/:strings.txt:702decompile/46DEE1.c:147
- 但参考项目 HTTP 版使用
https://cdnws.api.huya.com/?baseinfo=...。 - 需要实际抓一次样本运行请求,或继续追连接对象初始化,才能最终确认业务发送 URL。
- 目前只看到它作为
-
线程控件的赋值点还未完全映射。- 样本有 18 个同构槽位回调。
- 截图中设置为 5。
sub_43597D通过连接对象方法读取可用数量和投递任务,但 UI 控件到连接池容量的路径还需继续找。
-
getActPrizeList的 requestId。- JS 传
null。 Wup.prototype.setRequestId=function(t){this.iRequestId=t||++this.iRequestId}。- 所以
null会触发自增,初始值为 0 时会变成 1。 - 这解释了 C 侧奖品列表落入
iRequestId == "1"分支。
- JS 传
-
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/uploadhttp://115.159.215.151:1822/uploadhttp://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+ 条无效的“兑换货币不足”。
对抢购逻辑的更新判断
-
远端配置确认了这次奖品参数:
- 活动 id:
17096 - 兑换代码/父奖品或活动奖品标识:
2203 - 子奖品 id:
10329
- 活动 id:
-
实际样本运行确认使用 WSS:
- 静态字符串里的
wss://65cecb21-ws.va.huya.com/不是备用字符串。 - 登录阶段按账号建立 WSS,抢购阶段复用连接发请求。
- 静态字符串里的
-
HAR 未保存 WSS 帧:
- 不能从当前 HAR 直接提取
WebSocketCommand、iRequestId或兑换请求 WUP body。 - 仍需从反编译、JS/WUP 定义、参考 Python 项目或重新抓 WebSocket frames 来补齐。
- 不能从当前 HAR 直接提取
-
样本抢购策略:
- 提前约 3 秒开始轮询/尝试。
- 多账号并发。
- 成功后继续重试,直到后续查询/循环结束。
- 频控文案不是致命错误,样本会继续尝试。
-
Go 复刻建议:
- 账号维度维护持久 WSS 连接,而不是每次兑换新建 HTTP 请求。
- 抢购开始时间可设置为官方时间前 2-3 秒,允许“暂未开放”继续重试。
- 每个账号成功一次后立即停止该账号对同一奖品的兑换重试。
- “操作频繁”做短退避;“兑换货币不足”可视为该账号已成功或积分不足,停止重试。
- 禁止实现样本的远端
upload上报;只保留本地脱敏日志。
仍需补证
- 重新抓包时需要确认工具能保存 WebSocket binary frames;当前 HAR 只有 WSS handshake。
- 如果抓不到帧,可以在 Go 版用已实现的 WUP 编码对照参考项目,先构造:
webActUI.getActPrizeListwebActUI.getUserScorewebActUI.scoreExchangePrize
- 然后补样本 WSS 外层:
WebSocketCommandverifyCookiedoLaunchregisterGroup- 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 文本:
cdnws.api.huya.com的request.postData.text和response.content.text是 WUP 二进制包的 base64。wss://77bc035c-ws.va.huya.com/的request.postData.text/response.content.text是原始 WebSocket wire bytes 的 base64。- WebSocket client -> server 帧:
opcode=2masked=1RSV1=1- 启用了
permessage-deflate
- 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=17096gameIdList=cjmlivePlatId=huyacode、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=14servant=webActUIfunc=scoreExchangePrizestatus=200msg=请求成功orderId=sOrderId_2203_12889_<uid>_1783328404_957exchangeInfo.pid=12889exchangeInfo.exchangeScore=988exchangeInfo.desc=兑换奖品:套装-潮萌小黄鸭
1783328404 转成本地时间为 2026-07-06 17:00:04,说明这次浏览器手动兑换成功发生在 17:00:04 左右。
该回包进一步确认:
- 本次手动小黄鸭兑换参数是
sid=2203、pid=12889。 - 小黄鸭套装消耗积分是
988。 - 兑换成功后页面会继续请求
getActPrizeList和getUserScore刷新列表与积分。
奖品列表信息
getActPrizeList 回包包含“小黄鸭”系列多项奖品,例如:
套装-潮萌小黄鸭潮萌小黄鸭头盔潮萌小黄鸭背包潮萌小黄鸭降落伞平底锅-潮萌小黄鸭破片手榴弹-潮萌小黄鸭背包挂件-潮萌小黄鸭
getActPrizeDetail 在兑换前后各请求了一次,回包都指向 套装-潮萌小黄鸭;这解释了页面手动点击前会先刷详情,再发兑换。
与软件 HAR 的对比
-
软件 HAR:
- 只保存 WSS handshake,没有保存 frames。
- host 是
65cecb21-ws.va.huya.com。 - 3 个账号登录阶段各建一条 WSS。
- 成功后会向作者服务器
82.156.166.142:1822/upload外传 Cookie 和结果。
-
浏览器手动 HAR:
- 保存了完整 WebSocket frames。
- host 是
77bc035c-ws.va.huya.com,由launch.queryHttpDns返回。 - 真实兑换函数和参数已经确认:
webActUI.scoreExchangePrize(sid=2203,pid=12889)。 - 没有软件作者的
upload外传行为。
-
共同点:
- 都使用
*-ws.va.huya.comWSS 通道。 - 都依赖 WUP/TAF。
- 都围绕
webActUI的活动接口:列表、积分、详情、兑换。
- 都使用
对 Go 复刻的更新建议
-
参数修正:
- 当前“套装-潮萌小黄鸭”手动兑换的
pid应使用12889。 - 远端配置里的
子奖品代码=10329可能是软件作者配置中的另一层子奖品/旧字段;从浏览器成功回包看,实际scoreExchangePrize使用的是pid=12889。 sid=2203明确用于getActPrizeList、getUserScore、getActPrizeDetail、scoreExchangePrize。
- 当前“套装-潮萌小黄鸭”手动兑换的
-
传输层:
- HTTP
cdnws.api.huya.com可用于 launch、queryHttpDns、部分初始化请求。 - 真正贴近浏览器/软件的抢购应走 WSS。
- WSS 要支持
permessage-deflate;建议 Go 版使用成熟 WebSocket 库,并确认是否能开启压缩。
- HTTP
-
WSS 外层:
- 每个 WebSocket message 不是裸 WUP,前面有约 6/7 字节外层,后面有追踪尾部。
- 内部 WUP 包可以按 4 字节大端长度切出。
- Go 版需要补
WebSocketCommand外层编码/解码,至少要能构造出和浏览器一致的 client frame payload。
-
请求顺序:
- 初始化:
wsLaunch、getConfig、getLiveLinkParam、queryHttpDns - 抢购前:
getActPrizeList、getUserScore、getActPrizeDetail - 抢购:
scoreExchangePrize - 成功后:
getActPrizeList、getUserScore
- 初始化:
-
UA/环境:
- WUP 元数据中常见
webh5&0.0.1&websocket&&diypc_52775。 - Cookie 中可见
huya_ua=webh5&0.1.0&websocket。 - Go 版不要写死单一 UA,应按接口/阶段可配置。
- WUP 元数据中常见