|
|
1 hafta önce | |
|---|---|---|
| .. | ||
| README.md | 1 ay önce | |
| YamlLoader.py | 1 ay önce | |
| application.yml | 1 ay önce | |
| kj_auth.py | 1 ay önce | |
| kj_daily_spider.py | 1 hafta önce | |
| kj_history_spider.py | 1 ay önce | |
| mysql_pool.py | 1 ay önce | |
| requirements.txt | 1 ay önce | |
card.kaji.com2.5.39,versionCode 10003(huawei 渠道)卡集_2.5.39.apk(约 86MB)server.ssl1.kaji6.com(主列表 / videoPlay)、page.ssl1.kaji6.com(商户列表 / 详情 / 玩家)assets/apps/__UNI__EFAB5FD/www/app-service.js(3.6MB)、app-view.js(4.9MB)io.dcloud.application.DCloudApplication;入口 io.dcloud.PandoraEntrylibweexcore.so / libweexjss.so / libuts-runtime.solibflutter.so / flutter_assets(非 Flutter);无 index.android.bundle / libhermes(非 React Native)io.dcloud.*、com.taobao.weex.*),无壳 Stub。
assets/39285EFA.dex + lib39285EFA.so 是 DCloud uni-app 自带的分包/加密机制,不是第三方壳。libzxprotect.so(局部代码保护)、libaliyunaf.so(阿里聚安全)、libsecuritydevice.so(阿里 dtf 设备风控,约 3.9MB,疑似带反调试/反 Frida,静态未坐实)。pin-sha256 / CertificatePinner 配置值均为 0)。握手拦截来自标准证书链校验(targetSdk 30 + 无 networkSecurityConfig)+ DCloud 的 HostnameVerifier 主机名校验。Authorization: Open-Auth-Sig(每请求签名,非登录 token),详见第 4 节。libsecuritydevice.so)负责环境检测,与 HTTP 校验无关。Reqable Certificate Installer(装 CA 到系统库)+ Move Certificates(用户库证书迁移到系统库)dc.squareup.okhttp3.*(第三方 SDK 才走标准 okhttp3.*),证书装配点是 io.dcloud.common.adapter.util.DCloudTrustManager。okhttp3.CertificatePinner.check、javax.net.ssl.*),而卡集类前缀是 dc.squareup.okhttp3.*,名字对不上 → hook 没挂上。SSLContext.init 底层兜底放行了走网络栈的 API,但没 hook WebView 的证书校验(onReceivedSslError),而 uni-app 的 view 层是 WebView、图片由 WebView 拉取。经验:unpinning 能抓到什么流量,取决于它 hook 的覆盖面;「API 能抓、图片/WebView 内容不显示」通常就是 WebView 那条 SSL 校验路径没被 hook。
抓包发现部分接口带 Authorization: Open-Auth-Sig Swap="..",Timestamp=..,Nonce=..,Signature="..",且每请求都变。要让爬虫长期运行,必须还原其生成算法(而非抠固定值——Timestamp 有时效)。
app-service.js,直接搜 Open-Auth-Sig / Signature / Swap 定位。app-service.js webpack 模块 "665c" 的 GetCrypto(path)(偏移约 1511900),入参为接口相对路径。i,0..255 全排列);MD5 库 n("e133");requestVersion = "/api/v4/"。sign = md5(ts + "_" + goodCode + "_" + playCode + "_videoPlayKsj")。PostWithCrypto / GetWithCrypto 显式调用——所以只有部分接口带签名(解释了抓包「有的带有的不带」)。用三条真实抓包样本正推 Signature,逐字节匹配;videoPlay 的 sign 用 goodCode=MC5388325 正推命中 aba9d5be1f290cec5029e12c3d747d9f。算法确认无误,已落地为 kj_auth.py。
kj_auth.gen_authorization(path))输入:接口相对路径 path(不含域名 / /api/v4/ 前缀 / query,如 goodlist/forsale/main)。
noce(Nonce)= 随机整数 [1, 500]swap = 从 0..255 取 8 个不重复字节 → 转 16 位小写 hex(每请求随机)ts(Timestamp)= 当前 Unix 秒full_path = ("/api/v4/" + path),去掉 /dataApi 子串,去掉 ?queryl = f"{swap}_{ts}_{noce}_{full_path}"(顺序固定:swap _ ts _ noce _ path)f = MD5(l) → 32 位小写 hexr = f.encode('ascii').hex()(把 32 字符 md5-hex 当 ASCII 再转 hex → 64 字符)a = list(I_TABLE)for s in range(0,8,2): a[c[s]], a[c[s+1]] = a[c[s+1]], a[c[s]]r 每个字符 ch:取 a[ord(ch)] 转 2 位大写 hex,拼接 → 128 位 SignatureOpen-Auth-Sig Swap="{swap}",Timestamp={ts},Nonce={noce},Signature="{sig}"置换表
I_TABLE:256 项(0..255 全排列),来自app-service.js偏移约 1513290,完整值见kj_auth.py。
kj_auth.video_sign(ts, good_code, play_code))sign = MD5(f"{ts}_{good_code}_{play_code}_videoPlayKsj") # 固定盐 videoPlayKsj
POST good/videoPlay/{goodCode},body = {"playCode":.., "sign":.., "ts":..},不需要 Authorization,响应的 media_url 即回放视频地址。
playCode 在 detail.good.broadcast.playCode:拼团完成 / 回放就绪时非空,否则为空字符串。
完整取视频链路:detail → broadcast.playCode → videoPlay → media_url。
0-9、a-f 共十来种取值),查表后输出字节自然集中在约 10 种值,看起来像「伪 hex」。swap 每请求随机 → 置换表被洗牌 → 同一 MD5 每次输出的 Signature 都不同,这是它的抗重放 / 防特征提取设计。签名 = 标准 MD5 + 256 项置换表 + swap 洗牌,拼接串只有 swap/ts/noce/path,无任何密钥 / appSecret / 登录 token。因此爬虫无需登录、无需维护 token、永不过期,天然适合长期无人值守运行(比需维护 token 的项目更省心)。
接口(/api/v4/ 之后) |
方法 | 域名 | 签名 | 作用 |
|---|---|---|---|---|
goodlist/forsale/main |
GET | server | Open-Auth-Sig | 全局在售商品主列表(发现商户) |
merchant/1/goodlist/{商户码}?tp=2 |
GET | page | 免签名 | 某商户已售/已完成商品列表 |
good/{code}/1/detail |
GET | page | Open-Auth-Sig | 商品详情(时间/规格/商户 publisher/broadcast) |
good/{code}/result/merge |
GET | page | Open-Auth-Sig | 玩家/中奖名单(仅拼团完成的商品有) |
good/videoPlay/{code} |
POST | server | body sign | 拆卡回放视频(返回 media_url) |
kj_auth.py:签名模块。gen_authorization(path) 生成请求头、video_sign(...) 生成 videoPlay 签名、gen_signature/_md5_hex 为底层,含 256 项置换表。三处均以真实样本验证通过。kj_daily_spider.py:日常增量脚本(schedule 每天 00:01 定时)+ 全部公共函数(请求/签名/翻页/解析/get_video/refill_videos)。三级管道 = 在售主列表发现商户 → 遍历各商户已售列表(+detail 补充) → 玩家名单,player_state 断点标记(0 未抓 / 1 已抓 / 2 无数据 / 3 异常),loguru 日志 + tenacity 重试。
get_sold_list(incremental=True) 采某商户前,先用 get_shop_stop_pid 取该商户 start_at 最新那条的 pid(SELECT pid ... WHERE shop_id=%s ORDER BY start_at DESC LIMIT 1,依赖 (shop_id, start_at) 复合索引,单值查询);merchant?tp=2 按 start_at 倒序(新在前),翻页用 goodCode 匹配到这个 pid 即 break——它之后都是已采过的,不必再翻、也不发 detail。首次采(无记录)则全量。merchant?tp=2 返回 code=1/msg='无效商家' → 标记 is_deleted=1;kj_main 查商户带 WHERE is_deleted=0 自动跳过。商户重新在售时 save_shops 的 ON DUPLICATE KEY UPDATE ... is_deleted=0 会把它「复活」。refill_videos 针对拼团完成(player_state=1)但 video_url 仍空的商品,重取 detail.broadcast.playCode → videoPlay,覆盖首次采集时回放未就绪(正在/即将拆卡,playCode 为空)的情况;下一轮自愈直到拿到地址。FETCH_DETAIL(是否每商品补 detail)、USE_PROXY(遇 IP 风控再开)。kj_history_spider.py:一次性历史全量脚本(跑一次即止,无 schedule)。from kj_daily_spider import ... 复用全部公共函数,只写 history_main 调度:商户发现 → get_sold_list(incremental=False) 全量深翻所有页(不早停) → 玩家全采 → 视频补采。日常增量与历史全量共用同一套函数,靠 incremental 开关区分,不重复维护。merchantAlias(商户码) / merchantName / goodCode / title / pic / price / totalNum / currentNum / startAt / overAt / statestartAt / overAt / state / spec{name,content} / publisher{alias,name,deal(成交),fans,level} / broadcast{playCode,name,state,roomId}userId(匿名为 0) / userName(匿名为空) / total(份数) / anonymous / anonymousCodeokhttp3 改成 dc.squareup.okhttp3,原版 JustTrustMe、frida-multiple-unpinning、objection 默认脚本按标准类名匹配一律打不中;换 JustTrustMePro / 手动补 hook。onReceivedSslError(JustTrustMePro 覆盖了)。/api/v4/ 前缀:gen_authorization 内部已处理(补前缀 + 裁 query)。pid 列已由 int 改为 varchar(product、player 两张表都改)。broadcast.state=1「即将拆卡」时 playCode 为空,get_video 已对空值返回 None。libsecuritydevice.so(阿里 dtf)疑似带反 Frida 检测(静态未坐实)。若后续上 Frida,attach 闪退用 magisk-frida / 改名 gadget 隐身。DCloudApplication + assets/apps/*/www/app-service.js;抓包 unpinning 用 JustTrustMePro;业务签名/加密/token 基本都在明文 app-service.js,搜关键字符串(签名头名、sign、Authorization)定位 webpack 模块即可。ssl_verify_peer_cert;Native pinning → so 里 hook 校验函数;加固 → 先脱壳。