声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!
前言
最近在逛某 SDN 的时候,突然触发了滑块验证码(应该是风控权重加高了),因为没有相关业务,还是第一次看到该站的验证码,正好,可以用来充实验证码逆向专栏。风控的验证码不好触发、调试,不过一般登录会上和风控一样的验证码,目前登录是文字点选验证码,风控是滑块验证码,算法上差别不大。本文将对登录触发的验证码进行逆向分析:
逆向目标
- 目标:某 SDN 文字点选验证码
- 网址:
aHR0cHM6Ly9rZ3NwaWRlci5ibG9nLmNzZG4ubmV0
抓包分析
打开无痕浏览器,进入页面,点击右上角的登录,选择验证码登录,输入格式正确的手机号,获取验证码即会调用 /sendVerifyCode
接口,响应状态码为 521,触发了安全风控:
该接口的请求参数有三个,code 为中国国际电话区号,mobile 即登录的手机号,type 对应验证类型,该接口会返回一个响应 cookie,waf_captcha_marker
,可看作是当前验证码的标识,后续请求需要携带。
接着,出现两个 /convert
接口,第一个返回验证码类型,文字点选对应的 click_v2
,第二个返回图片链接,经过了加密处理:
这两个接口需要的请求参数差别不大,都经过了加密,后文将对其进行分析:
按题目依次点击图片文字,触发 /verify
校验接口,响应返回的 result 参数为 success 即验证成功,反之失败:
验证通过,会响应返回 yd_captcha_token
参数,cookies 携带该参数与 waf_captcha_marker
参数,即可成功发送短信验证。该接口的请求参数和 convert
接口类似,只不过加密了坐标、轨迹等信息,接下来,对这些加密参数进行逆向分析。
逆向分析
先从第一个 convert 接口入手,从控制台可以看到,该接口是 xhr 类型的,下个 xhr 断点,重新触发验证码,即可断住:
此时还看不出什么关键信息,向上跟栈分析,到 app.js
文件中,该文件经过了 ob 混淆处理,一看就是藏了东西的:
该验证码的处理流程并不复杂,直接硬跟加密,或者使用 ast 技术解混淆都可以。当然,不论哪种方案,由于该 js 会随时间戳 v 动态变化,最好都本地固定一套,再替换调试。ast 解混淆后的 js 文件,已同步到知识星球中,感兴趣的小伙伴可以参考下。
接下来逐个分析下相关加密参数,第一个 convert 的加密参数,在 xhr 断点断住后,向上跟到第二个 app.js
的堆栈处即可找到:
由上图可知,除了 captcha_protect
参数外,其余参数都是经过 gzip 压缩字符串后得到的,标准算法,JavaScript 的话,直接用 pako 库即可复现:
const pako = require('pako');function base64Uint8ArrayToString(fileData) {var dataString = "";for (var i = 0; i < fileData.length; i++) {dataString += String.fromCharCode(fileData[i]);}dataString = decodeURIComponent(escape(dataString));return dataString;
}function gzip(text) {let compressedText = pako.gzip(text);// 将压缩后的字节数组转换为字符串return base64Uint8FromByteArray(compressedText);
}
搜索 _0x43706c["_bsc_cv"]["fpv"]
即可找到生成 fpv 参数的算法:
level、type、originalImage 是 gzip 的固定字符串,_0x43706c["_bsc_cv"]["wlocation"]
包含了些环境参数:
captcha_protect
稍微复杂点,跟到 _0x43706c["captcha_protect"]
中去,如下图所示,控制流打乱了执行顺序:
还原后,执行顺序如下:
// 原始逻辑
_0x478615["end_time"] = new Date().getTime(); // case '1'
_0x478615["guid"] = window["_bsc_cv"]["guid"](); // case '5'
var _0x29e761 = CryptoJS.MD5(_0x478615["name"] + '_' + _0x478615["fpv"]).toString(); // case '2'
var _0x36ae83 = window["rsaEncrypt"](_0x29e761); // case '4'
var _0x6c0312 = window["aesEncryptKey"](_0x29e761, JSON.stringify(_0x478615)); // case '0'
return encodeURIComponent(window["gzip"](_0x36ae83 + "captcha_protect" + _0x6c0312)); // case '3'
时间戳、随机数、fpv 等,经过 MD5、RSA 以及 AES 算法加密后,再 gzip 压缩得到参数值,都是标准算法,其中 AES 加密流程如下:
// 原始逻辑
function originalLogic(_0x1f96f3, _0x3ebdd2) {// 1. 生成 MD5 哈希var _0x132fa1 = CryptoJS.MD5(_0x43706c["_bsc_cv"]["fpv"]).toString();// 2. 前 16 字节作为 AES 密钥var _0x2afe7e = CryptoJS.enc.Utf8.parse(_0x132fa1.substring(0, 16));// 3. 后 16 字节作为 AES IVvar _0x13d566 = CryptoJS.enc.Utf8.parse(_0x132fa1.substring(16));// 4. 执行加密var _0x20b3ba = _0x3ebdd2(_0x1f96f3, _0x2afe7e, _0x13d566);// 5. 返回结果return _0x20b3ba;
}
各加密调库、扣对应算法复现即可,也可以考虑用 python 还原。
后续接口相关参数的算法,都是类似的,无非是 gzip 的字符串或者入参有差异而已。获取到真实的图片链接后,下载时需要注意,保持 cookie 中的 waf_captcha_marker
值以及 ip 与之前的接口一致,否则会导致响应状态码为 521,无法成功下载图片。
有些参数值与之前接口的一致,验证接口多了个 body 参数,加密的坐标、轨迹以及 convert 接口返回的 randomKey 参数:
需要注意的是,verify 接口的 captcha_protect
中的 start_time
和 end_time
不能写成定值,否则一段时间后,会导致验证失败,响应如下:
{"time": 1758097027127,"message": "检测未通过,验证失败","ret": 0,"code": 521,"result": "block"
}
过了验证码之后,将获取到的 yd_captcha_token
参数添加到 cookies 中,即可成功发送短信验证,至此,整套流程分析就结束了。相关算法和解混淆后的 js 文件,会分享到知识星球中,仅供学习交流。