Un esempio di... pirateria

Il presente paragrafo intende rappresentare esclusivamente un esempio di come i debugger e il linguaggio C possano essere utilizzati per gestire in profondità l'interazione tra hardware e software; il caso pratico descritto[1] va interpretato esclusivamente come un espediente didattico: non abbiamo alcuna intenzione di incitare, neppure indirettamente, il lettore all'illecito.

Inoltre non è questa la sede per addentrarsi in una descrizione dettagliata delle tecniche utilizzate per proteggere il software dalla duplicazione; basta ricordare che esse si basano spesso su modalità particolari di formattazione del dischetto da proteggere, tali da rendere il medesimo non riproducibile dai programmi FORMAT e DISKCOPY. Il programma protetto effettua uno o più controlli, di norma accessi in lettura e/o scrittura alle tracce formattate in modo non standard, dai risultati dei quali è in grado di "capire" se il proprio supporto fisico (in altre parole, il disco) è la copia originale oppure ne è un duplicato.

Individuare la strategia di protezione

Veniamo ora al nostro esempio. Il programma in questione, PROG.COM, è protetto contro la duplicazione non autorizzata; il comando DISKCOPY è in grado di copiare il disco sul quale esso si trova, ma con risultati scadenti: la copia di PROG.COM, non appena invocata, visualizza un messaggio di protesta e restitusce il controllo al DOS. Come si può facilmente prevedere, neppure il comando COPY è in grado di superare l'ostacolo: copiando il contenuto del disco originale sul disco fisso (o su altro floppy disk) si ottiene un risultato analogo al precedente. Quando viene invocata la copia su hard­disk di PROG.COM la spia del drive A: si illumina per qualche istante, viene visualizzato il solito messaggio e l'esecuzione si interrompe.

Proviamo a studiare un interessante frammento tratto dal disassemblato di PROG.COM, ottenibile, ad esempio, con il solito DEBUG del DOS:

CS:0100 EB50          JMP    0136
....
CS:0199 3C20          XOR    AL,AL        ; azzera AL
CS:019B 8AD0          MOV    DL,AL        ; muove AL in DL
CS:019D 32F6          XOR    DH,DH        ; azzera DH
CS:019F 8CDB          MOV    BX,DS        ; muove DS in ES
CS:01A1 8EC3          MOV    ES,BX        ; attraverso BX
CS:01A3 BBF007        MOV    BX,18FB      ; carica BX
CS:01A6 B90102        MOV    CX,0201      ; carica CX
CS:01A9 B80402        MOV    AX,0204      ; carica AX
CS:01AC CD13          INT    13           ; chiama int 13h
CS:01AE 720A          JC     01BA         ; salta se CF = 1
CS:01B0 BABA00        MOV    DX,154F      ; carica DX
CS:01B3 B409          MOV    AH,09        ; stampa stringa
CS:01B5 CD21          INT    21           ;    tramite DOS
CS:01B7 E97DFF        JMP    0156         ; salta indietro
CS:01BA 80FC04        CMP    AH,04        ; confr. AH con 4
CS:01BD 75F1          JNE    01B0         ; salta se !=
CS:01BF B82144        MOV    AX,4421
....

Come per tutti i programmi .COM, l'entry point[2] si trova alla locazione CS:0100: dopo l'istruzione JMP 0152 l'esecuzione prosegue a CS:0136, presumibilmente con le opportune operazioni di inizializzazione, ininfluenti ai nostri fini. Le istruzioni commentate sul listato meritano particolare attenzione. A CS:01AC viene richiesto l'int 13h (che gestisce i servizi BIOS relativi ai dischi; vedere il capitolo dedicato agli interrupt).

I valori caricati nei registri della CPU rivelano che PROG.COM, mediante l'int 13h, legge in memoria i settori 1, 2, 3 e 4 della seconda traccia del lato 0 del disco che si trova nel drive A:. Al ritorno dall'int 13h (CS:01AE) viene effettuato un test sul CarryFlag: se questo è nullo, cioè se in fase di lettura non si è verificato alcun errore (comportamento "normale" se il disco è formattato secondo lo standard DOS) l'esecuzione prosegue a CS:01B1, è stampata una stringa (il messaggio di protesta) e viene effettuato un salto a ritroso alla locazione CS:0156, ove sono effettuate, con ogni probabilità, le operazioni di cleanup e uscita dal programma (infatti il programma termina l'esecuzione subito dopo avere visualizzato il messaggio). Se, al contrario, il CarryFlag vale 1 (condizione di errore), l'esecuzione salta a CS:01BA, dove PROG.COM effettua un controllo sul valore che la chiamata all'int 13h ha restituito in AH. Se esso è 04h (codice di errore per "settore non trovato") l'esecuzione prosegue a CS:01BF (controlli superati: il disco è la copia originale); in caso contrario avviene il salto a CS:01B0, con le conseguenze già evidenziate.

La strategia è ormai chiara: la traccia 2 del lato 0 della copia originale è formattata in modo non standard. Un tentativo di leggerne alcuni settori determina il verificarsi di una condizione di errore, ed in particolare di "settore non trovato". Se l'int 13h non riporta entrambi questi risultati, allora PROG.COM "conclude" che il disco è una copia non autorizzata. Si tratta ora, semplicemente, di trarlo in inganno.

Superare la barriera

Smascherata la strategia di PROG.COM, occorre resistere alla tentazione di modificare il codice disassemblato, ad esempio sostituendo una serie di NOP [3] alle istruzioni comprese tra CS:01ADCS:01BE (significherebbe eliminare l'accesso al disco e tutti i controlli), e riassemblarlo per ottenerne una versione meno "agguerrita": si rischierebbe di incappare in altri trabocchetti[4] e rendersi la vita difficile senza alcuna utilità. E' sicuramente più opportuno simulare il verificarsi delle condizioni di errore ricercate in fase di test: per ottenere tale risultato è sufficiente un programmino in grado di installare un gestore personalizzato dell'int 13h.

Questo deve scoprire se la chiamata proviene da PROG.COM: in tal caso occorre restituire le ormai note condizioni di errore senza neppure accedere al disco; altrimenti è sufficiente concatenare la routine originale di interrupt.

Intercettare la chiamata di PROG.COM è semplice: basta un'occhiata alla tabellina riportata poc'anzi per capire che un test sui registri AX, CX e DX può costituire una "trappola" (quasi) infallibile.

Ecco il listato:

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

   LOADPROG.C - Barninga_Z! - 1990

       Lancia PROG.COM gestendo opportunamente l'int 13h

   COMPILABILE SOTTO BORLAND C++ 2.0

       bcc -O -d -mt -lt loadprog.c

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

#pragma  inline
#pragma  option  -k-      // fondamentale!!! Evita std stack frame

#include <stdio.h>
#include <process.h>
#include <dos.h>

#define  CHILD_NAME     "PROG.COM"   // programma da lanciare

char *credit =
"GO-PROG.EXE - Cracking loader for "CHILD_NAME" - Barninga_Z!, 1992\n\n\
Press a key when ready...\a\n";

char *errorP =
"Error while executing "CHILD_NAME"\a";

void oldint13h(void)    // dummy function: puntatore a int 13h originale
{
    asm dd 0;      // riserva 4 bytes per l'idirizzo dell'int 13h
}

void far newint13h(void)
{
    if(_AX == 0x0204 && _CX == 0x0201 && _DX == 0) {      // PROG.COM chiama
        asm {
            stc;   // setta il carry per simulare errore lettura
            mov ax,0400h;  // simula errore "sector not found"
            ret 2; // esce e toglie flags da stack
        }
    }
    else
        asm jmp dword ptr oldint13h;   // concatena l'int 13h originale
}

void cdecl kbdclear(void)
{
    asm {
        mov ax,0C07h;
        int 21h;       // vuota buffer tastiera e attende tasto
    }
}

void cdecl main(void)
{
    puts(credit);
    kbdclear();
    asm cli;
    (void(interrupt *)())*(long far *)oldint13h = getvect(0x13);
    setvect(0x13,(void(interrupt *)())newint13h);
    asm sti;
    if(spawnl(P_WAIT,CHILD_NAME,CHILD_NAME,NULL))  // esegue PROG.COM
        perror(errorP);        // errore load/exec di PROG.COM
    asm cli;
    setvect(0x13,(void(interrupt *)())*(long far *)oldint13h);
    asm sti;
}

La struttura di LOADPROG.C è semplice: esso si compone di tre routine, a ciascuna delle quali è affidato un compito particolare.

La funzione main() installa il nuovo vettore dell'int 13h, lancia PROG.COM mediante spawnl() e, al termine dell'esecuzione di PROG.COM, ripristina il vettore originale dell'int 13h. Si noti che spawnl() viene invocata con la costante manifesta (definita in PROCESS.H) P_WAIT: ciò significa che LOADPROG.COM rimane in RAM durante l'esecuzione di PROG.COM (che ne costituisce un child process) in attesa di riprendere il controllo al termine di questo, in quanto è necessario effettuare il ripristino dell'int 13h originale. Se non si prendesse tale precauzione, una chiamata all'int 13h da parte di un programma eseguito successivamente avrebbe conseguenze imprevedibili (e quasi sicuramente disastrose, come al solito).

La funzione newint13h() è il gestore dell'int 13h. Essa, in ingresso, controlla se i registri AX, BX e DX contengono i valori utilizzati da PROG.COM nella chiamata all'interrupt: in tal caso viene posto uguale a 1 il CarryFlag, AX a 4 e AL a 0; il controllo ritorna alla routine chiamante senza che sia effettuato alcun accesso al disco. Si noti l'istruzione RET 2, che sostituisce la più consueta IRET: una chiamata ad interrupt salva automaticamente sullo stack i flag; se newint13h() restituisse il controllo a PROG.COM con una IRET, questa ripristinerebbe in modo altrettanto automatico i flag prelevandoli dallo stack e l'istruzione STC non avrebbe alcun effetto; d'altra parte una RET senza parametro "dimenticherebbe" una word sullo stack, causando probabilmente un crash di sistema[5]. Se, al contrario, i valori di AX, BX e DX non sono quelli cercati, newint13h() concatena il vettore originale saltando all'indirizzo salvato da getvect() nei byte riservati dalla funzione jolly oldint13h() [6], lasciando che tutto proceda come se LOADPROG non esistesse.

La funzione kbdclear() pulisce il buffer della tastiera e attende la pressione di un tasto.

Un'ultima osservazione: la direttiva

#pragma option -k-

evita che il compilatore generi il codice necessario al mantenimento della standard stack frame. In assenza di tale opzione sarebbe indispensabile aggiungere l'struzione POP BP prima della RET e della JMP in newint13h(); inoltre si potrebbe eliminare l'istruzione DD 0 in oldint13h() in quanto il codice della funzione occuperebbe di per sé 5 byte (gli opcode corrispondenti alle istruzioni necessarie alla standard stack frame stessa). La direttiva può essere eliminata qualora si specifichi ­k­ tra le opzioni sulla riga di comando del compilatore.

Sulla retta via...

A cosa può servire un esempio di pirateria? Ovviamente, a fornire spunti utili in situazioni reali (e lecite!). Vi sono programmi, piuttosto datati, che alla partenza modificano vettori di interrupt senza poi ripristinarli in uscita. Se tra i vettori modificati ve ne sono alcuni utilizzati da più recenti software di sistema, il risultato è quasi certamente la necessità di un reset della macchina. Un loader analogo a quello testè presentato può aggirare il problema.


OK, andiamo avanti a leggere il libro...

Non ci ho capito niente! Ricominciamo...