实战解析前端数据加密:从抓包分析到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加密);
  • signaturesigntype:结合“signtype: HMacSHA256”可知,这是基于HMacSHA256算法生成的请求签名,用于防止请求被篡改;
  • timestamp:格式为“yyyyMMddHHmmss”的时间戳,是签名计算的必要参数之一。

1.2 逆向目标与思路确定

本次逆向的核心目标是:还原datagram的加密过程,即明确“原始数据→加密密钥→加密算法→datagram”的完整链路。

基于前端加密的常见实现逻辑,制定逆向思路如下:

  1. 定位加密代码位置:通过搜索加密参数(如datagram)、关键关键词(如encryptencode)或分析调用堆栈,找到生成datagram的核心JS代码;
  2. 解析密钥生成逻辑:前端加密多使用对称加密算法(如AES、SM4),需确定密钥的来源(如localStorage存储、动态生成)与处理方式;
  3. 还原加密算法实现:分析核心加密函数,确认算法类型(如SM4)、加密模式(如ECB/CBC)、填充方式(如PKCS#5)等细节;
  4. 编写复现脚本:使用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"])的具体实现,需在关键代码处设置断点进行调试:

  1. u = Object(P["d"])(c, Object(P["i"])(l))这一行点击行号,设置断点;
  2. 重新触发前端请求,浏览器会在断点处暂停执行,此时可通过“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加密的实现,其加密流程可总结为:

  1. 明文处理:将原始数据的JSON字符串按UTF-8编码转为字节数组;
  2. PKCS#5填充:若字节数组长度不是16的倍数,用PKCS#5方式补足(填充字节值=填充长度);
  3. 密钥扩展:将16字节的原始密钥转为32个轮密钥;
  4. 分组加密:按16字节分组,每组通过32轮轮函数变换生成密文分组;
  5. 结果输出:将所有密文分组的字节数组合并,转为十六进制字符串,即为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 通用思路

  1. 抓包分析:优先通过DevTools捕获请求,识别核心加密参数,明确逆向目标;
  2. 代码定位:利用关键词搜索(参数名、加密关键词)、调用堆栈分析(“Call Stack”面板)等方式,快速缩小加密代码范围;
  3. 断点调试:在关键代码处设置断点,通过“Step Into”“Watch”等功能,追踪变量值变化与函数调用链路;
  4. 算法还原:解析核心加密函数,确认算法类型(对称加密、非对称加密、哈希算法等),提取关键参数(密钥、盐值、填充方式等);
  5. 代码复现:使用Python/Java等语言复现加密过程,验证结果一致性。

5.2 注意事项

  1. localStorage/sessionStorage:前端常将密钥、客户端ID等关键信息存储在本地存储中,需重点关注;
  2. 代码混淆:若前端代码经过混淆(如变量名替换、控制流平坦化),需先使用工具(如JSBeautifier)格式化代码,再通过断点调试逐步理清逻辑;
  3. 算法细节:对称加密算法需注意密钥长度、加密模式、填充方式,非对称加密需注意公钥/私钥格式,这些细节直接影响加密结果的正确性;
  4. 动态参数:时间戳、随机数等动态参数需按前端逻辑生成,避免因参数不匹配导致请求失败。

5.3 扩展应用

本次案例中仅还原了datagram的加密过程,实际项目中还可能涉及:

  • 请求头加密:如Authorization头的生成逻辑,可能基于Token+时间戳+签名;
  • 参数动态生成:如clientIdticket等参数的获取或生成逻辑;
  • 解密场景:若后端返回加密数据(如datagram解密),可基于相同的SM4算法实现解密(将加密标识设为0,去除填充即可)。

通过掌握本次案例的逆向思路与方法,读者可应对大多数前端简单加密场景,为后续处理更复杂的加密(如AES-CBC、RSA+SM4混合加密)奠定基础。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐