JS逆向实战:从登录加密到Python复现的完整流程解析

📅 2026/7/1 21:42:47
JS逆向实战:从登录加密到Python复现的完整流程解析
1. 项目概述从登录到数据一次完整的JS逆向实战做爬虫的朋友尤其是用Python的最常遇到的“拦路虎”之一就是那些需要登录才能获取数据的网站。你兴冲冲地写好了请求却发现返回的不是你想要的数据而是一句冷冰冰的“请先登录”。更让人头疼的是当你打开浏览器的开发者工具准备模拟登录请求时发现密码字段是一串看不懂的乱码或者提交的表单里藏着一个叫sign、token或者encryptedPassword的奇怪参数。恭喜你你遇到了前端加密也就是我们常说的JS逆向。今天这个实战项目我们就以“某乐网”的登录过程为例手把手带你走一遍完整的JS逆向分析流程。这不仅仅是一个案例更是一套可以复用到其他类似网站上的方法论。我们会从最基础的请求观察开始一步步定位关键加密代码理解其逻辑最后用Python完整地复现整个登录过程。无论你是刚接触JS逆向的新手还是想系统梳理一下思路的老手相信都能从中获得启发。整个过程的精髓在于“观察、定位、理解、复现”这八个字。2. 逆向前的侦察网络请求与关键参数分析在动手写一行代码之前充分的侦察是成功的一半。盲目地扎进JS文件里就像在黑暗的迷宫里乱撞。2.1 捕获登录请求首先我们打开目标网站这里我们统称为“某乐网”的登录页面。按下F12打开开发者工具切换到Network网络面板。为了清晰记得勾选上“Preserve log”保留日志防止页面跳转后请求记录被清空。然后在登录页面的表单里输入你的测试账号务必使用测试账号切勿使用个人重要账号和密码点击登录按钮。此时Network面板会刷出一系列新的请求。我们的目标就是找到那个真正提交登录信息的请求。通常这个请求会有以下特征请求方法为POST因为提交表单数据。请求URL包含login、signin、auth等关键字。请求的Form Data或Payload里包含用户名和密码或加密后的密码。找到这个请求后点击它查看其详细信息。重点是Headers和Payload两个标签页。2.2 解析请求载荷与参数在Payload标签页下我们通常能看到两种视图view source原始视图和view URL-encoded表单视图。这里我们看表单视图它更直观。你可能会看到类似这样的数据username: test_user password: aBcDeFgHiJkL1234567890 rememberMe: true或者更复杂一些的account: test_user pwd: 7a57a5a743894a0e sign: 1a2b3c4d5e6f7890abcdef1234567890 timestamp: 1646389473000核心发现password或pwd字段的值明显不是我们输入的明文密码例如“123456”。它可能是一串Base64编码的字符串也可能是一段十六进制的哈希值或者是一个很长的乱码。同时请求里可能还多出了我们没有直接输入的参数比如sign、timestamp、nonce等。这些多出来的、被加密的参数就是JS逆向需要攻克的核心目标。我们的任务就是找出前端JavaScript代码是如何生成这些参数的。注意在分析过程中你可能会在Headers里看到Cookie中有一些像是SESSION或token的字段。请注意这些通常是服务端返回的、用于维持登录状态的凭证而不是我们本次逆向的目标。我们的目标是生成登录请求时提交的加密参数这两者不要混淆。2.3 初步判断加密类型通过观察加密后的字符串可以做一些初步猜测结尾有或很可能是Base64编码。固定长度的十六进制字符串如32位、40位、64位可能是MD5、SHA1、SHA256等哈希算法。长度不固定且包含特殊字符的乱码可能是AES、RSA等对称或非对称加密或者自定义的混淆算法。有了这些基本信息我们就可以进入下一步深入JavaScript的“心脏地带”。3. 深入JS腹地定位与解析加密函数知道要找什么之后下一步就是去哪儿找。所有前端的逻辑都藏在加载的JavaScript文件里。3.1 搜索关键入口在开发者工具的Sources源代码面板或直接在整个开发者工具界面按Ctrl Shift FWindows或Cmd Opt FMac打开全局搜索。我们可以用以下关键词进行搜索加密参数名直接搜索password、pwd、encrypt、sign。可能的加密函数名搜索MD5、SHA1、RSA、AES、encrypt、encode。事件监听搜索addEventListener、onclick、submit找到登录按钮绑定的点击事件处理函数。例如搜索password你可能会在某个JS文件里找到类似这样的代码$(#loginBtn).on(click, function() { var username $(#username).val(); var password $(#password).val(); var encryptedPwd encryptPassword(password); // 关键函数 // ... 后续组装数据并发送请求 });这里的encryptPassword就是我们梦寐以求的加密函数。点击搜索结果就能跳转到该函数定义的位置。3.2 使用“XHR/断点”进行精准定位如果全局搜索效果不佳或者代码被混淆得难以阅读我们可以使用更高级的“请求断点”功能。回到Network面板找到我们之前捕获的那个登录POST请求右键点击它选择“Copy” - “Copy as cURL”。然后在Sources面板中找到右侧的“XHR/Fetch Breakpoints”。点击号将我们复制的请求URL的一部分通常是包含login的关键路径粘贴进去并添加断点。设置成功后再次点击登录按钮。此时浏览器会在发送这个登录请求之前自动暂停Paused in debugger。这时调用堆栈Call Stack会显示当前暂停位置的所有函数调用链。我们沿着调用栈一层层往下看通常是从上往下从最新的调用往更早的调用看寻找那些看起来像是处理我们输入数据的函数特别是包含password、username、data等变量的地方。找到后就在该行代码前点击设置行断点然后刷新页面重新触发进行单步调试。3.3 动态调试与“跟值”定位到可疑函数后最关键的步骤就是动态调试。在加密函数的关键行设置断点然后重新触发登录。当代码执行到断点处暂停时你可以观察“Scope”作用域或“Console”控制台查看此时局部变量、全局变量的值。将鼠标悬停在变量上也能看到其当前值。使用“Console”进行实时计算在控制台里你可以直接引用当前作用域下的函数和变量手动调用加密函数看输出是否与网络请求中的加密结果一致。这是验证你是否找对函数的最直接方法。// 假设我们怀疑 window.encrypt 是加密函数 // 在代码暂停时在Console里输入 window.encrypt(123456) // 查看输出是否与捕获的请求中的密码字段一致单步执行F10与步入F11使用F10Step Over逐行执行使用F11Step Into进入函数内部。一步步跟踪你的明文密码是如何被加工成最终密文的。实操心得很多网站的加密并非一次完成可能是MD5(password salt)然后再Base64编码或者先RSA加密一个密钥再用这个密钥AES加密密码。一定要耐心地一步一步跟下去理解每一层变换。把关键变量的值在控制台里打印出来或者用笔记下转换过程。4. Python复现从算法理解到代码实现经过一番调试我们终于弄明白了加密逻辑。假设我们分析出“某乐网”的密码加密方式如下对明文密码进行MD5哈希得到32位十六进制字符串。将得到的MD5字符串与一个固定盐值salt比如”lE!2024“拼接。对拼接后的字符串再次进行MD5哈希。将第二次MD5的结果转换为小写。同时还发现了一个sign参数它是用timestamp当前13位时间戳、username和一个密钥key通过某种规则比如MD5(timestamp username key)生成的。现在我们用Python来复现它。4.1 环境准备与依赖安装首先确保你的Python环境已就绪。我们将使用requests库发送HTTP请求使用hashlib和base64进行加密编码。这些通常是标准库或极易安装的。# 通常只需要安装 requests hashlib 和 base64 是内置的 pip install requests4.2 核心加密函数复现根据上面分析出的逻辑我们编写对应的Python函数。import hashlib import time import requests def encrypt_password(password: str, salt: str lE!2024) - str: 模拟前端密码加密逻辑 1. MD5(明文密码) 2. MD5(第一次MD5结果 salt) 3. 结果转为小写 # 第一步MD5 first_md5 hashlib.md5(password.encode(utf-8)).hexdigest() # 第二步MD5 (拼接盐值) second_md5 hashlib.md5((first_md5 salt).encode(utf-8)).hexdigest() # 返回小写结果hexdigest默认就是小写这里明确一下 return second_md5.lower() def generate_sign(timestamp: int, username: str, key: str some_secret_key) - str: 生成请求签名 sign 规则MD5(字符串形式的timestamp username key) sign_string f{timestamp}{username}{key} return hashlib.md5(sign_string.encode(utf-8)).hexdigest() # 测试加密函数 if __name__ __main__: test_pwd myPassword123 encrypted_pwd encrypt_password(test_pwd) print(f明文密码: {test_pwd}) print(f加密后密码: {encrypted_pwd}) # 对比在浏览器控制台里执行相同的JS加密逻辑看输出是否一致。关键点hashlib.md5().hexdigest()返回的就是32位小写十六进制字符串与JavaScript中常见的CryptoJS.MD5(...).toString()结果一致。如果JS端结果是大写你可能需要在Python端加上.upper()。4.3 组装请求与模拟登录加密函数准备好后就可以组装完整的登录请求了。def login_to_somele(username, password): 模拟登录某乐网的主函数 # 1. 获取必要参数 current_timestamp int(time.time() * 1000) # 13位时间戳与JS的 Date.now() 对应 encrypted_pwd encrypt_password(password) sign generate_sign(current_timestamp, username) # 2. 组装登录数据 (根据实际抓包到的字段名修改) login_data { account: username, # 也可能是 username, user, email等 pwd: encrypted_pwd, # 也可能是 password, encryptedPwd等 timestamp: current_timestamp, sign: sign, rememberMe: true # 根据实际情况添加 } # 3. 设置请求头 (尽量模拟浏览器) headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Content-Type: application/x-www-form-urlencoded; charsetUTF-8, # 常见格式 Referer: https://www.somele.com/login.html, # 登录页地址 Origin: https://www.somele.com, # X-Requested-With: XMLHttpRequest # 如果是Ajax请求可能需要 } # 4. 登录接口URL (从Network抓包获取) login_url https://api.somele.com/v1/auth/login # 5. 发送POST请求 session requests.Session() # 使用Session可以自动管理Cookies try: response session.post(login_url, datalogin_data, headersheaders) response.raise_for_status() # 检查HTTP错误 result response.json() # 假设返回JSON print(登录响应:, result) # 6. 检查登录是否成功 if result.get(code) 200 or result.get(success): print(f登录成功用户名: {username}) # 登录成功后session 会自动保存服务器返回的Cookie如Session ID # 后续的请求直接使用这个 session 对象即可 # 例如 profile_resp session.get(https://api.somele.com/v1/user/profile) return session else: print(f登录失败: {result.get(message, 未知错误)}) return None except requests.exceptions.RequestException as e: print(f网络请求异常: {e}) return None except ValueError as e: print(f解析JSON响应失败: {e}, 原始文本: {response.text[:200]}) return None # 使用示例 if __name__ __main__: # 请务必使用测试账号 test_username test_userexample.com test_password test_password_123 session_obj login_to_somele(test_username, test_password) if session_obj: print(Session已建立可用于后续请求。)注意事项字段名和URLlogin_data中的字段名如account,pwd和login_url必须与你抓包时看到的一模一样。请求头Content-Type非常重要。如果抓包显示是application/json那么data就应该换成jsonlogin_data。我们这里用的是常见的表单格式。Session使用requests.Session()是关键。它会在内部自动保存登录后服务器通过Set-Cookie头部下发的会话标识如JSESSIONID并在后续请求中自动携带模拟浏览器保持登录状态的行为。错误处理网络请求总是可能出错良好的异常处理和响应状态码检查 (response.raise_for_status()) 是必不可少的。5. 进阶挑战与通用应对策略上面的例子是一个相对标准的流程。但现实中的网站防御手段层出不穷下面是一些你可能遇到的进阶情况及应对思路。5.1 对抗代码混淆与压缩你找到的JS文件可能看起来像这样function a(b){var c,d,e;for(c0;cb.length;c)db.charCodeAt(c),eString.fromCharCode(d^5);return e}function f(g,h){return CryptoJS.MD5(gh).toString()}这就是混淆。应对方法使用浏览器调试器即使代码被压缩在调试器中设置断点后你可以看到变量实时的、未被混淆的值。这是动态调试的核心优势。寻找入口点混淆不会改变网络请求的触发点。依然可以通过搜索addEventListener、fetch、XMLHttpRequest或加密参数名来定位关键代码块。借助工具对于简单的字符串混淆如^5异或可以在控制台直接测试还原。对于复杂的混淆可以尝试使用AST抽象语法树反混淆工具进行初步还原但学习成本较高。5.2 处理动态密钥与盐值有些网站每次登录的盐值salt或密钥key是动态的可能藏在首次GET请求的响应中在打开登录页时一个初始的HTML或JS响应里可能包含一个隐藏的input字段或者一段内联JS变量。独立的接口中可能有一个/api/getPublicKey或/api/getSalt的接口需要先调用它获取加密公钥或盐值再用其加密密码。复杂的算法生成密钥可能是由页面URL、时间戳、用户代理User-Agent等因子通过一个固定算法计算出来的。应对策略你的爬虫脚本需要模拟完整的浏览器行为。通常是第一步session.get(login_page_url)获取登录页面HTML。第二步从页面HTML中解析出动态的盐值或密钥用BeautifulSoup或正则表达式。第三步或者调用那个获取密钥的独立接口session.get(key_api_url)。第四步使用获取到的动态值进行加密然后提交登录。5.3 调试无限Debugger与反调试陷阱有些网站会设置反调试比如在代码开头加入debugger;语句或者检测开发者工具是否打开导致代码不断暂停。应对方法禁用断点在Sources面板右侧点击{}美化代码按钮下方的断点设置区域可以取消所有断点的激活状态。条件断点对于debugger;语句可以右键点击行号选择 “Never pause here”。重写函数在控制台输入Function.prototype.constructor function() {};或重写debugger关键字此方法可能不总是有效且刷新页面后失效。使用“Overrides”本地替换在Sources面板的Overrides选项卡可以将线上的JS文件映射到本地修改过的版本直接删除里面的反调试代码。这是最彻底的方法。5.4 验证码与行为验证如果登录有图形验证码或滑块验证码这就超出了纯JS逆向的范畴进入了“验证码识别”或“行为模拟”的领域。简单图形验证码可以使用PILPillowpytesseractOCR进行识别但成功率取决于验证码复杂度。复杂验证码或滑块通常需要借助第三方打码平台人工或AI识别或者使用selenium、playwright等浏览器自动化工具来模拟人的拖动操作。这会使爬虫效率降低且容易被更高级的反爬策略如WebDriver检测识别。6. 常见问题排查与调试技巧实录即使按照流程操作你也可能会卡在某个环节。下面是一些常见问题及解决方法。6.1 加密结果与浏览器不一致这是最典型的问题。检查编码确保Python和JavaScript的字符串编码一致。hello.encode(utf-8)是Python的方式。JS内部是UTF-16但在与服务器交互时现代浏览器通常使用UTF-8。如果JS代码里对字符串做了特殊处理如unescape(encodeURIComponent(str))你需要在Python中模拟 (urllib.parse.quote。检查步骤确认你的加密步骤和JS中完全一致。是MD5(password)还是MD5(MD5(password))盐值是拼接在前面还是后面是否进行了大小写转换在线工具辅助使用在线的加密工具如 https://tool.oschina.net/encrypt?type2分别用你的Python逻辑和理解的JS逻辑对同一个字符串进行加密对比结果。控制台验证在浏览器调试暂停时将你的Python中间变量输出然后在JS控制台里手动计算对应步骤的结果进行逐环节比对。6.2 登录请求返回“参数错误”或“签名无效”这说明你的请求数据与浏览器发送的有细微差别。逐一比对将你的Python脚本生成的login_data字典与浏览器Network面板里抓到的Form Data逐字逐句进行比对。特别注意字段名是否完全一致大小写、下划线/连字符。字段值是否一致特别是时间戳精确到毫秒。是否遗漏了某些字段如一个看似无关的csrfToken。检查请求头Content-Type、User-Agent、Referer、Origin等头部信息是否与浏览器发送的一致。Cookie头在初次登录时通常为空或只有少量非登录相关Cookie但有些网站可能需要一个初始的sessionid。时间戳同步确保你的服务器时间与网站服务器时间没有巨大偏差。可以使用网站首页或一个公开接口返回的时间来校准。6.3 登录成功但无法获取后续数据登录接口返回成功但用同一个session去请求其他接口时返回未登录或403。检查Cookie打印session.cookies看是否包含了关键的会话Cookie如JSESSIONID,SESSION,token。有时登录成功会跳转或设置多个Cookie。检查重定向登录请求可能返回302重定向。requests默认会自动处理重定向。但有些网站的重定向逻辑复杂可能需要手动处理。可以设置allow_redirectsFalse先看看原始响应。Token机制有些网站使用JWTJSON Web Token登录成功后返回一个access_token这个token需要放在后续请求的Authorization头部如Bearer token而不是Cookie里。仔细查看登录成功的响应体。6.4 代码被VM或高强度混淆遇到完全无法阅读的、被虚拟机VM保护的代码常规调试手段失效。评估成本首先评估目标数据的价值是否值得投入大量时间。这类逆向需要极高的JS功底和耐心。寻找漏洞尝试寻找是否在更早的、未被混淆的版本中泄露了加密逻辑或者是否有移动端APIAPP的接口其加密方式可能更简单。终极方案如果逆向算法成本过高可以考虑使用selenium、playwright等工具直接控制浏览器进行登录和操作。这牺牲了速度和资源效率但绕过了算法逆向。对于需要处理复杂JS渲染或高强度反爬的页面这有时是唯一可行的方案。最后的心得JS逆向没有一成不变的公式它更像是一场与网站开发者的“猫鼠游戏”。核心能力是耐心、细致的观察力和逻辑推理能力。多练习从简单的网站开始逐步挑战更复杂的。每次成功逆向不仅是为了拿到数据更是对你技术思维的一次极佳锻炼。当你看到自己编写的Python脚本成功模拟登录并获取到数据时那种成就感是无与伦比的。记住保持合法合规的道德底线只将技术用于学习和授权范围内的数据获取。