Files
huya-go/purchase_logic_analysis.md
T
2026-07-06 18:47:10 +08:00

42 KiB
Raw Blame History

抢购/抢兑逻辑静态分析记录

分析范围

  • 项目目录:/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

这是继续监控线程入口:

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

可见它们是一套多账号/多槽位并发模板。

流程伪代码

启动自动监控:
  初始化账号列表和活动/任务列表
  等待连接对象 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) 的编码结果或运行时响应样本。

一句话总结

程序的抢兑链路是:

查活动奖品列表 -> 匹配自定义 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 复刻建议蓝图

包结构

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 版可以同时记录直接字段和估算字段,避免误判。
  • 频控:
    • 样本识别 操作频繁,请稍后再试 后仍会继续后续重试路径。
    • 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,应按接口/阶段可配置。