Deobfuscation of obfuscated software Kurucsai István
Tartalom Bevezetés Obfuszkáció típusai Mit és hol? Deobfuszkáció Szintaxis alapú módszerek Szemantikus módszerek Összefoglalás
Bevezetés Anti-reversing: programok visszafejtésének megnehezítése Az előadás keretében: natív Windows alkalmazások Motivációk: Illegális szoftvermásolás akadályozása Kritikus/értékes algoritmusok védelme Kártékony kódok elrejtése Csak nehezítés, a visszafejtés időigényét és költségét növelhetjük Tökéletes védelem nem megvalósítható
Anti-reversing ~ ára Kódméret növekedése Teljesítmény csökkenése Kompatibilitási kérdések SecuROM Hatékonyság nehezen mérhető: mennyivel nehezebb visszafejteni a kódot? Embernek Automatikus eszközöknek
Anti-reversing módszerek Szimbólumok/debug információk eltávolítása Natív alkalmazások esetén szinte mindig megtörténik Bytecode alapú nyelveknél fontos Packerek/wrapperek (csomagolás) Virtualizáció Anti-debugging/disassembly/VM/dumping trükkök Obfuszkáció Gyakran együtt jelennek meg
Refused és Stubborn Önálló laboratórium keretében készített programok Refused: source-to-source transformer Különböző obfuszkációs módszerek Stubborn: Windows PE packer
Kártékony kódok elrejtése (packerek)
Obfuszkáció A funkcionalitás megőrzése mellett a kód összezavarása, az olvashatóság csökkentése Layout obfuscations: szimbólumok és kommentek eltávolítása, formázás megváltoztatása Natív bináris készítésekor a fordító elvégzi Preventív transzformációk: a deobfuszkálásnál felhasznált eszközök esetleges gyengeségeinek kihasználása
Adat obfuszkáció A program által használt adatok struktúrája és a konstansok nagyban segítik a visszafejtést
Adat obfuszkáció Számos módszer Tárolás és kódolás transzformációja Tömbök átszervezése (tömbök összevonása és szétválasztása, dimenziók számának növelése/csökkentése, elemek permutációja) A hatékonyság és általánosság hiánya illetve költségeik gyakran kérdésessé teszi az alkalmazhatóságukat
Konstansok védelme A konstansok védelme azonban egyszerű és hatásos lehet Hashelés Amennyiben a konstanst csak egyenlőség jellegű vizsgálatoknál használja a program és nincs szükség a konkrét értékére (==,!=,!strcmp, stb...) if (a == 24) helyett if (hash(a) == 356725135) A megfelelő hash kiválasztása Ütközésmentesség: a program helyes műküdése Egyirányúság: visszafejtés nehézsége
Konstansok védelme II Ha a konstans eredeti értékére is szükség van Titkosítás majd futásidőben a használat előtt ideiglenes dekódolás Stringek egyszerű (XOR, SUB) rejtjelezése Bonyolultabb algoritmusok, például blokkrejtjelezők alkalmazása nem jelent különösebb nehézséget, mert futásidőben úgyis megjelenik a konstans eredeti értéke és a kulcs is
Adat obfuszkáció Összességében A konstansok védelme alapfunkciónak számít Az általánosabb adat obfuszkációs megoldások viszont kevésbé elterjedtek Nehéz az általános megvalósítás Nagy overhead Gyenge védelem
Control-flow obfuscation A program vezérlési struktúrájának összezavarása Sokat kutatott terület A legtöbb obfuszkátor alapja
Opaque predicates (átlátszatlan feltételek) Olyan elágazás-feltételek, melyek mindig igazra vagy hamisra értékelődnek ki, de ezt nehéz belátni a visszafejtett programot vizsgálva Egyszerű példa: (x + x^2) % 2 == 0 A hatásosság nagyban függ a predikátumok változatosságától és bonyolultságától Ilyen elágazásokat elhelyezve a programban annak vezérlésfolyama bonyolítható
Példa feltételek
Opaque predicates példa I (Refused)
Opaque predicates példa II
Opaque predicates példa III
Opaque predicates Platformfüggő feltételek Kihasználva egyes operációs rendszerek vagy API-k sajátosságait Regiszter értéke a program indulásakor vagy valamelyik API visszatérése után Erős feltételek hozhatóak létre Kompatibilitás hiánya Összességében: Hatásos és egyszerű Jól kombinálható más módszerekkel
CFG flattening (vezérlési gráf kilapítása) Célja a vezérlésfolyam elrejtése Függvény basic blockokra osztása Eredetileg más mélységben lévő blokkok egy szintre hozása switch kifejezésben (kiválasztás) A blokkok végén a switch-et vezérlő változó megfelelő beállítása A kiválasztás ciklusba ágyazása
CFG flattening példa
CFG flattening példa II
Diablo CFG flattening
Apple FairPlay CFG flattening
Junk code insertion (szemét kód beszúrása) Do-nothing vagy do-little Az ember számára olvashatatlan és értelmezhetetlen kód automatizált eszközökkel gyakran hatékonyan kezelhető
Junk code példa
További vezérlésfolyam obfuszkációk CFG arches meshing Inlining/outlining Cloning of functions Combined functions
Mit és hol? A cél a kimeneti bináris obfuszkációja De az egyes módszerek alkalmazhatóak a fordítás különböző lépéseiben (forráskód, köztes reprezentáció, tárgykód) kész binárison is
Mit és hol? II Célszerű a legmegfelelőbb helyet kiválasztani minden transzformációhoz Packelés csak a kész binárison CFG kilapítása bármelyik lépésben Szemét kód Ha a forrásba illesztjük, a fordító kioptimalizálhatja Ha a kész binárisba, ügyelni kell a mellékhatásokra (regiszterek, különösen EFLAGS)
Opaque predicates példa I
Refused Source-to-source transformation C/C++ kódok támogatása Funkciók: Opaque predicates Szemét kód (félkész) CFG flattening (félkész)
Clang/LLVM C nehezen parse-olható, a C++ még nehezebben Az LLVM frontendjének felhasználása (Clang) Transzformációk az abstract syntax tree-n (AST)
Morpher Morpher is a versatile and flexible automatic software transformation toolkit focused on protection of software algorithms. Despite of other software protection solutions on the market which operate only on the final binary Morpher is tightly coupled with the industry-standard C/C+ + compiler and thus can exploit much more information about sources to be protected. Note that such information is normally lost during compilation and codegeneration. Functions interleaving and merging; Opaque predicates insertion; Opaque variables insertion (generic framework used by other transformation passes); Automatic constants (e.g. string literals) protection and encryption; Code flow graph altering; LLVM optimalizációs lépésekként implementált obfuscating compiler
Deobfuszkáció szintaktikus vs. szemantikus módszerek Szintaxis-alapú: csak az objektumok formátumát figyelembe vevő módszerek Gépi kód / assembly utasítások bájtjainak sorozata, esetleges wildcardok Szemantikus: az objektumok jelentését is számításba vevő módszerek Az utasítások hatásai/mellékhatásai
Szintaxis-alapú módszerek Pro Gyors (gyakorlatilag mintaillesztés) Néhány problémára remek megoldást jelentenek Con Sok probléma egyáltalán nem oldható meg így Tipikus felhasználás: Signatures: packer entrypoint, FLIRT, anti-virus Egyszerű deobfuszkáció
Szintaxis-alapú deobfuszkáció Amennyiben a transzformáció valamilyen felismerhető mintázatot tartalmaz Emulált jmp és call: push/ret
Szintaxis-alapú deobfuszkáció II A korábbi szemét kód példa:
Korlátok Mi történne, ha az előző példa tartalmazna egy aritmetikai utasítást amire nem készültünk fel? egy feltételes ugrást? egy átlátszatlan feltételt? A szintaxis-alapú módszerek hatékonyak bizonyos esetekben, de gyorsan korlátokba ütközünk
Szemantikus módszerek Az objektumok jelentését is számításba vevő módszerek Számos felhasználás a reverse engineering területén Automatikus kulcsgenerátor generálás :) Automatikus hibafelderítés (részben) generikus deobfuszkáció A program analízis akadémiai eredményeinek gyakorlatba való átültetése népszerű téma
Egy utasítás szemantikája... Az and (x86) utasítás köztes reprezentációja (IR - Intermediate Language translation)
Absztrakt interpretáció A program analízis klasszikus technikája Bonyolult módszer, de alapja egyszerű Absztrakt állapot a konkrét helyett Absztrakt szemantika a konkrét helyett Egyszerű példa: váltózok előjelének meghatározása A változók konkrét értéke helyett csak előjelüket tartjuk nyilván Ennek megfelelően definiáljuk az egyes műveletek szemantikáját is
Absztrakt interpretáció példa Minden más információt figyelmen kívül hagyunk
Absztrakt interpretáció Gyakran arra vagyunk kíváncsiak, hogy egy adott pont a programban egy adott változó milyen értékeinél érhető el Erre a kérdésre ad választ a szimbolikus végrehajtás, Az absztrakt interpretáció speciális esete
Szimbolikus végrehajtás Minden elágazás korlátozza az értékkészletet valamilyen módon
SMT (Satisfiability Modulo Theories) solving Alapötlet: alakítsuk a kódot logikai formulákká, amelyeken aztán különböző tulajdonságokat bizonyíthatunk be illetve egyszerűsíthetjük Felhasználás: input generálás (input crafting) Milyen értéket kell az eax regiszternek tartalmaznia kezdetben, hogy a kód lefutása után értéke 0x12345678 legyen?
SMT solving A köztes reprezentációból készített SMT formula (Z3)
SMT solving II assert(t175d == 0x12345678); T175d eax végső értéke Sat => a formula kielégíthető A kimenet egy model, ami kielégíti a formulát Az utolsó sorban eax értéke
LLVM IR obfuszkátor Kryptonite Az összeadás utasítást lecseréli egy assemblyben megvalósított bitenkénti összeadóra Egy összeadásból 2000 sor assembly kód lesz Junk code insertion extrém esete Deobfuszkáció egy lehetséges megoldása: Szimbolikus végrehajtással meghatározzuk, hogy a függvény kimenete hogy függ a bemenetektől (a két összeadandó szám) Egy SMT megoldóval megpróbáljuk egyszerűsíteni az előző lépés végén kapott formulát Ha minden jól megy megkapjuk, hogy a kimenet minden esetben a két bement összege
Kryptonite I (szimbolikus végrehajtás) A szimbolikus végrehajtó motor működése: Szimbolikus változók tárolása (gyk. ha inicializálatlan területről találunk olvasást, akkor létrehozunk egy változót). Ezek a változók lesznek a bemenetek. Az egyes utasítások emulálása, az állapotok frissítése Az obfuszkátor sajátosságai miatt viszonylag egyszerű
Kryptonite II (SMT solving) A függvény végén kapott formula még az SMT solverrel való egyszerűsítés után sem túl barátságos
Kryptonite II (SMT solving) Miért? Azonban Az SMT solver simplify funkciója többnyire szintaktikus szabályok alapján dolgozik, túl bonyolult a kifejezés prove(eax == Sum(sym.sym_variables)) True Azaz: valóban összeadást végez a függvény De: ha ezt nem tudtuk volna előre, hogy jöttünk volna rá hogy mit kérdezzünk a solvertől? Nyitott kérdés
OptiCode Deobfuszkációs megoldás Opaque predicates Junk code LLVM köztes reprezentáció Z3 SMT solver Részben manuális megoldás
OptiCode (junk code) A különböző fordítók optimalizáló és kódgeneráló részei folyamatosan fejlődnek. Kiválóan alkalmasak redundáns kódok eltávolítására Szintaktikus és szemantikus módszerek Az obfuszkált kódot a fordító köztes reprezentációjára konvertálva lefuttathatjuk rajta az optimalizációs lépéseket A kimenetet újra lefordítva tisztább kódot kapunk
OptiCode (opaque predicates) Dinamikus analízis Átlátszatlan feltételek kiszűrése Részben automatizált megoldás Az analízist végző plusz információkkal segíti az eszköz működését, illetve ellenőrzi a feltételek kiválasztását LLVM IR-ből az egyes feltételek SMT formuláinak meghatározása Z3 SMT solverrel a formulák vizsgálata Amennyiben a feltétel mindig igaz vagy hamis, csak a megfelelő ág követése
Összefoglalás - deobfuszkáció A szemantikus módszerek egyre nagyobb teret nyernek Aktív kutatási terület (illetve a kutatási eredmények gyakorlati felhasználása lendületesen halad) Tökéletes védelem nem létezik De reális cél a visszafejtés költségeit jelentősen növelni Kevés a jól használható és ingyenes eszköz Sok nyitott kérdés
Kérdések? Köszönöm a figyelmet!
Felhasznált források http://www.wtbw.co.uk/semantics-based-methods.pdf http://doar-e.github.io/blog/2013/09/16/breaking-kryptonites-obfuscation-with-symbolic-exec Binary-Code Obfuscations in Prevalent Packer Tools by KEVIN A. ROUNDY and BARTON P. MILLER http://www.inf.u-szeged.hu/~akiss/pub/pdf/laszlo_obfuscating.pdf http://blog.sei.cmu.edu/post.cfm/semantic-code-analysis-for-malware-code-deobfuscation http://www.data.proidea.org.pl/confidence/11edycja/nguyen_anh_quynh.pdf