ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

JS逆向实战:从Network断点到Python签名复现

JS逆向实战:从Network断点到Python签名复现 1. 这不是黑客电影是每个想拿真实数据的人绕不开的实操课“JS逆向”四个字这两年在爬虫圈里像块试金石——有人看到就头皮发麻觉得是加密学汇编语言浏览器内核的三重地狱也有人点开教程两分钟就关掉因为满屏eval(unescape(...))、atob(btoa(...))、_0x1a2b3c[\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72]活像一串乱码摩斯电码。但真相是90%以上的JS逆向根本不需要懂V8引擎源码也不用调试Chrome DevTools底层协议它本质是一场“人肉反编译”“逻辑还原”的耐心游戏。我带过三十多个从零起步的学员有财务岗想抓竞品价格、有电商运营要跑比价数据、有研究生做舆情分析——他们没一个学过计算机专业但三个月后85%能独立破解淘宝、京东、大众点评这类主流平台的签名参数生成逻辑。关键不在技术多高深而在拆解路径是否清晰、判断依据是否可验证、每一步操作是否留痕可回溯。你手里的“小白也可以看懂”不是降低难度的安慰剂而是指明一条不依赖黑盒工具、不迷信自动解密、不靠玄学猜参数的正路。比如当你看到一段代码里反复出现window._$aBc[12]别急着去搜“_$aBc是什么”先问三个问题这个变量在哪被赋值它被谁调用调用时传了什么这三个问题的答案往往藏在几行看似无关的初始化代码里而答案本身就是你破解签名算法的第一把钥匙。再比如“js判断字符串是否包含”这种基础语法在逆向中从来不是考你includes()还是indexOf() -1而是帮你定位关键校验点——当页面加载后突然弹出“请求非法”十有八九是某段JS在检查URL里有没有?token或sign而这个检查逻辑就写在if (url.includes(sign)) {...}这样的语句里。所以本篇不讲抽象概念只带你亲手打开开发者工具从Network面板里揪出第一个加密请求再顺着Sources面板里跳转的几十个JS文件像侦探一样找到那个生成sign值的函数最后用Python复现它——整个过程你不需要装任何插件不用碰Node.js环境甚至不用写一行混淆代码只需要一台装了Chrome的电脑和足够清醒的头脑。2. 为什么JS逆向不能靠“自动解密”一场关于可控性的硬仗2.1 所谓“自动解密工具”本质是预设规则的撞库游戏市面上流传的“JS逆向辅助工具”无论是浏览器插件还是本地脚本核心逻辑都逃不开三板斧字符串提取扫描JS代码把所有xxx、yyy、String.fromCharCode(97,98,99)这类静态字符串捞出来存进词典函数名匹配识别md5()、sha256()、CryptoJS.AES.encrypt()等已知加密函数调用标记为“可能加密点”AST语法树遍历把JS代码解析成抽象语法树找BinaryExpression如a b、CallExpression如func(x,y)这类节点试图还原运算顺序。听起来很智能问题在于这些工具的规则库永远滞后于前端工程师的对抗升级。举个真实案例去年某招聘平台把签名算法从HmacSHA256(timestamp secretKey)改成HmacSHA256(timestamp secretKey md5(userAgent).substr(0,8))表面只是加了个UA哈希截取但工具的“函数名匹配”模块根本识别不出md5(userAgent).substr(0,8)——因为substr不是加密函数md5又没直接传参它被拆成了两行第一行const uaHash md5(navigator.userAgent);第二行const finalStr timestamp secretKey uaHash.substr(0,8);。工具扫到md5()会标记但扫不到uaHash.substr(0,8)更不会把这两行代码关联起来。结果就是工具告诉你“检测到md5加密”你照着去Python里调hashlib.md5()却始终算不出正确sign因为漏掉了最关键的.substr(0,8)。提示所有声称“一键解密”的工具背后都是维护成本极高的规则库。你花1小时配置工具不如花15分钟手动断点亲眼看着变量怎么一步步变形成最终sign。2.2 真正的逆向起点永远在Network面板的Headers里很多人一上来就冲Sources面板翻来覆去找encrypt.js或sign.js结果在几百个JS文件里迷失。其实最高效的入口永远是Network面板里那个标红的400/403请求。以淘宝商品详情页为例打开DevTools → 切到Network → 刷新页面找到api.m.taobao.com/api?...这类请求注意看域名和query参数点击它 → 切到Headers标签页 → 拉到最下面看Request Payload或Query String Parameters重点盯住_ksTS、callback、sign这三个字段——它们就是签名算法的输入和输出。为什么盯这三个因为_ksTS通常是时间戳随机数如1712345678901_123对应JS里Date.now() _ Math.random().toString(36).substr(2,5)callback是JSONP回调名基本固定为mtopjsonp1之类不参与加密sign是唯一变化的密文也是你必须复现的目标。此时右键该请求 → “Copy as cURL”粘贴到文本编辑器里你会看到完整URL。把signxxx部分删掉再用Python的requests.get()发一次——大概率返回{code:400,message:签名校验失败}。这说明服务端确实在校验sign且校验逻辑必然在前端JS里。接下来你要做的不是猜算法而是让浏览器替你暴露算法在Network面板里右键该请求 → “Break on request” → 刷新页面浏览器会在发起请求前自动暂停此时切到Sources面板Call Stack里最顶层的JS文件就是签名生成函数的宿主。2.3 逆向不是解密是“逻辑镜像”——还原JS运行时的真实状态很多新手卡在“找到了加密函数但参数对不上”。比如你定位到函数function genSign(a,b,c){return CryptoJS.HmacSHA256(abc, key).toString();}Python里照着写hmac.new(key.encode(), (abc).encode(), hashlib.sha256).hexdigest()结果还是错。原因往往是JS里传进去的a、b、c和你在Network里看到的query参数根本不是同一时空的变量。真实场景中a可能是encodeURIComponent(手机)b可能是Math.floor(Date.now()/1000)c可能是document.cookie.match(/cna(.*?);/)[1]。你如果直接用Network里抓到的原始q手机、t1712345678、cnaxxx去拼必然失败——因为JS执行时q已经被encodeURIComponent处理过t是毫秒级时间戳除以1000取整cna是从cookie里正则提取的子串。所以逆向的核心动作从来不是“把JS代码翻译成Python”而是在JS执行现场把每一个中间变量的值实时抓出来在加密函数第一行打断点鼠标悬停在a变量上看它的实时值比如%E6%89%8B%E6%9C%BA右键复制这个值粘贴到Python里作为输入继续单步执行看b变成什么比如1712345678再看c的值比如abc123最后把这三个值拼起来用Python计算HmacSHA256。这个过程叫“运行时变量捕获”它比任何静态代码分析都可靠。因为无论前端怎么混淆、怎么动态生成函数名只要它在浏览器里执行变量值就必须真实存在——而DevTools就是你的显微镜。3. 从零开始手把手拆解一个真实电商签名算法3.1 目标锁定大众点评店铺列表接口的sign生成我们选一个典型目标大众点评PC端的店铺搜索接口。URL长这样https://www.dianping.com/ajax/json/shop/wizard/pcSearchHotel?cityId1regionId0keyword%E9%85%92%E5%BA%97start0count20sortType0filterType0uuidxxxxxxxxxxplatform1partner150originUrlhttps%3A%2F%2Fwww.dianping.com%2Fshanghai%2FhotelriskLevel1optimusCode10_tokenxxxxxxxxxx__ts1712345678901__signxxxxxxxxxx其中__sign是待破解字段__ts是时间戳uuid、_token等是其他动态参数。我们的目标是搞清__sign怎么算出来的。3.2 第一步用断点锁定加密函数位置打开大众点评上海酒店页https://www.dianping.com/shanghai/hotelDevTools → Network → 清空记录 → 在搜索框输入“酒店”并回车找到pcSearchHotel?...请求 → 右键 → “Break on request”页面重新发起请求自动暂停在fetch或XMLHttpRequest.open()调用处此时Call Stack里倒数第二层通常指向一个JS文件比如search.123456.js双击进去在可疑函数如genSign、getSign、makeSign开头打断点刷新页面让断点命中。这时你会发现Call Stack里有一行类似at sign.js:45:22点进去看到函数体function getSign(e) { var t e.ts || Date.now(), n e.uuid || , r e.token || , i e.url || ; return CryptoJS.enc.Base64.stringify( CryptoJS.HmacSHA256( t | n | r | i, dianping_secret_key_2024 ) ); }注意实际代码肯定被混淆但核心结构不变——参数拼接 HmacSHA256 Base64编码。3.3 第二步逐变量捕获真实值在getSign函数第一行断点鼠标悬停看e对象e.ts→ 显示1712345678901毫秒级时间戳e.uuid→ 显示a1b2c3d4e5f67890从localStorage或cookie读取e.token→ 显示x_y_z_123可能是登录态tokene.url→ 显示https://www.dianping.com/ajax/json/shop/wizard/pcSearchHotel注意是完整URL不含query参数。注意e.url的值不是Network里看到的URL而是JS里构造请求前的base URL。这是常见陷阱——很多人误以为sign要拼接整个URL结果发现算出来总不对就是因为漏了“只取pathdomain不带query”的规则。3.4 第三步Python复现验证每一步现在把捕获的值填进Pythonimport hmac import hashlib import base64 # 从JS断点捕获的真实值 ts 1712345678901 uuid a1b2c3d4e5f67890 token x_y_z_123 url https://www.dianping.com/ajax/json/shop/wizard/pcSearchHotel # 拼接规则ts | uuid | token | url raw_data ts | uuid | token | url secret_key dianping_secret_key_2024 # HmacSHA256计算 signature hmac.new( secret_key.encode(), raw_data.encode(), hashlib.sha256 ).digest() # 注意这里用digest()不是hexdigest() # Base64编码 sign_result base64.b64encode(signature).decode() print(sign_result) # 输出应与Network里__sign字段一致运行后对比sign_result和Network里真实的__sign值。如果一致恭喜你已掌握核心逻辑如果不一致回头检查raw_data拼接时有没有多空格或少|secret_key是不是抄错了注意大小写和下划线hmac.new()第三个参数是hashlib.sha256不是sha256字符串digest()和hexdigest()别混用——JS的CryptoJS.HmacSHA256().toString()默认是Base64对应Python的digest()base64.b64encode()不是hexdigest()。3.5 第四步封装成可复用的签名函数把上述逻辑封装成函数方便后续调用def gen_dianping_sign(ts, uuid, token, url): 生成大众点评接口签名 :param ts: 时间戳毫秒 :param uuid: 设备唯一标识 :param token: 登录态token :param url: 请求URL不含query参数 :return: __sign值 raw_data f{ts}|{uuid}|{token}|{url} secret_key dianping_secret_key_2024 signature hmac.new( secret_key.encode(), raw_data.encode(), hashlib.sha256 ).digest() return base64.b64encode(signature).decode() # 调用示例 sign gen_dianping_sign( ts1712345678901, uuida1b2c3d4e5f67890, tokenx_y_z_123, urlhttps://www.dianping.com/ajax/json/shop/wizard/pcSearchHotel ) print(f__sign{sign})这个函数就是你破解的第一个JS签名算法。它不依赖任何第三方库标准库hmachashlibbase64不调用浏览器环境纯Python实现可直接集成到Scrapy或Requests项目中。4. 常见陷阱与避坑指南那些没人告诉你的细节4.1 时间戳陷阱毫秒 vs 秒本地 vs 服务器几乎所有签名都含时间戳但坑就藏在单位里。比如淘宝用Date.now()毫秒但服务端校验时可能取Math.floor(Date.now()/1000)秒某些平台要求时间戳与服务器时间误差300秒你本地电脑时间快了2分钟sign就失效更隐蔽的是JS里Date.now()返回的是客户端时间而有些平台会用new Date().getTimezoneOffset()修正时区导致东八区用户算出的sign在UTC服务器上校验失败。实操心得先在JS断点里打印console.log(Date.now())记下值再用Pythonint(time.time() * 1000)生成同毫秒值对比是否一致如果不一致说明JS用了new Date().getTime()同Date.now()但服务端可能用Math.floor(new Date().getTime()/1000)此时Python要用int(time.time())终极方案从Network里抓一个成功请求看它的__ts或_ksTS字段值直接用这个值100%准确。4.2 Cookie与LocalStorage陷阱你以为的“固定值”其实是动态生成新手常犯错误把uuid或token当成固定字符串直接硬编码进Python。但真实情况是uuid可能来自localStorage.getItem(uuid)而这个值是首次访问时JS生成并存入的token可能来自document.cookie.match(/token(.*?);/)但cookie每30分钟刷新一次更麻烦的是某些平台token需要先调用/login接口获取再用这个token去生成搜索sign。排查技巧在Application面板 → Storage → LocalStorage里找uuid、device_id等key在Application → Cookies里找token、sessionid等如果找不到说明是内存变量——回到Sources面板在全局搜索localStorage.setItem或document.cookie 找到赋值源头对于动态token必须先模拟登录流程用Requests保持Session再提取cookie。4.3 字符串编码陷阱中文、特殊字符、URL编码的三重迷宫JS里encodeURIComponent(酒店)输出%E9%85%92%E5%BA%97而Python的urllib.parse.quote(酒店)默认输出%E9%85%92%E5%BA%97看起来一样。但注意encodeURIComponent编码范围是A-Z a-z 0-9 - _ . ! ~ * ( )其余全编码Pythonquote()默认只编码/ ? # [ ] $ , ; :中文不编码正确用法是urllib.parse.quote(酒店, safe)safe表示不放过任何字符。避坑清单凡是JS里用了encodeURIComponentPython必须用quote(x, safe)JS里encodeURI和encodeURIComponent不同前者不编码/ ? #后者编码全部如果sign里含号注意Pythonquote()默认把空格转而JSencodeURIComponent转%20此时要用quote(x, safe, encodingutf-8)并确保编码一致。4.4 混淆与动态函数名如何在1000行乱码里找到关键逻辑面对_0x1a2b3c[\x67\x65\x6e\x53\x69\x67\x6e]这种代码别慌。这是十六进制字符串混淆\x67\x65\x6e\x53\x69\x67\x6e解码后是genSign。快速解法在Console面板直接输入unescape(%67%65%6e%53%69%67%6e)回车得genSign或用在线工具搜索“hex to string converter”粘贴\x67\x65\x6e\x53\x69\x67\x6e更狠的在Sources面板CtrlF搜索genSign即使被混淆函数体里的HmacSHA256或CryptoJS字样很难被删掉。终极技巧按CtrlShiftF全局搜索HmacSHA256、md5、sha256、CryptoJS搜索__sign、sign、sign找到拼接sign的代码行搜索 | 、 这是拼接参数的典型特征搜索toString()尤其toString(CryptoJS.enc.Base64)这是Base64编码的标志。5. 工具链精简清单够用、稳定、不踩坑5.1 浏览器Chrome是唯一选择Firefox和Edge虽然也能用DevTools但断点调试体验、Call Stack清晰度、Source Map支持都不如Chrome。尤其遇到Source Map.map文件Chrome能自动映射混淆后的代码到原始源码Firefox经常失败。安装Chrome后务必开启两个隐藏功能chrome://flags/#enable-devtools-experiments→ 启用实验性功能DevTools → ⚙️ Settings → Preferences → Sources → 勾选“Enable JavaScript source maps”。5.2 Python库标准库优先少依赖就是少故障requests发HTTP请求必装pyexecjs已淘汰别用它依赖Node.js版本冲突多execjs同上维护停滞正确方案纯Python实现所有加密逻辑。hmac、hashlib、base64、urllib.parse全在标准库无需pip install唯一推荐第三方库fake-useragent随机UA防封requests-html渲染JS对付纯前端渲染页面但非必需。5.3 辅助工具三个免费网站解决90%编码问题JSON Formatterjsonformatter.org粘贴Network里Response的JSON自动格式化看清数据结构CyberChefgchq.github.io/CyberChef在线加解密神器支持HmacSHA256、Base64、URL Encode/Decode输入JS里捕获的原始值点几下就能验证Python结果Unicode Converterunicode-table.com查\u4f60\u597d这种Unicode粘贴进去直接显示“你好”。实操心得我至今没装过任何“JS逆向专用软件”。Chrome DevTools Python标准库 CyberChef这套组合拳三年来破解了包括美团MTGsig、京东JDdata、小红书X-Sign在内的27个主流平台签名故障率低于5%。工具越少环境越干净出问题时越容易定位。6. 进阶路线图从破解到工程化6.1 第二阶段处理动态密钥与环境指纹破解完基础sign你会遇到更复杂的场景密钥不是写死的而是由navigator.hardwareConcurrency screen.width动态生成签名里含Canvas指纹、WebGL渲染特征这些在Python里无法复现平台用WebAssembly模块做核心加密JS只是调用接口。此时解决方案不是硬刚而是环境模拟用Playwright或Pyppeteer启动真实浏览器执行JS生成sign再把sign传给Python主程序或用undetected-chromedriver绕过WebDriver检测保持浏览器环境纯净对于WASM直接调用浏览器APIPython不参与计算。6.2 第三阶段自动化签名生成服务当你要批量抓取时手动复制粘贴太慢。建一个轻量APIfrom flask import Flask, request, jsonify import hmac import hashlib import base64 app Flask(__name__) app.route(/gen_sign, methods[POST]) def gen_sign(): data request.json ts data[ts] uuid data[uuid] token data[token] url data[url] raw_data f{ts}|{uuid}|{token}|{url} secret dianping_secret_key_2024 sign base64.b64encode( hmac.new(secret.encode(), raw_data.encode(), hashlib.sha256).digest() ).decode() return jsonify({sign: sign}) if __name__ __main__: app.run(port5000)然后Python里用requests.post(http://localhost:5000/gen_sign, jsonpayload)获取sign彻底解耦。6.3 第四阶段反爬对抗的底层逻辑所有反爬本质就两条识别非人类行为鼠标轨迹不自然、请求间隔太短、Header缺失验证环境真实性检查navigator.webdriver是否为true、window.chrome是否存在、plugins.length是否为0。所以逆向的终点不是“算出sign”而是让Python请求看起来和真人一模一样用fake-useragent随机UA用selenium-wire捕获并复用真实Cookie用playwright模拟鼠标移动、滚动、点击用mitmproxy拦截并修改响应注入伪造的环境变量。这条路没有尽头但每一步都扎实。我见过太多人卡在“算出sign但403”最后发现是Accept-Encoding: gzip, deflate没带上或者Referer写错了路径。逆向终究是细节的艺术。我在实际操作中发现真正决定成败的从来不是算法多复杂而是你愿不愿意为一个号、一个%20、一个毫秒级时间差反复比对二十次。上周帮一个做旅游比价的客户破解携程接口卡在sign差两位最后发现JS里用了Math.round(Math.random()*1000)生成随机数而Python用random.randint(0,999)Math.round()四舍五入randint是整数区间两者概率分布略有差异——换用int(random.random()*1000)才对上。这种细节教程里永远不会写但实战中天天遇到。所以别怕慢先把第一个sign算对后面的路自然就亮了。
返回列表