Все посты
6 мин чтения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 вообще не проверялись. Это навело меня на мысль, что настоящая проверка происходит в другом месте.

Поиск настоящего привратника

Я стал разбирать cookie один за другим и в итоге нашёл ключевой элемент: gtm_ga_ggbit. В отличие от остальных cookie, этот действительно проверялся. Однако поиск в интернете почти ничего не дал — за исключением годичной давности поста на Freelancer от китайского пользователя, который бился над той же проблемой.

Документации не было никакой, поэтому я перешёл к подходу «сначала отладка», чтобы понять, как выставляется этот cookie.

На помощь приходит отладка через Tampermonkey

Я подозревал, что за генерацию gtm_ga_ggbit отвечает JavaScript, поэтому написал скрипт для 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-функцию, которая генерировала gtm_ga_ggbit, используя атрибуты цвета из SVG-элементов, встроенных во фронтенд.

Логика на 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 расшифровывается в идентификаторы четырёх элементов фронтенда: 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.

Буду рад услышать, встречал ли кто-то ещё похожие техники!

Follow & Contact Me

Если этот разбор вам понравился, подписывайтесь на меня:

Или напишите напрямую:

anti-botweb-scrapingreverse-engineeringweb-automationjavascript

Пропустите реверс-инжиниринг.

Takion возвращает свежие cookie, заголовки и токены для любой крупной антибот-стены. Один POST, без браузера, первый вызов в течение часа.