爬虫逆向实战:从抓包定位到Python还原加密签名参数

爬虫逆向实战:从抓包定位到Python还原加密签名参数 做爬虫最怕的不是请求失败而是参数做得像一堵墙。我前两天在抓央视频某个内容接口时就撞上了这样一堵墙直接请求业务接口服务器返回的是{code:400,msg:invalid sign}可在 App 里明明一切正常。把同一个请求参数原封不动复制到 Postman 里重新发依然报错说明这些参数里藏着动态加密内容而且很可能和请求时间绑定了。这篇文章就聊聊我是怎么从抓包定位、JS 断点调试到最后用 Python 还原加密参数、跑通完整请求的。整个过程不依赖付费软件也不需要逆向工程里那些花哨的 hook 框架只要会基本的抓包和 Python 语法就能跟下来。适合那些刚开始接触爬虫逆向、被各种sign、ts、device_id参数折磨得一头雾水的朋友。1. 先把目标参数“逮”出来抓包与关键词定位做加密参数逆向第一步从来不是打开代码编辑器而是先把“这个参数到底长什么样”搞清楚。任何加密算法最终都要落到 HTTP 请求上抓包看到的就是加密参数最终的样子这一步差不了后面才不会白忙。1.1 环境准备代理抓包与 HTTPS 证书抓包工具我习惯用 CharlesFiddler 也行如果你更偏好命令行mitmproxy 也可以。我的组合是 Android 模拟器 Charles模拟器里把 Wi-Fi 代理指向电脑的 8888 端口电脑端打开 Proxy Settings勾选启用 SSL Proxying然后把 Charles 生成的证书安装到模拟器里。具体步骤如下电脑和模拟器处于同一局域网。模拟器中 Wi-Fi 长按修改网络设置手动代理主机写电脑局域网 IP端口写 8888。访问chls.pro/ssl下载并安装证书。Charles 的 Proxy - SSL Proxying Settings 里添加*:443把 HTTPS 流量解开。这里要提醒一句Android 7.0 以上很多 App 默认不信任用户证书抓 HTTPS 会看到一堆Client SSL handshake failed。解决方式有几个用 Android 7.0 以下的老版本模拟器、把证书装进系统证书目录、或者干脆抓 Web 端和小程序端的接口来绕过 App 证书校验。我最开始就卡在这里后来发现同款功能在央视频的小程序端也有接口参数几乎一样抓包难度瞬间降了一个等级。1.2 从请求列表里区分哪些参数是动态的抓包跑通之后先在 App 里点几个页面找到目标业务接口然后盯着请求 URL 和 query 参数看。我拿到的请求长这样GET https://api.example.com/media/video/info?vid885009 deviceida1b2c3d4e5f67890 appid6001 version2.0.2 ts1735212364000 sign0e2f5a8c4b6d3f1a9c7e8b0d2f4a6c8e这串参数里vid是视频 IDdeviceid看着像固定设备标识appid和version是常量ts是毫秒时间戳——每次请求都会变而sign是一段 32 位的十六进制字符串凡是这种长度和字符集的大概率是 MD5 的摘要。为了确认哪些字段参与了加密我把请求重新发一遍。第一次请求里的ts已经过期重新提交还会报无效签名说明服务端校验了时间窗口。如果只改deviceid签名也失效说明设备 ID 大概率也参与签名。用排除法缩小范围最终目标就是回答一个问题sign到底是由哪些字段、按照什么顺序、用什么算法生成的。1.3 全局搜索锁定签名生成代码定位加密代码最快的方式不是逐条阅读 JS而是直接在抓包结果里搜索关键词。加密参数在代码里必然有一个赋值过程常见写法有sign md5(...)sign: CryptoJS.MD5(...)params.sign hexdigest(...)setSign(...)在 Charles 的 Search 功能里输入sign 或sign:定位到包含签名算法的 JS 文件。再打开这个 JS 文件搜索md5、sha1、CryptoJS、hexdigest这些词基本能看到签名函数。有个小经验Web 端和小程序端的 JS 往往比 App 原生包更容易定位。央视频小程序端是典型的前端打包产物打开开发者工具的 Sources 面板搜索后能直接跳到sign生成代码附近它的缩进甚至没有做压缩比直接在 App 里 hook 舒服太多。2. 顺着 JS 执行栈找加密函数断点调试实战很多教程会直接告诉你“签名就是 MD5直接拿 Python 算就行”这样的结论对当下有效却解决不了下次遇到新接口的问题。真正有价值的是顺着 JS 执行栈找到加密函数那一刻你的思路和工具链条才算建立起来。2.1 在 JS 代码里打断点回追调用栈拿到目标 JS 文件后我不急着看完整内容而是先搜索sign赋值的地方。打开 Chrome DevTools 的 Sources 面板在左侧找到当前页面加载的 JS 文件如果没有格式化先点击左下角{}按钮美化。然后按CtrlShiftF全局搜索sign 或sign:点击搜索结果跳到对应的行在行号上单击打一个断点。断点打完后回到页面触发一次业务请求。此时 DevTools 会自动停在断点位置右侧 Call Stack 面板显示了完整的调用链Scope 面板则能看到当前函数作用域内所有变量。我当时的调用链大致是fetchVideoInfo (media.js:482) - handleRequest (utils.js:271) - generateSign (security.js:136)沿着调用栈往上翻很快就能找到真正的签名函数位置。这种“断点往回追”的方式比人肉阅读 JS 高效得多尤其是遇到 Webpack 打包后的代码变量名全是t、e、n直接读懂几乎不现实。2.2 拆开 Webpack 打包逻辑不需要全部看懂央视频小程序端加载的 JS 通常经过 Webpack 打包产物里会出现__webpack_require__这样的模块引用。很多初学者看到这串代码就头皮发麻其实完全没必要。我们在断点处只需要关心当前这个函数在做什么不用管它是怎么被加载进来的。我碰到的情况是这样generateSign函数接收一个对象内部先取出对象的appid、deviceid、version、ts字段把它们拼成一个字符串再套一层md5。关键代码类似function generateSign(data) { var raw appid data.appid deviceid data.deviceid version data.version ts data.ts key4f5e...; return md5(raw).toString(); }注意这里拼参数字符串时用的是固定顺序而且末尾拼接了一个看似随机的key。这个key就是常说的“盐”。签名算法本身并不复杂复杂的是你必须确认哪个字段在前、哪个字段在后、盐值是什么、字符串中间有没有其他符号。差一个字符签名结果就完全不同所以断点调试比猜重要得多。2.3 参数组合规律分析到底哪些字段参与了签名把断点处的变量逐个记下来回到代码里比对比对通常参与签名的字段不会太多。常见组合有两种简单拼接式appidxxdeviceidxxversionxxtsxxkey盐字典排序式先把参数按照 key 的字符顺序排序再拼接最后签名判断是哪一种直接看 JS 代码里有没有sort()或Object.keys(...).sort()。我遇到的央视频接口属于第一种顺序固定不需要排序这对 Python 还原来说特别友好。把这些字段整理成一张表记录下来后面写 Python 时就是照着这张表逐个取值字段名类型来源是否参与签名vidstring业务参数是appidstring常量是deviceidstring设备标识是versionstring常量是tsstring毫秒时间戳是keystring固定盐值是3. 用 Python 把签名算法“翻译”出来确认了签名算法是“固定顺序拼接 MD5”之后Python 还原其实只需要几十行代码。这个阶段的关键不是写代码而是把 JavaScript 的字符串处理逻辑完整地映射到 Python 上。3.1 先厘清参与签名的字段与编码从 JS 断点里我拿到了完整字段顺序和盐值接下来先做一步“模拟签名验证”我把从断点里抄出来的各字段原值按顺序拼接后手动算一个 MD5和请求里的sign对比。如果一致说明算法确认无误。这一步要在正式写 Python 前做避免辛辛苦苦写完代码最后发现是参数字段漏了某一个。最简单的验证方式是在浏览器 Console 里直接执行这段 JS 签名函数或者用 Node.js 跑一遍node -e const md5require(crypto).createHash(md5); console.log(md5.update(appid6001deviceida1b2c3d4e5f67890version2.0.2ts1735212364000key4f5e...).digest(hex))把输出结果和小程序请求头里的sign对比。如果一致说明算法没问题如果不一样回到断点继续查字符串拼接格式。很多签名对不上的原因不是算法错了而是ts用了秒而不是毫秒或者字符串里多了个看不见的换行符。3.2 核心签名函数先看是 MD5 还是 HMAC从 JS 代码中看到md5(...)就能确定是摘要算法。如果看到的是类似HmacSHA256(value, key)那就要用 Python 的hmac模块。两种写法差别不大但要注意MD5 类的签名参数通常拼在原始字符串里HMAC 类的签名密钥是独立参数不会拼进明文字符串。央视频这个接口属于前者。我写了一个通用的签名生成函数方便后续参数调整时直接复用import time import hashlib import requests from urllib.parse import urlencode SALT 4f5e... # 从 JS 中提取的固定盐值真实环境记得替换 def make_sign_v1(params: dict, salt: str) - str: items list(params.items()) # 这里保持 JS 验证时的顺序不要随便排序 raw .join(f{k}{v} for k, v in items) raw fkey{salt} return hashlib.md5(raw.encode(utf-8)).hexdigest()代码非常短但要注意三点第一params里的键值顺序要和 JS 里拼接时的顺序完全一致第二拼接时是用连接还是纯字符串拼接必须以 JS 代码为准第三编码统一用UTF-8。3.3 构造完整请求签名和请求头一起处理签名算法摸清以后请求整个流程就顺了。以下是我最终跑通的完整代码删掉了和具体业务强相关的部分只保留核心结构import time import hashlib import requests SALT 4f5e... VIDEO_URL https://api.example.com/media/video/info def generate_sign(params: dict) - str: raw_string .join(f{k}{v} for k, v in params.items()) fkey{SALT} return hashlib.md5(raw_string.encode(utf-8)).hexdigest() def fetch_video_info(vid: str): # 取一次时间戳后面签名和请求都用同一个 ts str(int(time.time() * 1000)) params { vid: vid, appid: 6001, version: 2.0.2, deviceid: a1b2c3d4e5f67890, ts: ts, } sign generate_sign(params) params[sign] sign headers { User-Agent: Mozilla/5.0 ..., Referer: https://servicewechat.com/, Content-Type: application/json, } resp requests.get( VIDEO_URL, paramsparams, headersheaders, timeout10, ) return resp.json() if __name__ __main__: print(fetch_video_info(885009))这套代码里每一部分都有明确作用ts先存一次保证签名和请求参数完全一致params的字段顺序和参与签名的顺序保持一致sign字段本身不再参与签名请求头里的Referer和User-Agent来自抓包原始请求避免被服务器判定为异常请求。3.4 如果算法太重用 execjs 直接执行 JS 兜底有些接口的加密不只是一次 MD5而是混杂了 AES、RSA 甚至自定义编码Python 重写成本会直线上升。这时候我建议不要硬翻译直接用 Python 调 Node.js 执行原始 JS 代码段。import execjs js_code function generateSign(data) { var raw appid data.appid deviceid data.deviceid ... return md5(raw).toString(); } ctx execjs.compile(js_code) sign ctx.call(generateSign, { appid: 6001, deviceid: a1b2c3d4e5f67890, version: 2.0.2, ts: 1735212364000, })execjs 的思路是把 JS 代码当作黑盒保留原有逻辑避免翻译过程中引入差异。不过它有个明显缺点需要本地有 Node.js 环境执行速度也比纯 Python 慢。我的建议是碰到 MD5/SHA 这类简单摘要优先用 Python 重写碰到 AES、RSA 或好几层自定义混淆时再用 execjs 兜底。4. 从签名到完整请求集成时的细节与坑签名函数写出来只是第一步真正把整个请求流程跑稳定中间还有一堆让人头大的小问题。这些问题刚开始看起来像加密算法有问题最后发现基本都是“签名和请求不一致”或“请求头被校验”导致的。4.1 时间戳先存一遍不要在签名和请求中间再次调用最容易出现的坑就是签名用的时间戳和请求参数里的时间戳不是同一个。看下面这段错误示范params { ts: str(int(time.time() * 1000)), ... } sign generate_sign(params) params[sign] sign # 看起来没问题如果generate_sign内部再次调用time.time()生成 ts而 params 里的 ts 是之前生成的那么服务端用 params 里的 ts 验签就永远对不上。你以为算法错了其实是两个时间戳差了那么零点几秒。正确做法是签名前先把时间戳赋值给一个变量签名和参数字典都用这个变量。ts str(int(time.time() * 1000)) params {...}4.2 参数拼接顺序、大小写与编码问题MD5 的输入一旦改变输出就完全变样。最容易中招的地方有三个大小写JS 里如果先toUpperCase()再拼接Python 也要先统一转大写URL 编码如果某个参数值是中文或特殊字符JS 里可能出现encodeURIComponentPython 里要对应使用urllib.parse.quote拼接符号有的接口用连接有的直接纯字符串连续拼接甚至还会在中间插入固定字符这些必须以断点里的实际代码为准。我自己踩过的坑是JS 里拼接时用了把字符串连接起来而我在 Python 里用join时多加了导致整个签名错误。后来老老实实回到断点处把raw的实际值打印出来和 Python 里生成的字符串做了一次逐位对比才找到问题。4.3 请求头里的 Referer、User-Agent、Cookie 不能乱填签名验证通过之后请求可能依然返回 403 或 412。这时候问题基本出现在请求头。很多服务端会校验Referer是否合法、User-Agent是否像是真实客户端。央视频小程序端接口对Referer很敏感直接发请求需要带上Referer: https://servicewechat.com/。如果你抓的是 App 接口通常还需要带Cookie或Authorization字段。建议直接复制抓包时的原始请求头逐个字段保留不要嫌多。即使有些字段看起来没用先留着也能减少变量。等确认能稳定跑通以后再逐字段删减测试哪些可以省略。4.4 验证“签名通过”后还会遇到的风控签名的本质是反篡改和防重放但它不是唯一的安全手段。请求频率过高、设备 ID 频繁变化、IP 的并发量过大都可能触发风控。我在测试时遇到过一次比较典型的限制连续请求十几条后接口返回了{code:429,msg:too many requests}。这个状态码说明前面签名已经完全正确只是请求频率太高。处理方式就是控制节奏每次请求之间加一个随机延时import time import random for vid in vid_list: fetch_video_info(vid) time.sleep(random.uniform(1.5, 3.5))这里想多说一句不要在爬虫项目里无脑上高并发尤其是对国内主流平台的接口既不符合平台规则也容易被封 IP。刘润那句话放在技术圈一样成立慢慢来比较快。4.5 逆向代码的维护心态加密参数还原不是一次性的工作。平台只要升级了前端代码签名算法就可能变今天能跑的代码明天一个JS文件更新就废了。所以我把签名生成逻辑单独放在一个模块里接口调用模块和签名模块解耦等下次签名算法变了只需要改签名模块。更重要的是每次逆向都要把抓包到定位的过程沉淀成文档。我当时记录了一份包含“参数表 算法验证方法 参考请求头”的小文档下次平台更新我只需要拿着文档重新对一遍字段顺序很快就能恢复请求能力。这个习惯帮我节省了大量重复时间。5. 写在最后爬虫逆向的边界与安全提醒技术层面聊得差不多了最后说点更重要的。逆向解析加密参数这件事在很多技术社区里都处于灰色地带。从学习角度来说分析签名算法、理解前端加密逻辑能帮你深入理解 HTTP 通信、反爬设计、签名机制这些能力在开发高可用系统时非常有用。但从合规角度来说未经授权地批量采集平台数据、绕过访问控制可能带来法律风险。我个人的习惯是除非有明确授权否则只把逆向当作学习手段不做大规模采集更不会把采集到的数据用于商业用途。如果你想练习这类逆向建议选择自己开发的服务端接口做实验或者只在本地模拟微信小程序环境测试签名逻辑效果是一样的。回到技术本身。央视频这个接口的加密强度并不算高搞懂它更像是一道“逆向入门练习题”。真正难的是后续平台升级、参数变化、混合加密、反调试这些进阶内容但只要把“抓包定位 - 断点追栈 - Python 还原 - 请求验证”这套方法练熟了再复杂的东西也能一点一点啃下来。最后分享一个我常用的调试小技巧在 Python 里生成签名后先把整个 URL 完整打印出来放到浏览器地址栏里直接访问。如果浏览器端能拿到数据说明参数和签名都是正确的如果不行就说明还有请求头校验。这个技巧帮我在无数个抓包深夜里省下了整整一小时。希望它也能帮到你。