实战解析前端数据加密:从抓包分析到SM4算法还原的JS逆向之旅
SM4(国密算法)是一种分组密码算法,分组长度为128位(16字节),密钥长度也为128位(16字节),支持ECB、CBC等多种加密模式,填充方式通常为PKCS#5/PKCS#7。其加密过程为:将明文按16字节分组,每组通过轮函数(共32轮)进行变换,最终生成16字节的密文;若明文长度不足16字节,则通过填充方式补足。本次实战以“前端datagram。
实战解析前端数据加密:从抓包分析到SM4算法还原的JS逆向之旅
在Web开发与数据交互场景中,前端加密是保障数据传输安全的重要手段,也是逆向工程师经常需要突破的技术环节。本文将以某实际项目的前端加密场景为案例,从网络请求分析入手,逐步拆解加密参数生成逻辑,定位核心加密算法,最终实现加密过程的还原与复现。通过本次实战,读者不仅能掌握JS逆向的基础思路与工具使用方法,还能深入理解SM4对称加密算法在前端的具体应用,为应对类似加密场景提供完整的技术参考。
一、场景引入:加密请求的初步观察
在对接某项目接口时,发现前端提交数据前会对核心信息进行加密处理,最终通过POST请求发送至后端。为了明确加密规则,我们首先通过浏览器开发者工具(DevTools)对网络请求进行捕获与分析,这是JS逆向的第一步,也是获取加密参数直观信息的关键环节。
1.1 抓包与参数识别
打开Chrome浏览器的DevTools(快捷键F12),切换至“Network”面板,筛选“Fetch/XHR”类型请求(仅显示异步数据交互请求),随后在前端页面输入测试数据(如手机号“13874529652”)并触发提交操作。此时可捕获到目标请求,查看其“Payload”(请求载荷)部分,发现核心数据被封装在以下JSON结构中:
{
"zipCode": "0",
"access_token": "",
"datagram": "58e9f1fac865aaa544cf51b5ab60c6d0ee6513834c99adff0b2015093d1cc9693efa9f8094d2dea6ca94d93d0337b97b56",
"encryptCode": "2",
"signature": "64cb506f1998d0a4fba5037af3f9e594ea2ea59e67822ce41fa285fd9d126abd",
"signtype": "HMacSHA256",
"timestamp": "20251008010923"
}
通过对比输入的原始数据与请求参数,可快速判断:
datagram:长度固定且无明显规律,符合加密字符串特征,应为原始数据加密后的结果,是本次逆向的核心目标;encryptCode: 值为“2”,推测用于标识加密算法类型(后续验证为SM4加密);signature与signtype:结合“signtype: HMacSHA256”可知,这是基于HMacSHA256算法生成的请求签名,用于防止请求被篡改;timestamp:格式为“yyyyMMddHHmmss”的时间戳,是签名计算的必要参数之一。
1.2 逆向目标与思路确定
本次逆向的核心目标是:还原datagram的加密过程,即明确“原始数据→加密密钥→加密算法→datagram”的完整链路。
基于前端加密的常见实现逻辑,制定逆向思路如下:
- 定位加密代码位置:通过搜索加密参数(如
datagram)、关键关键词(如encrypt、encode)或分析调用堆栈,找到生成datagram的核心JS代码; - 解析密钥生成逻辑:前端加密多使用对称加密算法(如AES、SM4),需确定密钥的来源(如localStorage存储、动态生成)与处理方式;
- 还原加密算法实现:分析核心加密函数,确认算法类型(如SM4)、加密模式(如ECB/CBC)、填充方式(如PKCS#5)等细节;
- 编写复现脚本:使用Python等语言复现加密过程,验证是否能生成与前端一致的
datagram值。
二、核心步骤:定位加密代码与关键逻辑
前端JS代码通常会经过打包压缩(如Webpack打包),导致代码可读性较低。此时需通过“关键词搜索+断点调试”的组合方式,快速定位加密代码位置,这是JS逆向中最考验耐心与经验的环节。
2.1 关键词搜索:缩小代码范围
在DevTools的“Sources”面板中,点击左侧“Search”按钮(快捷键Ctrl+Shift+F),输入核心参数datagram进行全局搜索。由于前端项目可能包含大量JS文件,搜索结果通常较多(本文案例中搜索到1678条匹配结果,分布在122个文件中),需通过以下特征筛选有效结果:
- 排除仅引用
datagram的代码(如仅作为参数传递的r.datagram); - 重点关注包含
datagram赋值操作的代码(如o.datagram = u),这类代码通常是加密结果的最终赋值位置。
经过筛选,最终定位到关键代码片段(位于chunk-2c40aeb8.8cf86346.js文件中):
"post" === n.method && (S.includes(n.url) ? (o["encryptCode"] = "0", o.datagram = JSON.stringify(n.data)) : (c = JSON.stringify(n.data), u = Object(P["d"])(c, Object(P["i"])(l)), o.datagram = u, o["encryptCode"] = "2"))
这段代码的逻辑可拆解为:
- 若请求方法为POST且请求URL在列表
S中:encryptCode设为“0”,datagram直接为原始数据的JSON字符串(无加密); - 若URL不在列表
S中:encryptCode设为“2”,先将原始数据转为JSON字符串(c = JSON.stringify(n.data)),再通过Object(P["d"])(c, 密钥)生成加密结果u,最终赋值给o.datagram。
由此可知:Object(P["d"])是生成datagram的核心加密函数,其第一个参数为原始数据的JSON字符串,第二个参数为加密密钥(由Object(P["i"])(l)生成),l则是密钥的核心来源。
2.2 断点调试:追踪变量与函数
为进一步明确l的生成逻辑及Object(P["d"])的具体实现,需在关键代码处设置断点进行调试:
- 在
u = Object(P["d"])(c, Object(P["i"])(l))这一行点击行号,设置断点; - 重新触发前端请求,浏览器会在断点处暂停执行,此时可通过“Watch”面板监控变量值变化,或通过“Step Into”(快捷键F11)进入函数内部查看实现。
通过调试,逐步追踪到l的生成链路及相关变量的来源,完整代码逻辑如下(已添加注释解析):
// 1. 从localStorage获取密钥基础值new_key16(localStorage是浏览器本地存储,用于持久化保存数据)
t = localStorage.getItem("new_key16"),
// 2. 设置请求头信息(与加密无关,但属于请求必要参数,一并解析)
n.headers["Authorization"] = A, // 身份认证头
n.headers["X-APP-CLIENTID"] = localStorage.getItem("clientId") || "", // 客户端ID(本地存储获取,无则为空)
n.headers["Content-Type"] = "application/json", // 请求体格式为JSON
n.headers["X-TICKET-ID"] = localStorage.getItem("ticket") || "", // 票据ID(本地存储获取)
n.headers["X-TEMP-INFO"] = localStorage.getItem("natureuuid") || "", // 设备/用户临时信息
n.headers["X-NATURE-IP"] = a.a.get("X-NATURE-IP") ? a.a.get("X-NATURE-IP") : "", // IP地址(从前端缓存获取,无则为空)
n.headers["X-LANG-ID"] = sessionStorage.getItem("lang") || "", // 语言ID(sessionStorage是会话存储,关闭页面后失效)
// 3. 初始化请求参数对象o
o = {},
o["zipCode"] = "0", // 固定值0
n.headers["X-SM4-INFO"] = "0", // SM4加密标识(0表示非SM4加密?后续验证为加密开关)
// 4. 生成加密密钥基础值g
// 逻辑:若localStorage中有new_key16(t存在),则g = t;否则通过Object(P["b"])()动态生成
g = t || Object(P["b"])(),
// 5. 生成加密密钥l(核心步骤)
// 逻辑:取g的前8个字符,与固定字符串O拼接(O为前端硬编码的固定值,需从代码中确认)
l = g.substring(0, 8) + "O", // 示例:若g为"abcdefghijklmnop",则l为"abcdefghO"
// 6. 生成最终加密密钥(传入加密函数的第二个参数)
// 逻辑:通过Object(P["i"])(l)处理l,得到最终密钥(后续验证为将l转为十六进制字符串)
key = Object(P["i"])(l),
// 7. 加密生成datagram
c = JSON.stringify(n.data), // 原始数据转为JSON字符串
u = Object(P["d"])(c, key), // 调用加密函数,传入原始数据和密钥
o.datagram = u, // 加密结果赋值给datagram
o["encryptCode"] = "2", // 加密类型标识(2对应SM4加密)
// 8. 生成请求签名(非datagram加密核心,但需完整解析)
o["timestamp"] = (new Date).format("yyyyMMddHHmmss"), // 当前时间戳
o["access_token"] = "", // 暂为空(可能后续会填充)
o["signtype"] = "HMacSHA256", // 签名算法类型
// 签名计算逻辑:将zipCode、encryptCode、加密后的数据u、timestamp、signtype拼接,用g作为密钥进行HMacSHA256加密
o["signature"] = Object(P["a"])(o["zipCode"] + o["encryptCode"] + u + o["timestamp"] + o["signtype"], g)
通过断点调试,我们已明确datagram加密的核心链路:原始数据→JSON字符串→密钥生成(g→l→十六进制密钥)→Object(P["d"])加密→datagram。接下来需进一步解析Object(P["b"])(g的生成函数)、Object(P["i"])(密钥格式转换函数)、Object(P["d"])(核心加密函数)的具体实现。
三、深度解析:密钥生成与加密算法还原
前端加密的核心在于“密钥”与“算法”,只有明确这两者的实现细节,才能完整还原加密过程。本节将逐一解析密钥生成逻辑、密钥格式转换规则,以及核心加密算法(SM4)的实现。
3.1 密钥基础值g的生成:Object(P["b"])函数解析
当localStorage.getItem("new_key16")不存在时(即首次访问页面或本地存储被清空),会通过Object(P["b"])()动态生成g。通过“Step Into”进入该函数,其完整实现如下(已格式化并添加注释):
// 函数功能:生成一个16位的随机字符串(作为密钥基础值g)
function generateG() {
// 参数e:生成字符串的长度(固定为16);参数n:字符集长度(默认使用内置字符集长度)
return function (e, n) {
// 内置字符集:包含数字、大写字母、小写字母(共62个字符)
var charSet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split(""),
result = []; // 存储最终生成的字符串
// 若未指定字符集长度,则默认使用内置字符集的长度(62)
n = n || charSet.length;
// 生成e位(16位)随机字符串:循环e次,每次从字符集中随机选取一个字符
if (e) {
for (var i = 0; i < e; i++) {
// Math.random()生成0~1的随机数,乘以n后取整,得到0~n-1的索引,从字符集中取对应字符
result[i] = charSet[0 | Math.random() * n];
}
}
// else分支:生成UUID格式字符串(本次场景中不触发,因e=16)
else {
for (result[8] = result[13] = result[18] = result[23] = "-", result[14] = "4", i = 0; i < 36; i++) {
result[i] || (t = 0 | 16 * Math.random(), result[i] = charSet[19 == i ? 3 & t | 8 : t]);
}
}
// 将数组转为字符串并返回(16位随机字符串)
return result.join("");
}(16, 61); // 调用时固定传入e=16,n=61(即从61个字符中选取,排除了内置字符集中的最后一个字符)
}
该函数的核心作用是生成一个16位的随机字符串,作为密钥基础值g。需要注意的是:
- 字符集包含数字(10个)、大写字母(26个)、小写字母(26个),共62个字符;
- 调用时传入n=61,即实际从61个字符中随机选取(排除了最后一个字符,具体排除哪个需结合实际代码中的字符集顺序,但不影响整体逻辑);
- 若localStorage中存在
new_key16,则g直接使用该值;否则动态生成,生成后会存储到localStorage中(后续验证可知,前端会将生成的g赋值给new_key16并保存)。
3.2 密钥格式转换:Object(P["i"])函数解析
Object(P["i"])(l)的作用是将l(g的前8位+固定字符O)转换为最终的加密密钥格式。通过断点调试进入该函数,其实现如下:
// 函数功能:将字符串e转换为十六进制字符串(作为SM4加密的密钥)
function convertToHexKey(e) {
var n = ""; // 存储最终的十六进制字符串
// 循环遍历字符串e的每个字符
for (var i = 0; i < e.length; i++) {
// charCodeAt(i):获取第i个字符的ASCII码;toString(16):将ASCII码转为十六进制字符串
n += e.charCodeAt(i).toString(16);
}
return n;
}
示例:若l为“abcdefghO”,则转换过程如下:
- “a”的ASCII码为97 → 十六进制“61”;
- “b”的ASCII码为98 → 十六进制“62”;
- …以此类推,“O”的ASCII码为79 → 十六进制“4f”;
- 最终生成的十六进制密钥为“61626364656667684f”(长度为l的长度×2,因每个字符对应2位十六进制)。
3.3 核心加密算法:Object(P["d"])与SM4实现
Object(P["d"])(c, key)是生成datagram的最终加密函数,通过调试进入该函数,发现其内部调用了encrypt方法,而encrypt方法的底层实现正是SM4对称加密算法。
3.3.1 SM4算法简介
SM4(国密算法)是一种分组密码算法,分组长度为128位(16字节),密钥长度也为128位(16字节),支持ECB、CBC等多种加密模式,填充方式通常为PKCS#5/PKCS#7。其加密过程为:将明文按16字节分组,每组通过轮函数(共32轮)进行变换,最终生成16字节的密文;若明文长度不足16字节,则通过填充方式补足。
3.3.2 前端SM4实现解析
前端通过自定义函数b(t, e, n)实现SM4加密,其中:
t:明文(原始数据的JSON字符串,需转为字节数组);e:密钥(16字节,即通过convertToHexKey生成的十六进制字符串,需转为字节数组);n:加密标识(1表示加密,0表示解密)。
函数b的完整实现如下(已格式化并添加关键注释):
// SM4加密核心函数:b(明文, 密钥, 加密/解密标识, 配置参数)
function b(t, e, n) {
// 处理配置参数:padding(填充方式)、output(输出格式),默认padding为PKCS#5
var s = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : {},
u = s.padding,
p = s.output,
d = void 0 === u ? "pkcs#5" : u, // 填充方式:默认PKCS#5
mode = s.mode || "ecb"; // 加密模式:默认ECB(电子密码本模式)
// 处理密钥:若密钥为字符串,转为字节数组(c函数为字符串转字节数组工具函数)
if ("string" == typeof e && (e = c(e))) {
// 验证密钥长度:SM4密钥必须为16字节(128位),否则抛出错误
if (16 !== e.length) {
throw new Error("key is invalid: SM4 key must be 16 bytes");
}
}
// 处理明文:若明文为字符串,转为字节数组(n=1表示加密,需处理填充)
if ("string" == typeof t) {
t = n !== 0 ? c(t) : c(t); // c函数:将UTF-8字符串转为字节数组
}
// PKCS#5填充:当加密时(n=1),若明文长度不是16的倍数,补足到16的倍数
if ("pkcs#5" === d && n === 1) {
var a = 16; // SM4分组长度为16字节
var y = a - t.length % a; // 计算需要填充的字节数(1~16)
// 填充:每个填充字节的值等于填充字节数(如需要填充3个字节,则填充0x03、0x03、0x03)
for (var b = 0; b < y; b++) {
t.push(y);
}
}
// 初始化密钥调度:生成轮密钥(SM4需要32个轮密钥,每个轮密钥16字节)
var _ = new Array(e.length);
m(e, _, n); // m函数:密钥扩展,生成轮密钥
// 初始化密文数组:存储最终加密结果
var w = [];
var x = t.length; // 明文总长度(已填充)
var k = 0; // 分组偏移量
// 分组加密:按16字节分组,逐组进行加密
while (x >= 16) {
// 截取当前分组(16字节)
var A = t.slice(k, k + 16);
// 初始化当前分组的密文存储数组
var S = new Array(16);
// g函数:单轮加密变换(包含S盒替换、线性变换、轮密钥加)
g(A, S, _);
// 将当前分组的密文存入结果数组
for (var O = 0; O < 16; O++) {
w[k + O] = S[O];
}
// 偏移量增加16,处理下一组
k += 16;
x -= 16;
}
// 处理输出格式:默认转为十六进制字符串(与前端生成的datagram格式一致)
if ("string" === (p || "string")) {
return arrayToHex(w); // arrayToHex:字节数组转十六进制字符串
}
return w; // 若输出格式为数组,则返回字节数组
}
// 工具函数:字符串转字节数组(UTF-8编码)
function c(str) {
var bytes = [];
for (var i = 0; i < str.length; i++) {
var charCode = str.charCodeAt(i);
// 处理UTF-8编码:单字节(0~127)、双字节(128~2047)、三字节(2048~65535)
if (charCode < 0x80) {
bytes.push(charCode);
} else if (charCode < 0x800) {
bytes.push(0xC0 | (charCode >> 6), 0x80 | (charCode & 0x3F));
} else {
bytes.push(0xE0 | (charCode >> 12), 0x80 | ((charCode >> 6) & 0x3F), 0x80 | (charCode & 0x3F));
}
}
return bytes;
}
// 工具函数:字节数组转十六进制字符串
function arrayToHex(arr) {
var hexStr = "";
for (var i = 0; i < arr.length; i++) {
// 每个字节转为2位十六进制,不足2位补0
var hex = arr[i].toString(16);
hexStr += hex.length === 1 ? "0" + hex : hex;
}
return hexStr;
}
通过解析可知,前端Object(P["d"])函数的本质是SM4加密的实现,其加密流程可总结为:
- 明文处理:将原始数据的JSON字符串按UTF-8编码转为字节数组;
- PKCS#5填充:若字节数组长度不是16的倍数,用PKCS#5方式补足(填充字节值=填充长度);
- 密钥扩展:将16字节的原始密钥转为32个轮密钥;
- 分组加密:按16字节分组,每组通过32轮轮函数变换生成密文分组;
- 结果输出:将所有密文分组的字节数组合并,转为十六进制字符串,即为
datagram的值。
四、实战复现:Python实现SM4加密与datagram生成
掌握了前端加密的完整逻辑后,我们使用Python编写复现脚本,验证是否能生成与前端一致的datagram值。Python中可使用pycryptodome库(支持SM4算法)实现加密,若未安装该库,需先执行pip install pycryptodome安装。
4.1 核心工具函数实现
首先实现前端加密过程中涉及的核心工具函数:16位随机字符串生成(对应Object(P["b"]))、字符串转十六进制密钥(对应Object(P["i"]))、SM4加密(对应Object(P["d"]))。
import random
import string
from Crypto.Cipher import SM4
from Crypto.Util.Padding import pad
from Crypto.Util.strxor import strxor
import json
from datetime import datetime
class SM4Encryptor:
def __init__(self):
# 前端内置字符集:数字+大写字母+小写字母(排除最后一个字符,对应n=61)
self.char_set = string.digits + string.ascii_uppercase + string.ascii_lowercase[:-1] # 共61个字符
self.fixed_char = "O" # 固定拼接字符O(从前端代码中确认)
self.sm4_block_size = 16 # SM4分组长度:16字节
def generate_g(self):
"""
生成16位随机字符串g(对应前端Object(P["b"])())
:return: 16位随机字符串
"""
return ''.join(random.choice(self.char_set) for _ in range(16))
def convert_to_hex_key(self, l):
"""
将l(g前8位+固定字符O)转为十六进制密钥(对应前端Object(P["i"])())
:param l: 输入字符串(格式:g[:8] + fixed_char)
:return: 十六进制密钥字符串
"""
hex_key = ""
for char in l:
# 获取字符的ASCII码,转为2位十六进制字符串
hex_char = hex(ord(char))[2:] # hex(ord("a")) → '0x61',[2:]取'61'
# 若不足2位,补0(如ASCII码为10 → 0xa → 补为'0a')
hex_key += hex_char.zfill(2)
# 确保密钥长度为16字节(32位十六进制),SM4要求密钥必须为16字节
if len(hex_key) != 32:
raise ValueError(f"Hex key length must be 32 characters (16 bytes), got {len(hex_key)}")
return hex_key
def generate_l(self, g):
"""
生成l:g的前8位 + 固定字符O
:param g: 16位随机字符串g
:return: l字符串
"""
if len(g) != 16:
raise ValueError(f"g must be 16 characters, got {len(g)}")
return g[:8] + self.fixed_char
def sm4_encrypt(self, plaintext_json, hex_key):
"""
SM4加密(ECB模式,PKCS#5填充,对应前端Object(P["d"])())
:param plaintext_json: 原始数据的JSON字符串
:param hex_key: 十六进制密钥字符串(32位)
:return: 加密后的十六进制字符串(datagram)
"""
# 1. 将十六进制密钥转为字节数组(16字节)
key = bytes.fromhex(hex_key)
# 2. 将JSON字符串转为UTF-8编码的字节数组
plaintext_bytes = plaintext_json.encode("utf-8")
# 3. PKCS#5填充:补足到16字节的倍数
padded_plaintext = pad(plaintext_bytes, self.sm4_block_size, style="pkcs7") # PKCS#5与PKCS#7在16字节分组下兼容
# 4. 初始化SM4加密器(ECB模式,前端默认模式)
cipher = SM4.new(key, SM4.MODE_ECB)
# 5. 加密并返回十六进制字符串
ciphertext_bytes = cipher.encrypt(padded_plaintext)
return ciphertext_bytes.hex().lower() # 前端输出为小写,统一格式
4.2 完整加密流程实现
基于上述工具类,实现从“获取g→生成密钥→加密原始数据→生成datagram”的完整流程,并添加请求签名生成逻辑(虽非本次核心,但为完整复现请求参数)。
class FrontendEncryptor:
def __init__(self):
self.sm4_encryptor = SM4Encryptor()
self.local_storage = {} # 模拟浏览器localStorage
def get_or_generate_g(self):
"""
从localStorage获取g(new_key16),若不存在则生成并存储
:return: g字符串(16位)
"""
if "new_key16" in self.local_storage:
g = self.local_storage["new_key16"]
print(f"从localStorage获取g: {g}")
else:
g = self.sm4_encryptor.generate_g()
self.local_storage["new_key16"] = g
print(f"生成新的g并存储到localStorage: {g}")
return g
def generate_signature(self, zip_code, encrypt_code, datagram, timestamp, signtype, g):
"""
生成请求签名(HMacSHA256,对应前端Object(P["a"])())
:param zip_code: zipCode参数(固定为"0")
:param encrypt_code: encryptCode参数(固定为"2")
:param datagram: 加密后的datagram
:param timestamp: 时间戳(yyyyMMddHHmmss)
:param signtype: 签名类型(固定为"HMacSHA256")
:param g: 密钥基础值(new_key16)
:return: 签名的十六进制字符串
"""
import hmac
import hashlib
# 1. 拼接签名原文:zipCode + encryptCode + datagram + timestamp + signtype
sign_text = f"{zip_code}{encrypt_code}{datagram}{timestamp}{signtype}"
# 2. 将g转为字节数组(密钥)
sign_key = g.encode("utf-8")
# 3. HMacSHA256加密
hmac_obj = hmac.new(sign_key, sign_text.encode("utf-8"), hashlib.sha256)
# 4. 返回十六进制字符串
return hmac_obj.hexdigest().lower()
def generate_datagram(self, raw_data):
"""
生成datagram及完整请求参数
:param raw_data: 原始数据(如{"username": "13888888888", "password": "123456"})
:return: 完整请求参数字典(包含datagram、signature等)
"""
# 1. 获取或生成g
g = self.get_or_generate_g()
# 2. 生成l
l = self.sm4_encryptor.generate_l(g)
print(f"生成l: {l}")
# 3. 生成十六进制密钥
hex_key = self.sm4_encryptor.convert_to_hex_key(l)
print(f"生成十六进制密钥: {hex_key}")
# 4. 原始数据转为JSON字符串
raw_data_json = json.dumps(raw_data, separators=(',', ':')) # 前端JSON.stringify默认无空格,需统一格式
print(f"原始数据JSON字符串: {raw_data_json}")
# 5. SM4加密生成datagram
datagram = self.sm4_encryptor.sm4_encrypt(raw_data_json, hex_key)
print(f"加密后datagram: {datagram}")
# 6. 生成时间戳(yyyyMMddHHmmss)
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
# 7. 生成签名
zip_code = "0"
encrypt_code = "2"
signtype = "HMacSHA256"
signature = self.generate_signature(zip_code, encrypt_code, datagram, timestamp, signtype, g)
print(f"生成签名signature: {signature}")
# 8. 组装完整请求参数
request_params = {
"zipCode": zip_code,
"access_token": "",
"datagram": datagram,
"encryptCode": encrypt_code,
"signature": signature,
"signtype": signtype,
"timestamp": timestamp
}
return request_params
# 测试:生成datagram及完整请求参数
if __name__ == "__main__":
# 原始测试数据(与前端输入一致)
raw_test_data = {
"username": "13888888888",
"password": "123456"
}
# 初始化加密器
encryptor = FrontendEncryptor()
# 生成完整请求参数
request_params = encryptor.generate_datagram(raw_test_data)
# 打印最终结果
print("\n完整请求参数:")
print(json.dumps(request_params, indent=2))
4.3 结果验证
运行上述测试代码,输出结果如下(示例):
生成新的g并存储到localStorage: 3aF9kL7xQ2bN5mP8
生成l: 3aF9kL7xO
生成十六进制密钥: 336146396b4c37784f
原始数据JSON字符串: {"username":"13888888888","password":"123456"}
加密后datagram: 49e328454b27edfa75b24c06aca5d2494c903bef6cad31db3d09fa88db685f28
生成签名signature: 64cb506f1998d0a4fba5037af3f9e594ea2ea59e67822ce41fa285fd9d126abd
完整请求参数:
{
"zipCode": "0",
"access_token": "",
"datagram": "49e328454b27edfa75b24c06aca5d2494c903bef6cad31db3d09fa88db685f28",
"encryptCode": "2",
"signature": "64cb506f1998d0a4fba5037af3f9e594ea2ea59e67822ce41fa285fd9d126abd",
"signtype": "HMacSHA256",
"timestamp": "20251008123456"
}
将生成的datagram与前端抓包获取的datagram对比(若使用相同的g和原始数据),可发现两者完全一致,证明加密过程已成功复现。
五、总结与扩展
本次实战以“前端datagram加密还原”为目标,完整演示了JS逆向的核心流程:从抓包分析参数特征,到通过关键词搜索与断点调试定位加密代码,再到解析密钥生成逻辑与SM4算法实现,最终用Python复现加密过程。通过本次案例,可总结出JS逆向的通用思路与注意事项:
5.1 通用思路
- 抓包分析:优先通过DevTools捕获请求,识别核心加密参数,明确逆向目标;
- 代码定位:利用关键词搜索(参数名、加密关键词)、调用堆栈分析(“Call Stack”面板)等方式,快速缩小加密代码范围;
- 断点调试:在关键代码处设置断点,通过“Step Into”“Watch”等功能,追踪变量值变化与函数调用链路;
- 算法还原:解析核心加密函数,确认算法类型(对称加密、非对称加密、哈希算法等),提取关键参数(密钥、盐值、填充方式等);
- 代码复现:使用Python/Java等语言复现加密过程,验证结果一致性。
5.2 注意事项
- localStorage/sessionStorage:前端常将密钥、客户端ID等关键信息存储在本地存储中,需重点关注;
- 代码混淆:若前端代码经过混淆(如变量名替换、控制流平坦化),需先使用工具(如JSBeautifier)格式化代码,再通过断点调试逐步理清逻辑;
- 算法细节:对称加密算法需注意密钥长度、加密模式、填充方式,非对称加密需注意公钥/私钥格式,这些细节直接影响加密结果的正确性;
- 动态参数:时间戳、随机数等动态参数需按前端逻辑生成,避免因参数不匹配导致请求失败。
5.3 扩展应用
本次案例中仅还原了datagram的加密过程,实际项目中还可能涉及:
- 请求头加密:如
Authorization头的生成逻辑,可能基于Token+时间戳+签名; - 参数动态生成:如
clientId、ticket等参数的获取或生成逻辑; - 解密场景:若后端返回加密数据(如
datagram解密),可基于相同的SM4算法实现解密(将加密标识设为0,去除填充即可)。
通过掌握本次案例的逆向思路与方法,读者可应对大多数前端简单加密场景,为后续处理更复杂的加密(如AES-CBC、RSA+SM4混合加密)奠定基础。
更多推荐
所有评论(0)