Однажды, не очень давно, мне порекомендовали фоновую качалку потокового видео под названием Jaksta, которая позволяет записывать потоковое видео на диск прямо во время просмотра YouTube, Facebook видео, GoogleVideo и так далее. В результате ее установки я получил стойкий BSOD при каждой загрузке Windows. Переключившись в Safe Mode я снес нафиг это творение, но возникли вопросы.
Краткое изучение софтины показало что она устанавливет NDIS Miniport драйвер, который конкретно в моей системе стал умирать при загрузке. «Нафига такие сложности?», подумал я и решил поэкспериментировать с реализацией перехвата потокового видео из браузера без всяких драйверов.
Данный опус предполагает некое знание Windows, WinAPI и немного C++, поэтому если какие то очевидные для меня моменты требуют более подробного разъяснения, то спрашивайте. Сразу внесу ясность, готовой программы для перехвата видео, построенной на принципах изложенных в этом посте, не существует (по крайней мере я ничего такого не писал). Существуют некие заготовки и теоретические измышления, в основном мотивированные как антагонисты решению с NDIS Miniport драйвером и голубыми экранами.
Итак, если гипотетически предположить что у нас есть некий модуль способный перехватывать HTTP или TCP/IP пакеты из браузера, то как именно мы будем ловить видео? Есть два варианта:
Конечное решение наверняка потребует комбинации 1) и 2) для эффективной работы, но в этом посте мы для начала сосредоточимся на перехвате пакетов.
Первое что приходит на ум для написания перехватчика, это внедрение DLL в процесс браузера. Некоторые в этом месте прекратят читать, потому что дальше все понятно. Кому все понятно могут скачать исходники тут и скомпилированную версию вот тут (да, всего 3Кб). Если вы решили попробовать как все это работает, то настоятельно рекомендую взять 32-битный браузер и вырубить весь софт который использует похожие трюки, например AdMuncher (привет Murray & shannow!), потому что данный пример плевать хотел на корректное сожительство с таким софтом (это исправимо). Результаты работы ищите в виде .log файлов в %TEMP%.
Для всех остальных придется кто-что пояснить, хотя системные ловушки и внедрение DLL в процессы Windows это достаточно избитая тема. Конечный результат внедрения своей DLL в процесс браузера будет выглядеть примерно так:
То есть внутри каждого процесса браузера будет сидеть наша самописная DLL, которая и будет перехватывать нужные пакеты. Сразу возникает два вопроса
Попробуем ответить на них ниже…
На первой стадии очевидно что нам необходимо написать два модуля – главное приложение и DLL. Главное приложение будет внедрять, а DLL соответственно внедряться. Не мудрствуя лукаво запустим Vistual Studio и сразу напишем главное приложение (Injector.cpp):
Что делает вышеописанный код? В первой строке из соображений компактности кода мы ставим точку входа в приложения прямо в WinMain(), без прелюдий. В исходниках я вообще отрезал MSVCRT за ненадобностью.
Далее, мы загружаем наш перехватчик, находим в нем экспортную функцию под номером 1 (import by ordinal) и ставим глобальную ловушку типа CBT, используя созданые параметры. Потом мы просто выводим модальное сообщение нажать кнопку ”OK” для завершения и уходим в астрал. Все. Этого достаточно для внедрения DLL во все процессы, которые так или иначе используют User32 WinAPI для работы с окнами.
CBT, это сокращение от Computer-Based Training. Ловушка WH_CBT вообще хороша тем что ”… Система вызывает данную ловушку перед активацией, созданием, уничтожением, минимизацией, максимизацией, перемещением или изменением размера окна. А таже перед завершением системной команды, перед удалением события клавиатуры или мышки из очереди сообщений, перед установкой фокуса ввода и перед синхронизацией с системной очередью сообщений...” Такой вот вольный перевод MSDN. На деле это означает что она сработает для 99% приложений которые написаны в соответствии со стандартной оконной архитектурой.
Прелесть данного метода в том что на самом деле нам не надо будет возиться с системой ловушек Windows как таковой.
Поскольку мы пишем перехватчик, а не что-то еще, ему будет достаточно того что:
Стоит сказать что данная техника требует соответствия модулей по разрядности. Это означает что 64-битный браузер будет нуждаться в 64-битной DLL-перехватчике, то же самое касается 32-битных приложений. Не стоит ждать что 32-битный перехватчик будет загружен в 64-битное приложение и тем более наоборот.
Итак, напишем скелет будущего перехватчика(Interceptor.cpp):
Кроме того нам надо указать экспорт под номером 1. Для этого мы пишем стандартный DEF файл (Interceptor.def) и не забываем скормить его линковщику через параметр /DEF:
Все. Теперь DLL у нас подклеивается к процессам и сидит в них до завершения. Для того чтобы нам не внедряться в ненужные процессы и корректно себя вести внутри главного приложения (да-да, ведь оно же загружает и инициализирует DLL) сделаем дополнительную проверку:
Таким образом, если приложение нам неизвестно, то мы в него не грузимся. Теперь займемся непосредственно перехватом функций WinSock.
Для начала нужно пояснить очевидные вещи. Поскольку те кому все уже и так ясно до этой строчки все равно не дочитают, то сделаю отступ. К моему изумлению я понял что некоторые программисты, весьма умные и продвинутые, не всегда четко представляют себе как работают системные библиотеки в процессах Windows. В связи с этим настоятельно рекомендую просмотреть другой пост на Хабре – ”Пошаговое руководство к исполняемым файлам (EXE) Windows” .
Важно понимать вот что:
Все DLL библиотеки, включая системные и те что указаны в PE заголовке как импорты, загружаются непосредственно в адресное пространство приложения которое их использует. С логической точки зрения каждое запущенное приложение имеет свой индивидуальный набор копий системных и прочих DLL.
Таким образом, самым простым способом перехватить пакеты в браузере будет перехватить вызов определенных функций в системной библиотеке, отвечающих за отправку и прием пакетов. На этом месте некоторые опять прекратят читать, потому что все опять понятно и ничего нового, с чем я полностью согласен. Но для всех остальных продолжу.
Перехват можно сделать на разных уровнях: WinHTTP, WinINet, WinSock. Для меня наиболее универсальным представляется перехват функций WinSock из библиотеки WS2_32.DLL. Он имеет свои недостатки, особенно при работе с HTTPS, где пакеты шифруются. Для HTTPS, с моей точки зрения, наилучшим решением будет перехват функций WinHTTP и/или библиотек OpenSSL. Но начнем с простого.
Итак, выделим основные моменты того что нам нужно сделать:
Согласно древним традициям обратной совместимости в Windows есть несколько способов сделать одно и то же путем вызова разных функций, поэтому будем стараться отловить их все. Для нашей задачи должно хватить перехвата следующих из WS2_32:
Причем три последних нужны исключительно в целях создания и уничтожения контекста привязанного к конкретному соединению, если оное вообще нужно. В данном примере я постараюсь избавиться от контекста вообще. Однако в реальности скорее он будет нужен, чтобы правильно собирать пары HTTP запрос — HTTP ответ. При этом перехват connect() и WSAConnect() строго говоря не обязателен, так как новый контекст для нового сокета может быть создан де-факто при первой записи в него.
Итак, как заведем в нашей DLL структуру чтобы переписывать и восстанавливать точку входа в функциях WinSock:
Про то что такое HOOK_CODE_SIZE и от чего он зависит читаем дальше.
Чтобы перехватывать вызов функции в точке входа нам придется патчить код. Итак, самый простой алгоритм будет такой:
Есть несколько разных методов перехвата, с функциональной точки зрения. Самый простой метод, это постоянно переписывать код в самом начале точки входа с изначального на свой и обратно (при вызове изначальной функции). Есть более сложные методы при которых не надо постоянно переписывать код, но которые требуют написания анализатора инструкций, дабы правильно внедрить свой перехватчик в середину изначальной функции. Остановимся пока на самом простом – перезапись в начале кода.
Опять же есть несколько способов вызвать свой обработчик. Не углубляясь в детали выделю два из них: безусловный переход и возврат по стеку вызова. В первом случае концепция такова:
Это предельно просто и в 32-битном выражении требует 5 байт, один для кода инструкции безусловного перехода JMP и четыре для относительного адреса. Почему относительного, позже. Во втором случае концепция немного другая:
Это требует 6 байт — два байта на коды инструкций PUSH <32-bit DWORD> и RETN и четыре для абсолютного адреса. Да-да. В первом случае адрес считается относительно текущего адреса исполнимого кода. Во втором он постоянен и считается относительно начала адресного пространства. Я пойду первым методом.
Пишем установщик перехватчика:
Не думаю что вышеописаный код нуждается в дополнительном пояснении. Стоит заметить, что патченый код мы вначале формируем в структуре, отвечающей за содержание информации о перехватчике функций, а непосредственно патч мы делаем макросами посредством memcpy(). Любители эстетики могут добавить туда lock, но на мой взгляд это лишнее, догадайтесь почему?
Чтобы включить ловушку, мы копируем новый код, содержащий только переход на адрес собственного обработчика. Чтобы ловушку выключить мы восстанавливаем 5 изначальных байт, сохраненных в массиве под именем oldCode.
Поскольку установку ловушки на функцию мы написали, стоит написать и восстановитель изначального состояния кода функции:
Теперь, когда мы умеем внедряться в процесс, ставить, убирать, включать и выключать ловушки, самое время заняться собственными обработчиками.
Итак, для начала определим массив прототипов перехватываемых функций. Как указано выше, попробуем перехватывать только самые необходимые функции:
Как видно из макроса, каждой системной функции с абстрактным именем name сответствует собственный обработчик под именем my_name. Теперь надо определить обработчики указаные в массиве. Сделаем это на примере send():
Так будет выглядеть пустая обертка системной функции, которая ничего не делает кроме того что вызывает изначальный код. Поскольку обертка по сути одна и та же для всех собственных обработчиков, то имеет смысл сделать пару макросов ради удобства определения оных:
и для выхода из функции тоже:
Далее мы используем эти макросы, чтобы определить оставшиеся ловушки из массива.
Последняя строчка в нашем OnLoad() вызывает некую магическую функцию InstallHooks(). Поскольку все составляющие решения у нас налицо, напишем пакетную установку всех определенных ловушек:
Вот так лаконично. И пакетное удаление ловушек не менее лаконичное:
Ну вот мы плавно подобрались к самому интересному. Итак, у нас два типа пакетов – HTTP запросы и HTTP ответы. Соответственно, первые отправляются функциями типа send(), вторые принимаются функциями типа recv(). Функции отправки надо перехватывать ДО вызова изначального кода, пока буфер отправки еще девственно чист. Функции приема, соответственно надо перехватывать ПОСЛЕ исполнения изначального кода, иначе не увидим что именно принято.
Есть еще асинхронные функции. Идея там простая. При вызове WSASend() или WSARecv() задается структура WSAOVERLAPPED в которой прописывается Event. Асинхронные функции завершаются мгновенно, и по завершению выдают SOCKET_ERROR с GetLastError() установленным в WSA_IO_PENDING. Далее основное приложение ждет события Event любым способом, например WaitForSingleObject(), и как только статус события установлен, то приложение дочитывает буфер через WSAGetOverlappedResult().
Если снять данные с синхронных функций не составляет труда, то с асинхронными придется немного повозиться. Вначале поста я упоминал что полностью от контекстов избавиться не получится, а асинхронные операции это именно то почему. Более детально. Вызов WSAGetOverlappedResult() не несет в себе никакой информации о буфере отправки или приема. Поэтому очевидно что нужно создавать контекст и хранить указатель на буфер там.
Есть еще одна причина по которой нужен контекст. Поскольку для нашей задачи, перехвата поточного видео, требуются и HTTP запросы и ответы, то наиболее логично решение которое будет собирать разрозненные вызовы send(), recv() в пары. Итак, заведем структуру для контекста, который будет пригоден и для сбора пар HTTP запросов и ответов, и для работы с
асинхронными функциями:
Для чего нужно все вышеозначенное? По номеру сокета socket мы будем определять соответствие запроса и ответа. То есть основное приложение отправляет и получает запрос по одному и тому же TCP сокету, иначе быть не может. Указатель request будет ссылаться на HTTP запрос. Указатель LPWSABUF будет использоваться в случае асинхронных функций. То бишь при вызове WSASend()/WSARecv() мы указатель на буфер будем сохранять, а при завершении его WSAGetOverlappedResult() мы его оттуда будем вынимать. Опять же соответствие определяется через номер сокета.
Забегая вперед скажу что для WSASend() асинхронные вызовы не используются ни в одном из браузеров который я успел протестировать за время написания данного поста и болванки перехватчика.
Для чего нужен next? Для организации односвязного списка. Логично, что контексты для пар запрос-ответ надо куда то помещать, чтобы они не потерялись. Чтобы не раздувать размер программы и не использовать что-то типа шаблонов STL, мне было проще всего решить задачку для школьной олимпиады и написать реализацию односвязного списка. Как будет лучше вам, смотрите сами.
Не вдаваясь в продробности опишем функции для работы со связным списком контекстов запрос-ответ (детали смотрите в исходниках):
Далее мы пишем общий обработчик для всех функций send():
И общий обработчик для всех recv():
Телемаркет. Поясню для тех кто еще догоняет. При отправке пакетов проверяется их содержание на предмет наличия ’GET’ в начале. Если это HTTP GET то HTTP заголовок вырезается и сохраняется в контексте открытого сокета. Всякий прием пакетов соответственно проверяет их содержание на предмет наличия ’HTTP’ и если это ответ, то мы пытаемся найти ранее отправленный GET из контекста сокета. Если запрос и ответ найдены то можно их анализировать далее. В данном примере мы просто их сбрасываем в лог файл по адресу %TEMP%\<Имя процесса.exe>-<Идентификатор процесса>.log
Осталось разобраться с асинхронными функциями. Итак, на примере WSARecv():
То есть если вызов асинхронный, то мы находим контекст сокета и прописываем туда указатель на буфер. Потом мы его извлекаем и передаем все в общий обработчик всех функций типа read():
В заключение эпопеи мы пропишем обработчик closesocket() дабы прибивать ненужные контексты, если допустим сокет сдох до получения ответа. Полируем, компилируем, запускаем, запускаем браузер, идем на YouTube…
И вот какой интересный пакет мы выловили из Google Chrome на попытку посмотреть видео по пресловутому адресу www.youtube.com/watch?v=o78nFVB1tJA (выдернуто напрямую из лога):
Ну и собственно видеопоток там начинается сразу после последней строки HTTP заголовка.
Кто-то после этого еще сомневается, можно ли написать перехватчик потокового видео без драйверов или нет? Лично я думаю что задача принципиально выполнена, поэтому переходим к выводам.
Перехватчик работает что мы и продемонстрировали. Для нормальной его реализации скорее всего придется писать некий IPC для общения между множеством перехватчиков и основным приложением-инжектором. Есть несколько вариантов на выбор:
Еще один момент. До сих пор мы работали только с YouTube / Flash Video. Для других сайтов и видео кодеков будут другие особенности. Тем не менее я более чем уверен что в 90% случаев можно перехватывать мультимедиа потоки чисто ориентируясь на содержимое заголовка ”Content-type”.
Недостатки данной реализации:
Что еще? Два аспекта:
Надеюсть что данный пост кому-то будет интересен.
UPD: Написана вторая часть про ловушки без постоянного переписывания кода.
С уважением,
//st
Краткое изучение софтины показало что она устанавливет NDIS Miniport драйвер, который конкретно в моей системе стал умирать при загрузке. «Нафига такие сложности?», подумал я и решил поэкспериментировать с реализацией перехвата потокового видео из браузера без всяких драйверов.
Предисловие
Данный опус предполагает некое знание Windows, WinAPI и немного C++, поэтому если какие то очевидные для меня моменты требуют более подробного разъяснения, то спрашивайте. Сразу внесу ясность, готовой программы для перехвата видео, построенной на принципах изложенных в этом посте, не существует (по крайней мере я ничего такого не писал). Существуют некие заготовки и теоретические измышления, в основном мотивированные как антагонисты решению с NDIS Miniport драйвером и голубыми экранами.
Итак, если гипотетически предположить что у нас есть некий модуль способный перехватывать HTTP или TCP/IP пакеты из браузера, то как именно мы будем ловить видео? Есть два варианта:
- Анализировать адреса в виде URL.
Для этого потребуется перехватывать исходящие пакеты содержащие HTTP GET, и смотреть куда именно этот GET направлен. Решение достаточно сомнительное, поскольку требует специфических знаний о конкретном сайте. С другой стороны адреса подходящие под шаблон типа «www.youtube.com/watch?v=o78nFVB1tJA» позволят отфильтровать запросы именно потокового видео непосредственно ДО получения потока. - Проверять ответы с сервера
Для этого потребуется перехватывать входящие пакеты и проверять их HTTP заголовки на предмет Content-Type. Очевидно что для видео они будут специфичны для конкретных форматов. Например для вышеупомянутой ссылки на Flash Video от YouTube,где я играю на саксофоне,ответ сервера будет содержать заголовок «Content-Type: video/x-flv», что однозначно сообщает нам о том что за заголовком пойдет Flash Video. В случае с MPEG4 заголовок будет содержать video/mp4 ну и так далее
Конечное решение наверняка потребует комбинации 1) и 2) для эффективной работы, но в этом посте мы для начала сосредоточимся на перехвате пакетов.
Ловушки и внедрение DLL
Первое что приходит на ум для написания перехватчика, это внедрение DLL в процесс браузера. Некоторые в этом месте прекратят читать, потому что дальше все понятно. Кому все понятно могут скачать исходники тут и скомпилированную версию вот тут (да, всего 3Кб). Если вы решили попробовать как все это работает, то настоятельно рекомендую взять 32-битный браузер и вырубить весь софт который использует похожие трюки, например AdMuncher (привет Murray & shannow!), потому что данный пример плевать хотел на корректное сожительство с таким софтом (это исправимо). Результаты работы ищите в виде .log файлов в %TEMP%.
Для всех остальных придется кто-что пояснить, хотя системные ловушки и внедрение DLL в процессы Windows это достаточно избитая тема. Конечный результат внедрения своей DLL в процесс браузера будет выглядеть примерно так:
То есть внутри каждого процесса браузера будет сидеть наша самописная DLL, которая и будет перехватывать нужные пакеты. Сразу возникает два вопроса
- Как внедрить DLL?
- Как ловить пакеты?
Попробуем ответить на них ниже…
Внедрение DLL в чужой процесс
На первой стадии очевидно что нам необходимо написать два модуля – главное приложение и DLL. Главное приложение будет внедрять, а DLL соответственно внедряться. Не мудрствуя лукаво запустим Vistual Studio и сразу напишем главное приложение (Injector.cpp):
#pragma comment(linker, "/entry:WinMain /nodefaultlib")
void APIENTRY winMain()
{
HMODULE interceptor = LoadLibrary(TEXT("Interceptor.dll"));
if (interceptor != NULL)
{
HOOKPROC cbtHook = (HOOKPROC) GetProcAddress(interceptor, (LPCSTR) 1);
HHOOK hHook = (HHOOK) SetWindowsHookEx(WH_CBT, cbtHook, interceptor, 0);
if (hHook != NULL)
{
MessageBox(NULL,
TEXT("Press OK to terminate."),
TEXT("Interceptor is working."),
MB_OK);
UnhookWindowsHookEx(hHook);
}
FreeLibrary(interceptor);
}
}
Что делает вышеописанный код? В первой строке из соображений компактности кода мы ставим точку входа в приложения прямо в WinMain(), без прелюдий. В исходниках я вообще отрезал MSVCRT за ненадобностью.
Далее, мы загружаем наш перехватчик, находим в нем экспортную функцию под номером 1 (import by ordinal) и ставим глобальную ловушку типа CBT, используя созданые параметры. Потом мы просто выводим модальное сообщение нажать кнопку ”OK” для завершения и уходим в астрал. Все. Этого достаточно для внедрения DLL во все процессы, которые так или иначе используют User32 WinAPI для работы с окнами.
CBT, это сокращение от Computer-Based Training. Ловушка WH_CBT вообще хороша тем что ”… Система вызывает данную ловушку перед активацией, созданием, уничтожением, минимизацией, максимизацией, перемещением или изменением размера окна. А таже перед завершением системной команды, перед удалением события клавиатуры или мышки из очереди сообщений, перед установкой фокуса ввода и перед синхронизацией с системной очередью сообщений...” Такой вот вольный перевод MSDN. На деле это означает что она сработает для 99% приложений которые написаны в соответствии со стандартной оконной архитектурой.
Прелесть данного метода в том что на самом деле нам не надо будет возиться с системой ловушек Windows как таковой.
Начинаем писать DLL
Поскольку мы пишем перехватчик, а не что-то еще, ему будет достаточно того что:
- Приложение проинициализрует DLL при загрузке в процесс
- DLL останется в адресном пространстве процесса до его завершения
- По завершению или по внешнему событию приложение деинициализирует DLL перед выгрузкой
Стоит сказать что данная техника требует соответствия модулей по разрядности. Это означает что 64-битный браузер будет нуждаться в 64-битной DLL-перехватчике, то же самое касается 32-битных приложений. Не стоит ждать что 32-битный перехватчик будет загружен в 64-битное приложение и тем более наоборот.
Итак, напишем скелет будущего перехватчика(Interceptor.cpp):
HINSTANCE g_hDllInstance;
// Единственный экспорт, для установки глобальной ловушки
LRESULT CALLBACK CBT_Hook(int nCode, WPARAM wParam, LPARAM lParam)
{
return 0;
}
// Инициализация перехватчика при его загрузке в процесс
BOOL onLoad()
{
return TRUE;
}
// Деинициализация перехватчика при его выгрузке из процесса
BOOL onUnload()
{
return TRUE;
}
BOOL WINAPI DllMain(HINSTANCE hDllInstance, DWORD dwReason, LPVOID lpRsrv)
{
switch(dwReason)
{
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls(hDllInstance);
g_hDllInstance = hDllInstance;
return onDllLoad();
break;
case DLL_PROCESS_DETACH:
return onDllUnload();
break;
default:
break;
}
return TRUE;
}
Кроме того нам надо указать экспорт под номером 1. Для этого мы пишем стандартный DEF файл (Interceptor.def) и не забываем скормить его линковщику через параметр /DEF:
LIBRARY Intercept
EXPORTS
CBT_Hook @1
Все. Теперь DLL у нас подклеивается к процессам и сидит в них до завершения. Для того чтобы нам не внедряться в ненужные процессы и корректно себя вести внутри главного приложения (да-да, ведь оно же загружает и инициализирует DLL) сделаем дополнительную проверку:
const char *appsToIntercept[] = {
"chrome.exe",
"iexplore.exe",
"opera.exe",
"firefox.exe",
"safari.exe",
0};
char thisProcessPath[MAX_PATH], *thisProcessName;
char thisDllPath[MAX_PATH], *thisDllName;
BOOL onLoad()
{
BOOL rv = FALSE;
// Получим полный путь к процессу в thisProcessPath и имя процесса в thisProcessName
GetModuleFileName(NULL, thisProcessPath, sizeof(thisProcessPath) - 1);
GetFullPathName(thisProcessPath, sizeof(thisProcessPath),
thisProcessPath, &thisProcessName);
*(TCHAR*) ((TCHAR*) (thisProcessName - sizeof(TCHAR))) = 0;
// Получим полный путь к DLL в thisDllPath и имя DLL в thisDllName
GetModuleFileName(g_hDllInstance, thisDllPath, sizeof(thisDllPath) - 1);
GetFullPathName(thisDllPath, sizeof(thisDllPath),
thisDllPath, &thisDllName);
*(TCHAR*) ((TCHAR*) (thisDllName - sizeof(TCHAR))) = 0;
// Если нас загрузили из главного приложения, то прикинемся что все в порядке
if (!lstrcmpi(thisProcessPath, thisDllPath))
return TRUE;
// Проверяем в какой процесс нас хотят загрузить
for (int i = 0; appsToIntercept[i] != 0; i++)
{
if (!lstrcmpi(thisProcessName, appsToIntercept[i])) {
rv = TRUE;
break;
}
}
// Если ни один из процессов нам не подходит по имени, то выгружаемся
if (!rv)
return FALSE;
// Иначе устанавливаем ловушки на WinSock2
return installHooks();
}
Таким образом, если приложение нам неизвестно, то мы в него не грузимся. Теперь займемся непосредственно перехватом функций WinSock.
Механизм перехвата функций
Для начала нужно пояснить очевидные вещи. Поскольку те кому все уже и так ясно до этой строчки все равно не дочитают, то сделаю отступ. К моему изумлению я понял что некоторые программисты, весьма умные и продвинутые, не всегда четко представляют себе как работают системные библиотеки в процессах Windows. В связи с этим настоятельно рекомендую просмотреть другой пост на Хабре – ”Пошаговое руководство к исполняемым файлам (EXE) Windows” .
Важно понимать вот что:
Все DLL библиотеки, включая системные и те что указаны в PE заголовке как импорты, загружаются непосредственно в адресное пространство приложения которое их использует. С логической точки зрения каждое запущенное приложение имеет свой индивидуальный набор копий системных и прочих DLL.
Таким образом, самым простым способом перехватить пакеты в браузере будет перехватить вызов определенных функций в системной библиотеке, отвечающих за отправку и прием пакетов. На этом месте некоторые опять прекратят читать, потому что все опять понятно и ничего нового, с чем я полностью согласен. Но для всех остальных продолжу.
Перехват можно сделать на разных уровнях: WinHTTP, WinINet, WinSock. Для меня наиболее универсальным представляется перехват функций WinSock из библиотеки WS2_32.DLL. Он имеет свои недостатки, особенно при работе с HTTPS, где пакеты шифруются. Для HTTPS, с моей точки зрения, наилучшим решением будет перехват функций WinHTTP и/или библиотек OpenSSL. Но начнем с простого.
Итак, выделим основные моменты того что нам нужно сделать:
- Определить адрес перехватываемой функции
- Переписать вызов функции в точке входа так чтобы вызывался собственный обработчик
- В собственном обработчике выполнить некие действия до вызова изначальной функции
- Вызвать изначальную функцию
- Сохранить результат
- В собственном обработчике выполнить некие действия после вызова изначальной функции
- Вернуть результат в процедуру вызова
Согласно древним традициям обратной совместимости в Windows есть несколько способов сделать одно и то же путем вызова разных функций, поэтому будем стараться отловить их все. Для нашей задачи должно хватить перехвата следующих из WS2_32:
- send()
- WSASend()
- recv()
- WSARecv()
- WSAGetOverlappedResult()
- connect()
- WSAConnect()
- closesocket()
Причем три последних нужны исключительно в целях создания и уничтожения контекста привязанного к конкретному соединению, если оное вообще нужно. В данном примере я постараюсь избавиться от контекста вообще. Однако в реальности скорее он будет нужен, чтобы правильно собирать пары HTTP запрос — HTTP ответ. При этом перехват connect() и WSAConnect() строго говоря не обязателен, так как новый контекст для нового сокета может быть создан де-факто при первой записи в него.
Итак, как заведем в нашей DLL структуру чтобы переписывать и восстанавливать точку входа в функциях WinSock:
// Структура для управления перехватчиком функции
typedef struct _APIHOOK {
BOOL isInstalled; // Установлен ли перехватчик функции?
const TCHAR *moduleName; // Имя модуля (библиотеки)
const TCHAR *functionName; // Название перехваченной функции
LPVOID newAddr; // Адрес собственного обработчика
LPVOID oldAddr; // Адрес перехваченной функции
DWORD oldCodeSize; // Размер старого кода в байтах
char newCode[HOOK_CODE_SIZE]; // Код перехвата
char oldCode[HOOK_CODE_SIZE]; // Изначальный код
} APIHOOK, *PAPIHOOK;
Про то что такое HOOK_CODE_SIZE и от чего он зависит читаем дальше.
Немного ассемблера
Чтобы перехватывать вызов функции в точке входа нам придется патчить код. Итак, самый простой алгоритм будет такой:
- Определить свой обработчик.
При этом тип вызова cdecl или stdcall, а так же все входные параметры должны быть точно такими же как и в изначальной функции, иначе мы запортачим стек. - Определить точку входа в интересующую нас функцию
С этим все просто, нужно вызвать GetProcAddress() из kernel32.dll. - Сохранить код из точки входа в функцию
Тут тоже все просто – побайтно скопировать в укромное место - Пропатчить точку входа
Грубо говоря все сводится к переписыванию кода в точке входа так чтобы при вызове оной происходил переход на наш обработчик
Есть несколько разных методов перехвата, с функциональной точки зрения. Самый простой метод, это постоянно переписывать код в самом начале точки входа с изначального на свой и обратно (при вызове изначальной функции). Есть более сложные методы при которых не надо постоянно переписывать код, но которые требуют написания анализатора инструкций, дабы правильно внедрить свой перехватчик в середину изначальной функции. Остановимся пока на самом простом – перезапись в начале кода.
Опять же есть несколько способов вызвать свой обработчик. Не углубляясь в детали выделю два из них: безусловный переход и возврат по стеку вызова. В первом случае концепция такова:
MyFuncHandler:
<blablablablabla>
OriginalFunction:
JMP MyFunсHandler
Это предельно просто и в 32-битном выражении требует 5 байт, один для кода инструкции безусловного перехода JMP и четыре для относительного адреса. Почему относительного, позже. Во втором случае концепция немного другая:
MyFuncHandler:
<blablablablabla>
OriginalFunction:
PUSH MyFuncHandler
RETN
Это требует 6 байт — два байта на коды инструкций PUSH <32-bit DWORD> и RETN и четыре для абсолютного адреса. Да-да. В первом случае адрес считается относительно текущего адреса исполнимого кода. Во втором он постоянен и считается относительно начала адресного пространства. Я пойду первым методом.
Пишем установщик перехватчика:
// Размер кода ловушки
#define HOOK_CODE_SIZE 5 // JMP XX XX XX XX
//Установка ловушки на функцию
BOOL hookInstall(PAPIHOOK thisHook)
{
UCHAR asmJMP = 0xE9;
if (!thisHook
|| thisHook->isInstalled == TRUE) {
SetLastError(ERROR_ALREADY_EXISTS);
return FALSE; // Ежели уже установлено, то не надо и мучаться
}
// Определяем адрес нужной импортированной функции
if (thisHook->moduleName
&& thisHook->functionName
&& !(thisHook->oldAddr = GetProcAddress(
GetModuleHandle(thisHook->moduleName),
thisHook->functionName)
)
) {
SetLastError(ERROR_NOT_FOUND);
return FALSE; // Неправильное имя модуля, функции или импорт не найден
}
// Проверяем можно ли оттуда читать
if (IsBadReadPtr(thisHook->oldAddr, HOOK_CODE_SIZE)) {
SetLastError(ERROR_INVALID_ADDRESS);
return FALSE; // Адрес нечитаем
}
// Проверяем а не пропатчена ли точка входа на тот же обработчик
if ( *(DWORD*)((PBYTE) thisHook->oldAddr + 1) ==
((DWORD) thisHook->newAddr - (DWORD) thisHook->oldAddr - HOOK_CODE_SIZE)
&& *(BYTE*) thisHook->oldAddr == asmJMP) {
return TRUE; // Если переход указывает на тот же адрес, сделаем вид что установились
}
// Снимаем запрет на запись и проверяем можно ли туда писать
DWORD oldFlags;
if (!VirtualProtect(thisHook->oldAddr, HOOK_CODE_SIZE, PAGE_EXECUTE_READWRITE, &oldFlags)
|| IsBadWritePtr(thisHook->oldAddr, HOOK_CODE_SIZE)) {
SetLastError(ERROR_WRITE_PROTECT);
return FALSE; // Невозможно переписать точку входа
}
// Сохраняем старый код в укромном месте
memcpy(thisHook->oldCode, thisHook->oldAddr, HOOK_CODE_SIZE);
// Пишем JMP
thisHook->newCode[0] = asmJMP;
// Пишем относительный адрес
*(DWORD *) &thisHook->newCode[1] =
((DWORD) thisHook->newAddr - HOOK_CODE_SIZE - (DWORD) thisHook->oldAddr);
// Все готово для перехвата
thisHook->isInstalled = TRUE;
// Макрос для включения перехвата функции
#define hookEnable(p) memcpy(p->oldAddr, p->newCode, HOOK_CODE_SIZE);
// Макрос для выключения перехвата функции
#define hookDisable(p) memcpy(p->oldAddr, p->oldCode, HOOK_CODE_SIZE);
// Включаем перехват
hookEnable(thisHook);
return TRUE;
}
Не думаю что вышеописаный код нуждается в дополнительном пояснении. Стоит заметить, что патченый код мы вначале формируем в структуре, отвечающей за содержание информации о перехватчике функций, а непосредственно патч мы делаем макросами посредством memcpy(). Любители эстетики могут добавить туда lock, но на мой взгляд это лишнее, догадайтесь почему?
Чтобы включить ловушку, мы копируем новый код, содержащий только переход на адрес собственного обработчика. Чтобы ловушку выключить мы восстанавливаем 5 изначальных байт, сохраненных в массиве под именем oldCode.
Поскольку установку ловушки на функцию мы написали, стоит написать и восстановитель изначального состояния кода функции:
// Снятие ловушки с функции
BOOL hookRemove(PAPIHOOK thisHook)
{
// Если не установлен, то и спросу нет
if (!thisHook->isInstalled)
return FALSE;
// Восстанавливаем изначальное состояние
hookDisable(thisHook);
// Маркируем перехватчик как не установленный
thisHook->isInstalled = FALSE;
// Чистим код перехвата и сохраненный код функции
thisHook->newAddr = (LPVOID) NULL;
thisHook->oldAddr = (LPVOID) NULL;
return TRUE;
}
Теперь, когда мы умеем внедряться в процесс, ставить, убирать, включать и выключать ловушки, самое время заняться собственными обработчиками.
Собственные обработчики функций WinSock
Итак, для начала определим массив прототипов перехватываемых функций. Как указано выше, попробуем перехватывать только самые необходимые функции:
// Макрос для объявления перехваченных функции
#define DECLARE_HOOK(module, name) {0, module, #name, my_##name}
// Массив перехваченных функций и собственных обработчиков
APIHOOK hookList[] = {
DECLARE_HOOK(winSockDll, send),
DECLARE_HOOK(winSockDll, WSASend),
DECLARE_HOOK(winSockDll, recv),
DECLARE_HOOK(winSockDll, WSARecv),
DECLARE_HOOK(winSockDll, WSAGetOverlappedResult),
DECLARE_HOOK(winSockDll, closesocket),
0};
Как видно из макроса, каждой системной функции с абстрактным именем name сответствует собственный обработчик под именем my_name. Теперь надо определить обработчики указаные в массиве. Сделаем это на примере send():
int WSAAPI my_send(SOCKET s, char *buf, int len, int flags)
{
PAPIHOOK thisHook = hookFind(my_send);
if (NULL == thisHook)
return (int) 0;
hookDisable(thisHook);
int rv;
rv = send(s, buf, len, flags);
hookEnable(thisHook);
return rv;
}
Так будет выглядеть пустая обертка системной функции, которая ничего не делает кроме того что вызывает изначальный код. Поскольку обертка по сути одна и та же для всех собственных обработчиков, то имеет смысл сделать пару макросов ради удобства определения оных:
// Удобный макрос для определения своего обработчика функции
#define DEFINE_HOOK(RTYPE, CTYPE, NAME, ARGS)\
RTYPE CTYPE my_##NAME ##ARGS \
{ \
PAPIHOOK thisHook = hookFind(my_##NAME); \
if (NULL == thisHook) \
return (RTYPE) 0; \
hookDisable(thisHook); \
RTYPE rv;
и для выхода из функции тоже:
// Удобный макрос для выхода из обработчика фукции
#define LEAVE_HOOK() } \
hookEnable(thisHook); \
return rv;
Далее мы используем эти макросы, чтобы определить оставшиеся ловушки из массива.
Установка ловушек
Последняя строчка в нашем OnLoad() вызывает некую магическую функцию InstallHooks(). Поскольку все составляющие решения у нас налицо, напишем пакетную установку всех определенных ловушек:
// Установка всех ловушек из массива
BOOL installHooks()
{
BOOL rv = FALSE;
for (int i = 0; hookList[i].moduleName; i++)
{
if (hookInstall(&hookList[i]))
rv = TRUE;
}
return rv;
}
Вот так лаконично. И пакетное удаление ловушек не менее лаконичное:
// Удаление всех установленных ранее ловушек
BOOL removeHooks()
{
BOOL rv = FALSE;
for (int i = 0; hookList[i].moduleName; i++)
{
if (hookRemove(&hookList[i]))
rv = TRUE;
}
return rv;
}
Перехват HTTP пакетов
Ну вот мы плавно подобрались к самому интересному. Итак, у нас два типа пакетов – HTTP запросы и HTTP ответы. Соответственно, первые отправляются функциями типа send(), вторые принимаются функциями типа recv(). Функции отправки надо перехватывать ДО вызова изначального кода, пока буфер отправки еще девственно чист. Функции приема, соответственно надо перехватывать ПОСЛЕ исполнения изначального кода, иначе не увидим что именно принято.
Есть еще асинхронные функции. Идея там простая. При вызове WSASend() или WSARecv() задается структура WSAOVERLAPPED в которой прописывается Event. Асинхронные функции завершаются мгновенно, и по завершению выдают SOCKET_ERROR с GetLastError() установленным в WSA_IO_PENDING. Далее основное приложение ждет события Event любым способом, например WaitForSingleObject(), и как только статус события установлен, то приложение дочитывает буфер через WSAGetOverlappedResult().
Если снять данные с синхронных функций не составляет труда, то с асинхронными придется немного повозиться. Вначале поста я упоминал что полностью от контекстов избавиться не получится, а асинхронные операции это именно то почему. Более детально. Вызов WSAGetOverlappedResult() не несет в себе никакой информации о буфере отправки или приема. Поэтому очевидно что нужно создавать контекст и хранить указатель на буфер там.
Есть еще одна причина по которой нужен контекст. Поскольку для нашей задачи, перехвата поточного видео, требуются и HTTP запросы и ответы, то наиболее логично решение которое будет собирать разрозненные вызовы send(), recv() в пары. Итак, заведем структуру для контекста, который будет пригоден и для сбора пар HTTP запросов и ответов, и для работы с
асинхронными функциями:
struct REQUEST {
SOCKET socket;
char *request;
LPWSABUF wsaBuf;
PREQUEST next;
}
Для чего нужно все вышеозначенное? По номеру сокета socket мы будем определять соответствие запроса и ответа. То есть основное приложение отправляет и получает запрос по одному и тому же TCP сокету, иначе быть не может. Указатель request будет ссылаться на HTTP запрос. Указатель LPWSABUF будет использоваться в случае асинхронных функций. То бишь при вызове WSASend()/WSARecv() мы указатель на буфер будем сохранять, а при завершении его WSAGetOverlappedResult() мы его оттуда будем вынимать. Опять же соответствие определяется через номер сокета.
Забегая вперед скажу что для WSASend() асинхронные вызовы не используются ни в одном из браузеров который я успел протестировать за время написания данного поста и болванки перехватчика.
Для чего нужен next? Для организации односвязного списка. Логично, что контексты для пар запрос-ответ надо куда то помещать, чтобы они не потерялись. Чтобы не раздувать размер программы и не использовать что-то типа шаблонов STL, мне было проще всего решить задачку для школьной олимпиады и написать реализацию односвязного списка. Как будет лучше вам, смотрите сами.
Не вдаваясь в продробности опишем функции для работы со связным списком контекстов запрос-ответ (детали смотрите в исходниках):
// Находим запрос по номеру сокета
PREQUEST findRequest(SOCKET s);
// Добавляем пару сокет - запрос в начало списка
PREQUEST addRequest(SOCKET s, char *request);
// Удаляем запрос из списка
void delRequest(SOCKET s);
Далее мы пишем общий обработчик для всех функций send():
// Добавляем отправляемый пакет в очередь
BOOL commonSendHandler(PAPIHOOK thisHook, SOCKET s, char *buf, int len, BOOL isWsa)
{
// Проверяем валидность параметров и наличие 'GET ' в начале пакета
if …
// Вырезаем HTTP заголовок
char *request = getHttpHeaders((const char *) buf, len);
if (request != NULL)
addRequest(s, request);
return TRUE;
}
И общий обработчик для всех recv():
// Находим отправленный пакет соответствующий принятому и пишем их в лог
BOOL commonRecvHandler(PAPIHOOK thisHook, SOCKET s, char *buf, int len, BOOL isWsa)
{
// Проверяем валидность параметров и наличие 'HTTP' в начале пакета
if …
// Находим запрос, соответствующий данному ответу
PREQUEST req = findRequest(s);
if (NULL == req)
return FALSE;
// Вырезаем HTTP заголовок
char *response = getHttpHeaders((const char *) buf, len);
if (response != NULL)
{
// Пишем запрос и ответ в лог
...
delRequest(s);
}
return TRUE;
}
Телемаркет. Поясню для тех кто еще догоняет. При отправке пакетов проверяется их содержание на предмет наличия ’GET’ в начале. Если это HTTP GET то HTTP заголовок вырезается и сохраняется в контексте открытого сокета. Всякий прием пакетов соответственно проверяет их содержание на предмет наличия ’HTTP’ и если это ответ, то мы пытаемся найти ранее отправленный GET из контекста сокета. Если запрос и ответ найдены то можно их анализировать далее. В данном примере мы просто их сбрасываем в лог файл по адресу %TEMP%\<Имя процесса.exe>-<Идентификатор процесса>.log
Осталось разобраться с асинхронными функциями. Итак, на примере WSARecv():
// WSARecv()
DEFINE_HOOK(int, WSAAPI, WSARecv, (SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine))
{
rv = WSARecv(s, lpBuffers, dwBufferCount, lpNumberOfBytesRecvd, lpFlags,
lpOverlapped, lpCompletionRoutine);
// Синхронный вызов
if (!rv
&& NULL != lpNumberOfBytesRecvd)
{
commonRecvHandler(thisHook, s, lpBuffers->buf, *lpNumberOfBytesRecvd, TRUE);
} else
// Асинхронный вызов
if (rv == SOCKET_ERROR
&& WSAGetLastError() == WSA_IO_PENDING) {
// Если WSARecv асинхронный, то запоминаем WSA буфер, туда будет писаться ответ
PREQUEST req = findRequest(s);
if (req != NULL)
req->wsaBuf = lpBuffers;
}
LEAVE_HOOK();
}
То есть если вызов асинхронный, то мы находим контекст сокета и прописываем туда указатель на буфер. Потом мы его извлекаем и передаем все в общий обработчик всех функций типа read():
// WSAGetOverlappedResult()
DEFINE_HOOK(BOOL, WSAAPI, WSAGetOverlappedResult, (SOCKET s,
LPWSAOVERLAPPED lpOverlapped, LPDWORD lpcbTransfer, BOOL fWait,
LPDWORD lpdwFlags))
{
rv = WSAGetOverlappedResult(s, lpOverlapped, lpcbTransfer, fWait, lpdwFlags);
if (rv
&& NULL != lpcbTransfer
&& *lpcbTransfer > MIN_HTTP_HEADER_SIZE)
{
// Проверяем, а был ли мальчик?
PREQUEST req = findRequest(s);
if (req != NULL
&& req->wsaBuf != NULL)
commonRecvHandler(thisHook, s, req->wsaBuf->buf, *lpcbTransfer, TRUE);
}
LEAVE_HOOK();
}
В заключение эпопеи мы пропишем обработчик closesocket() дабы прибивать ненужные контексты, если допустим сокет сдох до получения ответа. Полируем, компилируем, запускаем, запускаем браузер, идем на YouTube…
И вот какой интересный пакет мы выловили из Google Chrome на попытку посмотреть видео по пресловутому адресу www.youtube.com/watch?v=o78nFVB1tJA (выдернуто напрямую из лога):
[22:28:48] [SOCKET = 0EB0, REQUEST = 1327 bytes, RESPONSE = 329 bytes]
->GET /videoplayback?algorithm=throttle-factor&burst=40&cp=U0hTS1RRU19OTUNOM19MS1dBOlR1eGNSd1JHRkdy&expire=1346465093&factor=1.25&fexp=926900%2C910103%2C922401%2C920704%2C912806%2C924412%2C913558%2C912706&gcr=fi&id=a3bf27155075b490&ip=91.155.190.10&ipbits=8&itag=34&keepalive=yes&key=yt1&ms=au&mt=1346441292&mv=m&range=13-1781759&signature=7415093589702691B2E46681B2EF24EC370C2F1F.D6D55168E2211687994A3F47D8919AC5470C567D&source=youtube&sparams=algorithm%2Cburst%2Ccp%2Cfactor%2Cgcr%2Cid%2Cip%2Cipbits%2Citag%2Csource%2Cupn%2Cexpire&sver=3&upn=GlJDbjcQ-2w HTTP/1.1
Host: o-o---preferred---elia-hel1---v11---lscache1.c.youtube.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.26 Safari/537.4
Accept: */*
Referer: http://www.youtube.com/watch?v=o78nFVB1tJA
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Cookie: VISITOR_INFO1_LIVE=UxycPwPFJBs; __utma=27069237.1349026492.1343302158.1343302158.1343302158.1; __utmz=27069237.1343302158.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); use_hitbox=d5c5516c3379125f43aa0d495d100d6ddAEAAAAw; recently_watched_video_id_list=697d12b6b10771c1d93bb1bb4cf53148WwEAAABzCwAAAG83OG5GVkIxdEpB; PREF=fv=11.3.31; ACTIVITY=1346441327664
<-HTTP/1.1 200 OK
Last-Modified: Wed, 09 May 2012 00:20:14 GMT
Content-Type: video/x-flv
Date: Fri, 31 Aug 2012 19:28:48 GMT
Expires: Fri, 31 Aug 2012 19:28:48 GMT
Cache-Control: private, max-age=23465
Accept-Ranges: bytes
Content-Length: 1781747
Connection: keep-alive
X-Content-Type-Options: nosniff
Server: gvs 1.0
Ну и собственно видеопоток там начинается сразу после последней строки HTTP заголовка.
Кто-то после этого еще сомневается, можно ли написать перехватчик потокового видео без драйверов или нет? Лично я думаю что задача принципиально выполнена, поэтому переходим к выводам.
Выводы
Перехватчик работает что мы и продемонстрировали. Для нормальной его реализации скорее всего придется писать некий IPC для общения между множеством перехватчиков и основным приложением-инжектором. Есть несколько вариантов на выбор:
- Передавать URL видео из перехватчика в основное приложение и тянуть его уже оттуда.
- Передавать само видео из перехватчика в процессе просмотра. Траффик будет дублироваться через IPC, но это не страшно, так как затраты на прокачку локального траффика не такие существенные.
- Писать поток сразу на диск из перехватчика и информировать основное приложение только о ходе процесса.
Еще один момент. До сих пор мы работали только с YouTube / Flash Video. Для других сайтов и видео кодеков будут другие особенности. Тем не менее я более чем уверен что в 90% случаев можно перехватывать мультимедиа потоки чисто ориентируясь на содержимое заголовка ”Content-type”.
Недостатки данной реализации:
- Постоянное переписывание кода в месте ловушки.
Как я написал выше, есть методы позволяющие не переписывать код при вызове изначальной функции. Однако такие методы требуют уже более детального анализа кода, определения размера инструкций и в ряде случаев дизассемблирования. Если это кому-то реально интересно, то я могу попробовать написать об этом отдельный пост . - Невозможность перехвата HTTPS
Собственно говоря, я также не вижу возможности перехватывать HTTPS и в случае с NDIS Miniport драйвером. Тем не менее данная методика позволяет это сделать на уровне другой библиотеки типа WinHTTP или OpenSSL.
Что еще? Два аспекта:
- Данная методика может использоваться не только для перехвата потоков или реализации сниффероподобных приложений. Например при желании можно написать фильтры для HTTP траффика, обрезку рекламы и так далее. Причем принципиальной разницы какой браузер нет ибо все работает именно на уровне TCP/IP.
- Данная методика работает не только для WinSock. То есть в принципе с незначительными изменениями можно подклеивать перехватчик куда угодно и перехватывать какие угодно функции. Это дает некий простор для действий и полета мысли.
Надеюсть что данный пост кому-то будет интересен.
UPD: Написана вторая часть про ловушки без постоянного переписывания кода.
С уважением,
//st