1. 项目概述与核心价值最近在做一个财务自动化相关的项目需要批量查验增值税发票的真伪自然而然就盯上了官方的增值税发票查验平台。但凡做过爬虫或者数据对接的朋友都知道这类涉及核心业务的官方平台前端参数加密是家常便饭。直接模拟请求大概率会碰一鼻子灰返回的不是“参数错误”就是“请求非法”。所以逆向分析其前端JavaScript的加密逻辑并用后端语言比如C#还原就成了打通自动化流程的必经之路。这不仅仅是写几行代码调用API那么简单它更像是一场与前端工程师的“智力博弈”你需要从混淆、压缩过的JS代码中梳理出清晰的加密链路和密钥生成逻辑。这个“手把手”的项目就是要带你完整走一遍这个逆向流程。从最基础的浏览器开发者工具抓包开始定位关键加密函数一步步分析其算法很可能是AES、RSA或者自定义的摘要算法理解其密钥管理方式最后用C#将整个加密过程还原出来。最终目标是让你能构造出和浏览器端完全一致的、合法的请求参数成功调用查验接口。这对于需要集成发票查验功能的企业应用、财务软件或者RPA机器人来说是实打实的核心技术点。无论你是C#开发者想解决具体问题还是对Web安全逆向感兴趣这篇文章都能给你一套可复现的方法论和可直接参考的代码。2. 逆向分析前的环境与工具准备工欲善其事必先利其器。逆向分析前端加密我们不需要什么高深莫测的黑客工具用好浏览器和几个插件再加上得心应手的代码分析环境就足够了。2.1 核心工具链搭建首先浏览器是主战场。Chrome或基于Chromium的Edge浏览器是首选因为它们的开发者工具DevTools功能强大且标准。重点关注“网络”Network和“源代码”Sources这两个面板。光有浏览器还不够我们需要一些“辅助瞄准镜”油猴插件Tampermonkey这不是用来写脚本的而是用来加载其他分析脚本的载体。很多针对特定网站的解密、Hook脚本都以油猴脚本的形式发布。浏览器调试插件比如XHR/fetch Breakpoint这类插件可以方便地对特定的URL请求或请求方法如fetch下断点比手动在Sources里找要高效得多。但请注意在正式生产环境或敏感系统中使用任何第三方插件都需谨慎评估安全风险最好在隔离的测试环境中进行。代码格式化工具线上JS代码通常被压缩成一行难以阅读。浏览器Sources面板自带“Pretty Print”功能那个{}图标是第一步。对于更复杂的混淆可能需要结合使用本地工具如prettier或在线JS美化网站。在本地你需要一个顺手的代码编辑器和C#开发环境。Visual Studio 2022或VS Code with C#插件都可以。重点是准备好Newtonsoft.Json或System.Text.Json用于处理JSON以及加解密相关的库我们主要会用到System.Security.Cryptography命名空间下的类。2.2 关键分析思路与抓包定位开始分析前必须明确目标找到生成那个“加密参数”比如叫key9、encryptStr或signature的JavaScript函数并理解它的输入和输出。操作步骤如下清空缓存开启无痕模式避免浏览器缓存或插件干扰用无痕窗口打开增值税发票查验平台。打开DevTools切换到Network面板勾选“Preserve log”保留日志防止页面跳转后请求记录消失。执行一次完整的查验操作在页面输入发票代码、号码、日期、校验码等信息点击查验按钮。筛选和分析请求在Network面板中你会看到一系列请求。重点关注类型为XHR或Fetch的请求其URL通常包含query、check、verify等关键字。点击这个请求查看它的“Headers”和“Payload”。Headers注意Content-Type通常是application/json或application/x-www-form-urlencoded。Payload这里是重点。你会看到提交的表单数据或JSON数据。除了明文的发票信息极有可能存在一个或多个看起来是随机字符串的字段比如key9: aBcDeFgHiJkL...。这个就是我们的目标加密参数。记下这个请求的URL和请求方法POST/GET。注意有些平台的加密参数可能放在请求头Headers里比如Authorization、X-Sign等也需要一并检查。3. 深入JS代码定位与解析加密函数找到加密参数所在的请求后真正的逆向工作才开始——找到生成这个参数的JS代码。3.1 使用断点进行动态调试这是最有效的方法。在Network面板中对刚才那个请求右键选择“Copy” - “Copy as cURL (bash)”可能不太直观更直接的方法是使用“XHR/fetch Breakpoint”功能。在Sources面板添加URL断点打开DevTools的Sources面板在右侧的“XHR/fetch Breakpoints”区域点击“”。你可以选择拦截所有XHR请求但更精准的是添加一个包含特定URL关键词的断点比如包含query的URL。添加后刷新页面并再次点击查验。请求被拦截当浏览器发起匹配的请求时代码执行会自动暂停。此时调用栈Call Stack面板会显示当前暂停点的函数调用链。在调用栈中寻找加密痕迹在Call Stack中从上到下查看各个函数。你需要寻找那些看起来与加密、哈希、签名相关的函数名。常见的“嫌疑犯”包括直接包含encrypt、decrypt、sign、hash、md5、sha、aes、rsa、Crypto等字眼的函数。包含key、secret、iv初始化向量等参数的函数。函数内部调用了btoa、atobBase64、crypto.subtleWeb Crypto API、或者引入了类似CryptoJS库的函数。步入分析点击调用栈中可疑的函数会跳转到对应的JS文件。如果代码是压缩的先点击{}格式化。然后你可以在这个函数内部的关键行比如return语句前或者调用其他加密函数的地方打上普通的行断点然后放开XHR断点让代码继续执行。当程序运行到你打的断点时又会暂停此时你可以观察“Scope”或“Console”中变量的值特别是传入的参数和返回的结果是否与你在Network里看到的加密参数一致。3.2 静态搜索与全局Hook如果动态断点因为代码过于复杂或混淆太强而难以定位可以尝试静态搜索。在Sources面板全局搜索使用CtrlShiftFWindows打开全局搜索。搜索关键词可以是加密参数的名字如key9也可以是加密算法名如AES、RSA或者是像encrypt这样的方法名。注意混淆后的代码可能把函数名改成a、b、c所以也可以搜索一些常量字符串比如加密模式CBC、填充方式PKCS7等。Hook关键函数这是一种高级技巧。在Console面板中你可以重写Hook一些JavaScript原生函数或对象方法来监控它们的调用。例如你可以HookJSON.stringify来看什么数据被序列化了或者Hookcrypto.subtle.encrypt来捕获加密调用。但这需要对JS有较深理解且可能因网站代码的自我保护机制而失效。实操心得在实际分析增值税发票查验平台时我发现其加密函数往往被包裹在一个立即执行函数表达式IIFE中并且局部变量名被严重混淆。这时不要急于去理解每一行代码而是抓住主线找到最终的出口函数。这个函数通常接收一个明文字符串或对象返回一个加密后的字符串。通过断点监控这个函数的输入和输出就能确定它的功能。4. 算法识别与密钥追踪一旦定位到核心加密函数下一步就是弄清楚它用了什么算法以及密钥从哪里来。4.1 常见加密算法特征识别AES对称加密特征代码中可能出现AES、mode如CBC、ECB、padding如PKCS7、PKCS5、iv初始化向量、key密钥长度可能是128/192/256位。在JS中的常见实现使用CryptoJS.AES.encrypt(message, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 })。如果看到CryptoJS这个对象大概率就是它了。RSA非对称加密特征涉及公钥和私钥。前端通常用公钥加密。代码中可能出现RSA、PUBLIC_KEY一串很长的Base64或十六进制字符串、encrypt、setPublicKey等方法。在JS中的常见实现使用JSEncrypt库如new JSEncrypt().setPublicKey(publicKey).encrypt(data)。消息摘要/哈希如MD5, SHA256特征用于生成签名sign。将参数按特定规则拼接后进行哈希计算得到一个固定长度的字符串。代码中可能出现MD5、SHA256、createHash、update、digest‘hex’或‘base64’等。自定义编码/混淆有时并非标准加密而是自定义的字符串变换比如Base64编码后反转、穿插特定字符等。这需要你一步步跟踪代码逻辑。4.2 密钥来源分析密钥的获取方式是逆向的另一个关键点也往往是难点。硬编码在JS中最简单的情况公钥或对称密钥直接以字符串形式写在JS文件里。全局搜索KEY、SECRET、PUBLIC_KEY等可能有收获。由服务器动态返回更常见的情况是在页面加载时或首次请求时服务器会返回一个密钥或用于生成密钥的“种子”seed、nonce。你需要检查在查验请求之前是否有其他初始化请求比如获取一个token或key其响应体中包含了加密所需的信息。由前端固定算法生成密钥可能由前端根据时间戳、用户代理User-Agent或其他固定参数通过某种算法计算得出。这需要你逆向这个生成算法。注意事项在分析密钥时务必注意其格式和编码。它可能是Base64字符串、十六进制字符串或者是一个JSON对象。在C#中还原时必须确保以完全相同的格式和编码方式使用它。5. C#代码还原实战以AES-CBC-PKCS7为例假设我们通过逆向分析确定增值税发票查验平台的加密方式为AES-128-CBC填充模式为PKCS7密钥和IV均从服务器动态获取明文数据是JSON字符串加密结果进行Base64编码后作为key9参数提交。下面我们一步步用C#还原这个过程。5.1 项目搭建与依赖首先创建一个C#控制台应用或类库项目。我们需要使用System.Security.Cryptography命名空间。对于JSON处理可以使用.NET Core/5内置的System.Text.Json或者经典的Newtonsoft.Json。using System; using System.Security.Cryptography; using System.Text; using System.Text.Json; // 使用 System.Text.Json // using Newtonsoft.Json; // 或者使用 Newtonsoft.Json5.2 模拟密钥获取在真实场景中你需要先模拟一个HTTP请求去获取服务器下发的密钥和IV。这里我们假设你已经通过抓包拿到了它们并以Base64字符串形式存在。public class EncryptService { // 假设从服务器获取的密钥和IV (Base64格式) private readonly string _base64Key 你的128位密钥Base64字符串; // 16字节对应AES-128 private readonly string _base64Iv 你的初始化向量Base64字符串; // 16字节 private byte[] _keyBytes; private byte[] _ivBytes; public EncryptService() { _keyBytes Convert.FromBase64String(_base64Key); _ivBytes Convert.FromBase64String(_base64Iv); // 简单验证长度 if (_keyBytes.Length ! 16) throw new ArgumentException(密钥长度必须为16字节(AES-128)); if (_ivBytes.Length ! 16) throw new ArgumentException(IV长度必须为16字节); } }5.3 核心加密方法实现C#的Aes类默认使用PKCS7填充在.NET中叫PKCS7但等同于PKCS5这正好匹配我们的需求。public string EncryptRequestData(object requestData) { // 1. 将请求对象序列化为JSON字符串 string jsonString JsonSerializer.Serialize(requestData); // 如果使用Newtonsoft.Json: string jsonString JsonConvert.SerializeObject(requestData); Console.WriteLine($明文JSON: {jsonString}); // 2. 将明文字符串转换为字节数组 byte[] plainBytes Encoding.UTF8.GetBytes(jsonString); // 3. 使用AES-CBC进行加密 byte[] encryptedBytes; using (Aes aesAlg Aes.Create()) { aesAlg.Key _keyBytes; aesAlg.IV _ivBytes; aesAlg.Mode CipherMode.CBC; // aesAlg.Padding PaddingMode.PKCS7; // 默认就是PKCS7可省略 // 创建加密器 ICryptoTransform encryptor aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); // 执行加密 using (MemoryStream msEncrypt new MemoryStream()) { using (CryptoStream csEncrypt new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) { csEncrypt.Write(plainBytes, 0, plainBytes.Length); csEncrypt.FlushFinalBlock(); // 确保所有数据写入 encryptedBytes msEncrypt.ToArray(); } } } // 4. 将加密后的字节数组转换为Base64字符串 string base64Encrypted Convert.ToBase64String(encryptedBytes); Console.WriteLine($加密后Base64: {base64Encrypted}); return base64Encrypted; }5.4 构造完整请求示例现在我们模拟一个完整的发票查验请求。public class InvoiceQueryRequest { public string fpdm { get; set; } // 发票代码 public string fphm { get; set; } // 发票号码 public string kprq { get; set; } // 开票日期 public string kjje { get; set; } // 开票金额 // ... 可能还有其他字段根据实际抓包确定 } class Program { static void Main(string[] args) { var encryptor new EncryptService(); // 构造请求数据对象 var request new InvoiceQueryRequest { fpdm 1234567890, fphm 12345678, kprq 20230501, kjje 666.66 }; // 加密得到 key9 参数 string key9 encryptor.EncryptRequestData(request); // 模拟构造最终POST请求的Payload var finalPayload new { // 其他可能的明文参数... key9 key9 }; string finalJson JsonSerializer.Serialize(finalPayload); Console.WriteLine($最终提交的JSON: {finalJson}); // 接下来你可以使用 HttpClient 将 finalJson 发送到查验接口 // SendHttpRequestAsync(finalJson).Wait(); } }6. 逆向与还原过程中的常见问题与排查在实际操作中你几乎一定会遇到下面这些问题。这里记录了我的踩坑实录和解决方案。6.1 加密结果与浏览器不一致这是最令人头疼的问题。明明算法和密钥都一样为什么C#加密出来的Base64字符串和浏览器生成的不一样排查清单字符编码确保明文字符串在JS和C#中使用的编码一致。99%的问题出在这里JS通常使用UTF-16或UTF-8但在进行加密前CryptoJS默认会将字符串当作“字面量”处理而C#的Encoding.UTF8.GetBytes()是明确的UTF-8。你需要确认JS端加密函数的输入到底是什么。验证方法在JS加密函数入口打断点查看传入的参数类型和值。如果传入的是一个对象看它是先被JSON.stringify了还是直接调用了toString()。CryptoJS.enc.Utf8.parse()是常见的将字符串转为UTF-8字节块的方法如果JS中用了这个那C#端就必须用Encoding.UTF8.GetBytes()。密钥和IV的格式确认密钥和IV在JS和C#中是完全相同的字节序列。JS中CryptoJS.enc.Base64.parse()解析出来的和C#中Convert.FromBase64String()解析出来的应该一致。可以分别将解析后的字节数组转为十六进制字符串对比。填充模式确认填充模式。AES的CBC模式必须填充。PKCS7和PKCS5在AES的上下文中是等价的。但有些JS实现可能用了ZeroPadding或其他填充。C#中通过aesAlg.Padding属性设置。加密模式确认是CBC、ECB还是其他模式。C#中通过aesAlg.Mode设置。输出格式JS的CryptoJS.AES.encrypt默认返回一个CipherParams对象你需要调用.toString()或.ciphertext.toString(CryptoJS.enc.Base64)来获取Base64字符串。确保你获取的是最终用于传输的字符串而不是中间对象。调试技巧在C#中将每一步的中间结果如明文字节数组的Hex、加密后的字节数组的Hex打印出来。同时在JS调试中也打印出对应的中间结果。进行逐字节对比差异点就是问题所在。6.2 密钥动态变化无法硬编码如果密钥每次都会变你需要先模拟获取密钥的请求。分析密钥接口在Network中找到返回密钥或seed的那个请求。分析它的请求参数可能包含时间戳、固定标识等和响应格式。用C#模拟该请求使用HttpClient库完全模拟浏览器的请求包括必要的Headers如User-Agent,Referer,Cookie等获取密钥。集成到加密流程在你的EncryptService构造函数或某个初始化方法中先调用获取密钥的方法然后再进行加密。public async Task InitializeAsync() { using (var httpClient new HttpClient()) { // 添加必要的请求头模拟浏览器 httpClient.DefaultRequestHeaders.Add(User-Agent, 你的浏览器User-Agent); // 可能还需要Cookie需要先处理登录或会话保持 var response await httpClient.GetAsync(https://发票平台/获取密钥的路径); response.EnsureSuccessStatusCode(); var jsonResponse await response.Content.ReadAsStringAsync(); // 解析JSON提取key和iv var keyData JsonSerializer.DeserializeKeyResponse(jsonResponse); _keyBytes Convert.FromBase64String(keyData.EncryptKey); _ivBytes Convert.FromBase64String(keyData.EncryptIv); } }6.3 JS代码混淆严重难以阅读面对混淆的代码不要试图去“反混淆”或理解所有变量名。抓大放小只关注核心逻辑流。找到加密函数的入口和出口。利用断点观察在疑似入口函数打上断点观察传入的参数是什么是不是一个JSON对象。在出口处return语句打上断点观察返回的是什么。搜索特征常量搜索字符串常量如加密模式CBC、填充模式Pkcs7、或者明显的Base64字符集ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/这些地方往往是配置或关键操作点。尝试Hook通用函数如果代码使用了CryptoJS或JSEncrypt尝试在Console中重写这些库的入口函数打印出调用参数和结果这能帮你快速定位。6.4 请求除了加密参数还有签名Sign很多平台采用“加密签名”的双重验证。加密保护数据内容签名验证请求完整性和来源。识别签名参数在请求Payload或Headers中寻找如sign、signature的参数。分析签名算法签名通常是将所有请求参数包括加密后的key9按特定规则如字典序排序拼接成一个字符串然后加上一个secret密钥再进行MD5或SHA256哈希。这个secret可能和加密密钥不同且可能更隐蔽。分别还原你需要先还原加密流程得到key9再还原签名算法得到sign。在C#中需要先完成加密然后用结果参与签名计算。7. 安全、合规与伦理考量在兴奋地跑通代码之前我们必须严肃地讨论这个问题。逆向分析第三方平台尤其是涉及税务、金融等敏感领域的官方平台存在明确的法律和道德风险。尊重知识产权与服务条款平台的前端代码是其知识产权的一部分。擅自逆向、破解用于商业用途或干扰其正常服务可能违反其用户协议甚至触犯相关法律法规。明确使用目的本文所述技术仅用于学习交流、安全研究以及在获得明确授权的前提下为企业内部系统与官方平台进行合规的技术集成。绝对禁止用于制作恶意爬虫、刷票、攻击服务器、窃取数据或任何干扰平台正常运行的行为。频率限制与友好访问即使技术上行得通在自动化调用时也必须严格遵守平台的访问频率限制Rate Limiting。过高的请求频率会被视为攻击导致IP被封禁也可能对公共服务资源造成不必要的压力。建议添加合理的延时模拟人工操作间隔。数据隐私处理发票信息等数据时务必遵守《数据安全法》和《个人信息保护法》等相关规定确保数据在传输、存储和处理过程中的安全不泄露、不滥用。个人建议对于企业级的、稳定的发票查验需求首选官方提供的企业API接口。这些接口通常需要申请资质、签订协议但提供了合法、稳定、受支持的数据通道是唯一推荐的生产环境解决方案。本文的逆向分析方法更适用于理解技术原理、进行学术研究或在官方API不可用、不满足特定临时需求时的技术储备。在决定使用前请务必进行全面的法律风险评估。技术是把双刃剑用对地方才能创造价值。