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
SelectCode
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.
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).
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.
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