4.
Offuscamento parametrizzato di variabili
Verranno ora trattate le trasformazioni di offuscamento riguardanti variabili primitive, in particolare per tipi di dati interi.
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.
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
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.
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
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.
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
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.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.