Перехват данных Internet Explorer
Чтобы определить какие данные Internet Explorer посылает и принимает при нажатиии на кнопку отправки формы, достаточно создать программу, имитирующую эти действия. Но к счастью, IE использует WININET.DLL - системную Win32R DLL, которая обеспечивает высокоуровневый доступ к протокола HTTP, FTP, и Gopher, освобождая Вас от необходимости программирования сокетов WindowsR.
Исследуя вызовы, сделанные в WININET.DLL, несложно создать программу, делающую простые запросы API функций в WININET, но уже не прибегая к использованию браузера Internet. Возможность наблюдать за действиями IE очень полезна. Например, очень интересно было наблюдать как IE загружает классы Java, использует cookies, и кэширует только что загруженные страницы и картинки.
Давние читатели MSJ наверняка помнят о моей статье про универсальную программу перехвата API вызовов под названием APISPY32. Теоретически APISPY32 можно использовать для мониторинга за вызовами, сделанными в WININET.DLL, однако я столкнулся с определёнными техническими трудностями, которые не хотелось бы здесь рассматривать. Поэтому я решил использовать более классический способ перехвата API. Проект был назван WininetSpy.
Ядро WininetSpy - это DLL, которая совместно использеут общее имя с WININET.DLL и экспортирует многие из функций, которыми Microsoft снабдила WININET.DLL. Каждая экспортированная функция производит при необходимости регистрацию для вызова соответствующей API в Microsoft WININET.DLL.
Так же, в промежутке между IE и WININET.DLL учавствуют две системные DLL-ки: MSHTML.DLL и URLMON.DLL. Комбинируя список функций WININET, импортирующихся этими DLL-ками, я созал список функций, которые экпортирует моя WININET.DLL. Важное замечание: код WininetSpy был протестирован на IE 3.02, используя Windows NTR 4.0. Если будущие версии IE будут импортировать дополнительные функции WININET, то IE вероятно не сможет работать с моей WININET.DLL. Решается данная проблема путём добавления регистрации для необходимых функций, появившихся в WININET. Естевственно, что если Вы удалите мою WININET.DLL из того места, в котором она должна находиться, то всё должно сново заработать. Ниже Вы увидите почему.
Итак сразу возникают два вопроса. Первый - как в процессе могут существовать две DLL-ки с одни и тем же именем? Второй - как я заставляю систему подключить MSHTML.DLL и URLMON.DLL к моей WININET.DLL вместо системной WININET.DLL? Ответ кроется в местоположении.
В отличие от 16-битной Windows, Win32 не заботится о том, что в адресном пространстве процесса присутствует несколько DLL с одинаковым именем. При отслеживании загруженных DLL, операционная система Win32 использует в качестве имени DLL её полный путь, в то время как 16-битная Windows использует основное имя файла DLL (такое как WININET) в качестве имени модуля. Поэтому Вы можете иметь две копии WININET.DLL, загруженных из разных директорий.
В документации по загрущику в Win32 (API функция LoadModule) сказано, что сначала загрузчик ищет DLL в директории, в которой расположена исполняемая программа. Это то, что нужно для WininetSpy. Поэтому, поместив мою WININET.DLL в директорию, в которой находится IE (IEXPLORE.EXE), тем самым я заставляю мою WININET.DLL загрузиться до загрузки Microsoft WININET.DLL (которая находится с системной директории). Как только моя WININET.DLL загружена, то не составляет труда вызвать LoadLibrary, чтобы загрузить настоящую WININET.DLL. Итак давайте посмотрим на код, чтобы понять все вышеперечисленные рассуждения.
Пример демонстрирует собственно код для WININET.DLL которая скомпилирована используя makefile. Большая часть кода - это шаблоный функций, которые необходимы для регистрации перед вызовом реальных функций из системной WININET.DLL.Мы рассмотрим их позже. А сейчас сконцентрируем всё внимание на функции DllMain, расположенной в начале файла. Эта функция начинает выполняться сразу после загрузки DLL, сразу же запрещая уведомления потока путём вызова функции DisableThreadLibraryCalls. Моей WININET.DLL не нужно знать о создании и завершении потока, поэтому данный вызов говорит системе не бесопокоить мою DllMain о деятельности, связанной с потоком. Далее, DllMain пытается загрузить системную WININET.DLL при помощи вызова LoadLibrary с полным путём для DLL. В моём примере предполагается, что системная WININET.DLL будет находиться в системной директории Win32, которую можно получить при помощи API функции GetSystemDirectory.
Если всё идёт как запланировано, то после того как завершится функция LoadLibrary, то системная WININET.DLL будет загружена и готова к работе. Следующая наша задача заключается в том, чтобы найти адрес каждой функции, экспортируемой системной WININET.DLL. Из примера видно, что я это делаю через GetProcAddress для каждой экспортируемой API. На данный момент WININET.DLL экспортирует около 100 функций, однако в WININETSPY.CPP Вы не найдёте соответствующее количество вызовов GetProcAddress. Вместо этого Вы обнаружите следующий код:
#define SPYMACRO( x ) \ g_pfn##x = GetProcAddress( hModWininet, #x ); #include "wininet_functions.inc"
Данный фрагмент кода использует препроцессор C++ для создания шаблона. Макрокоманда SPYMACRO используется для получения имени функции на входе и расшифровывается как команда:
g_pfnInternetOpenA = GetProcAddress(hModWininet, "InternetOpenA" );
Строка #include "wininet_functions.inc" это список функций, экспортируемых из WININET.DLL. Содержимое этого файла выглядит примерно так:
SPYMACRO( AuthenticateUser ) SPYMACRO( CommitUrlCacheEntryA )
DllMain вызывает GetProcAddress на эту функцию и назначает адрес возврата на указатель данной функции, объявленный глобально. Но почему бы просто не включить весь список функций в DllMain? Подумайте о всех тех указателях функций, которые необходимо объявить глобально и, соответственно за пределами DllMain. Для этого пришлось бы добавить около 100 строчек кода для объявления этих переменных.
Помещая список функций в отдельный файл, я могу использовать различные определения для макроса SPYMACRO. В данный момент SPYMACRO выглядит так:
#define SPYMACRO( x ) FARPROC g_pfn##x;
Это расшифровывается как:
FARPROC g_pfnInternetOpenA;
Расположение всех функций в отдельном файле имеет два преимущества. Во первых, изменить имя переменной можно прямо в макросе SPYMACRO. Во вторых, при добавлении новой функции WININET в файл, как указатель функции, так и соотвествующая GetProcAddress будут автоматически добавлены при перекомпиляции.
Оставшийся код в DllMain просто напросто открывает файл, ведущий лог и закрывает его, при завершении процесса. А теперь давайте рассмотрим процесс регистрации.
Давайте взглянем на InternetCanonicalizeUrlA. Как Вы успели заметить, возвращаемое значение, соглашение о вызове и параметры полностью соответствуют прототипу из WININET.H. Фактически, я просто скопировал соответствующие прототипы из WININET.H в WININETSPY.CPP и сделал из них функции. Суть каждой функции регистрации довольно стандартна и сводится к следующему:
- Объявляем локальную переменную для хранения возвращаемого значения.
- Вызываем реальную функцию WININET при помощи макрокоманды SPYCALL.
- Регистрируем имя функции и соответствующие параметры.
- Возвращаем значение, которое вернула реальная функция WININET.
Наиболее интересная часть данной последовательности, это макрокоманда SPYCALL. Если Вы когда нибудь использовали указатель на функцию, полученный из GetProcAddress, то Вы знаете как это неудобно. В C++ Вам необходимо создать соответствующий typedef для объявления функции, а так же typecast возвращаемого значения из GetProcAddress для этого typedef:
typedef INTERNETAPI (BOOL WINAPI *PFNINTERNETCLOSEHANDLE) (HINTERNET hInternet); g_pfnInternetCloseHandle = (PFNINTERNETCLOSEHANDLE)GetProcAddress( hModWininet, "InternetCloseHandle");
Теперь помножте эту связку на 100 функций WININET. Увы, это должно быть примерно так, чтобы компилятор мог проверить параметры и возвращаемые значения для соответствующего прототипа. Макрос SPYMACRO предоставляет более простой способ вызова реальных функций WININET. См. код SPYMACRO в начале WININET.CPP.
Макрокоманда SPYMACRO - это последовательность встроенных команд ассемблера, которая использует два макропараметра: указатель на функцию, которая вызывается и число типа DWORD, которое передаётся в функцию как число аргументов. К счастью, количество DWORD-ов обычно соответствует количеству аргументов. Код на ассемблере делает копию параметров функции на более низкое расположение в стеке, а затем вызывает через указатель на функцию. Указатель на функцию - это то, что передаёт управление реальной функции в системной WININET.DLL. Просматривая код, Вы заметите, что параметр указателя функции SPYMACRO это всегда одна из глобальных переменных g_pfnXXX, которые я описал ранее.
После того, как реальная функция отработает, макрос SPYCALL очистит скопированные параметры из стека и скопирует возвращённое значение (в EAX) в локальную переменную. Код SPYMACRO предполагает, что Вы объявили локальную переменную с именем retValue, это избавляет компилятор от проверки соответствия типов. В целом, данный приём на практике не рекомендуется, но если Вы достаточно уверены в себе, чтобы пойти на риск, то это стоит того!
Последняя часть кода WININETSPY.CPP собственно и есть регистрация. Регистрация происходит через функцию printf. На самом деле эта функция не является стандартной функцией C++, так как я переопределил эту функцию (в начале WININETSPY.CPP) с той целью, чтобы она записывала результаты в файл. Данное переопределение предполагает, что глобальная переменная с именем g_hOutputFile была инициализирована с допустимым дескриптором файла. Данная инициализация происходит в DllMain.
Когда я начинал писать WININETSPY.CPP, то выходной файл находился на диске в виде текстового файла. С ним довольно просто работать в обычном редакторе. Но со временем мне понадобилось в реальном времени наблюдать выходные данные, например в окошке другой программы. Немного подумав я решил применить технологию почтовых слотов (Win32 mailslo). Вместо того, чтобы открывать существующий файл на диске, DllMain открывает существующий почтовый слот и записывает в него как в файл. Дальнейших изменений в коде не потребовалось. Мы не будем здесь углубляться в детали почтовых слотов. Программа отображения всего процесса просто считывает из почтового слота каждое сообщение и отображает его.
Программа для мониторинга и отображения сообщений была написана на Visual Basic и содержит в себе элемент управления edit, в котором отображается каждая строка из почтового слота. Впоследствие edit был заменён на rich text, чтобы получить некоторые эффекты, такие как поиск и буфер более 64KB. Программа была названа WININETSPYMon (см. Рисунок 2).
Рисунок 2 - WININETSPYMon
Мне бы не хотелось здесь углубляться в детали WININETSPYMon, однако две важные процедуры заслуживают внимания. В процедуре Form_Load сперва создаётся почтовый слот с именем wininetspymon_mailslot. В процедуре Timer1_Timer циклически вызываются GetMailslotInfo и ReadFile до тех пор, пока не останется сообщений. Каждое сообщение добавляется в конец edit контрола. Процедура Timer1_Timer вызывается каждые 50 миллисекунд через стандартный таймер Visual Basic.
Для очистки окна достаточно нажать кнопку Clear output. Для поиска итроки достаточно набрать текст в нижнем окошке и нажать Find. Так же для продолжения поиска можно снова нажать Find. Главно окно с результатами имеет атрибут "только чтение", но Вы можете выделать определённые фрагменты и копировать.
Для использования WininetSpy необходимо проделать следующие вещи.
Не забудьте запустить программу инсталяции WININETSPYMon, чтобы убедиться в наличие библиотеки RICHTX32.OCX, требуемой для проектов Visual Basic 5.0.
Скопируйте WininetSpy версию WININET.DLL в директорию, в которой расположен IE. У меня Windows NT 4.0 и, соответственно папка C:\Program Files\Plus!\ Microsoft Internet.
Запустите WININETSPYMon до запуска IE. Это необходимо, потому что WININETSPYMon создаёт почтовый слот, который моя WININET.DLL ищет в своей DllMain.
Если IE работает неправильно (например, не может соединиться или отобразить вебстраничку), то проблема может скрываться в новых версиях системных DLL таких как URLMON.DLL или MSHTML.DLL. Эти DLL могут импортировать дополнительные функции из WININET.DLL, для которых не обеспечена регистрация. Чтобы устранить данную проблему, достаточно добавить заглушки для недостающих функций.
Незабудьте после окончания перехвата данных удалить WININET.DLL из директории IE. Например, я потратил целый час, прежде чем выяснил, почему браузер не хотел открывать страницу. Оказалось, что для просмотра страницы требовалось 128-битное кодирование и DLL кодирования (SCHANNEL.DLL) определила, что версия моей WININET.DLL не совпадает с версией аналогичной DLL от Microsoft. Удачи в начиначинаниях!