某x滑块补环境分析

查看 91|回复 10
作者:BinYoooo   

声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请联系作者删除。

研究对象


image.png (167.65 KB, 下载次数: 6)
下载附件
2025-8-9 18:16 上传

目标
  • 补环境代码与实现
  • 提交参数验证成功

    抓包分析
    获取验证码接口
    通过刷新验证码结果可以发现带有 cap_union_prehandle 的接口返回值含有验证码相关参数:带缺口的背景图、滑块等参数。
    由于图片大小问题,仅展示部分参数图片。


    image-1.png (115.47 KB, 下载次数: 6)
    下载附件
    1
    2025-8-9 18:14 上传

    返回值分析:
  • sess : 该参数不做处理,用于提交时携带
  • data :
  • comm_captcha_cfg:
  • pow_cfg  : md5 和 prefix 参数会用于后续计算,相当重要!!!
  • tdc_path : js 文件,每次验证码请求时都会刷新,其中有两个重要参数在提交接口用到,也是本次补环境的目标,后续会说明。
  • dyn_show_info:
  • bg_elem_cfg : 这里背景图的相关参数,关注 img_url 即可。
  • sprite_url  : 滑块图片,但其中还含有拖动条等,需要裁剪出滑块图片。



    验证码提交接口
    手动提交时,会请求一个验证接口,参数如下:


    image-2.png (222.47 KB, 下载次数: 7)
    下载附件
    2
    2025-8-9 18:14 上传

    请求参数分析:
  • collect      : tdc.js 文件产生,也是本次的目标。包含轨迹和环境的校验。
  • tlg          : collect 的长度
  • eks          : 也是通过 tdc.js 文件产生。当collect参数能正常产出,该值也能正常产出。
  • sess         : 获取验证码接口返回的值
  • ans          : 滑块的缺口值
  • pow_answer   : md5 和 prefix计算所产生的值,后续说明。
  • pow_calc_time: md5 和 prefix计算完成时所消耗的时间。

    补环境
    这里仅给出部分补环境代码与思路,按照思路来是没有问题的。
    堆栈分析
    通过验证请求接口分析,查看请求堆栈:


    image-3.png (78.48 KB, 下载次数: 5)
    下载附件
    3
    2025-8-9 18:14 上传

    发现有关键字样:e.verify,这里是请求组装完成的地方。点击进入,并添加断点。


    image-4.png (58.84 KB, 下载次数: 4)
    下载附件
    4
    2025-8-9 18:14 上传

    重新发起请求,进入断点,发现c参数已经生成好了。


    image-5.png (111.9 KB, 下载次数: 5)
    下载附件
    5
    2025-8-9 18:14 上传

    往上分析,发现c参数在这里生成,同时可以发现collect参数通过o.getTdcData生成


    image-6.png (75.59 KB, 下载次数: 6)
    下载附件
    6
    2025-8-9 18:14 上传

    进入o.getTdcData方法,可以发现最终的返回值是由window.TDC.getData(!0)产生,添加断点,并重新请求。


    image-7.png (41.03 KB, 下载次数: 6)
    下载附件
    7
    2025-8-9 18:14 上传

    进入断点,鼠标放在window.TDC.getData(!0)上面,可以发现,该方法的实现在tdc.js文件中。


    image-8.png (50.11 KB, 下载次数: 4)
    下载附件
    8
    2025-8-9 18:14 上传

    进入该js文件,会发现是jsvmp。至于什么是jsvmp,问问度娘吧~
    开始之前
    在开始之前,可能会有问题:补环境,该怎么补?该补哪些属性?这个问题的答案跟着本文走一遍,相信你应该会有答案。
    首先需要一个代理器(Proxy),这是ChatGPT5对于Proxy的解释:

    Proxy 是 JavaScript 中一个强大的内建对象,它用于创建一个 代理对象,通过它可以拦截并定义基本操作(如属性读取、赋值、函数调用等)的自定义行为。

    简单的理解就是:通过Proxy可以知道对象调用什么了方法、属性等信息。
    下面是本次目标需要的代理器实现:
    const printLog = true;
    // 控制是否输出
    function log() {
        if (printLog) {
            console.log(...arguments);
        }
    }
    function watch(obj, name) {
        return new Proxy(obj, {
            get: function (target, property, receiver) {
                try {
                    if (typeof target[property] === "function") {
                        log(`监控对象 get => ${name} ,读取属性:`, property + `,值为:` + 'function' + `,类型为:` + (typeof target[property]));
                    } else {
                        log(`对象 => ${name} ,读取属性:`, property + `,值为:` + target[property] + `,类型为:` + (typeof target[property]));
                    }
                } catch (e) { }
                return Reflect.get(...arguments);
            },
            set: function (target, property, newValue, receiver) {
                try {
                    log(`监控对象 set => ${name} ,设置属性:`, property + `,值为:` + newValue + `,类型为:` + (typeof newValue));
                } catch (e) { }
                return Reflect.set(target, property, newValue, receiver);
            },
            // 监控 `in` 操作符
            has: function (target, prop) {
                log(`监控对象 has => ${name} , 属性 => ${prop} , 是否存在 => ${prop in target}`);
                return Reflect.has(target, prop);;  // 如果属性存在,返回 true,否则返回 false
            },
            getPrototypeOf: function (target) {
                log(`监控对象 prototype => ${name}`, `Object.getPrototypeOf(${name})`);
                return Reflect.getPrototypeOf(target);
            },
            ownKeys: function (target) {
                log("监控对象 ownKeys =>", name);
                return Reflect.ownKeys(target);
            },
            getOwnPropertyDescriptor: function (target, prop) {
                log("监控对象 getOwnPropertyDescriptor =>", name, "属性值:", prop, `Object.getOwnPropertyDescriptor(${name},"${prop}")`);
                return Reflect.getOwnPropertyDescriptor(target, prop);
            },
        });
    }
    有了上述代码,来实现一个简单的补环境。
    document = {
        cookie: "",
    };
    screen = {
        height: 100,
    };
    // Proxy代理
    document = watch(document, "document");
    screen = watch(screen, "screen");
    document.cookie;
    screen.width;
    在终端运行命令
    node test.js > test.log
    在test.log文件中查看日志,会发现document.cookie;和 screen.width;操作都被记录下来了。


    image-9.png (39.07 KB, 下载次数: 6)
    下载附件
    9
    2025-8-9 18:14 上传

    正式开始
    将所有对象开始监听,下列是主要代码:
    window = globalThis;
    document = {};
    location = {};
    navigator = {};
    window = watch(window, "window");
    location = watch(location, "location");
    document = watch(document, "document");
    navigator = watch(navigator, "navigator");
    require("./tdc.js");
    function getCollect(){
        return decodeURIComponent(window.TDC.getData(!0));
    }
    const collect = getCollect();
    log("输出结果",collect);
    log("输出长度",collect.length);
    运行代码,会发现日志输出中有些是undefined,这个时候就需要注意了,与浏览器进行对比,浏览器有才补,没有不补。


    image-10.png (109.17 KB, 下载次数: 6)
    下载附件
    10
    2025-8-9 18:14 上传



    image-11.png (69.6 KB, 下载次数: 4)
    下载附件
    11
    2025-8-9 18:14 上传

    这里添上screen,但同时不要忘记使用Proxy,只要是对象就使用,防止错过某些重要的值!
    window.screen = {};
    window.screen = watch(window.screen,"window.screen");
    接着运行代码,查看日志,随着环境补的越来越多,就需要仔细分析日志。


    image-12.png (166.74 KB, 下载次数: 6)
    下载附件
    12
    2025-8-9 18:14 上传

    还有一个小问题,会发现有需要地方调用了toString。


    image-13.png (153 KB, 下载次数: 7)
    下载附件
    13
    2025-8-9 18:14 上传

    这里需要拿本地运行的效果与浏览器控制台运行的效果看是否一致。


    image-14.png (28.44 KB, 下载次数: 7)
    下载附件
    14
    2025-8-9 18:14 上传



    image-15.png (13.69 KB, 下载次数: 7)
    下载附件
    15
    2025-8-9 18:14 上传

    不难发现,本地运行的返回[object global],浏览器运行的返回[object Window],明显不一样,这里需要重写toString方法。
    window.toString = function toString(){
        return '[object Window]';
    }
    在此运行代码,发现返回的与浏览器一致了,但是衍生出一个新问题,如果运行下面代码会发生什么呢?
    log(window.toString.toString());


    image-16.png (29.61 KB, 下载次数: 5)
    下载附件
    16
    2025-8-9 18:14 上传



    image-17.png (18.47 KB, 下载次数: 8)
    下载附件
    17
    2025-8-9 18:14 上传

    这里与浏览器运行的结果又不一样了,是不是也可以拿来作为检测点对不对!不用怕,也有应对方法。
    function safeFunction(func) {
        Function.prototype.$call = Function.prototype.call;
        const $toString = Function.toString;
        const myFunction_toString_symbol = Symbol('('.concat('', ')'));
        const myToString = function myToString() {
            return typeof this === 'function' && this[myFunction_toString_symbol] || $toString.$call(this);
        }
        const set_native = function set_native(func, key, value) {
            Object.defineProperty(func, key, {
                "enumerable": false,
                "configurable": true,
                "writable": true,
                "value": value
            });
        }
        delete Function.prototype['toString'];
        set_native(Function.prototype, "toString", myToString);
        set_native(Function.prototype.toString, myFunction_toString_symbol, "function toString() { [native code] }");
        const safe_Function = function safe_Function(func) {
            set_native(func, myFunction_toString_symbol, "function" + (func.name ? " " + func.name : "") + "() { [native code] }");
        }
        return safe_Function(func)
    }
    通过safeFunction函数来保护window.toString方法,应用代码并在此运行检测,发现已经与浏览器的一致了。
    safeFunction(window.toString);


    image-18.png (25.81 KB, 下载次数: 6)
    下载附件
    18
    2025-8-9 18:14 上传

    这里在讲解一下原型的补法,执行下列代码并分析。
    document.__proto__.toString();
    document.__proto__.__proto__.toString();


    image-19.png (99.8 KB, 下载次数: 7)
    下载附件
    19
    2025-8-9 18:14 上传



    image-20.png (26.89 KB, 下载次数: 7)
    下载附件
    20
    2025-8-9 18:14 上传

    发现本地环境这边直接报错了,因为在创建document对象时,给的就是{},这种方式创建的对象默认在Object下,已经是最顶层了,所以无法在调用__proto__.__proto__,所以这里需要补原型了。
    document的原型是HTMLDocument,下面给出代码:
    class HTMLDocument{};
    safeFunction(HTMLDocument);
    HTMLDocument.prototype.toString = function toString(){
        return "[object HTMLDocument]";
    }
    safeFunction(HTMLDocument.prototype.toString);
    document = new HTMLDocument();
    按照这种思路一边看日志分析一边与浏览器进行对比,按照这个思路来慢慢补就行了,我补了大概500多行左右,就可以正常出值了。下面给出部分补好的环境代码,剩下的仿照继续补。
    class EventTarget {
        constructor() {
            safeFunction(this.addEventListener);
            safeFunction(this.dispatchEvent);
            this.listeners = {}; // 存储事件监听器
        }
        // 模拟 addEventListener 方法
        addEventListener(type, callback) {
            if (!this.listeners[type]) {
                this.listeners[type] = [];
            }
            log("注册监听器", type, callback.toString());
            this.listeners[type].push(callback);
        }
        // 模拟 dispatchEvent 方法
        dispatchEvent(event) {
            const listeners = this.listeners[event.type];
            if (listeners) {
                listeners.forEach(listener => listener(event));
            }
        }
    }
    window = global;
    window.top = window;
    window.self = window;
    window.Buffer = Buffer;
    delete window.navigator;
    delete global
    delete Buffer
    delete process;
    delete __dirname;
    delete __filename;
    class HTMLDocument extends EventTarget { };
    HTMLDocument.prototype.characterSet = "UTF-8";
    HTMLDocument.prototype.charset = "UTF-8";
    HTMLDocument.prototype.cookie = "";
    HTMLDocument.prototype.URL = "https://turing.captcha.gtimg.com/1/template/drag_ele.html";
    document = new HTMLDocument();
    screen = {};
    screen.toString = function toString() {
        return "[object Screen]";
    };
    safeFunction(screen.toString);
    location = {
        href: "https://turing.captcha.gtimg.com/1/template/drag_ele.html"
    };
    location.toString = function toString() {
        return "https://turing.captcha.gtimg.com/1/template/drag_ele.html";
    }
    safeFunction(location.toString);
    class Navigator { };
    Navigator.prototype.platform = "MacIntel";
    Navigator.prototype.languages = ["zh-CN", "zh"];
    Navigator.prototype.vendor = "Google Inc.";
    Navigator.prototype.appName = "Netscape";
    Navigator.prototype.hardwareConcurrency = 10;
    Navigator.prototype.webdriver = false;
    Navigator.prototype.cookieEnabled = true;
    Navigator.prototype.appVersion = "";
    Navigator.prototype.userAgent = "";
    Navigator.prototype.serviceWorker = watch({}, "navigator.serviceWorker");
    Navigator.prototype.requestMIDIAccess = function requestMIDIAccess() {
        debugger;
    };
    safeFunction(Navigator.prototype.requestMIDIAccess);
    Navigator.prototype.toString = function toString() {
        return "[object Navigator]";
    };
    safeFunction(Navigator.prototype.toString);
    navigator = new Navigator();
    滑块裁剪
    滑块的原本图像为这样


    image-21.png (67.19 KB, 下载次数: 5)
    下载附件
    21
    2025-8-9 18:14 上传

    这里就需要裁剪,裁剪的代码,
    img = cv2.imread("slide.png", cv2.IMREAD_UNCHANGED)
    x_start, y_start = 140, 490
    width, height = 120, 120  # 假设你要切割 100x100 的区域
    cropped_image = img[y_start : y_start + height, x_start : x_start + width]
    缺口位置识别
    接着提取滑块的缺口位置,这里使用ddddocr来实现
    from ddddocr import DdddOcr
    det = DdddOcr(det=False, ocr=False)
    res = det.slide_match(
        open("slide.png", "rb").read(),
        open("bg.png", "rb").read(),
        simple_target=True
    )["target"]
    ans = f'[{{"elem_id":1,"type":"DynAnswerType_POS","data":"{res[0]},{res[1]}"}}]'
    pow_answer 和 pow_calc_time 实现
    pow_answer本质上是通过 验证码接口返回的 prefix 加上数字在经过哈希算法的结果与返回的md5的值一致就是。下面是通过ChatGPT5生成的计算代码。
    def get_workload_result(nonce, target, timeout_ms=30000):
        """
        通过暴力枚举,找到一个整数 u 使得 MD5(nonce + u) == target
        :param nonce: 字符串前缀
        :param target: 目标 MD5 值
        :param timeout_ms: 超时限制,单位毫秒
        :return: 字典 {'ans': u, 'duration': 耗时毫秒}
        """
        start_time = time.time()
        u = 0
        while True:
            # 计算 MD5
            hash_result = md5_hash(f"{nonce}{u}")
            # 如果匹配,返回结果
            if hash_result == target:
                return {
                    "ans": f"{nonce}{u}",
                    "duration": int((time.time() - start_time) * 1000),
                }
            # 检查超时
            if (time.time() - start_time) * 1000 > timeout_ms:
                break
            # 增加 u
            u += 1
        # 如果超时,返回当前 u 和已消耗时间
        return {"ans": f"{nonce}#{u}", "duration": (time.time() - start_time) * 1000}
    注意有个问题,如果这里的pow_answer计算随机的话,接口也会给你返回正确的参数,但是!将该参数提交过去验证,就会提示验证码错误!


    image-22.png (57.65 KB, 下载次数: 7)
    下载附件
    22
    2025-8-9 18:14 上传

    正确计算的结果


    image-23.png (57.44 KB, 下载次数: 6)
    下载附件
    23
    2025-8-9 18:14 上传

    最后

    补环境一定要仔细,耐心,中间不会的问问AI,再结合实践,你也是可以的!

    下载次数, 下载附件

  • AjiaJiShu   

    大佬,六宫格的验证这个环境也能过吗
    NXXX13   

    最近也在搞 写的不错
    zhoujin1   

    非常不错,就是很难阿
    7001   

    算出模拟滑块的轨迹,还是模拟滑块移动更好?
    rustc   

    又是学习新知识的一天
    tianmiao   

    逆向大佬牛
    ak472pj   

    写的好,有些地方还看不太明白,努力学习
    青衫桑   

    是直接把tdc.js拿下来补环境吗?怎么加上代理之后没有发现访问screen
    atookpp   

    无敌,逆向的都是最强的
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部