Un esempio di... disinfestante

Chi non si è ancora imbattuto in qualche mutante di... virus informatico? A titolo di esempio riportiamo e commentiamo il listato C di una semplice utility in grado di individuare nei programmi il virus Vienna B e di "risanarli".

Un programma colpito dal Vienna B è riconoscibile in base alle seguenti caratteristiche:

1
è un file .COM
2
la sua dimensione è maggiore per 648 byte (codice del virus) rispetto al normale
3
in questa "appendice" vi è la stringa "*.COM" preceduta da una sequenza di 3 byte identici ai primi 3 byte del file
4
i 3 byte che precedono tale sequenza erano, prima del contagio, i primi 3 byte del file stesso

L'anti­Vienna deve pertanto aprire il file sospetto, leggerne i primi 3 byte e concatenare a questi la stringa "*.COM" per ricostruire l'identificatore del virus. Esso deve poi leggere gli ultimi 648 byte del file in un buffer appositamente allocato e scandirli alla ricerca della stringa identificatrice composta in precedenza. Se essa viene localizzata, allora il file analizzato ha subìto il contagio. Per risanarlo è sufficiente sovrascrivere i suoi primi 3 byte con i 3 byte che, nel buffer, precedono la stringa identificatrice e troncarlo ad una dimensione pari alla lunghezza attuale diminuita di 648.

Vediamone il listato.

/********************

    BARNINGA_Z! - 1990

    DISINFES.C - Elimina il virus Vienna (versione B) dai file contagiati

    COMPILABILE CON TURBO C++ 1.0

        tcc -O -d disinfes.c wildargs.obj

********************/
#pragma warn -pia       /* no warnings per assegnazioni all'interno di if */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <io.h>

#define  _VIRUSDIM_  ((size_t)648)        /*  dimensione del virus */
#define  _S_STRING_  ((unsigned char *)"   *.COM")  /* da cercare */
#define  _S_BYTES_   3     /* n. bytes da sostituire */
#define  _O_BYTES_   -3    /* offset dei bytes rispetto alla stringa */

unsigned char *sstring = _S_STRING_;

char *msg[] = {
    "%s non è contaminato.\n",
    "%s decontaminato.\n",
    "\nDISINFES : Elimina virus Vienna B - Barninga_Z!, 1990.\n",
    "USO: disinfes nome_file(s) (sono consentiti * e ?)\n\n",
    "Impossibile aprire %s.\n",
    "Errore di allocazione della memoria.\n",
    "Impossibile decontaminare %s.\n",
    "Errore nella gestione di %s.\n",
    "Impossibile chiudere %s dopo la decontaminazione.\n",
    "Impossibile chiudere %s.\n",
    "ATTENZIONE: è contaminato.\a\n",
    "\nFile: %s:\n"
};

unsigned char *srchstr(unsigned char *bufptr,unsigned char *sstring)
{
    register int i, sl;
    unsigned char *stop;

    stop = bufptr+_VIRUSDIM_-(sl = strlen((char *)sstring));
    for(; bufptr < stop; bufptr++) {
        for(i = 0; i < sl; i++)
            if(*(bufptr+i) != *(sstring+i))
                break;
        if(i == sl)
            return(bufptr);
    }
    return(NULL);
}

int tronca(FILE *file,unsigned char *bufptr,int flen)
{
    if(fread(sstring,(size_t)1,_S_BYTES_,file) != _S_BYTES_)
        return(7);
    if(fseek(file,(long)flen,SEEK_SET) != 0)
        return(7);
    if(fread(bufptr,(size_t)1,_VIRUSDIM_,file) != _VIRUSDIM_)
        return(7);
    if(bufptr = srchstr(bufptr,sstring)) {
        bufptr += _O_BYTES_;
        printf(msg[10]);
        rewind(file);
        if(fwrite(bufptr,(size_t)1,_S_BYTES_,file) < _S_BYTES_)
            return(7);
        if(chsize(fileno(file),(long)flen) != 0)
            return(6);
        return(1);
    }
    return(0);
}

int disinfesta(char *fname,unsigned char *bufptr)
{
    register int ret = 0;
    int flen;
    FILE *file;

    printf(msg[11],fname);
    if(!(file = fopen(fname,"rb+")))
        return(4);
    if((flen = (int)filelength(fileno(file))-_VIRUSDIM_) > 0)
        ret = tronca(file,bufptr,flen);
    if(fclose(file))
        if(!ret)
            ret = 9;
        else
            ret = 8;
    return(ret);
}

void main(int argc,char **argv)
{
    unsigned char *bufptr;

    printf(msg[2]);
    if(argc < 2)
        printf(msg[3]);
    else
        if(!(bufptr = (unsigned char *)malloc(_VIRUSDIM_)))
            printf(msg[5]);
        else
            for(--argc; argc; argc--)
                printf(msg[disinfesta(argv[argc],bufptr)],argv[argc]);
}

La funzione main() provvede a visualizzare un opportuno messaggio qualora il programma sia lanciato senza specificare sulla command line alcun nome di file. Se DISINFES è consolidato con WILDARGS.OBJ è possibile utilizzare le wildcard "?" e "*" nei nomi di file; main() provvede inoltre ad allocare il buffer necessario ai 648 byte letti dal file analizzato e a gestire il ciclo di chiamate (una per ogni file specificato sulla command line) alla funzione disinfesta(). Tale ciclo può, a prima vista, apparire di difficile interpretazione; l'algoritmo risulta tuttavia banale se si tiene conto di alcune particolarità. In primo luogo, è noto che la variabile argc contiene il numero di elementi presenti nell'array argv: per ottenere l'indice massimo di argv occorre dunque, inizialmente, decrementare di uno argc. Inoltre, dal momento che argv[0] è, per default, il pathname completo di DISINFES, è necessario escludere detto elemento dal ciclo, il quale si arresta quando argc assume valore nullo[1]; al compimento di ogni iterazione argc è decrementata, dunque l'analisi procede dall'ultimo file specificato sulla command line di DISINFES sino al primo[2]. All'interno del ciclo vi è solamente una chiamata a printf(): per ogni file analizzato viene dunque stampato un messaggio individuato, nell'array msg, dal valore restituito dalla funzione disinfesta().

Questa apre il file e ne calcola la lunghezza[3], decrementandola di 648 (se esso è contaminato, la variabile flen contiene dunque la sua dimensione originaria) e, dopo avere invocato tronca(), restituisce un valore atto a individuare l'opportuno messaggio d'errore nell'array msg qualora non fosse possibile chiudere il file al ritorno da tronca(); in caso contrario, il valore restituito da quest'ultima viene a sua volta restituito a main().

La funzione tronca() provvede a leggere i primi 3 byte del file nel buffer sstring, che è una variabile (puntatore) globale inizializzata a _S_STRING_. L'esame della direttiva

#define _S_STRING_ "   *.COM"

rivela che sstring contiene, dopo l'operazione di lettura, i primi 3 byte del file seguiti immediatamente da "*.COM": essa può dunque essere utilizzata per identificare il virus. Vengono poi letti nel buffer allocato in main() gli ultimi 648 byte del file analizzato: la funzione srchstr() [4] li scandisce alla ricerca della stringa di identificazione.

L'algoritmo utilizzato da srchstr() è di carattere generale: all'interno di un primo ciclo, che incrementa ad ogni iterazione il puntatore al buffer e valuta, mediante un confronto con un altro puntatore, se il buffer è stato interamente scandito, si trova un secondo ciclo, il quale confronta tra loro i caratteri di buffer e stringa che si trovano all'offset corrispondente all'iterazione attuale e si interrompe se essi sono differenti. Se all'uscita da questo ciclo il contatore è uguale alla lunghezza della stringa ricercata, essa è stata individuata (perché il ciclo non è stato interrotto prima del termine), e il puntatore al buffer referenzia ora, in realtà, la stringa all'interno del buffer medesimo: tale puntatore è restituito a tronca().

Un valore non nullo restituito da srchstr() è altresì, per tronca(), il segnale che il virus è presente nel file analizzato: basta sommare al puntatore bufptr l'offset, rispetto alla stringa, della sequenza di byte che dovrebbe trovarsi in testa al file (attenzione: _O_BYTES_ è definito pari a ­3, perciò i conti tornano), riscriverla "al suo posto" e troncare la dimensione del file per completare il ripristino delle sue caratteristiche originarie; l'elaborazione descritta è ripetuta sul successivo file eventualmente specificato sulla riga di comando.

DISINFES.C, il cui impianto logico e tecnico è, come si vede, assai semplice, appare suscettibile di perfezionamenti[5]. Un esame più attento del comportamento del Vienna B, effettuato disassemblando un file "infetto", rivela che i 3 byte da esso scritti in testa ad ogni programma contagiato rappresentano una istruzione di salto JMP (1º byte) seguita (2º e 3º byte) dall'offset al quale viene in effetti trasferita l'elaborazione. In tutti i casi esaminati tale offset è risultato pari alla lunghezza originaria del programma diminuita di 3: se ne trae che il programma contagiato, non appena è eseguito, salta all'indirizzo immediatamente successivo alla fine del proprio codice, cioè all'inizio del codice del virus, il quale, in tal modo, ha la garanzia di poter assumere per primo il controllo della situazione. L'indirizzo ricavato dai 3 byte che originariamente erano in testa al file (JMP e near offset) sono utilizzati per riprendere la normale esecuzione del programma quando Vienna B termina le proprie operazioni.

L'affidabilità di DISINFES potrebbe essere allora accresciuta dotandolo di una funzione che confronti l'integer costituito dal 2º e 3º byte del file con la sua presunta lunghezza originale diminuita di 3:

int jmp_test(char *sstring,int flen)
{
    return((*((int *)(sstring+1)))-(flen-_S_BYTES_));
}

Si consideri l'espressione (*((int *)(sstring+1))): sommando 1 al puntatore alla stringa utilizzata per identificare il virus se ne "scavalca" il primo byte (l'istruzione JMP); l'operazione di cast forza il compilatore C a considerare puntatore a int l'espressione (sstring+1); l'indirezione restituisce quindi l'intero rappresentato dai 2 byte di cui si è detto. Pertanto, se la funzione jmp_test() restituisce un valore non nullo si può concludere che il file esaminato non contiene il virus, pur contenendo la stringa sospetta.

Concludiamo il presente paragrafo presentando la modifica da apportare alla funzione tronca() per inserire jmp_test() in DISINFES.C:

    ....
    if((bufptr = srchstr(bufptr,sstring)) && !jmp_test(sstring,flen)) {
        bufptr += _O_BYTES_;
    ....

Il codice di jmp_test(), qualora non si intenda dichiararne il prototipo, deve essere inserito nel listato prima della definizione di tronca().


OK, andiamo avanti a leggere il libro...

Non ci ho capito niente! Ricominciamo...