得卡 DECA App 逆向分析文档
目标:采集得卡「商家展位列表 / 在售商品列表」,供业务侧统计 新增 / 商家账号 / 商品售卖进度。
核心难点:所有接口带 signature 请求头签名校验,部分接口需 JWT 登录态。
结论:signature 已 100% 还原(无加壳、纯 Java),登录+续签闭环打通,可长期无人值守。
日期:2026/08/02 | Python 3.12.10
1. 目标信息
- App:得卡 DECA
得卡DECA_1.0.0.apk(appVersion 1.0.0)
- 接口域名:
https://api.decalive.com
- 网络栈:OkHttp 5.3.2(
User-Agent: okhttp/5.3.2)
- 鉴权/加密措施:
- 每个请求都带
signature(32 位十六进制,双重 MD5)+ currentTime(13 位毫秒时间戳)。
- 部分接口需
Authorization: Bearer <JWT>,access token 有效期 15 分钟。
- 加固情况:无加壳、无字符串加密、签名不依赖 Native(so);混淆为中等(类名字母混淆,salt 明文硬编码)。
涉及接口
| 用途 |
方法 & 路径 |
是否需登录 |
请求体 |
| 商家展位列表 |
POST /api/v1/app/merchant/exhibition/list |
否 |
{"page":N} |
| 在售商品列表 |
POST /api/v1/app/groupbuy/merchant/on-sale-list |
是 |
{"merchantUserId","page","pageSize":20} |
| 密码登录 |
POST /api/v1/app/auth/password/login |
否 |
{"countryCode","password","phone"} |
| Token 续签 |
POST /api/v1/app/auth/token/refresh |
否 |
{"refreshToken"} |
2. 分析过程
- 抓包:得到 3 个业务请求 + 1 个登录请求的 curl(含 4 组真实
signature/currentTime/body 样本,用于最终验证)。
- 反编译:jadx 1.5.3 反编译 APK(
--no-res),产物 16044 个 java 文件,classes.dex/classes2.dex 均为真实业务码,无壳。
- 定位签名:从头部装配器
mi/w.java(组装全部公共头 + signature)顺藤摸瓜到签名计算核心 mf/c0.java。
关键类
| 类 / 方法 |
作用 |
mi.w.f(Context, url) |
头部装配器,组装公共头 + signature |
mf.c0.a(Map) |
签名计算核心:生成 currentTime + 双重 MD5 |
mf.c0.b(String) |
MD5 → 小写 hex |
mf.b0 |
每个参数 entry 渲染为 key+value(无分隔符)的 lambda |
eg.p |
Retrofit 认证接口(login / sms / wechat / aliyun / token refresh) |
3. 逆向思路
- 障碍:所有接口非签名不可用;商品接口还要登录态,且 token 15 分钟即过期,无法直接长期跑。
- 突破:
- 在
mf/c0.a() 读出签名逻辑——参数排序 + 双重 MD5 + 固定 salt,salt 是明文常量。
- 用抓包的 4 个样本反推确认拼接细节(key 升序、key 直接拼 value、无
= 无分隔符、只取时间戳后 6 位)。
- 登录接口样本(无 Authorization)也能仅凭 body 算出一致签名 → 证明签名不依赖 token,可脚本自动登录。
- 在
eg/*.java 发现 token/refresh + refreshToken 存储 → 续签闭环成立。
4. 算法要点
4.1 signature 生成规则(已用 4 样本验证一致)
plain = 参数按 key 升序,每项 "key"+"value" 直接拼接(无分隔符),剔除 value 为 null 的项
inner = md5(plain) # 32 位小写 hex
signature = md5( inner + currentTime[-6:] + "biu_card_nbclass" )
- 哈希:MD5,UTF-8,输出小写 hex,双重 MD5。
- 固定 salt(硬编码于
mf/c0.java):biu_card_nbclass
currentTime:13 位毫秒时间戳,同一个值同时放进 currentTime 头;签名只用其后 6 位。
- 空 body:
plain="" → inner=md5(""),无特殊分支。
- Authorization / token 不参与签名。
4.2 Python 复现
import hashlib, time
SALT = "biu_card_nbclass"
def md5(s): return hashlib.md5(s.encode("utf-8")).hexdigest()
def make_signature(params: dict, current_time: str) -> str:
clean = {k: v for k, v in params.items() if v is not None}
plain = "".join(f"{k}{clean[k]}" for k in sorted(clean))
return md5(md5(plain) + current_time[-6:] + SALT)
# current_time = str(int(time.time()*1000)),同一值放进 currentTime 头
4.3 验证样本(4/4 全中)
| plain |
currentTime |
signature |
page1 |
1785662750434 |
c0670b1219 |
page2 |
1785662751198 |
0dea97b713 |
merchantUserId593731690page1pageSize20 |
1785664667848 |
5416ca7036 |
countryCode86passwordpass2022phone19521500850 |
1785665137653 |
81464e9bf4 |
4.4 登录 / Token
password/login 响应 data:accessToken / refreshToken / expiresIn(秒,900=15分钟) / userId 等。
- 续签:
token/refresh body {"refreshToken":...},返回同结构新 token。
- 脚本策略:access 剩余 <60s 先续签,续签失败回退重新登录(登录已验证,绝对兜底)。
5. 踩坑记录
- body 必须紧凑 JSON:抓包
Content-Length:10 对应 {"page":1}(无空格)。发送用 json.dumps(..., separators=(",",":")),否则 Content-Length 与实际不符。签名本身只用参数串,与 body 序列化格式无关。
- Windows 控制台 GBK:脚本打印 emoji/部分字符会
UnicodeEncodeError。运行加 PYTHONIOENCODING=utf-8 PYTHONUTF8=1,日志文件本身写 UTF-8 无碍。
- token 15 分钟过期:一轮采集可能超 15 分钟,必须在请求前动态检查并续签,不能只登录一次。
- 数据库握手被拒(2013):内网库
100.64.0.25 需网络/白名单就绪,TCP 通但 MySQL 握手断时,先排查 VPN/授权白名单,非代码问题。
- jadx 插件 NPE:为旧版本编译的
jadx-ai-mcp 插件会导致 jadx 启动崩溃,反编译前需临时清空插件配置。
- 很多商家
activeGroupbuyCount=0:即当前无在售,属正常;采商品时对每个商家都会发一次接口确认。
- 微信自动发送库
wxauto 已下架:PyPI 上原 wxauto 已被作者移除(pip install wxauto 报 from versions: none),现改推 wxauto4(对应微信 4.0 客户端,免费)/ wxautox4(付费 plus)。安装 pip install wxauto4;import 名即 wxauto4,用 WeChat().SendFiles(filepath, who, exact) 发文件。若 PC 微信仍是 3.9 旧版,则改用 wxautox。发送依赖微信窗口可见的 UI 自动化,无人值守时需保证微信一直登录、窗口不最小化。
6. 举一反三
- 同类 App 签名套路:
sign = hash( 排序参数串 + 时间戳(部分位) + 固定 salt ) 是移动端最常见范式。定位思路统一:先找组装请求头的拦截器/装配器,再找里面调用的 hash 工具方法,salt 常为明文常量。
- 是否 URL 编码:
mf.b0.a 声明抛 UnsupportedEncodingException,暗示内部可能对 value 做 URLEncoder.encode(v,"UTF-8")(空格→+)。当前样本均无特殊字符无法区分;若日后遇到搜索关键词等含空格/中文的参数,需对 value 先编码再拼。
- 嵌套/数组参数:
c0 会把嵌套对象递归转 TreeMap、数组保序后按 toString 拼接。当前接口均为扁平 body,遇到嵌套 body 时需另取样本校准渲染格式。
- 长期无人值守要点:签名 + 自动登录 + 续签三者齐备才算闭环;只解签名不解 token 续签,跑不过 15 分钟。
附:项目文件
| 文件 |
说明 |
deca_daily_spider.py |
常驻每日爬虫(登录→商家→在售商品+每日快照;采完自动出图并发微信) |
deca_report.py |
统计报表出图(KPI + 今日新增商家 + 各商家在售/售卖进度),生成 reports/ 下 PNG,并按开关自动发微信 |
deca_wechat.py |
通过 wxauto4 把图片发到 PC 微信好友/群 |
schema.sql |
三张表 DDL:deca_shop_record / deca_product_record / deca_product_daily_record |
stats.sql |
统计查询 SQL(新增 / 商家账号 / 售卖进度趋势) |
init_db.py |
一键建表 |
requirements.txt |
依赖清单(版本实测) |
部署:
- 装依赖:
pip install -r requirements.txt(wxauto4 若受 requires-python 限制加 --ignore-requires-python);本地公共库 charley-utils 需 editable 安装(见 requirements 尾注)。
- 建表:
python init_db.py
- 常驻运行:
python deca_daily_spider.py(每天 09:00 / 15:00 及启动各采集一轮,采完自动生成报表并发微信)。
报表与微信推送:
- 报表图输出到
reports/deca_report_YYYYMMDD_HHMMSS.png,命名到秒,一天多张互不覆盖。
- 微信发送依赖 PC 微信 4.0 已登录且窗口可见;目标好友备注名须精确等于
deca_report.WECHAT_TO(默认 backup)。
- 手动补发:
python deca_wechat.py "reports/xxx.png" backup。
- 关闭自动发送:把
deca_report.SEND_WECHAT 置为 False。
登录与 token(避开验证码):
- 密码登录
password/login 受阿里云验证码2.0 风控,脚本无法自动过验证码;改走 token/refresh 续期(无验证码)。
- 首次:在 APP 正常登录后抓包,把
refreshToken 粘入项目根目录 token.json 的 refresh 字段:{"access":null,"refresh":"<粘这里>","exp":0}。
- 之后爬虫用它自动续期并滚动写回
token.json;refreshToken 失效(过期/被踢)时日志会报警,需重新抓包更新。
- 商品上架时间/结束时间来自详情接口
POST api/v1/app/groupbuy/detail(body {"code":商品code},需 token),仅对今日新增商品拉取,存入 deca_product_record 的 publish_at/sale_start_at/sale_end_at。商家入驻时间接口无,新增按入库时间判定。