Analýza podozrivého súboru "Outstanding Payment.jar" - 1. časť

Je antivírus stopercentnou ochranou proti škodlivým súborom? Aké techniky používajú autori malwaru na to, aby sa vyhli detekcii? Séria článkov popisuje náš postup pri statickej analýze podozrivého Java súboru a odhaľuje zaujímavé zistenia o jeho štruktúre aj o samotnom procese analýzy.

Pred nedávnom sa nám na stôl dostal podozrivý súbor „Outstanding Payment.jar“. Niektoré antivíry tento súbor označili za malware, zaujímali nás ale ďalšie podrobnosti. Predovšetkým nám išlo o to zistiť, aké konkrétne aktivity tento malware na infikovanom stroji vykonáva, a prípadne identifikovať protistrany, s ktorými komunikuje.

Analýza súboru podľa Virus Total
Analýza súboru podľa Virus Total

Po extrahovaní archívu bolo podľa názvov jednotlivých súborov na prvý pohľad jasné, že ide o obfuskovanú Java aplikáciu. Archív obsahoval:

  • triedy p5569.classp5595.class,
  • 3 binárne súbory:
    • n720175702
    • n1436460589
    • n1139827387

Prvým krokom bola prirodzene dekompilácia, pri ktorej poslúžil program Java Decompiler (https://github.com/java-decompiler/jd-gui).

Java Decompiler
Java Decompiler

Názvy metód a premenných boli samozrejme tiež obfuskované, vďaka čomu bol kód aplikácie nečitateľný.

Layer I – Strings Obfuscation

Jednou z prvých vecí na ktoré sa v takejto chvíli sústredím sú textové reťazce. Ukázalo sa, že v aplikácii existuje množstvo krátkych metód, ktoré po krátkom aritmetickom výpočte vracajú pravdepodobne dekódovaný reťazec:

public static String m481768778533406()
{
    return new String(new BigInteger("4jeg9121941i687a1gbe73i8j4", 20)
        .add(BigInteger.valueOf(685346116L))
        .divide(BigInteger.valueOf(185L))
        .divide(BigInteger.valueOf(255L))
        .divide(BigInteger.valueOf(252L)).toByteArray());
}
public static String m481768780200993()
{
    return new String(new BigInteger("-29357j83g", 25).divide(BigInteger.valueOf(-393L))
        .subtract(BigInteger.valueOf(-1309792666L))
        .subtract(BigInteger.valueOf(-352014025L))
        .subtract(BigInteger.valueOf(197930807L))
        .subtract(BigInteger.valueOf(495429948L)).toByteArray());
}

Keďže každá metóda používala odlišný spôsob výpočtu (iná množina a poradie operácií), nebolo možné vytvoriť jednotný dekódovací algoritmus. Na druhej strane, zdrojový kód každej dekódujúcej metódy bol k dispozícii, preto stačilo všetky tieto metódy pozbierať, napísať Java kód na ich zavolanie, skompilovať a výsledné reťazce uložiť.

Na tento účel som vytvoril krátky Python skript, ktorý prešiel všetky zdrojové .java súbory a vyhľadal metódy (názov a návratový výraz) podľa jednoduchého regulárneho výrazu:

/String (.*?)\(\)[\{\n\s]*.*?return (new String.*?);/

Z pozbieraných metód potom vygeneroval Java program StringDecoder.java, ktorý pri spustení dekódoval všetky reťazce a vyprodukoval výstup vo formáte „METHOD_NAME [TAB] DECODED_STRING“. Keďže celkový počet reťazcov bol vyše tisíc, narazil som na obmedzenie maximálnej dĺžky metódy v Jave, a musel som preto rozdeliť dekódovacie volania do viacerých pomocných blokov (metódy m0-m11).

import java.math.BigInteger;
public class StringDecoder {
public static void main(String[] args) {
    m0();
    m1();
    m2();
    
}
public static void m0() {
    System.out.println("m481768246224778	" + 
        new String(new BigInteger("61645375321174103340046020127427", 8)
        .divide(BigInteger.valueOf(463L)).toByteArray()));

    System.out.println("m481768254833617	" + 
        new String(new BigInteger("19ii868571al8ad74754hl529m3h", 24)
        .subtract(BigInteger.valueOf(981117809L))
        .divide(BigInteger.valueOf(-83L))
        .divide(BigInteger.valueOf(456L))
        .divide(BigInteger.valueOf(-75L)).toByteArray()));

    System.out.println("m481769630835738	" + 
        new String(new BigInteger("-dbf7k67bbb4i0", 21)
        .divide(BigInteger.valueOf(271L))
        .divide(BigInteger.valueOf(-9L))
        .divide(BigInteger.valueOf(322L))
        .divide(BigInteger.valueOf(43L))
        .subtract(BigInteger.valueOf(1066591023L)).toByteArray()));

Vygenerovaný program na dekódovanie reťazcov

Väčšina dekódovaných reťazcov obsahovala názvy tried a metód, dalo sa teda predpokladať, že narazíme na množstvo reflexívnych volaní. Okrem iného išlo aj o kryptografické metódy naznačujúce šifrovanie:

m481768246224778	n1436460589
m481768254833617	setAccessible
m481769630835738	push
m481769631469437	javax.crypto.spec.SecretKeySpec
m481769632835956	f481768237511843
m481769634291287	java.lang.reflect.Array
m481769635523836	java.lang.Object

V neposlednom rade zoznam reťazcov obsahoval riadok:

m481768664717772	?boot-main-class:operational.Jrat

Už v tomto kroku teda bolo jasné (alebo aspoň veľmi pravdepodobné), že ide o variantu Java malwaru jRAT (Remote Administration Tool written in Java).

Keďže názvy dekódovacích metód boli unikátne naprieč celou aplikáciu, pomocou ďalšieho krátkeho Python skriptu bolo možné nahradiť ich volania za dekódované reťazce. To ale nepomohlo z hľadiska analýzy funkcionality – tok programu (control flow) nebol viditeľný najmä kvôli množstvu krátkych niekoľko riadkových metód s nezmyselnými menami.

Layer I – Control Flow Obfuscation

Ďalším krokom bolo teda rozbalenie volaní krátkych metód (proces ktorý bežne vykonáva prekladač pri volaniach inline funkcií). Výhodou bol fakt, že všetky metódy boli statické, opäť teda stačilo použiť niekoľko regulárnych výrazov na extrakciu definícií a nahradenie volaní priamo telom funkcie.

Ukázalo sa, že celé jadro aplikácie je tvorené stavovým automatom:

while(condition)
{
    switch (((Integer)p5595.f481768237511843).intValue()) {
        case 4035: 
            java.util.LinkedList.getDeclaredMethod("push", new Class[] { java.lang.Object })
                .invoke(p5595.f481768237438446, new Object[] { Integer.valueOf(4089) });
            p5595.f481768237511843 = Integer.valueOf(5979);
            break;
        case 5857: 
            ((java.lang.reflect.Method)p5595.f481768237798877)
                .invoke(p5578.f481768476627625, new Object[] { p5587.f481768598564702 });
            p5595.f481768237511843 = Integer.valueOf(5860);
            break;
        case 5424: 
            if (((Boolean)p5584.f481768548009292).booleanValue()) {
                p5595.f481768237511843 = Integer.valueOf(5465);
            }
            else {
                p5595.f481768237511843 = Integer.valueOf(5794);
            }
            break;
...
}
Riadiaci stavový automat

Na začiatku programu sa stavová premenná nastavila na počiatočnú hodnotu, a potom v každej iterácii cyklu došlo k vykonaniu jedného kroku výpočtu a nastaveniu novej hodnoty stavovej premennej.

Zrejme teda došlo k transformácii pôvodne sekvenčného kódu, a to tak, že jednotlivým príkazom (alebo krátkym blokom kódu) boli priradené unikátne stavy, a ich poradie bolo následne poprehadzované:

Control Flow Transformation
Transformácia toku riadenia

Túto techniku bežne používajú niektoré automatizované obfuskátory a pri pohľade na takýto kód je neľahké, až prakticky nemožné sledovať tok programu (postupnosť krokov, vetvenia, cykly...).

Navyše, v tomto prípade tok programu nebol riadený iba jednou stavovou premennou. Okrem nej bol použitý zásobník, do ktorého sa ukladali a neskôr vyberali budúce stavy, manuálna analýza po krokoch by teda bola veľmi náročná.

Našťastie bolo možné operácie, ktoré upravovali stav automatu, izolovať. Išlo konkrétne o 3 typy operácií:

  • priradenie číselnej hodnoty do stavovej premennej (p5595.f481768237511843 = ...),
  • vloženie novej hodnoty do zásobníka (java.util.LinkedList.getDeclaredMethod(„push“, ...)),
  • odobratie hodnoty z vrcholu zásobníka (java.util.LinkedList.getDeclaredMethod(„pop“, ...).

Stačilo teda opäť napísať krátky Python skript, ktorý celý automat načítal, a postupne interpretoval operácie upravujúce aktuálny stav, pričom ostatné príkazy v jednotlivých stavoch vypisoval na výstup. Výsledkom bol už oveľa čitateľnejší sekvenčný kód, vhodný na manuálnu analýzu:

p5582.f481771201719702 = java.lang.System.out;
p5574.f481768415949647 = java.lang.Class.forName(new Object[] { "p5595"; })
p5572.f481768374256940 = /*java.lang.Class*/p5574.f481768415949647.getProtectionDomain(new Object[0])
p5576.f481768456904606 = /*java.lang.Class*/p5574.f481768415949647.getClassLoader(new Object[0])
p5570.f481768327573006 = java.lang.String.getDeclaredMethod("getBytes", new Class[0]).invoke("2173853979931072";, new Object[0]);
p5576.f481768456850592 = java.io.ByteArrayOutputStream.getConstructor(new Class[0]).newInstance(new Object[0]);
p5589.f481768623886303 = "n720175702";;
p5592.f481768700379315 = Integer.valueOf(303016);;
p5595.f481768237942623 = Integer.valueOf(1);;
p5594.f481768739066596 = /*java.lang.ClassLoader*/p5576.f481768456904606.getResourceAsStream(new Object[] { p5589.f481768623886303 })
p5572.f481768374193814 = java.io.DataInputStream.getConstructor(new Class[] { java.io.InputStream }).newInstance(new Object[] { p5594.f481768739066596 });
/*java.io.DataInputStream*/p5572.f481768374193814.skipBytes(new Object[] { p5592.f481768700379315 })
p5581.f481768521696645 = p5572.f481768374193814;
p5573.f481768391322862 = javax.crypto.Cipher.DECRYPT_MODE;
...

Layer I – Resources Encryption

Ukázalo sa, že aplikácia načíta svoje 3 zdroje, a postupne z nich sofistikovaným spôsobom dekóduje súbory. Každý výsledný súbor je zložený z niekoľkých častí, ktoré sú rozprestreté medzi 3 zdrojmi – na rôznych pozíciách, s rôznou veľkosťou – a zašifrované algoritmom AES. Jednotlivé fragmenty sú definované pomocou reťazcov vo formáte: „size:offset:resId;size:offset:resId...“.

File Fragments
Fragmenty zašifrovaných súborov

Nazačiatku sú dekódované 3 súbory:

Loader$Handler.class:1:303016:n720175702;1:266016:n1139827387;988:266208:n1139827387
Loader$Connection.class:1:303179:n720175702;1:303340:n720175702;1:22210:n1436460589;1:22377:n1436460589;1425:22747:n1436460589
Loader.class:1:303502:n720175702;1:23652:n1436460589;1:23813:n1436460589;1:266852:n1139827387;1:303663:n720175702;6291:24591:n143646058

Trieda Loader je potom použitá na dekódovanie ďalších dát, pričom najprv je načítaná a dekódovaná mapa súborov (serializovaný Java objekt typu java.util.Map<String, String>):

1:300918:n720175702;1:301078:n720175702;8223:301462:n720175702

Po dekódovaní je možné ju načítať v Jave pomocou triedy ObjectInputStream:

Map<String, String> addresses = ((Map)new ObjectInputStream(new ByteArrayInputStream(decodedBytes)).readObject());

Táto mapa obsahuje popisy fragmentov (size:offset:resId;size:offset:resId...) a názvy súborov a trieda Loader ich všetky postupne načíta a dešifruje. AES kľúč je natvrdo uložený v kóde aplikácie a je rovnaký pre všetky dekódované súbory.

Úspešné dekódovanie a spojenie všetkých fragmentov odhalilo celý rad nových súborov:

Decoded Files
Dekódované súbory

Bohužiaľ, pravdepodobne ide o ďalšiu vrstvu obfuskácie – je vidieť množstvo tried s nezmyselnými názvami.

Každopádne, v tejto fáze sa mi podarilo rozlúsknuť prvú vrstvu malwaru. Jej hlavnou úlohou bolo dopraviť škodlivý kód na cieľovú stanicu a vyhnúť sa detekcii.

Samotný škodlivý kód bol zašifrovaný a rozkúskovaný do 3 binárnych súborov, vďaka čomu antivírus nebol schopný rozpoznať akúkoľvek signatúru.

Kód tvoriaci obálku využil na maskovanie samého seba 3 hlavné techniky:

  • reflexívne volania
    • java.lang.String.getDeclaredMethod("getBytes", new Class[0]).invoke("2173853979931072", new Object[0]);
  • obfuskáciu textových reťazcov
  • transformáciu toku programu (control flow)

Analýze súborov, ktoré sa mi podarilo získať sa budem venovať v ďalšom pokračovaní tohto článku.