Stringhe

Da Hacknowledge.

La gestione delle stringhe è alla base della programmazione in qualsiasi linguaggio di programmazione. Ogni oggetto che viene stampato sullo schermo è una stringa. I messaggi che abbiamo scritto finora su stdout con la printf non sono altro che stringhe, lo stesso vale anche per le stringhe di formato della scanf ecc.

In C una stringa non è altro che un array di elementi di tipo char. Linguaggi di programmazione più moderni, come Java, Perl, Python, PHP e lo stesso C++, tramite l'uso della classe 'string', consentono di usare le stringhe in modo più avanzato, come tipi predefiniti all'interno del linguaggio stesso. La visione del C (ovvero stringhe=array di tipo char) può essere più macchinosa e a volte anche più pericolosa, ma mette in mano al programmatore la gestione di questo tipo di entità al 100%.

Indice

[modifica] Dichiarazione di una stringa

Abbiamo visto che in C una stringa non è altro che un array di elementi di tipo char. Questo ci fa pensare subito a un tipo di dichiarazione immediato (ma alquanto scomodo):

char my_string[] = { 'H','e','l','l','o' };

La dichiarazione vista sopra non è comodissima, ragion per cui il C consente di dichiarare le stringhe direttamente così:

char my_string[] = "Hello";

o ancora così, sfruttando una scrittura di tipo puntatore:

char *my_string = "Hello";

Ovviamente possiamo anche dichiarare delle stringhe senza inizializzarle. In questo caso le dichiariamo specificando il nome e la dimensione:

char my_string[20];  // Stringa che può contenere 20 caratteri

e vale anche lo stesso discorso che abbiamo fatto con gli array per l'inizializzazione dinamica di una stringa:

char *my_string;
int n;
 
.......
 
printf ("Quanti caratteri deve contenere la tua stringa? ");
scanf ("%d",&n);
 
my_string = (char*) malloc (n*sizeof(char));

Per leggere una stringa invece possiamo ricorrere alla funzione scanf, passando come stringa di formato '%s':

char str[20];
 
........
 
printf ("Inserisci una stringa: ");
scanf ("%s",str);

Si noti che non ho usato la scrittura '&str' nella scanf, in quanto la stringa già di suo rappresenta un puntatore (in quanto un array non è altro che, a livello del compilatore, un puntatore al suo primo elemento, come abbiamo visto prima).

Attenzione: l'uso della scanf per la lettura delle stringhe è potenzialmente dannoso per la stabilità e la sicurezza di un programma. In seguito valuteremo metodi per fare letture in tutta sicurezza.

Esercizio pratico: un programmino che prende in input una stringa e trasforma tutti i suoi eventuali caratteri alfabetici maiuscoli in caratteri minuscoli:

#include <stdio.h>
#include <string.h>
 
// Funzione per la conversione di tutti i caratteri
// maiuscoli in caratteri minuscoli
void toLower(char *s)  {
     int i;
 
     for (i=0; i<strlen(s); i++)
          // Se il carattere corrispondente della stringa è
          // un carattere maiuscolo, ovvero è compreso tra A e Z...
          if ( (s[i]>='A') && (s[i]<='Z') )
               s[i]+=32;
}
 
main()  {
     char s[20];
     int i;
 
     printf ("Inserisci una stringa: ");
     scanf ("%s",s);
 
     toLower(s);
     printf ("Stringa convertita completamente in caratteri minuscoli: %s\n",s);
}

Da notare l'uso della funzione strlen, definita in string.h. Tale funzione ritorna la lunghezza di una stringa, ovvero il numero di caratteri presenti fino al carattere terminatore della stringa. Ogni stringa possiede infatti un carattere terminatore per identificarne la fine (ovvero fin dove il compilatore deve leggere il contenuto della stringa). Tale carattere è, per convenzione, il carattere NULL, identificato dalla sequenza di escape '\0' e associato al codice ASCII 0. Ogni stringa quindi, anche se non è specificato, ha N+1 caratteri, ovvero gli N caratteri che effettivamente la compongono e il carattere NULL che ne identifica la fine:

char *str = "Hello";
// In realtà a livello del compilatore 'str' è vista come
// 'H','e','l','l','o','\0'

Con le conoscenze che abbiamo in questo momento possiamo anche capire come è scritto il codice della funzione strlen:

int strlen(char *s)  {
  int len;
 
  for (len=0; s[len]!=0; len++);
  return len;
}

ovvero un ciclo for dove la variabile contatore viene incrementata finché il carattere corrispondente all'indice all'interno della stringa non è uguale al carattere NULL (appunto con codice ASCII uguale a 0). Il valore della variabile contatore a questo punto rappresenta il numero effettivo di caratteri fino al NULL, ovvero il numero effettivo di caratteri all'interno della string, valore che viene ritornato dalla funzione. Scrivere un

for (i=0; i<strlen(s); i++)

equivale quindi a dire "cicla finché la stringa s contiene dei caratteri, o finché non viene raggiunta la fine della stringa".

Questa scrittura

if ( (s[i]>='A') && (s[i]<='Z') )
  s[i]+=32;

equivale a dire "se il carattere attuale è maggiore o uguale ad A e minore o uguale a Z, ovvero è una lettera maiuscola, somma al suo valore ASCII attuale il valore 32". 32 è l'offset che nella tabella dei caratteri ASCII esiste tra i caratteri maiuscoli e quelli minuscoli. Per verificare:

printf ("%d\n",'a'-'A');

[modifica] Operare sulle stringhe - La libreria string.h

Abbiamo incontrato nel paragrafo precedente la funzione strlen, definita in string.h. Questo header mette a disposizione molte funzioni per operare su questi tipi di dati. Tenteremo di esaminare le più importanti nel corso di questo paragrafo.

[modifica] strcmp

La funzione strcmp (STRing CoMPare) confronta tra di loro i valori di due stringhe, il suo prototipo è qualcosa di simile:

int strcmp(const char *s1, const char *s2);

dove s1 e s2 sono le due stringhe da confrontare. La funzione ritorna

  • Un valore > 0 se da un confronto byte a byte s1 ha più caratteri il cui codice ASCII è maggiore del corrispondente codice ASCII di s2
  • 0 se le due stringhe sono uguali
  • Un valore < 0 nei casi rimanenti

questa funzione è utilizzatissima per vedere se due stringhe hanno lo stesso contenuto. Sono infatti da evitare come la peste scritture del genere:

char *s1;
char *s2="pippo";
 
........
 
if (s1==s2)
  printf ("Ciao pippo\n");

questo perché la scrittura sopra non fa altro che confrontare gli indirizzi fisici in memoria delle due stringhe, ed effettuare le operazioni richieste se gli indirizzi coincidono. Ciò ovviamente non sarà mai verificato, dato che due variabili diverse in memoria hanno anche indirizzi diversi, quindi il codice scritto sopra è praticamente inefficiente. Per confrontare due stringhe è invece necessario ricorrere alla funzione strcmp, ricordando che la funzione ritorna 0 quando il contenuto di due stringhe è lo stesso. Ecco quindi la versione corretta del codice di sopra:

char *s1;
char *s2="pippo";
 
........
 
if (!strcmp(s1,s2))
// Equivale a scrivere
// if (strcmp(s1,s2)==0)
  printf ("Ciao pippo\n");

[modifica] strncmp

La funzione strncmp è molto simile a strcmp, con l'eccezione che confronta solo i primi n caratteri sia di s1 che di s2. La sua sintassi è qualcosa di simile:

int strncmp(const char *s1, const char *s2, size_t n);

dove n è il numero di caratteri da confrontare.

[modifica] strcpy

La funzione strcpy copia una stringa in un'altra. La sua sintassi è qualcosa di simile:

char *strcpy(char *dest, const char *src);

dove dest è la stringa all'interno della quale viene copiato il nuovo valore e src è la stringa da copiare. Il valore di ritorno della funzione è un puntatore a dest.

Quando si vuole copiare una stringa in un altra è infatti sconsigliabile usare una scrittura del genere:

char *s1="pippo";
char *s2;
 
s2=s1;  // ATTENZIONE!!

La scrittura di sopra infatti copia il puntatore al primo elemento della stringa s1 nella stringa s2. Ciò vuol dire che ogni eventuale modifica di s1 modifica anche s2, dato che entrambe le variabili agiscono sulla stessa zona di memoria, e viceversa, il che è decisamente un effetto collaterale. La scrittura corretta è qualcosa del tipo

char *s1="pippo";
char s2[32];
 
strcpy(s2,s1);

in quanto la funzione strcpy genera in s2 una copia esatta di s1, che però, essendo residente in una zona di memoria diversa, è completamente indipendente da s1.

La funzione strcpy ha un codice simile:

char* strcpy (char *s1, char *s2)  {
  int i;
 
  // Finché la stringa s2 ha dei caratteri...
  for (i=0; i<strlen(s2); i++)
    // ...copia il carattere in s1
    s1[i]=s2[i];
 
  return s1;
}

Questa funzione è però potenzialmente dannosa per la sicurezza dell'applicazione. Vedi il paragrafo su "Uso delle stringhe e sicurezza del programma".

[modifica] strncpy

La funzione strncpy ha una sintassi molto simile a strcpy, con la differenza che copia solo i primi n caratteri della stringa sorgente nella stringa di destinazione, per tenere il processo di copia sotto controllo ed evitare problemi di sicurezza nell'operazione, come vedremo in seguito. La sua sintassi è

char *strncpy(char *dest, const char *src, int n);

La sintassi è la stessa di strcpy, a parte per n, che identifica appunto il numero di caratteri di src che verranno copiati in dest. L'uso di questa funzione è preferibile a quello di strcpy quando possibile, proprio per evitare problemi di sicurezza legati ad una copia non controllata.

[modifica] strcat

La funzione strcat concatena l'inizio di una stringa alla fine di un'altra. La sua sintassi è

char *strcat(char *dest, const char *src);

dove dest è la stringa alla cui fine viene concatenata la stringa src. Esempio di utilizzo:

#include <stdio.h>
#include <string.h>
 
main()  {
  char s1[20];
  char *s2 = "pippo";
 
  // Copio all'interno di s1 la stringa "Ciao "
  // copiando esattamente il numero di byte che
  // mi servono, tramite l'operatore sizeof
  strncpy (s1,"Ciao ",sizeof("Ciao "));
 
  strcat (s1,s2);
  // s1 ora contiene "Ciao pippo"
}

La funzione strcat ritorna un puntatore a char che rappresenta un puntatore alla zona di memoria dove è salvato dest.

Attenzione: anche la strcat, così come la strcpy ed altre funzioni che vedremo in seguito, è sul libro nero delle funzioni a potenziale rischio di sicurezza per un'applicazione. Il suo uso, quando possibile, va evitato.

[modifica] strncat

La sua sintassi è molto simile a strcat, con la differenza che in strncat vanno specificati anche il numero di caratteri di src da copiare in dest, in modo da tenere la copia sotto controllo. La sua sintassi è

char *strncat(char *dest, const char *src, int n);

dove n rappresenta il numero di caratteri di src da copiare. Il suo uso, quando possibile, è preferibile a quello di strcat.

[modifica] strstr

La funzione strstr serve per verificare se esiste una sottostringa all'interno della stringa di partenza. La sua sintassi è

char *strstr(const char *haystack, const char *needle);

dove haystack (lett. 'pagliaio') è la stringa all'interno della quale cercare, needle (lett. 'ago') è la stringa da cercare (da notare ancora una volta il sottile umorismo degli sviluppatori del C). La funzione ritorna

  • Un puntatore intero, che rappresenta la zona di memoria in cui è stata trovata la sottostringa, nel caso in cui la sottostringa dovesse essere trovata
  • NULL nel caso in cui la sottostringa non dovesse essere trovata

Esempio:

/*
* Questo programmino chiede in input all'utente due stringhe e
* verifica se la seconda stringa è localizzata all'interno della prima
*/
 
#include <stdio.h>
#include <string.h>
 
main()  {
  char s1[32];
  char s2[32];
 
  printf ("Inserire la stringa all'interno della quale cercare: ");
  scanf ("%s",s1);
 
  printf ("Inserire la stringa da cercare: ");
  scanf ("%s",s2);
 
  if (strstr(s1,s2))
  // Equivale a scrivere
  // if (strstr(s1,s2) != 0)
  // ovvero se il valore di ritorno della funzione non è NULL
    printf ("Stringa \"%s\" trovata all'interno di \"%s\", in posizione %d\n",
             s2,s1,(strstr(s1,s2)-s1));
  else
    printf ("Stringa \"%s\" non trovata all'interno di \"%s\"\n",s2,s1);
}

Si noti questa scrittura:

strstr(s1,s2)-s1

strstr ritorna infatti l'indirizzo dell'area di memoria in cui si trova s2 all'interno di s1. Se a questo indirizzo sottraggo l'indirizzo di s1, ovvero l'indirizzo del primo carattere di s1, ottengo la locazione effettiva della sottostringa all'interno della stringa di partenza. Se ad esempio s1="Ciao pippo" e s2="pippo", strstr(s1,s2)-s1 = 5.

[modifica] Altre funzioni sulle stringhe

[modifica] sprintf

La funzione sprintf, definita in stdio.h, è del tutto analoga alla printf. La differenza è che la printf scrive dell'output formattato su standard output, mentre la sprintf scrive dell'output formattato direttamente su una stringa, che rappresenta il primo argomento della funzione. Esempio:

#include <stdio.h>
 
main()  {
  char s1[32];
  char s2="Ciao";
  char s3="pippo";
  int age=2;
 
  sprintf (s1,"%s %s ho %d anni",s2,s3,age);
  // Scrivo su s1 attraverso la sprintf
  // Ora s1 contiene la stringa "Ciao pippo ho 2 anni"
}

Anche la funzione sprintf è sulla lista di quelle da usare con cautela, e solo quando si è sicuri che la stringa di destinazione è in grado di contenere tutti i byte che si stanno per copiare al suo interno.

[modifica] snprintf

La funzione snprintf è un'alternativa più sicura alla sprintf, e al suo interno va specificato anche il numero massimo di caratteri da copiare. La sua sintassi è quindi

int snprintf(char *str, int size, const char *format, ...);

dove size rappresenta il numero massimo di byte della stringa di formato da copiare all'interno di str.

[modifica] sscanf

La funzione sscanf è del tutto analoga alla scanf classica, solo che invece di leggere i dati dalla tastiera li legge dall'interno di una stringa. Esempio, ecco un uso classico di sscanf. Abbiamo una stringa che rappresenta una data, in formato 'gg/mm/aaaa'. Vogliamo ottenere, dall'interno di questa stringa, il giorno, il mese e l'anno e salvarli all'interno di 3 variabili intere. Con sscanf la cosa è presto fatta:

char *date = "13/08/2007";
int d,m,y;
 
sscanf (date,"%d/%d/%d",&d,&m,&y);
// d=13, m=8, y=2007

La funzione ritorna un intero che rappresenta il numero di campi letti all'interno della stringa. Il controllo su questo valore di ritorno può tornare utile per verificare se l'utente ha inserito la stringa nel formato giusto:

#include <stdio.h>
#include <stdlib.h>
 
main()  {
  char date[16];
  int d,m,y;
 
  printf ("Inserisci una data: ");
  scanf ("%s",date);
 
  // Se non leggo almeno 3 interi nella stringa
  // inserita separati da '/'...
  if (sscanf(date,"%d/%d/%d",&d,&m,&y) != 3)  {
    printf ("Errore: data inserita non valida: %s\n",date);
    exit(1);
  }
 
  printf ("Giorno: %d\n",d);
  printf ("Mese: %d\n",m);
  printf ("Anno: %d\n",y);
}

[modifica] gets

Un piccolo limite della lettura delle stringhe sta nel fatto che la lettura si interrompe quando incontra uno spazio. Se ad esempio un'applicazione richiede all'utente di inserire una stringa e l'utente inserisce "Ciao mondo", se la lettura avviene tramite scanf molto probabilmente la stringa risultante dopo la lettura sarà semplicemente "Ciao". Per evitare questo piccolo inconveniente si ricorre alla funzione gets, nonostante il suo uso sia deprecato dai nuovi standard C. Esempio di utilizzo:

#include <stdio.h>
 
main()  {
  char str[32];
 
  printf ("Inserisci una stringa: ");
  gets (str);
 
  // Se ora inserisco stringhe con degli spazi in mezzo vengono salvate ugualmente nella
  // stringa finale, in quanto la gets legge tutti i caratteri fino al fine linea
 
  printf ("Stringa inserita: %s\n",str);
}

Attenzione: anche la gets è nella lista delle funzioni a rischio. Presto vedremo subito in che modo operare sulle stringhe con le funzioni giuste e nel modo giusto, e i rischi che si ottengono lavorando in modo non controllato sulle stringhe.

[modifica] atoi

La funzione atoi (ASCII to int), definita in stdlib.h, è usata per convertire una stringa in un valore intero. La sua sintassi è molto semplice:

int atoi(const char *nptr);

e restituisce il valore convertito. Nel caso in cui la stringa non contenga un valore numerico valido, la funzione ritorna zero. Esempio di utilizzo:

#include <stdio.h>
#include <stdlib.h>
 
main()  {
  int n;
  char *s = "3";
 
  n=atoi(s);
  // Ora n contiene il valore intero 3
}

Della stessa famiglia sono le funzioni atol (ASCII to long) e atof (ASCII to float).

[modifica] Argomenti passati al main

Sappiamo che molte applicazioni accettano una lista di parametri passati dall'utente in input. Ad esempio, il comando dir del DOS è in grado di accettare alcuni parametri per configurarne il funzionamento (ad esempio dir /h e simili...). Idem per net send (net send indirizzo_host messaggio) e per, ad esempio, il comando ls di Unix (ls -l -h). È possibile passare questi parametri ad un programma sfruttando gli argomenti del main. Il main è una funzione come tutte le altre, e quindi può anche ricevere argomenti in input. In particolare, è possibile leggere i parametri eventuali passati ad un programma tramite l'uso di argv, un vettore di stringhe da passare al main. Il numero di argomenti passati viene invece salvato nella variabile intera argc. Esempio di utilizzo:

#include <stdio.h>
 
main(int argc, char **argv)  {
  printf ("Nome dell'eseguibile in esecuzione: %s\n",argv[0]);
}

La prima stringa del vettore argv contiene infatti il nome dell'eseguibile (quindi la variabile argc è sempre settata almeno a uno). Gli eventuali argomenti successivi passati al programma vengono salvati in argv[1],...,argv[n]. Esempio pratico:

#include <stdio.h>
 
main(int argc, char **argv)  {
  int i;
 
  printf ("Argomenti passati al programma:\n");
 
  for (i=1; i<argc; i++)
    printf ("%s\n",argv[i]);
}

Se compiliamo questo eseguibile come 'stampa_arg' e lo invochiamo con gli argomenti "Ciao mondo come stai", così (in ambiente Unix):

./stampa_arg Ciao mondo come stai

avremo come output qualcosa del tipo

Argomenti passati al programma: Ciao mondo come stai

[modifica] Uso delle stringhe e sicurezza del programma

Nei paragrafi precedenti abbiamo preso in esame alcune funzioni sulle stringhe che possono rivelarsi potenzialmente dannose per la stabilità e la sicurezza di un'applicazione. In questo paragrafo esamineremo i rischi concreti connessi ad una cattiva gestione delle stringhe.

Esempio pratico:

#include <stdio.h>
#include <string.h>
 
main()  {
  char s1[2];
  char *s2 = "La vista teresa sta tra l'erbetta";
 
  strcpy (s1,s2);
}

Eseguendo un codice del genere molto probabilmente l'applicazione andrà in crash. Se siamo su un sistema Unix il kernel ci risponderà con un bel segmentation fault, su Windows ci comparirà una finestra che ci avverte che l'applicazione ha tentato la scrittura su un'area di memoria non valida. Quello che abbiamo fatto è tentare di copiare in un buffer più byte di quelli che il buffer stesso può contenere, e in modo non controllato (la strcpy non effettua una copia controllata, non si ferma se i limiti di capienza della stringa vengono raggiunti). Il risultato è che l'applicazione va in crash, in quanto, attraverso la strcpy, è andata a scrivere su una zona di memoria al di fuori di quella della stringa stessa, andando a sovrascrivere l'indirizzo di ritorno della funzione con un indirizzo non valido. L'indirizzo viene letto dalla CPU, che tenta di leggere l'istruzione a quell'indirizzo. Indirizzo che nella maggior parte dei casi non sarà un indirizzo di memoria valido, quindi provocherà il crash del programma.

Ma il crash del programma, nonostante sia un danno non da poco, non è nemmeno il minore dei danni. Esempio pratico con un'applicazione:

#include <stdio.h>
#include <string.h>
 
main(int argc, char **argv)  {
  char str[16];
 
  strcpy (str,argv[1]);
}

Attenzione all'uso di strcpy in questa applicazione. La funzione copia il primo argomento passato al programma nella stringa str, che può tenere 16 caratteri, senza fare ulteriori controlli sulla lunghezza effettiva della stringa da copiare. Proviamo ad avviare l'applicazione con il nostro debugger preferito (in questo caso userò Gdb) per vedere cosa succede in memoria quando passo al programma un argomento molto lungo:

(gdb) run `perl -e 'print "A" x32'`
Starting program: /home/blacklight/prog/c/5 `perl -e 'print "A" x32'`

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()

Il comando `perl -e 'print "A" x32'` non fa altro che richiamare l'interprete Perl (un linguaggio di programmazione), stampando la lettera "A" 32 volte (un modo per evitare di scrivere 32 volte "A", giusto una comodità). Il programma, tentando di copiare un buffer troppo grande in una stringa che non è in grado di contenere tanti byte, va in crash. Ma vediamo cosa succede a livello dei registri:

(gdb) i r
eax 0xbfab7890 -1079281520
ecx 0xfffff098 -3944
edx 0xbfab8818 -1079277544
ebx 0xb7edeffc -1209143300
esp 0xbfab78b0 0xbfab78b0
ebp 0x41414141 0x41414141
esi 0xbfab7934 -1079281356
edi 0xbfab78c0 -1079281472
eip 0x41414141 0x41414141
eflags 0x210282 [ SF IF RF ID ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51

Da notare il registro EIP. Tale registro contiene, nelle architetture Intel-based, l'indirizzo in memoria della prossima istruzione da eseguire. L'indirizzo è stato sovrascritto da una sequenza di 0x41. E 0x41, in esadecimale, corrisponde al carattere ASCII "A". In pratica il nostro buffer lungo è andato a sovrascrivere il registro EIP, cambiando il valore dell'indirizzo della prossima istruzione da pescare in memoria. In questo caso, la sequenza di 0x41 non rappresenta un indirizzo di memoria valido, o almeno un indirizzo nel quale il programma può accedere, ragion per cui il programma crasha. Ma, oltre ad una semplice sequenza di "A", possiamo anche inserire un buffer costruito apposta, che inietta nel registro un indirizzo valido che punta ad un codice arbitrario. Siamo quindi nella situazione di un buffer overflow sfruttato in modo da poter eseguire codice arbitrario sul sistema, codice che può mirare ad aggiungere un nuovo utente con certi privilegi su quel sistema, ad ottenere i privilegi di amministratore in modo indebito o ad aprire una shell remota o locale in modo indebito. In ogni caso, quando un attaccante ha sfruttato un codice vulnerabile iniettando del codice arbitrario al suo interno ha il controllo totale della macchina, anche se indebito. I bollettini di sicurezza in giro per il web pullulano di bug del genere trovati ancora oggi in molte applicazioni, e dovuti proprio all'uso errato di funzioni come quelle che abbiamo visto sopra, bug che in genere sono corretti il più in fretta possibile dopo la scoperta per evitare che i danni ai sistemi che usano quelle applicazioni diventino maggiori. Non vedremo in questa sede, per evitare di divagare troppo nel discorso, in che modo sfruttare tali vulnerabilità per acquisire il controllo di un sistema, ma per ora ci basta sapere che usando certe funzioni la cosa è possibile e sapere in che modo funziona.

In conclusione, le funzioni potenzialmente vulnerabili a buffer overflow e da usare con cautela sono:

  • scanf
  • gets
  • strcpy
  • strcat
  • sprintf

Queste funzioni vanno usate solo quando si è sicuri al 100% delle dimensioni del buffer di destinazione. In alternativa, è più sicuro usare funzioni come

  • fgets
  • strncpy
  • strncat
  • snprintf

Soffermiamoci un attimo sulla fgets (ne faremo solo una trattazione sommaria per le stringhe in questa sede, mentre la studieremo in modo più approfondito nel capitolo sui file). Abbiamo visto prima che, per la lettura di una stringa da input, sia l'uso di scanf che di gets è pericoloso. Per leggere stringhe la cosa migliore è fare ricorso a questa funzione, che prende come primo argomento la stringa di destinazione, come secondo argomento il numero massimo di caratteri da leggere da input e come terzo argomento il descrittore da cui leggere (nel nostro caso lo standard input, identificato da stdin). Esempio di uso:

char str[16];
 
printf ("Inserisci una stringa: ");
fgets (str,sizeof(str),stdin);
str[strlen(str)-1]=0;
 
printf ("Stringa inserita: %s\n",str);

La notazione sizeof(str) dice di leggere da input al massimo tanti caratteri quanti sono quelli supportati dalla dimensione di str (ovvero 16 in questo caso), mentre stdin, costante definita in stdio.h, identifica lo standard input. La scrittura str[strlen(str)-1]=0; serve perché la funzione fgets salva nella stringa anche la pressione del carattere invio. Questa scrittura setta il carattere NULL un byte prima, in modo da rimuovere il carattere invio dalla stringa.

Attenzione anche ad evitare scritture del genere:

char *str = "Ciao";
printf (str);

Se non viene specificato esplicitamente il formato della stringa da stampare nella printf l'applicazione può potenzialmente essere vulnerabile a format string overflow, una vulnerabilità scoperta abbastanza recentemente che consente di scrivere dati arbitrari sullo stack. Seguendo questi passi per evitare buffer overflow e format string overflow si può essere sicuri almeno a un 70% di scrivere applicazioni relativamente sicure.

Strumenti personali