4. Offuscamento parametrizzato di variabili

 

Verranno ora trattate le trasformazioni di offuscamento riguardanti variabili primitive, in particolare per tipi di dati interi.

 


 

4.1 Il problema

 

Le trasformazioni di offuscamento parametrizzato si prefiggono i seguenti obiettivi:

 

*     produrre tecniche di offuscamento che rallentino il  reverse engineering.

 

*     usare la parametrizzazione in modo tale che, qualora un malintenzionato riesca ad aggirare un dato sistema basandosi solamente su dei dati interni ad esso, abbia poco vantaggio dalla precedente impresa con dati interni diversi. In pratica i dettagli dell’offuscamento dovrebbero dipendere da dati random.

 

*     supportare operazioni aritmetiche (addizione, moltiplicazione, operatori logici) direttamente sulla versione offuscata del software.

 

*     nascondere un parametro necessario p. Infatti il processo di una variabile parametrizzata può essere rappresentato come una tripla [A,p,e] dove:

 

-e: è un valore/variabile non offuscato;

-A: è il tipo di algoritmo della trasformazione offuscante;

-p: è il parametro a run-time che modifica il comportamento della trasformazione.

 

*     nascondere le relazioni tra le variabili. Ossia il malintenzionato non deve essere in grado di capire lo schema di offuscamento, anche se si presentano blocchi simili offuscati con lo stesso schema, o blocchi in relazione da alcune variabili.

 

 


 

 

4.1.1 Uso di variabili

 

Il modo in cui una variabile (di un particolare tipo) viene usata, è un fattore chiave per il successo di una trasformazione offuscante. Una variabile usata solo come valore di riferimento (o solo per essere trasmessa ad un altro sistema) può supportare un piccolo numero di operazioni rispetto ad una variabile usata come accumulatore (contatore di un ciclo). Se un sistema usa una variabile solo per trasmetterla ad un altro sistema, allora la trasformazione di offuscamento è la stessa richiesta da un’operazione di cifratura come AES o RSA.

 

 

1.      Il limite per un contatore: (con uso facoltativo del contatore all'interno del ciclo):

 

Dato il seguente ciclo for:

 

for (int i = 0; i < secret_int; ++i)

{

  bar(foo(a),b);

}

 

Se secret_int è non negativo allora il test del ciclo può essere rimpiazzato con  i != secret_int. Il valore del contatore del ciclo è utilizzato all’interno del ciclo stesso, quindi è possibile rimpiazzare:

 

bar(foo(a),b); con  bar(foo(i), b).

 

Se il contatore è offuscato in modo da poter essere confrontato con secret_int, un malintenzionato che riesce ad accedere alla versione offuscata del frammento, può vedere le seguenti cose:

 

*           offuscamento di zero confrontabile con l’offuscamento di secret_int;

 

*           l’operazione offuscata ++i confrontabile con l’offuscamento di secret_int;

 

*            il test di uguaglianza, che alcuni sistemi eseguono con un semplice test sui bit ;

 

*            se il corpo del ciclo contiene bar(foo(i), b) allora il malintenzionato può vedere il deoffuscamento compatibile con l’offuscamento di secret_int;

 

*            se il corpo del ciclo contiene bar(foo(i), b) e il deoffuscamento avviene in foo(), allora il malintenzionato può vedere le tecniche di offuscamento supportate per tutte le operazione di cui necessita foo().

 

 

2.      Accumulatore limitato (dove il valore di foo( ) può essere abbastanza grande):

 

Dato il seguente frammento di codice:

 

secret_total = 0;

# run time oppure compile time

 

while (secret_total < secret_int) {

 secret_total = secret_total + foo();

}    

 

Se secret_total è offuscato in modo da essere confrontato con secret_int e sommato con  foo(), un malintenzionato che ha accesso alla versione offuscata di questo frammento di codice, può vedere le seguenti cose:

 

*            il test relazionale offuscato “<” tra secret_total e secret_int;

 

*            l’operazione aritmetica offuscata di foo()+secret_total o il deoffuscamento di entrambi e l’uso della normale addizione seguito dall’offuscamento;

 

*            entrambi i valori di ritorno offuscati di foo() in un modo confrontabile con secret_total, prima o dopo il return. Bisogna fare attenzione che l’uso di tecniche di offuscamento non indebolisca l’offuscamento del control-flow.

 

 

3.      Aritmetica semplice usando interi segreti:

 

Dato il seguente assegnamento:

 

secret_int_A = (secret_int_B + secret_int_C)* secret_int_A;

 

Se le variabili sono offuscate in modo tale che “+” e “*” hanno la stessa forma offuscata allora un malintenzionato può vedere quanto segue:

 

*            l’aritmetica offuscata “+” e “*”;

 

*            l'uso di offuscamenti confrontabili provoca relazioni fra le variabili offuscate. Questi rapporti transitivi fra le variabili vengono chiamati “transitività operativa” e possono limitare separatamente le differenze dell’ offuscamento del programma.

 

 

4.      Calcoli complessi usando interi segreti (interi a precisione multipla mod n):

 

In questo esempio secret_exponent_temp e encrypted_message hanno bisogno di essere offuscati.

 

secret_total = 1;

secret_partial = encrypted_message;

while (secret_exponent_temp != zero ) {

if (lsb(secret_exponent_temp) == 1 )

secret_total = secret_total * secret_partial;

secret_exponent_temp == secret_exponent_temp >> 1;

secret_partial = secret_partial * secret_partial;

}    

# secret_total contiene il messaggio decifrato ma offuscato

 

Un malintenzionato che ha accesso a questo codice vedrà il deoffuscamento dei valori a meno che le tecniche di offuscamento supportino l'equivalente di  secret_total * secret_partial”. L’offuscamento di secret_exponent_temp può essere realizzato in diversi modi.

 

*            L’offuscamento di 1 in modo compatibile con secret_int.

 

*            L’operazione offuscata “*” tra secret_total  e  secret_partial.

 

*            L’operazione “*” offuscata con bit shiftati  tra secret_exponent_temp e il test di uguaglianza offuscato con il bit meno significativo del valore non offuscato di secret_exponent_temp.

 

 

5.      Variabili e operazioni booleane:

 

Data la seguente istruzione condizionale:

 

if (foo AND bar )

{

  total = total + 1;

}

 

Un malintenzionato che ha accesso a questo codice vedrà entrambe i deoffuscamenti dei valori o l’equivalente offuscato dell’and logico. Per le variabili booleane è importante che esistano gli stati per ogni corrispondente valore offuscato.  L’offuscare solo uno tra “true” e “false” indebolisce l’offuscamento.

 

 

6.      Variabili di ogni tipo:

 

*            La variabile è offuscata a tempo di compilazione (che è impercettibile al malintenzionato) o a run-time. Nell’esempio seguente sensitive_info non offuscato è stato ottenuto semplicemente dal programma attraverso il suo environment, sensitive_info allora è offuscato in modo tale da permettere al risultato, secret_integer, di essere usato altrove dove  necessario all'interno del programma.

 

  # obfus_tran_A() è implementato con l’inline bytecodes

  secret_integer = obfus_tran_A(param_foo, sensitive_info);

  # sovrascrittura

  sensitive_info = <unused data>;

 

Un malintenzionato che ha accesso a questo codice può essere in grado di esaminare       obfus_tran_A(p, arg) ed è quindi in grado di determinare p e deoffuscare ogni variabile offuscata in modo simile. Inoltre egli può effettuare delle corrette operazioni aritmetiche e logiche, sulle variabili offuscate o sulle variabili correlate attraverso la conoscenza dell’algoritmo usato dal tool di offuscamento.

 

*            Il programma offuscato può scegliere di deoffuscare un valore ed esporlo ad un ambiente esterno a patto che si verifichino determinate condizioni.

 

  # obfus_tran_A() è implementato con l’inline bytecodes  

  sensitive_info = deobfus_tran_A(param_foo, secret_integer);

     

Un malintenzionato che ha accesso a questo codice può essere in grado di deoffuscare il codice e le variabili (offuscate in modo simile). Inoltre, può effettuare corrette operazioni aritmetiche e logiche sulle variabili offuscate o sulle variabili correlate attraverso la conoscenza dell’algoritmo usato dal tool di offuscamento.

 

Le variabili parametrizzate dovrebbero  resistere sia all’analisi statica che dinamica.

 

Abbiamo presentato diversi esempi dove operazioni su interi correlati, sono spesso distribuite nella sequenza di esecuzione del programma. Se l’analisi dinamica può essere effettuata su tale distribuzione di valori in modo efficiente in termini di tempo, allora un malintenzionato può usare questi attacchi sulle operazioni più deboli o su una combinazione di operazioni scoprendo le relazioni più impercettibili tra le operazioni disponibili.

 

 


 

 

4.2 Criteri per il  successo

 

Generalmente lo stato deoffuscato di una variabile dovrebbe essere uguale a quello della variabile priva di offuscamento nello stesso punto dell’esecuzione del programma non offuscato. All'offuscamento delle variabili non è permesso di bloccare l’input del programma. Tuttavia, questo è soltanto relativo ad input legale. Nel caso "di input irragionevole" o illegale al programma, il comportamento può divergere in direzioni più o meno simili alla versione non offuscata.

 

Ora mostreremo delle tecniche per generare i parametri di trasformazione di offuscamento e le relative tecniche usate per offuscare.

 

 


 

 

4.2.1 Criteri per generare parametri di offuscamento

 

I criteri su cui si devono basare le tecniche per generare parametri di offuscamento sono i seguenti:

 

*      Il malintenzionato per capire il funzionamento completo del programma offuscato deve determinare i parametri di offuscamento della variabile.

 

*      L’assegnamento di un parametro di una variabile non deve aiutare un malintenzionato  nella comprensione del programma offuscato. Per esempio, l'accesso allo stesso parametro di offuscamento da due frammenti differenti del codice offuscato non dovrebbe permettere al malintenzionato di capire facilmente che i frammenti sono correlati. Usando lo stesso parametro per due variabili offuscate potrebbe confonderlo nel pensare che due variabili indipendenti sono dipendenti, ma se egli  riesce a deoffuscare una variabile allora avrà una migliore comprensione dell'altra.

 

*      L'insieme dei parametri possibili per una variabile dovrebbe essere abbastanza grande ed essere distribuito uniformemente in modo random, in modo che la ricerca esaustiva del parametro corretto sia sufficientemente difficile.

 

*      I parametri di offuscamento dovrebbero essere esposti quanto meno possibile e per il periodo di tempo più breve possibile.

 

 


 

 

4.2.2 Criteri per proteggere la rappresentazione di dati

 

Dovrebbero esserci un numero significativo di parametri di offuscamento per ogni tipo di dato primitivo all’interno delle librerie di offuscamento. I test di verifica delle trasformazioni di offuscamento sono i seguenti:

 

*      Se  possibile deve essere tenuta segreta quale tipo di trasformazione di offuscamento è stata usata per proteggere una particolare variabile. Assumendo che il malintenzionato  abbia una copia del sistema di offuscamento e quindi conosce l'insieme delle possibili trasformazioni, dovrà applicare un certo sforzo per determinare quale trasformazione si usa per proteggere una particolare variabile.

 

*      Il malintenzionato che non conosce il parametro di offuscamento della variabile non dovrebbe poter deoffuscare una variabile tranne che con la ricerca esaustiva.

 

*      La trasformazione di offuscamento dovrebbe servirsi di semplici operazioni di base:

 

o       Per le variabili intere le operazioni di base sono:

addizione, sottrazione, moltiplicazione, divisione, resto, AND, OR, NOT, XOR e bit shift.

Come detto precedentemente ci sono alcune situazioni dove una variabile non ha bisogno di supportare nessuna operazione (decodifica semplice). Altre volte si ha bisogno di supportare solo poche operazioni su un piccolo sottoinsieme di interi (incremento di 1 in un ciclo).

Offuscare interi java generalmente è difficile. C’è bisogno di supportare un gran numero di valori anche per poche operazioni. Una difficoltà significativa è la transitività delle operazioni; una data variabile ha bisogno di supportare operazioni che non sono utilizzate direttamente. Per esempio prendiamo A, B, C, D, T (T è l’intero offuscato) e la seguente istruzione:

 

T = A * B;

 

               e senza cambiare T:

 

D = C + T;

 

Tutte e 4 le variabili hanno bisogno di utilizzare la stessa tecnica di offuscamento che supporti sia il “+” che il “*”. In altre parole tutte le variabili dovranno usare parametri di offuscamento equivalenti. La JVM non indica eccezioni per errori aritmetici di divisione per 0  durante operazioni di resto e divisioni di interi. Questo errore è manipolato da ArithmeticException. Ciò è un rischio perché viene a mancare il corretto comportamento del programma nel caso di divisione per 0.

 

o       Per le variabili booleane le operazioni di base sono:

test, AND, OR, NOT.

 

 


 

 

4.3 Tecniche di offuscamento

 

Descriviamo due soluzioni per parametri di offuscamento utilizzati per controllare l’offuscamento delle variabili dei tipi base.

 

 


 

 

4.3.1 Tecniche per generare parametri di offuscamento

 

*      Utilizzo del control-flow offuscato:

 

In questo approccio i parametri usati per (de)offuscare variabili sono costruiti come parti di una serie di blocchi di codice. Affinché il malintenzionato  sia in grado di determinare un parametro di offuscamento in un dato punto dell’esecuzione del programma, egli deve conoscere la corretta esecuzione delle sottosequenze del programma.

 

Ad esempio, un programma consiste di un insieme di blocchi di codice, il blocco A seguito dal blocco B seguito dal blocco C dove la variabile offuscata v è adoperata da Funct() usando il parametro variabile pv1. Il blocco C è seguito dal blocco D dopo l’esecuzione di Funct(). Le etichette E e F si riferiscono ad altri blocchi di codice del programma offuscato che non fanno parte dell’esecuzione della sottosequenza:

A, …, B, … , C, … , D.

In questo esempio le variabili i, j, k e l non sono uguali quando sono utilizzate:

 

Il blocco A contiene:

 

            # T(i) si riferisce al parametro variabile pv1

# pv1 è conosciuto a priori ed è uguale a w

T(i) = T(i) + x

 

Il blocco B contiene:

 

# S(j) si riferisce al parametro variabile pv1

S(j) = S(j) * y ## in alternativa S(j) = S(j) OR y

 

Il blocco  C contiene:

 

# P(m) si riferisce al parametro variabile pv1

# Funct() è una operazione binaria

W(k) = W(k) + a

# P(m) è uguale a  ((w+x)*y)+a)

Z = Funct(P, m, V, X)

 

Il blocco D contiene:

 

# R(k) si riferisce al parametro variabile pv1

R(k) = R(k) + z

 

Il blocco E contiene:

 

# Q(l) si riferisce al parametro variabile pv1

L = Q(l) + zz

 

Se i blocchi sono eseguiti in ordine differente allora pv, diventa scorretto quando Funct() è eseguito. L’uso di un array di elementi per riferirci a pv serve per rendere un’analisi statica più difficile richiedendo al malintenzionato di effettuare ulteriori analisi.

 

L'uso di array di elementi per realizzare le assegnazioni ingannevoli ai parametri di offuscamento dovrebbe rendere l'associazione tra i blocchi del codice più difficile e forzare i malintenzionati ad effettuare una vasta analisi del control-flow per determinare i valori dei parametri di offuscamento.

 

 

 

*       Environment testing (test d’ambiente):

 

In questo metodo i parametri sono derivati dalle informazioni eventualmente offuscate che sono ottenute dal programma offuscato durante l'esecuzione, tipicamente queste informazioni sono fornite dal sistema che fa funzionare il programma offuscato, compreso il suo stato. Questo approccio è basato sul lavoro di Riordan e Schneier [9].

Ecco un semplice esempio: Il valore di X è nascosto creando hash(X). Nel programma offuscato viene memorizzato hash(X) e non X. Il programma opera ed interroga il sistema operativo usando una determinata interfaccia, o esamina lo stato del programma, anche diverse volte. I risultati Y1, Y2….   vengono sottoposti alla funzione hash creando hash(Y1) e hash(Y2) e confrontati con hash(X). Se hash(Yi) è esattamente uguale ad hash(X), allora Yi, o qualche altra funzione di Yi, è il deoffuscamento di hash(X). Senza l’appropriato input della funzione hash, il malintenzionato non è in grado di recuperare X. Questo metodo è simile alla tecnica trovata in [9].

 

Per esempio il blocco A contiene:

 

# DeObfs è una funzione di deoffuscamento parametrizzato

# Test() estrae alcune informazioni potenzialmente nascoste

# dagli stati del programma

x = Test()

if (y == Hash(x) ) then

z = DeObfs(x, v)

 

Questa tecnica dipende dalla configurazione della policy di offuscamento o da ciò che riguarda l'ambiente della macchina su cui il programma offuscato dovrebbe essere eseguito. Se il malintenzionato può attaccare il programma offuscato su uno degli host per cui il programma offuscato è stato configurato per funzionare, apportando piccoli cambiamenti distinguibili al programma offuscato questa tecnica fallisce.

 

 


 

 

4.3.2 Tecniche per proteggere la rappresentazione dei dati

 

1.      Cifrari a blocchi a chiave simmetrica e cifrari di flusso (DES, AES, Pohlig-Hellman, SEAL etc.):

 

Questo metodo sostituisce i numeri interi con il numero intero cifrato, usando un cifrario a blocchi a chiave simmetrica quali il DES [5] oppure AES [2] oppure un cifrario di flusso quale SEAL [7]. Più interi possono essere cifrati allo stesso tempo utilizzando uno cifrario di flusso oppure un cifrario a blocchi abbastanza grande come ad esempio AES in modalità 256 bit, o usando diverse tecniche di concatenamento [5] come CBC utilizzando un minor numero di blocchi cifrati. I parametri di offuscamento, le chiavi di cifratura e alcuni vettori di inizializzazione, sono gli stessi sia per l’offuscamento (cifratura) che per il deoffuscamento (decifratura). La maggior parte dei cifrari non supporta nessuna operazione su dati cifrati ad eccezione, in genere, del test di uguaglianza e dell’incremento offuscato. L'offuscatore applicato n volte ad un cifrario a blocchi (n è il limite del ciclo) ad un dato input genera un valore, v, che serve come limite del ciclo. Un’eccezione alla limitazione di molti cifrari a blocchi e cifrari di flusso è il crittosistema a chiave simmetrica Pohlig-Hellman [PH] che inoltre supporta la moltiplicazione offuscata.

 

C = Me mod p ## p è primo

M = Cd mod p ## e * d = 1 mod (p-1)

The Pohlig-Hellman Cryptosystem

 

Inoltre la moltiplicazione offuscata per un contatore semplice può essere implementata convertendo entrambi gli incrementi in moltiplicazioni offuscate con un’appropriata corrispondenza di limite inferiore e superiore o con cifrature ripetute. I parametri di offuscamento utilizzati nel crittosistema Pohlig-Hellman basato sulle moltiplicazioni offuscate non sono gli stessi di quelli del deoffuscamento. Le moltiplicazioni offuscate che usano solo il mod p (esposte), non permettono da sole al malintenzionato di offuscare e deoffuscare. Tuttavia usando l’offuscamento esponiamo potenzialmente i suoi parametri e viceversa. Le operazioni “>” e “<”, per un numero non troppo elevato di valori, possono essere implementate utilizzando una tabella nel crittosistema a chiave simmetrica senza deoffuscamento. L'indicizzazione della tabella può essere realizzata facendo l’hash della concatenazione dei valori cifrati che sono usati come operandi. I sistemi a chiave simmetrica per esempio possono essere usati per offuscare un intero albero di Parsing o parti di esso.

 

 

2.      Tecniche di cifratura a chiave asimmetrica (chiavi pubbliche):

 

Usando sistemi di cifratura a chiave asimmetrica come RSA [8], El-Gamal [4] o XTR [6] per l’offuscamento si hanno le stesse limitazioni di cifrari simmetrici; tuttavia sono supportate incremento di offuscamento e maggiori moltiplicazioni offuscate. Le maggiori differenze tra l’approccio con i cifrari a chiave simmetrica e i cifrari a chiave pubblica sono:

 

*      La conoscenza della chiave di cifratura/offuscamento non permette al malintenzionato di conoscere la chiave di decifratura/deoffuscamento. Ciò è vero se la chiave pubblica è usata per offuscare (nel crittosistema a chiave pubblica) e la chiave privata è usata per deoffuscare. Alcuni crittosistemi a chiave pubblica come RSA permettono che i ruoli della chiave di offuscamento e della chiave di deoffuscamento possono essere invertiti.

 

*      Alcuni sistemi basati su crittosistemi a chiave pubblica hanno la proprietà che la conoscenza di entrambe le chiavi di offuscamento/cifratura e deoffuscamento/decifratura non permette al malintenzionato di recuperare contemporaneamente altre chiavi. Per esempio con il crittosistema RSA se entrambi gli esponenti (quello della chiave pubblica e quello della privata) sono difficili da trovare, i valori delle due chiavi si proteggono a vicenda. Chiameremo questa variante RSA-bidirezionale. Comunque non normalmente usato in crittografia, RSA-bidirezionale risulta abbastanza semplice.

 

C = Me mod n  ## n = pq,(p e q sono primi)

M = Cd mod n  ## e * d = 1 mod (p-1)(q-1)

Crittosistema RSA

 

L’algoritmo supporta le moltiplicazioni offuscate e il limite semplice per il contatore (spiegato nel 4.1.1) può essere supportato con un’appropriata corrispondenza dei limiti superiori e inferiori. Tuttavia c’è bisogno del deoffuscamento se il contatore del ciclo è usato per altri scopi.

 

Le operazioni “>” e “<”, per un numero non troppo elevato di valori, possono essere implementate utilizzando una tabella nel crittosistema a chiave simmetrica senza deoffuscamento. L'indicizzazione della tabella può essere realizzata facendo l’hash della concatenazione dei valori cifrati che sono usati come operandi. I sistemi a chiave asimmetrica (chiave pubblica) possono essere usati per offuscare un intero albero di Parsing o parti di esso.

 

 

3.      Interi memorizzati come alberi di Parser:

 

Questo metodo rimpiazza gli interi con un albero di Parser con struttura simile a quella per una espressione che valuta gli interi offuscati. L’operazione di offuscamento delle variabili, come l’addizione, crea nuovi alberi di Parser che vengono combinati. Per esempio il valore 56 può essere rappresentato da entrambi gli alberi

 

 

Due alberi di Parser per l’intero 56

 

 

in base al valore del parametro di offuscamento. L’esempio della sezione 4.1.1  secret_int_D = (secret_int_B + secret_int_C) * secret_int_A; può essere rappresentato dal seguente albero di Parser creato per  secret_int_D:

 

 

 

Albero di Parser per: secret_int_D = (secret_int_B + secret_int_C) * secret_int_A

 

 

L’offuscamento di interi utilizzando solo i Parser tree è potenzialmente vulnerabile. Tool per analisi della memoria possono identificare gli alberi attraverso la loro struttura. Le analisi possono essere rese inutili offuscando gli operandi e gli operatori utilizzando (combinando) altre tecniche di offuscamento come: tecniche di cifratura, tabelle, re-mapping degli interi attraverso semplici permutazioni. Un altro metodo per offuscare gli alberi di Parser consiste nell’usare parametri di offuscamento per controllare l’applicazione di cambiamenti reversibili dell’albero come lo swap dei nodi o l’aggiunta di nuovi sottoalberi all’albero. Il problema principale con questa tecnica è che fanno la coda soltanto le operazioni per  l'esecuzione successiva piuttosto che le operazioni per l'attuale esecuzione. Se le tecniche di alberi di Parser sono combinate con altre tecniche di offuscamento che necessitano di deoffuscamento per permettere operazioni aritmetiche, AND, OR, NOT, XOR o altre operazioni, allora il deoffuscamento avverrà meno frequentemente.

 

L’utilizzo di alberi di Parser può rendere più difficile utilizzare alcune tecniche di offuscamento. Se il malintenzionato scopre un albero di Parser o il punto in cui avviene la sua valutazione, allora può scoprire la corretta sequenza delle operazioni (o dei blocchi di codice).

 

 

4.      Interi offuscati attraverso la mappatura in strutture matematiche (conosciute o nascoste) che non sono basate su crittosistemi:

 

Il requisito minimo per questo tipo di tecnica è che sia supportato un sottoinsieme di interi (come ad esempio tutti gli interi da 0 a 100). 

 

Non è molto importante se i numeri interi al di fuori dell’intervallo supportato non sono definiti o non sono supportati correttamente.

Poiché queste tecniche non sono basate su una  crittografia forte, determinate proprietà, quali trasformazioni di deoffuscamento resistenti ad attacchi basati sulla conoscenza di coppie offuscate e deoffuscate, non possono essere presenti.

 

*      Mappare il teorema cinese del resto:

 

Un importante proprietà nella teoria dei numeri ci dice che: presi degli interi a1, a2,…. aj, e un insieme di primi positivi m1, m2, …. mj, esiste un unico X tale che 0 <= X <= (N = mj) tale che X = aj  mod  mj. Possiamo offuscare un intero X codificandolo nella sua rappresentazione in teorema cinese del resto. I parametri di offuscamento e deoffuscamento sono i valori di N e la sua parziale fattorizzazione  m1, m2, …. mj.

Le normali operazioni aritmetiche come addizione e moltiplicazione possono essere implementate su rappresentazioni di CRT. Tuttavia implementare queste operazioni rischia di esporre i valori di m1, m2, …. mj, e conseguentemente consentirebbe al malintenzionato di effettuare operazioni di deoffuscamento sulle variabili offuscate utilizzando gli stessi parametri. Alcuni valori come 0, 1, 2 non offuscano bene.

Fino a che le operazioni di “<” e  “>” non saranno direttamente supportate nel CRT, possono essere implementate senza deoffuscamento, per un numero ragionevole di valori, utilizzando le tabelle. L’indicizzazione delle tabelle può essere realizzare facendo l’hash della concatenazione della rappresentazione CRT degli operandi.

 

*      Permutazioni e altre corrispondenze di ordinamento interno alle liste:

 

Possiamo offuscare gli interi in permutazioni. I parametri di offuscamento possono essere usati per controllare la grandezza dell’insieme di elementi della permutazione/corrispondenza che corrispondono all’incremento. Definiamo 0 come l’operatore incremento, 1 definito come l’operatore incremento applicato due volte, e così via.

Per esempio, presa la permutazione (1 2) (7 6 5 4 3 8),  0 verrà offuscato come (1 2) (7 6 5 4 3 8), 1 verrà offuscato come (1) (2) ( 7 5 3) (8 6 4), 2 verrà offuscato come (1 2) (7 4) (8 5) (6 3) etc. Questa tecnica è sufficiente per implementare l’addizione (attraverso la combinazione di permutazioni) e di conseguenza l’incremento. Gli operatori “<” e “>” possono essere implementati con permutazioni, senza deoffuscamento per un numero ragionevole di valori utilizzando le tabelle. L’indicizzazione della tabella può essere organizzata facendo l’hash della rappresentazione delle permutazioni usate come operandi. L’operazione di offuscamento è equivalente all’operazione di deoffuscamento.

 

*      Altre manipolazioni della teoria dei numeri:

 

Un’altra tecnica di offuscamento mappa la variabile intera Y nel seguente valore:

 

(Y = y * p)

 

dove p è un intero primo più grande del valore assoluto di tutti gli interi non offuscati che necessitano di essere rappresentati usando p. il deoffuscamento è realizzato rimuovendo il fattore p dal valore offuscato X. Presa la parte intera I del logpX. Dividere X per pI per ottenere il valore deoffuscato. Sommando e moltiplicando i due interi offuscati W e Y abbiamo:

 

W + Y = ( p * (w + y) ) e W * Y = ( p2 * w * y )

 

In nessuno dei casi dobbiamo esporre il parametro di offuscamento p. i due valori offuscati avranno p come parte del loro MCD e la fattorizzazione di un singolo valore offuscato espone p. Questo metodo quindi è piuttosto debole. Un’altra tecnica di offuscamento mappa la variabile intera y nel seguente valore (n è la composizione di due primi quasi uguali controllati dal parametro di offuscamento a che è scelto a caso):

 

(Y = a * n + y), (W = b * n + w)

 

e le operazioni aritmetiche sono:

 

W + Y = ((a+b) * n + (w + y))

e

W * Y = (n(n + b*w + a*y + b*a) + w * y)

 

Il valore n è più grande del valore assoluto di tutti gli interi deoffuscati che necessitano di essere rappresentati usando n. l’operazione u = U mod n deoffusca U. Ne la moltiplicazione ne l’addizione espongono il parametro.

 

 

5.      Impacchettamento (di interi e/o booleani):

 

Questo metodo tenta di proteggere le variabili, da un’analisi dinamica dei riferimenti dei valori. Invece di  usare parole separate per memorizzare variabili diverse, più variabili sono impacchettate nella stessa parola.

 

*      Impacchettamento di bit:

 

Ogni insieme di bit è usato per memorizzare una variabile separata come una variabile booleana o un piccolo intero. I parametri per le variabili controllano quale bit della parola rappresenta la variabile. Addizioni, moltiplicazioni, etc. sono definite per l’appropriato sottoinsieme di interi. I campi dei bit che non fanno parte di una variabile sono definiti e manipolati come interi che non sono stati definiti, ma il modo migliore per trattarli è considerarli come piccoli interi.

 

*      Impacchettamento basato sui numeri primi:

 

Il valore di una parola (trattato come un intero senza segno) mod un numero primo o un numero composto, è il valore di una variabile separata. Per esempio, se una parola contiene 25 può essere usata per rappresentare due variabili, una contiene 7 (mod 9) e l’altra contiene 12 (mod 13). In questo esempio 9 e 13 sono i parametri. In pratica questa tecnica è il CRT su campi piccoli.

 

 

6.      Offuscamento di booleani:

 

Un metodo per l’offuscamento di variabili booleane è di usare variabili intere per rappresentarle e fare dei possibili assegnamenti di valori interi a TRUE e FALSE, per esempio si può dividere il valore della variabile per qualche valore intero per determinare il corrispondente booleano. L’esempio della sezione 6.1.1 può essere rimpiazzato da:

 

if ( ( gcd(foo * bar ) % obfuscation_param1) ) == 0) {

total = total + 1;

    }

o se è richiesto l’OR allora possiamo usare:

 

if ( ( (foo * bar ) % obfuscation_param1) == 0) {

total = total + 1;

    }

 

Questo metodo non nasconde l’esecuzione di AND e OR (o i loro rispettivi complementi). L’esempio seguente nasconde una variabile booleana ma con un grosso sforzo computazionale, c’è bisogno di più parametri di offuscamenti.

 

if ( (( foo + obfuscation_param1) *

( bar + obfuscation_param2)) % obfuscation_param3)) == 0) {

total = total + 1;

    }

 

In questo modo la variabile foo può essere settata a TRUE eseguendo:

 

foo = X;

# X == 15

 

seguito da:

 

foo = foo + Y[I];

# Y[I] == 13

 

e settando obfuscation_param3 = 14.

Se l’operazione di resto e un segno evidente che un’operazione booleana è stata appena effettuata, possiamo usare altri operatori come il meno.

 

 


 

4.4 Analisi

 


 

 

4.4.1 Analisi delle tecniche per generare parametri di offuscamento

 

Il metodo di generazione di parametri di offuscamento basato sul control-flow è l’unico che tiene conto del fatto che l’uso della tecnica non riduce l’efficacia dell’offuscamento.

 

Il metodo basato sull’environment-testing invece, presume che il programma offuscante abbia informazioni sull'ambiente di esecuzione o sull'esecuzione del programma offuscato. Entrambe le tecniche possono essere utilizzate per individuare alterazioni del programma e la loro combinazione è più efficace rispetto all’uso di una sola delle due. Quindi la scelta migliore è usarle entrambe.

 

 


 

 

4.4.2 Analisi delle tecniche per la rappresentazione dei dati

 

I meriti delle trasformazioni di offuscamento dei dati sono meno chiari rispetto ai meriti delle tecniche di generazione di parametri di offuscamento.

 

Operazioni di incremento e uguaglianza si possono realizzare con differenti tecniche, comprendendo tecniche che  moltiplicano ripetutamente una matrice di partenza S con un’altra matrice M, risultando nella sequenza di matrici S * M,   S * M * M,   S * M * M * M, etc. Il limite del ciclo è derivato da una matrice della sequenza.

 

L’uso di algoritmi crittografici (a)simmetrici per proteggere gli alberi di Parser offre una buona soluzione in situazioni dove questi alberi hanno senso. Bisogna fare attenzione che l’algoritmo di cifratura sia difficilmente distinguibile, altrimenti esso diviene l’oggetto dell’analisi. Un malintenzionato può vedere l’espansione dell’albero di Parser senza essere in grado di deoffuscare le parti più in alto nell’albero. Gli alberi di Parser tipicamente riducono, di una modesta quantità, l’esposizione delle trasformazioni di deoffuscamento o le variabili non offuscate, perchè  bisogna valutarlo prima di effettuare alcune operazioni su di esso. Questa trasformazione (Y = a * n + y), è utile da usare nell’addizione e nella moltiplicazione, un malintenzionato non può facilmente deoffuscare le variabili effettuando analisi dinamiche sull’espressione che utilizza addizione e moltiplicazione offuscate. Inoltre non sono supportate le operazioni comparative (<, <=, ==, >=, >).

Se il numero di interi che devono essere supportati non è troppo grande e non ci si preoccupa delle performance, allora RSA bidirezionale è una buona scelta. Le moltiplicazioni offuscate sono realizzate direttamente, mentre le addizioni offuscate e le comparazioni offuscate possono essere realizzate utilizzando le tabelle. La scoperta di uno dei parametri di offuscamento o di deoffuscamento non compromette l’altro. L’analisi dinamica delle moltiplicazioni può rendere noto il parametro di offuscamento o quello di deoffuscamento.

 

Ci sono diverse conseguenze usando le tabelle per le addizioni. Le tabelle potrebbero non supportare 0 o 1, che devono essere manipolati (se necessario) da una combinazione di riscrittura di espressioni. Per esempio per implementare il limite semplice per il contatore dell’esempio nella sezione 4.1.1 con questa tecnica il frammento di codice dovrebbe essere riscritto come:

 

for (int i = obfus_param;

i != secret_int*onfus_param - obfus_param;

i=i + obfus_param) {

bar(foo(a),b);

}

 

 Obfus_param è una costante di offuscamento che è determinata ad obfuscation-time. L’offuscamento di variabili booleane (convertendole in interi offuscati) è un buon metodo per proteggere i valori booleani.