Прорываемся сквозь защиту от ботов
В последнее время на многих зарубежных сайтах стала появляться incapsula — система которая повышает защищённость сайта, скорость работы и при этом очень усложняет жизнь разработчикам программного обеспечения. Суть данной системы — комплексная защита с использованием JavaScript, который, к слову, многие DDOS боты уже научились выполнять и даже обходить CloudFlare. Сегодня мы изучим incapsula, напишем деобфускатор JS скрипта и научим своего DDOS бота обходить её!
Сайт на скриншоте ниже взят в качестве хорошего примера для статьи, ничем от других не отличается, на форумах сомнительной тематики многие ищут под него бруты, однако у меня другая задача — ПО для автоматизации различных действий с сайтами.
Начнём с изучения запросов, благо их всего 2 и вот первый:
Этот запрос загружает скрипт который естественно обфусцирован:
Изменив eval на document.write () мы получаем чуть более читабельный код:
Не знаю почему, но автоматические инструменты для форматирования кода на выходе ломают этот код, по этому пришлось форматировать его руками и давать переменным нормальные имена. Для всех этих манипуляций я использовал простой notepad++ и функцию замены текста, в результате можем двигаться дальше и исследовать первую строчку:
var _0x3a59=['wpgXPQ==','cVjDjw==','Tk/DrFl/','GMOMd8K2w4jCpw==','wpkwwpE=','w6zDmmrClMKVHA==', ... ,'w4w+w5MGBQI=','w6TDr8Obw6TDlTJQaQ=='];
Это массив в котором содержатся зашифрованные имена функций и прочие строки, значит нам надо искать функцию которая этот массив расшифровывает, далеко ходить не надо:
var Decrypt=function(base64_Encoded_Param,Key_Param){
var CounterArray=[],
CRC=0x0,
TempVar,
result='',
EncodedSTR='';
base64_Encoded_Param=atob(base64_Encoded_Param);
for(var n=0,input_length=base64_Encoded_Param.length;n
Если мы вызовем её отдельно:
ParamDecryptor('0x0', '0Et]')
То результат будет далеко не таким, какой мы ожидали. А всему виной другая функция:
var shift_array=function(number_of_shifts){
while(--number_of_shifts){
EncodedParams['push'](EncodedParams['shift']());
}
};
Которая находиться в очень неожиданном месте — в функции которая вызывается в самом начале и проверяет куки. Видимо разработчики таким образом «защитили» проверку кук от выпиливания. Как видим — ничего сложного, цикл просто сдвигает массив на указанное количество элементов, в нашем случае 223 элемента. Откуда это магическое число 223? Это число я взял из вызова функции проверки кук, оно там выглядит как 0xdf и идёт по такому маршруту:
//вызов функции
function(EncodedParams,EncodedParamsShifts);
//с параметрами
(AllParams,0xdf); //0xdf => 223
//далее трюк с обфускацией
var _0x5e622e=function(_0x486a40,_0x1de600){_0x486a40(++_0x1de600);};
_0x5e622e(shift_array,EncodedParamsShifts);
//Или что бы было более понятнее:
shift_array(++EncodedParamsShifts);
Естественно оно меняется каждый раз, кто бы сомневался…
Теперь остаётся только заменить все вызовы
var _0x85e545=this[ParamDecryptor('0x0', '0Et]')];
на
var _0x85e545=this['window'];
Или, что ещё лучше, на
var ThisWindow=this.window;
Последнее преобразование я сделал обычной регуляркой. Ах да, совсем забыли привести такие строки:
\x77\x6f\x4a\x33\x58\x68\x6b\x59\x77\x34\x44\x44\x6e\x78\x64\x70
К нормальному виду. Тут ничего сложного, это обычный UrlEncode, меняем \x на % и декодируем что бы получить следующую строку:
woJ3XhkYw4DDnxdp
Затем я начал заменять все вызовы аля
ParamDecryptor('0x0', '0Et]')
на уже расшифрованные строки при помощи самописной функции из моего модуля. Да, код не блещет красотой, сроки горели (как обычно нужно было ещё вчера) и думать было лень ведь я привык программировать мышкой, тем не менее оно отлично работает:
Код переписывал с исходного практически 1в1.
Далее на глаза попался ещё один метод обфускации кода. Пришлось писать достаточно большую функцию которая занимается поиском таких вызовов:
case'7':while(_0x30fe16["XNg"](_0x13d8ee,_0x5a370d))
И заменой их на более простые аналоги:
var _0x30fe16={
'XNg':function _0x19aabd(_0x425e3c,_0x481cd6){return _0x425e3c<_0x481cd6;},
'sUd':function _0x320363(_0xa24206,_0x49d66b){return _0xa24206&_0x49d66b;},
'wMk':function _0x32974a(_0x2cdcf4,_0x250e85){return _0x2cdcf4>>_0x250e85;},
'FnU':function _0x22ce98(_0x2f5577,_0x4feea7){return _0x2f5577<<_0x4feea7;},
'mTe':function _0x35a8bc(_0x11fecf,_0x29718e){return _0x11fecf&_0x29718e;},
'doo':function _0x5ce08b(_0x4e5976,_0x4757ea){return _0x4e5976>>_0x4757ea;},
'vmP':function _0x5d415c(_0x39dc96,_0x59022e){return _0x39dc96<<_0x59022e;},
'bGL':function _0xd49b(_0x7e8c9f,_0x301346){return _0x7e8c9f|_0x301346;},
'rXw':function _0x4dfb4d(_0x39d33a,_0x36fd1e){return _0x39d33a<<_0x36fd1e;},
'svD':function _0x387610(_0x3cd4f7,_0x58fd9e){return _0x3cd4f7&_0x58fd9e;},
'cuj':function _0x472c54(_0x4e473a,_0x26f3fd){return _0x4e473a==_0x26f3fd;},
'OrY':function _0x3c6e85(_0x445d0b,_0x1caacf){return _0x445d0b|_0x1caacf;},
'AKn':function _0x4dac5b(_0x521c05,_0x27b6bd){return _0x521c05>>_0x27b6bd;},
'gtj':function _0x5416f0(_0x3e0965,_0x560062){return _0x3e0965&_0x560062;}
};
Что бы получить:
case'7':while(_0x13d8ee < _0x5a370d){
Алгоритм работы до ужаса прост, по сути просто подставляем переменные:
- Находим имя массива, в нашем случае: _0×30fe16
- Парсим входные параметры: _0×425e3c,_0×481cd6
- Парсим тело функции: _0×425e3c<_0x481cd6
- Заменяем _0×425e3c на _0×13d8ee
- Заменяем _0×481cd6 на _0×5a370d
- Получаем: _0×13d8ee < _0x5a370d
- Заменяем _0×30fe16.XNg (_0×13d8ee,_0×5a370d) на код выше
- Повторяем пока не закончатся функции
Парсинг имени, параметров и тела функции делается одной регуляркой. Конечно в итоговом варианте модуля этот метод особо не нужен, там всего 1 вызов который чуть сильнее обфусцирован, но заказчик сказал делать всё и по этому сделал, к тому же другие функции тоже стали более понятней. На других сайтах бывают ещё и такие конструкции:
var _0x4dc9f4 = {
'NTSjj': _0x3d6e1f.dEVDh,
'tZeHx': function(_0x2a40cd, _0x2faf22) {
return _0x3d6e1f.JIehC(_0x2a40cd, _0x2faf22);
},
'ocgoO': "https://site/login",
'WmiOO': _0x3d6e1f.vsCuf
};
//этот массив должен быть выше, но для удобства он идёт ниже:
var _0x3d6e1f = {
'dEVDh': "4|0|2|3|5|1",
'JIehC': function(_0x34757f, _0xd344e8) {
return _0x34757f != _0xd344e8;
},
'vsCuf': ".countdownGroup",
'awUzV': function(_0x4b3914, _0x1f9e41) {
return _0x4b3914 === _0x1f9e41;
},
'smOkd': "NSpHE",
'bvCub': function(_0x208c1d, _0x160d32) {
return _0x208c1d(_0x160d32);
},
'PmBNl': function(_0x33524f, _0x29b35a) {
return _0x33524f(_0x29b35a);
},
'Fhbrr': "#stopBtn",
'Vkpkf': function(_0x2de6ac, _0x31bb8b) {
return _0x2de6ac + _0x31bb8b;
},
'HbSaV': function(_0x429822, _0x1a46e9) {
return _0x429822 + _0x1a46e9;
},
'UsdKM': "https://site/register",
'JCXqh': "Timer started. ",
'GBXqx': function(_0x18f912, _0x5829b5) {
return _0x18f912 / _0x5829b5;
},
'sSdZf': function(_0x45f64c, _0x152cb4) {
return _0x45f64c(_0x152cb4);
},
'AAmKj': ".countdownTimer"
};
Как видно тут параметр из одного массива ссылается на другой. Я решил эту задачу просто:
- Парсим все массивы
- Вычищаем их из кода
- Копируем все элементы в ассоциативный массив (имя, значение)
- В цикле рекурсивным поиском ищем все вложенные функции
- Заменяем вложенные функции на действия которые они делают
- Разворачиваем все ссылки на строки таким же методом
После применения этого метода деобфускации код стал чуть менее запутанным. Сходу можно заметить функцию base64 по двум признакам. Первый:
CharArray="ABCDE...XYZabcde...xyz0123456789+/";
И второй:
if(!window["btoa"])window["btoa"]=_0x386a89;
Можно дальше не реверсить и переходить к другим, более важным функциям, а если быть точнее — к функции которая работает с куками. Я нашёл её по строке incap_ses_ и заметил ещё одну фишку обфускации — запутывание кода при помощи циклов:
var _0x290283="4|2|5|0|3|1"["split"]('|'), _0x290611=0x0;
while(!![]){
switch(_0x290283[_0x290611++]){
case'0':for(var n=0x0;n
Тут всё очень просто: переставляем строки в соответствии с порядком выполнения: 4×2|5×0|3×1 и получаем оригинальную функцию. Этот метод деобфускации тоже не нужен в итоговом варианте, но он не вызвал больших проблем, всё парсится элементарно, главное учесть что могут быть вложенные циклы и по этому я просто сделал рекурсивный поиск.
var _0x30fe16={
function _0x2829d5(){
var ResultCookieArray=new this.window.Array();
var _0x5ebd6a=new this.window.RegExp("^\s?incap_ses_");
var CookieArray=this.window.document.cookie["split"](';');
for(var n=0x0;n
Она просто заносит в массив значения всех кук которые начинаются с incap_ses_ и далее уже другой метод подсчитывает их контрольную сумму просто суммируя ASCII коды:
function TIncapsula.CharCRC(text: string): string;
var i, crc:integer;
begin
crc:=0;
for i:=1 to Length(text) do
crc:=crc+ord(text[i]);
result:=IntToStr(crc);
end;
function TIncapsula.GetCookieDigest: string;
var i:integer; res:string;
begin
res:='';
for i:=0 to FCookies.Count-1 do begin
if res='' then
res:=CharCRC(browserinfo+FCookies[i])
else
res:=res+','+CharCRC(browserinfo+FCookies[i]);
end;
result:=res;
end;
Контрольная сумма нам нужна будет чуть дальше, а сейчас давайте разберёмся что это за функция такая _0×4d5690 которая вызывается из разных мест. Для этого достаточно просто посмотреть на вызываемые методы и присвоить им соответствующие имена:
function CheckDebugger(){
if(new this.window.Date()["getTime"]() - RunTime) > 0x1f4){
FuckDebugger();
}
}
Автор этого скрипта очень наивный :)
Ещё один важный момент:
ParamDecryptor('0x65', '\x55\xa9\xf9\x1c\x1a\xd5\xfc\x60')
//result = "ca3XP6zjTSB3w3gEwMl6lqgsdEVDTV9aF4rEDQ==";
Отсюда нам нужны первые 5 букв: ca3XP, чуть ниже расскажу зачем. И помните мы подсчитывали контрольные суммы от значений кук? Вот теперь они нам понадобятся что бы получить так называемый хеш.
function TIncapsula.GetDigestHash(Digest: string): string;
var i:integer; CookieDigest, res:string;
begin
CookieDigest:=GetCookieDigest; //85530,85722
res:='';
for i:=0 to Length(Digest)-1 do begin
res:=res+IntToHex(ord(Digest[i+1]) + ord(CookieDigest[i mod Length(CookieDigest)+1]),1);
end;
result:=res;
end;
Сравниваем:
Отлично! Остался последний этап — получение куки для ответа:
ResCooka=((((ParamDecryptor(btoa(PluginsInfo),"ca3XP")+",digest=")+DigestArray)+",s=")+AllDigestHash);
Set_Cookies("___utmvc",btoa(ResCooka),0x14);
Оригинальный код в начале добавлял в конец сдвинутого массива AllParams закодированные в base64 параметры браузера, «шифровал» их функцией ParamDecryptor с ключом ca3XP и потом удалял добавленный ранее элемент. Я могу предположить что данный костыль был сделан из-за не большой особенности: функция ParamDecryptor принимает индекс элемента в массиве и ключ, а значит передать строку туда можно только через массив. Почему бы не сделать нормально? Программисты, сэр.
Ну собственно всё, кука готова, осталось её установить и отправить запрос. Правда её у вас не примет из-за одной маленькой детали про которую я предпочту умолчать.
Оптимизация
Кусочки кода на Delphi — это всего лишь прототип. Весь код деобфускатора был переписан на ассемблер в связи с требованиями заказчика и скорость его выполнения увеличилась в несколько раз. Так же положительно сказалось на скорости следующее:
- Вырезание лишних кусков кода и массивов за одну итерацию цикла. Это нужно что бы значительно уменьшить количество кода и ускорить поиск в дальнейшем.
- Так как функции в коде не перемешаны, то мы знаем их примерное размещение, следовательно если функция установки кук находится в самом конце то искать её надо как минимум с середины.
- Поиск ключевых функций заранее. Алгоритм на ассемблере ищет их ещё в момент очистки кода от не нужного мусора.
- В конечном варианте избавился примерно от половины функций деобфускатора которые нужны были для понимания кода и не нужны для бота так как нужные параметры достаются без проблем
Заключение
Когда я захожу на сайт — я хочу что бы он работал быстро, и обфускация JS скриптов таким методом это неуважение к пользователю. Помогает ли это в защите от ботов? Нет конечно, как видно обходится буквально за вечер, а из затрат только пара бутербродов и несколько чашек чая.
Цель данной статьи рассказать про принцип работы данного решения и показать на сколько оно бесполезно. По понятным причинам готовый модуль для обхода данной защиты опубликован не будет (после его утечки — геймстоп бедный перешёл на akamai), кому надо тот сделает сам основываясь на данном исследовании (которое проводилось около года назад и до сих пор актуально), а школьники которые хотят побрутить чужие аккаунты идут лесом. Если будут вопросы то всегда готов ответить на них в комментариях.