Перехват данных Internet Explorer
Исходиник для данной статьи: Hood0997.exe (48KB)
Чтобы определить какие данные Internet Explorer
посылает и принимает при нажатиии на кнопку
отправки формы, достаточно создать программу,
имитирующую эти действия. Но к счастью, IE
использует WININET.DLL - системную Win32® DLL, которая
обеспечивает высокоуровневый доступ к протокола
HTTP, FTP, и Gopher, освобождая Вас от необходимости
программирования сокетов Windows®.
Исследуя вызовы, сделанные в 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 NT®
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
|