Winsock w Win32asm

by caspar *CookieCRK*


Pomyslalem, ze napisze ten tekst, jako ze ostatnio zajmowalem sie wlasnie troche winsockiem. Mam nadzieje, ze komus sie przyda...

No wiec tak, bede tu mowil o socketach typu stream. Korzystaja one z protokolu TCP (Transmission Control Protocol), ktory umozliwia nawiazanie dwukierunkowych polaczen strumieniowych (stream) miedzy dwoma komputerami. Gwarantuje on, ze kazdy wyslany bajt zostanie dostarczony do miejsca docelowego (w kolejnosci nadania) lub pojawi sie wiadomosc o braku mozliwosci dostarczenia, czyli zaden bajt nam nie zaginie w akcji. Wiekszosc popularnych uslug jak np. FTP, SMTP czy HTTP korzysta z tego wlasnie protokolu.

To do roboty. Socket to takie cos, do czego przypisujemy numer portu i od tej pory mozemy z tego portu korzystac, tzn. nawiazywac polaczenia albo je odbierac. Socketow mozemy uzywac na dwa sposoby: w trybie blocking czyli synchronicznie albo w trybie non-blocking czyli asynchronicznie. Pierwszy dziala tak, ze jesli wywolujemy funkcje, ktora ma odebrac jakies dane to program nie bedzie wracal z funkcji dopoki te dane nie zostana odebrane. Program w tym czasie wyglada jakby nic nie robil (bo w sumie nie robi, tylko czeka 8-] ). Jesli dzialamy w drugim trybie odrazu otrzymujemy oczekujace dane lub informacje, ze nie ma nic do odebrania. Generalnie, piszac pod winde, w miare mozliwosci powinno stosowac sie sposob drugi.

Zobaczmy jak to wyglada w praktyce.

Zanim zaczniemy jakiekolwiek zabawy z socketami musimy odpalic funkcje WSAStartup. Wywolujemy ja z dwoma parametrami:

	wVersionRequested - z ktorej wersji winsocka bedziemy korzystac	(dla 2.0 - 200h)
	lpWSAData - adres struktury WSAData, w ktorej zwrocone beda szczegoly implementacji winsocka

funkcja zwraca 0 jesli wszystko jest ok.

Teraz tworzymy socket. Sluzy do tego funkcja socket :)

	af - format, jedyny mozliwy - PF_INET
	type - typ, tak jak mowilem stream - SOCK_STREAM	
	protocol - protokol, ma byc 0

zwraca INVALID_SOCKET w razie bledu, w przeciwnym wypadku identyfikator nowego socketa.

Przy okazji: jesli ktoras z funkcji zwraca nam komunikat o bledzie mozemy wywolac funkcje WSAGetLastError (bez argumentow), ktora zwraca kod bledu.

Tak stworzony socket, domyslnie dziala jako synchroniczny. Jesli nam to nie pasuje mozemy to zmienic. Mamy do wyboru dwie funkcje. Pierwsza WSAAsyncSelect

	s - identyfikator socketa
	hWnd - uchwyt okna, ktore bedzie otrzymywac wiadomosci o wybranych zdarzeniach
	wMsg - wiadomosc jaka ma byc wyslana w razie wystapienia zdarzenia np. WM_SOCKET (powiedzmy: WM_USER + 100)
	lEvent - kombinacja zdarzen, ktore nas interesuja (FD_READ, FD_WRITE, FD_OOB, FD_ACCEPT, FD_CONNECT, FD_CLOSE)

jesli w porzadku zwraca 0 else SOCKET_ERROR.

.code

	...

	push FD_CONNECT+FD_READ+FD_CLOSE	; chcemy wiedziec kiedy sie polaczy, kiedy bedzie cos do odebrania i kiedy polaczenie zostalo przerwane
	push WM_SOCKET				; np. WM_SOCKET	equ WM_USER + 100
	push [HWnd]
	push [sock]
	call WSAAsyncSelect

	test eax, eax
	jz succeeds

	...

Jednak jesli na przyklad nasz prog nie ma okna (jakis trojan czy cos :>) to tak nie da rady. Mozemy uzyc drugiej funkcji: ioctlsocket

	s - identyfikator socketa
	cmd - komenda, nas interesuje FIONBIO
	argp - adres parametru dla komendy, dla naszej parametr musi byc rozny od zera

i znowu jesli ok to zwraca 0.


Nom, no to mamy naszego socketa... i teraz tak, mozemy probowac sie polaczyc czyli bawic sie w klienta albo czekac na polaczenia czyli bedziemy w polaczeniu serverem.

Najpierw popatrzmy jak to wyglada w wariancie pierwszym.
Musimy wywolac funkcje connect

	s - identyfikator socketa (nie polaczonego)
	lpname - adres struktury sockaddr_in, w ktorej znajduje sie port i adres hosta, do 	ktorego sie chcemy podlaczyc
	namelen - rozmiar struktury (10h)

strukturka sockaddr_in prezentuje sie nastepujaco

	sockaddr_in STRUCT
	  sin_family	WORD	?   - dajemy PF_INET
	  sin_port  	WORD	?   - port, ktorego bedzie uzywal socket
	  sin_addr  	DWORD	?   - adres (numer, nie adres w pamieci) IP hosta
	  sin_zero  BYTE 8 dup (?)  - niepotrzebne %)
	sockaddr_in ENDS

Czyli przed wywolaniem funkcji connect wypelniamy ta strukture, a poniewaz nasz procek przechowuje adresy i porty nie tak jak nam potrzeba (w odwrotnej notacji) :( musimy zastosowac funkcje htons

	hostshort - numer portu

w rezultacie dostajemy odpowiednio zapisany numer portu i mozemy wrzucic go do sin_port.

Jesli chodzi o adres uzywamy funkcji inet_addr

	lpcp - adres do stringa (zakonczonego nullem oczywiscie :>) zawierajacego IP w formacie x.x.x.x

jesli IP byl prawidlowy w eax laduje to co musimy wrzucic do sin_addr, ale jesli nie, to dostajemy INADDR_NONE.

Mozemy jednak miec adres nie jako IP tylko jako nazwe (np. cookiecrk.org). W takiej sytuacji uzywamy gethostbyname

	lpname - adres do nazwy hosta

zwraca nam adres do struktury hostent lub 0 jesli cos jest nie tak.

	hostent STRUCT
	  lpname  DWORD ?  - adres do nazwy hosta
	  lplpalias  DWORD ? - adres do adresu aliasow hosta
	  addr  WORD ? - typ zwroconego adresu hosta
	  len	 WORD ? - dlugosc adresu hosta
	  lplplist  DWORD ? - adres do adresu do adresu hosta :> (to nas interesuje)
	hostent ENDS

i to co jest pod lplplist wrzucamy do sin_addr.

.code

	...

	push 40h
	push offset ip_address
	push IDC_HOST
	push [DlgHWnd]
	call GetDlgItemTextA		; np. wczytujemy z okienka textowego do ip_address adres hosta

	mov sin.sin_family, PF_INET	; sin sockaddr_in <0>

	push 666
	call htons			; przerabiamy port

	mov sin.sin_port, ax		; wrzucamy numer portu zwrocony przez htons

	push offset ip_address		; uzytkownik mogl podac ip albo nazwe hosta
	call inet_addr			; najpierw probojemy wywolac funkcje dla ip

	cmp eax, INADDR_NONE		; porownujemy z INADDR_NONE
	jne ip_ad			; jesli rozne, to w eax mamy adres hosta
					; jesli rowne, to to nie byl ip :>
	push offset ip_address
	call gethostbyname		; wiec probojemy znalezc hosta po nazwie

	test eax, eax
	jz addr_error			; nie moze znalezc takiego adresu

	mov eax, [eax+12]		; w eax adres do hostent - ustawiamy sie na lplplist
	mov eax, [eax]
	mov eax, [eax]			; teraz mamy w eax, to o co nam chodzi - adres hosta

ip_ad:

	mov sin.sin_addr, eax		; adres do sin_addr

	push 16
	push offset sin			; odkladamy wypelniona strukture
	push [sock]
	call connect			; i wywolujemy connect

	...

Funkcja connect zwraca zero jesli polaczenie sie udalo lub SOCKET_ERROR jesli nie. I tak, jesli socket dziala jako blocking to powrot z funkcji nastapi wtedy, kiedy bedzie juz wiadomo czy polaczenie nastapilo czy nie. Czyli wynik dzialania funkcji w tym trybie oznacza faktyczny wynik proby polaczenia. W wypadku socketa non-blocking funkcja musi normalnie odrazu wrocic, a to z reguly za malo czasu na polaczenie. Nie ma sie wiec co dziwic, ze prawie zawsze dostaniemy SOCKET_ERROR. No i musimy sprawdzic czy naprawde jest cos nie tak, czy po prostu zabraklo czasu :). Wywolujemy funkcje WSAGetLastError, od ktorej dostaniemy kod ostatniego bledu. Jesli jest to WSAEWOULDBLOCK (2733h) to blad wystapil bo socket "by blokowal", a ustawilismy przeciez ze ma nie blokowac :) No i co teraz ? Jesli uzywalismy funkcji WSAAsyncSelect to przy jej wywolaniu do interesujacych nas zdarzen dorzucamy, ze chcemy wiedziec kiedy socket sie polaczy czyli FD_CONNECT. I juz... uznajemy, ze jest ok i czekamy az nasze okno otrzyma odpowiednia wiadomosc. Ale jesli nie uzywalismy WSAAsyncSelect musimy teraz uzyc funkcji select i sprawdzic czy socket jest do zapisu (writeable). jesli tak, to sie polaczyl :) , select:

	nfds - ignorowane
	lpreadfds - adres do struktury fd_set z danymi socketow, ktore maja byc sprawdzane czy sa do odczytu
	lpwritefds - adres do struktury fd_set z danymi socketow, ktore maja byc sprawdzane czy sa do zapisu
	lpexceptfds - adres do struktury fd_set z danymi socketow, ktore maja byc sprawdzane na wystapienie bledow
	lptimeout - adres do struktury timeval z czasem jaki funkcja ma czekac

z trzech srodkowych parametrow wybieramy te, ktore nas interesuja (w naszej sytuacji lpwritefds). Pozostale dajemy 0.

	fd_set STRUCT
	  fd_count DWORD ? - ile identow socketow
	  fd_array SOCKET 64 dup(?) - no i tablica na identyfikatory socketow
	fd_set ENDS

select zwraca liczbe socketow, ktore spelnily okreslone warunki lub zero jesli uplynal limit czasu. Jesli wystapil jakis blad dostaniemy SOCKET_ERROR.
Jeszcze krotko do czego to sluzy. A wiec socket stanie sie do zapisu jesli probowal sie polaczyc i proba sie powiodla (dlatego mozemy tu z tej funkcji skorzystac). Jesli byl polaczony znaczy to, ze mozna uzyc funkcji send zeby cos wyslac. Kiedy socket stanie sie do odczytu znaczy to, ze jesli oczekiwal na polaczenia to wlasnie jakies nadeszlo i mozna je zaakceptowac funkcja accept (o tym pozniej), jesli nie oczekiwal na polaczenia (byl z czyms polaczony) to znaczy, ze mozna uzyc funkcji recv zeby odebrac dane, ktore przyszly. Jesli na sockecie, ktory probowal sie polaczyc pojawil sie blad znaczy to, ze proba sie nie powiodla. Chyba za bardzo tego nie zakrecilem 8-]

.code				; przyklad dla socketa non-blocking

	...

	push 16
	push offset sin
	push [sock]
	call connect

	test eax,eax
	jz success

	call WSAGetLastError

	cmp eax, WSAEWOULDBLOCK
	jz success

	jmp connect_error

success:

	mov [fd_setstruc.fd_count], 1		; fd_setstruc fd_set <0>
	mov eax, [sock]
	mov [fd_setstruc.fd_array], eax
	lea eax, fd_setstruc

	push 0
	push 0
	push eax		; niech czeka az bedzie do zapisu (czyli sie polaczyl)
	push 0
	push 0
	call select		; odpalamy i czekamy

	cmp eax, SOCKET_ERROR
	jne connected		; jesli nie ma bledu tzn ze socket stal sie do zapisu (polaczyl sie)
				; gdybysmy dali limit czasu mogloby jeszcze byc 0 - timeout
	jmp connection_refused

	...

To tyle jesli chodzi o podlaczanie sie.


Zobaczmy teraz jak wyglada wariant drugi, czyli jak sie oczekuje i przyjmuje polaczenia.
Na poczatku trzeba socket zbindowac czyli przyporzadkowac mu numer portu i adres. Sluzy do tego funkcja bind

	s - identyfikator socketa
	lpaddr - adres do struktury z danymi (uzyjemy sockaddr_in)
	namelen - rozmiar struktury (10h)

Struktura sockaddr_in jest opisano nieco wyzej wiec nie bede sie powtarzal. Do sin_family wrzucamy znowu PF_INET, do sin_port port na ktorym bedziemy oczekiwac polaczen (musimy uzyc funkcji htons - patrz wyzej), a do sin_addr mozemy dac INADDR_ANY wtedy automatycznie zostanie przyporzadkowany socketowi odpowiedni adres.

Jesli wszystko sie udalo bind zwraca 0, inaczej SOCKET_ERROR. W razie bledu dobrze jest wywolac WSAGetLastError, kod WSAEADDRINUSE (2740h) oznacza, ze najprawdopodobniej jakis inny program uzywa tego portu.

.code

	...

	mov sin.sin_family, PF_INET

	push 21				; np. server ftp :)
	call htons

	mov sin.sin_port, ax
	mov sin.sin_addr, INADDR_ANY

	push 16
	push offset sin
	push [sock]
	call bind

	cmp eax, SOCKET_ERROR
	jne binded

	call WSAGetLastError

	cmp eax, WSAEADDRINUSE
	jz addr_in_use			; czyzby jakis srv ftp juz tu chodzil ? :)

	jmp bind_error

	...

Po tym wszystkim mozemy wreszcie ustawic socket w tryb nasluchiwania nadchodzacych polaczen. Funkcja listen

	s - identyfikator socketa
	backlog - maxymalna liczba nie rozstrzygnietych polaczen jakie moga oczekiwac

Dostajemy 0 lub SOCKET_ERROR.

No to czekamy na polaczenia... kiedy w koncu jakies przyjdzie musimy je zaakceptowac funkcja accept

	s - identyfikator socketa
	lpaddr - adres do struktury sockaddr_in, gdzie znajda sie dane dotyczace polaczenia, ktore przyszlo, mozna dac 0
	lpaddrlen - adres do wielkosci struktury, jak poprzednio jest 0 to tu tez dajemy 0

accept zwraca nam nowy identyfikator socketa, przez ktorego bedziemy obslugiwac przyjete polaczenie. Socket uzyty do polaczenia pozostaje otwarty i dalej czeka. Jesli wystapil blad otrzymamy INVALID_SOCKET. accept w trybie synchronicznym (blocking) bedzie czekal az nie przyjdzie polaczenie i wtedy sproboje je zaakceptowac. W trybie asynchronicznym accept musimy wywolac tylko jesli wiemy, ze cos oczekuje na przyjecie bo inaczej od razu wroci z INVALID_SOCKET. A skad wiemy, ze polaczenie przyszlo ? No coz, znowu, jesli uzywalismy funkcji WSAAsyncSelect do zdarzen dodajemy FD_ACCEPT. No i jak polaczenie nadejdzie nasze okienko dostanie wiadomosc. Jesli nie uzywalismy, mozemy zastosowac zaraz po listen funkcje select. Wysylamy jako parametr lpreadfds adres do fd_set z identem naszego socketa, do tego mozemy ustawic limit czasu jesli nie chcemy czekac wiecznie. Jesli funkcja wroci z liczba rozna od zera i SOCKET_ERROR znaczy, ze cos sie doczlapalo i wtedy accept. W sumie mozna tez zalatwic to petla

.code

	...

	push 1
	push [sock]		; powiedzmy ze tu mamy ident naszego socketa
	call listen

waiting:

	push 1000
	call Sleep		; troche odczekamy

	push offset len
	push offset addr	; addr sockaddr_in <0>
	push [sock]
	call accept

	cmp eax, 0
	jle waiting		; accept failed ?

	mov [csock], eax 	; csock dd ? - nowy ident - ten socket jest faktycznie polaczony

	...

I juz. Teraz pare slow o tym jak sie wysyla i odbiera dane.

Dane przesyla sie funkcja send

	s - ident polaczonego socketa
	lpbuf - bufor z danymi
	len - dlugosc danych
	flags - flagi

Funkcja zwraca ilosc wyslanych bajtow lub SOCKET_ERROR w razie bledu. Zanim zaczniemy wysylac dane mozemy tez upewnic sie czy druga strona jest w stanie je odebrac. Robimy to funkcja select sprawdzajac czy socket jest writeable.

Aby dane odebrac uzywamy funkcji recv

	s - ident polaczonego socketa
	lpbuf - bufor na dane
	len - rozmiar bufora
	flags - flagi

Funkcja zwraca liczbe odebranych bajtow. Przez recv mozemy sie tez dowiedziec o zamknieciu polaczenia. Zwraca wtedy 0. W razie jakiegos bledu dostaniemy SOCKET_ERROR.
Jesli recv zostalo wywolane a nie ma zadnych danych do odebrania to czeka na nadejscie danych jesli jest blocking lub zwraca SOCKET_ERROR z kodem WSAEWOULDBLOCK jesli jest non-blocking. O nadejsciu danych mozemy sie dowiedziec przez wiadomosc wyslana do okna programu jesli ustawilismy w funkcji WSAAsyncSelect FD_READ. Mozemy tez uzyc select i czekac az socket bedzie readable.
Jesli ustawimy flags na MSG_PEEK to dane zostana wrzucone do bufora ale nie zostana usuniete z kolejki oczekujacych danych. Czasem to tez sie przydaje :>

Kiedy konczymy korzystac z winsocka wywolujemy funkcje WSACleanup (bez argumentow) zeby po sobie posprzatac. Zwraca 0 jesli ok.

To chyba tyle. Jesli ktos ma jakies pytania czy uwagi to mailme: 

caspar@amber.bti.pl || caspar@cookiecrk.org
albo na ircnecie #crackpl

CookieCrk 2000