新版
This commit is contained in:
parent
5a94ac454e
commit
4b342ff9c2
174
findings.md
174
findings.md
@ -302,6 +302,180 @@
|
|||||||
- 该参考配置不等于 APP 的精确 Aegon/Cronet 指纹。若 A/B 仍返回 705,应采集
|
- 该参考配置不等于 APP 的精确 Aegon/Cronet 指纹。若 A/B 仍返回 705,应采集
|
||||||
APP 实际 ALPN/JA3/HTTP2 SETTINGS 后再替换 profile,不能据此倒推缺业务字段。
|
APP 实际 ALPN/JA3/HTTP2 SETTINGS 后再替换 profile,不能据此倒推缺业务字段。
|
||||||
|
|
||||||
|
## 2026-08-27 - 签到/签到看视频 H5 链路 → ksjsb.py
|
||||||
|
|
||||||
|
- 新抓包 `签到_2026_08_27_14_50_32.har`(428 条)/`签到看视频_2026_08_27_14_51_46.har`
|
||||||
|
(307 条),App ksNebula/14.6.50.11735、Yoda WebView Chrome/121、userId=5402308233。
|
||||||
|
- 签到链(全部 Yoda WebView H5,非原生 API):
|
||||||
|
- `overview/popup` 响应 `data.unionSignInPopup.button.eventTrackingLogInfo`
|
||||||
|
(deliverOrderId=5118, EXTERNAL_SIGN_POPUP)是 `signIn/report` 的
|
||||||
|
`eventTrackingLogInfo` 来源;上报前在 `extParams` 追加 `source`。
|
||||||
|
- `signIn/report` 为 GET + query eventTrackingLogInfo,响应
|
||||||
|
`reportRewardResult.eventTrackingAwardInfo.awardInfo[].amount` 即奖励。
|
||||||
|
- 签到看视频链:`signIn/showPopup` 响应按钮(text=去看视频)的
|
||||||
|
`eventTrackingLogInfo`(deliverOrderId=2044, SIGN_IN_BUTTON_60038_RPC,
|
||||||
|
businessPriceUniqueId 服务端每次下发)→ `POST
|
||||||
|
/rest/wd/usergrowth/encourage/matrix/resource/action`
|
||||||
|
body=`{"actionType":1,"resourceSlotInfo":<slot+source>}`。
|
||||||
|
- 看视频奖励由第三方 CPX 广告(通义 TONGYI)S2S 回调发放
|
||||||
|
(`ad.partner.gifshow.com/track/activate?callback=...&event_type=84`),
|
||||||
|
明文抓包中无客户端领奖请求;`businessPriceUniqueId` 是回调绑定键。
|
||||||
|
- H5 sig3 纯算链对新端点交叉验证:抓包 query+cookie(document.cookie
|
||||||
|
编码值,如 `mod=OnePlus%28PJZ110%29`)重建 sign_input,CRC32 与
|
||||||
|
showPopup/report/popup(含 notificationKey)/signIn_resource/matrix action
|
||||||
|
(POST+JSON body)共 5 类真实 sig3 内嵌值完全一致;counter 进程级
|
||||||
|
101→121 单调递增。
|
||||||
|
- `multi-az-hebei-webh5.kuaishou.com` 是抓包代理的域名映射,真实
|
||||||
|
Host/Referer/Origin 均为 `nebula.kuaishou.com`。
|
||||||
|
- 交付:`ksjsb.py`(独立运行,不读 HAR;复用 core H5 纯算链)+
|
||||||
|
`tests/test_ksjsb.py`(8 用例:URL/键序黄金值、CRC 交叉验证、body
|
||||||
|
逐字节一致、sig3 往返+counter 递增、dry-run 无网络冒烟)。
|
||||||
|
|
||||||
|
## 2026-08-27 - 开宝箱/看广告 HAR → ksjsb.py 重写为顺序任务脚本
|
||||||
|
|
||||||
|
- 新增抓包: `开宝箱_15_01_21`、`开宝箱看广告_15_02_14`、`看广告得金币+打开
|
||||||
|
广告详情_15_04_32`(与上午同设备/账号会话,sig3 counter 连续递增到 162)。
|
||||||
|
- 宝箱冷却规则(treasureBox/info 响应): `remainSeconds=0 & status=3` 可开,
|
||||||
|
`remainSeconds=300 & status=2` 冷却中; `rewardCount` 当前宝箱金币。
|
||||||
|
- 开宝箱链: 任务列表宝箱挂件位(dailyTasks[].extParam.eventTrackingLogInfo,
|
||||||
|
taskId 20022)→ matrix/resource action(ext 追加 threshold/thresholdTaskId/
|
||||||
|
source)→ treasureBox/report(body 复用 info 响应 eventTrackingLogInfo
|
||||||
|
(taskId 20035)+source); 响应 `data.title.rewardCount` 即到账金币,
|
||||||
|
开箱后按钮 linkUrl(base64)=matrixMixedAd 广告跳转参数
|
||||||
|
(11101/100024064/20346/606,与 main.py AD_FETCH_FALLBACK_SCENE 一致)。
|
||||||
|
- 看广告得金币: overview/tasks dailyTasks[0](TASK_LIST_17_672_CD_PROGRESSING)
|
||||||
|
的 eventTrackingLogInfo 先做 matrix 点击上报, 再走原生广告链(拉取/播放/
|
||||||
|
上报全部在 tm-s.xxpkg.com:13500 隧道内,明文不可见;复用 main.py 已在线
|
||||||
|
验证的 reward/mixed/ad + ad/task/report 机器)。"打开广告详情"为第三方
|
||||||
|
广告 SDK 落地行为(meituan/qianwen),不在脚本范围。
|
||||||
|
- CRC 交叉验证新增 6 端点全对: treasureBox/info(可开/冷却两种状态)、
|
||||||
|
treasureBox/report(POST+JSON body)、matrix action(宝箱点击/开箱后/
|
||||||
|
任务点击)。body 键序细节: matrix 点击的 extParams 中 threshold/
|
||||||
|
thresholdTaskId 在 source 之前(抓包 #115)。
|
||||||
|
- `ksjsb.py` 重写为无 CLI 顺序任务脚本: 余额 → task_list(广告上下文)→
|
||||||
|
签到(showPopup 查 todaySigned,未签才 report)→ 开宝箱(冷却检查)→
|
||||||
|
看广告 random(20,30) 次(冷却等待重试,拉取失败提前结束,每 5 次刷新
|
||||||
|
任务列表)→ 最终余额。复用 main.KsNebulaClient 全部机器,子类 KsJsB
|
||||||
|
注册 2026-08-27 新指纹 header 模板(Chrome/121, per-run PxSessionId)。
|
||||||
|
- `tests/test_ksjsb.py` 重写为 11 用例: 6 个 URL/body 黄金值、7 端点 CRC
|
||||||
|
交叉验证、sig3 往返+counter 递变、资源位提取、顺序流程 dry-run 冒烟。
|
||||||
|
|
||||||
|
## 2026-08-27 - ksjsb.py 改为完全单文件(零项目内依赖)
|
||||||
|
|
||||||
|
- 用户要求: 不依赖 main.py/core,以 HAR 为准。重写为单文件(623KB),
|
||||||
|
唯一依赖 `requests`;零 `main`/`core` import(已验证)。
|
||||||
|
- 关键工程点: KWSG T1/T2 分组密码表(A=10400/true-mode sign, B=10418
|
||||||
|
sig3)无法纯算重建,以 base64 内嵌(147456+41216 字节 ×2);其余算法
|
||||||
|
全部内联: 10400 ZT envelope、10418 digest/state/时间驱动 seed、reward
|
||||||
|
sign、xfalcon(自定义 IV BLAKE2s + $TE_)、Weapon kaw/kas、KWF AES+
|
||||||
|
自定义 base64 kww、WebWeapon kws 票据、H5 sig3 envelope+双层 SHA、
|
||||||
|
API sig/tokensig/signed_api_url、reward strE、广告素材提取/上报 biz。
|
||||||
|
- 算法黄金值回归(与 core 逐位一致): 4 张表==bin 文件、10418 digest、
|
||||||
|
reward sign、10400 raw、report encData wrapper、xfalcon value、H5
|
||||||
|
sign_input/digest field/from_fields、kww、kas、signed_api_url(含
|
||||||
|
sig3; 需固定 unix_time 才可黄金化,已加透传参数)。
|
||||||
|
- H5 sig3 counter 依 2026-08-27 抓包改为递增(100+rand 起步,真机
|
||||||
|
101→162);elapsed=(开机秒<<16)|ms;state=0x10000|16bit 随机;seed
|
||||||
|
每次运行随机(新页面会话)。
|
||||||
|
- 传输层差异(已标注 TODO): API 请求用 requests/HTTP1.1,真机为
|
||||||
|
OkHttp/Cronet;Accept-Encoding 降级 gzip,deflate(无 brotli)。
|
||||||
|
- `tests/test_ksjsb.py` 只 import ksjsb(17 用例,单文件自洽证明):
|
||||||
|
6 URL/body 黄金值、6 端点摘要字段交叉验证、5 组算法黄金值、
|
||||||
|
sig3 往返+counter 递增、资源位/场景提取、dry-run 全流程冒烟。
|
||||||
|
全套件 289 用例,仍只有 4 个存量失败(test_main 导入、sms_login
|
||||||
|
captcha auto_solve 签名),无新增。
|
||||||
|
|
||||||
|
## 2026-08-27 - 在线首跑复盘 + 诊断日志
|
||||||
|
|
||||||
|
- 用户实测(账号 userId=1579452490/vivo): basicInfo/task_list OK;宝箱
|
||||||
|
matrix 点击 result=6001 但 treasureBox/report **开箱成功 +539 金币**
|
||||||
|
(证明点击上报非必需);广告 matrix 点击与 reward/mixed/ad 拉取均
|
||||||
|
result=6001,循环终止;签到 showPopup 无资源位(弹窗已关闭场景)。
|
||||||
|
- 历史抓包中 matrix/ad fetch 全部 result=1,6001 为脚本请求特有的
|
||||||
|
业务/风控拒绝;jadx/HAR 中无 6001 定义。候选根因:当日广告任务已完成
|
||||||
|
(该账号今日已重度使用)/传输指纹(requests HTTP1.1)/uQaTag 为 7 月
|
||||||
|
旧设备样本。待用诊断日志定位。
|
||||||
|
- 脚本改进:
|
||||||
|
1. 每请求 JSONL 诊断日志(out/ksjsb_replay_*.jsonl,完整 URL/body/
|
||||||
|
响应,仅脱敏 api_st/token 等登录票据;KS_RECORD_FILE=0 关闭)。
|
||||||
|
2. 签到判定: showPopup.todaySigned -> 弹窗关闭时回退 overview/popup
|
||||||
|
的 unionSignInPopup 资源位 -> 任务列表 20022/60038 任务在
|
||||||
|
finishedTaskList 则确认已签。
|
||||||
|
3. matrix 点击降级 best-effort: 失败一次即停发(避免重复触发风控),
|
||||||
|
不影响领奖流程。
|
||||||
|
4. 广告拉取 6001 时输出明确诊断(任务不可用/风控)并终止循环。
|
||||||
|
5. API body 字段序对齐真机抓包: cs, client_key, videoModelCrowdTag,
|
||||||
|
os, kuaishou.api_st, uQaTag, token(此前 api_st 在 uQaTag 后)。
|
||||||
|
|
||||||
|
## 2026-08-27 - 6001 根因定位(诊断日志 diff)
|
||||||
|
|
||||||
|
- 第二次在线跑(任务可用: 看广告任务 status=1 completed=0)仍 6001,
|
||||||
|
排除"任务已完成"。JSONL diff 发现两处与真机抓包的实质偏差:
|
||||||
|
1. **API query 多了 `sid` 和 `deviceName`**: 真机 reward/mixed/ad 与
|
||||||
|
ad/task/report 的 57 键参数集从不包含这两键(全部历史 HAR 实证);
|
||||||
|
系 main.py `_api_params` 从 cookie 盲目带入的偏差。已移除并加
|
||||||
|
参数集回归测试。
|
||||||
|
2. **`uQaTag` 设备错配**: 旧值 `DP:3hX9ONf4GgQ...` 是 7 月 OnePlus
|
||||||
|
设备的 QoE 采集态标签,现配 vivo 设备身份。2026-07-08 抓包实证
|
||||||
|
QoE 未采集的合法回退形态 `1##DP:null#ecBl:00#ecPp:--#cmNt:-1#
|
||||||
|
cmMnsl:-0` 被 result=1 接受且设备无关 —— 已改为默认值,
|
||||||
|
`KS_UQA_TAG` 可覆盖。7 月 main.py 在线验证通过时账号设备恰好与
|
||||||
|
uQaTag 同为 OnePlus,佐证设备绑定假设。
|
||||||
|
- matrix 点击 6001 与此独立(H5 端点,无 uQaTag);保持 best-effort。
|
||||||
|
- 待用户下次在线验证两处修复是否消除 ad fetch 6001。
|
||||||
|
|
||||||
|
## 2026-08-27 - 6001 第三轮: 传输指纹 + 设备一致性修复
|
||||||
|
|
||||||
|
- 第二轮修复(去 sid/deviceName、uQaTag null 形态)经日志确认已生效但
|
||||||
|
ad fetch 仍 6001 —— 排除参数集/标签问题,剩余嫌疑集中在传输指纹与
|
||||||
|
设备一致性。第三轮修复:
|
||||||
|
1. API 请求默认走 **OkHttp4/Android10 TLS 指纹 + HTTP/2**
|
||||||
|
(curl_cffi 0.15 已内联封装, 不可用时回退 requests,
|
||||||
|
KS_API_TRANSPORT=requests 可强制 A/B)。
|
||||||
|
2. `X-Client-Info.model` 从 cookie `mod` 括号内型号动态生成
|
||||||
|
(vivo(V2408A)->V2408A), 修复此前硬编码 PJZ110 与 vivo 设备的
|
||||||
|
头部错配。
|
||||||
|
3. 签名参数追加序对齐抓包: sig, __NS_sig3, __NS_xfalcon,
|
||||||
|
__NStokensig(此前 tokensig 在 xfalcon 前)。
|
||||||
|
- 黄金值回归: signed_api_url 的 sig/sig3 值不受参数序调整影响(逐位
|
||||||
|
不变); 19 项单文件测试 + 全套件 291 用例无新增失败。
|
||||||
|
- 待第四轮在线验证。若 okhttp 指纹下仍 6001, 剩余候选:
|
||||||
|
strE 内嵌 network_ip(硬编码 172.31.230.147, 可 KS_AD_NETWORK_IP
|
||||||
|
覆盖为出口公网 IP)、egid↔did 绑定状态、10418 seed/counter 模式。
|
||||||
|
|
||||||
|
## 2026-08-27 - 6001 第四轮: 版本对齐 + API 金丝雀探针
|
||||||
|
|
||||||
|
- okhttp 指纹 + X-Client-Info 修复后仍 6001。第三轮日志排查发现:
|
||||||
|
`appver=14.5.50.11631/ver=14.5`(cookie 7 月导出的旧版本号, 真机
|
||||||
|
当前 14.6.50.11735)与 `kcv=1627`(默认值, cookie 实为 1630)。
|
||||||
|
- 修复: API `appver/ver` 默认对齐 2026-08-27 抓包版本(14.6.50.11735,
|
||||||
|
KS_APPVER 可覆盖), `kcv` 改从 cookie 覆盖; strE appInfo.version 随
|
||||||
|
api_params 自动对齐。
|
||||||
|
- 新增 **styleTemplate 金丝雀探针**(`/rest/e/load/styleTemplate`,
|
||||||
|
2026-07 抓包实证 result=1): 同传输/sig3/xfalcon 管线但无鉴权
|
||||||
|
token、无加密体, 真机形态连 __NStokensig 都不带(已按抓包实现)。
|
||||||
|
广告步骤先跑探针 —— 探针过而 ad fetch 败 => 投放层拒绝
|
||||||
|
(api_st/strE/任务绑定); 探针也败 => 传输/签名层。
|
||||||
|
- fetch_ad 的 JSONL notes 现记录 strE 明文/scene/businessId/
|
||||||
|
neoParamsLen, 下次失败可直接检查加密体内容。
|
||||||
|
|
||||||
|
## 2026-08-27 - 6001 结案: 根因=旧 api_st; gzip 解压为最后一坑
|
||||||
|
|
||||||
|
- 用户用 `tools/sms_login_cli.py` 重新登录获得新鲜 api_st/h5_st/设备
|
||||||
|
(did=ANDROID_63fdacb5388e2572 + 在线注册 egid)并更新 .env 后:
|
||||||
|
- styleTemplate 探针 HTTP 200 + 12.7KB 模板 —— **实为成功**,此前
|
||||||
|
`result=None` 是 curl_cffi(okhttp 指纹)手动 `Accept-Encoding: gzip`
|
||||||
|
时 libcurl 不自动解压导致 `json()` 失败的解析 bug。
|
||||||
|
- 修复: `_request` 按 `\x1f\x8b` 魔数兜底 `gzip.decompress`
|
||||||
|
(requests 自动解压不受影响)。
|
||||||
|
- **广告全链路打通**: fetch result=1(16KB gzip 素材, 19.1s)→
|
||||||
|
按素材时长等待 → report result=1 `neoAmount:497金币` 实际到账。
|
||||||
|
- 6001 根因链(多因叠加, 逐轮排除): 旧 cookie 的 api_st 过期(H5 用
|
||||||
|
h5_st 所以全通, API 全挂)+ 传输指纹 + 版本号滞后 + uQaTag 设备
|
||||||
|
错配 + 参数集多余键。新会话 + okhttp 指纹 + 版本对齐后消除。
|
||||||
|
- 端到端验证后 19+291 测试回归无新增失败。诊断体系(JSONL 探针/
|
||||||
|
strE notes)保留, 供后续问题定位。
|
||||||
|
|
||||||
---
|
---
|
||||||
*每执行2次查看/浏览器/搜索操作后更新此文件*
|
*每执行2次查看/浏览器/搜索操作后更新此文件*
|
||||||
*防止视觉信息丢失*
|
*防止视觉信息丢失*
|
||||||
|
|||||||
363
tests/test_ksjsb.py
Normal file
363
tests/test_ksjsb.py
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""ksjsb.py 单文件脚本的行为锁定测试。
|
||||||
|
|
||||||
|
只 import ksjsb 一个项目文件(证明单文件自洽), 黄金值以字面量固化, 来自:
|
||||||
|
- 2026-08-27 五份抓包(签到/签到看视频/开宝箱/开宝箱看广告/看广告得金币):
|
||||||
|
URL query 键序编码、JSON body、真实 sig3 内嵌摘要字段(CRC 交叉验证)。
|
||||||
|
- core/ 算法模块的已知输出(10418/10400/xfalcon/H5 sig3/kww/kas 黄金值)。
|
||||||
|
|
||||||
|
ksjsb.py 运行时不读取 HAR, 也不依赖 main/core; 本测试同样只依赖 ksjsb。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import contextlib
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
from ksjsb import (
|
||||||
|
KsJsB,
|
||||||
|
build_h5_sign_input,
|
||||||
|
compact_json,
|
||||||
|
h5_sig3_digest_field,
|
||||||
|
parse_h5_sig3,
|
||||||
|
with_source,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 抓包 cookie 中参与 sign_input 的子集(H5_COOKIE_KEYS, 非敏感设备字段)。
|
||||||
|
# 真实 WebView 对 document.cookie 的编码值(%28/+ 等)签名, 这里保持原样。
|
||||||
|
CAPTURED_COOKIE = (
|
||||||
|
"kpn=NEBULA; kpf=ANDROID_PHONE; userId=5402308233; did=ANDROID_f05497e9cef09a7f; "
|
||||||
|
"c=OPPO; appver=14.6.50.11735; language=zh-cn; mod=OnePlus%28PJZ110%29; did_tag=0; "
|
||||||
|
"egid=DFP280DA456E3BBF93A99727CB5A97D727E442E38694913A197D1C201C5CF982; "
|
||||||
|
"oDid=ANDROID_46a032e0a2af8184; androidApiLevel=36; newOc=OPPO; browseType=3; "
|
||||||
|
"socName=Qualcomm+Snapdragon+8750; ftt=; abi=arm64; userRecoBit=0; device_abi=arm64; "
|
||||||
|
"grant_browse_type=AUTHORIZED; iuid=; rdid=ANDROID_741de4351c44850d; "
|
||||||
|
"oaid=700ED68650EF49ECB686B2AAA9994C1Dca31c779ee5acb7d4c3b03337599a591"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- 真实请求 query(去掉 __NS_sig3, 保留原始顺序) ----
|
||||||
|
SHOW_POPUP_QUERY = (
|
||||||
|
"sigCatVer=1&source=bottom_guide_first&ladderRedPackGuide=false"
|
||||||
|
"&bizParams=%7B%22source%22%3A%22bottom_guide_first%22%2C%22ladderRedPackGuide%22%3Afalse%7D"
|
||||||
|
)
|
||||||
|
REPORT_QUERY = (
|
||||||
|
"sigCatVer=1&eventTrackingLogInfo=%7B%22deliverOrderId%22%3A%225118%22%2C%22eventTrackingTaskId%22"
|
||||||
|
"%3A20022%2C%22resourceId%22%3A%22externalFeed_externalSignPopup%22%2C%22extParams%22%3A%7B%22signIn"
|
||||||
|
"ThresholdTaskType%22%3A0%2C%22autoSignInSuccess%22%3Afalse%2C%22source%22%3A%22bottom_guide_first%22"
|
||||||
|
"%7D%2C%22resourceFirstLevelName%22%3A%22EXTERNAL_FEED%22%2C%22resourceSecondLevelName%22%3A"
|
||||||
|
"%22EXTERNAL_SIGN_POPUP%22%7D"
|
||||||
|
)
|
||||||
|
TREASURE_INFO_QUERY = "sigCatVer=1&source=bottom_guide_first"
|
||||||
|
|
||||||
|
# ---- 真实 sig3 样本(摘要字段交叉验证) ----
|
||||||
|
CAPTURED_SIG3 = {
|
||||||
|
"show_popup": "4151162679c8e853bd1d741e19181571a0bed5eefa41a30358df0e0e08080b0a3515",
|
||||||
|
"sign_in_report": "9d8dcafaa514348f61c1a9c2c5c474fbd628113d269d7fdf1771d2d2d4d4d7d6e9c9",
|
||||||
|
"treasure_info": "5b4b0c3c63d2f249a70780040302289a4b2effd11e58b9193b6f1414121211102f0f",
|
||||||
|
"treasure_report": "8797d0e0bf0e2e957bdb5ed8dfde440eb392cd11c28465c56be7c8c8cececdccf3d3",
|
||||||
|
"matrix_treasure": "9181c6f6a91838836dcd4fcec9c883c4a4d83813d49273d389d3deded8d8dbdae5c5",
|
||||||
|
"matrix_task": "d0c087b7e85979c22c8c2c8f8889dcd94f6e33ba97d332924b7d9f9f99999a9ba484",
|
||||||
|
}
|
||||||
|
|
||||||
|
TREASURE_TRACKING = {
|
||||||
|
"eventTrackingTaskId": 20035,
|
||||||
|
"resourceId": "externalFeed_externalTreasureChestWidget",
|
||||||
|
"extParams": {"isServerRecordClickAction": True},
|
||||||
|
"deliverOrderId": "5128",
|
||||||
|
"resourceFirstLevelName": "EXTERNAL_FEED",
|
||||||
|
"resourceSecondLevelName": "EXTERNAL_TREASURE_CHEST_WIDGET",
|
||||||
|
}
|
||||||
|
TASKLIST_TREASURE_SLOT = {
|
||||||
|
"eventTrackingTaskId": 20022,
|
||||||
|
"resourceId": "externalFeed_externalTreasureChestWidget",
|
||||||
|
"extParams": {"isServerRecordClickAction": True},
|
||||||
|
"deliverOrderId": "5128",
|
||||||
|
"resourceFirstLevelName": "EXTERNAL_FEED",
|
||||||
|
"resourceSecondLevelName": "EXTERNAL_TREASURE_CHEST_WIDGET",
|
||||||
|
}
|
||||||
|
TASKLIST_AD_SLOT = {
|
||||||
|
"deliverOrderId": "422",
|
||||||
|
"materialKey": "TASK_LIST_17_672_CD_PROGRESSING",
|
||||||
|
"eventTrackingTaskId": 17,
|
||||||
|
"resourceId": "earnPage_taskList_1",
|
||||||
|
"extParams": {
|
||||||
|
"isServerRecordClickAction": True,
|
||||||
|
"businessPriceUniqueId": "EPuAgIDgi5CfChiuBCDBxpyQhDQo2O7EtgM=",
|
||||||
|
},
|
||||||
|
"resourceFirstLevelName": "EARN_PAGE",
|
||||||
|
"resourceSecondLevelName": "TASK_LIST",
|
||||||
|
}
|
||||||
|
CAPTURED_OAID = "700ED68650EF49ECB686B2AAA9994C1Dca31c779ee5acb7d4c3b03337599a591"
|
||||||
|
|
||||||
|
GOLDEN_TREASURE_BODY = (
|
||||||
|
'{"imei":"","idfa":"","oaid":"' + CAPTURED_OAID + '","eventTrackingLogInfo":"{\\"eventTrackingTaskId\\":20035,'
|
||||||
|
'\\"resourceId\\":\\"externalFeed_externalTreasureChestWidget\\",\\"extParams\\":{\\"isServerRecordClickAction\\":true,'
|
||||||
|
'\\"source\\":\\"bottom_guide_first\\"},\\"deliverOrderId\\":\\"5128\\",\\"resourceFirstLevelName\\":\\"EXTERNAL_FEED\\",'
|
||||||
|
'\\"resourceSecondLevelName\\":\\"EXTERNAL_TREASURE_CHEST_WIDGET\\"}"}'
|
||||||
|
)
|
||||||
|
GOLDEN_MATRIX_TREASURE_BODY = (
|
||||||
|
'{"actionType":1,"resourceSlotInfo":{"eventTrackingTaskId":20022,'
|
||||||
|
'"resourceId":"externalFeed_externalTreasureChestWidget","extParams":{"isServerRecordClickAction":true,'
|
||||||
|
'"threshold":0,"thresholdTaskId":0,"source":"bottom_guide_first"},"deliverOrderId":"5128",'
|
||||||
|
'"resourceFirstLevelName":"EXTERNAL_FEED","resourceSecondLevelName":"EXTERNAL_TREASURE_CHEST_WIDGET"}}'
|
||||||
|
)
|
||||||
|
GOLDEN_MATRIX_AD_BODY = (
|
||||||
|
'{"actionType":1,"resourceSlotInfo":{"deliverOrderId":"422","materialKey":"TASK_LIST_17_672_CD_PROGRESSING",'
|
||||||
|
'"eventTrackingTaskId":17,"resourceId":"earnPage_taskList_1","extParams":{"isServerRecordClickAction":true,'
|
||||||
|
'"businessPriceUniqueId":"EPuAgIDgi5CfChiuBCDBxpyQhDQo2O7EtgM=","source":"bottom_guide_first"},'
|
||||||
|
'"resourceFirstLevelName":"EARN_PAGE","resourceSecondLevelName":"TASK_LIST"}}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_client() -> KsJsB:
|
||||||
|
return KsJsB(CAPTURED_COOKIE, "", timeout=5, dry_run=True)
|
||||||
|
|
||||||
|
|
||||||
|
def query_without_sig3(url: str) -> str:
|
||||||
|
return "&".join(p for p in url.split("?", 1)[1].split("&") if not p.startswith("__NS_sig3="))
|
||||||
|
|
||||||
|
|
||||||
|
class UrlAndBodyTests(unittest.TestCase):
|
||||||
|
def test_show_popup_query_matches_capture(self):
|
||||||
|
self.assertEqual(query_without_sig3(make_client().sign_in_show_popup().url), SHOW_POPUP_QUERY)
|
||||||
|
|
||||||
|
def test_sign_in_report_query_matches_capture(self):
|
||||||
|
tracking = {
|
||||||
|
"deliverOrderId": "5118",
|
||||||
|
"eventTrackingTaskId": 20022,
|
||||||
|
"resourceId": "externalFeed_externalSignPopup",
|
||||||
|
"extParams": {"signInThresholdTaskType": 0, "autoSignInSuccess": False},
|
||||||
|
"resourceFirstLevelName": "EXTERNAL_FEED",
|
||||||
|
"resourceSecondLevelName": "EXTERNAL_SIGN_POPUP",
|
||||||
|
}
|
||||||
|
self.assertEqual(query_without_sig3(make_client().sign_in_report(tracking).url), REPORT_QUERY)
|
||||||
|
|
||||||
|
def test_treasure_info_query_matches_capture(self):
|
||||||
|
self.assertEqual(query_without_sig3(make_client().treasure_box_info().url), TREASURE_INFO_QUERY)
|
||||||
|
|
||||||
|
def test_treasure_report_body_matches_capture(self):
|
||||||
|
self.assertEqual(
|
||||||
|
compact_json({
|
||||||
|
"imei": "",
|
||||||
|
"idfa": "",
|
||||||
|
"oaid": CAPTURED_OAID,
|
||||||
|
"eventTrackingLogInfo": compact_json(with_source(TREASURE_TRACKING)),
|
||||||
|
}),
|
||||||
|
GOLDEN_TREASURE_BODY,
|
||||||
|
)
|
||||||
|
record = make_client().treasure_box_report(TREASURE_TRACKING)
|
||||||
|
self.assertEqual(query_without_sig3(record.url), "sigCatVer=1")
|
||||||
|
|
||||||
|
def test_matrix_treasure_body_matches_capture(self):
|
||||||
|
resource = dict(TASKLIST_TREASURE_SLOT)
|
||||||
|
ext = dict(resource["extParams"])
|
||||||
|
ext.setdefault("threshold", 0)
|
||||||
|
ext.setdefault("thresholdTaskId", 0)
|
||||||
|
resource["extParams"] = ext
|
||||||
|
self.assertEqual(compact_json({"actionType": 1, "resourceSlotInfo": with_source(resource)}), GOLDEN_MATRIX_TREASURE_BODY)
|
||||||
|
|
||||||
|
def test_matrix_ad_body_matches_capture(self):
|
||||||
|
self.assertEqual(compact_json({"actionType": 1, "resourceSlotInfo": with_source(TASKLIST_AD_SLOT)}), GOLDEN_MATRIX_AD_BODY)
|
||||||
|
|
||||||
|
|
||||||
|
class Sig3CrossValidationTests(unittest.TestCase):
|
||||||
|
"""抓包 query/body + cookie 重建 sign_input, 摘要字段与真实 sig3 内嵌值一致。"""
|
||||||
|
|
||||||
|
def test_captured_digest_fields(self):
|
||||||
|
cases = {
|
||||||
|
"show_popup": (dict(urllib.parse.parse_qsl(SHOW_POPUP_QUERY, keep_blank_values=True)), None, "GET"),
|
||||||
|
"sign_in_report": (dict(urllib.parse.parse_qsl(REPORT_QUERY, keep_blank_values=True)), None, "GET"),
|
||||||
|
"treasure_info": (dict(urllib.parse.parse_qsl(TREASURE_INFO_QUERY, keep_blank_values=True)), None, "GET"),
|
||||||
|
"treasure_report": ({"sigCatVer": "1"}, GOLDEN_TREASURE_BODY, "POST"),
|
||||||
|
"matrix_treasure": ({"sigCatVer": "1"}, GOLDEN_MATRIX_TREASURE_BODY, "POST"),
|
||||||
|
"matrix_task": ({"sigCatVer": "1"}, GOLDEN_MATRIX_AD_BODY, "POST"),
|
||||||
|
}
|
||||||
|
cookie = dict(part.split("=", 1) for part in CAPTURED_COOKIE.split("; "))
|
||||||
|
for label, (query, body, method) in cases.items():
|
||||||
|
with self.subTest(label=label):
|
||||||
|
sign_input = build_h5_sign_input(cookie, query=query, body=body, method=method, request_type="json")
|
||||||
|
calc = h5_sig3_digest_field(sign_input)
|
||||||
|
captured = parse_h5_sig3(CAPTURED_SIG3[label])["digest_field"]
|
||||||
|
self.assertEqual(calc, captured)
|
||||||
|
|
||||||
|
def test_generated_sig3_roundtrip_and_counter_increments(self):
|
||||||
|
client = make_client()
|
||||||
|
query = dict(urllib.parse.parse_qsl(SHOW_POPUP_QUERY, keep_blank_values=True))
|
||||||
|
s1 = client._h5_sig3(query=query)
|
||||||
|
s2 = client._h5_sig3(query=query)
|
||||||
|
f1, f2 = parse_h5_sig3(s1), parse_h5_sig3(s2)
|
||||||
|
cookie = dict(part.split("=", 1) for part in CAPTURED_COOKIE.split("; "))
|
||||||
|
expected = h5_sig3_digest_field(build_h5_sign_input(cookie, query=query))
|
||||||
|
self.assertEqual(f1["digest_field"], expected)
|
||||||
|
self.assertEqual(f2["digest_field"], expected)
|
||||||
|
# 2026-08-27 抓包: counter 进程级单调递增(101->162)
|
||||||
|
self.assertEqual(f2["counter"], f1["counter"] + 1)
|
||||||
|
self.assertEqual(len(s1), 68)
|
||||||
|
|
||||||
|
|
||||||
|
class AlgorithmGoldenTests(unittest.TestCase):
|
||||||
|
"""内联算法黄金值(与已逆向 core 实现逐位一致的固化输出)。"""
|
||||||
|
|
||||||
|
def test_kwsg_10418_digest(self):
|
||||||
|
from ksjsb import KWSG_10418_SIG3_HMAC_KEY, KWSG_10418_SIG3_PREFIX_CODE, _kwsg_tables, kwsg_10418_digest24, kwsg_10418_prefix8
|
||||||
|
|
||||||
|
t1, t2 = _kwsg_tables("B")
|
||||||
|
got = kwsg_10418_digest24(
|
||||||
|
"test-input-中文-10418".encode(), 7, 1787813423, t1, t2,
|
||||||
|
prefix8=kwsg_10418_prefix8(KWSG_10418_SIG3_PREFIX_CODE, 0x0610FDA7),
|
||||||
|
hmac_key=KWSG_10418_SIG3_HMAC_KEY,
|
||||||
|
).hex()
|
||||||
|
self.assertEqual(got, "56473214b4ef0116181e1d1cecfaa8cc28d88a6e030f0117")
|
||||||
|
|
||||||
|
def test_kwsg_10400_raw(self):
|
||||||
|
from ksjsb import REWARD_SDK, _kwsg_tables, kwsg_10400_raw
|
||||||
|
|
||||||
|
t1, t2 = _kwsg_tables("A")
|
||||||
|
got = kwsg_10400_raw('{"x":"encData-黄金值"}'.encode(), REWARD_SDK, t1, t2, epoch_seconds=1787813423).hex()
|
||||||
|
self.assertEqual(
|
||||||
|
got,
|
||||||
|
"5a54eecde4d4ea6193f79db96e3254547f6c770401437d63823030fad0f3d66671d4550214754963c06e257b78857c73a4fff39f4b37ea8996c1b0878f69cef97898e2bebf872583",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_xfalcon_value(self):
|
||||||
|
from ksjsb import xfalcon_value_from_input_bytes
|
||||||
|
|
||||||
|
value = xfalcon_value_from_input_bytes(b"/rest/e/reward/mixed/adclient_key=2ac2a76dos=android")
|
||||||
|
self.assertTrue(value.startswith("HUDR_sFnX+n5uAUNVsMPNK3DOP5wnti1Lc8Axjy5z88T61A==$TE_"))
|
||||||
|
self.assertEqual(
|
||||||
|
value.split("$TE_")[1],
|
||||||
|
"1d029bfdfd210e0c6c7356210e0c6c7356575154566189cefaec418425e348f87e0fdf570c00688c2d0068ad56",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_kww_generation(self):
|
||||||
|
from ksjsb import kwf_generate_kww
|
||||||
|
|
||||||
|
kww = kwf_generate_kww(3, 1787813423000)
|
||||||
|
self.assertTrue(kww.startswith("PnGU+9+Y8008"))
|
||||||
|
self.assertEqual(
|
||||||
|
kww,
|
||||||
|
"PnGU+9+Y8008S+nH0U+0mjPf8fP08f+98f+nLlwnrIP9Pl+9rh+/WA+/mD+9PAPAHh+/Z7G/SD8e+DPBLh8fzS8/rU8/SYGAPAP/GMPBr7+0pSGn8DweDUGAPAPepDG/qAG9+0w/LlG/40+Bz0+nzDweLM+0P7GfzDPe4jG0LA+/P=",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_weapon_kas(self):
|
||||||
|
from ksjsb import weapon_proof_headers
|
||||||
|
|
||||||
|
headers = weapon_proof_headers("https://api.e.kuaishou.com/rest/e/reward/mixed/ad?__NS_sig3=abcd", "kaw-value")
|
||||||
|
self.assertEqual(headers["kas"], "00418873e6dae68db455e61dca1fdfd426")
|
||||||
|
|
||||||
|
def test_signed_api_url_sig3(self):
|
||||||
|
from ksjsb import Kwsg10418State, signed_api_url
|
||||||
|
|
||||||
|
state = Kwsg10418State(session_seed=0x0610FDA7, counter=0x60)
|
||||||
|
url = signed_api_url(
|
||||||
|
"/rest/e/reward/mixed/ad",
|
||||||
|
{"client_key": "2ac2a76d", "os": "android", "ud": "5402308233"},
|
||||||
|
[("encData", "AAA"), ("kuaishou.api_st", "ST")],
|
||||||
|
"salt",
|
||||||
|
state,
|
||||||
|
unix_time=1787813423,
|
||||||
|
)
|
||||||
|
self.assertIn("sig=4df442297f332ac4bce562f7b720229c", url)
|
||||||
|
self.assertIn("__NS_sig3=8d9ce9cf6f34dacda5c5c6c70c09b185f30351b5d8d4dacc", url)
|
||||||
|
self.assertIn("__NStokensig=7e2cf6b51f8e8b86ea855ee1ed3e0be550eba81d5ef7effc76a54216fb65f2fa", url)
|
||||||
|
self.assertIn("__NS_xfalcon=", url)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiParamSetTests(unittest.TestCase):
|
||||||
|
"""API query 参数集与真机抓包逐键一致(2026-08-27 在线 6001 排查结论)。"""
|
||||||
|
|
||||||
|
CAPTURED_AD_QUERY_KEYS = [
|
||||||
|
"earphoneMode", "mod", "appver", "isp", "language", "ud", "did_tag", "egid",
|
||||||
|
"thermal", "net", "kcv", "app", "kpf", "bottom_navigation", "ver", "android_os",
|
||||||
|
"oDid", "boardPlatform", "kpn", "newOc", "androidApiLevel", "slh", "country_code",
|
||||||
|
"nbh", "hotfix_ver", "did_gt", "keyconfig_state", "cdid_tag", "sys", "max_memory",
|
||||||
|
"cold_launch_time_ms", "oc", "sh", "deviceBit", "browseType", "ddpi", "socName",
|
||||||
|
"is_background", "c", "sw", "ftt", "apptype", "abi", "userRecoBit", "device_abi",
|
||||||
|
"icaver", "totalMemory", "grant_browse_type", "iuid", "rdid", "sbh", "darkMode",
|
||||||
|
"did",
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_ad_fetch_query_keys_match_capture(self):
|
||||||
|
client = make_client()
|
||||||
|
params = client._api_params()
|
||||||
|
# 真机广告请求: 上述设备键 + sig/__NS_sig3/__NS_xfalcon/__NStokensig
|
||||||
|
self.assertNotIn("sid", params) # 真机广告请求从不携带(2026-07 抓包实证)
|
||||||
|
self.assertNotIn("deviceName", params)
|
||||||
|
expected = set(self.CAPTURED_AD_QUERY_KEYS)
|
||||||
|
self.assertTrue(expected.issubset(params), f"缺失: {expected - set(params)}")
|
||||||
|
|
||||||
|
def test_uqatag_default_is_device_neutral_null_variant(self):
|
||||||
|
"""uQaTag 默认用 QoE 未采集回退形态(2026-07-08 抓包 result=1 接受)。"""
|
||||||
|
from ksjsb import API_COMMON_BODY
|
||||||
|
|
||||||
|
self.assertEqual(API_COMMON_BODY["uQaTag"], "1##DP:null#ecBl:00#ecPp:--#cmNt:-1#cmMnsl:-0")
|
||||||
|
client = make_client()
|
||||||
|
pairs = dict(client._api_body_common())
|
||||||
|
self.assertEqual(pairs["uQaTag"], API_COMMON_BODY["uQaTag"])
|
||||||
|
# 字段序对齐真机: cs, client_key, videoModelCrowdTag, os, api_st, uQaTag, token
|
||||||
|
self.assertEqual(
|
||||||
|
[k for k, _ in client._api_body_common()],
|
||||||
|
["cs", "client_key", "videoModelCrowdTag", "os", "kuaishou.api_st", "uQaTag", "token"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WithSourceTests(unittest.TestCase):
|
||||||
|
def test_appends_source_and_preserves_key_order(self):
|
||||||
|
tracking = dict(TREASURE_TRACKING)
|
||||||
|
out = with_source(tracking)
|
||||||
|
self.assertEqual(list(out.keys()), list(tracking.keys()))
|
||||||
|
self.assertEqual(list(out["extParams"].keys()), ["isServerRecordClickAction", "source"])
|
||||||
|
self.assertNotIn("source", tracking["extParams"])
|
||||||
|
|
||||||
|
|
||||||
|
class TaskSlotTests(unittest.TestCase):
|
||||||
|
def test_update_ad_contexts_extracts_slots_and_scene(self):
|
||||||
|
import base64 as b64
|
||||||
|
|
||||||
|
link_url = b64.b64encode(
|
||||||
|
compact_json({"pageId": 11101, "subPageId": 100026367, "posId": 24067, "businessId": 672}).encode()
|
||||||
|
).decode()
|
||||||
|
client = make_client()
|
||||||
|
client._update_ad_contexts({
|
||||||
|
"data": {
|
||||||
|
"dailyTasks": [
|
||||||
|
{"eventTrackingLogInfo": TASKLIST_AD_SLOT, "linkUrl": link_url, "extParam": {"descCountDown": 30000}},
|
||||||
|
{"extParam": {"eventTrackingLogInfo": TASKLIST_TREASURE_SLOT}},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.assertEqual(client.task_ad_slot, TASKLIST_AD_SLOT)
|
||||||
|
self.assertEqual(client.task_treasure_slot, TASKLIST_TREASURE_SLOT)
|
||||||
|
self.assertEqual(client.ad_fetch_scenes["normal"], (11101, 100026367, 24067))
|
||||||
|
self.assertEqual(client.ad_task_countdown_ms["normal"], 30000)
|
||||||
|
|
||||||
|
|
||||||
|
class SequentialRunTests(unittest.TestCase):
|
||||||
|
def test_run_dry_run_completes_all_steps_without_network(self):
|
||||||
|
from ksjsb import run
|
||||||
|
|
||||||
|
env = {"ksck": f"test#{CAPTURED_COOKIE}#salt", "KS_DRY_RUN": "1", "KS_TIMEOUT": "5"}
|
||||||
|
buf = io.StringIO()
|
||||||
|
old = {k: os.environ.get(k) for k in env}
|
||||||
|
os.environ.update(env)
|
||||||
|
try:
|
||||||
|
with contextlib.redirect_stdout(buf):
|
||||||
|
code = run()
|
||||||
|
finally:
|
||||||
|
for k, v in old.items():
|
||||||
|
if v is None:
|
||||||
|
os.environ.pop(k, None)
|
||||||
|
else:
|
||||||
|
os.environ[k] = v
|
||||||
|
self.assertEqual(code, 0)
|
||||||
|
text = buf.getvalue()
|
||||||
|
for marker in ("1/5 查询金币/余额", "3/5 签到", "4/5 开宝箱", "5/5 看广告得金币", "最终余额", "任务执行完成"):
|
||||||
|
self.assertIn(marker, text)
|
||||||
|
self.assertIn("开宝箱 dry-run method=POST", text)
|
||||||
|
self.assertIn("/rest/e/reward/mixed/ad", text)
|
||||||
|
self.assertIn("/rest/r/ad/task/report", text)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in New Issue
Block a user