...

среда, 11 сентября 2019 г.

Прорываемся сквозь защиту от ботов

В последнее время на многих зарубежных сайтах стала появляться 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<input_length;n++){
                        EncodedSTR+='%'+('00'+base64_Encoded_Param.charCodeAt(n).toString(0x10)).slice(-0x2);
                }
                
                base64_Encoded_Param=decodeURIComponent(EncodedSTR);
                
                for(var n=0x0;n<0x100;n++){
                        CounterArray[n]=n;
                }
                
                for(n=0x0;n<0x100;n++){
                        CRC=(CRC+CounterArray[n]+Key_Param.charCodeAt(n%Key_Param.length))%0x100;
                        TempVar=CounterArray[n];
                        CounterArray[n]=CounterArray[CRC];
                        CounterArray[CRC]=TempVar;
                }                                       
                
                n=0x0;
                CRC=0x0;
                for(var i=0x0;i<base64_Encoded_Param.length;i++){
                        n=(n+0x1)%0x100;
                        CRC=(CRC+CounterArray[n])%0x100;
                        TempVar=CounterArray[n];
                        CounterArray[n]=CounterArray[CRC];
                        CounterArray[CRC]=TempVar;
                        result+=String.fromCharCode(base64_Encoded_Param.charCodeAt(i)^CounterArray[(CounterArray[n]+CounterArray[CRC])%0x100]);
                }
                
                return result;                                  
};

Если мы вызовем её отдельно:

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){

Алгоритм работы до ужаса прост, по сути просто подставляем переменные:

  1. Находим имя массива, в нашем случае: _0x30fe16
  2. Парсим входные параметры: _0x425e3c,_0x481cd6
  3. Парсим тело функции: _0x425e3c<_0x481cd6
  4. Заменяем _0x425e3c на _0x13d8ee
  5. Заменяем _0x481cd6 на _0x5a370d
  6. Получаем: _0x13d8ee < _0x5a370d
  7. Заменяем _0x30fe16.XNg(_0x13d8ee,_0x5a370d) на код выше
  8. Повторяем пока не закончатся функции

Парсинг имени, параметров и тела функции делается одной регуляркой. Конечно в итоговом варианте модуля этот метод особо не нужен, там всего 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"
};

Как видно тут параметр из одного массива ссылается на другой. Я решил эту задачу просто:
  1. Парсим все массивы
  2. Вычищаем их из кода
  3. Копируем все элементы в ассоциативный массив (имя, значение)
  4. В цикле рекурсивным поиском ищем все вложенные функции
  5. Заменяем вложенные функции на действия которые они делают
  6. Разворачиваем все ссылки на строки таким же методом

После применения этого метода деобфускации код стал чуть менее запутанным. Сходу можно заметить функцию 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<CookieArray["length"];n++){
                        var _0x27e53a=CookieArray[n]["substr"](0x0,CookieArray[n]["indexOf"]('='));
                        var _0x4b4644=CookieArray[n]["substr"](CookieArray[n]["indexOf"]('=')+0x1,CookieArray[n]["length"]);
                        if(_0x5ebd6a["test"](_0x27e53a)){ResultCookieArray[ResultCookieArray["length"]]=_0x4b4644;}
                }
                
                continue;
                
                case'1':return ResultCookieArray;continue;
                case'2':var _0x5ebd6a=new this.window.RegExp("^\s?incap_ses_");continue;
                case'3':_0x4d5690();continue;
                case'4':var ResultCookieArray=new this.window.Array();continue;
                case'5':var CookieArray=this.window.document.cookie["split"](';');continue;
        }
        break;
}


Тут всё очень просто: переставляем строки в соответствии с порядком выполнения: 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<CookieArray["length"];n++){
                var _0x27e53a=CookieArray[n]["substr"](0x0,CookieArray[n]["indexOf"]('='));
                var _0x4b4644=CookieArray[n]["substr"](CookieArray[n]["indexOf"]('=')+0x1,CookieArray[n]["length"]);
                if(_0x5ebd6a["test"](_0x27e53a)){ResultCookieArray[ResultCookieArray["length"]]=_0x4b4644;}
        }
        _0x4d5690();
        return ResultCookieArray;
}


Она просто заносит в массив значения всех кук которые начинаются с 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;



Контрольная сумма нам нужна будет чуть дальше, а сейчас давайте разберёмся что это за функция такая _0x4d5690 которая вызывается из разных мест. Для этого достаточно просто посмотреть на вызываемые методы и присвоить им соответствующие имена:
               
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 — это всего лишь прототип. Весь код деобфускатора был переписан на ассемблер в связи с требованиями заказчика и скорость его выполнения увеличилась в несколько раз. Так же положительно сказалось на скорости следующее:
  1. Вырезание лишних кусков кода и массивов за одну итерацию цикла. Это нужно что бы значительно уменьшить количество кода и ускорить поиск в дальнейшем.
  2. Так как функции в коде не перемешаны, то мы знаем их примерное размещение, следовательно если функция установки кук находится в самом конце то искать её надо как минимум с середины.
  3. Поиск ключевых функций заранее. Алгоритм на ассемблере ищет их ещё в момент очистки кода от не нужного мусора.
  4. В конечном варианте избавился примерно от половины функций деобфускатора которые нужны были для понимания кода и не нужны для бота так как нужные параметры достаются без проблем

Заключение


Когда я захожу на сайт — я хочу что бы он работал быстро, и обфускация JS скриптов таким методом это неуважение к пользователю. Помогает ли это в защите от ботов? Нет конечно, как видно обходится буквально за вечер, а из затрат только пара бутербродов и несколько чашек чая.

Цель данной статьи рассказать про принцип работы данного решения и показать на сколько оно бесполезно. По понятным причинам готовый модуль для обхода данной защиты опубликован не будет (после его утечки — геймстоп бедный перешёл на akamai), кому надо тот сделает сам основываясь на данном исследовании (которое проводилось около года назад и до сих пор актуально), а школьники которые хотят побрутить чужие аккаунты идут лесом. Если будут вопросы то всегда готов ответить на них в комментариях.

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий