国密算法实战:从滑块验证到登录全流程的SM2/SM4/HMacSHA256逆向解析

📅 发布时间:2026/7/5 16:55:14 👁️ 浏览次数:
国密算法实战:从滑块验证到登录全流程的SM2/SM4/HMacSHA256逆向解析
1. 开篇当滑块遇上国密一次完整的登录逆向之旅大家好我是老张在爬虫和安全研究这个圈子里摸爬滚打了十来年见过各种各样的验证和加密。最近几年国密算法SM2、SM3、SM4等在政务、金融这些对安全要求极高的场景里用得越来越普遍很多朋友一看到这个就头疼觉得比常见的AES、RSA要复杂神秘得多。今天我就拿一个非常典型的、集成了滑块验证和全套国密算法的Web登录系统作为实战案例带大家从头到尾“拆解”一遍。咱们的目标很明确不光要弄明白它每一步在干什么还要亲手写出能自动化完成登录的脚本。这个过程就像侦探破案一步步追踪线索最终还原出完整的作案手法。你会发现只要理清了逻辑国密算法实战并没有想象中那么难。这个案例来源于一个真实的政务服务网站它的登录流程堪称“教科书级”的防御先来个滑块验证码拦住机器接着用SM2非对称加密协商密钥然后用SM4对称加密保护你的账号密码全程再用HMacSHA256做消息认证确保数据没被篡改。听起来是不是层层设卡别慌咱们一层层剥开它的外壳。我会用最直白的话把SM2、SM4、HMacSHA256在这些环节里扮演的角色讲清楚并且给出每一步可操作的代码。无论你是想学习国密算法逆向的安全爱好者还是需要搞定这类登录的爬虫工程师这篇内容都能给你一条清晰的路径。2. 初探战场网络请求中的五个关键“哨卡”动手逆向之前咱们得先看看对手的布防。打开浏览器无痕窗口访问目标登录页随便输入账号密码点击登录然后手动完成那个滑块验证。这时候打开开发者工具的Network面板你会发现页面在后台默默地发起了一系列请求。别被数量吓到咱们重点关注其中五个它们就是登录流程的核心“哨卡”。第一个哨卡getPublicKey获取公钥。这个请求通常是最先发起的。它的载荷里你会看到一个叫signature的参数是加密的但其他部分可能是明文。查看它的响应核心是返回一个uuid和一个publicKey。这个uuid就像本次会话的身份证号后续所有请求都会带着它而publicKey就是SM2算法的公钥用于后续的加密通信初始化。所以这个接口的使命就是“握手打招呼”为后续的加密对话准备好基础材料。第二个哨卡sendSm4发送SM4密钥信息。紧接着第二个请求就来了。它的载荷里signature依然加密同时多了一个datagram参数里面包含了刚才拿到的uuid以及一个加密生成的secret。此外请求头X-TEMP-INFO也被设置成了那个uuid。这个secret是干嘛的呢它是后续SM4加密要用到的关键密钥信息但它是被服务器的公钥publicKey加密过的只有服务器用自己的私钥才能解开。这一步完成了密钥的安全传递。第三个哨卡getCaptcha获取验证码。轮到滑块验证码登场了。这个请求的datagram和signature也都是加密的。响应里会返回滑块拼图和背景图的两张Base64图片数据以及一个本次验证码的uuid。我们的脚本需要识别出滑块的滑动距离。第四个哨卡verifyCaptcha验证滑块。识别出滑动距离后我们将这个距离值和图片的uuid作为参数发起验证请求。同样参数被加密。如果滑动正确服务器会返回一个加密的datagram解密后里面包含一个宝贵的ticket票据。这个ticket是登录许可的关键凭证。第五个哨卡accountLogin账号登录。这是最后一步。我们将账号、密码、以及之前获得的ticket等信息通过加密的方式提交。服务器验证通过后登录才算真正成功。理清这五个请求的顺序和依赖关系至关重要。它们环环相扣getPublicKey是起点产生的uuid和publicKey贯穿始终sendSm4传递了加密的会话密钥getCaptcha和verifyCaptcha处理了人机验证最后accountLogin完成身份认证。任何一个环节出错流程都会中断。3. 逆向核心定位加密函数与参数构造逻辑知道了流程接下来就是最关键的环节——找到加密是怎么发生的。我们回到浏览器打开开发者工具的Sources面板或者直接全局搜索关键参数名比如signature、datagram、encryptCode。通常前端会有一个统一的请求拦截器或封装函数来处理加密。搜索signature你可能会找到类似下面的代码逻辑if (post n.method) { if (S.includes(n.url)) { o[encryptCode] 0; o.datagram JSON.stringify(n.data); } else { c JSON.stringify(n.data); u Object(P[d])(c, Object(P[i])(l)); o.datagram u; o[encryptCode] 2; } // ... 计算 signature ... }这段代码透露了两个重要信息第一有一个白名单列表S里面的接口请求比如getPublicKey和sendSm4是不加密的encryptCode为0datagram直接是数据的JSON字符串。第二非白名单的请求如登录、验证码datagram会经过Object(P[d])和Object(P[i])这两个函数处理且encryptCode变为2。这基本就锁定了加密发生的位置。接下来我们逐个分析关键参数的生成signature签名它的作用是防止数据被篡改。通过跟栈或搜索HMacSHA256你会发现它的生成方式类似这样signature HmacSHA256(zipCode encryptCode u timestamp signtype, g)其中zipCode、signtype通常是固定值encryptCode就是上面的0或2u是datagram的值未加密时为空字符串timestamp是当前时间戳而g是一个随机的字符串密钥。这个g非常重要它在getPublicKey请求时随机生成并在后续请求中一直使用。HMacSHA256保证了只要原始数据或密钥g有任何变动签名都会不同服务器借此验证请求的完整性。datagram数据报文对于需要加密的请求它是这样来的将请求数据如{“account”: “xxx”, “password”: “yyy”}序列化成JSON字符串c。使用一个密钥l进行SM4加密。这个密钥l又是怎么来的呢它等于随机字符串g的前8位再拼接上一个固定的字符串O。即l g.substring(0, 8) O。加密前密钥l会被转换成16进制格式Object(P[i])函数的作用。最终datagram SM4_Encrypt(c, hex(l))。secret秘密密钥在sendSm4请求中这个参数很特殊。它是将随机字符串g使用getPublicKey返回的publicKeySM2公钥通过SM2算法加密得到的。即secret SM2_Encrypt(g, publicKey)。这样只有拥有对应私钥的服务器才能解出原始的g从而确保了后续SM4加密密钥l的安全分发。4. 算法拆解SM2、SM4、HMacSHA256各司其职现在我们来深入看看这三位“国密明星”在流程中具体扮演什么角色。理解了它们的职责整个加密体系就清晰了。SM2非对称加密安全传递“信使”。SM2相当于我们熟悉的RSA用于密钥交换。在这个案例里它只出现在sendSm4这一步。客户端生成了一个随机的“信使”——字符串g这个g是后续所有对称加密和签名的根基。为了安全地把g告诉服务器客户端用服务器公布的公钥publicKey对g进行SM2加密生成secret。网络上传送的是secret即使被截获没有私钥也无法解密。服务器收到后用自己的私钥解密得到g。至此双方安全地共享了同一个秘密g。SM4对称加密保护“对话内容”。SM4相当于AES速度快适合加密大量数据。拿到共同的g后双方可以推导出SM4的加密密钥ll g前8位 固定值O。之后所有敏感的业务数据比如滑块移动距离、账号、密码在传输前都会用这个密钥l进行SM4加密生成datagram。服务器收到后用同样的密钥l解密即可。因为加密解密用的是同一个密钥所以叫对称加密。HMacSHA256消息认证确保“消息未被掉包”。这相当于一个防伪标签。在每次请求中客户端会把一些关键信息如zipCode、encryptCode、datagram、timestamp等拼接起来然后用共享的秘密g作为密钥计算一个HMacSHA256的哈希值这就是signature。服务器收到请求后会用同样的算法和密钥g再计算一次签名如果和自己计算的结果一致就证明数据在传输过程中没有被篡改。它不负责加密内容只负责验证完整性。我们可以用一个简单的表格来总结它们的分工算法类型在本流程中的作用出现环节SM2非对称加密加密传输后续加密所用的核心随机数gsendSm4请求中的secret生成SM4对称加密加密具体的业务数据滑块距离、账号密码等getCaptcha、verifyCaptcha、accountLogin请求的datagramHMacSHA256消息认证码生成签名验证请求数据的完整性所有请求的signature参数5. 实战还原从零构建自动化登录脚本理论分析透了咱们就动手把代码写出来。我会用Python作为主要语言配合execjs来调用还原出的JavaScript加密函数。5.1 环境准备与依赖安装首先确保你的Python环境已经就绪。我们需要安装几个关键的库pip install requests pyexecjs loguru ddddocrrequests用于发送HTTP请求。pyexecjs一个执行JavaScript代码的库因为加密逻辑通常在前端JS里我们直接调用它还原的函数最方便。loguru一个非常好用的日志库方便我们调试时查看输出。ddddocr一个强大的开源验证码识别库这里我们用它来识别滑块缺口距离。另外国密算法需要对应的JavaScript实现。我们可以使用sm-crypto这个库。在Node.js环境下可以用npm安装但在Python中我们通常把相关的JS代码提取到一个文件中供execjs调用。你需要准备一个包含了SM2、SM4、HMacSHA256等函数实现的JS文件比如sm_utils.js。5.2 关键JavaScript函数还原根据之前的逆向分析我们需要在JS环境里实现几个核心函数。下面是我还原后的关键部分你可以把它们保存到一个文件比如tax_login.js中const CryptoJS require(crypto-js); // 用于HMacSHA256 const smcrypto require(sm-crypto); // 用于SM2, SM4 // 固定字符串O用于合成SM4密钥 var O dt!P^bfrR6LhHTUutGk3GwdKdewgdewgjfekqwgfgfjg.substring(0, 4) fwejkfjqfgjgfhewvfvq^R4qd^VLrf^2^ujqgM2Rpb9t.substring(fwejkfjqfgjgfhewvfvq^R4qd^VLrf^2^ujqgM2Rpb9t.length - 4); // 不加密的白名单接口列表 var S [/auth/oauth2/getPublicKey, /auth/white/sendSm4, /auth/user/logout, /auth/user/checklogin, /auth/qrcode/verifyQRCode, /auth/message/sendSmsCode, /auth/oauth2/revokeToken, /auth/white/getAreCode, /auth/white/getSecondAuthInfo, /auth/oauth2/checkRedirectUrl, /auth/message/captchaImage]; // 生成随机字符串g function generateRandomG() { var chars 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.split(); var len 16; var result []; for (var i 0; i len; i) { result[i] chars[Math.floor(Math.random() * chars.length)]; } return result.join(); } // HMacSHA256签名函数 function p_a(message, key) { return CryptoJS.HmacSHA256(message, key).toString(); } // 字符串转16进制用于SM4密钥处理 function p_i(str) { var hex ; for (var i 0; i str.length; i) { hex str.charCodeAt(i).toString(16); } return hex; } // SM4加密 function p_d(data, keyHex) { return smcrypto.sm4.encrypt(data, keyHex); } // SM4解密 function sm4_decrypt(cipherText, randomG) { var keyPart randomG.substring(0, 8) O; var keyHex p_i(keyPart); return smcrypto.sm4.decrypt(cipherText, keyHex); } // SM2加密用于生成secret function sm2_encrypt(data, publicKey) { return smcrypto.sm2.doEncrypt(data, publicKey, 1); // 1 代表使用C1C3C2模式 } // 核心参数构造函数 function get_params(requestData, randomG, needEncrypt) { var params {}; var u ; var g randomG || generateRandomG(); // 如果没有传入g则生成一个新的 var l g.substring(0, 8) O; // 合成SM4密钥基础字符串 params[zipCode] 0; params[timestamp] new Date().format(yyyyMMddHHmmss); // 需要提前补全Date的format方法 params[access_token] ; params[signtype] HMacSHA256; if (!needEncrypt) { // 白名单接口不加密 params[encryptCode] 0; params.datagram JSON.stringify(requestData); } else { // 非白名单接口需要SM4加密 params[encryptCode] 2; var dataStr JSON.stringify(requestData); u p_d(dataStr, p_i(l)); // 加密后的datagram params.datagram u; } // 计算签名注意未加密时u为空字符串 params.signature p_a(params[zipCode] params[encryptCode] u params[timestamp] params[signtype], g); return {o: params, g: g}; // 返回参数和随机数g } // 生成sendSm4请求需要的secret参数 function get_secret(uuid, randomG, publicKey) { var encryptedG sm2_encrypt(randomG, publicKey); return { uuid: uuid, secret: encryptedG }; }注意上面的代码中Date.prototype.format方法需要你根据原始代码补全或者用其他方式生成yyyyMMddHHmmss格式的时间戳。5.3 Python主流程脚本编写有了JS函数我们就可以用Python来串联整个流程了。代码逻辑完全遵循我们分析的五个请求步骤。import base64 import json import execjs import requests from loguru import logger import ddddocr # 初始化会话和JS环境 session requests.Session() with open(tax_login.js, r, encodingutf-8) as f: js_env execjs.compile(f.read()) # 滑块识别函数使用ddddocr def slide_ocr(target_b64, background_b64): target_b64: 滑块小图的base64字符串带data:image前缀 background_b64: 背景大图的base64字符串带data:image前缀 # 去掉Base64数据头只取数据部分 target_data base64.b64decode(target_b64.split(,)[-1]) background_data base64.b64decode(background_b64.split(,)[-1]) det ddddocr.DdddOcr(detFalse, show_adFalse, ocrFalse) # slide_match 返回一个字典包含目标位置的x坐标等信息 result det.slide_match(target_data, background_data) return result # 1. 初始化请求头根据实际情况调整 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Content-Type: application/json, Origin: https://tpass.jiangsu.chinatax.gov.cn:8443, Referer: https://tpass.jiangsu.chinatax.gov.cn:8443/, # 其他必要的固定头部... } # 2. 第一步获取公钥和UUID logger.info(Step 1: 请求getPublicKey...) get_public_key_url https://tpass.jiangsu.chinatax.gov.cn:8443/sys-api/v1.0/auth/oauth2/getPublicKey # 此请求在白名单S中needEncryptFalse params_obj js_env.call(get_params, {}, None, False) random_g params_obj[g] # 获取生成的随机数g request_data json.dumps(params_obj[o], separators(,, :)) resp session.post(get_public_key_url, headersheaders, datarequest_data) resp_json resp.json() uuid json.loads(resp_json[datagram])[uuid] public_key json.loads(resp_json[datagram])[publicKey] headers[X-TEMP-INFO] uuid # 重要更新请求头 logger.success(f获取到 UUID: {uuid}, PublicKey: {public_key[:50]}..., Random G: {random_g}) # 3. 第二步发送SM2加密后的密钥信息 logger.info(Step 2: 请求sendSm4...) send_sm4_url https://tpass.jiangsu.chinatax.gov.cn:8443/sys-api/v1.0/auth/white/sendSm4 # 构造secret参数 secret_data js_env.call(get_secret, uuid, random_g, public_key) # 此请求也在白名单中不加密 params_obj2 js_env.call(get_params, secret_data, random_g, False) request_data2 json.dumps(params_obj2[o], separators(,, :)) resp2 session.post(send_sm4_url, headersheaders, datarequest_data2) logger.info(fsendSm4响应: {resp2.text}) # 4. 第三步获取滑块验证码图片 logger.info(Step 3: 请求getCaptcha...) get_captcha_url https://tpass.jiangsu.chinatax.gov.cn:8443/sys-api/v1.0/auth/captcha/getCaptcha captcha_req_data { client_id: mcsc7e2ssscb4sfmbsmas35sass2753b, redirect_uri: https://etax.jiangsu.chinatax.gov.cn:8443/mhzx/api/mh/tpass/code } # 此请求需要加密needEncryptTrue params_obj3 js_env.call(get_params, captcha_req_data, random_g, True) request_data3 json.dumps(params_obj3[o], separators(,, :)) resp3 session.post(get_captcha_url, headersheaders, datarequest_data3) resp3_json resp3.json() img_data json.loads(resp3_json[datagram]) # 这里返回的datagram需要SM4解密 # 注意实际场景中getCaptcha返回的datagram可能是加密的需要先解密。 # 根据逆向此处datagram是加密的我们需要用相同的密钥解密。 decrypted_img_data js_env.call(sm4_decrypt, resp3_json[datagram], random_g) img_info json.loads(decrypted_img_data) block_src img_info[blockSrc] # 滑块小图 canvas_src img_info[canvasSrc] # 背景大图 captcha_uuid img_info[uuid] logger.success(f获取到验证码图片UUID: {captcha_uuid}) # 5. 第四步识别滑块并验证 logger.info(Step 4: 识别滑块并验证...) slide_result slide_ocr(block_src, canvas_src) slide_distance slide_result[target][0] # 获取缺口x坐标 logger.success(f识别滑块距离: {slide_distance}) verify_url https://tpass.jiangsu.chinatax.gov.cn:8443/sys-api/v1.0/auth/captcha/verifyCaptcha verify_req_data { blockX: slide_distance, uuid: captcha_uuid } params_obj4 js_env.call(get_params, verify_req_data, random_g, True) request_data4 json.dumps(params_obj4[o], separators(,, :)) resp4 session.post(verify_url, headersheaders, datarequest_data4) resp4_json resp4.json() # 验证返回的datagram也是加密的需要解密 decrypted_ticket_data js_env.call(sm4_decrypt, resp4_json[datagram], random_g) ticket_info json.loads(decrypted_ticket_data) ticket ticket_info[ticket] headers[X-TICKET-ID] ticket # 重要将ticket放入请求头 logger.success(f滑块验证成功获取到Ticket: {ticket}) # 6. 第五步携带Ticket进行账号登录 logger.info(Step 5: 请求账号登录...) login_url https://tpass.jiangsu.chinatax.gov.cn:8443/sys-api/v1.0/auth/user/first/accountLogin login_req_data { client_id: mcsc7e2ssscb4sfmbsmas35sass2753b, redirect_uri: https://etax.jiangsu.chinatax.gov.cn:8443/mhzx/api/mh/tpass/code, account: 你的账号, password: 你的密码 } params_obj5 js_env.call(get_params, login_req_data, random_g, True) request_data5 json.dumps(params_obj5[o], separators(,, :)) resp5 session.post(login_url, headersheaders, datarequest_data5) logger.success(f登录请求完成响应: {resp5.text}) # 检查resp5.json()中的状态码或消息判断登录是否成功5.4 调试技巧与常见问题写脚本的过程很少一帆风顺这里分享几个我踩过的坑和调试技巧。首先时间戳格式必须完全一致yyyyMMddHHmmss这个格式要补全到JavaScript的Date对象上或者用Python生成后传入JS确保和服务端校验的一致。其次随机数g的生命周期它必须在getPublicKey步骤生成并在后续所有步骤中保持不变一旦中途重新生成签名对不上整个流程就断了。关于滑块识别ddddocr的slide_match方法返回的是一个包含目标位置信息的字典通常我们取result[‘target’][0]作为滑块的X轴偏移距离。但不同网站的滑块可能有不同的特性比如需要一定的偏移量补偿或者验证轨迹。如果识别通过率低可以尝试对图片进行一些预处理比如灰度化、二值化或者多识别几次取平均值。最头疼的可能是环境依赖问题。execjs在调用复杂的JS库时如果Node.js环境没配好或者库版本不对可能会报错。一个稳妥的办法是把关键的加密函数用Python重写。比如可以用gmssl这个Python库来实现国密算法。这样虽然前期工作量稍大但避免了JS环境的不稳定性。例如用gmssl实现SM4加密from gmssl import sm4 def sm4_encrypt(data: str, key: str): data: 待加密字符串 key: 16字节的密钥字符串 cryptor sm4.CryptSM4() cryptor.set_key(key.encode(), sm4.SM4_ENCRYPT) encrypt_data cryptor.crypt_ecb(data.encode()) # ECB模式根据实际情况选模式 return encrypt_data.hex() # 返回16进制字符串6. 总结与安全思考走完这一整套流程你会发现看似复杂的国密算法登录体系其核心思想依然是经典的密码学应用非对称加密安全交换密钥对称加密保护数据消息认证码验证完整性。只是算法从国际通用的RSA/AES/HMAC-SHA256换成了国密的SM2/SM4/HMacSHA256。逆向的关键在于耐心梳理网络请求的顺序和依赖定位前端的加密函数并理清各个参数尤其是随机数g、密钥l、签名signature的生成和传递链条。在实战中我建议一定要善用浏览器的开发者工具。除了Network看请求Sources里打断点跟栈是定位加密代码最有效的方法。可以先搜索encrypt、sign、SM4、Hmac等关键词缩小范围。对于加密函数不必一开始就追求完全理解每一行先搞清楚它的输入输出是什么关键参数从哪里来往往就能模拟出调用过程。最后从安全研究的角度看这种设计确实大大增加了自动化脚本的难度。但作为开发者我们也应该认识到任何前端实施的加密在原理上都是可以被分析和模拟的真正的安全不能完全依赖前端混淆和加密。对于企业而言后端的风控策略、行为分析、接口限流等手段同样重要。而对于我们学习者和研究者来说理解这些机制不仅能解决具体的爬虫或自动化需求更能加深对现代Web安全体系的认识知道攻击面在哪里才能更好地进行防御。希望这个详细的拆解过程能帮你打开国密算法逆向的大门下次遇到类似的系统你也能从容应对。