RSS

Virus e Antivirus: la ricerca algoritmica dei virus… (2)

12 Novembre 2011

Varie

antivirus-64

Riprendiamo l’analisi dell’antivirus dopo aver visto precedentemente le varie tipologie di virus.

Un fattore forse trascurato sui virus, ma chè è stato alla base del mio programma antivirus, è che quasi la totalità di virus attua un sistema per identificare se un file è già infetto o se è già attiva una copia di se stesso in memoria (per i virus TSR).

Pensatici un attimo: se così non fosse, avrete che un singolo file potrebbe venir infettato decine/centinaia di volte dallo stesso virus, rendendo la dimensione di file in perenne crescita, allungandone i tempi di caricamento e rendendo molto sospetta l’infezione fin dal primissimo contagio. Per i TSR poi, se ad ogni file infetto lanciato il virus attivasse più e più volte se stesso in memoria, qualsiasi chiamata di sistema diventerebbe molto lenta e la memoria potrebbe calare nonostante i virus siano di pochi KB.

L’idea di fondo è quindi presto lanciata:

  • un virus attua una serie di test su di un file per stabilire se esso è già infetto e ne evita l’infezione in caso di risposta positiva: ci basta pertanto replicare l’esatto comportamento attuato dal virus (solo per quanto riguarda questa fase di test) e avremo in tempo rapidissimo la risposta sullo stato di infezione del file, senza dover fare lunghe scansioni di pattern.

Ma come realizzare un antivirus di questo genere?
Il primo metodo che viene in mente sarebbe quello di usare le stesse istruzioni a basso livello del virus (spesso con una decina di istruzioni macchina su di un file si ottiene la risposta sullo stato di infezione) come file eseguibile da richiamare dall’antivirus. Questo comporterebbe però qualche problema in fase di rilascio degli aggiornamenti, dato che verrebbe rilasciato codice macchina che deve essere caricato dinamicamente in fase di scansione dall’antivirus (la cosa è fattibile, ma il problema è che essendo codice eseguibile potrebbe a sua volta essere infettato da virus!).
Il secondo metodo è quello che oggi si ritrova in forma evoluta su Java: tramite un linguaggio di programmazione si compila in byte code la routine di scansione algoritmica e poi tramite un interprete (un computer virtuale) rappresentato dall’antivirus si realizza il test del file.

Ovviamente ho attuato questo metodo, in considerazione che è pressochè naturale che prima o poi un informatico sia alle prese con la codifica di compilatori…o così almeno era negli anni 90… 🙂

Tramite il linguaggio di programmazione SVDL (Step Virus Detect Language) codificavo la sequenza di operazioni da eseguire su di un file per fare il test di contagio. Il file veniva compilato e il codice VDX (il bytecode) era aggiunto alla libreria di scansione usata dall’interprete (l’antivirus).

L’interprete infatti operava in questo modo (per ogni file da analizzare)

  • leggeva in un buffer le parti significative del file (ovvero l’inizio e la fine che sono quelle utilizzate normalmente dal virus)
  • eseguiva ogni codice VDX della libreria su quel file

Vediamo un esempio:

{ Search For 1701 }
CODE 01;
A% := DIMH;
IF A% <= 00
- A% := DIML;
  IF A% <= 0F93B -
    B# := [00];
    IF B# = 0E9
    - B% := 0F959;
      A%:= A% ADD B%;
      IF [01+] = A%
      + '1701 (COM)'

Visto così fa paura e non si capisce nulla, ma, considerando che {} sono commenti, che ogni IF non ha mai un ramo else (che vorrebbe dire file non infetto), ma solo then (rappresentato dal – o dal + se è l’ultima istruzione) e che [] rappresenta una lettura sul file nella posizione data all’interno delle parentesi, rimangono misteriosi solo alcuni passaggi:

  • Nel registro word (16 bits: %) A si legge la dimensione (parte alta) del file corrente (la dimensione del file è un numero a 32 bits, qui legge i 16 bits più alti)
  • Solo se la dimensione del file è inferiore o uguale a zero si prosegue (il virus non infetta eseguibili troppo grandi)
  • In A legge la dimensione bassa del file corrente e si verifica che sia inferiore al valore esadecimale 0xF93B, ovvero non sia troppo vicino alla fine del blocco di memoria.
  • Nella variabile di tipo byte (8 bits: #) B si legge il valore del file alla posizione 0 solo se equivale a 0xE9 (che in linguaggio macchina corrisponde a un JMP, salto ad un punto del programma) si somma alla dimensione bassa del file il valore fisso 0xF959 (il valore E9h esclude anche che si tratti di un file .exe)
  • Se questo valore è pari al valore posto alla posizione 1 e 2 [1+] del file allora il file è infetto.

Tradotto in linguaggio ancora meno informatichese…il virus infetta un file e pone il codice di esecuzione di se stesso all’inizio (prima istruzione del programma) come un salto JMP al suo indirizzo posto verso la fine del file, ma in una posizione nota, ovvero 0x6A6 (il numero in complemento a 2 di 0xF959) bytes prima della fine del file.

Se questo si verifica il file è infetto dal virus 1701, altrimenti il file non è infetto da quel virus. Ovviamente esiste una certa percentuale di probabilità che questo porti ad un falso positivo, ovvero un file non infetto che venga dichiarato infetto. Da notare che se l’antivirus dichiara il file non infetto, sicuramente non lo è (e questo perchè il virus per infettarlo avrebbe messo il codice tale per cui questo test darebbe risultato positivo).

Rimane misteriosa l’istruzione iniziale: CODE 01
Questa è la famosa ancora di salvezza che metteva al riparo il linguaggio SVDL da virus futuri diversi dagli attuali, rendendo le modifiche in avanti possibili. Infatti il bytecode VDX, dipendendo da un interprete virtuale, aveva certe istruzioni e variabili previste (ad esempio 16 o 8 bit) che coprivano la casistica che riguardava i virus di quegli anni. Era ovvio che con l’avanzare tecnologico dei processori (oggi siamo a 64 bits, domani ne avremo il doppio) e lo sviluppo dei sistemi operativi, il linguaggio prima o poi non avrebbe più potuto coprire i nuovi tipi di virus. Per questo, la prima istruzione avrebbe abilitato un nuovo set di istruzioni e solo un compilatore/interprete aggiornato lo avrebbero riconosciuto e usato (in Java, ad esempio, un codice Java 6 gestisce più cose rispetto a un Java 5, che a sua volta migliorava il 4).

C’è subito da dire che il virus 1701 usa la crittografazione per nascondere il proprio codice, anche se la routine di decrittazione è fissa (e quindi gli antivirus a pattern avrebbero cercato il pattern rappresentato dalla routine fissa).

Non tutti i virus hanno test molto sofisticati per riconoscere i file infetti, a volte sono più blandi (vedremo tra poco un esempio), per cui a priori sarebbe possibile attribuire un grado di efficenza nel non avere falsi positivi in base al metodo usato dal virus:

CODE 01;
IF [00]=0E9          {JMP  xxyy}
- IF [03]=56         {PUSH SI  }
  + 'Mr (1.10) (COM)'

Questo programma rintraccia il virus Mr (versione 1.10) su di un file .COM infetto. Il virus controlla che ci siano due istruzioni particolari all’inizio del programma eseguibile: il salto al corpo del virus (istruzione JMP che ha l’opcode 0xE9 nei processori x86) e il push del registro SI del processore (opcode 0x56) come istruzione successiva.

E’ evidente che questo metodo non è il massimo di affidabilità, perchè troppo generico (in questo caso si può migliorare l’affidabilità aggiungendo che venga confrontato un pezzo di codice leggendo ciò che si trova ad un punto del file con la parte di virus che ci si aspetta di trovare).

Vediamo ora un esempio più complicato:

CODE 01;
IF [00+]='ZM'
- A%:=[08+];      {paragrafi dell'header}
  B%:=[16+];      {CS iniziale}
  A%:=A% ADD B%;
  D%:=10;
  Z%:=0;
  A%:=A% MUL D%;
  B%:=[14+];      {IP iniziale}
  A%:=A% ADD B%;
  C%:=0;
  Z%:=Z% ADC C%;  {Z%:A% indirizzo}
  D%:=68;
  A%:=A% SUB D%;
  E%:=Z% SBB C%;  {il virus ora sposta il puntatore del file ad E%:A%}
  F%:=708;
  A%:=A% ADD F%;
  E%:=E% ADC C%;
  L%:=DIML;
  H%:=DIMH;
  IF L%=A% -
   IF H%=E% + 'DarkAvenger (EXE)'

Si tratta del virus DarkAvenger, il capostipite di quelli che poi sarebbero diventati i virus polimorfici grazie al Mutation Engine diffuso dallo stesso autore di questo virus. Per rintracciarlo servono queste operazioni:

  • verifica che il file sia di tipo EXE (cerca MZ)
  • legge le informazioni di dove parte il codice eseguibile (CS:IP, i puntatori del processore)
  • ne aggiunge/toglie degli offset, ottenendo un nuovo valore a 32 bits (E%: A%)
  • L’indirizzo è confrontato con la dimensione del file e in caso di esito positivo, il file è infetto (c’è da dire che il virus poi faceva un ulteriore scansione per verificare di trovare il suo corpo in quel punto, ma già così la routine di scansione ha una bassissima probabilità di dare falsi positivi)

Vediamo adesso un esempio molto “complicato”, ovvero la ricerca del virus polimorfico Flip (capostipite del virus Tequila, ancora più tosto quanto a polimorfismo…)

CODE 01;
IF [_47]=0E          {PUSH CS
 - IF [_46]=0BB      {MOV BX,..
   + 'FLIP detected'

Bastano solo due confronti in due precise posizioni alla fine del file per la ricerca di quelle che sono due istruzioni che il virus si aspetta di trovare per garantire che esso sia presente, al dilà di tutta la criptazione che viene effettuata sul suo corpo e alle mutazioni della routine di decriptazione!

La ricerca poteva essere affinata in questo modo (giusto per vedere meglio le potenzialità del linguaggio SVDL):

CODE 01;
IF [_47]=0E            {PUSH CS  }
 - IF [_46]=0BB        {MOV BX,..}
  - IF [_43]=1F        {POP DS   }
   - IF [_42]=0B9      {MOV CX,..}
    - IF [_3F]=0B2     {MOV DL,..}
     - IF [_3D]=81     {ADD ...  }
      - IF [_3C]=0C1   {... CX,..}
       - IF [_39]=0EB  {JMP ...  }
        - A#:=[_38];
          B#:=37;
          B#:=B# SUB A#;
          IF [_B#]=00                 {ADD ...}
           - B#:=DEC;
             IF [_B#]=97              {... [BX],DL}
              - C#:=3;
                B#:=B# SUB C#;
                IF [_B#]=43           {INC BX}
                 - B#:=DEC;
                   IF [_B#]=0EB       {JMP ...}
                    - B#:=DEC;
                      A#:=[_B#];
                      B#:=DEC;
                      B#:=B# SUB A#;
                      IF [_B#]=0E2    {LOOP ...}
                       - B#:=DEC;
                         B#:=DEC;
                         IF [_B#]=0E9 {JMP ...}
                          + 'FLIP (COM-EXE)'

Concettualmente non cambia molto: si ricercano certe istruzioni in certe posizioni, ma ora le posizioni dipendono dal codice dato che si segue a dove punta un salto (JMP) che può essere variabile in base alla mutazione del codice  di decriptazione (la lettura su file avviene infatti in base alla posizione del valore assunto dalla variabile B). Ovviamente non vanno incluse le istruzioni di cui il mutation engine cambia tra una infezione e l’altra, altrimenti il codice trova solo un sotto insieme tra le possibili infezioni.

C’è da evidenziare che molto spesso c’erano degli evidenti errori di programmazione nei virus causati probabilmente dall’uso del linguaggio assembler che essendo a basso livello può portare ad errori più facili. Ad esempio, esistono due flag utili per il confronto (che è trattato come una differenza) di due numeri nel processore: il carry e il segno e la loro funzione cambia a seconda di come sono intesi i numeri da confrontare (normali o in complemento a due), per cui è abbastanza semplice sbagliare l’istruzione di confronto fra le tante dispobibili e avere un comportamento errato anche se il codice sembra scritto correttamente.

Errori come questi impedivano di attuare l’azione “maligna” nei tempi stabiliti, o peggio, non riconoscere il file come infetto e quindi reiterando l’infezione ancora una volta (è il caso del virus Jerusalem che procedeva correttamente solo coi file COM e sbagliava con i file EXE: correggendo però la routine per SVDL mettendo il giusto flag di confronto sarebbe stato possibile rintracciare anche questo virus).

Fornisco un esempio curioso:

CODE 01;
IF [00+]='ZM'
- A%:=TIME;
B%:=001F;
A%:=A% AND B%;    {A% = secondi}
IF A%=1F          {1F = 62 secondi !!}
+ 'ALABAMA (EXE)'

Il virus Alabama per rintracciare se stesso modificava il numero di secondi nella data del file in modo che fosse pari a 62 secondi, un valore ovviamente errato!

Potrei fare altri esempi, ma rimando i curiosi a scaricarsi l’SDVL da qui:
http://digilander.libero.it/ice00/program/pc-virus/svdl.tar.gz

Indubbiamente SVDL era agli inizi, offrendo solo un sottoinsieme di istruzioni per replicare le operazioni di test dei virus che il processore metteva a disposizione, ma penso che l’approccio algoritimico potesse essere promettente, soprattutto con l’analisi dei virus polimorfici.

La ricerca più interessante la vedremo la prossima volta, quando analizzeremo la “scansione” delle memoria per trovare i virus TSR e scopriremo come vengono gestite le chiamate al sistema operativo/bios.

One Response to “Virus e Antivirus: la ricerca algoritmica dei virus… (2)”

  1. Francesco.SPOW Says:

    decisamente una lettura interessante. complimenti!

Leave a Reply