Jak programować w Linuksie

Rus3k
Ten artykuł wymaga dopracowania!

Jeżeli możesz popraw ten artykuł według zaleceń, które możesz znaleźć na stronie Artykuły do poprawy. Po dopracowaniu tego tekstu można usunąć ten komunikat.

Wymagania ogólne:
mózg
trochę wolnego czasu i chęci

Wymagania szczegółowe:
znajomość języka C
znajomość systemu Linux

Wymagania techniczne:
*Linux + biblioteki (development)

1 Wprowadzenie
2 Funkcje systemowe i biblioteczne: porównanie.
     2.1 Kompilacja
3 Inne funkcje systemowe.
4 Procedury biblioteczne {<stdio.h>}.
     4.2 4.1. Funkcje wyświetlające.
     4.3 4.2. Funkcje pobierające.
5 Katalogi i pliki
     5.4 Pliki
     5.5 Katalogi

Wprowadzenie

Systemy Unix (Linux) są w ogromnym stopniu oparte na systemie plików, prawie wszystko może być potraktowane jako plik. Wystarczy zaglądnąć do katalogu /dev. Znajdują się tam plikowe reprezentacje urządzeń fizycznych. Np. żeby zamontować sobie dysk C:, na którym masz DOS-a wpisujesz:
mount /dev/hda1 /mnt/c
o czym jako użytkownik Linuksa na pewno wiesz. Pamięć również jest traktowana jest jako plik w tym systemie. Jako plik traktuje się również gniazdka sieciowe, a więc za pomocą funkcji systemowych pisze się do nich i czyta jak do i z plików. Z tego względu umiejętność programowania w Unixie, to umiejętność posługiwania się funkcjami obsługującymi pliki. Oczywiście to nie wszysko (dobrze by było ;). Z drugiej jednak strony budowa systemów Unixowych jest dosyć prosta i przejrzysta (of kors na odpowiednim poziomie).

Tutorial ten ma Ci pokazać podstawy programowania w Linuksie. Aby osiągnąć więcej trzeba pisać i czytać kod, którego jest mnóstwo w sieci. Mam nadzieję, że gdy zobaczysz jakiś exploit, to będziesz mniej więcej rozumiał o co w nim chodzi i nie będziesz się nim bezmyślnie posługiwał.

Funkcje systemowe i biblioteczne: porównanie.

Pliki w systemie Linux można na poziomie programowania obsługiwać na dwa sposoby: za pomocą funkcji systemowych oraz bibliotecznych, przy czym funkcji bibliotecznych jest więcej i są one bardziej przyjazne.

Funkcje systemowe to takie funkcje, które wykorzystują gotową obsługę udostępnioną przez jądro systemu. Jądro jest cały czas podczas działania systemu w pamięci i kontroluje wszystkie procesy i każdy dostęp do systemu plików. Również funkcje biblioteczne używają funkcji systemowych.

Podstawowa różnica między tymi dwoma sposobami leży w kodzie samego programu. Kiedy program korzysta z funkcji systemowych, sam kod nie jest dołączany do programu - program zleca to zadanie dla jądra (jest to zwykle wykonywane za pomocą przerwań programowych). W przypadku biblioteki kod w niej zawarty jest dołączany do programu.

Dlaczego zatem używa się funkcji bibliotecznych? Przede wszystkim funkcje te są bardziej przyjazne - nie trzeba na przykład przejmować się obsługą buforów, o wiele trudniej jest się pomylić. Funkcje biblioteczne są też bardziej rozbudowane.

Poniżej znajdują się przykłady prostych programów, które wykonują te same zadania wykorzystując funkcje systemowe lub biblioteczne.

system.c

/*wykorzytanie funkcji systemowych do operacji na pliku*/
/*program liczy znaki w pliku 'plik' Ilosc znakow to rownoczesnie 
  wielkosc pliku w bajtach*/
 
#include <stdlib.h> //wykorzystanie dla funkcji exit()
#include <fcntl.h>
 
#define BUFROZ 512
main()
{
    int desp;       //deskryptor pliku
    char buf[BUFROZ];
    ssize_t czytaj;
    long licz_zn; 
    /*
    */
    if((desp = open("plik", O_RDONLY)) == -1) //w wypadku błędu funkcja
                        //open zwraca -1
    {
    printf("Nie moge otworzyc pliku 'plik'n");
    exit(1);
    }
 
    while((czytaj = read(desp, buf, BUFROZ)) >0)
    licz_zn+=czytaj;
 
    printf("Ilosc znakow w pliku wynosi : %ldn", licz_zn);
 
    close(desp);
 
    exit(0);    
}

Jak widzimy program użył kilku funkcji systemowych: open, read, close.


Funkcja open:

int open(const char *ścieżka, int flagi, [mode_t mode]);

służy do otwierania pliku. Drugi argument tej funkcji jest typu int. Wykorzystujemy tu odpowiednie flagi (w naszym przypadku O_RDONLY), które są zdefiniowane w pliku nagłówkowym < fcntl.h >:

O_RDONLY - otwórz tylko do odczytu
O_WRONLY - otwórz tylko do zapisu
O_RDWR - otwórz do odczytu i zapisu

Trzeci argument [mode_t mode] jest używany jedynie z flagą O_CREAT oznaczający utworzenie pliku, natomiast argument [mode_t mode] będzie ustawiał prawa dostępu (np. 0622).


Funkcja read:

ssize_t read(desp, buf, BUFROZ);

służy do czytania z otworzonego pliku. Argumenty:
desp - deskryptor pliku uzyskany podczas wywołania funkcji open
buf - bufor, który będzie zawierał odczytane z pliku dane
BUFROZ - rozmiar bufora (w jakich porcjach będzie funkcja read czytać)

Zaleca się używanie BUFROZ równego wielokrotności 512. Jeżeli funkcja zwróci wartość ssize_t <0 oznacza to koniec pliku. (-1 - EOF)


Funkcja close:

close(desp);

służy do zamknięcia pliku poprzez zwolnienie deskryptora.

proced.c

/*wykorzystanie procedur bibliotecznych*/
/*program liczy znaki w 'plik'*/
 
#include <stdio.h> //biblioteka I/O 
#include <stdlib.h>
 
main()
{
    FILE *p;
    int c;
    long licz_zn = 0;
 
    if ((p = fopen("plik", "r")) == NULL)
    {
    printf("Nie moge otworzyc pliku 'plik'n");
    exit(1);
    }
 
    while ((c = getc(p)) != EOF)
    {
        licz_zn+=1;
    }    
 
    printf("Ilosc znakow w pliku wynosi : %ldn", licz_zn);  
 
    fclose(p);
 
    exit(0);    
}

Funkcja fopen i fclose:

    FILE* fopen(const char *nazwa_pliku, const char *typ);
    int fclose(FILE* stream);

Plik jest identyfikowany za pomocą wskaźnika do struktury FILE.
typ:
"r" - tylko do odczytu
"w" - tylko do zapisu (jesli nie istnieje utwórz)
"a" - do odczytu, dane będą dodawane na końcu pliku
"r+"- do odczytu i zapisu
"w+"- do odczytu i zapisu (jesli nie istnieje utwórz)
"a+" - do odczytu i zapisu, dane będą dodawane na końcu pliku


Funkcja getc:

    int getc(FILE *p);

pobiera numer znaku z pliku opisanego przez FILE

system2.c

/*   kopiuje plik1 do plik2  używając funkcji systemowych*/
 
#include <unistd.h>
#include <fcntl.h>
 
#define BUFSIZE 512
#define PERM    0644
 
/* funkcja kopiujaca */
int copyfile(const char *plik1, const char *plik2)
{
    int infile, outfile;
    ssize_t nread;
    char buffer[BUFSIZE];
 
    if((infile = open(plik1, O_RDONLY)) == -1)
    return(-1);
 
    if((outfile = open(plik2, O_WRONLY|O_CREAT|O_TRUNC, PERM)) == -1)
    {
    close(infile);
    return(-2);
    }   
 
    /*teraz czytaj z plik1 po BUFSIZE znakow naraz*/
    while((nread = read(infile, buffer, BUFSIZE))>0)
    {
    /*zapisz bufor do ploku wyjsciowego*/
    write(outfile, buffer, nread);
    }
    close(infile);
    close(outfile);
 
}
/////////////////////////////
int main(int argc, char** argv)
{
    if (argc<3)
    {
     printf("Uzyj: %s plik_zrodlo plik_celn",argv[0]);
     exit(1);
    }
 
    copyfile(argv[1],argv[2]);
}

Program używa znanych już funkcji (oprócz write) do skopiowania jednego pliku do drugiego.


Funkcja write:

ssize_t write(int desp, const char buffer, ssize_t nread);

służy do zapisu danych do pliku o deskryptorze desp, danych zawartych w buffer w liczbie ssize_t nread.


Flaga O_TRUNC razem z O_CREAT sprawia, że jeśli plik już istnieje zostanie on obcięty do zerowej długości. Oczywiście jeśli nie chcemy stracić ważnych danych powinniśmy użyć razem O_CREAT|O_EXCL wtedy funkcja open zwróci błąd jeżeli już taki plik istnieje.

Jeszcze małe wyjaśnienie funkcji int main(int argc, char argv):
int argc - to liczba argumentów podanych w linii poleceń, w naszym przypadku musimy podać ich trzy: nazwa_programu plik_zrodlo plik_cel
char
argv - jest to tablica wskaźników pokazujących na nasze argumenty podane w linii poleceń podczas uruchamiania skompilowanego programu (np. argv[0] wskazuje na nazwę naszego programu).

proced2.c

/*program wykorzystauje procedury biblioteczne*/
/*
program kopiuje plik1 do plik2
*/
 
#include <stdio.h>
#include <stdlib.h>
 
/*funkcja kopiujaca*/
int copyfile(const char *plik1, const char *plik2)
{
    FILE *in, *out;
    int c;
 
    if ((in = fopen(plik1,"r")) == NULL)
    return(-1);
 
    if ((out = fopen(plik2,"w")) == NULL)
    {
    fclose(in);
    return(-2);
    }       
 
    while((c = getc(in)) != EOF)
    putc(c, out);
 
    fclose(in);
    fclose(out);    
}
////////////////////////////////
 int main(int argc, char** argv)
 {
     if(argc<3)
     {
         printf("Uzyj: %s plik_zrodlo plik_celn",argv[0]);
     exit(1);
     }
 
     copyfile(argv[1],argv[2]);
 }

Funkcja putc:

int putc(int c, FILE* out);

pisze do out -opisany przez FILE- znak c.


Kompilacja

W Linuksie kompilację wykonuje się w następujący sposób:
gcc [opcje] plik.c
Zajmijmy się trochę opcjami.

Kompilacja gcc plik.c automatycznie tworzy plik binarny o nazwie a.out. Jeżeli chcemy aby plik wyjściowy miał konkretną nazwę używamy opcji -o.
gcc -osystem system.c
w ten sposób kompilator utworzy plik system gotowy do uruchomienia. Inną ciekawą opcją jest -S - kompilator tworzy plik wyjściowy, a raczej przejściowy, w asemblerze. Po wpisaniu polecenia
gcc -S system.c
kompilator utworzy plik system.s. Dla naszego przykładu wygląda on tak:

.file   "system.c"
    .version    "01.01"
gcc2_compiled.:
.section    .rodata
.LC0:
    .string "plik"
    .align 32
.LC1:
    .string "Nie moge otworzyc pliku 'plik'n"
    .align 32
.LC2:
    .string "Ilosc znakow w pliku wynosi : %ldn"
.text
.align 4
.globl main
    .type    main,@function
main:
    pushl %ebp
    movl %esp,%ebp
    subl $512,%esp
    pushl %edi
    pushl %esi
    pushl %ebx
    pushl $0
    pushl $.LC0
    call open
    movl %eax,%esi
    addl $8,%esp
    cmpl $-1,%esi
    jne .L17
    pushl $.LC1
    call printf
    pushl $1
    call exit
    .p2align 4,,7
.L17:
    leal -512(%ebp),%ebx
    jmp .L18
    .p2align 4,,7
.L20:
    addl %eax,%edi
.L18:
    pushl $512
    pushl %ebx
    pushl %esi
    call read
    addl $12,%esp
    testl %eax,%eax
    jg .L20
    pushl %edi
    pushl $.LC2
    call printf
    pushl %esi
    call close
    pushl $0
    call exit
.Lfe1:
    .size    main,.Lfe1-main
    .ident  "GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)"

Jak widać polecenia asemblerowe różnią się nieco od tych znanych z kompilatrów dosowych. W Linuksie dodajemy do większości poleceń literę "l" np.
movl %eax,%esi -kopiuj wartość z rej. esi do eax
Litery "l" nie dodaje się do polecenia call (chyba wiadomo dlaczego) i do funkcji skoków.

W samym kodzie źródłowym polecenia asemblerowe trzeba poprzedzić odpowiednią dyrektywą np.
asm("movl %esp,%eax");
asm wstawiamy przed każdym poleceniem lub

 __asm__("clcn"
                "1:t"
                "lodslnt"
                "adcl %%eax, %%ebxnt"
                "loop 1bnt"
                "adcl $0, %%ebxnt"
                "movl %%ebx, %%eaxnt"
                "shrl $16, %%eaxnt"
                "addw %%ax, %%bxnt"
                "adcw $0, %%bx");

asm używamy do zgrupowania naszych asemblerowych poleceń. Oczywiście mam świadomość, że asemblera będziesz używać bardzo rzadko lub wcale, jednak jest on dość często używany w różnych exploitach. Programowanie na niskim poziomie daje możliwość pełnej kontroli nad komputerem, ale jest stosunkowo trudne.

Chciałbym przedstawić jeszcze jeden argument wydawany kompilatorowi. Jest to argument optymalizacji -O[numer_optymalizacji]. Generalnie preferuje się optymalizację na poziomie 2, zwiększanie jego poziomu daje często odwrotne do zamieżonych rezultaty. Przykład:

gcc -osystem -O2 system.c

W ten sposób nasz kompilator spróbuje poprawić szybkość i zmniejszyć wielkość programu wynikowego. Aby sprawdzić w jaki sposób to robi można kompilować z opcją -S przed optymalizacją i po niej, a póżniej porównywać kod przejściowy.

Inne funkcje systemowe.

Oprócz funkcji open z flagą O_CREAT do tworzenia pliku można wykorzystać funkcję systemową creat.

Funkcja creat:

int creat(const *ścieżka, mode_t mode);

np.

    int desp;
    desp = creat("/home/reksio/plik", 0622);

Do zmiany pozycji wskaźnika odczytu-zapisu wykorzystujemu funkcję
systemową lseek.

Funkcja lseek:

off_t lseek(int desp, off_t offset, int pozycja_poczatkowa);

Do programu należy dołączyć plik nagłówkowy <unistd.h>.
Przykład użycia:

    off_t pozycja;
    int desp;
    desp = open("plik", O_RDONLY);  
    pozycja = lseek(desp, (off_t)+10, SEEK_SET);

Pozycja wskaźnika jest w tym przypadku 10 bajtem liczonym od początku pliku.

Argument [int pozycja_poczatkowa] może przybierać następujące wartości:
SEEK_SET - początek pliku
SEEK_CUR - bieżace położenie
SEEK_END - koniec pliku


Za pomocą funkcji lseek można w łatwy sposób sprawdzić wielkość pliku np.

    int desp;
    off_t w_pliku;
    desp = open("plik", O_RDONLY);
    w_pliku = lseek(desp, (off_t)0, SEEK_END);

Do obsługi błędów można użyć pliku nagłówkowego <errno.h>, która za pomocą zmiennej errno przekazuje numer błędu, który z kolei jest powiązany z komunikatami o błędach. Aby użyć świadomie tego numeru trzeba znać listę kodów błędów. Na szczęście Linux dostarcza funkcję biblioteczną perror, która pobiera kod błędu i pokazuje odpowiedni komunikat (jeżeli twoje ustawienia lokalizacyjne są konfigurowane na język polski, komunikaty o błędach będą również po polsku).

np.

#include <fcntl.h>
#include <errno.h>
main()
{
    int desp;
 
    if ((desp = open("plik", O_RDONLY)) == -1)
        perror("Blad podczas otwarcia pliku");
}

Dzięki funkcji perror w razie błędu (-1) zostanie pobrany odpowiedni kod błędu i wyświetlony czytelny komunikat. W naszym przypadku w razie nieistnienia pliku "plik" w bieżącym katalogu zostanie wyświetlony komunikat:

Blad podczas otwarcia pliku: Nie ma takiego pliku lub katalogu

Do usuwania plików można zastosować dwie metody: unlink i remove. Remove korzysta z stdio.h

Funkcja unlink i remove:

 
    #include <unistd.h>
    int unlink(const char *ścieżka)
 
    #include <stdio.h>
    int remove(const char *ścieżka)

  1. Procedury biblioteczne {<stdio.h>}.

Plik stdio.h jest również nazywany Standardową Biblioteką I/O. Głównym zadaniem tej biblioteki jest ułatwienie dostępu do pliku oraz zapewnienie efektywnej operacji wejścia/wyjścia. Część funkcji była już pokazana wcześniej: fopen, fclose, getc, putc. Do SB I/O należą również funkcje:printf, scanf, getchar. Są jedne z częściej używanych funkcji służących do komunikacji z użytkownikiem.

4.1. Funkcje wyświetlające.

Funkcja printf:

int printf(const char *kom, arg1, arg2....arg3);

wyświetla ciąg znaków zawarty w między " ". Dodatkowow można wysyłać tzw. conversions specifications. Np.

 
    pi = 3.1415926
    printf("tLiczba pi wynosi: %fn", pi);

po kolei:
t - znak tabulacji przesuwa ciąg wyświetlanych znaków
%f - wyświetli wartość typu float ze zmiennej pi
n - przejście do następnej linii

a więc:

Liczba pi wynosi: 3.1415926

Do wyświetlania znaku % trzeba użyć %%.
Po znaku % używa się róznych liter w celu wyświetlenia zmiennych różnych typów, a więc:

%d - typu int
%u - unsigned int
%o - typ int ósemkowo
%x - typ int szesnastkowo
%X - typ int szesnastkowo ale z dużymi literami ABCDEF
%ld - typ long

%f - typ float lub double
%e - typ float lub double w formie naukowej(wykładniczej)
%g - w zależności od wielkości wyświetli tak jak %f lub %e

%c - typu char
%s - wskaźnik znakowy (napis)


Funkcja fprintf:

int fprintf(FILE *plik, const char *kom, arg1, arg2...arg3);

służy do wyświetlania komunikatów o błędach.

Za każdym razem gdy uruchamiany jest program Linuks otwiera trzy pliki:
stdin - standardowe wejście(klawiatura)
stdout - standardowe wyjście(ekran)
stderr - standardowe wyjście komunikatów o błędach
Wszystkie są ponumerowane odpowiednio 0, 1, 2. Z reguły używa się ich znakowych odpowiedników.

Rozgraniczenie stdout i stderr wynika z tego, że nie zawsze jest potrzeba wysyłania błędów na ekran, równie dobrze można odpowiednie komunikaty wysyłać do pliku.

Funkcja fprintf jest prawie identyczna jak printf, jedynie pierwszy argument zawiera stderr. Np.

fprintf(stderr, "BLAD nr %d w %s", arg1, arg2);

Funkcja sprintf:

int sprintf(char *string, const char *kom, arg1, arg2...argn);

nie jest typową funkcją wypisującą, gdyż wyprowadza komunikat do tablicy
znakowej string. Np.

      char prmpt[81];
      sprintf(prmpt,"%s ",ProgName);

Poniższy program pokazuje parametry środowiska oraz argumenty wpisane w linii poleceń:

#include <stdio.h>
#include <stdlib.h>
 
int i=0;
main(int argc, char* argv[], char* env[])
{
    printf("Parametry srodowiska Linux: n");
    do {
    printf("%s n", env[i]);
    i++;
    }
    while (env[i]!=NULL);
        printf("Lista parametrow programu: n");
 
   for(i=1;i<=argc-1;i++) printf("%s n", argv[i]);
 
    printf("Nazwa programu: n");
    printf("%s n", argv[0]);
    return 0;
}

4.2. Funkcje pobierające.

Są to funkcje odwrotne do funkcji wypisujących. Pobierają one z klawiatury lub z pliku i zapisują do zmiennej odpowiedniego typu.


Funkcja scanf:

int scanf(const char *napis, &zmienna1, &zmienna2...);

pobiera ze standardowego wejścia (stdin) i zapisuje w zmiennej. Znak & (ampresand) to adres w pamięci, gdzie znajduje się zmienna. Np.

    int a, b;
    float c;
 
    scanf("%2d %2d %f", &a, &b, &c);

Funkcja fscanf:

int fscanf(FILE *wskaz, const char *napis, &zmienna1, &zmienna...);

czyta z pliku wskazywanego przez wskaz


Funkcja sscanf:

int sscanf(const char *string, const char *napis, &zmienna1...);

pobiera dane z tablicy string i wpisuje do zmiennych.


Inną często używaną funkcją ze Standardowej Biblioteki I/O jest getchar.
Czyta ona jeden znak ze standardowego wejścia programu (zwykle klawiatura)

getchar:

int getchar(void);

Np.

    int z;
 
    z = getchar();
    if (z==10) printf("Nacisnales ENTER");

Poniższy przykład usuwa plik w argumencie podanym jako ścieżka, program czeka na potwierdzenie operacji.

/*del --program do kasowania pliku*/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
 
FILE *p;
char *uzycie = "Uzycie: del sciezka_do_plikun";
char c[0];
 
int usun(const char *plik)
{
    /*sprawdzanie za pomoca funkcji open czy plik istnieje*/    
    if ((p = fopen(plik,"r")) == NULL)  
    {       
        fprintf(stderr, "Plik %s nie istniejen",plik);      
        return(-1); 
    }   
 
    fclose(p);  
 
    printf("Czy mam na pewno usunac %s? T/Nn", plik);       
    scanf("%s", c); 
 
    if(c[0] == 't' || c[0] == 'T')  
    {       
        remove(plik);       
        printf("Plik usunietyn");   
        return(0);
    }
 
//  printf("%sn",c);
    printf("Anulowano...n");
 
}
 
int main(int argc, char** argv)
{
    if (argc<2) 
    {       
        fprintf(stderr, uzycie);        
        exit(1);    
    }   
 
    usun(argv[1]);
    exit(0);
}

Istnieje oczywiście większa liczba funkcji SB I/O. Jeżeli jesteś ich ciekawy to zaglądnij do stdio.h (/usr/include).

Katalogi i pliki

Pliki

Katalogi i pliki to podstawa każdego systemu plików. W Unixach plik traktowany jest w specjalny sposób. Oprócz tego każdy plik jest czyjąś własnością, a to daje pewne przywileje jego właścicielowi, który może z tym plikiem zrobić co zechce. Przede wszystkim właściciel pliku może nadawać odpowiednie uprawnienia według własnego uznania. Uprawnienia określają prawa dostępu dla różnych użytkowników. Istnieją ich trzy typy:
właściciel
grupa do której należy właściciel
*pozostali użytkownicy.

Prawa dostępu to:
czytanie z pliku (4)
pisanie do niego (2)
*wykonywanie (jeżeli jest to plik wykonywalny) (1).

System przechowuje o każdym pliku dokładne dane, które można podglądnąć używając funkcji stat zawartej w <sys/stat.h>. Najczęściej uprawnienia ustawia się za pomocą zapisu ósemkowego np.

0777 - każdy użytkownik może czytać, pisać i wykonywać plik(4+2+1),
0644 - właściciel może czytać i pisać, osoby należące do tej samej grupy
mogą czytać, czytać mogą pozostali użytkownicy,

Oprócz tego możemy plikom nadać dodatkowe uprawnienia:

04000 - sławny SUID, czyli chwilowe uprawnienia właścieiela są nadawane
każdemu, kto jest właścicielem procesu. Jeżeli właścicielem jest
root w trakcie działania programu z suidem kazdy ma uprawnienia
root'a
02000 - SGID - to samo co SUID tylko dla grupy
01000 - SVTX - obecnie wykorzystywany tylko dla katalogów (bit cachujący)

Do sprawdzenia, czy plik jest dostępny używa się funkcji access


Funkcja access:

    #include <unistd.h>
    int access(const char *ścieżka, int mode);

Argument [int mode]:
R_OK - czy proces może czytać?
W_OK - czy proces może pisać?
X_OK - czy proces może wykonać plik?

W razie błędu funkcja zwraca wartość -1

/*program sprawdza czy jest możliwy dostęp do pliku*/
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int main(int argc, char** argv)
{
    if (argc<2)
    {
        printf("Uzycie: %s plikn", argv[0]);
        exit(1);
    }
 
    if(access(argv[1], R_OK) == -1)
        fprintf(stderr, "Nie mozesz czytac z pliku %sn", argv[1]);
 
    printf("Czytanie z pliku dozwolonen");
 
    if(access(argv[1], W_OK) == -1)
        fprintf(stderr, "Nie mozesz pisać do pliku %sn", argv[1]);
 
    printf("Pisanie do pliku dozwolonen");
 
    if(access(argv[1], X_OK) == -1)
        fprintf(stderr, "Nie mozesz uruchamiać pliku %sn", argv[1]);
 
    printf("Uruchamianie pliku dozwolonen");
 
    exit(0);
 
}

Do zmiany uprawnień dla pliku służy funkcja chmod (podobnie jak z linii
poleceń).

Funkcja chmod:

#inlcude <sys/types.h>
#include <sys/stat.h>
int chmod(const char *ścieżka, mode_t nowy);

Funkcja ta może być używana przez właściciela pliku lub administratora.

    if(chmod("/etc/passwd",0777) == -1)
        perror("");

Do zmiany właściciela pliku oraz grupy służy funkcja chown.

Funkcja chown:

    #include <sys/types.h>
    #include <unistd.h>
 
    int chown(const char *ścieżka, uid_t id_wlaściciela, gid_t id_grupy);

np.

    int wartosc
    if(wartosc = chown("/etc/passwd", 44, 7) == -1)
        perror("");

Możliwa jest przekazanie komuś właśności pliku. Taka operacja nie może być anulowana. Chroni to przed kradzieżą przywilejów.


Za pomocą funkcji link można utworzyć do pliku tzw. twarde łącze. Utworzenie twardego łącza sprawia, że tzw. licznik łączy jest zwiększony. Dopiero usunięcie wszystkich twardych łączy oznacza pełne usunięcie pliku z systemu (funkcja unlink). Licznik łaczy powinien dla tego pliku wynosić 0. Funkcja link nie tworzy kopii pliku, sprawia, że z innego miejsca mamy łączność z plikiem macierzystym. Łącze twarde jest swoistym wskaźnikiem do pliku.

Funkcja link:

#include <unistd.h>
int link(const char *ścieżka_do_pliku_macierzystego, const char *nowa);

np.

link("/home/neo/matrix", "/home/cartman/platrix");

W razie błędu funkcja zwraca -1.


Jak już wcześniej wspomniałem istnieje metoda poznania wszystkich własności
pliku, które zawiera system plików.


Funkcja stat:

#include <sys/types.h>
#inlcude <sys/stat.h>
int stat(const char *ścieżka, struct stat *statystyka);

Strukturę stat najlepiej poznać w praktyce. Oto kod wykorzystujący stat:

/*program pokazuje strukture stat dla podanego pliku*/
 
#include <stdio.h>
#include <sys/stat.h>
 
int main(int argc, char** argv)
{
    struct stat statystyka;
 
    if(argc<2)
    {
    printf("Uzycie: %s plikn",argv[0]);
    exit(1);
    }
 
    if(stat(argv[1], &statystyka) == -1)
    {   
        perror("");
        return(-1);    
    }
 
    printf("Dane dla %s odczytane za pomoca funkcji 'stat' :n", argv[1]);
    printf("Urzadzenie logiczne (st_dev)   : %dn", statystyka.st_dev);
    printf("Numer i-wezla (st_ino)   : %dn", statystyka.st_ino);
    printf("Uprawnienia (st_mode)  : %dn", statystyka.st_mode);
    printf("Liczba laczy (st_nlink) : %dn", statystyka.st_nlink);
    printf(" UID (st_uid) : %dn", statystyka.st_uid);
    printf(" GID (st_gid) : %dn", statystyka.st_gid);
    printf("Opis urzadzenia (st_rdev) : %dn", statystyka.st_rdev);
    printf("Wielkosc pliku (st_size) : %ldn", statystyka.st_size);   
    printf("Ostatni odczyt (st_atime) : %dn", statystyka.st_atime);   
    printf("Ostatnia modyfikacja (st_mtime) : %dn", statystyka.st_mtime);   
    printf("Ostatnia zmiana w 'stat' (st_ctime) : %dn", statystyka.st_ctime);   
    printf("Wielkosc bloku I/O (st_blksize) : %ldn", statystyka.st_blksize);   
    printf("Ilosc fiz. blokow dla pliku (st_blocks) : %ldn", statystyka.st_blocks);   
 
}

*st_dev i st_ino jednoznacznie identyfikują plik, st_dev jest numerem
urządzenia logicznego, na którym znajduje się plik (numer ten można
również poznać przeglądając /proc/sys/kernel/real-root-dev), st_ino to numer i-węzła,
i-węzeł opisuje plik (katalog) w hierarchicznej strukturze systemu
plików (drzewo katalogów)

*st_mode opisuje uprawnienia do pliku (12 najmłodszych bitów), można
również dowiedzieć się jakie urządzenie reprezentuje plik - 060000 to
urządzenie blokowe, 020000 to urządzenie znakowe

*st_nlink pokazuje ilość łączy dla pliku

  • st_uid i st_gid pkazują numer id użytkownika i grupy

*st_rdev pokazuje numer urządzenia nadrzędengo i podrzędnego, numery
te są potrzebne do jednoznacznej identyfikacji sterownika, z którego
dane uzrządzenie korzysta. Numery nadrzędne przydzielone statycznie
są pokazane w /proc/devices. Lepszym sposobem na poznanie tych numerów
jest polecenie ls -l np. w katalogu /dev:

brw-rw----   1 root     disk       3,   1 May  5  1998 hda1
brw-rw----   1 root     disk       3,  10 May  5  1998 hda10
brw-rw----   1 root     disk       3,  11 May  5  1998 hda11
brw-rw----   1 root     disk       3,  12 May  5  1998 hda12
brw-rw----   1 root     disk       3,  13 May  5  1998 hda13
brw-rw----   1 root     disk       3,  14 May  5  1998 hda14
brw-rw----   1 root     disk       3,  15 May  5  1998 hda15
brw-rw----   1 root     disk       3,  16 May  5  1998 hda16
brw-rw----   1 root     disk       3,   2 May  5  1998 hda2
brw-rw----   1 root     disk       3,  64 May  5  1998 hdb
brw-rw----   1 root     disk       3,  65 May  5  1998 hdb1
brw-rw----   1 root     disk       3,  74 May  5  1998 hdb10
brw-rw----   1 root     disk       3,  75 May  5  1998 hdb11

litera b oznacza, że jest to urządzenie blokowe, numer 3 oznacza, że
wszystkie pokazane urządzenia są obsługiwane przez
ten sam sterownik, numer podrzędny pozwala sterownikowi jednoznaczenie
rozumieć, które urządzenie ma być rzeczywiście obsługiwane

*st_size to jedno z ważniejszych z punktu widzenia użytkownika pole,
jest wielkością jaką zajmuje plik

*st_atime, st_mtime, st_ctime to w kolejności: czas ostatniego odczyty,
czas ostatniej modyfikacji, czas ostatniej zmiany informacji w strukturze
stat

*st_blksize to wielkość bloku I/O w systemie plików, w moim przypadku
wartość 4k (dysk podzielony na 4 kilowe klastry)

*st_blocks jest liczbą bloków jaką zajmuje fizycznie plik, wilekość nie pokrywa
się z rzeczywistą wielkością pliku (jeżeli wielkość pliku = 10k, to plik
zajmuje 3 bloki fizyczne czyli 12k). Im mniejsza wielkość bloku I/O w
systemie plików, tym mniejsze straty pojemności dysku.

Katalogi

Katalogi są w systemach Unix traktowane prawie tak samo jak pliki. Jedyna,
ale bardzo ważna różnica to hierarchia jaką tworzą katalogi. Katalogi
porządkują system plików tworząc drzewo katalogów. Katalog jest kontenerem
dla plików. Katalogami są również . i .. - są to łącza do bieżącego katalogu
i jego przodka. Katalog / jest korzeniem. (wypróbuj polecenie ls -ld)

Każdy katalog jest identyfikowany za pomocą tzw. i-węzła np:

             /[1]
             ..[1]
             |
            home[4]
            /  
                 ..[4]
            [5]neo  cartman[6]  

Dla przykładu korzeń będzie miał i-węzeł [1], home[4] itd. Katalog .. w neo i cartman ma ten sam i-węzeł co home, podobnie .. i /.

Podobnie jak pliki również katalogi posiadają swych właścicieli jednak prawa dostępu interpretuje się w nieco inny sposób:

prawo odczytu oznacza możliwość wyświetlenia zawartości katalogu
prawo zapisu umożliwia utworzenie i usunięcie plików w katalogu (oczywiście dostęp do już istniejących plików zależy od uprawnień na poziomie pliku)
*prawo wykonywania pozwala wejść do katalogu za pomocą polecenia cd

Do obsługi katalogów na poziomie programowania istnieje oddzielna rodzina
funkcji.

Tworzenie i usuwanie katalogów:

Funkcja mkdir i rmdir:

#include <sys/types.h>
#include <sys/stat.h>

int mkdir(const char *ścieżka, mode_t mode);

np:

int wartosc;
wartosc = mkdir("/home/uzyt1/kat1", 0644);

#include <unistd.h>

int rmdir(const char *ścieżka);

Funkcja usuwania zadziała tylko wtedy, gdy katalog będzie pusty tzn. będzie
zawierał (.) i (..).


Otwieranie i zamykanie katalogów:


Funkcje opendir i closedir:

#include <sys/types.h>
#include <dirent.h>

DIR *opendir(const char *nazwa_katalogu);
<dirent.h> zawiera definicję DIR (strumień katalogu). Porównaj z FILE. W razie błędu funkcja zwraca NULL. #include <dirent.h> int closedir(DIR *wskanik_dir); Np. DIR *wks_dir; if((wks_dir = opendir("/home/uzyt1/kat1")) == NULL) { perror(""); exit(1); } ... closedir(wsk_dir); ____ Funkcja readdir i rewinddir: #include <sys/types.h> #include <dirent.h> struct dirent *readdir(DIR *wks_dir); void rewinddir(DIR *wks_dir); readdir - służy do odczytania zawartości katalogu dochodząc od początku do końca (wskaźnik NULL) rewinddir - ustawia wskaźnik na początek katalogu Struktura dirent znajduje się w pliku /bits/dirent.h dołączonym do dirent.h i wygląda ona następująco: struct dirent { #ifndef __USE_FILE_OFFSET64 __ino_t d_ino; __off_t d_off; #else __ino64_t d_ino; __off64_t d_off; #endif unsigned short int d_reclen; unsigned char d_type; char d_name[256]; /* We must not include limits.h! */ }; Generalnie wykorzystuje się d_ino(zakres i-węzła katalogu) oraz d_name(nazwa pliku o max. dlugości [256]). Np. struct dirent *dr; DIR *wsk_dir; if((wks_dir = opendir(argv[1])) == NULL) { perror(""); exit(1); } while(dr=readdir(wsk_dir)) { if (dr->d_ino != 0) printf("%sn",dr->d_name); } ____ Funkcja chdir i getcwd: #include <unistd.h> int chdir(const char *ścieżka); sprawia, że ścieżka staje się nowym katalogiem bieżącym dla programu. #include <unistd.h> char *getcwd(char *nazwa_katalogu, ssize_t rozmiar); funkcja wpisuje do [nazwa_katalogu] bieżący katalog roboczy o rozmiarze [rozmiar] Np. char n_kat[50]; getcwd(n_kat, 50); printf("katalog biezacy to %sn",n_kat); w razie błędu funkcja zwraca NULL. ____ Czasami istnieje potrzeba przejścia naszego drzewa katalogów poczynając od określonego miejsca. Do tego najlepiej wykorzystać gotową funkcję ftw: ____ Funkcja ftw: #include <ftw.h> int ftw(const char *ścieżka, int(*funkcja) (), int głębokość); - pierwszy parametr jest ścieżką do katalogu, od którego funkcja rozpocznie rekurencyjne przeszukiwanie - drugi parametr to funkcja, która zostanie uruchomiona za każdym razem ,gdy zmieni się katalog przeszukiwań - parametr trzeci to liczba otwartych katalogów (deskryptory), należy uważać aby nie przekroczyć ich maksymalnej liczby, którą moze obsługiwać system; jeżeli przeszukujemy większą liczbę katalogów to można ustawić ten parametr na liczbę większą niż 1, w ten sposób zwiększy się nieco szybkość przeszukiwania (katalogi nie będą otwierane po każdej zmianie w ścieżce przeszukiwań tylko od razu zostanie pobrany z pamięci kolejny deskryptor) Funkcja wywoływana za każdym razem po zmianie ścieżki bieżącej powinna być wywoływana w następujący sposób: int funkcja(const char *nazwa, const struct stat *statystyka, int typ) - nazwa to wskaźnik do nazwy bieżącego katalogu(pliku) - statystyka to wskaźnik do struktury stat, za każdym razem dla katalogu lub pliku są pobierane dane i umieszczane w strukturze stat (tak jak po wywołaniu funkcji stat) - typ określa z jakiego rodzaju obiektem mamy doczynienia: FTW_F - plik FTW_D - katalog FTW_DNR - katalog bez możliwości odczytu FTW_SL - łącze symboliczne FTW_NS - dla tego obiektu nie można wypełnić stat (nie jest łączem symbolicznym) Poniżej znajduje się krótki program korzystający z tej funkcji. Program pokazuje, które pliki lub katalogi mają uprawnienia 0777. /*program korzystajacy z ftw*/ #include <stdlib.h> #include <sys/stat.h> #include <ftw.h> int szukaj(const char *nazwa, const struct stat *statystyka, int typ) { if(typ == FTW_D) printf("%-79sr",nazwa); if (typ == FTW_D && (statystyka->st_mode&0777) == 0777) printf("r%-20st%dt%3otkatalogn",nazwa, statystyka->st_ino, statystyka->st_mode&0777); if (typ == FTW_F && (statystyka->st_mode&0777) == 0777) printf("r%-20st%dt%3otplikn",nazwa, statystyka->st_ino, statystyka->st_mode&0777); return 0; } main() { printf("SCIEZKAtt I-WEZELtPRAWAtTYPn"); ftw("/", szukaj, 1); //zaczynamy szukać od korzenia printf("n"); exit(0); } --cut-- ____ Krótko o procesach. ============== Każdy uruchomiony program jest w systemie Linux określany jako proces. Każdemu procesowi zostaje przydzielona wirtualna przestrzeń adresowa. Linux rozróżnia trzy typy procesów: - interaktywne - uruchamiane przez powłokę i przez nią kontrolowane - wsadowe - rezydentne (demony) System identyfikuje procesy przydzielając im numer(PID). Za pomocą polecenia ps można z linii powłoki zobaczyć jakie procesy są uruchomione. Za pomocą polecenia kill [numer] można usunąć proces. Stosuje się to głównie w stosunku do niedziałających procesów lub do przeresetowania serwerów. Każdy proces może utworzyć proces potomny. Funkcja jaka jest używana w tym celu nosi nazwę fork(). ____ Funkcja fork: #include <sys/types.h> #include <unistd.h> pid_t fork(void); powoduje utworzenie przez jądro nowego procesu, który jest kopią procesu wywołującego. Proces potomny jest wierną, ale samodzielnie działającą kopią procesu rodzicielskiego - oznacza to, że proces potomny to coś zupełnie różnego od tzw. wątku. Wątek posiada kopię kodu programu, ale korzysta z tych samych zmiennych, co program rodzicielski. pid_t to liczba identyfikująca proces potomny ____ Funkcja fork bardzo często jest wykorzystywana w programach sieciowych typu serwer. Obsługa procesów to bardzo złożony problem. Nam wystarczy na razie posługiwanie się funkcją fork(). Programowanie sieciowe ============== Pisania programów działających w sieci to bardzo ważna umiejętność. Na szczęście system Linux daje nam odpowiednie narzędzia, aby w prosty sposób poznać tę "tajemną wiedzę". Na początku warto się zastanowić w jaki sposób następuje poączenie między dwoma komputerami. Weźmy dwa hosty o numerach 204.23.104.8 i 192.168.20.2. Aby połączenie doszło do skutku trzeba ustalić kilka podstawowych rzeczy: protokół - określony jednoznacznie mechanizm za pomocą którego będzie możliwa wymiana danych (TCP, UDP lub inny ) port - numer 16-bitowy określający z jakim procesem aplikacji ma nastąpić połączenie (telnet - numer 23) gniazdo - można o nim myśleć na dwa sposoby: pierwszy z punktu widzenia systemu to numer jednoznacznie identyfikujący proces , port, który jest przydzielany dynamicznie, drugi z punktu widzenia programisty to specjalny plik (gniazdko), z którym powiązane są odpowiednie struktury i który jest końcowym i początkowym elementem wirtualnego połączenia. Określenie plik w stosunku do gniazda oznacza tu, że o gniazdku można myśleć jako o pliku (deskryptor). Port przydzielany dynamicznie to port, który jest przypisany przez system do programu (klienta). O ile dla sesji telnetowej mamy z góry założony numer 23- port z którym zawsze trzeba się połączyć, aby móc skorzystać z usługi, o tyle klient ,aby mógł być jednoznacznie identyfikowany przez serwer i system hosta, musi mieć przydzielony port (pierwszy wolny w systemie) Przykład: (dla TCP) 1.HOST (192.168.20.2) Uruchomienie klienta system przydziela wolny numer portu 1034 2.SERWER (204.23.104.8) Serwer odbiera rządanie od 192.168.20.2:1034 zapamiętuje je 3.SERWER (204.23.104.8) Serwer wysyła sygnał gotowości na przyjmowanie poleceń Numery portów przydzielone statycznie do określonych usług znajdują się w pliku /etc/services. Aby utworzyć gniazdko sieciowe musimy wypełnić pola w odpowiednich strukturach. Ogólna struktura adresu gniazda jest zdefiniowana w pliku nagłówkowym <sys/socket.h>: ```c struct sockaddr { sa_family_t sa_family; /*typ*/ char sa_data[]; /*adres*/ }; ``` jest to gniazdo ogólne (może również służyć do komunikacji między procesami). Gniazdo do komunikacji sieciowej zostało zdefiniowane w <netinet/in.h>: ```c struct sockaddr_in{ sa_family_t sin_family; /*rodzaj adresu internetowego*/ in_port_t sin_port; /*numer portu*/ struct in_addr sin_addr; /*specjalna struktura do przechowania IP*/ unsigned char sin_zero[8] /*wypełnienie*/ }; ``` Aby utworzyć gniazdo musimy określić jego rodzaj za pomocą funkcji socket. ____ Funkcja socket: #include <sys/socket.h> int socket(int rodzaj, int typ, int protokół); argument 1 - rodzaj adresów: AF_INET - adresy internetowe AF_UNIX - do komunikacji między procesami znajdującymi się na tym samym komputerze argument 2 - typ gniazda: SOCK_STREAM - tryb połączeniowy, wykorzystuje strumień, jest to protokół TCP SOCK_DGRAM - tryb bezpołączeniowy oparty na datagramach, jest to protokół UDP (jest to tryb zawodny, ale szybciej działający, dość rzadko się go używa) argument 3 - określa, który protokół powinien być używany przez gniazdo ustawiany na zero (domyślny protokół: dla SOCK_STREAM TCP, dla SOCK_DGRAM UDP) ____ Do gniazda przypisywane są dwa adresy: hosta i serwera. Adres hosta jest dopisywany automatycznie przez system. Adres serwera trzeba podać. Można zrobić to na dwa sposoby: 1. Jeżeli klient ma służyć do łączenia się z jednym określonym adresem możemy ustawić to na sztywno wypełniając strukturę: struct sockaddr_in adres; //utworzenie struktury adres typu sockaddr_in adres.sin_addr.s_addr = inet_addr("204.23.104.8");//wypełnienie adresem 2. O wiele częściej istnieje potrzeba podania adresu (również nazwy). Do tego służy funkcja gethostbyname. ____ Funkcja gethostbyname: ```c #include <netdb.h> struct hostent *gethostbyname(const char *name) ``` szuka numeru IP dla podanej nazwy, jeżeli od razu podamy numer funkcja wypełnia strukturę hostent (funkcja poszukuje IP w kolejności zdefiniowanej w systemie np. plik /etc/hosts a później serwer DNS) Struktura hostent znajduje się w netdb.h. ```c struct hostent { char *h_name; //nazwa hosta char **h_aliases; //lista aliasów int h_addrtype; //typ adresu zdefiniowany w <sys/socket.h> int h_length; //rozmiar adresu w bajtach (dla IP zawsze 4) char **h_addr_list; #define h_addr h_addr_list[0]; //lista adresów IP }; ``` ____ Kiedy mamy wszystkie potrzebne dane należy nawiązać połączenie za pomocą funkcji connect. ____ Funkcja connect: ```c #include <sys/types.h> #include <sys/socket.h> int connect(int desp, const struct sockaddr *adres, size_t add_len); ``` - 1 -deskryptor gniazda uzyskany po wywołaniu funkcji socket (deskryptor ten ma takie samo znaczenie jak w przypadku plików dlatego też do obsługi gniazda można użyć funkcji read i write) - 2 - adres do struktury typu sockaddr - 3 - wielkość struktury w bajtach ____ Gdy zostaje nawiązane połączenie należy napisać do gniazda coś sensownego, wszystko zależy z jaką usługą mamy doczynienia. Dla każdej usługi istnieje pewien z góry określony sposób komunikacji (standardy komunikacji dla usług są podawane w dokumentach RFC (Request For Comments)). Poniżej znajduje się program korzystający z usługi finger. Po wpisaniu dowolnego parametru program zapisuje w pliku odpowiednie struktury (pod warunkiem, że mamy uprawnienia root'a) ```c /* * Nazwa programu : k_finger+ * * Opis : klient uslugi finger z mozliwoscia podgladu * struktur * * Sposob uzycia : k_finger+ host_docelowy uzytkownik [z] * z-dowolny argument (pokazuje struktury) * np : k_finger+ 192.168.20.203 root * * Wymagania : UNIX/LINUX * dodatkowe: mozliwosc podgladu struktur tylko z poziomu * root'a lub z atrybutem +s dla programu * * */ #include <stdio.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> //MAIN int main(int argc, char** argv) { struct hostent *host; //deklaracja struktury host typu hostent struct servent *serv; //deklaracja struktury serv typu servent struct sockaddr_in adres; //deklaracja struktury adres typu sockaddr_in int gnz, i; char buf[1024]; FILE *file; ////////////////////////////////////////////////////////////////////////////// printf("ntt------------------------------"); printf("ntt k_finger+ Ver.0.00002 -2000-"); printf("ntt------------------------------nn"); ////////////////////////////////////////////////////////////////////////////// if (argc<3) { printf("Uzycie: %s [host_docelowy] [uzytkownik] [jakis arg do pok. strukt.]nn",argv[0]); return 1; } adres.sin_family=AF_INET; //adresy internetowe host=gethostbyname(argv[1]); //poszukiwanie IP dla nazwy serwera if (host!=NULL) //jeżeli powiodło się to wypełnij strukturę adres { bcopy(host->h_addr,(char *)&adres.sin_addr,host->h_length); ///// ///// } else { perror ("Blad: Nie moge znalezc hosta_docelowego"); return 1; } adres.sin_port=htons(79); //wpisz do struktury adres numer portu //funkcja htons formatuje liczbę 79 (port fingera) //w odpowiednio zrozumiały dla kompa sposób gnz=socket(AF_INET,SOCK_STREAM,0); //utworzenie gniazda TCP (mamy deskryptor) if (gnz==-1) { perror("Blad funkcji socket!"); return 1; } if (connect(gnz, (struct sockaddr *)&adres, sizeof(adres))<0) //połącz { close(gnz); //w razie błędu zamknij gniazdo // i pokaż rodzaj błędu perror("Nie moge polaczyc!"); } //komunikacja write (gnz, argv[2],strlen(argv[2])); //piszemy do gniazda użytkownika write (gnz, "rn",2); // i dodajemy znak powrotu karetki i końca linii while ((i=read(gnz,buf,sizeof(buf)))>0) // odbieramy dane z serwera write (0,buf,i); //wypisanie na ekran if(!geteuid()) //jeżeli jest to proces root'a { if(argv[3]) // i podano dodatkowy parametr { file=fopen("struct.log","w"); fprintf(file,"ntKontrolne sprawdzanie strukturnntStruktura hostent :n th_name = %sntth_aliases = %sntth_addrtype = %d th_length = %dntth_addr = %s nntStruktura sockaddr_in :n tsin_family = %dnttsin_port = %d nttsin_addr = %s tsin_zero = %sn", host->h_name,host->h_addrtype,host->h_length,adres.sin_family, ntohs(adres.sin_port),adres.sin_zero); fclose(file); } } close(gnz); return 0; } ``` Plik struct.log może wyglądać tak: Kontrolne sprawdzanie struktur Struktura hostent : h_name = 192.168.20.203 h_addrtype = 2 h_length = 4 Struktura sockaddr_in : sin_family = 2 sin_port = 79 sin_zero = //tu mogą być jakieś śmiecie W przypadku usługi finger serwer oczekuje od klienta podania nazwy użytkownika oraz dodania znaków rn. Tylko tak sformatowany tekst jest zrozumiały dla serwera - w innym przypadku serwer zakończył by połączenie zwracając błąd. Inaczej jest w usługach typu SMTP - POP3, gdzie należy przeprowadzić dłuższą konwersację z zastosowaniem określonych reguł - poleceń. W przypadku SMTP połączenie wygląda następująco (klient): 1. helo 2. mail from:<e-mail> 3. rcpt to:<e-mail> 4. data 5. quit Serwer za każdym razem zwraca odpowiedni numer jako odpowiedź (np. 250 oznacza poprawną pracę serwera i przyjęcie polecenia). Dokładny opis protokołu znajduje się w dokumencie RFC 821. Poniżej przykład klienta pocztowego korzystającego z SMTP. ```c /* * * Nazwa programu : smtp_klient Ver.0.1 * * Opis : program do wysylania poczty * * Uzycie : smtp_klient [do_kogo] [na_jaki_host] * * Wymagania : UNIX/LINUX + Development * */ #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <errno.h> #include <string.h> /////////// /////////// struct hostent *host; struct sockaddr_in adres; int gnz,x; FILE *d; char buf1[15]; char buf2[40]; char buf3[1024]; ///////////////////////////////////MAIN/MAIN/MAIN//////////////////////////// int main(int argc, char** argv) { printf("nttt------------------------n"); printf("ttt SMTP-klient Ver.0.1n"); printf("ttt------------------------nn"); if (argc<3) { printf("Uzycie: %s [adresat] [host_docelowy]n",argv[0]); printf("Uwaga: program uzywa formy [email protected]_docelowyn"); exit(0); } host=gethostbyname(argv[2]); if (!host) { perror("gethostbyname"); exit(1); } bzero((char *)&adres,sizeof(adres)); //zerowanie struktury adres fprintf(stderr,"Tworzenie wiadomosci: nn"); bcopy(host->h_addr,(char*)&adres.sin_addr,host->h_length); adres.sin_family=host->h_addrtype; adres.sin_port=htons(25); /////////////////////////////////////////////////////////////////////// if ((gnz=socket(AF_INET,SOCK_STREAM,0))==-1) { perror("BLAD podczas tworzenia gniazda"); return 1; } if (connect(gnz,(struct sockaddr *)&adres,sizeof(adres))<0) { close(gnz); switch (errno) { default : perror("Nie moge polaczyc!"); return 1; } } printf("<adres nadawcy="NADAWCY">: "); scanf("%s",&buf1); printf("n<temat>: "); scanf("%s",&buf2); printf("n<tekst>: "); //w tym miejscu powinna znalezc sie obsluga //wpisywania dlgiego tekstu i konczenie go . // jak w programie mail scanf("%s",&buf3); if (!(d=fdopen(gnz,"w"))) { perror(""); exit(0); } ////////////////////////////////////////////////////////////////////// fprintf(d,"helo HOSTn"); fprintf(d,"mail from: <%s>n",buf1); fprintf(d,"rcpt to: <%s>n",argv[1]); fprintf(d,"datan"); fprintf(d,"To: %sn",argv[1]); fprintf(d,"Subject: %sn",buf2); fprintf(d,"%sn",buf3); fprintf(d,".n"); fprintf(d,"quitn"); fflush(d); //wysłanie zawartosci bufora do pliku zwiazanego ze //strumieniem (u nas plik to gniazdko) shutdown(gnz,2); //zamknięcie połaczenia close(gnz); //zwolnienie deskryptora gniazda } ``` W kliencie SMTP użyłem nowej funkcji fdopen. Funkcja ta jest niemal identyczna z fopen - różnica jest taka, że zamiast scieżki do pliku podaje się deskryptor (u nas gnz uzyskany z funkcji socket()). Funkcja shutdown przeprowadza proces zamykania połączenia (uwaga: nie zwalnia deskryptora), jest to jeden ze sposobów zamykania połączenia. Deksryptor zwalniamy przez close(). Skoro umiemy wysłać pocztę to przydał by się program do jego odbierania. O ile do wysyłania korzystamy z protokołu SMTP (numer 25), o tyle do odbierania używamy protokołu POP3. Wynika to z tego, że SMTP zmusza komputer docelowy do ciagłego działania - inaczej poczta nie może być dostarczona. Dlatego stworzono POP3 (wcześniej POP2). POP3 operuje na tzw. skrzynkach. Każda skrzynka to nic innego jak konto (użytkownik+hasło) jednak bez możliwości logowania (shell ustawiony na false). SMTP jest instalowany domyślnie w systemie Linux. POP3 jest dodatkową usługą i wymaga oddzielnego uruchomienia. Trzeba w pliku /etc/inetd.conf odznaczyć zaremowaną linijkę do pop3 (usunąć #), a następnie zainstalować pakiet imap. Po tym wystarczy przerestartować i mamy serwer poczty(!). Poniżej znajduje się krótki klient dla pop-3. ```c /* pop.c * * Nazwa programu : pop * * Opis : program do odbioru poczty (pop3) * * Uzycie : pop [host] * * Wymagania : UNIX/LINUX + Development * */ #include <stdio.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <curses.h> int gnz, i, q, lp=0; char buf[1024]; char ret[20]={"RETR "}; char usun[20]={"DELE "}; char znak, wyb; void przeglad(); void usuwanie(); WINDOW *okno; int main(int ilarg, char** argv) { struct hostent *host; struct servent *serv; struct sockaddr_in adres; char nazwa[100]; char haslo[100]; char uz[20]={"USER "}; char ha[20]={"PASS "}; if (ilarg<2) { printf("Uzycie: %s hostn,argv[0]");return 1;} okno=initscr(); scrollok(okno,1); wrefresh(okno); cbreak(); bzero((char*)&adres, sizeof(adres)); //zerowanie struktury adres adres.sin_family=AF_INET; host=gethostbyname(argv[1]); if(host!=NULL) bcopy(host->h_addr, (char*)&adres.sin_addr, host->h_length); else { perror("Blad: nie moge znalezc hosta"); endwin(); return 1; } serv=getservbyname("pop-3", "tcp"); //łaczy sie z serwerem i sprawdza //numer portu dla pop-3 if(serv!=NULL) { adres.sin_port=serv->s_port; } else { perror("Blad getservbyname"); endwin(); return 1; } gnz=socket(PF_INET, SOCK_STREAM,0); if (gnz==-1) { perror("Blad funkcji socket"); endwin(); return 1; } if (connect(gnz, (struct sockaddr*)&adres, sizeof(adres))<0) { close(gnz); perror("Nie moge polaczyc"); endwin(); } i=read(gnz, buf, sizeof(buf)); write(0, buf, i); do { wprintw(okno, "nPodaj nazwe skrzynki pocztowej: "); getstr(nazwa); for (q=5; q<=4+strlen(nazwa); q++) { uz[q]=nazwa[q-5]; } wprintw(okno,"Podaj haslo: "); q=0; do { znak=getch(); mvwdelch(okno,2,13); if (znak==10) break; else haslo[q++]=znak; }while(znak!=10); haslo[q]=0; for (q=5; q<=4+strlen(haslo); q++) { ha[q]=haslo[q-5]; } write(gnz, uz, strlen(uz)); write(gnz,"rn",2); i=read(gnz, buf, sizeof(buf)); //write(0, buf, i); if (buf[0]=='+') { write(gnz, ha, strlen(ha)); write(gnz,"rn",2); i=read(gnz, buf, sizeof(buf)); //write(0, buf, i); } clear(); if(buf[0]=='-') wprintw(okno,"Blad logowania"); lp++; }while((buf[0]=='-')&&(lp<3)); if(buf[0]=='+') wprintw(okno,"nMasz %c wiadomosci nn",buf[18]); if((buf[18]!='0')&&(buf[0]=='+')) { do { wprintw(okno," MENU n"); wprintw(okno," 1-przeglad n"); wprintw(okno," 2-usuwanie n"); wprintw(okno," 3-koniec n"); wyb=getch(); switch(wyb) { case '1': przeglad(); break; case '2': usuwanie(); break; case '3': break; } }while(wyb!='3'); } wrefresh(okno); write(gnz, "QUIT", strlen("QUIT")); write(gnz,"rn",2); wprintw(okno,"n"); wrefresh(okno); i=read(gnz, buf, sizeof(buf)); write(0, buf, i); close(gnz); endwin(); return 0; } void przeglad() { wprintw(okno,"nPodaj numer wiadomosci do przegladniecia : n"); znak=getch(); ret[5]=znak; clear(); //czyszczenie okna wrefresh(okno); write(gnz, ret, strlen(ret)); write(gnz,"rn",2); i=read(gnz, buf, sizeof(buf)); write(0, buf, i); move(0,25); } void usuwanie() { wprintw(okno,"nPodaj numer wiadomosci do usuniecia : n"); znak=getch(); usun[5]=znak; write(gnz, usun, strlen(usun)); write(gnz,"rn",2); clear(); //czyszczenie okna wrefresh(okno); i=read(gnz, buf, sizeof(buf)); write(0, buf, i); move(0,25); //przesuwanie do punktu 0,25 } ``` Sam program nie jest jakąś rewelacją. Przy kompilacji należy dodać opcję lcurses czyli w linii wpisujemy gcc -opop pop.c -lcurses. Wynika to z tego, że wykorzystany tu został tryb pełnoekranowy z wykorzystaniem biblioteki <curses.h>. Na początku deklarujemy chęć użycia okna WINDOW *okno: okno=initscr(); //inicjujemy okno wrefresh(okno); //odświeżamy okno Odświeżanie powinno następować po każdej mozliwej zmianie w oknie. Polecenie wprintw to to samo co printf - należy tylko podać do jakiego okna piszemy (okien moze być kilka). W programie użyto jeszcze jednej ważnej funkcji: mvwdelch(okno,2,13); - przydała się ona w trakcie wpisywania hasła do skrzynki, każdy wpisany znak jest kasowany poprzez przeniesienie kursora do punktu ekranu 2,13 - dzięki temu wpisywane hasło jest niewidoczne. Protokół jest opisany w RFC 1725 dlatego nie będę szczegółowo go opisywał. Wymienię tylko podstawowe polecenia: -USER [uzytkownik] -PASS [haslo] -RETR [numer] - pobranie konkretnej wiadomości -DELE [numer] - skasowanie wiadomości -QUIT - wyjście Serwer zwraca w przypadku powodzenie +OK lub niepowodzenia -ERR. Polecenia podstawowe są użyte w kodzie. Każdy protokół można sprawdzić bez odpowiedniego klienta, wystarczy do tego telnet np. # telnet host 110 Poniżej znajduje się prosty, ale nieco bliższy hakerce kod. Jest to skaner portów: ```c /* -*-C-*- tcpprobe.c */ /* tcpprobe - report on which tcp ports accept connections */ /* IO ERROR, [email protected], Sep 15, 1995 */ #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <errno.h> #include <netdb.h> #include <signal.h> int main(int argc, char **argv) { int probeport = 0; struct hostent *host; int err, i, net, filedes; struct sockaddr_in sa; if (argc != 2) { printf("Usage: %s hostnamen", argv[0]); exit(1); } for (i = 1; i < 1024; i++) { strncpy((char *)&sa, "", sizeof sa); sa.sin_family = AF_INET; if (isdigit(*argv[1])) sa.sin_addr.s_addr = inet_addr(argv[1]); else if ((host = gethostbyname(argv[1])) != 0) strncpy((char *)&sa.sin_addr, (char *)host->h_addr, sizeof sa.sin_addr); else { herror(argv[1]); exit(2); } sa.sin_port = htons(i); net = socket(AF_INET, SOCK_STREAM, 0); if (net < 0) { perror("nsocket"); exit(2); } err = connect(net, (struct sockaddr *) &sa, sizeof sa); if (err < 0) { printf("%s %-5d %sr", argv[1], i, strerror(errno)); fflush(stdout); } else { printf("%s %-5d accepted. n", argv[1], i); if (shutdown(net, 2) < 0) { perror("nshutdown"); exit(2); } } close(net); } printf(" r"); fflush(stdout); return (0); } ``` Działanie tego skanera jest proste - łączy się (pętla) z kolejnymi numerami portów w odległym hoście i sprawdza czy połączenie doszło do skutku, jeśli tak to zwraca numer portu (accepted). Jest to podstawowe narzędzie pozwalające zorientować się które usługi serwera są na chodzie. Można nim też sprawdzać, czy ktoś nie założył sobie na serwerze backdoora pod jakimś dziwnym portem (należałoby zwiększyć ilość portów do przeszukiwania of cos). Program można nieco rozbudować np. robiąc plik z nazwami usług, jeżeli jest to usługa z numerem 7, to powinna się znaleźć w siódemj linijce w pliku. Za pomocą funkcji lseek można by odczytać nazwę usługi dla odpowiadającego jej numeru (zmienna i). Prostsze mogłoby być utworzenie tablicy z tymi nazwami (znajdują się w pliku /etc/services). ____ Pora teraz na przyjrzenie się programowaniu serwera. Proces serwera różni się główni od klienta tym, że musi działać, być biernym i dostępnym w momencie próby połączenia od strony klienta. Serwery popularnych usług działają jako tak zwane demony (inaczej programy rezydentne). Dodatkowo w systemie Linuks usługi takie jak: inetd i network są wymagane przez wszystkie systemy sieciowe. Demon inetd (uruchamiany przez skrypt /etc/ini.d/inet lub inny podczas ładowania systemu) jest używany do uruchamiania większości usług sieciowych (tzw. usług na żądanie). Inetd na żądanie z innego hosta uruchamia odpowiednią usługę. Dzięki temu liczba demonów może być zmiejszona do minimum. Zamieszczony przykład będzie uruchamiany w shellu. Podstawowym zadaniem serwera jest nasłuchiwanie żądań połączeń od klientów. Z tego też względu serwer zachowuje się biernie. Dodatkowo serwer może działać na dwa sposoby: tworzy kolejkę połączeń, jest wieloprocesowy. Kolejka połączeń sprawia, że w jednym momencie może zostać obsłużony tylko jeden klient, reszta czeka w kolejce (jej max. ilość jest z góry ustawiana). Lepszym sposobem jest zaprogramowanie serwera wieloprocesowego. Do tego rozwiązania przydaje się właśnie funkcja fork(). Tworzy ona dla każdego połączenia odrębny proces. Aby ustawić serwer w tryb nasłuchiwania trzeba ustawić gniazdko nasłuchujące. Używamy do tego celu trzech funkcji: socket, bind, listen. ____ Funkcja bind: ```c #include <sys/socket.h> int bind(int desp, const struct sockaddr *nazwa, int dl_nazwy); ``` - desp - deskryptor gniazda uzyskany z funkcji socket - nazwa - struktura sockaddr - dl_nazwy - rozmiar struktury nazwa. Funkcja ta określa numer portu - wszystkie żądania do tego portu będą kierowane do gniazda serwera. ____ Funkcja listen: ```c #include <sys/socket.h> int listen(int desp, int kolejka); ``` - desp - deskryptor gniazda uzyskany z funkcji socket - kolejka - max. liczba nie obsłużonych nadchodzących połączeń Funkcja listen może być użyta tylko w gniazdach typu strumieniowego (TCP). Przepełnienie kolejki powoduje zwrócenie błędu do klienta. Nawet w przypadku wykorzystania funkcji fork() kolejka powinna mieć odpowiedni rozmiar, gdyż serwer może nie nadążać z tworzeniem procesów potomnych. ____ Po ustawieniu gniazda w tryb nasłuchujący serwer powinien móc obsłużyć pierwsze żądanie z kolejki połaczeń. Do sprawdzania i akceptacji żądania służy funkcja accept(). ____ Funkcja accept: ```c #include <sys/socket.h> int accept(int desp, struct sockaddr *adres, int *dl_adres); ``` - desp - deskryptor gniazda nasłuchującego - adres - struktura sockaddr - dl_adres - rozmiar struktury adres Funkcja tworzy nowe gniazdo i zwraca do niego deskryptor. Nowe gniazdo jest wypełniane adresem hosta żądającego.

12 komentarzy

swietny art oby więcej takich rzeczowych i KONTRETKNYCH artoow dla Linuxa. wiecej wiecej wiecej

poprawilem wiele w Programowanie sieciowe bo nie widzialem naglowkow include //

Poprawiłem kilka kodów (tzn, wziąłem je w bloki code) bo widzę, że nikt nie wprowadzał poprawek :(. Mam nadzieję, że ktoś (może autor? :)) poprawi resztę, żeby było czytelniejsze.

ciekawy art, dopiero teraz go zauwazylem :]
mala uwaga: w tej chwili niektore fragmenty sa nieczytelne (jak np. nazwy bibliotek w dyrektywie include)

Arcik jest gitt... teraz będe wprowadzał w życie te przykłądy :]

Art super;)
Sam jestem uzytkownkiem Linuxa od
niedawna i przyda sie na pewno.

w artykule w czesci o asmie jest napisane tak:
"movl %eax,%esi -kopiuj wartość z rej. esi do eax "
ta instrukcja skopiuje eax do esi, bo w skladnii asmowej AT&T czyta sie odwrotnie!!!

hm.. ja bym wolał żebyś z tego naprzykład sklepał kurs albo podzielił na kilka artów :))

Wszystko ladnie, tylko staraj sie pisac wlasne materialy, a nie przepisywac gotowe...

No właśnie przecież C jest stworzony do środowiska UNIX (Linux) Tak trzymać ! Oby więcej w tym temacie

Do krótkich ten artykuł chyba nie należy....

Ciekawy. Przymierzam się do Lincha może wkońcu zainstaluje sobie...