Использование технологии Microsoft Active Accessibility для доступа к содержимому браузера

  • Tutorial
Давайте придумаем решение вот такой-вот простенькой задачи.
Имеется: браузер (IE, Chrome или Firefox), уже запущенный пользователем.
Требуется: написать программу, которая получит URL, который в данный момент введён в адресной строке.

Давайте подумаем, каким образом эту простенькую задачу решить НЕ получится:

1. FindWindow + GetWindowText
Почему не получится
Первая идея — найти окно браузера, в нём дочернее окно адресной строки и взять URL оттуда. Практика показывает, что отдельное дочернее окно для адресной строки имеет только IE. FF и Chrome кросплатформенны, поэтому предпочитают весь свой контент отрисовывать самостоятельно.

2. Браузерное расширение, которое отдаст URL нашей программе (например, через запрос к localhost)
Почему не получится
Можно. Но во-первых, для трёх браузеров нужно будет написать 3 разных расширения, а во-вторых, для FF и Chrome мы будем вынуждены распространять его только через их магазины расширений. Писать программу, работоспособность которой зависит от того, зачешется ли сегодня левая пятка модератора — нет уж, увольте.

3. Давайте напишем сниффер и посмотрим что там пользователь открывал
Почему не получится
А давайте! Но что дальше? Даже если из потока трафика мы выделим данные, полученные именно браузером и расшифруем HTTP-протокол, мы всё-равно не узнаем именно текущий URL (ссылок в потоке будет много). Кроме того, сразу идут в сад HTTPS-соединения, HTTP/2, ссылки на локально открытые файлы, ссылки на внутренние страницы (типа chrome://settings) и т.д.

4. Давайте воспользуемся Remote Debugging Protocol ну или каким-нибудь Selenium-ом
Почему не получится
Не подходит из-за ограничения условий исходной задачи: браузер уже запущен, мы не можем запустить новый подконтрольный экземпляр, нам нужно взаимодействовать с уже имеющимся.

5. Может быть, хуки?
Почему не получится
Ну, внедриться-то мы в браузер сможем. А на что вешать хуки? Для IE всё ясно — SetWindowText для окна адресной строки (но с ним и более простой способ №1 проходил). А в FF и Chrome у нас нет каких-то чётко определённых объектов и интерфейсов, на которые мы можем завязаться. Можно что-то сделать с конкретной версией браузера, но универсального решения не получится.

6. Скриншот окна браузера, определение положения адресной строки, распознавание текста с картинки!
Почему не получится
Уже как-то начинает смахивать на отчаяние, правда? Прикинем все варианты цветовых схем ОС, разрешений, масштабов, учтём наличие в браузере плагинов, цветовых схем, нестандартного расположения элементов, right-to-left языковых локалей ну и закончим случаем, когда окно адресной строки слишком узкое, чтобы вместить URL полностью.

7. Ваш вариант
А напишите в комментариях, какие ещё решения вам приходят в голову и мы подумаем, получится или нет.

А теперь один из правильных ответов: мы воспользуемся уже старенькой, но весьма стабильной и поддерживаемой всеми браузерами во всех ОС с Win95 до Win10 технологией Microsoft Active Accessibility, которая даст нам возможность не только получить текущий URL (при чём одинаковым образом для всех браузеров), но и вообще дать доступ ко всему контенту браузера — от самого родительского окна с его заголовком, меню, тулбаром, вкладками и до содержимого открытой веб-страницы вплоть до самого последнего её элемента.

Введение


Microsoft Active Accessibility (MSAA) придумали аж в 1997-ом году и сделали её для того, чтобы стало возможным писать экранные лупы, приложения для чтения текста с экрана и создания прочих программ, улучшающим взаимодействие с компьютером людей с ограниченными физическими возможностями (проблемы со зрением, слухом и т.д.). Поддержка технологии в IE появилась давно, в FF и Chrome тоже была добавлена чуть позже. С выходом Vista появилось улучшение — Windows Automation API, однако и старый добрый MSAA никуда не делся, отлично работает с последними ОС и браузерами.

Код


В общем, ничего сложного в коде нет. Входной точкой для нас будет родительское окно браузера, которое можно получить по его ClassID:
FindWindow(L"IEFrame", NULL); // IE
FindWindow(L"MozillaWindowClass", NULL); // Firefox
FindWindow(L"Chrome_WidgetWin_1", NULL); // Chrome. Этот код может сработать, но вообще-то документация (http://www.chromium.org/developers/design-documents/accessibility) рекомендует перебирать все окна, класс которых начинающиеся с "Chrome", на случай, если им взбредёт в голову изменить название класса. Из практики можно добавить, что перебирать нужно окна с таким class name и непустым заголовком.


Дальше нужно у этого окна получить указатель на COM-интерфейс IAccessible
::AccessibleObjectFromWindow(hWndChrome, OBJID_CLIENT, IID_IAccessible, (void**)(&pAccMain));


Да, перед этим не забудьте:
  • Подключить заголовочный файл #include «oleacc.h»
  • Прилинковать Oleacc.lib
  • Инициализировать COM вызовом функции ::CoInitialize(NULL);
    Это очень важно не забыть! Без этого у вас что-то может начать работать, но в непредвиденные моменты вы получите странные ошибки. Также возможна ситуация, когда никаких ошибок не будет, но вы просто не получите часть данных. В общем, очень подлая и совершенное не поддающаяся отладке ошибка.


Итак, у нас есть указатель на IAccessible. Что это такое? Это корневой узел дерева, описывающего весь браузер — окно, заголовок, меню, тулбары, адресную строку, контент страницы, статусбар. Как бы это всё увидеть в наглядном виде? Нет ничего проще! Microsoft для этого предоставляет утилиту inspect.exe (поставляется с Windows SDK, у меня лежит в папке C:\Program Files (x86)\Windows Kits\8.0\bin\x64). Разработчики Хромиума рекомендуют утилиту aViewer.

Давайте посмотрим, как выглядят деревья доступных элементов браузеров:
IE


Chrome


Firefox


Как мы видим, адресная строка доступна через интерфейс IAccessible во всех браузерах. Названия элементов, положение в дереве в разных браузерах разное, но в общем-то для доступа к адресной строке нам нужно только пара функций: возможность получения имени и значения текущего элемента и возможность получения детей текущего элемента дерева.

И то и другое пишется просто, вот финальный код, получающий текущий URL для Chrome.

#include "stdafx.h"
#include <string>
#include <iostream>
#include "windows.h"
#include "oleacc.h"
#include "atlbase.h"

std::wstring GetName(IAccessible *pAcc)
{
	CComBSTR bstrName;
	if (!pAcc || FAILED(pAcc->get_accName(CComVariant((int)CHILDID_SELF), &bstrName)) || !bstrName.m_str)
		return L"";
	
	return bstrName.m_str;
}

HRESULT WalkTreeWithAccessibleChildren(CComPtr<IAccessible> pAcc)
{
	long childCount = 0;
	long returnCount = 0;

	HRESULT hr = pAcc->get_accChildCount(&childCount);

	if (childCount == 0)
		return S_OK;

	CComVariant* pArray = new CComVariant[childCount];
	hr = ::AccessibleChildren(pAcc, 0L, childCount, pArray, &returnCount);
	if (FAILED(hr))
		return hr;

	for (int x = 0; x < returnCount; x++)
	{
		CComVariant vtChild = pArray[x];
		if (vtChild.vt != VT_DISPATCH)
			continue;
		
		CComPtr<IDispatch> pDisp = vtChild.pdispVal;
		CComQIPtr<IAccessible> pAccChild = pDisp;
		if (!pAccChild)
			continue;

		std::wstring name = GetName(pAccChild).data();
		if (name.find(L"Адресная строка и строка поиска") != -1)
		{
			CComBSTR bstrValue;
			if (SUCCEEDED(pAccChild->get_accValue(CComVariant((int)CHILDID_SELF), &bstrValue)) && bstrValue.m_str)
				std::wcout << std::wstring(bstrValue.m_str).c_str();

			return S_FALSE;
		}

		if (WalkTreeWithAccessibleChildren(pAccChild) == S_FALSE)
			return S_FALSE;
	}

	delete[] pArray;
	return S_OK;
}

HWND hWndChrome = NULL;

BOOL CALLBACK FindChromeWindowProc(HWND hwnd, LPARAM lParam)
{
	wchar_t className[100];
	if (GetClassName(hwnd, className, 100) == 0 || wcscmp(className, L"Chrome_WidgetWin_1") != 0)
		return TRUE;

	wchar_t title[1000];
	if (GetWindowText(hwnd, title, 1000) == 0 || wcslen(title) == 0)
		return TRUE;
	
	hWndChrome = hwnd;
	return FALSE;
}


int _tmain(int argc, _TCHAR* argv[])
{
	::CoInitialize(NULL);
	EnumWindows(FindChromeWindowProc, 0);

	if (hWndChrome == NULL)
		return 0;

	CComPtr<IAccessible> pAccMain;
	HRESULT hr = ::AccessibleObjectFromWindow(hWndChrome, 1, IID_IAccessible, (void**)(&pAccMain)); // 1 - захардкоженный идентификатор ловушки

	CComPtr<IAccessible> pAccMain2;
	::AccessibleObjectFromWindow(hWndChrome, OBJID_CLIENT, IID_IAccessible, (void**)(&pAccMain2));

	WalkTreeWithAccessibleChildren(pAccMain2);

	return 0;
}


Результат работы:



Для остальных браузеров всё аналогично.

Мелкий нюанс


Технология MSAA в Chrome по-умолчанию отключена. Это связано с архитектурой Хрома: его разделение на процессы приводит к тому, что ни в каком одном процессе нет информации обо всём дереве элементов, необходимых MSAA. Разработчики Хрома не дураки и предусмотрели включение сбора этой информации и её кеширование в главном процессе. Но поскольку это всё несколько ресурсозатратно, а технология MSAA нужна относительно небольшому количеству людей — они её по-умолчанию выключили. Включить её можно двумя способами:
  • Ручной: пойти в Хроме по ссылке chrome://accessibility и включить
  • Программный: Хром создаёт специальную «ловушку», которой можно послать сообщение о том, что в системе присутствует приложение, использующее MSAA. Отправить в эту ловушку сообщение можно вот так:
    CComPtr<IAccessible> pAccMain;
    HRESULT hr = ::AccessibleObjectFromWindow(hwnd, 1, IID_IAccessible, (void**)(&pAccMain)); // hwnd - главное окно Хрома, 1 - захардкоженный идентификатор ловушки
    

Инфопульс Украина
186,00
Creating Value, Delivering Excellence
Поделиться публикацией

Комментарии 20

    +2
    Напомнило «Знаете, у нас есть крутой вирус, но для его работы нужны админские права»
      0
      Ну, это как с топором, которым можно рубить дрова, а можно зарубить соседа. MSAA даёт доступ к контенту в браузере, а уж будете вы это использовать для написания screen magnifier или для слежения за пользователем — это уже ваше личное дело.
      0
      Для IE и Firefox можно использовать DDE
      Для Хрома, насколько я понял, необходимо парсить окно полностью пока не найдем необходимый нам элемент

      if (name.find(L"Адресная строка и строка поиска") != -1)
      

      А что если у нас это не «Адресная строка и строка поиска». Тогда получается нужно добавлять несколько вариантов, для каждой локали Хрома
        0
        Ага. Второй вариант — отталкиваться от положения элемента в дереве (тип, номер в группе, тип родителя). Но тут мы уже никак не застрахованы от изменений в будущих версиях браузеров.
        +1
        Вот еще кривой способ: захват текста через буфер обмена.

        Находим окно, отправляем в него Ctrl+L (фокус на строку адреса), Ctrl+A (на всякий случай), Ctrl+C.

        Возможно, забираем искомый адрес из буфера обмена. Или не забираем. Как повезет.
          0
          С 5 не согласен. С помощью хуков на API можно достать данных из любых браузеров с шифрованием и без него. Вы просто не ту API ловите
            0
            DrawText? :)
              0
              А ну ка расскажите, что можно ловить в том же Хроме, который рисует всё окно одной картинкой, которая в итоге выводится через DirectX одной цельной текстурой?
                0
                а чего Вы к отрисовке текста привязались? обычно троянцы перехватывабт данные на этапе отправки, шифрования, получения данных.
                  0
                  Как я писал в разделе о снифферах — они бесполезны для HTTPS и HTTP/2 — а именно там всё интересное и бегает. Что касается шифрования, то Хром не использует ни виндовые функции шифрования, ни OpenSSL, так что я повторю вопрос — куда вешать хуки?
                    0
                    Не совсем так — fiddler умеет https, насчет http/2 не в курсе.
                      0
                      fiddler умеет https через импорт своего фейкового сертификата в доверенное хранилище системы. Это решение чисто для отладки, запускать такое на машине конечного пользователя нельзя.
                        0
                        Согласен, хотя в контексте «получить данные из 'интернет-обозревателя'(С)» и «А напишите в комментариях, какие ещё решения вам приходят в голову и мы подумаем, получится или нет.», сойдет.
                        Да и статья, как я понимаю, про MSAA, а не то, как стырить данные из браузера)
              –1
              А можно как-нибудь поменять текстовый инпут на страничке, кроссбраузерно? Скажем открывается клиент-банк, вводится номер счета им все деньги переводятся мне (шуточный пример).
                0
                См. следующий комментарий — fiddler+js.
                А за вопросы такие еще в детстве по жопе бить нужно(за шуточные — шуточно) ).
                0
                Есть еще один способ(по крайней мере, под шарп) — использование FiddlerCore.
                Перехватываем все исходящие/входящие соединения и, если перехваченный трафик содержит в себе html, добавляем к нему js.
                А внутри js уже можно писать любую необходимую логику, да и доступ к dom-елементам никто не отменял
                  +1
                  В своё время занимался похожей задачей, даже статью по этому поводу писал. Правда с тех пор браузеры изменились и, возможно, многое из этого уже не работает. Но, я уверен, что решения можно найти в файлах браузера. Так, я добывал URL хрома и FF из их же файлов в Linux. Т.е. я ничего против вашего решения не имею, но, на мой взгляд, универсальное решение, которое будет работать в других OS, всё же лучше.
                    0
                    Ваш вариант
                    А напишите в комментариях, какие ещё решения вам приходят в голову и мы подумаем, получится или нет.


                    Напрямую выскабливать дерево DOM. Между прочем, многие программы чтения экрана используют для обеспечения невизуальной доступности web-страницы именно прямое выскабливание контента из DOM, потому что accessibility API зачастую даёт недостаточный объём информации.

                    Под тот же Chrome есть вообще специальная локальная программа чтения экрана на JavaScript, которая работает исключительно с DOM, потому что нужно обеспечить кросс-платформенность. Называется ChromeVox, исходники можно посмотреть здесь.

                    А вообще статью надо было назвать «Забиваем гвозди костылями». :-)
                      0
                      «Забиваем гвозди костылями» — это клёво :)

                      Код вообще писался для другого — нынче есть мода писать всё-подряд в виде кастомизированного Хромиума. Например, десктопные клиенты Spotify или Slack — это просто обточенный напильником Хромиум с вшитыми ресурсами. Если хочется с этим монстриком как-то взаимодействовать — вот получаются такие костыли для гвоздей.
                      0
                      Спасибо за решение! Тут — самый лучший код во всём интернете =) Но… проверил на классической Опере и IE 11 — не берёт.
                      Что-то не так с кодом, или архитектура приложений несколько отличается?

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

                      Самое читаемое