SOISK - SYSTEMY OPERACYJNE I SIECI KOMPUTEROWE
Tomasz Puchała

PUS 7

Instrukcja Laboratoryjna 7
Bezpieczeństwo danych i poprawność funkcjonowania aplikacji
 
1) Wstęp teoretyczny
Celem ćwiczenia jest zapoznanie się metodami zabezpieczania funkcjonalności programów
wykorzystujących protokoły sieciowe.
Zaliczenie niniejszego laboratorium polega na modyfikacji wybranych aplikacji klienta i serwera w zabezpieczenia funkcjonowania.
Zabezpieczenie funkcjonalności polega między innymi na pewnej, świadomej (widocznej) wymianie informacji po stronie aplikacji klienta oraz serwera. Doposażenie ww aplikacji w przydatne funkcje
np.: przekształcenia pomiędzy adresami aplikacji, a nazwami, funkcje zbierające informacje
o procesach ...log, oraz funkcje zbierające informacje ułatwia „zwykłemu użytkownikowi”
zrozumienie funkcjonowania, a zarazem wyświetla instrukcje zastosowania danej aplikacji.
 
2) Przekształcenia adresów w nazwy
Funkcja gethostbyname:
przykład:
if(inet_pton(AF_INET, gethostbyname(argv[1]), &servaddr.sin_addr)<0)
err("Bledny adres IP");
Funkcja daje możliwość podania adresu IP w postaci nazwy stacji roboczej. Aby móc używać serwera nazw, należy wpisać alias do pliku, konfiguracyjnego z którego funkcja pobiera dane. Plik konfiguracyjny aliasów z numerami stacji roboczych: /etc/hosts
przykład:
Zastosowanie ww funkcji w aplikacjach klienckich, przyśpiesza uruchamianie, gdyż nie wymaga zapamiętywania adresów dziesiętno - kropkowych, zastępując je nazwami, które łatwiej można zapamiętać.
Aplikacja klienta wykorzystująca ww funkcję:
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <netinet/in.h>
#define MAX 4096
void err_sys(char *s){
printf("%sn",s);
exit (-1);
}
int main(int arg, char **argv)
{
int sockfd, n;
struct sockaddr_in servaddr;
char bufor[MAX];
if((sockfd=socket(AF_INET, SOCK_STREAM, 0))<0)
err_sys("Błąd otwierania socketa");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family=AF_INET;
if(inet_pton(AF_INET,gethostbyname(argv[1]),&servaddr.sin_addr)<0)
err_sys("Błąd adresu ip");
if((servaddr.sin_port = htons(atoi(argv[2]))) <0)
err_sys("Błąd formatu portu");
if(connect(sockfd,(struct sockaddr *)&servaddr, sizeof(servaddr))<0)
err_sys("Problem z połączeniem");
for(;;)
{
bzero(bufor,sizeof(bufor));
printf("nnwprowadź równanie(bądź wpisz q by wyjść): ");
fgets(bufor,sizeof(bufor),stdin);
if(bufor[0] == 'q')
break;
if(write(sockfd,bufor,strlen(bufor)) <=0)
err_sys("Błąd wysyłania");
bzero(bufor,sizeof(bufor));
n = read(sockfd,bufor,MAX);
bufor[n] = 0;
if(fputs(bufor,stdout) == EOF)
err_sys("Błąd wyświetlania");
if(n<0)
err_sys("Błąd odczytu");
}
if(close(sockfd)<0)
err_sys("błąd zamykania");
return 0;
}
Funkcja gethostbyaddr:
Funkcja pobiera adres IP w postaci dwójkowej i próbuje znaleźć nazwę stacji odpowiadającą temu adresowi. Jest odwrotnością funkcji gethostbyname.
#include <netdb.h>
struct hostent *gethostbyaddr (const char *addr, size_t len, int family);
przykład:
hbaddr = gethostbyaddr( (char*) &servaddr.sin_addr.s_addr, sizeof(servaddr.sin_addr.s_addr), AF_INET));
Funkcje getservbyname oraz getservbyport:
Stacje, oraz usługi mają swoje powszechnie używane nazwy. Określając usługi można w programach używać nazw, zamiast numerów portów. Zakładając że odwzorowanie nazw na numery portów jest zdefiniowane w pliku konfiguracyjnym /etc/services, można liczyć na to, ze jeżeli zmieni się numer portu, to wystarczy tylko zmienić jeden wiersz w pliku konfiguracyjnym, a nie trzeba zmieniać i ponownie kompilować wszystkich programów.
Przekazują NULL jeżeli funkcja zwróci błąd.
przykład:
if ((sbname=getservbyport(argv[2],”TCP”))==NULL)
err(„Blad portu dla %s”,argv[2]);
Pozostałe funkcje getaddrinfo, getnameinfo:
Funkcje pobierają gniazdową strukturę adresową i przekazują dwa napisy, z których jeden
reprezentuje stację, a drugi usługę.
3) Przyjazne komunikaty
Umieszczenie w programie komunikatów funkcyjnych, pomaga w identyfikacji błędu działania
programu. Stosowanie standardowych komunikatów wejścia / wyjścia, sprawdzanie wyniku
zadziałania funkcji „pokazuje” użytkownikowi gdzie popełnił błąd.
Funkcja obsługi komunikatu błędów:
int err(char* s) {
printf("%sn", s);
printf("Errno: %dn", errno);
fprintf(stderr, "%sn", strerror(errno));
exit(-1);
}
przykład:
if ((servaddr.sin_port = htons(atoi(argv[1]))) < 0)
err("Blad PORT-u");
printf("Adres IP serwera: %st PORT: %sn", inet_ntoa(servaddr.sin_addr), argv[1]);
if (bind(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0)
err("Blad gniazda");
Funkcja wysyłająca komunikaty:
przykład:
void Wiadomosc(int sockfd, const char* wiad) {
printf("Wyslano: t %s n", wiad);
if (write(sockfd, wiad, strlen(wiad)) <= 0) {
err("Nie wysylano komunikatu !!!n");
}
printf("Wyslano komunikat !!!n");
}
Sprawdzenie poprawności wprowadzonego argumentu numeru IP:
przykład:
inet_pton(AF_INET,argv[1],&servaddr.sin_addr)<0)
err("Zly format adresu IP");
if((servaddr.sin_port=htons(atoi(argv[2])))<0)
err("Zly numer portu");
Funkcje: inet_aton, inet_addr oraz inet_ntoa dokonują przekształceń adresów IPv4. Adres w postaci napisu w notacji kropkowo - dziesiętnej (np. 127.0.0.1) przekształcają do na 32-bitową wartość w kodzie dwójkowym, uporządkowaną zgodnie z sieciową kolejnością bajtów lub wykonują przekształcenie odwrotne. Nowsze funkcje, inet_pton oraz inet_ntop, przekształcają zarówno adresy
IPv4, jak i IPv6.
Wysyłanie zawartości zadeklarowanego bufora danych:
przykład:
void wyslij(int sockfd, const char* c) {
if(write(sockfd, c, strlen(c))<=0)
err("Nie wyslano !!!");
printf("Wyslano !!!");
}
Poprawność odczytu danych:
przykład:
n=read(sockfd, recvline, MAXLINE);
recvline[n]=0;
if(fputs(recvline, stdout) == EOF)
err("Blad wyswietlania (fputs)");
if(n<0)
err("Blad odczytu");
Na powyższych przykładach możemy zaobserwować jak funkcje, komunikaty we /wy upraszczają obsługę programu.
Wyświetlanie przydatnych informacji (ze struktury adresowej port, adres):
printf("Port polaczenia: %d n", servaddr.sin_port);
printf("Port polaczenia: %d n", servaddr.sin_addr);
Powyższe rozwiązanie nie jest najlepsze, gdyż nie które kompilacje systemu UNIX nie udostępniają wprost informacji ze struktury.
Funkcje sterowania gniazdami:
Istnieją różne sposoby pobierania i ustanawiania opcji, które oddziałują na gniazdo
Funkcje getsockopt i setsockopt.
Funkcja fcntl.
Funkcja ioctl.
Funkcje setsockopt i getsockopt:
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t
*optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval,
socklen_t optlen);
Argument level określa rodzaj oprogramowania systemowego, które ma interpretować daną opcję tj.:
ogólne oprogramowanie gniazd albo pewne oprogramowanie właściwe dla protokołu (np. IPv4, IPv6
lub TCP). Argument optname określa nazwę opcji. Argument optval jest wskaźnikiem do zmiennej, z której funkcja setsockopt ma pobrać nową wartość opcji albo w której funkcja getsockopt ma umieścić bieżącą wartosć opcji. Argument optlen, określa rozmiar tej zmiennej.
przykład:
//wysyłanie i odbieranie pakietów broadcast
int b = 1;
sockfd=socket(PF_INET, SOCK_DGRAM, 0);
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &b, sizeof(int));
printf ("Ustawiono wysyłanie / odbieranie pakietow broadcast: n");
 
Poniżej program wyświetlania opcji ustawienia gniazd:
przykład:
#include"naglowki.h"
#include <netinet/tcp.h>
union val {
int i_val;
long l_val;
char c_val[10];
struct linger linger_val;
struct timeval timeval_val;
} val;
static char *sock_str_flag(union val *, int);
static char *sock_str_int(union val *, int);
static char *sock_str_linger(union val *, int);
static char *sock_str_timeval(union val *, int);
struct sock_opts {
char *opt_str;
int opt_level;
int opt_name;
char *(*opt_val_str)(union val *, int);
}
sock_opts[] = {
"SO_BROADCAST", SOL_SOCKET, SO_BROADCAST, sock_str_flag,
"SO_DEBUG", SOL_SOCKET, SO_DEBUG, sock_str_flag,
"SO_DONTROUTE", SOL_SOCKET, SO_DONTROUTE, sock_str_flag,
"SO_ERROR", SOL_SOCKET, SO_ERROR, sock_str_int,
"SO_KEEPALIVE", SOL_SOCKET, SO_KEEPALIVE, sock_str_flag,
"SO_LINGER", SOL_SOCKET, SO_LINGER, sock_str_linger,
"SO_OOBINLINE", SOL_SOCKET, SO_OOBINLINE, sock_str_flag,
"SO_RCVBUF", SOL_SOCKET, SO_RCVBUF, sock_str_int,
"SO_SNDBUF", SOL_SOCKET, SO_SNDBUF, sock_str_int,
"SO_RCVLOWAT", SOL_SOCKET, SO_RCVLOWAT, sock_str_int,
"SO_SNDLOWAT", SOL_SOCKET, SO_SNDLOWAT, sock_str_int,
"SO_RCVTIMEO", SOL_SOCKET, SO_RCVTIMEO, sock_str_timeval,
"SO_SNDTIMEO", SOL_SOCKET, SO_SNDTIMEO, sock_str_timeval,
"SO_REUSEADDR", SOL_SOCKET, SO_REUSEADDR, sock_str_flag,
#ifdef SO_REUSEPORT
"SO_REUSEPORT", SOL_SOCKET, SO_REUSEPORT, sock_str_flag,
#else
"SO_REUSEPORT", 0, 0, NULL,
#endif
"SO_TYPE", SOL_SOCKET, SO_TYPE, sock_str_int,
#ifdef SO_USELOOPBACK
"SO_USELOOPBACK", SOL_SOCKET, SO_USELOOPBACK, sock_str_flag,
#else
"SO_REUSEPORT", 0, 0, NULL,
#endif
"IP_TOS", IPPROTO_IP, IP_TOS, sock_str_int,
"IP_TTL", IPPROTO_IP, IP_TTL, sock_str_int,
"TCP_MAXSEG", IPPROTO_TCP, TCP_MAXSEG, sock_str_int,
"TCP_NODELAY", IPPROTO_TCP, TCP_NODELAY, sock_str_flag,
NULL, 0, 0, NULL
};
int main(int argc, char **argv)
{
int fd, len;
struct sock_opts *ptr;
fd = socket(AF_INET, SOCK_STREAM, 0);
for (ptr = sock_opts; ptr->opt_str != NULL; ptr++)
{
printf("%s: ", ptr->opt_str);
if (ptr->opt_val_str == NULL)
printf("(undefined)n");
else
{
len = sizeof(val);
if (getsockopt(fd, ptr->opt_level, ptr->opt_name,&val, &len) == -1)
{
err_sys("getsockopt error");
}
else
{
printf("default = %sn", (*ptr->opt_val_str)(&val, len));
}
}
}
exit(0);
}
static char strres[128];
static char *
sock_str_flag(union val *ptr, int len)
{
if (len != sizeof(int))
snprintf(strres, sizeof(strres), "size (%d) not sizeof(int)", len);
else
snprintf(strres, sizeof(strres), "%s", (ptr->i_val == 0) ? "off" : "on");
return(strres);
}
static char *
sock_str_int(union val *ptr, int len)
{
if (len != sizeof(int))
snprintf(strres, sizeof(strres), "size (%d) not sizeof(int)", len);
else
snprintf(strres, sizeof(strres), "%d", ptr->i_val);
return(strres);
}
static char *
sock_str_linger(union val *ptr, int len)
{
struct linger *lptr = &ptr->linger_val;
if (len != sizeof(struct linger))
snprintf(strres, sizeof(strres),"size (%d) not sizeof(struct linger)", len);
else
snprintf(strres, sizeof(strres), "l_onoff = %d, l_linger = %d",
lptr->l_onoff, lptr->l_linger);
return(strres);
}
static char *
sock_str_timeval(union val *ptr, int len)
{
struct timeval *tvptr = &ptr->timeval_val;
if (len != sizeof(struct timeval))
snprintf(strres, sizeof(strres),"size (%d) not sizeof(struct timeval)", len);
else
snprintf(strres, sizeof(strres), "%d sec, %d usec",
tvptr->tv_sec, tvptr->tv_usec);
return(strres);
}
 
4) Funkcje rejestrowania zdarzeń
Rejestrowanie próby podłączenia przez klienta syslog:
Każda próba podłączenia się klienta do serwera może być zapisywana w dzienniku zdarzeń
systemowych. Domyślnie jest to plik /etc/var/log/user.log. Lokalizację tę można zmienić poprzez edycję pliku /etc/syslog.conf.
Funkcje openlog, syslog, closelog:
Funkcja obsługi komunikatu błędów wraz z zapisem do pliku komunikatu błędu:
przykład:
void err(char* s)
{
char *bufor;
sprintf(bufor,"%s n", s);
sprintf(bufor,"Errno: %dn", errno);
sprintf(bufor, "%sn", strerror(errno));
syslog(LOG_NOTICE|LOG_USER,bufor);
exit(-1);
}
przykład:
if((servaddr.sin_port = htons(atoi(argv[1])))<0)
{
syslog(LOG_ERR,"Zly format portu");
exit(1);
}
if(bind(listenfd,(struct sockaddr *)&servaddr, sizeof(servaddr))<0)
{
syslog(LOG_ERR,"Błąd przybicia gniazda TCP");
exit(1);
}
if(listen(listenfd,LISTENQ)<0)
{
syslog(LOG_ERR,"Błąd nasłuchu TCP");
exit(1);
}
Deklaracje wszystkich, wyżej wymienionych funkcji wraz z argumentami oraz niezbędnymi deklaracjami można znaleźć w internecie oraz materiałach wykładu Programowania Usług Sieciowych.
5) Zadania do wykonania
Zwiększyć funkcjonalność oraz poprawność działania, programów z poprzednich zajęć laboratoryjnych stosując powyższe instrukcje.