ksjsb/docs/region_ticket_static_chain.md
2026-07-30 20:25:56 +08:00

5.6 KiB
Raw Blame History

region_ticket 静态逆向结论

结论

region_ticket 不是 sig__NS_sig3 或 DFP 一类的客户端计算结果。 APK 将它建模为服务端响应顶层 region.ticket 中的不透明票据,按用户保存到 DefaultPreferenceHelper<uid>_Region,后续请求再把它放入 Cookie。

因此,纯 Python 的正确实现是“接收、持久化、复用”,不是本地生成:

  1. 解析每个普通 API JSON 响应的顶层 region
  2. ticket 时按 region.uid 保存;uid 为空时使用当前用户 ID。
  3. 后续请求注入 Cookie: region_ticket=<ticket>; __NSWJ=<value>
  4. 账号切换时按 UID 隔离,不能把一个账号的票据当作设备级常量。

静态调用链

任意普通 API JSON 响应
  -> ResponseDeserializer.deserialize()
     -> 读取顶层 region.uid / region.name / region.ticket
     -> ylm.d.mRegion
  -> u0a.f.buildObservableBeforeRetry()
     -> doOnNext(com.kwai.framework.network.regions.c)
  -> regions.c.accept()
     -> o2a.c.c(region, "New region received")
  -> DefaultPreferenceHelper[<uid>_Region] = Region JSON

下一次请求
  -> v0a.c.c(sceneName)
  -> q01.g.l0()
  -> NetworkAccessParams.e.l0()
  -> o2a.c.b(Region.class).ticket
  -> Cookie: region_ticket=<ticket>; __NSWJ=<value>

关键证据

1. IOC 实现绑定

  • pnm.b.b(-1479227965) 在生成的 IOCProviderImpl 中绑定到 com.kwai.framework.network.access.params.e
  • q01.g.l0() 的实际实现读取 o2a.c.b(Region.class),返回 Region.b()mTicket;不存在加密、哈希或 native 调用。
  • Region scheduler 的 IOC ID 1013182224 绑定到 o2a.e

证据文件:

  • out/region_ticket_jadx/IOCProviderImpl.java
  • out/region_ticket_jadx/NetworkAccessParamsE.java
  • out/region_ticket_jadx/RegionSchedulerProviderE.java

2. 响应下发与统一持久化

ResponseDeserializer 只读取响应 JSON 的顶层字段:

{
  "region": {
    "uid": "...",
    "name": "...",
    "ticket": "RT_..."
  }
}

u0a.fregions.c 注册为普通 API 的全局 doOnNext 处理器。 regions.c 检查 ylm.d.k(),非空时直接调用:

o2a.c.c(response.k(), "New region received");

这说明没有唯一的 region_ticket 申请接口。任何使用统一响应包的普通 API 都可以顺带下发或轮换 Region。

3. 本地存储

o2a.c 使用 DefaultPreferenceHelper

读取键: <QCurrentUser.id>_Region
写入键: <region.uid>_Region
回退键: <QCurrentUser.id>_Region
值格式: {"uid":"...","name":"...","ticket":"RT_..."}

未登录时当前 UID 回退为 0。如果响应明确带 region.uid,写入时优先使用 该 UID所以账号票据不会天然复制到另一个 UID。

v0a.c 先组合 region_ticketkuaishou.api_st__NSWJ,再序列化为 Cookie 请求头。region_ticket 为空时该项被省略,不会生成占位值。

5. RegionInfo 不是票据来源

o2a.e 会从 SharedPreferences 的 RegionInfo 或 raw 资源加载 API 区域路由。 APK 资源 raw/0x7f100082 只有 api_group_host_listapi_mapping,没有 ticket。它负责 follow/nearby 的区域 host 选择,不负责签发 Region 票据。

HAR 审计

新增 tools/analyze_region_ticket.py,结构化审计以下三类事件:

  • 响应 JSON 出现顶层或嵌套 region.ticket
  • 响应头出现 Set-Cookie: region_ticket=...
  • 请求 Cookie 携带 region_ticket

对仓库全部 7 个 HAR、8334 个 entry 的结果:

响应 region.ticket:       0
响应 Set-Cookie:          0
请求 region_ticket:     362
唯一票据:                12
票据长度:                全部 76
票据形态:                RT_ + 73 个十六进制字符

12 个样本的 73 个十六进制位置均有变化,没有固定版本位或可见字段边界。 这与“服务端不透明票据”一致,但仅凭格式不能反推出生成算法。

现有 HAR 的抓包窗口开始时票据已经存在:例如历史窗口的第一批 startup、spot、 reddot 请求已经携带同一票据。因此“首个携带票据的请求”不是签发接口,这批 HAR 不能确定首次下发发生在哪个 endpoint。

脱敏审计产物位于:

  • out/region_ticket_har_small.json
  • out/region_ticket_har_20260708_1416.json
  • out/region_ticket_har_20260710.json
  • out/region_ticket_har_20260726.json
  • out/region_ticket_har_ip_20260726.json
  • out/region_ticket_har_cdn_20260724.json

对短信登录 CLI 的影响

当前 CLI 已支持 --region-ticketKS_REGION_TICKET 和 app-fields 注入, 但当前 sms_device_latest.json 不包含该状态。静态链给出的实现边界是:

  • DFP 注册不会本地生成 region_ticket
  • passport_account_image、account_security 和请求签名也不会生成它。
  • CLI 应在 checker、发码、登录及后续普通 API 响应中检查顶层 region,收到后 更新当前 Session Cookie 并按 UID 保存。
  • 在首次收到服务端 Region 之前,只能不带该 Cookie不能构造一个等价票据。

缺少 region_ticket 会造成 APP/CLI 请求上下文差异,可能增加风控评分,但静态证据 不足以把 705 单独归因于它。要验证因果,应在同一设备画像、同一手机号、同一网络 下只改变该 Cookie比较 checker 和 mobileVerifyCode 的结果。

复现命令

uv run python -m tools.analyze_region_ticket `
  ks.har `
  nebula.kuaishou.com_2026_07_10_17_15_37.har `
  --out out/region_ticket_audit.json

uv run python -m unittest tests.test_analyze_region_ticket