猿人学第三届-第二题-滑块缺口之涟漪
1. 网络包分析
1.1 目标API
https://match2025.yuanrenxue.cn/match2025/topic/2_captcha_jpg1.2 关键载荷参数
- 参数名:
mmc - 作用: 这是我们需要还原的加密参数
2. JS代码解混淆
2.1 初始分析
通过启动器进入JS代码后,发现代码经过了严重混淆:
- 变量名使用了大量隐藏字符(Unicode特殊字符)
- 代码结构被打乱
- 如果不解混淆,代码几乎无法阅读
2.2 解混淆工具
使用在线工具进行解混淆:
https://js-deobfuscator.ve重要设置:
- ✅ 勾选"变量优化"
- ✅ 点击"清理代码"按钮,移除无用代码
2.3 解混淆后的问题修复
第一个报错
解混淆后替换原JS代码,浏览器报错:
2:1015 Uncaught ReferenceError: yrx_ﱞﱞﱞﱞﱞﱞﱞﱞﱞ is not defined at 2:1015:13 at 2:1166:11 at 2:1181:11 (匿名) @ 2:1015 (匿名) @ 2:1166 (匿名) @ 2:1181报错位置代码:
{try{g=yrx_ﱞﱞﱞﱞﱞﱞﱞﱞﱞ(f,newTextEncoder().encode(Navigator.prototype.userAgent.call(navigator))+"_|_");}catch(a){g=yrx_ﱞﱞﱞﱞﱞﱞﱞﱞﱞ(f,newTextEncoder().encode(navigator.userAgent+Date.now("r")));}}定位原始代码:
搜索关键字TextEncoder,找到解混淆前的代码:
try{yrx_ﱞﱞﱞﱞﱞﱞﱞ=yrx_ﱞﱞﱞﱞﱞﱞﱞﱞﱞ(yrx_ﱞﱞﱞﱞﱞﱞ,(newTextEncoder)["encode"](Navigator["prototype"]["userAgent"]["call"](navigator))+"_|_")}catch(yrx_ﱞ){yrx_ﱞﱞﱞﱞﱞﱞﱞ=yrx_ﱞﱞﱞﱞﱞﱞﱞﱞﱞ(yrx_ﱞﱞﱞﱞﱞﱞ,(newTextEncoder)["encode"](navigator["userAgent"]+Date["now"]("r")))}yrx_ﱞﱞﱞﱞ-=6132+7*-122+-10*527解决方案:
通过对比解混淆前后的代码,发现函数定义在下方:
varyrx_ﱞﱞﱞﱞﱞﱞﱞﱞﱞ=function(yrx_ﱞﱞﱞﱞ,yrx_ﱞﱞﱞﱞﱞ){varyrx_ﱞﱞﱞﱞﱞﱞ=yrx_ﱞﱞ(this,(function(){returnyrx_ﱞﱞﱞﱞﱞﱞ["toString"]()["search"]("(((.+)+)+)"+"+$")["toString"]()["constructo"+"r"](yrx_ﱞﱞﱞﱞﱞﱞ)["search"]("(((.+)+)+)"+"+$")}));yrx_ﱞﱞﱞﱞﱞﱞ();varyrx_ﱞﱞﱞﱞﱞﱞﱞ=yrx_ﱞﱞﱞ(this,(function(){varyrx_ﱞ=function(){varyrx_ﱞ;try{yrx_ﱞ=Function("return (fu"+"nction() "+("{}.constru"+'ctor("retu'+'rn this")('+" )")+");")()}catch(yrx_ﱞﱞ){yrx_ﱞ=window}returnyrx_ﱞ}// ... 更多代码}));}我们需要手动重命名这个函数。将其重命名为Getutf8arry:
// 修改后的代码try{g=Getutf8arry(f,newTextEncoder().encode(Navigator.prototype.userAgent.call(navigator))+"_|_");}catch(a){g=Getutf8arry(f,newTextEncoder().encode(navigator.userAgent+Date.now("r")));}d-=8;同时修改函数定义:
varGetutf8arry=functione(d,f){vari=a||27;for(;;){if(i<16){if(i<8){if(i<4){if(i<2){if(i<1){j[l]=255-l;i+=14;}else{// ... 更多代码}}}}}}}第二个报错
再次测试后发现还有一个报错位置:
}elseif(c<11){e=document.getElementById("captchaCanvas");c-=3;}else{k=yrx_ﱞﱞﱞﱞﱞﱞﱞﱞﱞ(f,j);// 这里还有一个未重命名的调用c-=7;}修改为:
}elseif(c<11){e=document.getElementById("captchaCanvas");c-=3;}else{k=Getutf8arry(f,j);// 修改函数名c-=7;}修改完成后,刷新页面,图片正常显示,说明解混淆已完成。
3. 加密逻辑分析
3.1 核心加密函数分析
从我们重命名的Getutf8arry函数入手,分析其返回值:
try{g=Getutf8arry(f,newTextEncoder().encode(Navigator.prototype.userAgent.call(navigator))+"_|_");}catch(a){g=Getutf8arry(f,newTextEncoder().encode(navigator.userAgent+Date.now("r")));}关键发现:
- try块是混淆陷阱:
new TextEncoder().encode(...) + "_|_"会报错,因为 Uint8Array 不能与字符串相加 - 真正的加密在catch块:实际执行的是
navigator.userAgent + Date.now("r") - 返回值:
g是一个长度为124的Uint8Array数组
这是典型的RC4 加密算法的特征。
3.2 加密参数还原
参数1:加密数据(data)
// 真正的加密数据生成newTextEncoder().encode(navigator.userAgent+Date.now("r"))还原为函数:
/** * 生成加密所需的输入数据(UA + 时间戳) * @returns {Uint8Array} 输入数据的字节数组 */functiongenerateInputData(){// 获取浏览器的 UserAgentconstuserAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36'// 获取当前时间戳consttimestamp=Date.now().toString();// 将UA和时间戳合并constcombinedString=userAgent+timestamp;// 转换为字节数组returnnewTextEncoder().encode(combinedString);}参数2:加密密钥(key)
通过调试,将密钥f转换为字符串:
console.log(newTextDecoder().decode(f))// 输出: b31ac05816bb570d发现是一个16位的随机字符串。向上查找代码,找到密钥生成逻辑:
if(d<12){if(d<10){if(d<9){e=Array.apply(null,Array(16)).map(function(){varb=a||0;for(;;){return"0123456789abcdef0123456789abcdef0123456789abcdef6789abcdef789abcdef6789abcdef".charAt(Math.floor(Math.random()*62));}}).join("");// ... 更多代码}}}验证:
"0123456789abcdef0123456789abcdef0123456789abcdef6789abcdef789abcdef6789abcdef".charAt(Math.floor(Math.random()*62));// 'd'"0123456789abcdef0123456789abcdef0123456789abcdef6789abcdef789abcdef6789abcdef".charAt(Math.floor(Math.random()*62));// 'e'"0123456789abcdef0123456789abcdef0123456789abcdef6789abcdef789abcdef6789abcdef".charAt(Math.floor(Math.random()*62));// 'f'每次运行生成一个字符,Array(16)生成16个字符,与密钥长度一致:
'b31ac05816bb570d'.length// 16还原为函数:
/** * 生成16位随机字符串作为加密密钥 * @returns {string} 16位随机字符串 */functiongenerateRandomKey(){constcharset="0123456789abcdef0123456789abcdef0123456789abcdef6789abcdef789abcdef6789abcdef";constkey=Array.apply(null,Array(16)).map(function(){returncharset.charAt(Math.floor(Math.random()*62));}).join("");returnkey;}将密钥转换为 Uint8Array:
/** * 将密钥字符串转换为Uint8Array * @param {string} key - 密钥字符串 * @returns {Uint8Array} 字节数组 */functionkeyToByteArray(key){returnnewTextEncoder().encode(key);}3.3 RC4加密算法还原
将Getutf8arry函数复制到本地:
/** * RC4变种加密算法 * @param {Uint8Array} key - 加密密钥(16字节) * @param {Uint8Array} data - 待加密数据 * @returns {Uint8Array} 加密后的数据 */functionrc4Encrypt(key,data){vari=27;// 固定为27(通过单步调试确定)//此处省略自行复制}重要修改:
- 将
i = a || 27改为固定值i = 27(通过单步调试确定)
3.4 Token格式化
找到将加密数组转换为最终token的位置。全局搜索mmc:
elseif(d<3){h.data={mmc:Array.from(g).map(function(b){varc=a||0;for(;;){returnb.toString(16).padStart(2,"0");}}).join("")+e};}单步调试后验证:
h.data// {mmc: '6a014756a723eec83e1ff6bf8e0ad3e420df3c896f60768a70...543f677c3633be4ab9ab1c9dc1c72e7e3d3f3db51a0982988'}还原为函数:
/** * 将加密结果转换为token格式 * @param {Uint8Array} encryptedData - 加密后的数据 * @param {string} key - 原始密钥 * @returns {object} 包含mmc字段的数据对象 */functionformatToken(encryptedData,key){constresult={data:{mmc:Array.from(encryptedData).map(function(byte){returnbyte.toString(16).padStart(2,"0");}).join("")+key}};returnresult;}格式说明:
- 将每个字节转换为2位16进制字符串
- 所有字节拼接后,追加原始密钥
- 最终形成完整的
mmc参数
4. 完整代码实现
constUSER_AGENT='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36'/** * 生成16位随机字符串作为加密密钥 * @returns {string} 16位随机字符串 */functiongenerateRandomKey(){constcharset="0123456789abcdef0123456789abcdef0123456789abcdef6789abcdef789abcdef6789abcdef";constkey=Array.apply(null,Array(16)).map(function(){returncharset.charAt(Math.floor(Math.random()*62));}).join("");returnkey;}/** * 将密钥字符串转换为Uint8Array * @param {string} key - 密钥字符串 * @returns {Uint8Array} 字节数组 */functionkeyToByteArray(key){returnnewTextEncoder().encode(key);}/** * 生成加密所需的输入数据(UA + 时间戳) * @returns {Uint8Array} 输入数据的字节数组 */functiongenerateInputData(){// 获取浏览器的 UserAgentconstuserAgent=typeofnavigator!=='undefined'?navigator.userAgent:USER_AGENT;// 获取当前时间戳consttimestamp=Date.now().toString();// 将UA和时间戳合并constcombinedString=userAgent+timestamp;// 转换为字节数组returnnewTextEncoder().encode(combinedString);}/** * RC4变种加密算法 * @param {Uint8Array} key - 加密密钥(16字节) * @param {Uint8Array} data - 待加密数据 * @returns {Uint8Array} 加密后的数据 */functionrc4Encrypt(key,data){// 此处省略具体实现,请从原始JS中复制完整的rc4Encrypt函数// 记得将 i = a || 27 改为 i = 27}/** * 将加密结果转换为token格式 * @param {Uint8Array} encryptedData - 加密后的数据 * @param {string} key - 原始密钥 * @returns {object} 包含mmc字段的数据对象 */functionformatToken(encryptedData,key){constresult={data:{mmc:Array.from(encryptedData).map(function(byte){returnbyte.toString(16).padStart(2,"0");}).join("")+key}};returnresult;}/** * 生成完整的加密token * @returns {string} 加密后的token字符串 */functiongenerateToken(){// 1. 生成随机密钥constkey=generateRandomKey();console.log("生成密钥:",key);// 2. 将密钥转换为字节数组constkeyBytes=keyToByteArray(key);console.log("密钥字节数组:",keyBytes);// 3. 生成输入数据(UA + 时间戳)constinputData=generateInputData();console.log("输入数据:",inputData);// 4. 执行RC4加密constencryptedData=rc4Encrypt(keyBytes,inputData);console.log("加密结果:",encryptedData);// 5. 格式化为最终tokenconsttokenData=formatToken(encryptedData,key);returntokenData.data.mmc;}// 使用示例consttoken=generateToken();console.log("最终Token:",token);5. 总结
5.1 加密流程
- 生成随机密钥:16位随机字符串(从特定字符集中选择)
- 准备加密数据:UserAgent + 当前时间戳
- RC4加密:使用密钥对数据进行RC4变种加密
- 格式化输出:将加密结果转为16进制字符串,并追加原始密钥
5.2 关键技术点
- 混淆陷阱:try块中的代码会报错,真正的逻辑在catch块
- RC4算法:使用了RC4的变种实现
- 状态机控制:通过变量
i控制执行流程(固定为27) - Token格式:加密数据的16进制表示 + 原始密钥
5.3 注意事项
- 时间戳必须与服务器时间同步
- UserAgent必须与浏览器一致
- 密钥每次请求都需要重新生成
- 最终的mmc参数包含加密数据和密钥两部分