全部文章
4 分钟阅读glizzykingdreko

拆解一种独特的反机器人手段

在给 DiscoverCars 做租车搜索自动化时,我偶然撞上了一种既有意思、又有点不寻常的安全手段,它是用来验证合法用户的。这篇文章会讲述我的调试过程、我如何找出其中的关键机制,以及这种方法为什么既有创意又存在缺陷。

拆解一种独特的反机器人手段

在给 DiscoverCars 做租车搜索自动化时,我偶然撞上了一种既有意思、又有点不寻常的安全手段,它是用来验证合法用户的。这篇文章会讲述我的调试过程、我如何找出其中的关键机制,以及这种方法为什么既有创意又存在缺陷。

初遇挑战

要拿到车辆列表,系统在发出关键请求之前,要求先走一套初始化会话流程

curl 'https://www.discovercars.com/en/search/get-result-list/{session_id}?gToken={recaptcha_token}' \
  -H 'accept: application/json, text/javascript, */*; q=0.01' \
  -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36' \
  -H 'x-requested-with: XMLHttpRequest'

试了很多次之后,我遇到了 HTTP 400 错误。奇怪的是,reCAPTCHA v3 的 token 根本没被校验。这让我怀疑真正的检查发生在别的地方。

找出真正的守门人

我开始逐个分析 cookie,最终锁定了那个关键的部分:gtm_ga_ggbit。和其他 cookie 不同,这一个是会被真正校验的。然而,在网上搜索几乎什么都找不到——除了一年前一个中国用户在 Freelancer 上发的、为同样问题发愁的帖子

在没有任何文档可参考的情况下,我转向了以调试为先的思路,去弄清这个 cookie 是怎么被设置的。

Tampermonkey 调试来救场

既然我怀疑是 JavaScript 负责生成 gtm_ga_ggbit,我就写了一段 Tampermonkey 脚本,去挂钩 document.cookie,并在这个特定 cookie 被设置时中断执行。

// ==UserScript==
// @name         Cookie gtm_ga_ggbit Debugger
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Break into debugger when gtm_ga_ggbit cookie is set by any method.
// @author       glizzykingdreko
// @match        *://*/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const TARGET_NAME = 'gtm_ga_ggbit';
    let lastCookieValue = document.cookie;  // store initial cookies

    // Helper: trigger debugger if target cookie present in document.cookie string
    function triggerDebuggerIfCookieFound(source) {
        // Check for the target cookie name in the cookie string
        const cookieStr = document.cookie;
        if (cookieStr && cookieStr.indexOf(TARGET_NAME + '=') !== -1) {
            console.log(`[Tampermonkey] Cookie "${TARGET_NAME}" detected via ${source}.`);
            alert(`Cookie ${TARGET_NAME} was set via ${source}!`);
            debugger;  // pause execution
        }
    }

    // Periodic check (polling) as fallback for any missed events (e.g., Set-Cookie headers)
    function startCookiePolling() {
        setInterval(() => {
            const current = document.cookie;
            if (current !== lastCookieValue) {  // cookies changed
                if (current.indexOf(TARGET_NAME + '=') !== -1 && lastCookieValue.indexOf(TARGET_NAME + '=') === -1) {
                    // target cookie appeared (wasn't in last value)
                    triggerDebuggerIfCookieFound('polling');
                }
                lastCookieValue = current;
            }
        }, 500);  // check every 500ms (adjust as needed)
    }

    // Hook document.cookie setter for JavaScript writes
    const docCookieDesc = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
    if (docCookieDesc && docCookieDesc.configurable) {
        const nativeSetter = docCookieDesc.set;
        const nativeGetter = docCookieDesc.get;
        Object.defineProperty(Document.prototype, 'cookie', {
            configurable: true,
            enumerable: true,
            get() {
                return nativeGetter.call(this);
            },
            set(value) {
                if (typeof value === 'string' && value.indexOf(TARGET_NAME + '=') === 0) {
                    console.log(`[Tampermonkey] Cookie "${TARGET_NAME}" being set via document.cookie:`, value);
                    alert(`Cookie ${TARGET_NAME} is being set via script!`);
                    debugger;
                }
                // Actually set the cookie using native setter
                nativeSetter.call(this, value);
            }
        });
    }

    // Hook Fetch API
    if (window.fetch) {
        const origFetch = window.fetch;
        window.fetch = function(...args) {
            return origFetch.apply(this, args).then(response => {
                // After response, check cookies
                triggerDebuggerIfCookieFound('fetch/XHR response');
                return response;
            });
        };
    }

    // Hook XMLHttpRequest
    if (window.XMLHttpRequest) {
        const origOpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(...args) {
            // (Optional: store URL or method if needed for debugging)
            this._debugUrl = args[1];
            return origOpen.apply(this, args);
        };
        const origSend = XMLHttpRequest.prototype.send;
        XMLHttpRequest.prototype.send = function(...args) {
            this.addEventListener('loadend', () => {
                triggerDebuggerIfCookieFound('XHR response');
            });
            return origSend.apply(this, args);
        };
    }

    // Hook WebSocket
    if (window.WebSocket) {
        const NativeWebSocket = WebSocket;
        // Override WebSocket constructor
        window.WebSocket = function(...args) {
            const ws = new NativeWebSocket(...args);
            ws.addEventListener('open', () => {
                triggerDebuggerIfCookieFound('WebSocket handshake');
            });
            return ws;
        };
        // Copy prototype to maintain instanceof checks
        window.WebSocket.prototype = NativeWebSocket.prototype;
        window.WebSocket.CONNECTING = NativeWebSocket.CONNECTING;
        window.WebSocket.OPEN = NativeWebSocket.OPEN;
        window.WebSocket.CLOSING = NativeWebSocket.CLOSING;
        window.WebSocket.CLOSED = NativeWebSocket.CLOSED;
    }

    // Use CookieStore API events if available (for modern browsers)
    if (window.cookieStore && window.cookieStore.addEventListener) {
        try {
            window.cookieStore.addEventListener('change', event => {
                // Check if our target cookie was added/changed
                for (const change of event.changed) {
                    if (change.name === TARGET_NAME) {
                        console.log(`[Tampermonkey] Cookie "${TARGET_NAME}" change detected via CookieStore API.`);
                        alert(`Cookie ${TARGET_NAME} was set (CookieStore API)!`);
                        debugger;
                    }
                }
            });
        } catch(e) {
            console.warn('CookieStore API event registration failed:', e);
        }
    } else {
        // Fallback to polling if CookieStore events not supported
        startCookiePolling();
    }
})();

有了这段脚本,我就能精确定位 gtm_ga_ggbit 到底是在哪里被设置的。

揭开基于 SVG 的混淆

关键的发现来自观察到一个奇怪的 JavaScript 函数,它用嵌在前端里的 SVG 元素的 color 属性生成 gtm_ga_ggbit。

JavaScript 逻辑

var svgConvertor = [
    [109, 97, 105, 110, 45, 119, 114, 97, 112, 112, 101, 114, 45, 98, 111, 100, 121],
    [102, 97, 118, 105, 99, 111, 110, 95, 49, 54, 120, 49, 54],
    [102, 97, 118, 105, 99, 111, 110, 95, 51, 50, 120, 51, 50],
    [97, 112, 112, 108, 101, 95, 116, 111, 117, 99, 104, 95, 105, 99, 111, 110, 95, 49, 56, 48, 120, 49, 56, 48]
];

var svgContent = '';
for (let i in svgConvertor) {
    let s = '';
    for (let j in svgConvertor[i]) {
        s += String.fromCharCode(svgConvertor[i][j]);
    }
    s = $('#' + s).attr('colour');
    svgContent += s.split('').reverse().join('');
}

if (typeof myCookie !== 'undefined') {
    myCookie.setCookie('gtm_ga_ggbit', svgContent);
}

逐步拆解

  • svgConvertor 数组解码后得到的是四个前端元素的 ID:main-wrapper-body、favicon_16x16、favicon_32x32、apple_touch_icon_180x180
  • 这几个元素每一个都有一个 colour 属性,里面装着一串看似随机的 hex 字符串
  • 脚本把这些值提取出来、翻转、拼接,然后赋给 gtm_ga_ggbit

例如:

<div class="main-wrapper" id="main-wrapper-body" colour="#d61805d3"></div>
<link id="favicon_16x16" colour="#8585e694">
<link id="favicon_32x32" colour="#7b589a35">
<link id="apple_touch_icon_180x180" colour="#5dae22af">

这种做法很独特,因为它根据前端渲染出的元素动态地混淆 cookie 值,很可能是为了让自动化工具更难预测。

用 Python 绕过它

尽管这方法有创意,它其实很容易逆向。我们只需解析前端、提取所需的值,然后重建这个 cookie。

soup = BeautifulSoup(frontend_response.text, 'html.parser')
svg_content = ''
for element in [
    "main-wrapper-body",
    "favicon_16x16",
    "favicon_32x32",
    "apple_touch_icon_180x180"
]:
    data = soup.find(id=element).get('colour')
    svg_content += ''.join(reversed(data))
svg_content = svg_content.replace('#', '')
session.cookies.set("gtm_ga_ggbit", svg_content)

结语

这是我见过的最有创意的反机器人技术之一。虽然它用一种巧妙的方式来混淆身份验证,但归根结底效果有限,因为:

  1. 这些值都在客户端,可以毫不费力地提取出来。
  2. 一旦分析清楚,逻辑就完全可预测
  3. 它增加了复杂度,却没有实质性地提升安全性。

不过话说回来,这种做法可能会骗到经验不足的开发者,让他们浪费时间去调试其他因素,因此它是一个关于"以混淆求安全"(security-through-obscurity)的有趣案例。

如果有人遇到过类似的技术,很想听听!

关注与联系我

如果你喜欢这次拆解,欢迎关注我:

或者直接联系我:

anti-botweb-scrapingreverse-engineeringweb-automationjavascript

跳过逆向工程。

Takion 为每一道主流反机器人防护墙返回新鲜的 cookie、请求头和令牌。一次 POST,无需浏览器,一小时内完成首次调用。