Я заберу у тебя всё, что

у тебя есть и ты об этом даже

не узнаешь. 

Я браузерное расширение

Мостовой Никита (@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

Made with Slides.com