Loading

【验证码逆向专栏】某 SDN 验证码逆向分析

pV4CESx.png

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!

前言

最近在逛某 SDN 的时候,突然触发了滑块验证码(应该是风控权重加高了),因为没有相关业务,还是第一次看到该站的验证码,正好,可以用来充实验证码逆向专栏。风控的验证码不好触发、调试,不过一般登录会上和风控一样的验证码,目前登录是文字点选验证码,风控是滑块验证码,算法上差别不大。本文将对登录触发的验证码进行逆向分析:

pV4CBhn.png

逆向目标

  • 目标:某 SDN 文字点选验证码
  • 网址:aHR0cHM6Ly9rZ3NwaWRlci5ibG9nLmNzZG4ubmV0

抓包分析

打开无痕浏览器,进入页面,点击右上角的登录,选择验证码登录,输入格式正确的手机号,获取验证码即会调用 /sendVerifyCode 接口,响应状态码为 521,触发了安全风控:

pV4lxeJ.png

该接口的请求参数有三个,code 为中国国际电话区号,mobile 即登录的手机号,type 对应验证类型,该接口会返回一个响应 cookie,waf_captcha_marker,可看作是当前验证码的标识,后续请求需要携带。

接着,出现两个 /convert 接口,第一个返回验证码类型,文字点选对应的 click_v2,第二个返回图片链接,经过了加密处理:

pV41uFI.png

这两个接口需要的请求参数差别不大,都经过了加密,后文将对其进行分析:

pV41MfP.png

按题目依次点击图片文字,触发 /verify 校验接口,响应返回的 result 参数为 success 即验证成功,反之失败:

pV411l8.png

验证通过,会响应返回 yd_captcha_token 参数,cookies 携带该参数与 waf_captcha_marker 参数,即可成功发送短信验证。该接口的请求参数和 convert 接口类似,只不过加密了坐标、轨迹等信息,接下来,对这些加密参数进行逆向分析。

逆向分析

先从第一个 convert 接口入手,从控制台可以看到,该接口是 xhr 类型的,下个 xhr 断点,重新触发验证码,即可断住:

pV4YnR1.png

此时还看不出什么关键信息,向上跟栈分析,到 app.js 文件中,该文件经过了 ob 混淆处理,一看就是藏了东西的:

pV4YdQP.png

该验证码的处理流程并不复杂,直接硬跟加密,或者使用 ast 技术解混淆都可以。当然,不论哪种方案,由于该 js 会随时间戳 v 动态变化,最好都本地固定一套,再替换调试。ast 解混淆后的 js 文件,已同步到知识星球中,感兴趣的小伙伴可以参考下。

接下来逐个分析下相关加密参数,第一个 convert 的加密参数,在 xhr 断点断住后,向上跟到第二个 app.js 的堆栈处即可找到:

pV4qpMd.png

由上图可知,除了 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 参数的算法:

pV4qkIf.png

level、type、originalImage 是 gzip 的固定字符串,_0x43706c["_bsc_cv"]["wlocation"] 包含了些环境参数:

pV4XezD.png

captcha_protect 稍微复杂点,跟到 _0x43706c["captcha_protect"] 中去,如下图所示,控制流打乱了执行顺序:

pV4XQeA.png

还原后,执行顺序如下:

// 原始逻辑
_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 IV
    var _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 参数:

pV4XsYV.png

需要注意的是,verify 接口的 captcha_protect 中的 start_timeend_time 不能写成定值,否则一段时间后,会导致验证失败,响应如下:

{
    "time": 1758097027127,
    "message": "检测未通过,验证失败",
    "ret": 0,
    "code": 521,
    "result": "block"
}

过了验证码之后,将获取到的 yd_captcha_token 参数添加到 cookies 中,即可成功发送短信验证,至此,整套流程分析就结束了。相关算法和解混淆后的 js 文件,会分享到知识星球中,仅供学习交流。

结果验证

pV4XblD.png

posted @ 2025-10-23 10:07  K哥爬虫  阅读(15)  评论(0)    收藏  举报