Web登录加密逆向实战:从CryptoJS到Python复现的完整流程
1. 项目概述从登录框到加密黑盒最近在分析一些高校系统的自动化流程时遇到了重庆大学统一身份认证的登录接口。乍一看就是一个常见的用户名密码登录页面但当你尝试用常规的requests库模拟登录时会发现提交的表单数据里密码字段是一串完全看不懂的、每次请求都不同的长字符串。这显然不是简单的Base64或MD5而是前端JavaScript进行了实时加密处理。对于需要实现自动登录、数据采集或系统集成的开发者来说理解并逆向这个加密过程就成了一个绕不开的“门槛”。今天我就以一个实际逆向者的视角带大家完整走一遍重庆大学登录加密的逆向分析流程不仅还原加密逻辑更重要的是分享在类似场景下的通用分析思路和避坑技巧。这个实战的目标很明确找到网页上点击“登录”按钮时密码从明文到密文的完整转换链条并用Python代码复现这一过程最终实现稳定的自动化登录。整个过程会涉及浏览器开发者工具的使用、JavaScript关键代码定位、加密算法识别、以及Python的模拟实现。无论你是对Web逆向感兴趣的新手还是遇到过类似加密困扰的同行相信这篇详尽的记录都能给你带来直接的帮助。2. 逆向环境准备与初步抓包工欲善其事必先利其器。在开始逆向之前准备好趁手的工具和环境是成功的第一步。对于Web端的加密逆向我们主要依靠浏览器和配套的调试工具。2.1 核心工具链选择我的主力浏览器是Chrome其内置的开发者工具DevTools功能强大且更新及时。对于加密逆向以下几个面板是关键Network网络记录所有网络请求这是我们分析的起点用于定位登录的API接口和查看提交的数据。Sources源代码查看和调试页面加载的所有JavaScript文件。Console控制台执行JavaScript代码片段用于测试我们的解密函数或调用特定的加密方法。除了浏览器一个代码编辑器如VSCode用于编写Python复现代码以及一个可以方便发送HTTP请求的工具如Postman或Hoppscotch用于测试就构成了基础环境。注意有些网站会检测开发者工具打开后页面行为异常或加密逻辑改变。如果遇到这种情况可以尝试使用无头浏览器模式如Puppeteer直接执行页面JS来获取加密结果或者寻找检测逻辑并绕过。幸运的是在此次分析中重庆大学的登录页面没有这类反调试机制。2.2 关键请求的定位与捕获打开重庆大学的统一身份认证登录页面按F12打开开发者工具并切换到Network面板。记得勾选上“Preserve log”保留日志防止页面跳转后请求记录被清空。在页面的用户名和密码框输入测试信息例如用户test 密码123456然后点击登录按钮。此时Network面板会刷出一系列新的请求。我们需要从中找到那个真正提交登录凭证的请求。通常这个请求会有以下特征请求方法为POST因为提交表单数据。URL路径包含login、auth、signin等关键字。请求的Payload负载中包含用户名和加密后的密码。快速浏览后我找到了目标请求一个指向/auth/login路径的POST请求。点击该请求查看其“Headers”和“Payload”。在“Payload”标签页下我们看到表单数据类似这样username: test password: U2FsdGVkX19vBwQ8p4sVZ5J7K9mN...一长串字符这里username是明文的而password正是我们想要破解的密文。同时在“Headers”中注意查看Content-Type通常是application/x-www-form-urlencoded这决定了我们后续模拟提交的数据格式。初步观察这个密文长度较长且含有/、等字符很可能是Base64编码后的结果。但Base64只是一种编码方式并非加密算法其原始内容才是加密后的二进制数据。我们的核心任务就是找到生成这串密文的JavaScript代码。3. 加密逻辑的定位与静态分析找到加密发生的位置是整个逆向过程中最具技巧性的环节。我们需要从数以万计的JavaScript代码行中定位到那几行关键的加密函数。3.1 搜索与断点追踪策略在Sources面板中我们可以使用全局搜索CtrlShiftF来查找可能的关键字。搜索“password”、“encrypt”、“encode”、“Crypto”、“AES”、“RSA”等词汇。在这次分析中搜索“password”在某个压缩过的JS文件里找到了多处匹配但代码可读性极差。更高效的方法是使用“Event Listener Breakpoints”事件监听器断点。在Sources面板的右侧找到这个选项展开“Mouse”事件勾选“click”。然后回到页面再次点击登录按钮。此时代码执行会立即在第一个鼠标点击事件处理函数处暂停。然后我们使用“Step Over”F10和“Step Into”F11逐行执行。关注执行过程中何时我们的明文密码被读取何时被传入某个函数进行处理。当程序执行到类似var encryptedPwd someFunction(document.getElementById(pwd).value);这样的语句时我们就找到了加密的入口。通过逐步跟踪我发现密码在被提交前被传入了一个名为encryptPassword的函数。双击这个函数名或者顺着调用栈我们就能跳转到该函数的定义位置。3.2 核心加密函数剖析最终定位到的encryptPassword函数其代码经过格式化后核心逻辑如下所示为保护原系统安全以下为模拟逻辑但算法和结构一致function encryptPassword(password) { // 1. 生成一个随机的16字节字符串作为盐Salt var salt CryptoJS.lib.WordArray.random(16); // 2. 将密码和盐拼接 var saltedPassword password salt.toString(); // 3. 进行多次SHA256哈希迭代 var key CryptoJS.SHA256(saltedPassword); for (var i 0; i 1000; i) { key CryptoJS.SHA256(key.concat(salt)); } // 4. 使用AES算法以key的前32字节作为密钥盐作为IV对原始密码进行加密 var iv salt; var encrypted CryptoJS.AES.encrypt( CryptoJS.enc.Utf8.parse(password), CryptoJS.enc.Hex.parse(key.toString().substring(0, 64)), // 取前64个16进制字符即32字节 { iv: CryptoJS.enc.Hex.parse(iv.toString()), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ); // 5. 将盐和加密后的密文拼接再进行Base64编码 var result salt.toString() encrypted.ciphertext.toString(); return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(result)); }逻辑拆解加盐Salt每次加密都生成一个随机的盐值目的是即使相同密码每次加密结果也不同防止“彩虹表”攻击。密钥派生使用密码和盐经过SHA256的多次迭代1000次生成一个复杂的密钥。这个过程称为PBKDF2Password-Based Key Derivation Function 2的简化模拟目的是增加暴力破解的难度。AES加密使用上一步派生的密钥取前32字节和盐作为初始化向量IV采用CBC模式和PKCS7填充对原始明文密码进行AES加密。组合与编码将盐和AES加密后的密文ciphertext拼接起来最后将整个拼接字符串进行Base64编码得到最终提交的password参数。实操心得遇到CryptoJS这个库逆向就成功了一大半。它是一个前端知名的加密库算法实现标准。我们的任务就从“猜算法”变成了“读参数”——弄清楚它调用的是CryptoJS的哪个算法、什么模式、用了哪些参数密钥、IV、盐。4. Python复现加密算法理解了JavaScript的加密逻辑后接下来就是用Python实现完全相同的流程。这里我们需要用到pycryptodome库它是Python下功能强大的加密工具库。4.1 环境搭建与库安装首先确保安装了必要的库pip install pycryptodome4.2 分步复现加密过程下面是根据逆向分析结果编写的Python加密函数import base64 import os from hashlib import sha256 from Crypto.Cipher import AES from Crypto.Util.Padding import pad def cqu_encrypt_password(password: str) - str: 模拟重庆大学登录密码加密过程 :param password: 明文密码 :return: 加密后的Base64字符串 # 1. 生成16字节随机盐 salt os.urandom(16) # 对应 CryptoJS.lib.WordArray.random(16) salt_hex salt.hex() # 转换为16进制字符串便于后续拼接 # 2. 密钥派生密码 盐迭代SHA256 # 注意JS中是 password salt.toString() salt.toString()默认是16进制字符串 salted_password (password salt_hex).encode(utf-8) key sha256(salted_password).digest() # 第一次SHA256 for i in range(1000): # 迭代1000次 # JS中是 key.concat(salt) 这里将当前key的字节与salt字节拼接 key sha256(key salt).digest() # 派生出的key是32字节SHA256输出长度。JS代码取前64个16进制字符即32字节作为AES密钥 aes_key key[:32] # 前32字节作为AES-256密钥 # 3. AES-CBC加密 # 使用盐作为IV iv salt cipher AES.new(aes_key, AES.MODE_CBC, iviv) # 对明文密码进行PKCS7填充并加密 padded_password pad(password.encode(utf-8), AES.block_size) ciphertext cipher.encrypt(padded_password) # 4. 组合与编码 # JS: salt.toString() encrypted.ciphertext.toString() # salt.toString() 是16进制字符串ciphertext.toString() 在CryptoJS里默认也是16进制字符串 combined salt_hex ciphertext.hex() # 最后对整个组合字符串进行UTF-8编码再Base64 # 对应 CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(result)) final_base64 base64.b64encode(combined.encode(utf-8)).decode(utf-8) return final_base64 # 测试 if __name__ __main__: encrypted_pwd cqu_encrypt_password(YourPassword123) print(f加密后的密码: {encrypted_pwd})关键点解析盐的格式CryptoJS.lib.WordArray.random(16)生成的是一个16字节的随机WordArray其.toString()方法默认输出16进制字符串。因此我们用os.urandom(16)生成字节再用.hex()转成16进制字符串来模拟。密钥派生细节JavaScript中的key.concat(salt)key是上一次哈希的结果一个WordArraysalt是最初的盐WordArray。在Python中我们需要将字节类型的key和字节类型的salt直接拼接。AES参数密钥是派生密钥的前32字节AES-256IV就是盐16字节模式为CBC填充为PKCS7。pycryptodome的pad函数帮我们处理了填充。最终编码最后一步的CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(result))很容易出错。它先将组合后的16进制字符串result用UTF-8编码成字节再对这个字节进行Base64编码。而不是对16进制字符串本身进行Base64编码。5. 完整登录流程模拟与测试有了加密函数我们就可以组装完整的登录请求了。这涉及到处理Cookie、Session以及可能的其他验证参数。5.1 使用Session维持状态在Python中使用requests.Session()对象可以自动处理Cookies模拟浏览器行为。import requests def cqu_login(username, password): login_url https://你的重庆大学认证中心地址/auth/login # 请替换为实际地址 # 使用Session session requests.Session() # 通常先访问一次登录页获取必要的初始Cookie或Token session.get(login_url) # 加密密码 encrypted_password cqu_encrypt_password(password) # 构造登录数据 login_data { username: username, password: encrypted_password, # 可能还有其他隐藏字段如csrf token需要从登录页HTML中提取 # _csrf: csrf_token } # 发送登录请求 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Content-Type: application/x-www-form-urlencoded, Referer: login_url } resp session.post(login_url, datalogin_data, headersheaders, allow_redirectsFalse) # 分析响应 print(f状态码: {resp.status_code}) print(f响应头: {resp.headers}) # 登录成功往往返回302跳转Location头指向登录后的页面 if resp.status_code 302: print(登录成功或跳转中) # 可以继续用这个session访问需要登录的页面 # home_resp session.get(resp.headers[Location]) return session else: print(登录可能失败) print(resp.text) # 查看错误信息 return None5.2 处理动态Token与验证码很多现代登录系统不止有密码加密还有CSRF Token或动态验证码。CSRF Token通常隐藏在登录表单的一个input typehidden标签里名字可能是_csrf、csrf_token等。我们需要先用Session访问登录页用BeautifulSoup或正则表达式解析出这个token值然后将其填入login_data。验证码如果遇到验证码处理会复杂很多。可能需要用Session下载验证码图片。人工识别或接入打码平台进行识别。将识别结果填入表单。对于复杂的点选、滑动验证码可能需要更复杂的逆向或使用自动化工具如Selenium、Playwright来模拟用户操作。在本次重庆大学的案例中核心挑战在于密码加密。如果系统还有Token只需在上述流程中增加提取和提交的步骤即可。6. 逆向过程中的常见问题与调试技巧即使按照步骤操作也难免会遇到加密结果对不上、登录失败的情况。这里分享几个排查思路和技巧。6.1 加密结果对比验证这是最有效的调试方法。在浏览器开发者工具的Console面板中直接调用我们找到的JavaScript加密函数对同一个密码进行加密。// 在Console中执行 var testPwd 123456; console.log(JS加密结果:, encryptPassword(testPwd));同时在Python中运行我们的cqu_encrypt_password(123456)对比两个输出是否完全一致。如果不一致问题一定出在复现过程的某个环节。对比排查清单盐的生成与格式确保Python生成的16字节随机盐在转换成16进制字符串后与JS中salt.toString()的格式一致。可以打印出来对比。密钥派生过程拼接顺序是password salt还是salt password字符串编码是UTF-8吗迭代次数确认是1000次吗第一次哈希的输入是什么中间状态可以在JS和Python中打印出第一次哈希后、第十次哈希后的结果转为Hex进行比对定位从哪一次开始出现分歧。AES加密参数密钥长度确认是取派生结果的前32字节吗AES-256密钥就是32字节。IV确认IV就是盐的原始字节而不是盐的16进制字符串。模式与填充一定是CBC模式和PKCS7填充。最终编码这是最容易出错的一步。仔细对照JS代码的最后一行理解Base64.stringify(Utf8.parse(result))的含义。result是盐和密文的16进制字符串拼接。在Python中是先把这个拼接字符串用.encode(utf-8)变成字节再用base64.b64encode编码。6.2 网络请求的细节差异即使加密结果一致登录请求也可能失败需要检查网络请求的细节请求头Headers仔细比对浏览器成功登录时的请求头和你用Python发送的请求头。特别注意Content-Type、User-Agent、Origin、Referer等字段。User-Agent最好模拟一个真实的浏览器。请求数据Payload除了username和password是否还有其他必填字段比如一个固定的clientId、grant_type或者从页面获取的lt、execution等参数。这些都需要从登录页的HTML源码或首次GET请求的响应中提取。Cookie处理确保你的Session正确接收和发送了Cookie。有些系统需要先访问页面获取一个会话ID。HTTPS与重定向确保requests正确处理了HTTPS通常没问题。allow_redirectsFalse可以阻止自动重定向方便我们查看登录接口的直接响应。登录成功后再手动处理重定向。6.3 应对代码混淆与反调试如果遇到的网站代码被严重混淆变量名变成a,b,c逻辑被打乱或者有反调试手段可以尝试以下方法搜索特征常量加密算法中常有一些固定值如AES的初始化向量IV如果是固定的可能会在代码中直接出现一长串数字或字符串。搜索这些特征串可能直接定位到加密函数附近。Hook关键函数在Console中重写标准的加密函数或API例如重写CryptoJS.AES.encrypt在里面打印出调用参数和结果这是动态分析的利器。var _encrypt CryptoJS.AES.encrypt; CryptoJS.AES.encrypt function(message, key, cfg){ console.log(AES Encrypt Called:); console.log(Message:, message); console.log(Key:, key); console.log(Cfg:, cfg); var result _encrypt(message, key, cfg); console.log(Result:, result); return result; };使用AST解混淆工具对于复杂的混淆可以尝试使用像de4js这样的在线工具或本地库进行反混淆但成功率取决于混淆强度。“油猴”脚本辅助编写Tampermonkey脚本在页面加载时注入我们的调试代码可以更早地介入执行流程。逆向工程就像侦探破案需要耐心、细致的观察和逻辑推理。每一次成功的逆向不仅解决了一个具体问题更积累了一套应对加密黑盒的方法论。重庆大学登录加密的案例涵盖了前端加密逆向的典型流程抓包定位、断点追踪、算法分析、代码复现和调试验证。掌握这个流程再遇到类似的登录加密你就能从容地拿出这套工具和方法一层层剥开它的外壳看到核心的逻辑。