Разбор необычной антибот-защиты
Работая над автоматизацией поиска автомобилей для 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)
Заключение
Это была одна из самых изобретательных антибот-техник, что мне встречались. Способ обфусцировать аутентификацию хитрый, но в итоге бесполезный, потому что:
- Значения находятся на стороне клиента и извлекаются без всякого труда.
- Логика предсказуема, стоит только её проанализировать.
- Она добавляет сложность, не давая существенного выигрыша в безопасности.
Тем не менее такой подход может заставить менее опытных разработчиков впустую тратить время на отладку других факторов, что делает его забавным примером security-through-obscurity.
Буду рад услышать, встречал ли кто-то ещё похожие техники!
Follow & Contact Me
Если этот разбор вам понравился, подписывайтесь на меня:
- GitHub: glizzykingdreko
- Medium: glizzykingdreko
Или напишите напрямую:
- Discord: glizzykingdreko
- Email: [email protected]
Пропустите реверс-инжиниринг.
Takion возвращает свежие cookie, заголовки и токены для любой крупной антибот-стены. Один POST, без браузера, первый вызов в течение часа.