stratificat confuzie: o taxonomie de software tehnici de confuzie pentru securitate stratificat

această secțiune sondaje tehnicile de confuzie pentru elemente de cod specifice. Acest strat acoperă majoritatea publicațiilor din zona de confuzie a software-ului. Așa cum se arată în Fig. 4, în funcție de ce elemente vizează o tehnică de ascundere, împărțim această categorie în cinci subcategorii: machete de ascundere, controale de ascundere, funcții de ascundere a datelor și clase de ascundere.

Fig. 4
figura4

tehnicile de confuzie ale stratului de cod-element

disimularea aspectului

disimularea aspectului amestecă aspectul codurilor sau instrucțiunilor păstrând în același timp sintaxa originală intactă. Această secțiune discută patru strategii de confuzie a aspectului: clasificatori fără sens, eliminarea simbolurilor redundante, separarea codurilor conexe și codurile nedorite.

identificatori fără sens

această abordare este, de asemenea, cunoscută sub numele de confuzie lexicală care transformă identificatorii semnificativi în cei fără sens. Pentru majoritatea limbajelor de programare, adoptarea unor reguli de denumire semnificative și uniforme (de exemplu, notația maghiară (Simonyi 1999)) este necesară ca o bună practică de programare. Deși astfel de nume sunt specificate în codurile sursă, unele ar rămâne în software-ul lansat în mod implicit. De exemplu, numele variabilelor și funcțiilor globale din C/C++ sunt păstrate în binare și toate numele Java sunt rezervate în bytecodes. Deoarece astfel de nume semnificative pot facilita analiza programului contradictoriu, ar trebui să le amestecăm. Pentru a face identificatorii ascunși mai confuzi, Chan și Yang (2004) au propus să folosească în mod deliberat aceleași nume pentru obiecte de diferite tipuri sau în domenii diferite. Astfel de abordări au fost adoptate de ProGuard (2016) ca o schemă implicită de confuzie pentru programele Java.

eliminarea simbolurilor redundante

această strategie elimină informațiile simbolice redundante din software-ul lansat, cum ar fi informațiile de depanare pentru majoritatea propgramelor (Low 1998). În plus, există și alte simboluri redundante pentru anumite formate de programe. De exemplu, fișierele ELF conțin tabele de simboluri care înregistrează perechile de identificatori și adrese. La adoptarea opțiunilor de compilare implicite pentru a compila programele C / C++, cum ar fi utilizarea LLVM (Lattner and Adve 2004), binarele generate conțin astfel de tabele de simboluri. Pentru a elimina astfel de informații redundante, dezvoltatorii pot folosi instrumentul strip al Linux. Un alt exemplu cu informații redundante este codurile Android smali. În mod implicit, codurile smali generate conțin informații începute cu .linie și .sursă, care poate fi eliminată în scopuri de confuzie (Dalla Preda și Maggi 2017).

separarea codurilor conexe

un program este mai ușor de citit dacă codurile sale legate logic sunt, de asemenea, apropiate fizic (Collberg și colab. 1997). Prin urmare, separarea codurilor sau instrucțiunilor aferente poate crește dificultățile de citire. Se aplică atât codurilor sursă (de exemplu, variabile de reordonare (Low 1998)), cât și codurilor de asamblare (de exemplu, instrucțiuni de reordonare (Wroblewski 2002)). În practică, angajarea de salturi necondiționate pentru a rescrie un program este o abordare populară pentru a realiza acest lucru. De exemplu, dezvoltatorii pot amesteca codurile de asamblare și apoi folosesc goto pentru a reconstrui fluxul de control original (Tu și Yim 2010). Această abordare este populară pentru codurile de asamblare și codurile bytecodes Java cu disponibilitatea instrucțiunilor goto (Dalla Preda și Maggi 2017).

coduri nedorite

această strategie adaugă instrucțiuni nedorite care nu sunt funcționale. Pentru binare, putem adăuga instrucțiuni de funcționare (NOP sau 0x00) (Dalla Preda și Maggi 2017; Marcelli și colab. 2018). În plus, putem adăuga și metode nedorite, cum ar fi adăugarea de metode defuncte în codurile Android smali (Dalla Preda și Maggi 2017). Codurile junk pot schimba de obicei semnăturile codurilor și, prin urmare, pot scăpa de recunoașterea statică a modelelor.

deoarece disimularea aspectului nu modifică sintaxa codului original, este mai puțin predispusă la probleme de compatibilitate sau erori. Prin urmare, astfel de tehnici sunt cele mai preferate în practică. Mai mult, tehnicile identificatorilor fără sens și eliminarea simbolurilor redundante pot reduce dimensiunea programelor, ceea ce le face în continuare atractive (ProGuard 2016). Cu toate acestea, potența aspectului este limitată. Are o rezistență promițătoare la atacurile de deobfuscare, deoarece unele transformări sunt unidirecționale, care nu pot fi inversate. Cu toate acestea, unele informații despre aspect nu pot fi modificate cu greu, cum ar fi identificatorii metodei din Java SDK. Astfel de informații reziduale sunt esențiale pentru ca adversarii să recupereze informațiile ascunse. De exemplu, Bichsel și colab. (2016) a încercat să deobfuscate aplicații ProGuard-obfuscated, și au recuperat cu succes în jurul valorii de 80% nume.

controale de ascundere

acest tip de tehnici de ascundere transformă controalele codurilor pentru a crește complexitatea programului. Poate fi realizat prin fluxuri de control false, fluxuri de control probabilistice, controale bazate pe dispecer și controale implicite.

fluxuri de control false

fluxuri de control false se referă la fluxurile de control care sunt adăugate în mod deliberat la un program, dar nu vor fi executate niciodată. Poate crește complexitatea unui program, de ex., în complexitatea McCabe (McCabe 1976) sau Harrison metrics (Harrison și Magel 1981). De exemplu, complexitatea McCabe (McCabe 1976) se calculează ca numărul de margini pe un grafic de control-flux minus numărul de noduri și apoi plus de două ori ale componentelor conectate. Pentru a crește complexitatea McCabe, putem introduce noi margini sau adăuga atât noi margini și noduri la o componentă conectată.

pentru a garanta inaccesibilitatea fluxurilor de control false, Collberg și colab. (1997) a sugerat utilizarea predicatelor opace. Ei au definit opac prezice ca predicat al cărui rezultat este cunoscut în timpul timpului de confuzie, dar este dificil de dedus prin analiza statică a programului. În general, un predicat opac poate fi constant adevărat (PT), constant fals (PF) sau dependent de context (P?). Există trei metode pentru a crea predicate opace: scheme numerice, scheme de programare și scheme contextuale.

scheme numerice

scheme numerice compun predicate opace cu expresii matematice. De exemplu, 7×2−1 y2 Y2 este valabil în mod constant pentru toate numerele întregi x și y. Putem folosi direct astfel de predicate opace pentru a introduce fluxuri de control false. Figura 5a demonstrează un exemplu, în care predicatul opac garantează că fluxul de control fals (adică ramura else) nu va fi executat. Cu toate acestea, atacatorii ar avea șanse mai mari să le detecteze dacă folosim frecvent aceleași predicate opace într-un program obfuscat. Arboit (2002), prin urmare, a propus să genereze automat o familie de astfel de predicate opace, astfel încât un obfuscator să poată alege de fiecare dată un predicat opac unic.

Fig. 5
figura5

obfuscarea fluxului de Control cu predicate opace

o altă abordare matematică cu o securitate mai mare este utilizarea funcțiilor criptografice, cum ar fi funcția hash \(\mathcal {H}\) (Sharif și colab. 2008) și criptare homomorfă (Zhu și Thomborson 2005). De exemplu, putem înlocui un predicat x==c cu \(\mathcal {H}(x)==C_{hash}\) pentru a ascunde soluția lui x pentru această ecuație. Rețineți că o astfel de abordare este folosită în general de malware pentru a se sustrage analizei dinamice a programului. De asemenea, putem folosi funcții criptografice pentru a cripta ecuații care nu pot fi satisfăcute. Cu toate acestea, astfel de predicate opace suportă mult deasupra capului.

pentru a compune constante opace rezistente la analiza statică, Moser și colab. (2007) a sugerat angajarea problemelor 3-SAT, care sunt NP-hard. Acest lucru este posibil deoarece se pot avea algoritmi eficienți pentru a compune astfel de probleme grele (Selman și colab. 1996). De exemplu, Tiella și Ceccato (2017) au demonstrat cum să compună astfel de predicate opace cu probleme de K-clică.

pentru a compune constante opace rezistente la analiza dinamică, Wang și colab. (2011) a propus să compună predicate opace cu o formă de presupuneri nerezolvate care se buclează de mai multe ori. Deoarece buclele sunt provocatoare pentru analiza dinamică, abordarea în natură ar trebui să fie rezistentă la analiza dinamică. Exemple de astfel de presupuneri includ conjectura Collatz, conjectura 5x+1, conjectura Matthews. Figura 5b demonstrează modul de utilizare a conjecturii Collatz pentru a introduce fluxuri de control false. Indiferent de modul în care inițializăm x, programul se termină cu x=1, iar originalCodes() poate fi întotdeauna executat.

scheme de programare

deoarece analiza adversară a programelor este o amenințare majoră pentru predicatele opace, putem folosi probleme provocatoare de analiză a programelor pentru a compune predicatele opace. Collberg și colab. sugerat două probleme clasice, analiza pointer și programe concurente.

în general, analiza pointerului se referă la determinarea dacă doi pointeri pot sau pot indica aceeași adresă. Unele probleme de analiză pointer pot fi NP-hard pentru analiza statică sau chiar indecidabile (Landi și Ryder 1991). Un alt avantaj este că operațiunile pointer sunt foarte eficiente în timpul execuției. Prin urmare, dezvoltatorii pot compune predicții opace rezistente și eficiente cu probleme de analiză a indicatoarelor bine concepute, cum ar fi menținerea indicatoarelor către unele obiecte cu structuri de date dinamice (Collberg și colab. 1998a).

programe concurente sau programe paralele este o altă problemă dificilă. În general, o regiune paralelă de n declarații are n! diferite moduri de execuție. Execuția nu este determinată doar de program, ci și de starea de rulare a unui computer gazdă. Collberg și colab. (1998a) a propus să utilizeze programe concurente pentru a îmbunătăți abordarea bazată pe pointer prin actualizarea simultană a indicatoarelor. Majumdar și Thomborson (2006) au propus să utilizeze programe paralele distribuite pentru a compune predicate opace.

în plus, unele abordări compun predicate opace cu trucuri de programare, cum ar fi utilizarea mecanismelor de manipulare a excepțiilor. De exemplu, Dolz și Parra (2008) au propus utilizarea mecanismului try-catch pentru a compune predicate opace pentru.NET și Java. Evenimentele de excepție includ împărțirea la zero, indicatorul nul, indexul în afara intervalului sau chiar anumite excepții hardware (Chen și colab. 2009). Semantica originală a programului poate fi realizată prin scheme adaptate de manipulare a excepțiilor. Cu toate acestea, astfel de predicate opace nu au o bază de securitate și sunt vulnerabile la atacuri avansate realizate manual.

scheme contextuale

scheme contextuale pot fi folosite pentru a compune predicate opace variante(adică, {P?}). Predicatele ar trebui să dețină unele proprietăți deterministe, astfel încât să poată fi folosite pentru a ascunde programele. De exemplu, Drape și et al. (2009) a propus să compună astfel de predicate opace care sunt invariante sub o constrângere contextuală, de exemplu, predicatul opac x mod3==1 este constant adevărat dacă X mod3:1?x++: x = x + 3. Palsberg și colab. (2000) predicate opace dinamice propuse, care includ o secvență de predicate corelate. Rezultatul evaluării fiecărui predicat poate varia în fiecare rulare. Cu toate acestea, atâta timp cât predicatele sunt corelate, comportamentul programului este determinist. Figura 5c demonstrează un exemplu de predicate opace dinamice. Indiferent de modul în care inițializăm *p și *q, programul este echivalent cu y=x+3,x=y+3.

rezistența fluxurilor de control fals depinde în mare parte de securitatea predicatelor opace. O proprietate de securitate ideală pentru predicatele opace este că acestea necesită timp exponențial în cel mai rău caz pentru a se rupe, dar numai timp polinomial pentru a construi. Rețineți că unele predicate opace sunt proiectate cu astfel de probleme de securitate, dar pot fi implementate cu defecte. De exemplu, problemele 3-SAT propuse de Ogiso și colab. (2003) se bazează pe Setări de probleme banale care pot fi ușor simplificate. Dacă astfel de predicate opace sunt puse în aplicare în mod corespunzător, acestea ar promite să fie rezistente.

fluxurile de control Probabilistic

fluxurile de control fals poate face probleme la analiza programului static. Cu toate acestea, ele sunt vulnerabile la analiza dinamică a programelor, deoarece fluxurile de control false sunt inactive. Ideea fluxurilor de control probabilistic adoptă o strategie diferită pentru a aborda amenințarea (Pawlowski și colab. 2016). Acesta introduce replicări ale fluxurilor de control cu aceeași semantică, dar sintaxă diferită. Când primiți aceeași intrare de mai multe ori, programul se poate comporta diferit pentru diferite perioade de execuție. Tehnica este utilă și pentru combaterea atacurilor cu canale laterale (Crane și colab. 2015).

rețineți că strategia fluxurilor de control probabilistic este similară fluxurilor de control false cu predicate opace contextuale. Dar ele sunt diferite în natură, deoarece predicatele opace contextuale introduc căi moarte, deși nu introduc coduri nedorite.

controale bazate pe Dispecer

un control bazat pe dispecer determină următoarele blocuri de coduri care urmează să fie executate în timpul rulării. Astfel de controale sunt esențiale pentru obfuscarea fluxului de control, deoarece pot ascunde fluxurile de control originale împotriva analizei statice a programului.

o abordare majoră bazată pe dispecerat este aplatizarea fluxului de control, care transformă codurile de adâncime în cele superficiale cu mai multă complexitate. Wang și colab. (2000) a propus în primul rând abordarea. Figura 6 demonstrează un exemplu din hârtia lor care transformă o buclă în timp ce într-o altă formă cu comutator-caz. Pentru a realiza o astfel de transformare, primul pas este transformarea codului într-o reprezentare echivalentă cu declarații if-then-goto așa cum se arată în Fig. 6; Apoi modifică declarațiile goto cu declarații de caz comutator așa cum se arată în Fig. 6. În acest fel, semantica inițială a programului este realizată implicit prin controlul fluxului de date al variabilei switch. Deoarece ordinea de execuție a blocurilor de cod este determinată dinamic de variabilă, nu se poate cunoaște fluxurile de control fără a executa programul. Cappaert și Preneel (2010) au formalizat aplatizarea fluxului de control ca folosind un nod dispecer (de exemplu, comutator) care controlează următorul bloc de cod care trebuie executat; după executarea unui bloc, controlul este transferat înapoi la nodul dispecer. În plus, există mai multe îmbunătățiri la aplatizarea fluxului de cod. De exemplu, pentru a spori rezistența la analiza statică a programului pe variabila de comutare, Wang și colab. (2001) a propus introducerea problemelor de analiză a indicilor. Pentru a complica și mai mult programul, Chow și colab. (2001) a propus adăugarea de blocuri de cod false.

Fig. 6
figura6

abordarea de aplatizare a fluxului de Control propusă de Wang și colab. (2000)

l Xktszl și Kiss (2009) au propus un mecanism de aplatizare a fluxului de control pentru a gestiona sintaxa specifică C++, cum ar fi try-catch, while-do, continue. Mecanismul se bazează pe arborele de sintaxă abstractă și folosește un model fix de aspect. Pentru fiecare bloc de cod pentru a ascunde, se construiește o declarație în timp ce în bucla exterioară și un compus comutator-caz în interiorul buclei. Compusul comutatorului implementează semantica originală a programului, iar variabila comutatorului este, de asemenea, utilizată pentru a termina bucla exterioară. Cappaert și Preneel (2010) au constatat că mecanismele ar putea fi vulnerabile la analiza locală, adică variabila de comutare este atribuită imediat astfel încât adversarii să poată deduce următorul bloc de executat doar uitându-se la un bloc curent. Ei au propus o abordare consolidată cu mai multe trucuri, cum ar fi utilizarea atribuirii de referință (de exemplu, swVar=swVar+1) în loc de atribuire directă (de exemplu, swVar=3), înlocuind atribuirea prin if-else cu o expresie uniformă de atribuire și folosind funcții unidirecționale în calcularea succesorului unui bloc de bază.

pe lângă aplatizarea fluxului de control, există mai multe alte investigații de confuzie bazate pe dispecer (de exemplu, (Linn și Debray 2003; Ge și colab. 2005; Zhang și colab. 2010; Schrittwieser și Katzenbeisser 2011)). Linn și Debray (2003) au propus să ascundă binare cu funcții de ramură care ghidează execuția pe baza informațiilor despre stivă. În mod similar, Zhang și colab. (2010) a propus să utilizeze funcții de ramură pentru a ascunde programele orientate pe obiecte, care definesc un stil de invocare a metodei unificate cu un grup de obiecte. Pentru a spori securitatea unor astfel de mecanisme, Ge și colab. (2005) a propus ascunderea informațiilor de control într-un alt proces independent și utilizarea comunicațiilor între procese. Schrittwieser și Katzenbeisser (2011) au propus să utilizeze blocuri de cod diversificate care să implementeze aceeași semantică.

disimularea bazată pe dispecer este rezistentă la analiza statică, deoarece ascunde graficul fluxului de control al unui program software. Cu toate acestea, este vulnerabil la analiza dinamică a programelor sau la abordările hibride. De exemplu, Udupa și colab. (2005) a propus o abordare hibridă pentru a dezvălui fluxurile de control ascunse atât cu analiză statică, cât și cu analiză dinamică.

controale implicite

această strategie convertește instrucțiunile explicite de control la cele implicite. Poate împiedica inginerii inversi să abordeze fluxurile de control corecte. De exemplu, putem înlocui instrucțiunile de control ale codurilor de asamblare (de exemplu, jmp și jne) cu o combinație de mov și alte instrucțiuni care implementează aceeași semantică de control (Balachandran și Emmanuel 2011).

rețineți că toate abordările existente de confuzie a fluxului de control se concentrează pe transformarea la nivel sintactic, în timp ce protecția la nivel semantic a fost rareori discutată. Deși pot demonstra o anumită rezistență la atacuri, eficacitatea lor de confuzie în ceea ce privește protecția semantică rămâne neclară.

disimularea datelor

tehnicile actuale de disimulare a datelor se concentrează pe tipuri comune de date, cum ar fi numere întregi, șiruri și matrice. Putem transforma datele prin divizare, fuziune, procedurizare, codificare etc.

divizarea/fuzionarea datelor

divizarea datelor distribuie informațiile unei variabile în mai multe variabile noi. De exemplu, o variabilă booleană poate fi împărțită în două variabile booleene, iar efectuarea operațiilor logice pe ele poate obține valoarea inițială.

datele care fuzionează, pe de altă parte, agregă mai multe variabile într-o singură variabilă. Collberg și colab. (1998b) a demonstrat un exemplu care îmbină două numere întregi pe 32 de biți într-un întreg pe 64 de biți. Ertaul și Venkatesh (2005) au propus o altă metodă care împachetează mai multe variabile într-un singur spațiu cu logaritmi discreți.

procedurizarea datelor

procedurizarea datelor înlocuiește datele statice cu apelurile de procedură. Collberg și colab. (1998b) a propus înlocuirea șirurilor cu o funcție care poate produce toate șirurile prin specificarea valorilor parametrilor paticulari. Drape și et al. (2004) a propus Codificarea datelor numerice cu două funcții inverse f și g. Pentru a atribui o valoare v unei variabile i, o atribuim unei variabile injectate j ca j=f (v). Pentru a folosi i, invocăm g (j) în schimb.

codificare date

codificare date codifică date cu funcții matematice sau cifre. Ertaul și Venkatesh (2005) au propus codificarea șirurilor cu cifruri afine (de exemplu, cifru Caser) și folosesc logaritmi discrete pentru a împacheta cuvinte. Fukushima și colab. (2008) a propus codificarea numerelor clare cu operațiuni exclusive sau apoi decriptarea rezultatului de calcul înainte de ieșire. Kovacheva (2013) a propus criptarea șirurilor cu cifrul RC4 și apoi decriptarea acestora în timpul rulării.

transformarea matricei

Matricea este una dintre cele mai frecvent utilizate structuri de date. Pentru a ascunde tablourile, Collberg și colab. (1998b) a discutat mai multe transformări, cum ar fi împărțirea unei matrice în mai multe subarrays, fuzionarea mai multor matrice într-o singură matrice, plierea unei matrice pentru a-i crește dimensiunea sau aplatizarea unei matrice pentru a reduce dimensiunea. Ertaul și Venkatesh (2005) au sugerat transformarea indicilor matricei cu funcții compozite. Zhu și colab. (2006); Zhu (2007) a propus utilizarea criptării homomorfe pentru transformarea matricei, inclusiv schimbarea indicelui, plierea și măgulirea. De exemplu, putem amesteca elementele unei matrice cu I m mod N, unde i este indexul original, n este dimensiunea matricei originale, iar m și n sunt relativ prime.

metode de ascundere

metodă inline/outline

o metodă este o procedură independentă care poate fi apelată prin alte instrucțiuni ale programului. Metoda inline înlocuiește apelul procedural original cu corpul funcției în sine. Metoda schiță funcționează în mod opus, care extrage o secvență de instrucțiuni și rezumate o metodă. Sunt companii bune care pot ascunde abstractizarea inițială a procedurilor (Collberg și colab. 1997).

clona metodei

dacă o metodă este puternic invocată, putem crea replicări ale metodei și putem apela aleatoriu una dintre ele. Pentru a confunda interpretarea contradictorie, fiecare versiune a replicării ar trebui să fie unică într-un fel, cum ar fi prin adoptarea diferitelor transformări de confuzie (Collberg și colab. 1997) sau semnături diferite (Ertaul și Venkatesh 2004).

metoda de agregare/împrăștiere

ideea este similară cu disimularea datelor. Putem agrega metode irelevante într-o singură metodă sau împrăștierea unei metode în mai multe metode (Collberg și colab. 1997; scăzut 1998).

metoda proxy

această abordare creează metode proxy pentru a confunda inginerie inversă. De exemplu, putem crea proxy-urile ca metode statice publice cu identificatori randomizați. Pot exista mai multe proxy-uri distincte pentru aceeași metodă (Dalla Preda și Maggi 2017). Abordarea este extrem de utilă atunci când metoda semnăturile nu pot fi modificate (Protsenko și Muller 2013).

clase de ascundere

clase de ascundere împărtășește câteva idei similare cu metode de ascundere, cum ar fi divizarea și clonarea (Collberg și colab. 1998b). Cu toate acestea, deoarece clasa există doar în limbaje de programare orientate pe obiecte, cum ar fi JAVA și.NET, le discutăm ca o categorie unică. Mai jos vă prezentăm strategiile majore pentru ascunderea claselor.

modificatori de cădere

programele orientate pe obiecte conțin modificatori (de ex., public, privat) pentru a restricționa accesul la clase și membrii claselor. Dropping modificatori elimină astfel de restricții și să facă toți membrii publice (Protsenko și Muller 2013). Această abordare poate facilita implementarea altor metode de confuzie de clasă.

clasa de divizare/coalescență

ideea coalescenței/divizării este de a ascunde intenția dezvoltatorilor atunci când proiectează clasele (Sosonkin și colab. 2003). La coalescența claselor, putem transfera variabile locale sau grupuri de instrucțiuni locale într-o altă clasă (Fukushima și colab. 2003).

clasa ierarhie aplatizare

interfață este un instrument puternic pentru programe orientate pe obiecte. Similar cu metoda proxy, putem crea proxy-uri pentru clase cu interfețe (Sosonkin și colab. 2003). Cu toate acestea, o modalitate mai puternică este de a rupe relația de moștenire originală între clasele cu interfețe. Lăsând fiecare nod al unui subarbor din ierarhia clasei să implementeze aceeași interfață, putem aplatiza ierarhia (Foket și colab. 2012).

Lasă un răspuns

Adresa ta de email nu va fi publicată.