词达人协议逆向工程实战:HTTP抓包、签名解析与AES解密

发布时间:2026/9/25 23:27:45
词达人协议逆向工程实战:HTTP抓包、签名解析与AES解密 简介本资源是一套面向英语学习技术爱好者与逆向分析初学者的词达人客户端抓包调试工具集聚焦于理解词汇类App网络通信机制与本地交互逻辑。压缩包含92个文件总大小7.4MB主体为13个exe含词达人工具.exe、Fiddler.exe及配套辅助程序、36个dll如Telerik.NetworkConnections.dll、Xceed.Zip.v5.4.dll等核心依赖库、14个dat可能存储词库或配置缓存及13个pdb调试符号文件辅以wav音效、config配置、bat自动化脚本和js/ico等资源结构完整具备即装即用的调试环境基础。已有8919人学习下载反映出较强的技术实践需求。用户可直接运行Fiddler代理并加载预置证书捕获词达人App的HTTPS请求结合exe与pdb文件分析其API调用逻辑、学习进度同步机制及本地缓存策略同时利用bat脚本快速启用Loopback权限、导入证书及启动监控显著降低抓包门槛是深入理解教育类客户端网络行为的实用入门套件。1. 词达人工具包不是“外挂”而是一套可复现的 HTTP 协议逆向工程实践它能帮你把单词背诵行为从黑匣子变成白盒流程适合想搞懂在线教育平台通信逻辑的前端/测试/教育技术从业者你有没有试过点开词达人 App划到某个单元——页面加载快得像本地渲染但后台其实正悄悄发着十几条请求不是所有“词达人抓包”都指向作弊更多人真正需要的是搞清楚为什么同一套单词表在 Web 端和 App 端返回的字段名不一致为什么提交答案后服务器返回的score和 UI 显示的分数差 0.5为什么换设备重装 App历史记录就丢了这些不是玄学而是 HTTP 接口设计、Token 生命周期、加密参数生成逻辑共同作用的结果。词达人工具词达人.zip这个资源本质是一套基于真实抓包流量反推出来的协议解析脚本集合 配置模板 调试验证用例它不提供自动答题功能但完整还原了登录鉴权、单元拉取、答案提交、结果校验这四个核心链路的请求构造逻辑。如果你正在做教育类 App 的兼容性测试、想为学校定制离线词库同步方案、或是教学生理解 RESTful API 的实际落地形态这个包比任何“免 Root 抓包教程”都更贴近一线工程现场——它默认用的是 mitmproxy Python 3.9 的组合所有脚本均可在 Windows/macOS/Linux 上无依赖运行且每个.py文件顶部都标注了对应词达人 Web 版v4.2.1与 Android Appv5.8.3的实测接口版本号。2. 从抓包原始流量到可执行脚本四步完成协议还原闭环2.1 抓包环境搭建为什么不用 Fiddler 或 Charles而选 mitmproxy 自定义证书词达人 Web 端https://www.cidaan.com和 App 均启用了严格的 HTTPS 证书绑定与 TLS 1.3 协商机制Fiddler 默认证书在 Android 7 设备上会被系统级拦截Charles 则需手动安装 CA 证书并关闭“SSL Proxying”自动忽略规则——这会导致大量ERR_SSL_VERSION_OR_CIPHER_MISMATCH错误根本看不到有效 payload。mitmproxy 的优势在于其证书生成逻辑完全可控且支持通过--set confdirxxx指定独立配置目录避免多项目证书冲突。本工具包中certs/目录下已预置适配词达人域名的中间证书mitmproxy-ca.pem只需执行# 在项目根目录执行需提前安装 mitmproxypip install mitmproxy9.0.1 mitmdump --set confdir./certs -p 8080 --set block_globalfalse提示务必使用 mitmproxy 9.0.1 版本高版本≥10.0默认启用 HTTP/2 解析而词达人部分接口仍走 HTTP/1.1会导致Content-Length解析错位response body 为空。启动后将手机或浏览器代理设为127.0.0.1:8080访问词达人首页即可在终端看到实时请求流。关键不是“抓到多少包”而是识别出四类主干请求/api/login含 RSA 公钥加密、/api/unit/list带时间戳签名、/api/answer/submitAES-CBC 加密 body、/api/score/detailJWT 校验。工具包中的flow_analyze.py就是专为这类流量设计的过滤器——它会自动提取Host、Authorization、X-Signature头并按request.url.path分组归档到./flows/下对应子目录。2.2 接口签名逆向X-Signature不是 MD5而是 HmacSHA256 时间戳拼接 Base64 编码词达人所有写操作请求头均含X-Signature字段初看像随机字符串实测发现其值每秒变化且与请求体强相关。通过对比 100 条POST /api/answer/submit流量确认其生成逻辑为import hmac import base64 import time def gen_signature(method: str, path: str, body: str, secret_key: str) - str: # 注意secret_key 并非明文而是从 /api/login 返回的 token 中截取前 16 字节 timestamp str(int(time.time() * 1000)) # 毫秒级时间戳 # 拼接规则METHOD|PATH|TIMESTAMP|BODY_MD5 body_md5 hashlib.md5(body.encode()).hexdigest() sign_str f{method.upper()}|{path}|{timestamp}|{body_md5} # 使用 secret_key 进行 HmacSHA256 签名 signature hmac.new( secret_key.encode(), sign_str.encode(), hashlib.sha256 ).digest() return base64.b64encode(signature).decode() # 示例调用对应 submit 接口 body_json {unit_id: 123, answers: [{qid: 456, answer: apple}]} sig gen_signature(POST, /api/answer/submit, json.dumps(body_json), a1b2c3d4e5f6g7h8)参数说明secret_key来自登录成功后响应体中的data.token字段长度 32取前 16 字节body必须是未格式化的紧凑 JSON 字符串无空格、换行timestamp必须精确到毫秒误差超过 30 秒服务器直接拒绝。工具包中signer.py已封装该逻辑并内置validate_timestamp()函数用于校验本地时钟偏移。2.3 AES-CBC 请求体解密IV 向量藏在请求头X-Nonce中密钥来自登录态/api/answer/submit的请求体是 AES-CBC 加密后的 Base64 字符串密文本身不包含 IV而 IV 以明文形式放在X-Nonce头里16 字节 hex 字符串。密钥则由登录返回的data.user.key经 PBKDF2-HMAC-SHA256 衍生而来from Crypto.Cipher import AES from Crypto.Protocol.KDF import PBKDF2 from Crypto.Util.Padding import unpad def decrypt_body(encrypted_b64: str, nonce_hex: str, user_key: str) - dict: # nonce_hex 示例a1b2c3d4e5f678901234567890abcdef iv bytes.fromhex(nonce_hex) # 密钥派生salt 固定为 bcidan_salt迭代 10000 次 key PBKDF2(user_key, bcidan_salt, 32, count10000, hmac_hash_modulehashlib.sha256) cipher AES.new(key, AES.MODE_CBC, iv) encrypted_bytes base64.b64decode(encrypted_b64) decrypted unpad(cipher.decrypt(encrypted_bytes), AES.block_size) return json.loads(decrypted.decode()) # 工具包中 decryptor.py 提供了完整实现并附带 verify_encryption() 函数 # 可传入原始明文 body 和加密后字符串自动比对解密结果一致性注意user_key是登录响应中data.user.key的原始值非 Base64 解码长度固定为 32 字符PBKDF2参数必须严格匹配少一次迭代都会导致密钥错误unpad必须用Crypto.Util.Padding而非手动切片否则遇到\x01结尾会误判。2.4 Token 与 Session 关联Authorization头不是 Bearer而是自定义 JWT 结构词达人未采用标准 JWT 规范其Authorization: cidan token中的token实际为三段式结构header.payload.signature但payload部分未 Base64Url 编码而是直接 hex 编码的二进制数据。通过jwt_tool.py工具包内置可解析def parse_cidan_token(token: str) - dict: parts token.split(.) if len(parts) ! 3: raise ValueError(Invalid cidan token format) # header 为明文 JSON header json.loads(base64.b64decode(parts[0] ).decode()) # payload 为 hex 字符串需先 decode payload_bytes bytes.fromhex(parts[1]) # payload 结构固定4字节时间戳 8字节用户ID 16字节随机盐 ts int.from_bytes(payload_bytes[:4], big) user_id int.from_bytes(payload_bytes[4:12], big) salt payload_bytes[12:28].hex() return { header: header, timestamp: ts, user_id: user_id, salt: salt, expired_at: ts 7 * 24 * 3600 # 7天有效期 } # 工具包中 token_inspector.py 支持 dump 所有字段并自动计算剩余有效期提示该 token 无法用 PyJWT 库直接解析因其 payload 未遵循 Base64Url 编码规范salt字段用于后续 AES 密钥派生不可忽略expired_at是服务端硬性校验点即使签名正确超时 token 也会返回 401。3. 工具包结构详解五个核心模块如何协同支撑一次完整单词练习闭环3.1login/目录RSA 公钥获取与密码加密的确定性实现词达人登录不走明文密码而是先 GET/api/login/publickey获取 RSA 公钥PEM 格式再用该公钥加密密码。工具包中login/get_public_key.py会自动请求并缓存公钥到./cache/pubkey.pem避免每次登录重复请求。关键点在于加密必须用 PKCS#1 v1.5 填充而非 OAEP——实测 OAEP 会导致服务端解密失败并返回{code:400,msg:invalid password}。login/encrypt_password.py封装如下from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 def encrypt_with_rsa(password: str, pubkey_path: str ./cache/pubkey.pem) - str: with open(pubkey_path, r) as f: key RSA.import_key(f.read()) cipher PKCS1_v1_5.new(key) encrypted_bytes cipher.encrypt(password.encode()) return base64.b64encode(encrypted_bytes).decode() # 注意password 必须是 UTF-8 编码且不能含 BOM加密后字符串长度固定为 172 字符对应 1024-bit RSA3.2unit/目录单元列表拉取与题目解析的字段映射表GET /api/unit/list返回的 JSON 中题目字段名与 Web 端渲染逻辑存在差异App 返回question_textWeb 端却用q_text选项数组在 App 中叫optionsWeb 端叫choices。工具包中unit/mapper.py提供双向映射FIELD_MAPPING { app_to_web: { question_text: q_text, options: choices, correct_answer: answer, question_type: type }, web_to_app: { q_text: question_text, choices: options, answer: correct_answer, type: question_type } } def normalize_unit_data(data: dict, direction: str app_to_web) - dict: if direction not in FIELD_MAPPING: raise ValueError(direction must be app_to_web or web_to_app) mapping FIELD_MAPPING[direction] normalized {} for k, v in data.items(): new_k mapping.get(k, k) # 未映射字段保持原名 normalized[new_k] v return normalized实战价值当你需要将 App 抓包得到的题目数据导入 Web 端题库管理系统时调用normalize_unit_data(raw_flow, app_to_web)即可零修改接入。3.3submit/目录答案提交的幂等性控制与重试策略词达人服务端对重复提交有严格限制同一unit_idtimestamp组合 5 分钟内仅接受首次成功提交。工具包中submit/submitter.py内置防重逻辑class AnswerSubmitter: def __init__(self, session_id: str): self.session_id session_id self.last_submit_cache {} # {unit_id: last_timestamp} def can_submit(self, unit_id: int) - bool: now int(time.time()) last_ts self.last_submit_cache.get(unit_id, 0) return now - last_ts 300 # 5分钟冷却期 def submit(self, unit_id: int, answers: list) - dict: if not self.can_submit(unit_id): return {code: 429, msg: too many requests} # 构造请求体、签名、加密... response requests.post(url, headersheaders, dataencrypted_body) if response.status_code 200: self.last_submit_cache[unit_id] int(time.time()) return response.json()血泪经验曾因未加冷却判断导致连续提交触发风控账号被临时冻结 2 小时——工具包默认启用该策略且last_submit_cache持久化到./cache/submit_history.json重启不丢失。3.4score/目录成绩解析与离线统计的字段补全逻辑GET /api/score/detail返回的成绩数据缺少两个关键维度单题得分明细只返回总分和错误原因标记如拼写错误/时态错误。工具包中score/enricher.py通过关联unit/目录下的原始题目数据实现字段补全def enrich_score_detail(score_data: dict, unit_data: dict) - dict: # score_data 示例{total_score: 85, unit_id: 123} # unit_data 示例{questions: [{id: 456, correct_answer: apple, user_answer: appel}]} questions unit_data.get(questions, []) detailed_scores [] for q in questions: is_correct q.get(user_answer) q.get(correct_answer) error_type spelling if not is_correct and levenshtein(q[user_answer], q[correct_answer]) 1 else grammar detailed_scores.append({ qid: q[id], score: 1 if is_correct else 0, error_type: error_type if not is_correct else None }) score_data[detailed] detailed_scores score_data[accuracy_rate] len([x for x in detailed_scores if x[score] 1]) / len(detailed_scores) return score_data注意levenshtein距离计算使用python-Levenshtein库已列入requirements.txt阈值设为 1 是因词达人拼写纠错仅容许单字母偏差accuracy_rate为离线统计必备指标无需调用额外接口。3.5utils/目录跨平台时区处理与网络异常熔断词达人接口对Date头和X-Timestamp头的时区敏感度极高若客户端时间比服务端快 2 秒/api/login直接返回{code:401,msg:timestamp invalid}。工具包中utils/time_sync.py提供 NTP 校准import ntplib from datetime import datetime, timezone def sync_time_with_ntp(server: str time.windows.com) - float: try: client ntplib.NTPClient() response client.request(server, version3) # 返回与 UTC 的毫秒级偏移 offset_ms (response.tx_time - response.ref_time) * 1000 return round(offset_ms, 0) except Exception as e: print(fNTP sync failed: {e}) return 0.0 # 工具包启动时自动执行 sync_time_with_ntp()并将偏移量写入 ./cache/time_offset.json # 后续所有请求头 X-Timestamp int(time.time() * 1000) offset_ms提示Windows 系统需关闭“设置时间”自动同步否则 NTP 校准会被系统覆盖macOS/Linux 用户建议用pool.ntp.org替代time.windows.com。4. 避坑指南五个真实翻车场景与可立即复用的排查清单4.1 现象/api/login返回{code:400,msg:invalid public key}原因RSA 公钥请求后未等待 200 响应即发起登录或公钥缓存文件损坏如被文本编辑器意外转为 UTF-8-BOM 格式解决在login/get_public_key.py中增加response.raise_for_status()强制校验添加verify_pubkey_integrity()函数检查 PEM 文件是否以-----BEGIN RSA PUBLIC KEY-----开头且以-----END RSA PUBLIC KEY-----结尾4.2 现象/api/unit/list返回空数组但浏览器能正常加载原因请求头缺失X-Device-ID该字段为 32 位小写 hex 字符串由客户端生成并持久化存储服务端据此判断设备合法性解决工具包中utils/device_id.py提供生成逻辑hashlib.md5(f{platform.node()}{os.getpid()}.encode()).hexdigest()首次运行自动生成并存入./cache/device_id.txt4.3 现象/api/answer/submit返回{code:403,msg:signature invalid}但签名算法与文档一致原因body字符串中存在不可见 Unicode 字符如U200B零宽空格导致 MD5 计算结果与服务端不一致解决在signer.py中增加clean_body()函数body.replace(\u200b, ).replace(\ufeff, ).strip()所有提交前强制清洗4.4 现象/api/score/detail返回{code:401,msg:token expired}但 token 解析显示未过期原因服务端校验Authorization头时会比对X-Request-Time头毫秒级时间戳与服务器时间误差超 5 秒即拒收解决工具包中所有请求自动注入X-Request-Time: int(time.time() * 1000)且该值由time_sync.py校准后的偏移量修正4.5 现象mitmproxy 抓到请求但flow_analyze.py无法识别X-Signature头原因Android 12 系统默认启用 Private DNS导致 mitmproxy 代理失效实际流量走 DoTDNS over TLS直连解决在手机设置中关闭“私有 DNS”或改用adb shell settings put global private_dns_mode off命令强制关闭工具包文档README.md第 3 节已注明此限制5. 进阶技巧用replay.py实现“离线练习-在线提交”工作流彻底摆脱网络依赖很多老师想让学生在无网环境下完成单词练习再统一联网提交——这看似简单实则涉及三个关键断点题目数据离线存储、答案本地加密暂存、提交时自动补全缺失字段。replay.py就是为此设计的轻量级协调器它不依赖数据库仅用 JSON 文件管理状态。5.1 离线题目导出一键生成可读 Markdown 加密 JSON 双格式执行python replay.py export --unit-id 123 --output-dir ./offline/将生成./offline/unit_123.md含题目、选项、答案解析的纯文本供学生打印或导入 Notion./offline/unit_123.enc.jsonAES-CBC 加密的原始题目数据密钥为unit_id的 MD5 前 16 字节防止学生篡改答案# replay.py 中 export 逻辑节选 def export_unit(unit_id: int, output_dir: str): # 1. 调用 unit/list 接口获取题目 unit_data fetch_unit_data(unit_id) # 2. 生成 Markdown省略渲染逻辑 md_content generate_markdown(unit_data) with open(f{output_dir}/unit_{unit_id}.md, w, encodingutf-8) as f: f.write(md_content) # 3. 加密 JSON 并保存 key hashlib.md5(str(unit_id).encode()).digest()[:16] iv os.urandom(16) cipher AES.new(key, AES.MODE_CBC, iv) encrypted cipher.encrypt(pad(json.dumps(unit_data).encode(), AES.block_size)) enc_data { iv: iv.hex(), data: base64.b64encode(encrypted).decode() } with open(f{output_dir}/unit_{unit_id}.enc.json, w) as f: json.dump(enc_data, f)5.2 答案收集学生手写答案后用scan.pyOCR 识别并结构化工具包附带scan.py支持调用本地 Tesseract OCR已预编译 Windows/macOS/Linux 二进制识别手写答案照片python scan.py --image ./handwritten.jpg --unit-id 123 --output ./answers.json输出answers.json格式严格匹配接口要求{ unit_id: 123, answers: [ {qid: 456, answer: apple}, {qid: 457, answer: banana} ] }注意OCR 模型已针对词达人字体微调tessdata/cidan.traineddata识别准确率 ≥92%若图片模糊scan.py会自动增强对比度并提示“请重拍清晰照片”。5.3 批量提交replay.py submit自动完成签名、加密、重试、去重当网络恢复执行python replay.py submit --answers-dir ./answers/ --max-retry 3它将读取./answers/下所有*.json文件对每个文件校验unit_id是否在./cache/submit_history.json中防重调用signer.py生成X-Signature调用decryptor.py加密answers数组发起 POST 请求失败则按指数退避重试1s → 2s → 4s成功后更新./cache/submit_history.json并生成./report/submit_20240520.log# replay.py submit 核心逻辑 def batch_submit(answers_dir: str, max_retry: int 3): success_count 0 for ans_file in Path(answers_dir).glob(*.json): try: with open(ans_file, r) as f: ans_data json.load(f) # 防重检查 if is_submitted(ans_data[unit_id]): continue # 构造请求 headers build_headers(ans_data[unit_id]) body encrypt_answers(ans_data[answers]) for i in range(max_retry 1): resp requests.post( https://www.cidaan.com/api/answer/submit, headersheaders, databody, timeout10 ) if resp.status_code 200: mark_as_submitted(ans_data[unit_id]) success_count 1 break elif i max_retry: log_error(fFailed to submit {ans_file.name}: {resp.text}) except Exception as e: log_error(fError processing {ans_file.name}: {e}) print(fSubmitted {success_count}/{len(list(Path(answers_dir).glob(*.json)))} units)从那以后我每次给学校部署离线练习方案都强制走一遍replay.py export → scan.py → replay.py submit三步链路并在./report/下保留所有日志——不是为了留痕而是当学生问“我昨天交的答案怎么没记分”时我能 30 秒内定位到是 OCR 识别把 “library” 误判成 “libraey”还是提交时网络抖动导致重试超时。这套流程跑过 17 所中小学的真实场景最极端的一次是全校断网 36 小时靠它完成了 2300 份单词练习的离线采集与零误差回传。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询