JS逆向实战:从“观鸟网”案例解析前端加密与数据获取
1. 项目概述从“观鸟网”看前端安全与数据获取的博弈最近在整理JS逆向的实战案例一个名为“观鸟网”的站点进入了我的视野。这并非一个真实的观鸟爱好者网站而是一个在逆向学习圈里流传的、专门用于练习JS逆向技术的“靶场”。它的价值在于它模拟了真实网站中常见的、用于保护核心数据接口的前端加密逻辑。对于前端开发者、安全研究员或是需要处理复杂数据采集的工程师来说这类案例就像一本活的教科书能让你直观地理解现代Web应用如何在前端与后端之间构建一道“数据防火墙”以及我们如何在不触及服务器核心逻辑的前提下从客户端“读懂”并复现这套通信规则。简单来说“观鸟网”案例的核心挑战是网站列表数据或详情页内容的加载并非通过简单的GET /api/birds这样的明文请求完成。当你点击翻页或查看详情时浏览器会向服务器发送一个请求这个请求的URL参数或请求体中包含了一些看似乱码的加密字符串。服务器只有解密这些字符串才能理解客户端的意图并返回正确数据。我们的目标就是逆向分析出生成这些加密参数的JavaScript代码逻辑并用Python或其他语言将其复现从而能够程序化地构造合法请求获取数据。这不仅仅是“爬虫”技巧更深层次的是对Web应用安全架构的理解。通过这个案例你会接触到浏览器开发者工具的深度使用、JavaScript代码调试与断点技巧、常见加密算法如Base64、AES、RSA、MD5、SHA系列在Web中的变形与应用以及最令人头疼的代码混淆与压缩后的逻辑追踪。接下来我将以一个从业者的视角完整拆解针对此类站点的通用逆向分析思路与实操步骤。2. 核心思路与逆向方法论面对一个需要逆向的网站最忌讳的就是一头扎进浩如烟海的JavaScript文件中。一个系统性的分析思路能极大提升效率减少在无用代码中徘徊的时间。2.1 逆向分析的核心流程我的通用流程可以概括为“由外而内由果溯因”现象观察与接口定位首先正常使用网站用浏览器开发者工具的“网络”Network面板捕获所有网络请求。重点关注在触发数据加载如翻页、搜索、点击详情时新产生的XHRXMLHttpRequest或Fetch请求。这些请求的URL、请求头Headers、请求体Payload是我们分析的重点。通常加密参数会出现在查询字符串Query String或请求体Form Data / JSON中。关键请求识别与参数关联从捕获的请求中找出那个真正返回了目标数据如JSON格式的列表数据的请求。然后对比多次操作如翻到第1页和第2页产生的请求找出变化的部分和不变的部分。变化的部分往往就是我们需要攻破的加密参数它可能包含了页码、时间戳、用户令牌或其他业务参数。加密入口定位这是最关键的一步。我们需要找到是哪个JavaScript函数生成了这个变化的加密参数。通常有两种主流方法搜索定位法在开发者工具的“源代码”Sources面板中全局搜索CtrlShiftF加密参数中的关键常量。例如如果参数名是sign、token、encryptData或者参数值中包含Base64填充符、/、Base64字符或明显的固定字符串都可以尝试搜索。堆栈追踪法XHR/Fetch断点在“网络”面板中找到目标请求右键选择“Copy - Copy as cURL”或查看其“发起者”Initiator标签页。更好的方法是在“源代码”面板中找到“事件监听器断点”Event Listener Breakpoints展开“XHR”或“Fetch”勾选“send”或“fetch”。然后回到网页触发操作代码执行会在发起网络请求的瞬间暂停调用堆栈Call Stack会清晰地展示出从点击事件到最终调用send方法的完整JavaScript执行路径加密逻辑必然在这条路径上。逻辑分析与代码还原在断点处或搜索到的代码处结合“控制台”Console和“作用域”Scope面板逐步调试Step Over, Step Into。观察参与加密计算的原始数据如页码、时间戳、使用的加密算法可能是CryptoJS、window.btoa、或自定义函数、以及密钥Key或初始化向量IV的来源。目标是将这段JavaScript逻辑逐行翻译成你熟悉的编程语言如Python。本地复现与验证将分析出的逻辑用Python实现用相同的输入参数如页码1运行看生成的加密字符串是否与浏览器捕获的一致。如果一致恭喜你逆向成功。如果不一致需要回头检查是否有环境依赖如浏览器特有的window对象属性、随机数如Math.random()或未捕获的隐式转换。2.2 针对“观鸟网”的初步侦查根据经验这类练习站点的加密方式不会用到特别复杂的非对称加密如RSA更可能采用以下几种方式或其组合Base64编码/解码这不算加密但常用于混淆。btoa()编码和atob()解码是浏览器内置函数。注意它可能不是对原始参数直接编码而是先拼接成一个字符串再编码。AES/DES对称加密使用CryptoJS库非常常见。你需要找到key、iv、mode如CBC、padding如Pkcs7等参数。消息摘要MD5, SHA1, SHA256用于生成签名sign确保参数未被篡改。通常将所有参数按特定规则排序后拼接再加上一个固定的secret盐然后计算摘要。自定义混淆算法开发者自己写的一套字符变换、位移、替换规则。这需要耐心地跟读代码。实操心得第一步的“网络抓包”一定要在无痕模式或清空缓存的浏览器中进行避免插件或其他缓存数据干扰。勾选上“Preserve log”保留日志并禁用缓存Disable cache确保捕获所有请求。3. 实战工具链与关键技巧详解工欲善其事必先利其器。JS逆向不仅仅是智力活也是工具活。下面我详细拆解每个环节需要用到的工具和高级技巧。3.1 浏览器开发者工具的深度使用大多数人只用它看元素和日志但逆向时这几个功能是核心网络面板Network过滤使用XHR、JS、Doc等过滤器快速定位请求。对于“观鸟网”主要看XHR和Fetch。重放Replay右键请求选择“Copy as cURL”然后可以在命令行或Postman中重放这是验证请求是否成功复现的黄金标准。也可以直接“编辑并重发”Edit and Resend在浏览器内快速修改参数测试。搜索在所有捕获的请求中搜索关键参数名或片段可以快速找到关联的请求。源代码面板Sources文件树与代码美化找到疑似加密的JS文件通常不是jquery.js这类库而是带有项目名的app.xxxx.js或chunk-xxx.js点击底部{}美化代码。断点Breakpoint除了行号断点更要善用条件断点。比如在加密函数入口处设置断点条件为param.page 2这样只有翻到第二页时才暂停效率极高。监听器断点Event Listener Breakpoints如前所述勾选XHR/Fetch的send是定位入口的神器。全局搜索支持正则表达式。搜索encrypt、sign、md5、CryptoJS、encode等关键词。控制台面板Console实时交互在断点暂停时可以在控制台打印或修改当前作用域内的任何变量用于测试你的理解。例如console.log(JSON.stringify(encryptFunction.toString()))可以输出函数的完整源码。函数重写你可以临时重写某个函数让它直接返回你期望的值用于绕过某些检查或快速测试后续逻辑。例如window.btoa function(data) { console.log(btoa called with:, data); return your_fake_base64; }应用面板Application查看Local Storage、Session Storage、Cookies。有时密钥或令牌会存储在这里并在加密时被读取。3.2 代码混淆与反混淆策略“观鸟网”的JS代码很可能被混淆过。混淆常见形式有变量名缩短function getBirdList()变成function a(b)。字符串常量加密将api/bird/list这样的字符串变成\x61\x70\x69\x2f\x62\x69\x72\x64\x2f\x6c\x69\x73\x74或atob(YXBpL2JpcmQvbGlzdA)。控制流平坦化将简单的if-else、for循环打乱插入大量无用的switch-case和跳转让代码执行流程难以阅读。僵尸代码注入插入大量永远不会被执行的无意义代码片段。应对策略不急于反混淆先尝试在混淆的代码中调试。因为核心逻辑如调用加密库函数、参数拼接的“形状”通常还能辨认。使用反混淆工具对于复杂的混淆可以借助工具。浏览器插件如ReRes可以拦截请求将混淆的JS文件替换为本地美化后的版本。更专业的离线工具有jsnice、prepack、astexplorer等但要注意完全自动化反混淆可能破坏代码可执行性。动态Hook这是更高级的技巧。通过注入自定义的JS脚本可以用浏览器插件如Tampermonkey或Fiddler/Charles的AutoResponder功能在关键函数如JSON.parse、Object.defineProperty、Function.prototype.toString上设置“钩子”在它们被调用时打印出参数和结果从而绕过静态代码分析。例如HookCryptoJS.AES.encrypt就能直接捕获明文、密钥和加密结果。3.3 Python复现的关键细节当你在浏览器中理清逻辑后用Python复现时有几个坑点一定要避开编码问题JavaScript的字符串是UTF-16而Python 3的字符串是Unicode。但在进行加密计算如MD5、SHA256时操作的对象通常是字节bytes。确保在拼接参数和计算签名前将字符串正确编码为字节如.encode(utf-8)。Base64编码时注意JS的btoa通常处理Latin1字符对于中文可能需要先做URI编码或特殊处理而Python的base64.b64encode接受字节输入。时间戳JS的Date.now()返回毫秒级时间戳13位Python的int(time.time() * 1000)可以对应。有时网站会使用一个经过计算的、包含固定偏移或特定格式的时间戳。数学运算与随机数JS的Math.random()返回[0, 1)的浮点数Python的random.random()行为类似。但如果加密算法依赖一个由Math.random()生成的固定“盐”你需要确保在Python中能复现相同的随机数序列通过设置相同的种子random.seed(...)但这通常很难因为种子往往来自不可控的浏览器环境。更常见的是这个“盐”是固定的或从其他可获取的地方派生。加密库对齐如果使用CryptoJS在Python中通常用pycryptodome或cryptography库对应。你需要精确匹配加密模式CBC/ECB等、填充方式PKCS7等、密钥和IV的格式字符串还是字节是否需要Hex或Base64解码。一个技巧是在JS调试时把key、iv、明文、密文都打印出来然后在Python中用这些值做单元测试确保加密结果一致。4. 模拟“观鸟网”案例的完整逆向过程由于“观鸟网”是一个动态变化的练习平台我无法提供其当前的确切加密代码。但我将构建一个高度仿真的模拟案例展示从零开始的完整逆向流程。我们假设目标网站www.bird-watching-practice.com的列表页接口存在加密签名。4.1 第一步网络抓包与请求分析打开无痕浏览器进入网站列表页。打开开发者工具F12切换到网络面板勾选“保留日志”和“禁用缓存”。点击第二页翻页按钮。在网络请求中发现一个名为getBirdList的XHR请求其状态为200返回了JSON格式的鸟类数据。查看该请求的“标头”和“负载”请求URL:https://api.bird-watching-practice.com/v1/list请求方法: POST请求负载 (Payload):{ page: 2, size: 10, timestamp: 1712345678901, sign: a1b2c3d4e5f67890123456789abcdef0 }响应: 成功的JSON数据。分析page和size是明文业务参数timestamp是当前时间戳sign是一个32位的十六进制字符串疑似MD5签名。我们的目标是找出sign的生成规则。4.2 第二步定位签名生成函数在“源代码”面板中打开“事件监听器断点”勾选“XHR”下的“send”。再次点击翻页第三页。代码执行立即在XMLHttpRequest.prototype.send处暂停。查看“调用堆栈”。在调用堆栈中从上往下看跳过jquery.js等库文件找到一个名为app.8a2f1b.js的文件其中有一个函数generateSign。点击跳转到这个函数并在其第一行打上常规断点然后取消全局的send断点继续执行。代码会在generateSign入口处暂停。此时在“作用域”中可以看到传入的参数。经过调试我们发现generateSign接收一个对象params包含page和size。函数内部逻辑如下已反混淆和简化function generateSign(params) { const secret BIRD_WATCHING_2024_SECRET; // 固定密钥在代码中被混淆成数组拼接 // 1. 参数排序并拼接成 keyvalue 格式 let str ; Object.keys(params).sort().forEach(key { str key params[key] ; }); // 2. 拼接密钥 str key secret; // 3. 计算MD5 const sign CryptoJS.MD5(str).toString().toLowerCase(); return sign; } // 调用 const params {page: 3, size: 10}; const timestamp Date.now(); const sign generateSign({...params, timestamp}); // 最终发送的负载是 {page: 3, size: 10, timestamp: ..., sign: ...}4.3 第三步Python代码复现现在我们使用Python的hashlib库来复现这个签名逻辑。import hashlib import time import json def generate_sign(page, size, timestampNone): 复现JS端的签名生成算法 if timestamp is None: timestamp int(time.time() * 1000) # 毫秒时间戳 secret BIRD_WATCHING_2024_SECRET # 1. 构造参数字典并排序 params { page: page, size: size, timestamp: timestamp } # 2. 按key排序拼接成 keyvalue 格式的字符串 param_str for key in sorted(params.keys()): param_str f{key}{params[key]} # 3. 拼接密钥 sign_str param_str fkey{secret} # 4. 计算MD5 (32位小写) m hashlib.md5() m.update(sign_str.encode(utf-8)) # 注意编码为bytes sign m.hexdigest() return timestamp, sign # 测试生成第3页的签名 timestamp, sign generate_sign(page3, size10) print(fTimestamp: {timestamp}) print(fSign: {sign}) # 构造最终请求负载 payload { page: 3, size: 10, timestamp: timestamp, sign: sign } print(fPayload to send: {json.dumps(payload, indent2)})运行这段Python代码将生成的sign与浏览器抓包获取的第三页请求的sign进行对比。如果一致说明逆向成功。4.4 第四步处理更复杂的情况——AES加密参数假设“观鸟网”的另一个详情接口使用了AES加密。抓包发现请求负载是一个名为data的字段其值是一长串Base64字符串。同样用XHR断点定位到加密函数发现使用了CryptoJS.AES.encrypt。调试发现加密模式为CBC填充为Pkcs7密钥key和偏移向量iv都是固定的字符串但在代码中被atob解码过说明它们以Base64形式存储。const key CryptoJS.enc.Utf8.parse(atob(S2V5MTIzNDU2Nzg5MGFiY2RlZg)); // 解码后为Key1234567890abcdef const iv CryptoJS.enc.Utf8.parse(atob(SXZlY3Rvcl8xMjM0NTY3OA)); // 解码后为Ivector_12345678 const encrypted CryptoJS.AES.encrypt( JSON.stringify({birdId: 1001}), key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ).toString(); // 输出Base64密文Python复现使用pycryptodome库from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 import json def encrypt_bird_id(bird_id): # 与JS端一致的Key和IV (UTF-8编码后的字节) key bKey1234567890abcdef # 注意长度必须是16, 24, 或 32字节 (AES-128, 192, 256) iv bIvector_12345678 # 必须为16字节 # 要加密的数据 data json.dumps({birdId: bird_id}).encode(utf-8) # 创建AES cipher对象模式CBC填充PKCS7 cipher AES.new(key, AES.MODE_CBC, iv) # 加密并转换为Base64 # PKCS7填充由 pad 函数完成 ct_bytes cipher.encrypt(pad(data, AES.block_size)) ct_b64 base64.b64encode(ct_bytes).decode(utf-8) return ct_b64 # 测试 encrypted_data encrypt_bird_id(1001) print(fEncrypted data (Base64): {encrypted_data})将Python输出的Base64字符串与浏览器抓包得到的data字段对比验证一致性。5. 常见问题、调试技巧与高级对抗在实际逆向中绝不会总是一帆风顺。下面是我踩过无数坑后总结的排查清单和进阶技巧。5.1 常见问题排查清单问题现象可能原因排查思路Python生成的签名/加密结果与JS不一致1. 字符串编码差异UTF-8 vs UTF-16。2. 参数拼接顺序或格式不同。3. 密钥、IV等常量值获取错误如多空格、不可见字符。4. 加密算法模式、填充方式不匹配。5. JS环境中有隐藏的全局变量或函数影响了计算。1. 在JS断点处将每一步的中间结果拼接后的字符串、编码后的字节、密钥字节都打印到控制台。2. 在Python中用完全相同的输入直接复制JS打印出的中间字符串执行加密隔离算法逻辑。3. 对比JS和Python中key、iv的字节表示Hex或Base64。4. 查阅CryptoJS和pycryptodome文档确认默认参数。断点无法命中或代码被动态加载1. 代码在eval或setTimeout中执行。2. 使用了Webpack等打包工具代码被分割。3. 有反调试机制如debugger语句循环。1. 使用“事件监听器断点”中的“脚本执行”断点Script - Script First Statement。2. 在“源代码”面板的“页面”标签下查找所有加载的JS文件包括动态注入的script标签。3. 对于反调试可以右键debugger行选择“Never pause here”或使用条件断点绕过。加密函数被极度混淆无法阅读控制流平坦化、字符串加密、变量名混淆。1. 优先尝试动态调试关注函数输入和输出忽略内部复杂流程。2. 使用Function.prototype.toString方法在控制台输出函数源码有时能得到反混淆后的代码。3. 考虑使用AST抽象语法树分析工具进行半自动反混淆但这需要较高的JS功底。请求中有多个加密参数依赖关系复杂参数A的加密结果作为参数B加密的输入或者依赖之前的请求返回的令牌。1. 理清请求顺序。第一个请求往往最“干净”用于获取后续请求所需的token或nonce。2. 全局搜索这些参数名看它们在哪些函数中被赋值或使用。3. 使用“内存断点”在“源代码”面板的“作用域”中右键变量选择“Break on value change”追踪其变化。5.2 高级对抗与Hook技术当网站采用了更复杂的保护措施时可能需要更激进的手段。无限Debugger有些网站会检测开发者工具并循环执行debugger;语句。解决方法在Sources面板找到该行代码右键选择“Never pause here”。在代码执行前通过控制台重写Function构造函数或eval过滤掉debugger关键字。使用浏览器插件如Disable JavaScript暂时禁用该脚本但可能影响功能。环境检测JS代码会检测navigator.webdriver、window.chrome等属性来判断是否由自动化工具如Selenium控制。在纯逆向分析时可以在控制台手动覆盖这些属性Object.defineProperty(navigator, webdriver, {get: () false})。但在编写自动化脚本时需要使用undetected-chromedriver等库。Hook关键函数这是动态分析的利器。在页面加载前或断点处注入Hook代码。例如HookCryptoJS.MD5来捕获所有MD5调用// 在控制台执行或通过Tampermonkey脚本注入 (function() { var originalMD5 CryptoJS.MD5; CryptoJS.MD5 function(message) { console.trace(MD5 called with message:, message); var result originalMD5.apply(this, arguments); console.log(MD5 result:, result.toString()); return result; }; })();同样可以HookJSON.stringify、XMLHttpRequest.prototype.send、window.fetch等。5.3 保持代码的健壮性逆向出的代码往往脆弱因为网站可能更新。在Python复现的代码中你应该将关键常量密钥、IV、盐提取为配置方便随时修改。实现完整的错误处理和日志记录当签名无效时能清晰看到哪一步出了问题。模拟浏览器环境对于依赖window、document等浏览器对象的JS代码可以考虑使用js2py、PyExecJS或Node.js子进程来执行原JS代码片段。但这会引入性能开销和依赖复杂性应作为最后手段。定期验证在生产环境中使用逆向方案时要有机制定期检查接口是否还能正常工作以便及时发现网站更新。逆向“观鸟网”这类项目真正的收获不在于获取那几条鸟类数据而在于掌握了一套应对前端加密的通用分析方法论和工具链。从抓包定位、断点调试到逻辑还原、代码复现每一步都需要耐心和细致的观察。遇到特别复杂的VMP虚拟机保护或wasm加密可能需要更底层的工具和二进制分析知识但那已经是另一个层次的挑战了。对于绝大多数Web接口本文所阐述的方法足以应对。记住核心思路永远是观察现象 - 定位入口 - 动态调试 - 理清逻辑 - 精准复现。多练几个案例这种感觉就会内化成你的本能。