Tous les articles
6 min de lectureglizzykingdreko

Décryptage d'une mesure antibot originale

En automatisant des recherches de voitures pour DiscoverCars, je suis tombé sur une mesure de sécurité fascinante — et quelque peu inhabituelle — conçue…

Décryptage d'une mesure antibot originale

En automatisant des recherches de voitures pour DiscoverCars, je suis tombé sur une mesure de sécurité fascinante — et quelque peu inhabituelle — conçue pour vérifier les utilisateurs légitimes. Cet article détaille mon processus de debug, comment j'ai identifié le mécanisme clé en jeu, et pourquoi cette méthode est à la fois créative et bancale.

Le défi de départ

Pour récupérer les annonces de voitures, le système exigeait un flux d'initialisation de session avant d'émettre une requête clé :

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'

Après plusieurs tentatives, je me suis heurté à des erreurs HTTP 400. Curieusement, les tokens reCAPTCHA v3 n'étaient même pas validés. Cela m'a fait soupçonner que le vrai contrôle se jouait ailleurs.

Trouver le vrai gardien

J'ai commencé à analyser les cookies un par un et j'ai fini par identifier l'élément critique : gtm_ga_ggbit. Contrairement aux autres cookies, celui-ci était activement validé. Cependant, les recherches en ligne ne donnaient presque rien à son sujet — à l'exception d'un post Freelancer vieux d'un an, d'un utilisateur chinois aux prises avec le même problème.

Sans aucune documentation disponible, je suis passé à une approche debug-first pour comprendre comment ce cookie était défini.

Le debug Tampermonkey à la rescousse

Comme je soupçonnais que du JavaScript était responsable de la génération de gtm_ga_ggbit, j'ai écrit un script Tampermonkey pour hooker document.cookie et interrompre l'exécution dès que ce cookie précis était défini.

// ==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();
    }
})();

Grâce à ce script, j'ai pu localiser précisément l'endroit où gtm_ga_ggbit était défini.

Mettre au jour l'obfuscation basée sur SVG

La révélation clé est venue de l'observation d'une étrange fonction JavaScript qui générait gtm_ga_ggbit à partir des attributs de couleur d'éléments SVG intégrés dans le frontend.

La logique 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);
}

Décomposition

  • Le tableau svgConvertor se déchiffre en les IDs de quatre éléments du frontend : main-wrapper-body, favicon_16x16, favicon_32x32, apple_touch_icon_180x180
  • Chacun de ces éléments possède un attribut colour contenant une chaîne hexadécimale d'apparence aléatoire.
  • Le script **extrait ces valeurs, les inverse, les concatène, et les affecte à **gtm_ga_ggbit.

Exemple :

<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">

Cette approche est originale car elle obfusque dynamiquement la valeur du cookie à partir d'éléments rendus par le frontend, ce qui la rend probablement plus difficile à prédire pour les outils automatisés.

Contournement en Python

Malgré sa créativité, cette méthode est facile à rétro-concevoir. Il suffit de parser le frontend, d'extraire les valeurs nécessaires et de reconstruire le 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)

Réflexions finales

C'est l'une des techniques antibot les plus créatives que j'aie rencontrées. Si c'est une façon astucieuse d'obfusquer l'authentification, elle reste au final inefficace parce que :

  1. Les valeurs sont côté client et peuvent être extraites sans effort.
  2. La logique est prévisible une fois analysée.
  3. Elle ajoute de la complexité sans améliorer significativement la sécurité.

Cela dit, cette approche pourrait piéger des devs moins expérimentés et leur faire perdre du temps à débugger d'autres facteurs, ce qui en fait un cas d'étude amusant de sécurité par l'obscurité.

J'aimerais bien savoir si d'autres ont déjà croisé des techniques similaires !

Me suivre et me contacter

Si ce décryptage vous a plu, n'hésitez pas à me suivre sur :

Ou à me contacter directement :

anti-botweb-scrapingreverse-engineeringweb-automationjavascript

Sautez l'étape de la rétro-ingénierie.

Takion renvoie des cookies, en-têtes et tokens frais pour tous les grands murs antibot. Un seul POST, aucun navigateur, premier appel dans l'heure.