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…

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 :
- Les valeurs sont côté client et peuvent être extraites sans effort.
- La logique est prévisible une fois analysée.
- 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 :
- GitHub : glizzykingdreko
- Medium : glizzykingdreko
Ou à me contacter directement :
- Discord : glizzykingdreko
- Email : [email protected]
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.