10 Algoritmi d’offuscamento

 

Data l’architettura d’offuscamento vista nella sezione 3, la definizione di qualità d’offuscamento nella sezione 5, e la discussione di varie trasformazioni offuscanti nelle sezioni da 6 a 9, ora siamo in grado di presentare degli algoritmi dettagliati.

Il ciclo principale di un tool d’offuscamento avrà la seguente struttura generale:

 

SelectCode restituisce il prossimo codice oggetto sorgente che deve essere offuscato.

SelectTransform restituisce la trasformazione che dovrebbe essere usata per offuscare il particolare codice oggetto sorgente.

Apply applica le trasformazioni al codice oggetto sorgente e di conseguenza aggiorna l’applicazione.

Done determina quando il richiesto livello d’offuscamento è stato ottenuto.

La complessità di queste funzioni dipenderà dalle sofisticazioni del tool di offuscamento.

L’algoritmo 1 dà una descrizione di un tool per l’offuscamento del codice con molte selezioni sofisticate e comportamento di terminazione. L’algoritmo fa uso di diverse strutture dati che sono costruite dagli algoritmi 5, 6 e 7:

 _ Per ogni codice oggetto sorgente S, è l’insieme dei costrutti del linguaggio che il programmatore ha usato in S.

è usato per trovare le trasformazioni offuscanti appropriate per S.

 

A _Per ogni  codice oggetto sorgente S, A(S) = {} è una mappatura tra le trasformazioni  e i valori  , che descrive quanto appropriato potrebbe essere applicare  a S.

L’idea è che certe trasformazioni possono essere inappropriate per un certo codice oggetto sorgente S, perché esse introducono nuovo codice che è innaturale per S. Il nuovo codice potrebbe trovarsi fuori posto in S e quindi potrebbe essere facile da individuare per un reverse engineer.

Maggiore è l’appropriatezza del valore  e meglio il codice introdotto dalla trasformazione  sarà opportuno.

 

I _  Per ogni codice oggetto sorgente  S, I(S) è la priorità d’offuscamento di S.

I(S) descrive quanto importante è offuscare il contenuto di  S. Se S contiene importanti segreti commerciali allora I(S) sarà alto, se S contiene principalmente codice non importante I(S) sarà basso.

 

R per ogni routine M, R(M) è il rango del tempo d’esecuzione di M.

R(M) = 1 se è speso più tempo per eseguire M che le altre routine.

 

L’input principale dell’Algoritmo 1 è un’applicazione A ed un insieme di Trasformazioni offuscanti

{} . L’algoritmo richiede anche informazioni riguardanti ogni trasformazione, in particolare tre funzioni di qualità , , e  (simili ai loro omonimi nella sezione 5, ma che restituiscono valori numerici.) ed una funzione :

 

 restituisce una misura della resilienza della trasformazione T quando è applicata  al codice oggetto sorgente S.

 restituisce una misura della potenza della trasformazione T quando è applicata  al codice oggetto sorgente S.

restituisce una misura del tempo d’esecuzione e dell’overhead di spazio aggiunto da T a S.

 mappa ogni trasformazione T nell’insieme dei costrutti del linguaggi che T aggiungerà all’applicazione.

 

I punti 1, 2, 3 dell’Algoritmo 1 caricano l’applicazione che deve essere offuscata, e costruiscono delle appropriate strutture dati interne. Il punto 4 costruisce , A(S), I(S), e R(M).

Il punto 5 applica le trasformazioni offuscanti finché il richiesto livello d’offuscamento non è stato ottenuto o finché non è superato il massimo overhead del tempo d’esecuzione.

Il punto 6, infine, riscrive la nuova applicazione A'.


 

ALGORITMO 1 (OFFUSCAMENTO DEL CODICE)

 

Input: 

a) Un’applicazione A costituita dai file  del  codice sorgente o del codice oggetto.

b) Le librerie standard  definite dal linguaggio.

c) Un insieme di trasformazioni offuscanti {}.

d) Una mappatura  che, per ogni trasformazione T dà l’insieme  dei costrutti del linguaggio che T aggiungerà all’applicazione.

e) Tre funzioni , , e che esprimono la qualità di una trasformazione T rispetto ad un codice oggetto sorgente S.

f) Un insieme di dati di input I={} ad A.

g) Due valori numerici AcceptCost>0 e ReqObf>0. AcceptCost è  una misura del massimo overhead in termini di tempo/spazio che l’utente è disposto ad accettare. ReqObf è una misura della quantità di offuscamento che è richiesta dall’utente.

 

Output:

Un’applicazione offuscata A' costituita da file contenenti codice sorgente o codice oggetto.

 

Passi: 

1.     Caricare l’applicazione  da offuscare. L’offuscatore può fare una delle seguenti cose:

     (a)      caricare file contenenti codice sorgente, nel quale caso l’offuscatore dovrebbe contenere un compilatore completo per la fase di front-end, cioè per eseguire analisi lessicale, sintattica e semantica.

     (b)      caricare file contenenti codice oggetto. Se il codice oggetto conserva parte o tutte le informazioni del codice sorgente ( come nel caso dei file contenenti le classi Java), questo metodo è preferibile.

2.     Caricare il codice contenuto nei file delle librerie  referenziate direttamente o indirettamente dall’applicazione.

 

3.     Costruire una rappresentazione interna dell’applicazione. La scelta della rappresentazione dipende dalla struttura interna del linguaggio sorgente e dalla complessità delle trasformazioni che l’offuscatore implementa. Un tipico insieme di strutture può includere:

(a) un grafo di controllo del flusso (CFG) per ogni routine A.

(b) un call-graph per le routine in A

(c) un grafo di ereditarietà per le classi in A.

 

4.     Costruire R(M) e (usando l’Algoritmo 5), I(S) (usando l’Algoritmo 6), e A(S) (usando l’Algoritmo 7).

 

5.     Applicare le Trasformazioni offuscanti all’applicazione. Ad ogni passo selezioniamo un codice oggetto sorgente S da offuscare ed una trasformazione adeguata T da applicare a S. Il processo termina quando il livello di offuscamento richiesto è stato raggiunto o il costo accettabile per il tempo di esecuzione è stato superato.

    REPEAT

       S  SelectCode(I);

       T SelectTransform(S,A);

       Applica T ad S ed aggiorna le strutture dati rilevanti del punto 3;

    UNTIL Done(ReqObf, AcceptCost, S, T, I)

 

6.     Ricostituire il codice oggetto sorgente offuscato in una nuova applicazione offuscata X.

 


 

ALGORITMO 2 (SelectCode)

 

Input :  Il mapping della priorità di offuscamento I come computato dall’Algoritmo 6.

 

Output : Un codice oggetto sorgente S.

 

I mappa ogni oggetto sorgente S in I(S), che è una misura di quanto è importante sia offuscare  S. Per selezionare il prossimo codice oggetto sorgente da offuscare, possiamo trattare I come una coda a priorità. In altre parole, selezioniamo S in modo da massimizzare I(S).

 


 

ALGORITMO 3 (SelectTransform)

 

Input :

a) Un codice oggetto sorgente S.

b) La mappa di appropriatezza come computato nell’Algoritmo 7.

 

Output : Una trasformazione T.

 

Qualunque numero di euristiche può essere usato per selezionare la trasformazione più adatta da applicare ad un particolare codice oggetto sorgente S. In ogni modo, ci sono due aspetti importanti da considerare. Primo, la trasformazione scelta deve essere inglobata in modo naturale con il resto del codice in S. Ciò può essere trattato favorendo le trasformazioni aventi un alto valore di appropriatezza in A(S). Secondo, vogliamo favorire le trasformazioni che danno alti livelli di offuscamento con bassi costi di overhead. Ciò è realizzato selezionando le trasformazioni che massimizzano la potenza e la resilienza., e minimizzare il costo. Queste euristiche sono raccolte dal seguente codice , dove  sono costanti definite dall’implementazione :

Restituisci una trasformazione T tale che

e   è massimizzata.

 


 

ALGORITMO 4 (Done)

Input:

a)     ReqObf, il livello d’offuscamento rimanente.

b)    AcceptCost, la rimanente penalità accettabile per il tempo d’esecuzione.

c)     Un codice oggetto sorgente S.

d)    Una trasformazione T

e)     Una mappa delle priorità d’offuscamento I.

 

Output:

a)     Un ReqObf aggiornato.

b)    Un AcceptCost aggiornato.

c)     Una mappa delle priorità d’offuscamento I aggiornata.

d)    Un valore di ritorno booleano che è TRUE se la condizione di terminazione è stata raggiunta.

 

La funzione Done svolge due compiti. Aggiorna la coda a priorità I la quale riflette che codice oggetto sorgente è stato offuscato e dovrebbe ricevere una riduzione del valore di priorità. La riduzione è basata su una combinazione della resilienza e della potenza della trasformazione.

Done aggiorna anche ReqObf e AcceptCost, e determina se la condizione di terminazione è stata raggiunta.   sono costanti definite dall’implementazione :

       

               

        ;

        RETURN AcceptCost≤0 OR ReqObf≤0;

 

 


 

 

ALGORITMO 5 ( INFORMAZIONI PRAGMATICHE )

Input:

a)        Un’applicazione A.

b)        Un insieme di dati in input I={} ad A.

 

Output:

a)     R(M) che, per ogni routine M in A, dà il rango del tempo d’esecuzione di M

b)    , che, per ogni codice oggetto sorgente S in A, dà l’insieme dei costrutti del linguaggio usati in S.

 

Computare le informazioni pragmatiche. Questa informazione sarà usata per scegliere il giusto tipo di trasformazione per ogni particolare codice oggetto sorgente.

1.     Calcolare le informazioni pragmatiche dinamiche. In altre parole, si esegue l’applicazione attraverso un profiler con l’insieme dei dati d’input I fornito dall’utente.

            Calcolare R(M) per ogni routine/basic block, indicando dove l’applicazione la maggior parte del suo tempo.

2      Calcolare le informazioni pragmatiche statiche .

         fornisce statistiche sui tipi di costrutti del linguaggio che il programmatore ha usato in S.

      FOR S ogni codice oggetto sorgente in A DO

            Ol’insieme d’operatori che S usa;

            Cl’insieme dei costrutti del linguaggio ad alto livello (WHILE, eccezioni, threads, etc.)  che S usa;

            L← l’insieme di classi/routine di libreria che S referenzia;

           ;

      END FOR

 


 

ALGORITMO 6 (PRIORITA’ DELL’OFFUSCAMENTO)

 

Input:

a)     Un’applicazione A.

b)    R(M), il rango di M.

 

Output:

I(S) il quale, per ogni codice oggetto sorgente S in A, dà la priorità dell’offuscamento di S.

 

I(S) può essere fornito esplicitamente dall’utente, o può essere calcolato usando un’euristica basata sui dati statistici raccolti con l’Algoritmo 5. Possibili euristiche possono essere:

1.     Per ogni routine M in A, sia I(M) inversamente proporzionale al rango di M, R(M). In altre parole, l’idea è che "se molto tempo è speso ad eseguire una routine M, allora M è probabilmente una procedura importante che dovrebbe essere pesantemente offuscata”.

2.     Sia I(S) la complessità di S, come definito da una delle metriche della Tabella 1. L’intuizione è che il codice complesso è più probabile che contenga importanti segreti commerciali che semplice codice.

 


 

ALGORITMO 7 (APPROPRIATEZZA DELL’OFFUSCAMENTO)

 

Input:    

a)     Un’applicazione A.

b)     che, per ogni trasformazione T, dà l’insieme dei costrutti del linguaggio saranno aggiunti all’applicazione.

c)     che, per ogni codice oggetto sorgente S in A, dà l’insieme dei costrutti del linguaggio usati in S.

output:

             A(S) che, per ogni codice oggetto sorgente S in A ed ogni trasformazione T, dà l’appropriatezza di T rispetto a S.

 

La computazione dell’insieme d’appropriatezza A(S) per ogni codice oggetto sorgente S è basata principalmente sulle informazioni pragmatiche statiche calcolate con l’ Algoritmo 5.

 

            FOR S ogni codice oggetto sorgente in A DO

               FOR Togni trasformazione DO

                   V grado di somiglianza tra e ;

                   ;

               END FOR

            END FOR