1 сентября 2012 в 02:15

Перехват видео в браузере или TCP сниффер под Windows на коленке (часть первая)

Однажды, не очень давно, мне порекомендовали фоновую качалку потокового видео под названием Jaksta, которая позволяет записывать потоковое видео на диск прямо во время просмотра YouTube, Facebook видео, GoogleVideo и так далее. В результате ее установки я получил стойкий BSOD при каждой загрузке Windows. Переключившись в Safe Mode я снес нафиг это творение, но возникли вопросы.
Краткое изучение софтины показало что она устанавливет NDIS Miniport драйвер, который конкретно в моей системе стал умирать при загрузке. «Нафига такие сложности?», подумал я и решил поэкспериментировать с реализацией перехвата потокового видео из браузера без всяких драйверов.

Предисловие


Данный опус предполагает некое знание Windows, WinAPI и немного C++, поэтому если какие то очевидные для меня моменты требуют более подробного разъяснения, то спрашивайте. Сразу внесу ясность, готовой программы для перехвата видео, построенной на принципах изложенных в этом посте, не существует (по крайней мере я ничего такого не писал). Существуют некие заготовки и теоретические измышления, в основном мотивированные как антагонисты решению с NDIS Miniport драйвером и голубыми экранами.
Итак, если гипотетически предположить что у нас есть некий модуль способный перехватывать HTTP или TCP/IP пакеты из браузера, то как именно мы будем ловить видео? Есть два варианта:

  1. Анализировать адреса в виде URL.
    Для этого потребуется перехватывать исходящие пакеты содержащие HTTP GET, и смотреть куда именно этот GET направлен. Решение достаточно сомнительное, поскольку требует специфических знаний о конкретном сайте. С другой стороны адреса подходящие под шаблон типа «www.youtube.com/watch?v=o78nFVB1tJA» позволят отфильтровать запросы именно потокового видео непосредственно ДО получения потока.
  2. Проверять ответы с сервера
    Для этого потребуется перехватывать входящие пакеты и проверять их 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 в процесс браузера будет выглядеть примерно так:

image

То есть внутри каждого процесса браузера будет сидеть наша самописная DLL, которая и будет перехватывать нужные пакеты. Сразу возникает два вопроса

  1. Как внедрить DLL?
  2. Как ловить пакеты?

Попробуем ответить на них ниже…

Внедрение 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


Поскольку мы пишем перехватчик, а не что-то еще, ему будет достаточно того что:
  1. Приложение проинициализрует DLL при загрузке в процесс
  2. DLL останется в адресном пространстве процесса до его завершения
  3. По завершению или по внешнему событию приложение деинициализирует 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” .

Важно понимать вот что:

image

Все DLL библиотеки, включая системные и те что указаны в PE заголовке как импорты, загружаются непосредственно в адресное пространство приложения которое их использует. С логической точки зрения каждое запущенное приложение имеет свой индивидуальный набор копий системных и прочих DLL.
Таким образом, самым простым способом перехватить пакеты в браузере будет перехватить вызов определенных функций в системной библиотеке, отвечающих за отправку и прием пакетов. На этом месте некоторые опять прекратят читать, потому что все опять понятно и ничего нового, с чем я полностью согласен. Но для всех остальных продолжу.
Перехват можно сделать на разных уровнях: WinHTTP, WinINet, WinSock. Для меня наиболее универсальным представляется перехват функций WinSock из библиотеки WS2_32.DLL. Он имеет свои недостатки, особенно при работе с HTTPS, где пакеты шифруются. Для HTTPS, с моей точки зрения, наилучшим решением будет перехват функций WinHTTP и/или библиотек OpenSSL. Но начнем с простого.

Итак, выделим основные моменты того что нам нужно сделать:
  1. Определить адрес перехватываемой функции
  2. Переписать вызов функции в точке входа так чтобы вызывался собственный обработчик
  3. В собственном обработчике выполнить некие действия до вызова изначальной функции
  4. Вызвать изначальную функцию
  5. Сохранить результат
  6. В собственном обработчике выполнить некие действия после вызова изначальной функции
  7. Вернуть результат в процедуру вызова

Согласно древним традициям обратной совместимости в 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 и от чего он зависит читаем дальше.

Немного ассемблера


Чтобы перехватывать вызов функции в точке входа нам придется патчить код. Итак, самый простой алгоритм будет такой:
  1. Определить свой обработчик.
    При этом тип вызова cdecl или stdcall, а так же все входные параметры должны быть точно такими же как и в изначальной функции, иначе мы запортачим стек.
  2. Определить точку входа в интересующую нас функцию
    С этим все просто, нужно вызвать GetProcAddress() из kernel32.dll.
  3. Сохранить код из точки входа в функцию
    Тут тоже все просто – побайтно скопировать в укромное место
  4. Пропатчить точку входа
    Грубо говоря все сводится к переписыванию кода в точке входа так чтобы при вызове оной происходил переход на наш обработчик

image

Есть несколько разных методов перехвата, с функциональной точки зрения. Самый простой метод, это постоянно переписывать код в самом начале точки входа с изначального на свой и обратно (при вызове изначальной функции). Есть более сложные методы при которых не надо постоянно переписывать код, но которые требуют написания анализатора инструкций, дабы правильно внедрить свой перехватчик в середину изначальной функции. Остановимся пока на самом простом – перезапись в начале кода.
Опять же есть несколько способов вызвать свой обработчик. Не углубляясь в детали выделю два из них: безусловный переход и возврат по стеку вызова. В первом случае концепция такова:

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 для общения между множеством перехватчиков и основным приложением-инжектором. Есть несколько вариантов на выбор:
  1. Передавать URL видео из перехватчика в основное приложение и тянуть его уже оттуда.
  2. Передавать само видео из перехватчика в процессе просмотра. Траффик будет дублироваться через IPC, но это не страшно, так как затраты на прокачку локального траффика не такие существенные.
  3. Писать поток сразу на диск из перехватчика и информировать основное приложение только о ходе процесса.

Еще один момент. До сих пор мы работали только с YouTube / Flash Video. Для других сайтов и видео кодеков будут другие особенности. Тем не менее я более чем уверен что в 90% случаев можно перехватывать мультимедиа потоки чисто ориентируясь на содержимое заголовка ”Content-type”.
Недостатки данной реализации:
  • Постоянное переписывание кода в месте ловушки.
    Как я написал выше, есть методы позволяющие не переписывать код при вызове изначальной функции. Однако такие методы требуют уже более детального анализа кода, определения размера инструкций и в ряде случаев дизассемблирования. Если это кому-то реально интересно, то я могу попробовать написать об этом отдельный пост .
  • Невозможность перехвата HTTPS
    Собственно говоря, я также не вижу возможности перехватывать HTTPS и в случае с NDIS Miniport драйвером. Тем не менее данная методика позволяет это сделать на уровне другой библиотеки типа WinHTTP или OpenSSL.

Что еще? Два аспекта:

  1. Данная методика может использоваться не только для перехвата потоков или реализации сниффероподобных приложений. Например при желании можно написать фильтры для HTTP траффика, обрезку рекламы и так далее. Причем принципиальной разницы какой браузер нет ибо все работает именно на уровне TCP/IP.
  2. Данная методика работает не только для WinSock. То есть в принципе с незначительными изменениями можно подклеивать перехватчик куда угодно и перехватывать какие угодно функции. Это дает некий простор для действий и полета мысли.

Надеюсть что данный пост кому-то будет интересен.

UPD: Написана вторая часть про ловушки без постоянного переписывания кода.

С уважением,
//st
+79
17944
362
stpark 116,0

Комментарии (38)

+4
XaocCPS #
интересно, как к вашим внедрениям в чужие процессы относятся антивирусы? :-)
+3
stpark #
Microsoft Security Essentials и Symantec молчат. Про других не знаю :) Вообще способ более чем легальный…
+3
AxisPod #
А чего им ругаться, взять тот же PuntoSwitcher, Lingvo, они внедряют свои dll в процессы и ничего, все нормально. Внедрить dll это ничего страшного по сути. Но на это точно ругается Outpost, на любые внедрения.
0
Lux_In_Tenebris #
Совсем никудышние антивирусы, если не ругаются.
+11
Malerok #
Один из самых интересных постов на хабре за последнее время, спасибо.
+1
halyavin #
Для Firefox и Chromium можно скомпилировать свою версию браузера. Преимущество такого способа — можно узнать по дороге много нового о их внутренней структуре. Недостаток — нужно перекомпилировать браузер каждые 6 недель.
+1
stpark #
Строго говорят для них же можно написать расширения, которые не будут иметь проблем с HTTPS например. Примером тому множество расширений для YouTube с возможностью скачивания видео. Да и тот же Adblock работает по схожему принципу, только вместо отдельных пакетов он получает страницу целиком. Но было все же интереснее попробовать более-менее универсальный метод…
0
sergeyv #
А можно где-то подобный софт поиметь? Может подскажете уже готовые реализации перехватчиков потокового видео, которые его отсылают, в идеале, в WMP? И можно смотреть не в браузере, а в плейере, ну и творить с ним что хочешь, например отсылать изображение на экран Smart TV :)
0
stpark #
Честно говоря я не знаю готового софта для этих целей. Написать его с нуля не такая уж и тяжелая работа, тем более что для WMP будет достаточно привинтить YouTube каталоги к DLNA серверу типа Twonky и объявить себя медиа сервером. SmartTV по умолчанию работает с DLNA серверами на ура. Но я пока не готов что-то подобное делать из-за отсутствия времени.
+2
zed91 #
x64?
+1
stpark #
Постараюсь на неделе найти время и все это сделать для x64.
+2
Gariks #
Есть еще мало знакомая в околохакерских кругах технология — LSP (Wnisock SPI).
Используя ее можно избежать использования NDIS драйвера и нормально работать в ring3, без танцев с бубном.
Кстати странно, что создатели Jaksta не знаю про WinPcap.
0
stpark #
Полностью согласен что LSP был бы более корректным методом. Но моей скрытой целью было показать как работает перехват функций DLL в принципе и возиться с написанием сервис провайдера для WinSock не хотелось :)
0
DarthRamone #
WinPcap/libpcap?
Анализ — фильтр — дамп?
+1
vk2 #
1) Собирать вручную tcp/ip сессию — это та еще радость
2) Вообще-то winpcap ставит ndis-драйвер
0
joedm #
1. Да ладно, чего там собирать-то…
2. Этот ndis-драйвер хотя бы в BSOD не выпадает.
+1
vk2 #
1. Возможные ретрансмиты, отсутствие гарантии прихода пакетов в естественном порядке (придется смотреть на sequence number) и так далее. Ничего сверхъестественного, но все же некоторая головная боль.

2. Это да.
0
tangro #
Автор, как давно Вы это всё писали? Вы в курсе, что последний Хром все процессы вкладок и плагинов держит в песочнице с Untrusted integrity level и многие пляски с внедрением DLL-ок и хуками перестали работать?
+3
stpark #
Писал я это все не далее как вчера, 31 августа 2012 года. Проверял в последней версии Google Chrome Beta, SRWare Iron и IE 9. Видимо дело в том что плагины и расширения в данном случае не играют существенной роли, независимо от того где их хром держит…
+5
twr #
Вынужден сильно поругать Ваш метод перехвата, хоть сама реализация и не является главной темой статьи.

1. Самый кошмар конечно в том, что происходит постоянная перезапись кода в контролируемых функциях. Вы задумывались что произойдёт, если какой-то параллельный поток в это же самое время будет выполнять код функции в затираемом месте?

2. Даже если от постоянной перезаписи избавиться, то остаётся ещё узкое место: однократная установка хука. По той же причине, описанной в предыдущем пункте, приложение может вылететь. Для того, чтобы решить данную проблему, Вы можете перечислять потоки и замораживать все, кроме своего, с помощью SuspendThread().

3. Правильная организация перехвата такова: Вы переносите несколько первых инструкций перехватываемой функции в заранее подготовленное место, после последней инструкции добавляете jmp на Ваш код-обработчик (получается эдакий переходник). В конце кода-обработчика jmp на первую незатёртую инструкцию оригинального кода. Ну и в начало перехватываемой функции лепите прыжок на «переходник». Это перехват до выполнения. Перехват после выполнения делается схожим образом, но там мы должны подменить адрес возврата на стеке в нашем «переходнике».

4. Нужно понимать, что не всякую функцию можно перехватить таким образом. К примеру, если перемещаемые инструкции будут содержать прыжки по относительным адресам, то после перемещения такой код перестанет быть рабочим. Благо, почти все WinAPI-функции содержат стандартный пролог в 5-6 байт, который можно смело двигать куда угодно.
0
stpark #
Критика справедливая и принимается. Я не зря написал что перезапись кода это недостаток именно данного примера. В данном посте мне стоило некоторых усилий держаться в рамках научно-популярной статьи и не сорваться в хардкор. К сожалению если бы я стал копать в сторону анализа инструкций, то хардкора было бы не избежать. Уверяю Вас что у меня есть другая реализация, очень похожая на ту что Вы описали.
0
northicewind #
А есть какая-либо возможность с ней ознакомиться? Интересно.
+1
stpark #
Постараюсь на следующей неделе написать апдейт или отдельный пост на эту тему и скомпилить x64, как обещал ранее.
+1
felize #
Годная статья для хабра. stpark, смотрите сорцы хукера зевса, там более корректно выппонено
+1
qw1 #
Лютый overkill.
1. Для записи видео достаточно прописать в браузере свой прокси-сервер и из него сейвить все ответы.
2. Для Firefox API расширений позволяет ловить все HTTP-реквесты и респонсы в своём javascript-фильтре.
Второй способ, в отличие от хуковского и проксёвого, пробивает https.

Видимо, автор не решал задачу сохранения видео, а захотел поиграться с системой.
0
stpark #
Видимо, я вкладываю другой смысл в выражение «перехват из браузера», извините. Другие люди не стесняются писать для этого целый драйвер…
0
A1ex #
Просто любопытно. Почему вы не произвели внедрение через CreateRemoteThread? Мне кажется этот вариант был бы лучше глобального хука.
0
qw1 #
VirtualAllocEx в чужом процессе + WriteProcessMemory + CreateRemoteThread.
Плюсы:
— подгрузить код можно избирательно, а не во все процессы
— можно обойтись без DLL на диске и в списке загруженных DLL процесса
Минусы:
— антивирусы сильнее ругаются на WriteProcessMemory, чем на хуки
— сложнее реализовать

Сложности в том, что внедряемому коду надо настроить relocations на адрес внедрения, настроить импорты из зависимых DLL (из-за ASLR заранее неизвестно, где системные DLL). То есть, реализовать часть DLL-загрузчика.
Если внедряемый код маленький (загрузка DLL и запуск функции оттуда), его настройка упрощается, но теряется второй плюс этого метода.
0
stpark #
Я не стал возиться с CreateRemoteThread() из за соображений наглядности. Глобальный хук это самое простое решение не требующее возни с чужим процессом извне. Чтобы избежать дополнительной возни с релокациями и импортами есть более простое решение. Делается оно так:

1. Открываем процесс на запись:

	HANDLE hp = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | 
							PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, 
							FALSE, <ИДЕНТИФИКАТОР ПРОЦЕССА>);

2. Резервируем кусок памяти для имени подгружаемой DLL:

	void *pLibName = VirtualAllocEx(hp, NULL, MAX_PATH, MEM_COMMIT, PAGE_READWRITE);

3. Пишем туда собственно имя:

	WriteProcessMemory(hp, pLibName, (void *) <ИМЯ DLL>, MAX_PATH, NULL)

4. Делаем трюк с kernel32.dll — он всегда грузится по одному и тому же адресу в разных процессах:
FARPROC pLoadLibrary = GetProcAddress(GetModuleHandle(«Kernel32»), «LoadLibraryA»);
5. Запускаем поток:
HANDLE ht = CreateRemoteThread(hp, NULL, 0, (LPTHREAD_START_ROUTINE) pLoadLibrary,
pLibName, 0, NULL);

После этого ждем выхода и все дела. DLL точно так же прогрузится в чужой процесс и поставит там хуки.
0
qw1 #
Это работало на XP.
В Windows 7 по умолчанию в каждом процессе свой адрес kernel32.dll, поэтому GetModuleHandle(«kernel32») в процессе, выполняющем внедрение, бесполезен
0
stpark #
Ок. Тогда придется-таки копаться в IAT :)
0
A1ex #
И это работает и в Windows 7 (да и в 8 тоже). Может это просто везение, но у меня еще не возникало проблем с этим способом внедрения.
0
twr #
В подавляющем большинстве случаев, в рамках одной сессии, адреса загрузки системных библиотек будут совпадать. К примеру, сейчас, на рабочей Win7 sp0, адрес загрузки kernel32.dll одинаков во всех процессах. Однако, вчера, на компьютере жены с Win7 sp1, я наблюдал различие адресов загрузки user32.dll. Адреса было всего два и они никак не зависели от пользователя, под которым запущен процесс.
+2
twr #
Однако же, получать адрес процедуры в другом процессе начиная с систем, в которых активен ASLR, лучше как-то так (delphi 7):

function GetModuleHandleEx(sLibName: string; dwPID: DWORD): pointer;
var
  me: TMODULEENTRY32;
  hSnap: DWORD;
begin
result := nil;
hSnap := CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID);
if hSnap <> DWORD(INVALID_HANDLE_VALUE) then
  begin
  me.dwSize := SizeOf(TMODULEENTRY32);
  if Module32First(hSnap, me) then
    repeat
      if UpperCase(string(me.szModule)) = UpperCase(sLibName) then
        begin
        result := pointer(me.modBaseAddr);
        break;
        end;
    until not Module32Next(hSnap, me);
  CloseHandle(hSnap);
  end;
end;

{$O-}
function GetProcAddressEx(sLibName, sFuncName: string; dwPID: DWORD): DWORD;
var
  hLib: DWORD;
begin
hLib := GetModuleHandle(PChar(sLibName));
result := DWORD(DWORD(GetProcAddress(hLib, PChar(sFuncName))) -
          hLib + DWORD(GetModuleHandleEx(sLibName, dwPID)));
end;
{$O+}
0
felize #
глобальный хук CreateRemoteThread вроде требует чтобы ZwResumeThread был похукан, дабы внедриться во все процесы
0
twr #
Глобальный хук тут не при чём. Alex имеет ввиду способ, при котором dll или кусок кода внедряется в АП целевого процесса (одного) и на него передаётся управление с помощью CreateRemoteThread().
+1
Mirraz #
Сложно, зато не универсально ))
Во-первых, таким способом без серьёзного перепиливания вы не перехватите RTMP-стрим.
А для остального можно установить AdBlock, прописать нужные правила. Тогда загрузка видеоконтента будет блокироваться и высвечиваться в списке заблокированного AdBlock-ом. Откуда можно просто скопировать ссылку и поставить на закачку.
Например, для youtube подойдёт такое правило:
http://*.youtube.com/videoplayback?*
Для подавляющего большинства остальных видеофайлов:
/^https?\:\/(\/[^\/\?]*){2,}\.(flv|iflv|f4v|mpg|mpeg|wmv|mp3|mp4|m4v|mov)(\?.*)?$/
+1
stpark #
На эту тему можно спорить бесконечно. Начать с того что Ваш фильтр для AdBlock так же не перехватит RTMP и тоже не универсален, потому что это плагин под конкретный браузер. Закончить можно тем что RTMP инкапсулированный в HTTP ловится точно очень даже легко как, за исключением того что помимо GET надо еще отсматривать POST.

А «чистый» RTMP протокол (который на порту 1935 под префиксом rtmp://) технически к браузерам имеет мало отношения и не всегда дружит с проксями. То есть да, его можно перехватить, но не с перепиливанием в смысле рефакторинга, а вообще с дописыванием отдельного обработчика, равно как и для rtp, rtsp, mms и тому подобные. Мой простенький пример даже HTTP-то через пень-колоду обрабатывает — это в целях наглядности.

Попробую повториться, целью поста было показать что перехват пакетов из браузера или чего то там еще, это не такая сложная задача, которая требует написания драйвера как это сделали в Jaksta. Я на нее потратил в общей сложности три часа, поэтому и функционал очень ограниченный.

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