Я заберу у тебя всё, что
у тебя есть и ты об этом даже
не узнаешь.
Я браузерное расширение
Мостовой Никита (@xnimorz)
nik.mostovoy@gmail.com
1
2
3
4
5
6
10+
Обучение
Работа
Комфорт
Когда писал плагин
Доступ к контенту сайта
Когда писал плагин
Доступ к контенту сайта
Мутации DOM
Когда писал плагин
Доступ к контенту сайта
Мутации DOM
Пермишены сайта
{
"manifest_version": 2,
"version": "4.2",
"icons": {...},
"background": { "scripts": ["background.js"] },
"permissions": ["tabs", "activeTab", "<all_urls>", "http://*/", "https://*/", "https://*/*"],
"browser_action": { "default_icon": {...}, "default_popup": "./popup/index.html" },
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"css": ["tab-styles.css"],
"js": ["./entry.js"]
}
],
}
Permissions
Tabs and ActiveTab
Tabs and ActiveTab
Tabs and ActiveTab
Plugin Script
Tabs and ActiveTab
Plugin Script
Active
Tabs and ActiveTab
Plugin Script
Active
Tabs and ActiveTab
Plugin Script
Active
Tabs and ActiveTab
Plugin Script
Active
< / >
Debugger
Debugger
command
Debugger
command
Chrome DevTools Protocol
Proxy and WebRequest
Proxy and WebRequest
<All_urls> или http://*.*/*
<All_urls> или http://*.*/*
< / >
BrowsingData
Remove BrowsingData
История
LocalStorage
Cookies
IndexedDB
Cache
Downloads
ServiceWorker
{
"manifest_version": 2,
"version": "4.2",
"icons": {...},
"background": { "scripts": ["background.js"] },
"permissions": ["tabs", "activeTab", "<all_urls>", "http://*/", "https://*/", "https://*/*"],
"browser_action": { "default_icon": {...}, "default_popup": "./popup/index.html" },
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"css": ["tab-styles.css"],
"js": ["./entry.js"]
}
],
}
{
"manifest_version": 2,
"version": "4.2",
"icons": {...},
"background": { "scripts": ["background.js"] },
"permissions": ["tabs", "activeTab", "<all_urls>", "http://*/", "https://*/", "https://*/*"],
"browser_action": { "default_icon": {...}, "default_popup": "./popup/index.html" },
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"css": ["tab-styles.css"],
"js": ["./entry.js"]
},
{
"matches": ["https://talantix.ru/*"],
"js": ["content-script.js"]
}
],
}
Manifest.json
{
"manifest_version": 2,
"version": "4.2",
"icons": {...},
"background": { "scripts": ["background.js"] },
"permissions": ["tabs", "activeTab", "<all_urls>", "http://*/", "https://*/", "https://*/*"],
"browser_action": { "default_icon": {...}, "default_popup": "./popup/index.html" },
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"css": ["tab-styles.css"],
"js": ["./entry.js"]
},
{
"matches": ["https://talantix.ru/*"],
"js": ["content-script.js"]
}
],
}
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
chrome.tabs.executeScript(tab.id, { file: './contentScript.js' });
});
Content Scripts execution env
DOM
Content Scripts execution env
DOM
Cookies
Content Scripts execution env
DOM
Cookies storage
Content Scripts execution env
DOM
Cookies storage
Permissions
Content Scripts execution env
JS страницы и наоборот
Content Scripts execution env
JS страницы и наоборот
Все API дублируются
Поток исполнения Content Scripts
while (true) {
// Do something
}
Поток исполнения Content Scripts
const start = window.performance.now();
const SEC_20 = 20000;
while (performance.now() - start < SEC_20) {
// ...
}
Поток исполнения Content Scripts
Поток исполнения Content Scripts
chrome.runtime.sendMessage(
{
action: 'saveResume',
data: message
},
() => {...}
);
chrome.runtime.onMessage.addListener(
(request, sender, sendResponse) => {
// do some job here
sendResponse({foo: 'bar'});
}
);
<body>
<h1>Content script enironment example</h1>
<button>Click Me</button>
<script src="./index.js"></script>
</body>
console.log('{PAGE — START}');
window.test = '123';
document.querySelectorAll = function() {
console.log('monkey patched');
return [];
};
document.querySelectorAll('button');
console.log('{PAGE — END}');
const ACTIONS = {
popupOpened: () => {
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
chrome.tabs.executeScript(tab.id, { file: './entry.js' });
});
},
};
console.log('{PLUGIN — START}');
console.log('window.test = ', window.test);
console.log('cookie = ', document.cookie);
console.log('localStorage = ', localStorage);
console.log(document.querySelectorAll);
console.log(document.querySelectorAll('button'));
const script = `
SCRIPT
`;
const scriptEl = document.createElement('script');
scriptEl.textContent = script;
document.body.appendChild(scriptEl);
document.body.removeChild(scriptEl);
console.log('{PLUGIN — END}');
(function() {
console.log('{INLINE SCRIPT FROM PLUGIN — START}');
console.log('window.test = ', window.test);
console.log('cookie = ', document.cookie);
console.log('localStorage = ', localStorage);
console.log('I work inside the script tag from extension');
console.log(document.querySelectorAll('button'))
const iframe = document.createElement('iframe');
iframe.classList.add('holy-example');
iframe.src = 'http://127.0.0.1:8080/ad';
iframe.width = 200;
iframe.height = 236;
document.body.appendChild(iframe);
console.log('{INLINE SCRIPT FROM PLUGIN — END}');
})();
Проблема больше,
чем с NPM-модулями
Background Scripts
Content Scripts
Векторы атаки
Background scripts
Ущерб веб-сайтам
Вычислительные ресурсы
Дай http://a.ru
Ущерб веб-сайтам
Держи
Ущерб веб-сайтам
Дай http://a.ru
Redirect
Ущерб веб-сайтам
Дай http://a.ru
a.ru?referral
Меняется урл
Ущерб веб-сайтам
Дай http://a.ru
Урл не изменился
a.ru
Ущерб веб-сайтам
a.ru?referral
Дай http://a.ru
Ущерб веб-сайтам
Дай http://a.ru
evil.site
Меняется урл
Вы ничего не сможете с этим сделать
(function anonymous(
) {
// FastProxy RU Chrome
const ext_id = 'mkelkmkgljeohnaeehnnkmdpocfmkmmf';
// epn
const url_aliexpress = 'https://alipromo.com/redirect/cpa/o/odfxz53gynjgxkiq35htlou4v5tbdo0a/';
const url_gearbest = 'http://epnclick.ru/redirect/cpa/o/odiekrsrct8vs5gm1qpwvxtcvtk7l03a/';
const url_aviasales = 'http://epnclick.ru/redirect/cpa/o/onn1f5j6rur2uwikz8y2ejtukpaqw5ux/';
const url_jdru = 'http://epnclick.ru/redirect/cpa/o/p48jt1dk8gycv38rm7w3kwhqroxejs1h/';
const url_hotellook = 'http://epnclick.ru/redirect/cpa/o/onn1gp5tc84flwj0ev08ztgan8k8282y/';
// RU
const url_money_man = 'https://trkleads.ru/click/5be4520e1ca5445aee9dc22d3ac22e18?aff_sub1=FP';
const url_kredito24 = 'https://trkleads.ru/click/ddffeef24df432e1d02962cc6d6cc576?aff_sub1=FP';
const url_lime = 'https://trkleads.ru/click/154faba8f33f6c1a753dc9ac3a5ad295?aff_sub1=FP';
const url_platiza = 'https://pxl.leads.su/click/7f52f90390f9012d07e2d277e9a15e55?aff_sub1=FP';
const url_greenmoney = 'https://trkleads.ru/click/09a3759e8b5d7f48e0d42351ac427d7c?aff_sub1=FP';
const url_dozarplati = 'https://trkleads.ru/click/c181880fb7d174b6c7224d73b1a54883?aff_sub1=FP';
const url_smartcredit = 'https://trkleads.ru/click/90a08d3e15775ff38f92a183e4fdf8ed?aff_sub1=FP';
const url_oneclickmoney = 'https://trkleads.ru/click/ffc8997a1ca0c812ca6942cfae288848?aff_sub1=FP';
const url_zaimonlain24 = 'https://pxl.leads.su/click/f15dc8902e9592ca77ff45eeef9283d1?aff_sub1=FP';
const url_credilo = 'https://trkleads.ru/click/77248505a77755668c94a5b5083a0821?aff_sub1=FP';
const url_honeymoney = 'https://pxl.leads.su/click/ef55760aa47a943066ef1be0547103b9?aff_sub1=FP';
const url_hotzaim = 'https://trkleads.ru/click/bdfe6c2766f6f38901c9777bcbb36b7d?aff_sub1=FP';
const url_zaimexpress = 'https://trkleads.ru/click/13581dd2e1ef87f58948d884702a55ba?aff_sub1=FP';
Background Scripts
Content Scripts
Векторы атаки
Имеет все права плагина и веб-сайта
Полный доступ к DOM и стораджам
Content Scripts
Фейковые страницы
на домене
Тело ответа
Service worker
Content Scripts НЕ может
Получаем доступ к телу ответа
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
chrome.tabs.executeScript(tab.id, { file: './contentScript.js' });
});
Шаг 1. Внедряем ContentScript
const script = "(function(){Do some work})();";
const scriptEl = document.createElement('script');
scriptEl.textContent = script;
document.body.appendChild(scriptEl);
Шаг 2. Внедряем inline script
const original = window.fetch;
window.fetch = (...args) => {
return original(...args).then(data => {
// Тело ответа получено
})
}
Шаг 3. Манкипатчим fetcher
const original = window.fetch;
window.fetch = (...args) => {
return original(...args).then(data => {
// Тело ответа получено
})
}
window.fetch.toString = () => original.toString();
Шаг 4. "Заметаем" следы
const original = window.fetch;
window.fetch = (...args) => {
return original(...args).then(data => {
// Тело ответа получено
})
}
window.fetch.toString = () => original.toString();
console.log(window.fetch);
console.log(window.fetch.toString());
// ƒ fetch() { [native code] }
Шаг 4. "Заметаем" следы
Тело Http запроса
Зачем плагину внедрять код?
Тело Http запроса
Доступ к нашему JS
Зачем плагину внедрять код?
Тело Http запроса
Доступ к нашему JS
Зачем плагину внедрять код?
Свои "фичи" для сайта
Тело Http запроса
Доступ к нашему JS
Отображение своей рекламы
Зачем плагину внедрять код?
Свои "фичи" для сайта
Здесь шансов задетектить или заблокировать плагин больше!
Content Security Policy
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self'; object-src 'self'"
/>
Content Security Policy
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self'; object-src 'self'"
/>
+ Mozilla
Content Security Policy
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self'; object-src 'self'"
/>
+ Mozilla
- Chromium
Content Security Policy
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self'; object-src 'self'"
/>
+ Mozilla
- Chromium
- Наши inline скрипты
IFrame для диалога
Изменения на странице
IFrame для диалога
"Фичи" плагина
Изменения на странице
IFrame для диалога
"Фичи" плагина
Рекламные вставки
Изменения на странице
IFrame для диалога
"Фичи" плагина
Рекламные вставки
Scripts
Изменения на странице
function callback(mutationsList) {
for (const mutation of mutationsList) {
mutation.addedNodes.some((addedNode) => {
if (addedNode.nodeType !== Node.ELEMENT_NODE) return false;
for (const ext in extensions) {
if (addedNode.classList.contains(extensions[ext])) {
sendAnalytics(ext);
observer.disconnect();
return true;
}
}
}
}
}
Mutation Observer (Black list)
Mutation Observer (Black list)
Работает только по известным плагинам + известным именам
function callback(mutationsList) {
for (const mutation of mutationsList) {
mutation.addedNodes.some((addedNode) => {
if (addedNode.nodeType !== Node.ELEMENT_NODE) return false;
if (!addedNode.classList.some(
className => allowedClasses.includes(className))) {
sendAnalytics(ext);
observer.disconnect();
return true;
}
}
}
}
Mutation Observer (White list)
// получаем при сборке, можем сделать специальный loader
const allowedClasses = ['a', 'b'];
const blackList = [
{
className: 'holy-example',
extName: 'holyjs',
remove: (element) => {
element.parentNode.removeChild(element);
},
},
];
Black and white list
// получаем при сборке, можем сделать специальный loader
const allowedClasses = ['a', 'b'];
function remove(element) {
element.parentNode.removeChild(element);
};
Black and white list
Свои CSS классы
White list
"Белые" плагины
Свои CSS классы
White list
"Белые" плагины
Совмещать вместе с черным списком
Свои CSS классы
White list
Что не попадает в черные или белые списки — логируем в аналитику
Аналитика
Что не попадает в черные или белые списки — логируем в аналитику
Аналитика
Либо удаляем весь контент, который не в белом списке
Text
Зачем?
contactsRef.addEventLister(
'click',
(e) => {
if (!e.isTrusted) {
// Плагин?
}
// Событие реальное
}
);
Нужно сделать действие
chrome.debugger.attach(target, "1.2", function() {
chrome.debugger.sendCommand(
target,
"Input.dispatchMouseEvent",
arguments
);
})
Debugger permission
contactsRef.addEventLister(
'click',
(e) => {
if (!e.isTrusted) {
// Не попадем сюда
}
isTrusted === true
Но событие эмулировано плагином
}
);
Debugger permission
document.addEventListener(
'blur',
(e) => {
if (document.hasFocus()) {
// Ушли со страницы
// Или выбран плагин
}
});
Окно в фокусе
document.addEventListener(
'blur',
(e) => {
if (document.hasFocus()) {
// Ушли со страницы
// Или выбран плагин
}
});
Работает только в Mozilla
Окно в фокусе
document.addEventListener('mousemove', (e) => {
console.log(`x=${e.movementX}, y=${e.movementY}`);
console.log(`x=${e.x}, y=${e.y}`);
});
Проверка на плавность
const position = {x: null, y: null}
document.addEventListener('mousemove', (e) => {
console.log(`x=${e.movementX}, y=${e.movementY}`);
console.log(`x=${e.x}, y=${e.y}`);
position = {x: e.x, y: e.y};
});
Проверка на плавность
const position = {x: null, y: null}
document.addEventListener('mousemove', (e) => {
console.log(`x=${e.movementX}, y=${e.movementY}`);
console.log(`x=${e.x}, y=${e.y}`);
position = {x: e.x, y: e.y};
});
document.querySelector('.js-contacts-button')
.addEventListener('click', () => {
// 1) Проверяем, что position внутри кнопки
});
const position = {x: null, y: null}
document.addEventListener('mousemove', (e) => {
console.log(`x=${e.movementX}, y=${e.movementY}`);
console.log(`x=${e.x}, y=${e.y}`);
position = {x: e.x, y: e.y};
});
document.querySelector('.js-contacts-button')
.addEventListener('click', () => {
// 1) Проверяем, что position внутри кнопки
// 2) Проверяем плавность
});
Проверка на плавность
const position = {x: null, y: null}
const prevPos = {x: null, y: null}
document.addEventListener('mousemove', (e) => {
console.log(`x=${e.movementX}, y=${e.movementY}`);
console.log(`x=${e.x}, y=${e.y}`);
prevPos = position
position = {x: e.x, y: e.y};
});
document.querySelector('.js-contacts-button')
.addEventListener('click', () => {
// 1) Проверяем, что position внутри кнопки
// 2) Проверяем плавность
if (Math.abs(position.x - prevPos.y) > TRESHOLD &&
Math.abs(position.y - prevPos.y) > TRESHOLD) {
// Что-то не то
}
});
+ Частично защищает от кликов плагина
Проверка на плавность
+ Частично защищает от кликов плагина
Проверка на плавность
- Работа с клавиатуры
+ Частично защищает от кликов плагина
Проверка на плавность
- Работа с клавиатуры
- Не 100% надежность
внедрение скриптов на страницу
Даже если тег Script удалили — сам скрипт будет исполнен
const windowApi = [
'fetch',
{ document: ['querySelectorAll', 'querySelector'] },
'postMessage'
];
Proxy
new Proxy(caller[item], {
apply: (target, thisValue, args) => {
const error = new Error('HOLY ERROR');
console.log(error.stack.toString());
if (error.stack.includes('anonymous')) {
// Логируем плагин и вызов в систему аналитики
throw new Error();
}
return target.apply(thisValue, args);
},
});
Proxy
function proxify(api, caller) {
api.forEach((item) => {
if (typeof item === 'object') {
return Object.entries(item).forEach(
([key, values]) => proxify(values, caller[key])
);
}
caller[item] = new Proxy(caller[item], {
apply: (target, thisValue, args) => {
const error = new Error('HOLY ERROR');
console.log(error.stack.toString());
if (error.stack.includes('anonymous')) {
// Логируем плагин и вызов в систему аналитики
throw new Error();
}
return target.apply(thisValue, args);
},
});
});
Document
Что проксировать?
Document
Fetch, XmlHttpRequest
Что проксировать?
Document
Fetch, XmlHttpRequest
PostMessage
Что проксировать?
Это нас спасет?
Run_at: Document_start
Это нас спасет?
Нет, но...
const original = window.fetch;
window.fetch = (...args) => {
return original(...args).then(data => {
// Тело ответа получено
})
}
window.fetch.toString = () => original.toString();
console.log(window.fetch);
console.log(window.fetch.toString());
// ƒ fetch() { [native code] }
Не панацея, но
Закешировать API
Заманкипатчить методы
Заманкипатчить prototype.toString()
Плагин должен:
document API
Fetch и XmlHttpRequest
MutationObserver
Плагин должен:
Что-то еще?
Усложняем жизнь разработчикам
Усложняем жизнь разработчикам
Шифруем:
Усложняем жизнь разработчикам
Шифруем:
Запрос и ответ
Усложняем жизнь разработчикам
Шифруем:
Запрос и ответ
JavaScript код
Реверс-инжиниринг
Как ведут себя площадки?
Как ведут себя площадки?
Как ведут себя площадки?
Как ведут себя площадки?
Сказки на ночь
Даже хороший плагин может стать "плохим"
Даже хороший плагин может стать "плохим"
Плагины могут исполнять загружаемый код
Сказки на ночь
Для защиты деликатной информации
Итого: Поведение юзера
Для защиты деликатной информации
Для сбора аналитики
Итого: Поведение юзера
Удаление чужого контента
Итого: Mutation Observer
Удаление чужого контента
Сбор аналитики
Итого: Mutation Observer
Нарушать работу сторонних скриптов
Итого: Proxy
Нарушать работу сторонних скриптов
Сбор аналитики
Итого: Proxy
Самозащита
Итого: Source Viewer
Самозащита
Определять как бороться с плагином
Итого: Source Viewer
А что там с видео-записью?
navigator.permissions.revoke(descriptor);
А что там с видео-записью?
navigator.permissions.revoke(descriptor);
navigator.permissions.revoke({name: 'camera'});
А что там с видео-записью?
navigator.permissions.revoke(descriptor);
navigator.permissions.revoke({name: 'camera'});
navigator.permissions.revoke({name: 'microphone'});
Браузерные расширения
Спасибо за внимание!
Мостовой Никита (@xnimorz)
nik.mostovoy@gmail.com
HolyJs Я заберу всё, что у тебя есть и ты об этом даже не узнаешь. Я — браузерный плагин.
By Nik Mostovoy
HolyJs Я заберу всё, что у тебя есть и ты об этом даже не узнаешь. Я — браузерный плагин.
- 1,824