RSA
RSA 是最常见的非对称加密算法。公钥加密,私钥解密;或私钥签名,公钥验签。JS 逆向中通常只需要用公钥加密(还原加密过程),私钥由服务端持有,无法获取。 逆向时在源码中找到的公钥常见两种形式: 还有一种是直接给出模数(n)和指数(e): 依赖库:pip install pycryptodome 或 pip install cryptography 搜索 JSEncrypt 或 -----BEGIN PUBLIC KEY----- 快速定位:这两个关键词几乎覆盖了 Web 端 RSA 的所有使用场景;其次搜索 setPublicKey、encrypt、B
官方文档:https://www.rfc-editor.org/rfc/rfc8017
适用场景:JS 逆向中提取 RSA 公钥、还原加密参数生成逻辑
RSA 是最常见的非对称加密算法。公钥加密,私钥解密;或私钥签名,公钥验签。JS 逆向中通常只需要用公钥加密(还原加密过程),私钥由服务端持有,无法获取。
关键概念
| 概念 | 说明 |
|---|---|
| 公钥 | 可公开,用于加密或验签 |
| 私钥 | 保密,用于解密或签名 |
| 密钥长度 | 常见 1024 / 2048 / 4096 位 |
| 填充方式 | PKCS#1 v1.5(含随机字节,同一明文每次密文不同)、OAEP(更安全) |
| 输出格式 | 通常为 Base64 |
公钥格式
逆向时在源码中找到的公钥常见两种形式:
-----BEGIN PUBLIC KEY----- ← SPKI 格式(X.509)
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----
-----BEGIN RSA PUBLIC KEY----- ← PKCS#1 格式
MIIBCgKCAQEA...
-----END RSA PUBLIC KEY-----
还有一种是直接给出模数(n)和指数(e):
// 源码中常见
var rsa = new JSEncrypt();
rsa.setPublicKey('-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----');
Python 实现
依赖库:pip install pycryptodome 或 pip install cryptography
使用 pycryptodome
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP, PKCS1_v1_5
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
import base64
# ── 生成密钥对(逆向时通常不需要,直接用现成公钥)──────────────
def generate_rsa_keypair(bits=2048):
key = RSA.generate(bits)
return {
'private': key.export_key().decode(),
'public': key.publickey().export_key().decode()
}
# ── PKCS#1 v1.5 加密(常见于老系统)──────────────────────────
def rsa_pkcs1_encrypt(plaintext: str, public_key_pem: str) -> str:
key = RSA.import_key(public_key_pem)
cipher = PKCS1_v1_5.new(key)
return base64.b64encode(cipher.encrypt(plaintext.encode())).decode()
def rsa_pkcs1_decrypt(ciphertext_b64: str, private_key_pem: str) -> str:
key = RSA.import_key(private_key_pem)
cipher = PKCS1_v1_5.new(key)
return cipher.decrypt(base64.b64decode(ciphertext_b64), None).decode()
# ── OAEP 加密(更安全,现代推荐)─────────────────────────────
def rsa_oaep_encrypt(plaintext: str, public_key_pem: str) -> str:
key = RSA.import_key(public_key_pem)
cipher = PKCS1_OAEP.new(key)
return base64.b64encode(cipher.encrypt(plaintext.encode())).decode()
def rsa_oaep_decrypt(ciphertext_b64: str, private_key_pem: str) -> str:
key = RSA.import_key(private_key_pem)
cipher = PKCS1_OAEP.new(key)
return cipher.decrypt(base64.b64decode(ciphertext_b64)).decode()
# ── 签名与验签 ────────────────────────────────────────────────
def rsa_sign(message: str, private_key_pem: str) -> str:
key = RSA.import_key(private_key_pem)
msg_hash = SHA256.new(message.encode())
signature = pkcs1_15.new(key).sign(msg_hash)
return base64.b64encode(signature).decode()
def rsa_verify(message: str, signature_b64: str, public_key_pem: str) -> bool:
key = RSA.import_key(public_key_pem)
msg_hash = SHA256.new(message.encode())
try:
pkcs1_15.new(key).verify(msg_hash, base64.b64decode(signature_b64))
return True
except (ValueError, TypeError):
return False
# ── 从模数和指数构造公钥(逆向中常见)────────────────────────
def rsa_from_n_e(n_hex: str, e: int = 65537) -> str:
"""从十六进制模数和指数构造公钥 PEM"""
n = int(n_hex, 16)
key = RSA.construct((n, e))
return key.publickey().export_key().decode()
使用 cryptography 库
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
import base64
# 加载公钥
def load_public_key(pem: str):
return serialization.load_pem_public_key(pem.encode())
# OAEP 加密
def rsa_oaep_encrypt(plaintext: str, public_key_pem: str) -> str:
pub_key = load_public_key(public_key_pem)
ciphertext = pub_key.encrypt(
plaintext.encode(),
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return base64.b64encode(ciphertext).decode()
# PKCS#1 v1.5 加密
def rsa_pkcs1_encrypt(plaintext: str, public_key_pem: str) -> str:
pub_key = load_public_key(public_key_pem)
ciphertext = pub_key.encrypt(plaintext.encode(), padding.PKCS1v15())
return base64.b64encode(ciphertext).decode()
JavaScript 实现
使用 JSEncrypt(浏览器,逆向中最常见)
// 浏览器环境,源码中常见此写法
const JSEncrypt = require('jsencrypt').JSEncrypt;
const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----`;
function rsaEncrypt(plaintext, pubKey) {
const rsa = new JSEncrypt();
rsa.setPublicKey(pubKey);
return rsa.encrypt(plaintext); // 返回 Base64 字符串
}
// 解密(需要私钥,逆向中通常无法获取)
function rsaDecrypt(ciphertext, privKey) {
const rsa = new JSEncrypt();
rsa.setPrivateKey(privKey);
return rsa.decrypt(ciphertext);
}
console.log(rsaEncrypt('hello', publicKey));
使用 node-forge
const forge = require('node-forge');
const publicKeyPem = `-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----`;
// PKCS#1 v1.5 加密
function rsaEncryptPkcs1(plaintext, pubKeyPem) {
const publicKey = forge.pki.publicKeyFromPem(pubKeyPem);
const encrypted = publicKey.encrypt(plaintext, 'RSAES-PKCS1-V1_5');
return forge.util.encode64(encrypted);
}
// OAEP 加密
function rsaEncryptOaep(plaintext, pubKeyPem) {
const publicKey = forge.pki.publicKeyFromPem(pubKeyPem);
const encrypted = publicKey.encrypt(plaintext, 'RSA-OAEP', {
md: forge.md.sha256.create(),
mgf1: { md: forge.md.sha256.create() }
});
return forge.util.encode64(encrypted);
}
// 签名(需要私钥)
function rsaSign(message, privKeyPem) {
const privateKey = forge.pki.privateKeyFromPem(privKeyPem);
const md = forge.md.sha256.create();
md.update(message, 'utf8');
return forge.util.encode64(privateKey.sign(md));
}
使用 Node.js 内置 crypto
const crypto = require('crypto');
const fs = require('fs');
// 加载公钥文件
const publicKey = fs.readFileSync('public.pem', 'utf8');
// OAEP 加密
function rsaOaepEncrypt(plaintext, pubKey) {
return crypto.publicEncrypt(
{
key: pubKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
Buffer.from(plaintext)
).toString('base64');
}
// PKCS#1 v1.5 加密
function rsaPkcs1Encrypt(plaintext, pubKey) {
return crypto.publicEncrypt(
{ key: pubKey, padding: crypto.constants.RSA_PKCS1_PADDING },
Buffer.from(plaintext)
).toString('base64');
}
// 私钥解密(OAEP)
function rsaOaepDecrypt(ciphertextB64, privKey) {
return crypto.privateDecrypt(
{
key: privKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256'
},
Buffer.from(ciphertextB64, 'base64')
).toString('utf8');
}
逆向中的定位方法
| 特征 | 说明 |
|---|---|
BEGIN PUBLIC KEY |
源码中直接硬编码公钥字符串 |
setPublicKey / setKey |
JSEncrypt 的 API |
publicKeyFromPem |
node-forge |
rsa.encrypt |
常见函数名 |
| 密文每次不同 | PKCS#1 v1.5 填充含随机字节,可此识别 RSA |
注意事项
- RSA 加密的数据长度受限:1024 位密钥最多加密 117 字节(PKCS#1 v1.5),因此 RSA 通常只用于加密对称密钥(如 AES key)
- 逆向时通常只能做加密,无法解密(没有私钥)
- 若源码使用
new JSEncrypt()并从接口动态获取公钥,需抓包找到公钥
最佳实践
搜索 JSEncrypt 或 -----BEGIN PUBLIC KEY----- 快速定位:这两个关键词几乎覆盖了 Web 端 RSA 的所有使用场景;其次搜索 setPublicKey、encrypt、BigInteger(RSA 底层大数运算)。
Hook JSEncrypt.prototype.encrypt 打印明文:
const _encrypt = JSEncrypt.prototype.encrypt
JSEncrypt.prototype.encrypt = function(str) {
console.log('[RSA encrypt input]', str)
const result = _encrypt.call(this, str)
console.log('[RSA encrypt output]', result)
return result
}
公钥从接口动态获取时先抓包再 Hook:若公钥通过接口下发,先抓包记录公钥内容;然后在 Python 中用相同公钥和 padding 方案重现加密:from Crypto.PublicKey import RSA; from Crypto.Cipher import PKCS1_v1_5。
区分 PKCS#1 v1.5 和 OAEP 填充:JSEncrypt 默认使用 PKCS#1 v1.5;SubtleCrypto 通常使用 OAEP。Python 中对应 PKCS1_v1_5.new(key).encrypt(data) 和 PKCS1_OAEP.new(key).encrypt(data),填充方案不同结果不同。
RSA 加密非确定性:同一明文每次加密结果不同(因为 PKCS#1 v1.5 / OAEP 含随机填充),不能通过对比密文验证。需用私钥解密或抓包后对比明文。
常见陷阱
陷阱:用 Python 加密后结果与 JS 不同
现象: Python PKCS1_v1_5.new(key).encrypt(data) 结果与 JS JSEncrypt.encrypt(data) 不同。
原因: 两者输出都正确——PKCS#1 v1.5 含随机 padding,每次加密结果天然不同。服务端用私钥解密后得到相同明文。
解决: 不能比较密文;验证方式是将 Python 加密的结果发给接口,看服务端能否正确解密处理。
陷阱:Base64 编码与 Hex 编码混淆
现象: Python RSA 加密结果是字节,转为十六进制后与 JS 的 Base64 输出对不上。
原因: JSEncrypt 默认输出 Base64,Python pycryptodome 返回 bytes,需要手动 base64.b64encode(result).decode()。
解决: 统一输出格式后再对比;注意标准 Base64 和 URL-safe Base64(+ → -,/ → _)的区别。
陷阱:公钥格式不同导致解析失败
现象: 接口返回的公钥没有 -----BEGIN PUBLIC KEY----- 头,Python RSA 库解析报错。
原因: 有些接口返回裸 Base64 编码的公钥(DER 格式),JSEncrypt 会自动识别,但 RSA.import_key() 需要完整 PEM 格式。
解决: 手动拼接头尾:"-----BEGIN PUBLIC KEY-----\n" + raw_key + "\n-----END PUBLIC KEY-----" 后再导入。