Cетевое хозяйство лежит на балансе виндовых библиотек в числе которых и wsock32.dll. Эта устаревшая либа пришла в наш век из прошлого и опирается на сокеты Беркли юникса – версия её WinSock v1.1. Начиная с Win-2000 мелкософт отошла от старых принципов, и полностью обновила библиотеку до версии 2.0, поместив новоиспечённые функции внутрь отдельной либы ws2_32.dll. Прежняя версия тоже осталась для совместимости с уже написанными на ней программами, только все дорожки из неё ведут опять к усовершенствованной ws2_32.dll. Проверить это можно в Hiew'e, запросив у него экспорт последовательностью
Enter->F8->F9:
Таким образом, мы можем с чистой совестью вызывать старые функции, тем-более что некоторые из них wsock32 исполняет всё-таки сама – это getsockopt(), recv(), recvfrom() и т.д. на скрине выше. Однако в новой либе есть и функции, которых нет в предыдущей версии. В этом случае придётся звать их напрямую из ws2_32.dll - они начинаются с приставки WSA_xxx().
Под большим системным замком, в подвале сетевой архитектуры трудятся ещё и драйвера, во-главе которых стоит дров интерфейса с сетевым адаптером NDIS.SYS, и драйвер транспортного протокола TDI. Грязными красками иерархию программной части сети можно расписать так:
На рисунке выше видно, что сетевые сокеты не вмешиваются в работу уровня приложений. В их задачи входит принять пакет-данных от софта и обернув его в красивый фантик, отправить получателю на другом конце связи. Сокеты – анархисты. Они не зависят ни от какого протокола, а наоборот подминают их под себя. При создании сокета, в его параметрах мы просто указываем уровень, на котором примем пакет отправителя. Например если в аргументе указать PROTO_IP, то пакет попадёт в наши руки уже после уровня TCP (или UDP). За это время протокол TCP уже мог что-нибудь сотворить с пакетом, и к нам он попадёт уже не "в чём мать родила".
С другой стороны, мы можем создать IP-сокет, который будет отправлять пакетики в сеть сразу с уровня IP. В этом случае TCP уже останется за бортом, и даже не будет знать о наших действиях. Другими словами, непосредственно из прикладной программы у нас имеется легальный доступ к любому уровню стека протоколов TCP/IP. Это открывает большие возможности. Что-то подобное делает например PING, доставая клиентов своими ICMP эхо-запросами.
Если-же мы хотим шифровать исходящий трафик, то делать это лучше на самом-верхнем уровне приложений, ..желательно вообще до стека протоколов, перехватывая непосредственно клавиатурный ввод юзера. А так.. ксорить траф можно на любом из уровней. Только если на другом конце связи его во-время не расшифровать, то из-за несовпадения контрольной суммы протоколы следующих уровней начнут просто дропать такие пакеты, отправляя их к праотцам. То-есть мы должны писать сразу клиент-серверное приложение и придумать для него свой протокол, которой будет расшифровывать наши данные. Это не так сложно, как может показаться на первый взгляд.
Socket как сущность – это комбинация трёх составляющих: IP-адрес, порт, и протокол. При создании сокета мы должны указать именно эти три аргумента, чтобы он мог присутствовать в системе, как самостоятельная личность:
SOCKET = 127.0.0.1 :69 :UDP.
"Интернет сессия" – это один цикл связи клиента и сервером. Сессия может продолжаться бесконечно, пока одному из участников это не надоест. TCP-cессия состоит из трёх фаз: Привет -> Обмен данными -> Пока! Нужно помнить, что сокеты привязываются к сессиям, и если во-время сессии связь нечаянно оборвётся, то сокет этой сессии становится недействительным, и закрыв его – нужно создать новый. Это основная ошибка, которую допускают новички.
В штанинах XP и 7 для нас припрятаны 32768 резервных сокетов. Такое кол-во позволяет создавать их пачками, для каждой сессии отдельно. Причём половина сокетов может прослушивать входящий трафик (т.н. демоны), а половина работать на приём/передачу. На серверных системах резерва ещё больше, т.к. они обслуживают по-более клиентов за раз, и для каждого нужен свой сокет. При создании большого числа сокетов главное не потеряться в них.
В природе имеются блокирующие и неблокирующие сокеты. Первые работают в синхронном режиме, а вторые в асинхронном. Разница их в том, что блокирующий сокет останавливает ход программы до тех пор, пока не выполнит свою задачу. Этим отличаются устаревшие функции из библиотеки wsock32.dll. Соответственно, неблокирующий НЕ ждёт ответа от сервера, а отправив запрос сразу передаёт управление на сл. участок кода программы. В этом случае, следить за состоянием неблокирующего сокета должна отдельная Call-Back функция. Управлять режимом работы сокета позволяет функция ioctlsocket(), с кодом операции FIONBIO – No Blocket I/O.
зы, прелюдия что-то растянусь..
5.1. Под копотом..
Если мы планируем работать с сокетами, то промышленная реализация алгоритма одной сессии выглядит так:
- WSAStartup() – инициализация библиотеки wsock32.dll;
- soсket() – создание сокета определённого типа;
- bind() – привязать к этому сокету сетевой адрес собеседника (IP, порт, протокол);
- connect() – попытка соединения с ним..;
- send() – если связь установлена, то отправить собеседнику пакет данных;
- recv() – принять от него ответный пакет;
- --- здесь если надо, то крутим цикл send() ---
- shutdown() – попрощаться с абонентом, и разорвать соединение;
- closesocket() – освободить занимаемый сокет.
На самом деле не всё так плохо, ведь перед нами самый сложный протокол из всех – протокол надёжной связи TCP. Если сокет у нас TCP, то мы можем убрать bing(), поскольку функция connect() сама привязывает сетевой адрес к сокету. А вот в случае с UDP ничто не мешает отправить на скамейку запасных функции connect() и shutdown(), т.к. это протокол без предварительной установки связи. Однако пролог алгоритма в виде первых трёх функций должен присутствовать всегда.
Ещё одна тонкость связана с форматом сетевых адресов, таких как IP и номер порта – функциям WinSock их нужно передавать в прямом порядке Big-Endian, в то время-как процессоры Intel/AMD используют обратный порядок байт Litle-Endian. Например IP-адрес 127.0.0.1 будет находится в памяти процессора как 4-байтное значение: 01 00 00 7F – это и есть LitleEndian, когда младший байт первый. Но функции WinSock требуют прямого порядка как мы записываем их на бумажке, т.е. 7F 00 00 01 или Big-Endian. Под этот-же каток попадают и номера портов, только они не 4-байтные, а 2. На сишном жаргоне размер определяется как LONG(4) и SHORT(2).
Для преобразования порядка байт из обычного в сетевой, предусмотрены WinSock функции htonl() и htons(). В дословном переводе это означает "Hex-To-Net-Long" (для преобразования 4 байтных IP-адресов) и "Hex-To-Net-Short" (для 2 байтных номеров порта). Когда сервер нам будет отвечать, ясный-перец он тоже отправит нам IP-адрес и порт в сетевом порядке байт. Если мы захотим их вывести на экран, то должны применить обратное преобразование, для чего имеются функции ntohl() и ntohs() – соответственно для LONG и SHORT. Такая вот хрень с этими адресами..
Ну и альфа-самцом во всей этой кухне является конечно-же файл с инклудами, которыми FASM к сожалению не может похвастаться. В его папке include\api есть wsock32.inc, в котором перечислены все знакомые ему сетевые функции. Ещё в одном дире include\equates под таким-же названием лежит инклуд с описанием служебных WinSock-структур. Здесь конечно разраб фасма Томаш Грыштар чуток подкачал, и перечислил только критические структуры без которых ну совсем уж никак. Поэтому я дополнил этот инклуд своими структурами, которые собирал со-всех источников. Он лежит в скрепке этого топика, а вам нужно просто заменить им текущий, из папки ..\equates. В нём я описал форматы заголовков всех уровней стека, от TCP и до Ethernet фрейма. Внимание!!! Без этого инклуда, представленные ниже программы работать не будут!
Чтобы было без соли, код будем писать консольный, а для ввода-вывода на экран подключим msvcrt.dll с её функциями printf() и scanf(). Первой можно передавать не ограниченное число аргументов, что позволит выводить сразу кучу информации. Вторая scanf() – для ввода с клавиатуры и считается уязвимой (юзер может переполнить приёмный буфер), поэтому выделим для неё резиновый буфф в конце секции-данных и пусть переполняет эту 4К-байтную секцию, пока не надоест. Её можно было-бы заменить на ReadConsole(), которая ограничивает ввод. Но эта read слишком громоздкая для обычного ввода, а мы стремимся к минимализму.
5.1.0. Программа перечисления портов
Начнём с того, что перечислим все зарезервированные порты на своей машине.
Нужно сказать, что среди WinSock-функций есть информационные функции, для которых не нужно создавать сокеты. Они работаю в офф-лайн и тупо предоставляют нам различную инфу. Две из таких функций я оформил в программу ниже – это WSAStartup() и getservbyport(). Первая загружает библиотеку WinSock и возвращает в структуру "WSADATA" её паспорт. Запрашивая смещения из этой структуры, мы можем выводит из неё инфу на экран. Если функция выполняется удачно, то возвращает нуль:
C-подобный:
invoke WSAStartup, 0x0101, wsa ;// 0x0101 = запрашиваемая версия v1.1
;// wsa = указатель на структуру WSADATA
;//-------------------------------------------------------------------------
struct WSADATA ;// всего 400 байт (0x190)
wVersion dw 0 ; мин.версия WinSock (обычно v1.1)
wHighVersion dw 0 ; макс.версия (обычно v2.2)
szDescription db 256+1 dup(0) ; строка с текущей версией
szSystemStatus db 128+1 dup(0) ; строка с текущим статусом (Run/Stop)
iMaxSockets dw 0 ; сколько TCP-сокетов можно создать
iMaxUdpDg dw 0 ; макс.размер UDP-датаграммы
_padding_ db 2 dup(0) ; резерв (байты выравнивания)
lpVendorInfo dd 0 ; указатель на данные производителя
ends
Вторая функция getservbyport() занимается тем, что по номеру порта заполняет уже свою структуру "servent". Если вызов успешный, то возвращает в EAX указатель на эту структуру, иначе нуль. Смысловая нагрузка приёмного буфера "servent" выглядит так:
C-подобный:
invoke getservbyport, port, 0 ;// port = номер порта, о котором хотим собрать инфу
;// 0 = все протоколы (можно фильтровать TCP или UDP)
;//-------------------------------------------------------------------------------------
struct servent ;// getservbyport (port, *proto)
s_name dd 0 ; линк на строку с именем сервиса
s_aliases dd 0 ; линк на NULL-массив альтернативных имён
s_port dw 0 ; номер порта
s_zero dw 0 ; резерв
s_proto dd 0 ; линк на строку с именем протокола
ends
Как говорилось выше, WinSock-функции насилуют драйвера TDI и NDIS. Понаблюдать за этим процессом в замочную скважину можно так.. Запускаем системный "Дисчетчер задач" и в меню вид устанавливаем галку "Вывод времени ядра". Теперь в окне "Быстродействие" диспетчера появится вторая шкала красного цвета, которая и покажет время обращения нашей софтины к ядерному уровню драйверов. Как выяснилось, не все функции прибегают к услугам драйвера – некоторые вполне самостоятельны.
Программа ниже выполняет перечисленные действия, и выводит инфу по каждому порту на экран. Вся нагрузка лежит тут на функции getservbyport(), которой в цикле подставляю следующий порт, пока счётчик не достигнет максимального порта под номером 65535. Ещё одна с прозрачным именем gethostname() возвращает имя нашего узла и сбрасывает его в буфер, на который указывает аргумент. Если выполнилась удачно – в EAX возвращает нуль:
C-подобный:
format pe console
entry start
include 'win32ax.inc'
;----------
.data
wsa WSADATA
sPort servent
wsaInfo db 13,10,' %s %s'
db 13,10,' Sockets count.....: %d'
db 13,10,' UDP-datagramm size: %d',0
hName db 13,10,' Host name.........: %s',0
capt db 13,10
db 13,10,' Enum Host Port-name v0.1'
db 13,10,' ============================',0
mes0 db 13,10,' Port %05d %s - %s',0
mesEnd db 13,10,' ============================'
db 13,10,' Total: %d ports',13,10,0
buff db 64 dup(0)
count dd 0
frmt db '%s',0
;----------
.code
start:
;// инициализация библиотеки WinSock, и вывод инфы о ней
invoke WSAStartup,0x0101,wsa ;
push 0 0 ; обнулить регистры
pop ecx edx ; ^^^
mov eax,wsa.szDescription ; версия WinSock
mov ebx,wsa.szSystemStatus ; текущий статус
mov cx,[wsa.iMaxSockets] ; всего сокетов в резерве
mov dx,[wsa.iMaxUdpDg] ; макс.размер UDP-датаграммы
cinvoke printf,wsaInfo,eax,ebx,ecx,edx ; выводим шапку
;// Получить имя своего узла
invoke gethostname,buff,64
cinvoke printf,hName,buff
;// Начало сканирования зарегистрированных портов
cinvoke printf,frmt,capt ; выводим шапку
mov ecx,0xffff ; всего портов 65535 (word)
xor eax,eax ; начинать с нулевого
@scan: push eax ecx ; запомнить номер порта и счётчик!
invoke htons,eax ; переводим #порта в сетевой порядок (..s это Short)
invoke getservbyport,eax,0 ; заполняем структуру "servent" по номеру порта
or eax,eax ; если EAX=0 значит ошибка
jz @fuck ; пропустить..
inc [count] ; иначе: "servent" заполнена, и счётчик найденых +1
xor ebx,ebx ; очистить EBX (в EAX сейчас лежит указатель на "servent")
mov bx,word[eax+8] ; BX = номер порта из структуры "servent"
xchg bh,bl ; поменять байты местами (из Net в Hex порядок)
mov edx,[eax+12] ; EDX = указатель на имя протокола
mov ecx,[eax] ; ECX = указатель на имя связанного с портом сервиса
cinvoke printf,mes0,ebx,edx,ecx ; выводим инфу порта на экран
@fuck: pop ecx eax ; порт и счётчик на родину!
inc eax ; следующий номер порта..
dec ecx ; счётчик -1
jnz @scan ; повторить, если счётчик не нуль
;// Выводим кол-во найденных портов
cinvoke printf,mesEnd,[count] ;
cinvoke scanf,frmt,frmt+5 ; ждём нажатия клавиши..
invoke ExitProcess,0 ; на выход!
;//--- Cекция импорта программы ------
section '.idata' import data readable ;
library kernel32,'kernel32.dll',\ ; импортируемые библиотеки
wsock32,'wsock32.dll',\ ;
msvcrt,'msvcrt.dll' ;
import msvcrt,printf,'printf',scanf,'scanf' ; эту FASM не знает, опишем вручную
include 'api\kernel32.inc' ; остальные функции есть в инклудах фасма.
include 'api\wsock32.inc' ;
На своей тестовой XP я получил всего 86 занятых портов, хотя на семёрке их уже 174.
Здесь видно, что например порт(43) работает по-протоколу TCP и его прослушивает сервис "NicName - Network Information Center", а это
Ссылка скрыта от гостей
Со всеми этими сервисами нужно будет работать по их собственному протоколу! Получается такая каша, разгребсти которую мы сможем только ко-второму пришествию. Для каждого из них имеется своя
Ссылка скрыта от гостей
, в которой описывается способ общения.5.1.1. Сканер открытых портов
Допустим мы получили список портов. Теперь можно просканировать их и посмотреть, к какому именно порту мы имеем удалённый доступ. Злейшим врагом сканера является файервол (брэндмауэр), который перекрывает кислород всем портам кроме тех, которые админ внёс в список исключений. Таким образом, из 86 портов моей портянки могут быть открыты только некоторые, и наша цель узнать – какие именно.
Суть в том, что мы выбираем узел и пробуем подключиться к каждому из его портов. Если к порту нет доступа, то встречаем ошибку. Поскольку нам нужно устанавливать связь, значит протокол UDP для этих целей не подходит и нужен TCP. Для передачи адреса-узла сокетам и функциям, в WinSock имеется специальная структура "sockaddr_in". Для секетов Беркли это была (теперь устаревшая) структура " in_addr", но её уже отправили на покой и с версии 2.0 заменили новой:
C-подобный:
struct sockaddr_in ;// всего 16 байт
sin_family dw 0 ; тип протокола (всегда AF_INET)
sin_port dw 0 ; номер порта
sin_addr dd 0 ; 4 байтный IP-адрес узла
sin_zero db 8 dup(0) ; резерв (выравнивание)
ends
Здесь мы видим важное для нас поле sin_port – его и будем менять в цикле, чтобы просканировать все порты данного IP-адреса. Выше говорилось, что с каждому сокету можно привязать только один адрес, но т.к. нам нужно менять в адресном пакете номер порта, значит это будет уже другой адрес. Придётся на каждом шаге закрывать старый сокет, и создавать новый. Если этого не сделать, то функция connect() начнёт атаковать наоборот нас, своими вечными ошибками. Запишем этот факт в свой кэш.. Вот прототип этой функции:
C-подобный:
;// Функция установки связи с удалённым узлом
;//-------------------------------------------
invoke connect, [s], addr, addr_len
s – дескриптор сокета;
addr – указатель на структуру "sockaddr_in";
addr_len – размер этой структуры.
Теперь перенесём всё сказанное в окно FASM'a и получим демонстрационный сканер портов. Скорость его перебора оставляет желать лучшего, и я специально добавил функцию вывода на экран не только открытых портов, но и закрытых, чтобы наглядно видеть шаг (получилось ~1 сек/порт). Чтобы ускорить этот процесс, нужно создать неблокирующий сокет, т.к. в этом примере он блокирующий. Поэтому connect() ждёт результата, что и тормозит повозку.
C-подобный:
format pe console
entry start
include 'win32ax.inc'
;//----------
.data
addr sockaddr_in
wsa WSADATA
capt db 13,10
db 13,10," Port-Scan v0.1"
db 13,10,' ====================='
db 13,10,' Please wait...',0
open db 13,10,' Port %05d ***** open',0
close db 13,10,' Port %05d close',0
mes1 db 13,10,' ========================'
db 13,10,' Scan OK!',0
port dd 0 ; текущий порт
s dd 0 ; под дескриптор сокета
host db '127.0.0.1',0 ; кого сканируем
frmt db '%s',0 ; для scanf(), и дальше буфер ввода (там болото нулей)
;//----------
.code
start: cinvoke printf,frmt,capt ; выводим шапку
invoke WSAStartup,0x0101,wsa ; инициализация WinSock
;// Заполняем адресный пакет "sockaddr_in"
invoke inet_addr, host ; IP из строки в Hex
mov [addr.sin_addr],eax ; записать его в поле адреса
mov [addr.sin_family],AF_INET ; протокол = Интернет
;// Начинаем сканирование портов..
@scan: invoke htons,[port] ; #порта в сетевой порядок байт
mov [addr.sin_port],ax ; в пакет адреса его..
invoke socket, AF_INET, SOCK_STREAM,0 ; создать TCP сокет (Stream/поток)
mov [s],eax ; запомнить его дексриптор
invoke connect,[s], addr, sizeof.sockaddr_in ; пробуем подключиться по адресу
cmp eax,-1 ; ошибка?
jnz @ok ; если нет..
cinvoke printf,close,[port] ; иначе: мессага Close и #порта
jmp @next ;
@ok: cinvoke printf,open,[port] ; порт открыт!
@next: invoke closesocket,[s] ; отработанный сокет на свалку
inc [port] ; следующий порт..
cmp [port],0xffff ; все порты просканировали?
jnz @scan ; нет - повторить
;// После (примерно) двух часов - прощальный диалог с юзером
cinvoke printf,frmt,mes1 ;
cinvoke scanf,frmt,frmt+5 ; ждём нажатия клавиши..
invoke ExitProcess,0 ; на выход!
;//---- Секция импорта программы -----
section '.idata' import data readable
library kernel32,'kernel32.dll',\
wsock32,'wsock32.dll',\
msvcrt,'msvcrt.dll'
import msvcrt,printf,'printf',scanf,'scanf'
include 'api\kernel32.inc'
include 'api\wsock32.inc'
Любой сканер – это не есть гуд, и чтобы не насканировать себе билеты в Магадан, лучше вообще не заниматься этим (мало-ли хороших дел на свете?). Этот пример тут только для того, чтобы знать откуда исходит угроза, и закрыть все порты на своём узле. Я установил у себя
Ссылка скрыта от гостей
(145 Кб для тестов то-что доктор прописал), и вот его реакция на эту прожку..Здесь видно, что установка с ним связи на порт(80) прошла успешно, но пока он собирался отправить мне ответ, я уже сбросил соединение, прихлопнув свой сокет внутри цикла как таракана. Остальные порты у меня под фаером, а вы можете проверить свои:
В следующий раз рассмотрим несколько протоколов, попробуем создать свои, пошифруем трафик и много ещё. Было-бы желание и время. Обитать на этом уровень очень интересно, и вдохновляет огромный запас возможностей. Ограничивает их - только наша фантазия.
Последнее редактирование: