ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JS加密逆向实战:从getcookie.js到完整爬虫的必经之路

JS加密逆向实战:从getcookie.js到完整爬虫的必经之路 简介一份面向Python爬虫进阶学习者的实战资源包重点解决登录密码加密、Cookie生成、签名计算、滑块验证等JS逆向难题也适合作为毕业设计或课程大作业的参考实现。内容围绕真实网站的加密参数逆向展开覆盖从抓包定位、断点调试到编写Python脚本模拟执行的完整流程预览中包含多家主流站点案例可对比不同加密风格与应对策略。包内共86个文件以42个JavaScript脚本和31个Python脚本为主体另有Markdown说明、代码截图等辅助材料压缩包仅1.13MB按站点模块清晰划分便于按需查阅。目前已有1102人学习下载。通过该资源可掌握CryptoJS、RSA、AES、SHA等常见加密算法的识别与还原思路直接借鉴各案例的爬虫框架与排错经验快速搭建属于自己的JS逆向解决方案。1. 从 getcookie.js 到完整逆向爬虫这个资源包里装了哪些必经之路写一个反爬接口的数据抓取最尴尬的不是不会写 Python而是明明看到接口返回正常 JSON却因为一个 JS 动态生成的cookie参数被弹回 403。这个实战资源包Python爬虫进阶 JS 解密逆向实战.zip里存放的正是这类场景的解法从getcookie.js到各站点的spider.py覆盖 JS 解密、验证码、并发与数据落地完整链路。如果你正在做爬虫方向毕业设计或大作业目标是“数据收集”这套代码能直接拆出多个可运行的实证模块如果你是有经验的工程师它也是评估社区逆向方案的好样本。2. 定位加密入口抓包断点 Node 执行 getcookie.js2.1 先从响应数据反推哪些参数是动态的不管是企查猫还是 IBank 这类接口逆向的第一步不是翻开 JS而是通过浏览器开发者工具 Network 面板把同一网址多刷新几次对比 URL Query、Header 和 Body。通常变化最明显的字段就是加密入口。比如qichamao目录下的搜索接口每次请求的js_token都不一样而它恰好由本地执行qm.js生成IBank的登录接口也是如此密码和签名都依赖getcookie.js产出的临时值。实际操作中我一般会先用 Chrome 的 Copy as cURL 把请求转成 Python 的requests代码再用 Compare 工具做两次请求的字段 diff。这样能快速锁定哪些是时间戳、哪些是随机数、哪些是真正由 JS 计算出的结果。下面这张表总结了常见动态参数的生成方式和定位方法参数名常见生成方式定位方法sign/tokenMD5/SHA 后拼接搜索接口域名和参数名cookieJS 修改 document.cookie断点观察 setCookietimestamp时间戳判断同一秒请求变化只需要把关注点落到那些“看起来无规律、但每次都在变”的参数上再去源码里搜索它被赋值的语句就能找到加密函数的位置。2.2 用 Node 直接执行 getcookie.js让爬虫复用它的输出找到加密函数后最常见的做法是把这个函数所在的 JS 文件在 Node 环境中跑起来拿到和浏览器一致的结果。这个资源包里的getcookie.js就是典型它依赖Crypto-js做 DES/AES 运算只要把Crypto-js路径正确引入Node 直跑几乎没有障碍。# 先确认依赖文件 node -e const getCookie require(./getcookie.js); console.log(getCookie.generate())// getcookie.js 关键导出片段 const CryptoJS require(crypto-js); function generate() { const timestamp Date.now().toString(); const key CryptoJS.enc.Utf8.parse(abc12345); const iv CryptoJS.enc.Utf8.parse(12345678); const encrypted CryptoJS.DES.encrypt(timestamp, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }).toString(); return encrypted; } module.exports { generate };上面的代码演示了两个要点第一module.exports把函数暴露给外部意味着可以用一条命令行直接获得加密结果第二DES 加密时的key和iv都是硬编码这在测试环境里很常见但真实项目里这些值往往存储在页面脚本或接口返回中。等到爬虫里调用时只需要用subprocess或child_process把node当作一个可执行程序调用就能把加密逻辑完全隔离。import subprocess # 调用 Node 执行 getcookie.js node_cmd node -e \console.log(require(./getcookie.js).generate())\ result subprocess.check_output(node_cmd, shellTrue, encodingutf-8).strip() print(cookie value:, result)这段 Python 代码的意义在于不需要理解 JavaScript 的加密实现细节直接依赖已经验证的 JS 结果。shellTrue在这里只用于本地开发部署到服务器时应改为列表式参数避免 shell 注入风险。如果getcookie.js依赖window或document对象说明它在浏览器环境里定义了全局变量此时要改用jsdom补齐环境。提示用 Node 跑 JS 前先确认文件里没有require浏览器专用模块否则先用jsdom把全局环境补上。2.3 缺失环境时用 jsdom 补齐window/document 未定义很多从压缩 JS 里拆出来的代码并不是纯粹的 Node 模块它可能读取navigator.userAgent、document.cookie甚至HTMLCanvasElement。直接用 Node 运行会出现window is not defined这时需要把jsdom的全局变量挂到 Node 进程里。const { JSDOM } require(jsdom); const dom new JSDOM(!DOCTYPE htmlhtmlbody/body/html, { url: https://example.com, userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }); global.window dom.window; global.document dom.window.document; global.navigator dom.window.navigator; // 此时再引入原生的 getcookie.js const getCookie require(./original_getcookie.js); console.log(getCookie.getCookie());注意jsdom并不是万能的某些依赖屏幕渲染或字体的指纹计算仍然会失败。我一般把它当作“能跑通主流程”的兜底如果遇到getContext未实现就直接在浏览器里用 Puppeteer 截取值而不是继续在 Node 端补环境。判断标准是目标加密函数是否需要真实渲染环境如果只做字符串和字节运算jsdom 足够。3. 算法识别与 Python 端融合DES/RSA/AES/MD5 到底怎么选3.1 从文件名和密文特征判断加密算法资源包里Crypto-js下躺着des.js、rsa.js而各项目目录里还有sm4.js、aes_crypto.js、c_sha1.js、c_sha256.js、c_md5.js。初学者最容易犯的错误是逐个尝试所有算法。正确做法是先看密文的形态算法类型密文特点常见文件DES/3DES通常是固定块长Base64 后 24/32 位密钥长度 8/24 字节des.jsAESBase64 长度随明文变化密钥 16/24/32 字节常见 CBC/ECB 模式aes_crypto.jsRSABase64 或 HEX 密文长度较大128/256/512 字节常伴随公钥文件rsa.jsMD5/SHA固定长度 32/40/64 位十六进制c_md5.js, c_sha256.jsSM4国密算法密钥 16 字节CBC/ECB 模式密文也是 Base64sm4.js把密文长度和填充特征记下来判断速度会快很多。例如myToken目录下的token.js生成的是 64 位 HEX 密文几乎可以确定是 SHA-256而Hotel里的password.js输出是 32 位 HEX则更接近 MD5。但要注意很多站点在 MD5 前会拼接盐值所以直接调用标准库往往匹配不上。3.2 用 Python 复现加密结果和 JS 逐项对照对于已在 Node 端跑通的 JS我们可以用pycryptodome在 Python 侧复现方便放进纯 Python 爬虫流程里。以 AES-CBC 为例CryptoJS 里enc.Utf8.parse(key)与 Python 直接使用字节串是等价的但模式、填充和 IV 必须完全一致。from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad import base64 def aes_encrypt(plaintext: str, key: str, iv: str) - str: 与 CryptoJS.AES.encrypt(text, key, {iv: iv, mode: CBC, padding: Pkcs7}) 对齐 key_bytes key.encode(utf-8) iv_bytes iv.encode(utf-8) cipher AES.new(key_bytes, AES.MODE_CBC, iv_bytes) padded pad(plaintext.encode(utf-8), AES.block_size) encrypted cipher.encrypt(padded) return base64.b64encode(encrypted).decode(utf-8) if __name__ __main__: result aes_encrypt(1575360000, 1234567890abcdef, abcdef1234567890) print(result)参数说明plaintext是待加密的字符串这里以时间戳示范key和iv必须是 16 字节AES-128长度不对会导致ValueErrorpad使用 PKCS7它是绝大多数 Web 加密的默认填充方式。运行结果应该和demo.js里输出完全一致。如果不一致优先检查iv是否被转成 UTF-8 字节以及是否误用了ECB模式。3.3 当 Python 复现成本过高时用 RPC 方式把 Node 当加密服务某些加密逻辑涉及浏览器环境、Redis 状态或者被混淆到无法快速翻译成 Python。更稳妥的工程方案是把 Node 变成一个本地 RPC 服务让 Python 通过 HTTP 调用。资源包里的node_service.js就是这种思路的具体实现它用 Express 包了一层接收参数返回加密结果。const express require(express); const crypto require(./demo1.js); const app express(); app.use(express.json()); app.post(/encrypt, (req, res) { const { data } req.body; if (!data) { return res.status(400).json({ error: missing data }); } try { const result crypto.encrypt(data); res.json({ result }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.listen(3000, () console.log(crypto service on 3000));import requests resp requests.post(http://127.0.0.1:3000/encrypt, json{data: 1575360000}) print(resp.json()[result])这段代码的意义在于把“不稳定的 JS 环境”和“稳定的 Python 爬虫”隔离开。Node 服务只负责计算加密值Python 端负责调度、解析和存储。注意要给 Node 服务加超时限制和并发锁否则多个爬虫线程同时调用会暴露同步代码的瓶颈。我一般会在 Node 里用cluster开启多个进程并通过PM2管理保证崩溃后自动重启。4. 验证码与混淆代码geetest、滑块轨迹和 AST 还原4.1 geetest 验证码的验证流程拆解geetest目录下放着ast.js和base.js这两个文件对应极验第三代的完整流程初始化时先请求register接口拿到gt、challenge然后加载ast.js在滑动过程中计算轨迹加密参数w最终提交到validate接口。这里最关键的逆向点是w参数它由轨迹数据、加密时间、随机数等多个部分拼接后再做二次加密得到的。实际抓包时你会发现w的长度会随轨迹点位数量变化因此直接固化字符串不可行。更好的方式是继续沿用 Node 调用ast.js的方式但需要先给每个滑动动作生成一个轨迹数组再把它传给加密函数。由于ast.js内部可能检测浏览器环境建议先用navigator相关属性补齐环境或者在 Pyppeteer 里执行。4.2 用轨迹模拟绕过滑块识别滑块验证码考验的是轨迹是否像人为操作。常见的错误是直接线性移动那样计算出的加速度为 0非常容易被识别。正确做法是模拟先加速再减速的物理过程并加入随机抖动。import random import time def generate_track(distance: int) - list: 模拟人类滑动轨迹从慢到快再到慢附带随机停顿 track [] current 0 mid distance * 0.7 while current distance: if current mid: step random.randint(3, 8) else: step random.randint(1, 4) current step track.append(step) time.sleep(random.uniform(0.01, 0.05)) return track track generate_track(300) print(ftrack points: {len(track)})这里step是每次移动的像素数前 70% 的距离使用较大步长模拟加速后 30% 使用小步长模拟减速。time.sleep模拟每步之间的微小延迟。将这段轨迹作为参数传给ast.js中的加密函数得到的就是可提交的w值。注意不能把轨迹生成逻辑写死在节点服务里最好让 Python 端生成好再发给 Node保持随机性由入口控制。4.3 用 AST 处理混淆 JS从变量重命名到控制流还原很多项目里的 JS 是经过混淆的比如black_box.js里全是_0x4f32这类变量名代码根本没法读。如果要把算法迁移到 Python首先得把变量名还原。用 AST抽象语法树可以批量替换混淆变量名我通常用subprocess调用esprima解析文件再遍历节点做重定向。AST 节点混淆场景还原思路Identifier变量名_0x3f2a根据作用域重命名CallExpression大量函数加壳找到解密函数实参ControlFlow扁平化 switch-case用状态机还原执行流import subprocess import json def parse_js_to_ast(file_path: str) - dict: # 通过 npx esprima 将 JS 文件解析成 JSON AST cmd [npx, esprima, file_path, --json] output subprocess.check_output(cmd, encodingutf-8) return json.loads(output) ast parse_js_to_ast(./black_box.js) print(ast.get(type)) # 输出 Program这段代码的意义不是直接还原所有变量名而是验证你能否把 JS 解析成结构化的 AST。进一步的还原可以配合escodegen将重命名后的 AST 重新生成可读代码或者使用webcrack这类自动化工具。注意 AST 处理对语法严格性要求很高不要用正则替换变量名很容易破坏作用域。对于有经验的工程师来说这里的关键是不要过度投资在还原上如果核心逻辑是几个加密函数直接 RPC 调用更省成本。5. 并发与数据落地从单请求到可用的爬虫调度5.1 线程池和异步怎么选先看目标是速度还是稳定逆向了加密参数之后爬虫的瓶颈通常不再是人机校验而是网络 IO 和被访问站的限流策略。资源包里的spider.py有明显的单线程风格适合学习但真要跑全量数据必须加并发。简单场景用ThreadPoolExecutor足够因为requests是同步库线程池能让多个请求并行。from concurrent.futures import ThreadPoolExecutor, as_completed import requests def fetch(params: dict) - dict: 单条请求封装这里会携带协议头、逆向出的 cookies 等 headers {user-agent: Mozilla/5.0, cookie: js_tokenxxx} resp requests.get(https://api.example.com/search, headersheaders, paramsparams, timeout10) return resp.json() params_list [{page: i, size: 20} for i in range(1, 21)] with ThreadPoolExecutor(max_workers8) as pool: future_map {pool.submit(fetch, p): p for p in params_list} for future in as_completed(future_map): page future_map[future] try: data future.result() print(fpage {page[page]}: {data}) except Exception as e: print(fpage {page[page]} failed: {e})这里max_workers设为 8不是越大越快对未做限速的服务10 个并发已经容易触发封禁。更合理的是先用as_completed观察失败率如果出现 403 或频繁超时就把并发降到 2~3并增加重试退避。5.2 会话保持与代理池轮换如果需要在一次抓取中维持登录态一定要用requests.Session()。它会把set-cookie自动保存并在后续请求中带上。代理池方面不要用随机代理那只是把错误率提高。正确做法是维护一个可用代理列表按权重分配。import requests from itertools import cycle session requests.Session() # 从本地代理池文件读取可用代理 with open(proxies.txt, r, encodingutf-8) as f: proxies [line.strip() for line in f if line.strip()] proxy_pool cycle(proxies) proxy next(proxy_pool) session.proxies {http: fhttp://{proxy}, https: fhttp://{proxy}} resp session.get(https://api.example.com) print(resp.status_code)注意代理格式本地代理池可能是ip:port或user:passip:port。维护方通常要求每秒只能调用一次所以需要给每个代理设置连续失败上限失败超过 3 次就从池中剔除。要不然一个被封锁的代理会拖垮整批请求。5.3 数据存储与去重用 SQLite 做轻量落地中小规模课程设计不需要上 Hadoop直接用 SQLite 即可。为了让数据可回溯我习惯在表里保存原始接口返回的 JSON 快照再把关键字段拆成结构化列。CREATE TABLE IF NOT EXISTS collection ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, response_json TEXT, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, hash_key TEXT UNIQUE ); CREATE INDEX IF NOT EXISTS idx_hash ON collection(hash_key);写入时用hash_key做去重它可以是普通 URL 的 SHA-256也可以是 URL参数组合的摘要。这样每次执行抓取前不需要查库而是依赖 UNIQUE 约束在插入时报错再通过INSERT OR IGNORE跳过重复数据。对于百万级数据记得用executemany批量插入避免逐条提交的性能退化。6. 建立自己的逆向调试工作台断点、日志与可持续维护6.1 用 Puppeteer 录制真实浏览器里的加密值当 Node 端始终无法复现某个加密参数时改用 Puppeteer 打开目标页面手动触发请求然后通过page.on(request)截获请求头。这其实是最后一层兜底但也是验证“JS 环境是否补齐”的黄金标准。const puppeteer require(puppeteer); (async () { const browser await puppeteer.launch({ headless: false }); const page await browser.newPage(); await page.setUserAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64)); await page.on(request, req { if (req.url().includes(/api/)) { console.log(req.headers()[cookie]); console.log(req.postData()); } }); await page.goto(https://example.com/login); await page.type(#password, test123); await page.click(button[typesubmit]); await new Promise(r setTimeout(r, 3000)); await browser.close(); })();这样拿到的 cookie 和表单数据用于对照 Node 端计算结果找出哪个环境变量造成差异。维护逆向代码时我会把“浏览器抓到的值”和“Node 生成的值”分别落到日志文件做一次全量比对通常在几秒内就能定位到是navigator属性缺失还是Date.now()偏差。6.2 用单元测试盯紧算法回归逆向代码最怕的是目标站升级加密算法。给每个函数补充独立的测试用例当getcookie.js输出不再符合正则时测试立即告警。这里用到pytest对 Python 侧进行断言而不是测试 JS 本身。import pytest from crypto_service import aes_encrypt def test_aes_encrypt_against_js(): # JS 端已算好的期望值 expected 7H3B6UinHgvM2YQpbOfvyg assert aes_encrypt(1575360000, 1234567890abcdef, abcdef1234567890) expected测试的意义是保留一份“当时正确”的样本。当加密算法调整后测试失败会提示你 JS 和 Python 哪一侧需要同步更新。我会把这类测试放在 CI 的定时任务里每 10 分钟跑一次一旦目标站改版邮件和飞书机器人就会收到通知。6.3 常见坑不要把逆向代码写到业务代码里最后一个建议是把所有逆向代码独立成目录和爬虫业务代码分开资源包顶层也做了类似划分。这样每次目标站 JS 更新时只需要重新抽取 JS 文件替换 Node 服务或 Python 加密模块而不会影响调度和存储模块。逆向工作台维护的核心就是隔离环境隔离、代码隔离、数据隔离有了这三层隔离再复杂的 JS 解密也更像是换一个黑盒而不是重写整个系统。本文还有配套的精品资源点击获取
返回列表