当前位置: 首页 > news >正文

JS逆向实战:RSA加密定位、分析与Python复现全解析

1. 项目概述:为什么JS逆向绕不开RSA?

如果你正在学习或者已经接触过Web安全、爬虫或者前端安全审计,那么“JS逆向”这个词对你来说一定不陌生。而在这个领域里,RSA加密算法就像一座绕不开的大山。无论是登录密码的加密、关键API请求参数的签名,还是某些核心业务数据的传输,前端JavaScript代码中大量使用RSA来对抗自动化脚本和逆向分析。我见过太多新手朋友,面对一个经过RSA加密的请求参数,抓耳挠腮,不知从何下手。他们能定位到加密函数,但面对那一串串看似随机的长字符串(密文)和复杂的数学运算,往往感到无从下手。

简单来说,JS逆向中的RSA,核心目标不是去破解RSA算法本身(这在现有计算能力下几乎不可能),而是逆向出前端JavaScript是如何调用RSA的,从而在本地复现整个加密流程。这包括了找到加密所用的公钥(通常是模数N和指数e)、理解数据拼接和填充方式(如PKCS1_v1_5或OAEP)、以及最关键的一步——模拟JavaScript的加密逻辑,用Python、Java等后端语言重新实现。这个过程,更像是一场“侦探游戏”,你需要仔细阅读混淆或压缩过的JS代码,找到关键的密钥片段和函数调用链。

对于爬虫工程师,掌握RSA逆向意味着能突破更高级别的反爬机制;对于安全研究员,这是分析前端安全漏洞和加密实现弱点的基本功;对于前端开发者,理解这个过程能帮助你写出更安全的代码。接下来,我将以一个典型的实战场景为例,带你一步步拆解RSA逆向的全过程,分享我踩过的坑和总结出的有效技巧。

2. 核心思路拆解:定位、分析与复现

面对一个使用了RSA加密的网站或应用,我们的逆向工作可以系统性地分为三个核心阶段:定位加密点、分析加密逻辑、本地化复现。这三个阶段环环相扣,缺一不可。

2.1 第一阶段:精准定位加密发生地

一切始于一个被加密的参数。通常,你在浏览器的开发者工具(F12)的网络(Network)选项卡中,会发现某个重要的POST请求,其请求体(Payload)或请求头(Headers)里包含一长串看似无规律的Base64字符串或十六进制字符串。这就是我们的“猎物”。

第一步,使用“搜索大法”定位关键代码。

  1. 全局搜索:在开发者工具的源代码(Sources)面板,直接使用Ctrl+Shift+F(Windows)或Cmd+Option+F(Mac)进行全局搜索。搜索的内容可以是:

    • 加密参数名:比如你发现了一个叫encryptedData的参数,就直接搜它。
    • 加密字符串的片段:复制密文开头和结尾的几个字符(避免因编码问题搜不到)进行搜索。
    • 关键函数名:如encryptRSApublicsetPublicKeyJSEncrypt(一个常用库)等。
  2. XHR/断点辅助:如果全局搜索效果不佳,可以在网络面板中找到那个发送加密请求的XHR/Fetch请求,右键选择“Reveal in Sources panel”或类似选项,这能直接带你到发起这个网络请求的JavaScript代码附近。然后,你可以在该行代码上打上“XHR/Fetch断点”,重新触发请求,代码执行就会在此处暂停,方便你一步步跟踪。

实操心得:很多网站的加密操作可能在Webpack打包的模块里。这时,找到的代码可能是一个数字标识符(如(0, i.encrypt)(t))。不要慌,继续在附近搜索encrypt或查看该模块的导出定义,往往就能找到真正的函数实现位置。

2.2 第二阶段:深入分析加密逻辑与密钥

找到加密函数后,真正的挑战才开始。眼前的代码很可能是经过混淆的,变量名可能是a,b,c,函数名可能是_0x123abc

1. 密钥(公钥)的寻找RSA加密需要公钥,公钥的核心是模数N和指数e。它们通常以以下几种形式存在:

  • 明文硬编码:最理想的情况,在JS代码中直接能找到类似setPublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...")的调用,引号里的长字符串就是Base64编码的公钥(通常是PKCS#1或PKCS#8格式)。你可以直接复制出来。
  • 分段变量拼接Ne可能被拆分成多个字符串或十六进制数,散落在代码不同地方,最后通过拼接、反转等操作组合起来。你需要仔细跟踪代码执行流,找到最终拼接成的字符串。
  • 网络请求获取:公钥可能由前端在初始化时通过另一个API请求从服务器获取。你需要检查更早的网络请求,找到一个返回包含publicKeymodulusexponent字段的响应。

2. 加密库的识别前端常用的RSA库有JSEncryptnode-rsa(浏览器版)、forge以及各大厂自研的加密模块。识别出使用的库有助于你理解其默认的填充模式和数据格式。

  • new JSEncrypt()JSEncrypt库的典型用法。
  • setPublicKey方法也很常见。
  • 观察加密函数的输入输出:输入是否是原始字符串?输出是否是Base64?这关系到填充方式。

3. 数据预处理分析在调用RSA加密前,原始数据(如密码、时间戳等)往往需要经过一系列处理:

  • 拼接username + "|" + timestamp
  • 哈希:先对数据进行MD5或SHA256哈希。
  • 编码转换:将字符串转为ArrayBuffer或特定的字节序列。
  • 填充(Padding):RSA加密本身要求输入数据长度必须小于密钥长度。因此需要对数据进行填充。常见的填充方案有PKCS#1 v1.5和OAEP。填充方案是复现时最容易出错的地方!必须通过代码逻辑或库的默认行为确定。

2.3 第三阶段:本地化复现加密流程

分析清楚后,目标是在Python(以pycryptodomecryptography库为例)或你使用的其他语言中,完全复现前端的加密过程。

复现步骤:

  1. 还原公钥:将找到的模数N和指数e,或者整个公钥PEM字符串,在Python中正确构造出公钥对象。
  2. 还原数据预处理:严格按照JS代码的逻辑,进行相同的拼接、哈希、编码等操作,得到待加密的原始字节数据。
  3. 应用相同的填充方案进行加密:使用与前端相同的填充模式(如PKCS1_v1_5)进行加密。
  4. 输出格式转换:将加密后的二进制密文,按照前端的方式(通常是Base64)进行编码,得到最终的加密字符串。

验证成功与否的唯一标准是:你用本地代码生成的加密结果,与浏览器发送的加密参数完全一致

3. 实战拆解:一个模拟登录场景的RSA逆向

假设我们遇到一个登录页面,其密码字段被加密,表单数据如下:

{ "username": "testUser", "password": "oX4fL9k...(很长的一段Base64)" }

我们的目标是逆向这个password的生成过程。

3.1 定位与初步分析

通过搜索password或加密后的片段,我们定位到一个主要的JavaScript文件login.xxxxxx.js。在其中搜索encrypt,找到如下关键代码段(经过一定美化):

function encryptPassword(pwd) { var rsa = new JSEncrypt(); rsa.setPublicKey('-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDlOJu6TyygqxfWT7eLtGDwajtN\nFOb9I5XRb6khyfD1Yt3YiCgQWMNW649887VGJiGr/L5i2XblLPMyNah+MBQYz5T4\nI/1n1zF8V6dN1kC4gLThX4+QJzVcM6eR5pV5t8vJc7P0Z2ZQIDAQAB\n-----END PUBLIC KEY-----'); var encrypted = rsa.encrypt(pwd); return encrypted; } // 调用处 var inputPwd = document.getElementById('pwd').value; var timestamp = Date.now(); var dataToEncrypt = inputPwd + '|' + timestamp; var finalEncryptedPwd = encryptPassword(dataToEncrypt);

分析结果:

  1. 使用的库JSEncrypt
  2. 公钥:直接硬编码在代码中,是标准的PEM格式公钥。
  3. 数据预处理:密码明文与当前时间戳用|连接,形成待加密字符串。
  4. 加密与输出:调用rsa.encrypt(),该库默认使用PKCS#1 v1.5填充,并输出Base64编码的字符串。

3.2 使用Python进行复现

现在,我们在Python中复现这一过程。我们将使用pycryptodome库。

from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 import base64 import time # 1. 准备公钥 (直接从JS代码中复制) public_key_pem = """-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDlOJu6TyygqxfWT7eLtGDwajtN FOb9I5XRb6khyfD1Yt3YiCgQWMNW649887VGJiGr/L5i2XblLPMyNah+MBQYz5T4 I/1n1zF8V6dN1kC4gLThX4+QJzVcM6eR5pV5t8vJc7P0Z2ZQIDAQAB -----END PUBLIC KEY-----""" # 2. 加载公钥 key = RSA.import_key(public_key_pem) cipher = PKCS1_v1_5.new(key) # 使用与JSEncrypt默认相同的PKCS#1 v1.5填充 # 3. 模拟前端的数据预处理 password_plain = "MySecretPassword123" timestamp = int(time.time() * 1000) # JS的Date.now()是毫秒时间戳 data_to_encrypt = f"{password_plain}|{timestamp}".encode('utf-8') # 转为bytes # 4. 加密 # 注意:PKCS1_v1_5加密的输入数据长度不能超过 (密钥长度/8 - 11)字节。 # 本例密钥1024位(128字节),所以最大数据长度为 128 - 11 = 117字节。 encrypted_bytes = cipher.encrypt(data_to_encrypt) # 5. 输出Base64 final_encrypted_password = base64.b64encode(encrypted_bytes).decode('utf-8') print(f"加密后的密码: {final_encrypted_password}")

运行这段代码,将password_plain替换为你想测试的密码,生成的final_encrypted_password应该与浏览器发送的加密密码(在相同时间戳下)完全一致。

3.3 处理更复杂的情况:分段密钥与自定义填充

并非所有网站都如此友好。更常见的情况是这样的:

var n = "a5c7d8...(很长十六进制)"; var e = "10001"; // 十六进制,对应十进制65537,这是最常用的公钥指数 function customEncrypt(t) { // ... 一系列复杂的位运算和拼接,最终调用了某个内部RSA函数 return rsaFunc(t, n, e); }

应对策略:

  1. 构造公钥:你需要将十六进制的模数n和指数e转换成Python的整数,然后构造RSA公钥。
    from Crypto.PublicKey import RSA import base64 n_hex = "a5c7d8..." e_hex = "10001" n_int = int(n_hex, 16) e_int = int(e_hex, 16) # 构造RSA key对象 key = RSA.construct((n_int, e_int)) # 后续加密步骤同上
  2. 确定填充:如果代码中没有明确显示填充方式,你需要:
    • 查阅库文档:如果识别出是特定库(如forge),查看其默认填充。
    • 逆向填充函数:跟踪rsaFunc内部或之前的代码,看是否有对输入数据添加特定前缀(如\x00\x02...)的步骤,这是PKCS#1 v1.5填充的特征。
    • 经验猜测与测试:最常用的填充是PKCS#1 v1.5。可以先尝试用它复现,如果结果对不上,再尝试其他填充或无填充(极少数情况)。

4. 核心工具链与调试技巧

工欲善其事,必先利其器。一套高效的JS逆向工具链能极大提升效率。

4.1 浏览器开发者工具进阶用法

  • 条件断点(Conditional Breakpoint):当加密函数被频繁调用(如按键事件),你可以在该行代码的断点上右键,添加条件,例如t.indexOf('password') > -1,这样只有当加密的数据包含“password”时才会暂停,避免无效中断。
  • 调用栈(Call Stack):在断点暂停时,查看调用栈面板,可以清晰地看到函数是如何被一层层调用的,帮助你理解加密触发的完整路径。
  • 作用域(Scope):在断点暂停时,查看局部作用域和闭包作用域,可以直接看到当前函数内所有变量的值,这是获取密钥、中间计算结果的黄金时刻。
  • Overrides(本地替换):在Sources面板的Overrides标签下,你可以选择一个本地文件夹,然后将线上JS文件保存并映射到本地。之后你可以在本地修改这个JS文件(例如,添加一些console.log来打印关键变量),刷新页面后浏览器会加载你修改过的本地版本。这是动态分析修改代码逻辑的神器。

4.2 辅助工具推荐

  1. CryptoJS识别与模拟:很多网站使用CryptoJS库进行哈希(MD5, SHA256)和AES加密。如果你在代码中看到CryptoJS.MD5(...)CryptoJS.AES.encrypt(...),就需要在Python中对应使用hashlibpycryptodome库来模拟。注意CryptoJS的AES加密默认输出可能是一个包含盐、密文等信息的特殊对象,需要解析其toString()ciphertext属性。
  2. Pythonexecjs:当JS加密逻辑极其复杂,涉及大量浏览器环境特有的对象或难以翻译的位操作时,可以考虑使用execjs库直接在Python中执行JavaScript代码片段。这相当于一个“降维打击”,但会牺牲一些性能和可移植性。
    import execjs with open('encrypt.js', 'r', encoding='utf-8') as f: js_code = f.read() ctx = execjs.compile(js_code) result = ctx.call('encryptPassword', 'myPassword', '1234567890') print(result)
  3. AST(抽象语法树)解析工具:对于高度混淆的代码,可以借助esprimababel等工具将JS代码解析成AST,然后编写脚本进行反混淆,比如还原变量名、控制流平坦化等。这是高阶逆向技能,入门阶段可以先了解。

5. 常见问题排查与避坑指南

在实际操作中,你一定会遇到各种问题导致本地加密结果与前端不一致。下面是一个常见问题排查清单:

问题现象可能原因排查思路与解决方案
加密结果长度不一致1. 编码方式不同。
2. 填充模式不同。
3. 公钥不一致。
1. 确认JS和Python端对明文、密文的编码(UTF-8, ASCII, Hex, Base64)每一步都一致。
2. 核对填充方案。JSEncrypt默认PKCS#1 v1.5,Python的PKCS1_v1_5需对应。
3. 检查公钥字符串或(N, e)值是否完全一致,注意换行符、头尾标记。
加密结果完全不同1. 待加密数据源不同。
2. 使用了随机盐或IV(多见于混合加密)。
3. 密钥是动态的。
1. 在JS加密函数入口打断点,精确捕获传入的原始字符串,与Python准备的字符串进行逐字符比对(包括不可见字符)。
2. 检查加密前是否有生成随机数并参与运算。如果是,需要将随机数也一并捕获并在Python中固定使用。
3. 确认公钥是否是每次页面加载或请求时动态从服务器获取的。
Python报错:ValueError: Plaintext is too long.待加密数据长度超过了所选填充模式下的最大允许长度。对于1024位密钥的PKCS#1 v1.5,最大明文长度为117字节。检查你的明文数据(转为bytes后)长度。如果超长,前端可能采用了分段加密或先哈希再加密的策略。
能加密但服务器不认可1. 时间戳等动态参数未同步。
2. 请求头(如User-Agent, Referer)缺失或不对,触发了风控。
3. 加密逻辑有细微差别(如字节序)。
1. 确保时间戳等动态值与浏览器请求时完全一致。可以尝试硬编码一个成功请求的时间戳来测试。
2. 在Python请求中,尽量完整模拟浏览器的请求头。
3. 使用最笨但最有效的方法:在JS加密函数的每一步都打印出中间变量的值(字符串、Hex、Base64),然后在Python中完全复现每一步,进行比对。
遇到ReferenceError: window/ document is not defined(在execjs中)JS代码运行在Node.js环境(execjs),但代码中使用了浏览器特有的BOM对象。1. 在execjs执行前,在JS代码全局注入模拟对象,如window = {}; document = {};
2. 更彻底的方法是,分析代码到底用这些对象做了什么(可能是获取cookie或userAgent),然后在Python中直接提供这些值。

核心避坑技巧“二分法”调试。不要试图一次性复现整个复杂流程。将过程拆解:先确保从相同输入(如固定字符串“test”)到相同输出。如果不对,就在JS代码中,在加密函数调用前一刻,把输入数据打印出来,在Python中用完全相同的数据加密。还不对,就打印出JS中加载的公钥的N和e,与Python中加载的进行比对。还不对,就单独测试填充函数。通过这种逐步缩小范围的方法,总能定位到差异点。

6. 从RSA到更广阔的前端加密逆向

掌握了RSA的逆向,你就拿到了打开前端加密世界大门的钥匙。许多更复杂的加密方案都是基于类似原理的叠加:

  1. 对称加密(AES/DES):关键在于找到密钥(Key)和初始向量(IV)。它们可能硬编码、由RSA加密传输、或通过特定算法动态生成。逆向思路同样是定位密钥生成或获取逻辑。
  2. 哈希与加盐(MD5, SHA, HMAC):重点是找到盐(Salt)值和哈希的轮次。盐可能固定、与用户相关或来自服务器。
  3. 混合加密:最常见的是“RSA加密AES密钥,AES加密业务数据”。你需要先逆向RSA部分拿到AES密钥,再用该密钥解密数据。
  4. 代码混淆与反调试:网站会使用obfuscator等工具混淆代码,增加阅读难度;还会设置反调试(如检测开发者工具打开、无限debugger循环)。对付混淆,需要耐心和AST工具;对付反调试,可以禁用断点、使用override功能替换检测代码。

逆向的本质是理解。理解开发者的保护思路,理解加密库的运作方式,理解数据在网络中的生命轨迹。这个过程没有一成不变的公式,每一个网站都可能是一个新的谜题。但只要你掌握了本文所述的核心方法论——定位、分析、复现,并辅以耐心和细致的调试,绝大多数前端加密的壁垒都将被你逐一攻克。记住,每一次成功的逆向,不仅是获得了一段可用的代码,更是对你逻辑思维和解决问题能力的一次锤炼。

http://www.jsqmd.com/news/1073865/

相关文章:

  • Windows HTTPS证书配置与Fiddler网络嗅探实战指南
  • Python pywifi库实战:从WiFi安全原理到密码强度测试脚本开发
  • 构建自动化图表分发管道:从数据可视化到可靠交付的工程实践
  • Embodied-AI入门指南:从仿真环境搭建到智能体训练实践
  • 使用Docker封装slowhttptest进行HTTP慢速攻击测试实战指南
  • OpenClaw本地部署指南:飞书智能体的可控调度引擎
  • OpenClaw不是模型而是智能网关:协议适配与模型路由原理
  • 终端渲染原理:React+Yoga+Canvas高性能实现解析
  • Simulink模型模块统计:从基础概念到工程实践
  • 深入解析Crossbar Switch仲裁机制:MPR与SGPCR配置实战指南
  • 用ChatGPT重构雅思听力:语音切分+逻辑动作双轨突破法
  • MATLAB uitable交互表格全解析:从创建到高级定制
  • 汇编语言与逆向工程:从基础指令到CTF实战的完整指南
  • 国产大模型合规应用指南:从选型到落地实践
  • Fancy Menus设计实战:从动效原理到性能优化的高效导航实现
  • 恒星形成中的FUor-like爆发:NGC 7538 MIR原恒星的多波段观测研究
  • MATLAB代码解析:从依赖分析到调试器实战的五步拆解法
  • LabVIEW机器视觉零件识别测量的工业落地实战指南
  • SGLang RBG调度器部署Qwen3-235B生产实践
  • 零成本本地大模型实战:Qwen3+Ollama+Next.js流式聊天全栈指南
  • PDF处理全栈实战:从系统打印到编程生成与AI解析
  • Workbuddy本地部署五大生存瓶颈与系统级调优指南
  • XSS-labs靶场通关指南:从原理到实战的20关Web安全进阶
  • Stable Diffusion本地部署全指南:从环境配置到模型管理
  • SO(10)大统一理论中的标量耦合增强机制与真空稳定性
  • 多语言大语言模型与大脑语言网络的因果关联研究
  • OpenClaw智能体框架:Git+API Key+Serverless的工程化实践
  • AI测试服务选型:三重角色与五大避坑指南
  • 构建无痛测试体系:从单元测试到E2E的实战分层防御策略
  • 合规AI编码助手接入方案:从模型部署到安全审计