1. 项目概述当数据穿上SM4的“小马甲”最近在分析某个行业数据查询平台这里我们姑且称之为“行行查”的接口时遇到了一个挺有意思的挑战。它的核心数据接口返回的响应体乍一看是一串毫无规律的、长得像Base64的字符串但常规的Base64解码后得到的却是乱码。这通常意味着数据在传输前被额外“包装”了一层。通过抓包工具对请求响应流程的仔细比对和逆向分析我发现这层“包装”正是国密算法SM4。这就像数据穿上了一件定制的“小马甲”不搞清楚这件马甲的材质和穿法你就没法看到里面的真实内容。SM4作为一种分组密码算法在不少对数据安全有特定要求的国内应用场景中越来越常见。它和AES类似都属于对称加密加解密使用同一个密钥。但它的算法结构、S盒和密钥扩展方式都是自主设计的。面对这样一个被SM4加密的响应体我们的目标很明确逆向分析出它的加密模式、填充方式、密钥以及初始向量IV最终实现一个能够稳定解密的工具或脚本。这个过程不仅涉及对加密算法本身的理解更考验我们在缺乏文档的情况下如何通过黑盒测试、代码分析和逻辑推理来还原整个加解密流程。对于从事数据爬取、安全研究或接口调试的开发者来说这是一项非常实用的技能。2. 逆向分析的核心思路与准备工作逆向一个加密接口不能像无头苍蝇一样乱撞。一个清晰的思路能事半功倍。我们的核心目标是还原服务端的加密过程并在客户端复现解密过程。整个过程可以拆解为几个关键步骤。2.1 逆向分析的基本方法论首先要明确逆向分析不是猜谜而是基于证据的推理。我们的所有结论都应该来源于对客户端通常是网页或App行为的观察和分析。基本思路是“由外而内动静结合”静态分析直接查看前端JavaScript代码对于Web端或反编译移动端App对于Android/iOS寻找与加密、SM4、Crypto等相关的关键字、函数定义和密钥硬编码。这是最直接的方法但现代前端代码往往经过混淆和压缩增加了阅读难度。动态调试在浏览器开发者工具中对疑似加密函数的入口设置断点单步跟踪执行观察函数的输入明文、密钥、IV和输出密文。这是最有效的手段可以直观地看到算法被调用时的所有参数。网络抓包比对这是我们的起点和验证依据。通过抓包工具如Fiddler、Charles或浏览器Network面板捕获API请求和响应。重点关注请求参数中是否包含加密相关的标识如encryptType: SM4以及响应头中是否有提示。更重要的是对比多次请求观察密文的变化规律这有助于推断加密模式如ECB模式下的相同明文对应相同密文CBC模式则不同。黑盒测试与推断当代码混淆严重或无法调试时我们可以通过构造特定的测试数据来推断加密细节。例如发送一个非常短的字符串如”a”观察密文长度可以推断出分组大小和填充方式PKCS#7填充会增加一个完整分组的数据。2.2 必备工具与环境搭建工欲善其事必先利其器。以下是进行此类逆向分析的常用工具栈1. 网络抓包与分析工具Fiddler / Charles功能强大的HTTP/HTTPS代理工具可以拦截、查看和修改所有经过设备的网络流量。配置手机代理后可以抓取App的请求这是分析移动端接口的必备技能。需要安装并信任其根证书以解密HTTPS流量。浏览器开发者工具 (DevTools)对于Web端分析这是核心工具。Network面板用于抓包Sources面板用于查看和调试JavaScript代码Console面板可以执行代码片段进行测试。2. 代码搜索与调试工具全局搜索在DevTools的Sources面板中使用CtrlShiftF进行全局文件搜索关键词如sm4、encrypt、decrypt、CryptoJS、bcprovBouncy Castle库的常见标识等。美化Pretty Print面对被压缩成单行的JS代码点击代码面板左下角的{}按钮进行格式化让代码恢复可读性。断点调试在疑似加密函数所在行号上点击设置断点。重新触发请求代码执行会在断点处暂停此时可以在Scope和Watch面板中查看所有变量的值。3. 加解密验证与算法库在线加解密工具用于快速验证猜想。例如可以搜索“SM4在线加密解密”找到一些提供Web界面的工具。输入你推测的密钥、IV、模式和明文看生成的密文是否与抓包结果一致。注意切勿在不可信的网站上使用真实业务的密钥或敏感数据。本地算法库最终实现解密需要依赖可靠的库。在Python中常用的有gmssl库国密算法实现和cryptography库。在JavaScript环境中可能会遇到CryptoJS的自定义扩展或直接使用bcprov-jdk18on库的Java版本在Android逆向中常见。4. 编程环境Python环境安装requests用于模拟请求gmssl用于SM4算法操作。pip install gmssl即可。Node.js环境如果加密逻辑是用JS实现的你可能需要在一个独立的Node环境中复现并调试这段JS代码。注意整个分析过程必须在合法的范围内进行仅用于学习、测试自己拥有权限的系统或获得明确授权的安全评估。未经授权对他人系统进行逆向分析和数据抓取可能涉及法律风险。3. 关键环节深度解析定位与破解SM4参数有了思路和工具我们就可以开始实战了。以“行行查”这类平台为例其Web端是常见的分析入口。我们假设通过抓包得到了一个加密的响应体字符串类似”U2FsdGVkX1…很长一串”但Base64解码后非ASCII字符。接下来就是一步步揭开它的秘密。3.1 密钥与IV的踪迹探寻对称加密的核心是密钥。寻找密钥通常有以下几种路径硬编码在前端代码中这是最简单也最不安全的方式。在格式化后的JS代码中搜索诸如key、secret、iv、cipherKey等变量名看其是否被直接赋值了一个字符串或Hex/Buffer。有时密钥可能是通过一个简单的变换如反转、Base64解码从某个常量字符串得来。由固定种子生成密钥可能不是直接写死的而是通过一个固定的“种子”Seed字符串经过某种哈希函数如MD5、SHA256计算得出。在代码中搜索MD5、SHA256或createHash等函数调用看其输入参数是否为固定值。从登录会话或初始化请求中动态获取更复杂的系统会在用户登录后由服务端生成一个临时会话密钥Session Key下发给客户端后续通信都用这个密钥加密。你需要分析登录接口的响应看看是否有类似encryptKey、sessionKey的字段。这个密钥本身可能还被非对称加密如RSA包装过需要先用客户端的私钥解密。隐藏在Webpack模块或异步加载的代码中现代前端工程化项目可能将加密模块单独打包在运行时动态加载。你需要关注网络请求中是否有额外的.js文件特别是带有chunk、vendor字样的并在这些文件中寻找密钥。以“行行查”的假设场景为例通过全局搜索sm4我们可能定位到一个名为sm4utils.js的文件或一个包含encryptSM4函数的模块。在该函数内部我们发现了类似这样的定义const SM4_KEY ‘0123456789abcdef0123456789abcdef’; // 32位十六进制字符串即16字节 const SM4_IV ‘abcdef0123456789abcdef0123456789’; // 32位十六进制字符串即16字节这就直接找到了密钥和IV。它们通常是16字节128位的十六进制字符串表示。有时也可能是Base64编码的需要先解码。3.2 加密模式与填充方式的判断SM4和AES一样有多种工作模式如ECB, CBC, CFB, OFB和填充方式如PKCS#7, ZeroPadding。模式决定了每个数据块如何被加密以及块与块之间的关系填充则解决了数据长度不是分组大小整数倍的问题。如何判断模式CBC模式最常见在Web应用中出于安全性考虑CBC密码分组链接模式是最常用的因为它需要一个IV来使相同的明文产生不同的密文。如果你在代码中找到了IV的使用那基本就是CBC模式。查看代码调用在JS代码中如果使用CryptoJS库可能会看到CryptoJS.mode.CBC如果使用某个具体的SM4实现函数参数或内部变量名可能会提示mode: ‘cbc’。通过密文推断发送两次完全相同的请求如果响应密文完全不同那么很可能使用了CBC或其它需要IV/随机数的模式。如果密文完全相同则可能是ECB模式不安全较少使用。如何判断填充PKCS#7/PKCS#5填充是标准这是最普遍的填充方式。当数据块长度不足时会填充若干个字节每个字节的值等于填充的字节数。例如需要填充3个字节则填充0x03 0x03 0x03。代码证据在加密函数中寻找padding相关配置如CryptoJS.pad.Pkcs7。测试推断加密一个长度为15字节的数据SM4分组为16字节。如果得到的密文长度是32字节两个分组那么很可能使用了PKCS#7填充因为它增加了一个完整的字节0x01凑满了16字节然后加密得到第二个分组。在我们的案例中通过调试发现加密函数调用类似于sm4.encrypt(data, key, {mode: ‘cbc’, iv: iv, padding: ‘pkcs7’})这就明确指出了是CBC模式和PKCS#7填充。3.3 数据编码与传输格式的确认服务端返回的密文并不是原始的二进制字节流为了能在JSON或文本协议中安全传输通常会进行编码。Base64编码这是最最最常见的编码方式。抓包看到的密文是一串由A-Z, a-z, 0-9, , /, 组成的字符串很可能就是Base64。你可以尝试用在线工具或编程语言解码如果解码后的二进制数据看起来杂乱无章包含很多不可打印字符那它就是加密后的原始字节。Hex编码十六进制有时也会直接使用十六进制字符串表示即0-9, a-f的字符组成长度为原始字节数的两倍。组合情况有些实现会先加密得到二进制数据然后进行Base64编码最后可能还会进行URL安全的处理将和/替换为-和_。在“行行查”的例子里抓包看到的响应体直接就是一个很长的Base64字符串。尝试用Python的base64.b64decode解码后得到了一堆乱码这正好印证了它是加密后的二进制数据再进行的Base64编码。4. 完整解密流程的代码实现与验证理论分析完毕参数也已齐备接下来就是用代码将解密过程实现出来。这里我们以Python为例使用gmssl库因为它对国密算法支持较好。4.1 Python解密代码实现假设我们已经通过逆向分析确定了以下参数密钥Key:0123456789abcdef0123456789abcdef(32位Hex16字节)初始向量IV:abcdef0123456789abcdef0123456789(32位Hex16字节)模式: CBC填充: PKCS#7密文编码: Base64以下是完整的解密函数import base64 from gmssl import sm4 def decrypt_response(encrypted_base64: str, key_hex: str, iv_hex: str) - str: 解密行行查风格的SM4-CBC加密响应。 Args: encrypted_base64: 服务端返回的Base64编码密文。 key_hex: 16字节密钥的十六进制字符串32字符。 iv_hex: 16字节初始向量的十六进制字符串32字符。 Returns: 解密后的原始明文字符串。 # 1. 将Hex字符串的密钥和IV转换为字节 key_bytes bytes.fromhex(key_hex) iv_bytes bytes.fromhex(iv_hex) # 2. Base64解码得到加密后的二进制数据 encrypted_bytes base64.b64decode(encrypted_base64) # 3. 创建SM4解密对象 crypt_sm4 sm4.CryptSM4() # 4. 设置解密模式为CBC并设置密钥和IV crypt_sm4.set_key(key_bytes, sm4.SM4_DECRYPT) # 注意这里是DECRYPT模式 crypt_sm4.set_iv(iv_bytes) # 5. 执行解密。gmssl的decrypt_xxx函数默认使用PKCS#7填充。 # 第一个参数是加密数据第二个参数是加密模式sm4.SM4_CBC decrypt_bytes crypt_sm4.crypt_cbc(encrypted_bytes) # 6. 移除PKCS#7填充 # PKCS#7填充的最后一个字节的值表示填充的长度 padding_len decrypt_bytes[-1] # 验证填充的合法性可选但推荐 if padding_len 1 or padding_len 16: raise ValueError(Invalid PKCS#7 padding detected.) for i in range(1, padding_len 1): if decrypt_bytes[-i] ! padding_len: raise ValueError(Invalid PKCS#7 padding bytes.) # 移除填充 plaintext_bytes decrypt_bytes[:-padding_len] # 7. 将字节解码为字符串假设原明文是UTF-8编码的JSON或文本 plaintext plaintext_bytes.decode(utf-8) return plaintext # 使用示例 if __name__ __main__: # 这是假设的加密响应实际应从抓包获取 encrypted_data 你的Base64密文字符串... my_key 0123456789abcdef0123456789abcdef my_iv abcdef0123456789abcdef0123456789 try: result decrypt_response(encrypted_data, my_key, my_iv) print(解密成功) print(解密结果:, result) except Exception as e: print(f解密失败: {e})4.2 分步详解与注意事项参数转换bytes.fromhex()是将我们找到的十六进制字符串转换为Python字节对象bytes的标准方法。确保你的密钥和IV字符串是有效的32位十六进制数0-9, a-f。Base64解码base64.b64decode()是标准库函数能处理标准的Base64编码。如果遇到URL安全的Base64-和_需要使用base64.urlsafe_b64decode()。设置解密模式set_key函数的第二个参数至关重要解密时必须传入sm4.SM4_DECRYPT。set_iv方法只有在CBC、CFB等模式下才需要调用。执行解密crypt_cbc方法内部完成了CBC模式的解密计算。gmssl库的这个方法在解密后会自动尝试移除PKCS#7填充但为了健壮性我们仍然在代码中手动验证和移除了一遍。有些其他库如某些Java实现可能不会自动处理填充需要手动调用。填充移除手动移除填充的代码是一个好习惯。它首先检查最后一个字节的值填充长度然后验证该字节数内的所有填充字节值是否都等于这个长度。这能有效防止因错误的密文或密钥导致的异常数据被误解析。编码解码最后一步的.decode(‘utf-8’)假设服务端返回的明文是UTF-8编码的JSON或文本。如果原始数据是其他编码如GBK或者根本就是二进制数据如图片则需要相应调整。解密后可以先打印plaintext_bytes的前几个字节看看是否是常见的JSON开头{或[。4.3 验证与调试技巧使用在线工具交叉验证在获得密钥和IV后可以先找一个在线的SM4加密工具。用相同的参数Key, IV, CBC, PKCS7加密一小段已知的测试明文如”test”看看生成的密文经过Base64编码后是否与你抓包到的短请求密文模式相似。这可以快速验证你的参数是否正确。从简单请求入手不要一开始就尝试解密复杂的、数据量大的响应。先找一个返回数据非常简单、固定的接口例如一个心跳接口或查询系统时间的接口进行解密测试。成功解密出可读的明文如{“status”:”ok”}能给你巨大的信心。打印中间结果在解密函数中打印出key_bytes、iv_bytes、encrypted_bytes的长度和头几个字节确保数据在转换过程中没有出错。处理异常代码中加入了基本的填充验证和异常捕获在实际使用中应该根据业务需要完善错误处理逻辑比如记录日志、重试或返回友好的错误信息。5. 逆向实战中的常见问题与排查实录即使思路清晰工具齐全在实际逆向过程中也难免会遇到各种“坑”。下面记录一些典型问题及其解决方案这些都是从一次次实战中积累下来的经验。5.1 问题一找到的密钥解密失败报“填充错误”或解密后是乱码这是最常见的问题可能的原因有多个层次原因A密钥或IV错误。这是最根本的原因。你可能找到了一个key变量但它可能不是最终使用的密钥。检查代码中这个key是否被后续的其他函数处理过如二次哈希、截取部分字符、与某个值进行异或等。排查方法在设置断点的加密函数入口处直接查看即将传入加密函数的key和iv变量的实时值而不是看源代码里的初始值。这个实时值才是真正的密钥。原因B加密模式或填充方式判断错误。你以为它是CBC但其实是ECB你以为它是PKCS7但实际是ZeroPadding零填充。排查方法模式如果代码中找不到明确的mode设置尝试用ECB模式解密一次不提供IV。如果ECB能解密出部分可读内容尽管可能末尾有乱码那可能就是ECB模式。填充如果解密后末尾有规律的乱码比如多个\x01、\x02…那可能是PKCS7填充被当作数据解码了。尝试在解密后不自动去除填充而是手动查看最后几个字节。对于ZeroPadding解密后的数据末尾可能是多个\x00。原因C数据编码/解码环节出错。服务端返回的密文可能不是标准的Base64。可能是Base64 URL Safe或者先进行了Hex编码再Base64甚至可能混合了其他字符。排查方法仔细查看抓包得到的原始密文字符串检查是否有-、_替换了、/或者是否有换行符。尝试不同的解码组合。原因D算法实现细节差异。SM4虽然标准统一但不同库在实现上可能有细微差别例如字节序Endian问题、S盒的实现版本等。排查方法尝试换一个算法库。如果在JS逆向中用的是CryptoJS的某个扩展那么用Python的gmssl解密可能不兼容。可以尝试用Node.js环境直接执行你提取出来的那段JS加密函数确保环境一致性。5.2 问题二密钥似乎是动态的每次请求都不同这说明系统使用了更安全的动态会话密钥机制。你需要找到这个密钥的生成或获取逻辑。场景登录接口的响应中有一个字段叫sessionKey或encryptKey后续所有数据请求都用这个key。逆向点找到处理登录响应的代码部分看这个sessionKey是如何被存储和传递给加密函数的。它可能被保存在内存变量、Vuex/Redux状态管理库、或者浏览器的sessionStorage中。更复杂的情况下发的sessionKey本身也是加密的例如用RSA公钥加密。你需要在代码中找到解密这个sessionKey的私钥逻辑通常硬编码在客户端。这时你需要先实现RSA解密得到明文的会话密钥再用它进行SM4解密。5.3 问题三代码混淆严重无法定位关键函数现代前端打包工具如Webpack配合代码混淆Obfuscation会让变量名和函数名变成a、b、c、_0xabc123等形式难以阅读。策略一搜索特征字符串。即使函数名被混淆但字符串常量如”encrypt”、”sm4″、”cbc”通常只是被编码不会被完全消除。在Sources面板全局搜索这些字符串可能会定位到关键代码块附近。策略二Hook关键API。在Console中你可以重写Hook标准的加密相关API来追踪调用。例如// Hook CryptoJS的加密函数如果用了CryptoJS let originalEncrypt CryptoJS.AES.encrypt; CryptoJS.AES.encrypt function(plaintext, key, cfg){ console.log(“[Hook] AES Encrypt called:”, plaintext, key, cfg); debugger; // 自动断点 return originalEncrypt.apply(this, arguments); }对于自定义的SM4函数如果知道其全局函数名即使被混淆也可以尝试类似的方法。策略三基于网络请求的XHR/Fetch断点。在DevTools的Sources面板右侧有一个XHR/Fetch Breakpoints。你可以添加一个包含特定URL片段的断点如api/data。当浏览器发起这个请求时JS执行会暂停此时调用栈Call Stack会显示是哪个函数发起的请求沿着调用栈向上回溯就有可能找到触发加密的函数。5.4 问题四解密出的数据是JSON但含有乱码或结构错误编码问题确保最后一步解码使用了正确的字符集。除了UTF-8也可能是GBK或GB2312。尝试plaintext_bytes.decode(‘gbk’)。数据压缩有些服务端会在加密前先对数据进行压缩如GZIP。解密后得到的是一串二进制数据用文本解码自然是乱码。你需要检查响应头Content-Encoding是否为gzip或deflate。如果是解密后的字节需要先用gzip.decompress()解压。import gzip decrypted_data decrypt_response(…) # 假设这里返回的是bytes if is_gzipped(decrypted_data): # 需要自己判断通常看前两个字节是否为0x1f 0x8b plaintext gzip.decompress(decrypted_data).decode(‘utf-8’)数据签名或附加结构解密出的数据可能不只是业务数据前面可能附加了时间戳、签名等信息。你需要观察解密后字符串的结构可能真正的JSON数据在某个固定偏移量之后。6. 安全、伦理与进阶思考成功逆向并解密了数据很有成就感但到这里我们必须停下来思考一些更重要的问题。安全与法律边界本文所讨论的技术仅适用于学习交流、安全研究在授权范围内、以及对自己拥有完全管理权限的系统进行测试。未经授权对第三方商业系统进行逆向分析、破解加密、爬取数据很可能违反该系统的《用户协议》侵犯其商业秘密并可能触犯《反不正当竞争法》、《网络安全法》乃至《刑法》中的相关条款。技术是一把双刃剑务必用在正道上。从逆向到理解设计逆向工程不仅仅是为了“破解”更深层的价值在于理解系统设计者的思路。为什么选择SM4而不是AES可能是为了满足特定行业的合规要求。为什么采用CBC模式而不是更快的ECB显然是出于对安全性的考量。密钥是如何管理和下发的这反映了系统的会话安全架构。通过逆向我们实际上是在进行一次深度的安全代码审查和学习。防御措施建议给开发者的启示作为开发者如何让接口更“难”被逆向单纯依赖前端加密并不可靠因为前端代码对用户是透明的。更安全的做法是关键逻辑后移将核心的加密、验签、敏感判断逻辑放在服务端前端只做展示和交互。使用动态密钥采用一次一密的会话密钥机制并由服务端通过非对称加密RSA安全下发。增加请求风控对异常频率、异常模式的请求进行识别和拦截如验证码、滑块验证如极验、网易易盾等。代码混淆与加固虽然不能根治但可以显著提高逆向分析的成本。使用标准且安全的HTTPS确保传输层安全防止中间人窃听。TLS本身已经提供了很强的加密和完整性保护业务层加密更多是针对特定场景的补充。技术的持续演进逆向与防护是一场持续的博弈。正如热词中提到的“极验3逆向”、“网易易盾滑块逆向”、“akamai逆向”这些复杂的验证码和Bot防护系统本身也是逆向工程的重要研究对象。攻防双方的技术都在不断升级。保持学习深入理解底层原理密码学、网络协议、浏览器原理、操作系统才能在这场博弈中保持清晰的认识。最后我个人在多次类似“行行查”这样的项目实战中最深的一点体会是耐心和细致远比炫技的自动化工具更重要。一个加密参数可能隐藏在一个极其不起眼的、经过多次赋值的变量里一个模式的判断可能依赖于对两三行晦涩代码的准确理解。往往是最笨的方法——逐行阅读代码、仔细比对每一次网络请求、系统地提出假设并验证——才能最终找到那把正确的“钥匙”。这个过程固然繁琐但每一次成功的解密都是对技术原理一次扎实的巩固这种收获是单纯调用一个现成的解密API所无法比拟的。