Прочтя заголовок, ты, наверное, ожидаешь, что сейчас тебе во всех подробностях расскажут о работе с сетью в ядре Windows.
Но задача это трудная по двум причинам: во-первых, из-за сложности
темы; а во-вторых, из-за практически полного отсутствия осмысленных
статей на эту тему на русском языке. Но, как говорят китайцы, «дорога в
тысячу ли начинается с первого шага». Мы с тобой начнем ее прямо сейчас.
Строго говоря, в нулевом кольце для программера доступно два интерфейса для работы с сетью: TDI (Transport Data Interface) и NDIS (Network Device Interface Specification).
Считается, что с TDI работать гораздо легче, чем с NDIS, что, впрочем,
и понятно, ведь при работе с NDIS кодеру нужно будет самому
реализовывать стек сетевых протоколов, общаться напрямую с сетевым
адаптером, что нерационально. При работе же с TDI программер опирается
на уже существующую в ядре реализацию TCP/IP-стека и задача его сильно
упрощается.
Итак, TDI. Вообще-то, изначально он создавался для работы в
usermode, однако затем разработчикам Windows что-то пришло в голову, и
они сделали его доступным в режиме ядра. На данный момент TDI
представляет собой набор документированных и не очень структур,
функций, макросов, большинство из которых определено в DDK в
заголовочных файлах <tdi.h> и <tdikrnl.h> (кстати, не
забудь заинклюдить их при сборке драйвера). Рассматриваемый нами
вариант прокатит на всей линейке Windows: от W2k до Vista. Со временем,
судя по сообщениям Microsoft, TDI обречен на вымирание - со следующей
ОС мелкомягкие намерены отказаться от его поддержки. Хотя кто их знает…
говорят одно, делают другое, спецификацию пишут для чего-то вообще
постороннего...
Как можно видеть на рисунке, структурно TDI занимает промежуточное место между реализацией NDIS и WinSock.
Сетевая инфраструктура в ядре
Программная модель TDI очень похожа на модель WinSock – работа
происходит почти аналогично, только с другими функциями, макросами и
структурами. Посредством TDI можно отправлять как TCP-, так и
UDP-пакеты.
Для того чтобы наколбасить драйвер, который будет взаимодействовать с сетью, нам понадобится лишь DDK (без него, как и без прямых рук, тру-низкоуровневому программеру вообще никуда).
Алгоритм драйвера самого примитивного TDI–клиента будет выглядеть примерно так:
- создание дескриптора соединения;
- создание дескриптора локального адреса;
- привязка объекта «соединение» к «локальному адресу»;
- реализация функции соединения с удаленным хостом.
Это минимальный необходимый набор действий, осуществляющих «хандшейк» - «рукопожатие» с удаленным хостом.
Кстати, при написании этой статьи автор предполагал, что читатель
обладает достаточными навыками в создании несложных драйверов и кодинга
на С. Приступим!
Создаем объект «соединение»
Основное, что здесь нам потребуется, - это создать и получить хэндл устройства \\Device\\Tcp через ZwCreateFile, создать пустой объект FileObject и вызовом ObReferenceObjectByHandle связать их вместе.
Предварительно для получения хэндла устройства \\Device\\Tcp нужно
заполнить структуру FILE_FULL_EA_INFORMATION. И все! Смотрим
нижеследующий код (объявление переменных и реализация общих для всех
функций моментов намеренно опущено, потому что журнал не резиновый -
смотри исходник на диске).
NTSTATUS CreateConnection(PHANDLE Handle, PFILE_OBJECT *FileObject)
{
Ea = (PFILE_FULL_EA_INFORMATION)&DataBlock;
Ea->EaNameLength = TDI_CONNECTION_CONTEXT_LENGTH;
Ea->EaValueLength = sizeof(CONNECTION_CONTEXT);
memcpy(Ea->EaName, TdiConnectionContext, Ea->EaNameLength + 1);
*(CONNECTION_CONTEXT*)(Ea->EaName +
(Ea->EaNameLength + 1)) = (CONNECTION_CONTEXT)conn_context;
Status = ZwCreateFile(Handle, FILE_READ_EA | FILE_WRITE_EA, &Attr,
&IoStatus, 0, FILE_ATTRIBUTE_NORMAL, 0,
FILE_OPEN, 0, Ea, sizeof(DataBlock));
return ObReferenceObjectByHandle(*Handle, GENERIC_READ |
GENERIC_WRITE, 0, KernelMode, (PVOID
*)FileObject, 0);
}
Создаем локальный адрес
Фактически здесь происходит то же самое, что и при создании
соединения, но теперь мы дополнительно заполняем структуру
TA_IP_ADDRESS. Ее описание ты легко найдешь в DDK или в Сети. Можно
заполнять поля самостоятельно, как показано ниже (например, поле
sin_port - порт, который будет открыт на локальной машине при установке
соединения), или оставить системе возможность самой назначить номер
порта. При самостоятельном заполнении sin_port можно использовать такой
макрос:
HTONS(a) (((0xFF&a)<<8) + ((0xFF00&a)>>8)).
После выполнения этой функции в системе будет создан объект «локальный адрес»:
NTSTATUS CreateAddress(PHANDLE Handle, PFILE_OBJECT *FileObject)
{
Ea = (PFILE_FULL_EA_INFORMATION)&DataBlock;
memcpy(Ea->EaName, TdiTransportAddress, Ea->EaNameLength + 1);
Ea->EaNameLength = TDI_TRANSPORT_ADDRESS_LENGTH;
Ea->EaValueLength = sizeof (TA_IP_ADDRESS);
Sin = (PTA_IP_ADDRESS)(&Ea->EaName + Ea->EaNameLength + 1);
Sin->TAAddressCount = 1;
Sin->Address[0].AddressLength = TDI_ADDRESS_LENGTH_IP;
Sin->Address[0].AddressType = TDI_ADDRESS_TYPE_IP;
Sin->Address[0].Address[0].sin_port = HTONS(номер_порта_на_локальной_машине);
Sin->Address[0].Address[0].in_addr = 0;
RtlZeroMemory(Sin->Address[0].Address[0].sin_zero,
sizeof Sin->Address[0].Address[0].sin_zero);
Status = ZwCreateFile(Handle, FILE_READ_EA |
FILE_WRITE_EA, &Attr, &IoStatus, 0,
FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN, 0, Ea, sizeof (DataBlock));
return ObReferenceObjectByHandle(*Handle, GENERIC_READ
| GENERIC_WRITE, 0, KernelMode, (PVOID
*)FileObject, 0);
}
Узелок на память...
Теперь необходимо связать оба созданных файловых объекта вместе.
Первое - универсальная функция TdiBuildInternalDeviceControlIrp, в
зависимости от переданных ей параметров (в чем убедимся далее)
создающая IRP-пакет, который передается функции
TdiBuildAssociateAddress. Все дальнейшее взаимодействие будет
происходить именно через этот IRP-пакет, который затем вызовом
IoCallDriver будет передан нижележащему драйверу в стеке.
Вообще, рекомендуется прикрутить ко всему этому эвент на тот случай,
если обработка IRP-пакета попадет в очередь, поскольку Windows - очень
занятая система и, для того чтобы попасть на прием к Его Величеству
Ядру, приходится выстаивать в очередях :). О работе с IRP-пакетами
можно почитать статьи Four-F на wasm.ru.
Связываем созданные объекты вместе:
NTSTATUS Bind(PFILE_OBJECT FileObject, HANDLE Address)
{
DeviceObject = IoGetRelatedDeviceObject(FileObject);
Irp = TdiBuildInternalDeviceControlIrp(TDI_ASSOCIATE_ADDRESS, DeviceObject,
FileObject, &Event, &IoStatus);
TdiBuildAssociateAddress(Irp, DeviceObject, FileObject, 0, 0, Address);
return IoCallDriver(DeviceObject, Irp);
}
Коннектимся...
Главное здесь - заполнить структуру TA_IP_ADDRESS,
которая будет описывать тот удаленный хост, к которому нужно
приконнектиться. При создании локального адреса мы это уже делали,
только теперь поля sin_port и in_addr нужно заполнить вручную. При
заполнении in_addr используй следующий макрос: INETADDR(a, b, c, d) (a
+ (b<<8) + (c<<16) + (d<<24)). Кроме того, нужно
заполнить структуру TDI_CONNECTION_INFORMATION. Непосредственный
коннект реализуется вызовом TdiBuildConnect и уже привычным
IoCallDriver. В остальном все в нижеприведенном коде должно быть
понятно.
Функция соединения с удаленным хостом:
NTSTATUS Connect(PFILE_OBJECT FileObject)
{
DeviceObject = IoGetRelatedDeviceObject(FileObject);
Irp = TdiBuildInternalDeviceControlIrp(TDI_CONNECT,
DeviceObject, FileObject, &Event, &IoStatus);
rem_adr.TAAddressCount = 1;
rem_adr.Address[0].AddressLength = TDI_ADDRESS_LENGTH_IP;
rem_adr.Address[0].AddressType = TDI_ADDRESS_TYPE_IP;
rem_adr.Address[0].Address[0].sin_port = HTONS(номер_порта);
rem_adr.Address[0].Address[0].in_addr = INETADDR(IP-адрес хоста);
RtlZeroMemory(rem_adr.Address[0].Address[0].sin_zero,
sizeof rem_adr.Address[0].Address[0].sin_zero);
remote_node.UserDataLength = 0;
remote_node.UserData = 0;
remote_node.OptionsLength = 0;
remote_node.Options = 0;
remote_node.RemoteAddressLength = sizeof(rem_adr);
remote_node.RemoteAddress = &rem_adr;
TdiBuildConnect(Irp, DeviceObject, FileObject,
0, 0, 0, &remote_node, 0);
return IoCallDriver(DeviceObject, Irp);
}
То, что мы рассмотрели, - это костяк TDI-клиента, который всего лишь
устанавливает соединение с выбранным хостом. Но ведь необходимо еще и
отправлять и получать данные! Для этого потребуется всего лишь
предусмотреть отдельную реализацию функций TdiBuildSend TdiBildRecieve с переданными параметрами TDI_SEND и TDI_RECEIVE соответственно. Что и как они делают, смотри в DDK. Для совсем ленивых на диске
лежит небольшой бонус, в котором можно найти вполне рабочий сорец
драйвера с уже реализованными функциями посылки и получения данных
(если все еще не ясно, пиши мне на мыло - объясню).
Чтобы по возможности избежать тех проблем, с которым сталкиваются начинающие, слушай мои советы.
Первое. При реализации функции получения данных через вызов TDI_RECEIVE нужно предусмотреть возможность получения ВСЕХ данных,
которые нам отправит сервер. Если этого не сделать, размер полученных
данных будет ограничен лишь размером ПРЕДВАРИТЕЛЬНО выделенного буфера.
Иначе говоря, если ты захочешь скачать 2 Мб, а размер буфера равен 0xff
байт, то свои 0xff ты и получишь. Остальное будет обрезано, и процедура
может просто подвиснуть. На мой взгляд, неплохим вариантом будет
динамическое выделение буфера в памяти под размер передаваемых данных,
который можно выдрать из поля Content-Length, которое, в свою очередь,
тебе вернет правильный веб-сервер (и то это при условии, что ты
работаешь именно с веб-сервером). Решение этой проблемы будет твоим
домашним заданием.
Второе. Полученный буфер будет ВРЕМЕННО храниться в ЯДРЕ.
Не пытайся искать скачанное в кэше Internet Explorer или где-то на
диске – это бесперспективно до тех пор, пока ты туда его насильно не
сохранишь. При этом не забывай освобождать память из-под буфера в ядре,
иначе... сам знаешь, чем это может грозить.
Ну и третье. При реализации функции приема данных через вызов TDI_RECEIVE нужно помнить, что, в случае если она не дождется данных от сервера
(мало ли что с удаленным хостом может случиться: заддосят негодяи или
просто админ будет где-то резаться в линейку, забыв о серваке), функция может вернуть вечный STATUS_PENDING,
то есть IRP-пакет будет стоять в очереди до тех пор, пока он не будет
обработан. А как следствие, зависший драйвер. Поэтому в коде
обязательно надо учесть подобное развитие ситуации. Особо продвинутым и
тем, кто хочет досконально разобраться в работе TDI, рекомендую скачать
tdfw-файрвол. Он open source, и его можно свободно найти в Сети.
Итак, мы рассмотрели простейший вариант TDI-клиента, который ничего
особенного не делает. Целью статьи было показать, дорогой читатель, что
работать с сетью в ядре Windows не так уж и сложно. А TDI на самом деле
предоставляет для этого кучу возможностей: там и TDI_LISTEN, и
TDI_ACCEPT, и много прочих вкусностей... MSDN и журнал «Хакер» помогут
тебе! И помни: дорогу осилит идущий...
Злоключение
«Зачем все это нужно, ведь можно спокойно юзать библиотеки WinSock в
user mode и при минимальных затратах решать поставленные задачи по
работе с сетью?» - спросит читатель. Как однажды сказал мой хороший
друг, «то, что ты написал, в C# можно уложить в 5 строк». Может быть.
Но если обратить внимание, можно заметить, что разработчики сетевых
решений безопасности (имеются в виду монстры типа Outpost Firewall) все
настойчивее стремятся к контролю за действиями пользователя на уровне
ядра. Реализовывать файрволы в user mode уже давно моветон. Работа с ядром, прямые манипуляции с объектами ядра - стандарт де-факто,
и «Хакер» об этом писал уже неоднократно. А чем мы хуже? Тем более,
почувствовав вкус ядра и пощупав его своими шаловливыми ручками, ты уже
ни за что не захочешь возвращаться в user mode... Ring0 - суровая среда
обитания, в которой выживают только самые настоящие брутальные
падаваны, но... способных учеников ядро награждает щедрыми дарами!
INFO
Крайне желателен для посещения tarasc0.blogspot.com,
чувак нереально много знает о работе с сетью в ring0. Достойная для
прочтения статья по кодингу TDI в ядре «Kernel mode sockets library for
the masses» лежит здесь: http://rootkit.com/newsread.php?newsid=416.
Про wasm.ru, codeproject.com, ntkernel.com, MSDN, google.com/codesearch и koders.com я уж промолчу :).
Для отладки драйвера обязательно нужен будет отладчик ядерного уровня типа SoftICE или WinDBG,
иначе на первых порах искать ошибки в коде будет сложновато. И
обязательно раздобудь продвинутый анализатор сетевых пакетов для
контроля за устанавливаемыми соединениями и анализа содержимого пакетов.
Осторожнее в работе с ядром! Грубые ошибки неминуемо ведут к BSOD'у, стрессу и смерти нервных клеток!
|