☰
某广告推广平台 API 签名算法逆向分析还原
2026/9/26 4:08:49 网站建设 项目流程

阅读须知

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号联系作者立即删除


分析概览

|
参数
|
所在位置
|
算法
|
密钥/参数来源
|
| — | — | — | — |
|
‎username
|
登录请求 Body
|
AES-256-CBC
|
Key/IV 硬编码在 JS bundle
|
|
‎password
|
登录请求 Body
|
RSA-1024 PKCS[#1](javascript:😉 v1.5
|
公钥硬编码在 JS bundle
|
|
‎iamData
|
每个请求 Header
|
RSA-1024 PKCS[#1](javascript:😉 v1.5
|
明文 = ‎dspAccountId,dspRoleId(来自登录响应)
|
|
‎Extra-Data
|
每个请求 Header
|
RSA-1024 PKCS[#1](javascript:😉 v1.5
|
明文 = ‎{}或 ‎{providerId, adAccountId}
|


JS 源码溯源

分析从抓包入手,核心代码全部位于 Vite 打包产物:

https://xxx.com/assets/index-Cnrq_27l.js ← 主 bundle,670 KB https://xxx.com/assets/index-Dzn80_QX.js ← 登录页组件,10 KB

模块别名追踪链(Vite tree-shaking 后函数名被混淆):

|
原始函数
|
主 bundle 导出别名
|
登录组件导入别名
|
作用
|
| — | — | — | — |
|
‎MH
|
‎ai
|
‎we
|
username AES 加密
|
|
‎tv
|
‎ah
|
‎he
|
RSA 加密(password / header)
|
|
‎bH
|
‎aj
|
‎ye
|
登录 API 调用
|

登录组件核心调用(index-Dzn80_QX.js):

const h={username: we(String(n.username).trim().toLowerCase()), // MH()password: he(n.password)// tv()};const t=await ye(h);// POST /account/login

请求拦截器(index-Cnrq_27l.js,每次请求自动注入):

gt.interceptors.request.use(async e=>{const t=da();// Pinia store const{accessToken: n, dspAccountId: r, dspRoleId: o}=t.userInfo||{};if(r&&o)e.headers.iamData=tv(`${r},${o}`);e.headers["UBiX-Auth"]="Bearer "+ n;const{adAccountId: s, providerId: a}=t.operatedAccountInfo||{};leti={};if(s&&a)i={providerId: a, adAccountId: s};e.headers["Extra-Data"]=tv(JSON.stringify(i));returne;});

加密算法还原

1. username — AES-256-CBC

JS 源码(函数名MH,使用 CryptoJS):

functionMH(e){const t=mn.enc.Utf8.parse("9h1ho2lhu0gr67fpe56fi09n9ned6g6h");// key(32bytes)const n=mn.enc.Utf8.parse("com.ubix.platfor");// iv(16bytes)const r=mn.enc.Utf8.parse(e);const o=mn.AES.encrypt(r, t,{iv: n, mode: mn.mode.CBC, padding: mn.pad.Pkcs7});returnmn.enc.Base64.stringify(o.ciphertext);}

算法参数:

|
项
|
值
|
| — | — |
|
算法
|
AES-256-CBC
|
|
Key
|
‎9h1ho2lhu0gr67fpe56fi09n9ned6g6h(32 字节 UTF-8 字符串)
|
|
IV
|
‎com.ubix.platfor(16 字节 UTF-8 字符串)
|
|
Padding
|
PKCS7
|
|
输入预处理
|
‎trim().toLowerCase()
|
|
输出格式
|
标准 Base64
|

验证(输入dddddddd@qq.com):

PHP 输出: Po67jmVvqLVmekwpghfGDpm/HXTLl6KBQ9e8UfnSiQA=抓包实际: Po67jmVvqLVmekwpghfGDpm/HXTLl6KBQ9e8UfnSiQA=匹配: ✅ 完全一致

PHP 实现:

define('UBIX_AES_KEY','9h1ho2lhu0gr67fpe56fi09n9ned6g6h');define('UBIX_AES_IV','com.ubix.platfor');functionubix_encrypt_username(string$username): string{$username=strtolower(trim($username));$encrypted=openssl_encrypt($username,'aes-256-cbc', UBIX_AES_KEY, OPENSSL_RAW_DATA, UBIX_AES_IV);returnbase64_encode($encrypted);}

2. password / Extra-Data / iamData — RSA-1024 PKCS[#1](javascript:😉 v1.5

三者共用同一个加密函数tv(),区别仅在于明文内容。

JS 源码(tv→fN):

functiontv(e){returnfN(e,"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDV-h87Pz_xcHOG...");}functionfN(e, t){const n=new JSEncrypt();// JSEncrypt 库(PKCS#1 v1.5)n.setPublicKey(t);// 公钥为 URL-safe Base64 编码的 SPKI const r=n.getKey();// 超过117UTF-8 字节时分段加密 // (每段 ≤117字节,分别 RSA 加密,hex 拼接后统一编码) const l=r.encrypt(e);// 返回 hex 字符串returnQ2(l);// 自定义 hex → base64(见下节)}

RSA 公钥(JS 中为 URL-safe Base64,PHP 中需转换为标准 PEM):

JS bundle 原始值(URL-safe Base64,- 代替 +,_ 代替 /): MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDV-h87Pz_xcHOGIlu6bizg... **算法参数**:|项|值||----|-----||算法|RSA-1024,PKCS#1 v1.5 填充 ||公钥格式|SPKI(SubjectPublicKeyInfo),DER 编码,Base64 包装||最大单次明文|117字节(128 字节模数 −11字节 padding overhead)||超长分段策略|每段 ≤117UTF-8 字节;各段独立 RSA 加密;原始字节拼接后统一 Base64||输出格式|标准 Base64(128 字节密文 →172字符,1 个`=`填充)|**PHP 实现**: ```phpfunctionubix_rsa_encrypt(string$plaintext): string{$pubKey=openssl_pkey_get_public(UBIX_RSA_PUBLIC_KEY);if(strlen($plaintext)<=117){openssl_public_encrypt($plaintext,$result,$pubKey, OPENSSL_PKCS1_PADDING);returnbase64_encode($result);}// 超长分段$chunks=[];$current='';$currentBytes=0;foreach(preg_split('//u',$plaintext, -1, PREG_SPLIT_NO_EMPTY)as$char){$cb=strlen($char);if($currentBytes+$cb>117&&$current!==''){$chunks[]=$current;$current='';$currentBytes=0;}$current.=$char;$currentBytes+=$cb;}if($current!=='')$chunks[]=$current;$bin='';foreach($chunksas$chunk){openssl_public_encrypt($chunk,$out,$pubKey, OPENSSL_PKCS1_PADDING);$bin.=$out;}returnbase64_encode($bin);}

关键发现:Q2 函数等价于标准 Base64

JS bundle 中对 RSA 密文未使用btoa(),而是自定义Q2()函数进行编码:

const vs="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";functionQ2(hexStr){lett=0, n, r="";for(;t +3<=hexStr.length;t+=3){n=parseInt(hexStr.substring(t, t +3),16);// 取3个 hex 字符=12bits r+=vs.charAt(n>>6)+ vs.charAt(n&63);// 输出2个 Base64 字符}// 处理剩余1或2个 hex 字符并补=填充...}

初看像自定义编码,实际上是标准 Base64 的等价实现。数学证明:

对于任意 3 字节[B1, B2, B3],其十六进制串为 6 个字符:

Q2 第一组"h1h2h3"(B1 的两个 nibble + B2 高 nibble): n=B1*16 +(B2>>4)→ n>>6=B1>>2=Base64 标准第1字符 n&63=B1[1:0]<<4|B2[7:4]=Base64 标准第2字符 Q2 第二组"h4h5h6"(B2 低 nibble + B3 的两个 nibble): n=B2[3:0]*256 + B3 → n>>6=B2[3:0]<<2|B3>>6=Base64 标准第3字符 n&63=B3[5:0]=Base64 标准第4字符

结论:Q2(hex(bytes))≡base64(bytes),PHP 直接base64_encode()即可,无需实现 Q2。


请求头生成逻辑

iamData

明文:"${dspAccountId},${dspRoleId}"

来源:登录响应data字段直接提供:

{"dspAccountId":"qq123456789","dspRoleId":9}

示例明文:qq123456789,9生成:ubix_rsa_encrypt("qq123456789,9")→ 172 字符 Base64


免责声明

本报告仅供安全研究和学习目的,请勿用于非法用途。本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号联系作者立即删除

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询