【验证码逆向专栏】某美世界卡状态查询滑块逆向分析

声明

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

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

前言

近期收到私信,有群友咨询某美世界的反爬相关问题。本文就针对某美世界的卡片状态查询页面,拆解其验证码机制与完整业务查询流程。这类场景有一个共性:真正的突破口并不藏在某一个独立接口内,而是存在于一整条有状态的请求链路之中:

逆向目标

  • 网址:aHR0cHM6Ly9wYXkud2FubWVpLmNvbS9uZXcvcXVlcnkvcXVlcnlDYXJkLmpzcA==

抓包分析

首先通过 queryCard.jsp 下发 cookie(JSESSIONID),后续请求必须复用同一个 Session。页面加载完成后,queryCard.js 执行 captchaInit() 拿到 capTicket:

然后请求 /mCaptcha/key 接口传入 capTicket 后返回 ,这里的 capKey 不是随机生成后就可以长期使用的普通参数,它是当前 capTicket 对应的验证码 key:

拿到 capKey 后,SDK 请求 /mCaptcha/info/<capKey> 返回 img 和 loc:

  • img 是验证码图片地址
  • loc 是背景图片的 tile 乱序表

mCaptcha/validate 接口验证通过后返回 rand 参数:

最终卡校验接口需要将之前的参数组合,正确即可查询成功:

逆向分析

图片还原

mCaptcha.pc.min.js 开头的 GetCaptcha 函数定义了几个固定值:

javascript 复制代码
var s = 260,
    c = 120,
    d = 20,
    i = 9,
    n = 61;

各自含义如下:

变量 含义
s 背景宽度 260
c 图片高度 120
d 每行 tile 数 20
i 位移修正量 9
n 缺口块宽度 61

因此背景区域被切成:

text 复制代码
20 列 x 2 行
每块 13 x 60
总计 40 块

图片接口调用成功后,将执行对应的函数处理逻辑:

javascript 复制代码
$.each(t.loc, function(e, t) {
    var i = n;
    a += i += t < d
        ? "background-position: -" + s/d*t + "px 0;"
        : "background-position: -" + s/d*(t%d) + "px -" + c/2 + "px;";
});

等价的 Python 算法:

python 复制代码
src_x = (src_idx % 20) * 13
src_y = 0 if src_idx < 20 else 60

dst_x = (dst_idx % 20) * 13
dst_y = 0 if dst_idx < 20 else 60

最终的 mCaptcha/validate 请求中有两个看起来像密文的字段(op 和 validData),进入相关堆栈,它们的生成位置在 mCaptcha.pc.min.js 文件的鼠标释放逻辑中:

javascript 复制代码
var e = JSON.stringify(r.getPartEventList(100));
var t = "";
h && (t = Encrypt(e, X(y)));

var m = g ? (new Date).getTime() - g : 0;
var data = {
    length: D,
    validateTimeMilSec: m
};
data = Encrypt(JSON.stringify(data), X(y));

$.ajax({
    type: "get",
    url: "https://captchas.wanmei.com/mCaptcha/validate",
    data: {
        capKey: x,
        validData: data,
        op: t,
        fp: o,
        label: l
    }
});

两者都调用 Encrypt 方法,并且第二个参数都是 X(y),接着继续向上找变量赋值,SDK 初始化时有 y = e.capTicket

而页面调用 SDK 的地方是:

javascript 复制代码
mCaptcha.init({
    appId: "10001",
    capTicket: data.result,
    capType: "embed",
    bindBtn: "#sub"
});

这里的 data.result 就是 captchasInit 接口返回的 capTicket。因此完整的反向调用链如下:

javascript 复制代码
mCaptcha/validate.validData
    <- Encrypt(明文 JSON, X(y))
    <- y = capTicket
    <- captchasInit.result

进入 x 函数为切片函数,目的就是最终返回一个 16 个字符(盲猜为 aes key):

javascript 复制代码
function X(e) {
    return e.substring(1,3)
        + e.substring(10,13)
        + e.substring(20,22)
        + e.substring(26,31)
        + e.substring(21,25);
}

再看 Encrypt 的实现,它位于字节 14738 附近:

javascript 复制代码
function Encrypt(e,t) {
    t = CryptoJS.enc.Utf8.parse(t);
    srcs = CryptoJS.enc.Utf8.parse(e);
    t = CryptoJS.AES.encrypt(srcs, t, {
        iv: t,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
    });
    return CryptoJS.enc.Base64.stringify(t.ciphertext);
}

分析可知 op 是行为轨迹:

javascript 复制代码
[
  [441, 515, 1, 0],
  [445, 515, 3, 120],
  [453, 516, 3, 240]
]

validData 是结果摘要 "length": 67,"validateTimeMilSec": 3200(代表含义:轨迹最后移动到了哪里,轨迹耗时与 validateTimeMilSec 是否一致)。

继续向上分析轨迹组成,UserOpListener 的初始化形式为:new UserOpListener(window, 100, 20000);

当前前端只会把事件记录成四元组:clientX, clientY, eventType, elapsedMilliseconds

mousemove 记录要求相邻事件间隔超过 100ms,且 getPartEventList(100) 在事件数量不超过 100 时会直接保留全部事件。因此当前实现把采样点数限制在一个小而稳定的范围内。

让 AI 分析复现一个即可,然后根据轨迹异常率不断调整:

python 复制代码
def build_op_plaintext(
    distance: float,
    validate_time_ms: int,
    start_x: float = 441.0,
    start_y: float = 515.0,
):
    track_delta = distance       # distance = e
    events = [[start_x, start_y, 1, 0]]
    sample_count = max(16, min(24, int(validate_time_ms / 125)))
    last_t = 0

    for index in range(1, sample_count + 1):
        ratio = index / sample_count
        progress = 1 - (1 - ratio) ** 3
        x = start_x + track_delta * progress
        y = start_y + math.sin(ratio * math.pi) * 0.6
        t = int((validate_time_ms - 450) * ratio)
        if t <= last_t:
            t = last_t + 105
        events.append([round(x, 1), round(y, 1), 3, t])
        last_t = t

    return events

最后再生成摘要字段:

javascript 复制代码
valid_data_plain = {
    "length": round(distance + pointer_offset, 1),
    "validateTimeMilSec": validate_time_ms,
}

因此两份明文的关系是:

javascript 复制代码
op 明文
  -> 事件的起点、终点、类型和相对时间

validData 明文
  -> length = e + 9
  -> validateTimeMilSec = 本轮验证耗时

fp 指纹参数

PC SDK 初始化时会异步加载 analysis/vendor/fp.min.js 文件然后调用:

javascript 复制代码
new Fingerprint({
    canvas: true,
    screen_resolution: true
}).get();

参与 hash 的字段

生成 keys 的顺序是:

text 复制代码
navigator.userAgent
navigator.language
screen.colorDepth
screen.height + "x" + screen.width
timezone offset
sessionStorage 可用性
localStorage 可用性
indexedDB 可用性
document.body.addBehavior 类型
window.openDatabase 类型
navigator.cpuClass
navigator.platform
navigator.doNotTrack
navigator.plugins
Canvas data URL

最后:

javascript 复制代码
murmurhash3_32_gc(keys.join("###"), 31)

Canvas 绘制函数位于底部附近,使用固定文字和颜色生成 Canvas data URL:

结果验证

相关推荐
万象观察录2 小时前
API面临越权、爬虫、撞库等风险,有什么解决方案?
爬虫
CDN3604 小时前
爬虫把价格库扒光、SQL注入打穿数据库?360CDN WAF规则引擎深度定制,把防护精度拉到逐字段级
数据库·爬虫·sql
深蓝电商API6 小时前
Scrapy 架构源码解析
爬虫·scrapy
IT毕设实战小研1 天前
基于大数据的WTA职业网球赛事演变与竞技格局数据可视化分析
大数据·后端·爬虫·python·信息可视化·课程设计
2601_962382431 天前
用“爬虫”抓取小红书内容侵权吗 福建法院判明边界
爬虫·数字经济·知识产权·不正当竞争·福建法院
Mycdn_WD1 天前
从 AI 爬虫变多了到 AI 抓取开始分流,CDN 行业进入下一阶段
人工智能·爬虫·cdn·pcdn·城域网·pcdn资源招募
该逃避避2 天前
爬虫代理配置指南:从0搭建Crawl4AI网页爬虫教程
爬虫
TechWayfarer2 天前
Cloudflare 9·15新政倒计时:用IP风险画像识别AI爬虫,防止误伤Googlebot
网络·人工智能·爬虫·python·tcp/ip·网络安全
susplus2 天前
【HTTP协议】HTTP介绍及基础【C语言爬虫实现】
c语言·爬虫·http·万维网